) {
+ let mut builder = Request::builder().method(Method::GET).uri(uri);
+ if let Some(cookie) = cookie {
+ builder = builder.header(header::COOKIE, cookie);
+ }
+ let request = builder.body(Body::empty()).expect("request should build");
+ let response = app.oneshot(request).await.expect("request should run");
+ let status = response.status();
+ let headers = response.headers().clone();
+ let bytes = to_bytes(response.into_body(), usize::MAX)
+ .await
+ .expect("body should read");
+ (status, headers, bytes.to_vec())
+}
+
+fn gunzip_utf8(bytes: &[u8]) -> String {
+ let mut decoder = GzDecoder::new(bytes);
+ let mut output = String::new();
+ decoder
+ .read_to_string(&mut output)
+ .expect("gzip body should decompress");
+ output
+}
+
async fn seed_run_with_job_logs(
pool: &PgPool,
owner: &User,
@@ -781,6 +811,28 @@ async fn assigned_runner_appends_finalizes_and_streams_job_logs() {
assert!(stream_body.contains("run cargo test"));
assert!(stream_body.contains("event: cursor"));
+ let download_uri = format!(
+ "/api/repos/{}/{}/actions/jobs/{}/logs/download",
+ owner.email, repo_name, job_id
+ );
+ let (download_status, download_headers, download_body) =
+ get_bytes_with_headers(app.clone(), &download_uri, Some(&owner_cookie)).await;
+ assert_eq!(download_status, StatusCode::OK);
+ assert_eq!(
+ download_headers
+ .get(header::CONTENT_TYPE)
+ .and_then(|value| value.to_str().ok()),
+ Some("application/gzip")
+ );
+ assert!(download_headers
+ .get(header::CONTENT_DISPOSITION)
+ .and_then(|value| value.to_str().ok())
+ .unwrap_or_default()
+ .contains(".log.gz"));
+ let decompressed_download = gunzip_utf8(&download_body);
+ assert!(decompressed_download.contains("boot runner"));
+ assert!(decompressed_download.contains("run cargo test"));
+
let row = sqlx::query(
r#"
SELECT job_logs.bytes_written,
diff --git a/crates/api/tests/owner_packages_contract.rs b/crates/api/tests/owner_packages_contract.rs
index e8fd603d..1a1786c0 100644
--- a/crates/api/tests/owner_packages_contract.rs
+++ b/crates/api/tests/owner_packages_contract.rs
@@ -386,8 +386,13 @@ async fn organization_packages_show_internal_to_members_only() {
)
.await;
- let (status, _, public_body) =
- get_json(app.clone(), &format!("/api/orgs/{marker}/packages"), None, &client_ip).await;
+ let (status, _, public_body) = get_json(
+ app.clone(),
+ &format!("/api/orgs/{marker}/packages"),
+ None,
+ &client_ip,
+ )
+ .await;
assert_eq!(status, StatusCode::OK, "{public_body}");
assert_eq!(public_body["total"], 0);
diff --git a/crates/api/tests/projects_workspace_contract.rs b/crates/api/tests/projects_workspace_contract.rs
index 10c1e168..85b5952a 100644
--- a/crates/api/tests/projects_workspace_contract.rs
+++ b/crates/api/tests/projects_workspace_contract.rs
@@ -2538,7 +2538,10 @@ async fn project_settings_mutations_enforce_permissions_policy_and_audit_events(
)
.await;
assert_eq!(status, StatusCode::OK, "{body}");
- assert!(body["teamGrants"].as_array().expect("team grants").is_empty());
+ assert!(body["teamGrants"]
+ .as_array()
+ .expect("team grants")
+ .is_empty());
let (status, _, body) = delete_json_body(
app,
diff --git a/crates/api/tests/repository_network_contract.rs b/crates/api/tests/repository_network_contract.rs
index 510d9706..2043f6cc 100644
--- a/crates/api/tests/repository_network_contract.rs
+++ b/crates/api/tests/repository_network_contract.rs
@@ -116,9 +116,10 @@ async fn cookie_header(pool: &PgPool, config: &AppConfig, user: &User) -> String
}
async fn get_json(app: axum::Router, uri: &str, cookie: Option<&str>) -> (StatusCode, Value) {
- let mut builder = Request::builder()
- .uri(uri)
- .header("x-forwarded-for", format!("198.51.100.{}", 10 + uri.len() % 100));
+ let mut builder = Request::builder().uri(uri).header(
+ "x-forwarded-for",
+ format!("198.51.100.{}", 10 + uri.len() % 100),
+ );
if let Some(cookie) = cookie {
builder = builder.header(header::COOKIE, cookie);
}
diff --git a/web/src/app/[owner]/[repo]/actions/jobs/[jobId]/logs/download/route.ts b/web/src/app/[owner]/[repo]/actions/jobs/[jobId]/logs/download/route.ts
index de774ee4..5c34fdb7 100644
--- a/web/src/app/[owner]/[repo]/actions/jobs/[jobId]/logs/download/route.ts
+++ b/web/src/app/[owner]/[repo]/actions/jobs/[jobId]/logs/download/route.ts
@@ -25,14 +25,18 @@ export async function GET(request: Request, { params }: RouteContext) {
const headers = new Headers();
headers.set(
"content-type",
- response.headers.get("content-type") ?? "text/plain; charset=utf-8",
+ response.headers.get("content-type") ?? "application/gzip",
);
const disposition = response.headers.get("content-disposition");
if (disposition) {
headers.set("content-disposition", disposition);
}
+ const encoding = response.headers.get("content-encoding");
+ if (encoding) {
+ headers.set("content-encoding", encoding);
+ }
- return new Response(await response.text(), {
+ return new Response(await response.arrayBuffer(), {
status: response.status,
headers,
});
diff --git a/web/src/components/RepositoryActionsJobLogPage.tsx b/web/src/components/RepositoryActionsJobLogPage.tsx
index 583da554..c00f4a69 100644
--- a/web/src/components/RepositoryActionsJobLogPage.tsx
+++ b/web/src/components/RepositoryActionsJobLogPage.tsx
@@ -248,11 +248,9 @@ export function RepositoryActionsJobLogPage({
return queryString ? `${jobPath}?${queryString}` : jobPath;
}
- function goToMatch(nextMatch: number) {
- if (!detail.search.query || detail.search.totalMatches < 1) {
- return;
- }
- router.push(searchHref(detail.search.query, nextMatch));
+ function navigateTo(href: string) {
+ window.history.pushState(null, "", href);
+ router.push(href);
}
async function copyPermalink(anchor?: string) {
@@ -300,8 +298,7 @@ export function RepositoryActionsJobLogPage({
raw: merged.rawLogs,
});
setOptionMessage("Saved log options");
- setOptionsOpen(false);
- router.push(href);
+ navigateTo(href);
router.refresh();
} catch {
setOptionMessage("Could not save log options");
@@ -477,7 +474,7 @@ export function RepositoryActionsJobLogPage({
className="btn primary"
href={jobDownloadHref(basePath, detail.job.id)}
>
- Download log
+ Download gzip
>
) : (
@@ -486,13 +483,13 @@ export function RepositoryActionsJobLogPage({
Download run archive
>
)}
- {optionMessage ? (
+ {optionMessage && !optionsOpen ? (
{optionMessage}
@@ -574,29 +571,48 @@ export function RepositoryActionsJobLogPage({
Steps and output