Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ pulldown-cmark = "0.13"
ammonia = "4"
regex = "1"
dotenvy = "0.15"
flate2 = "1"
1 change: 1 addition & 0 deletions crates/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ pulldown-cmark.workspace = true
ammonia.workspace = true
regex.workspace = true
dotenvy.workspace = true
flate2.workspace = true
10 changes: 5 additions & 5 deletions crates/api/src/domain/milestones.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,11 @@ pub async fn repository_milestone_detail_for_actor_by_owner_name(
let row = sqlx::query(&milestone_select_sql(
"milestones.repository_id = $1 AND milestones.id = $2",
))
.bind(repository.id)
.bind(milestone_id)
.fetch_optional(pool)
.await?
.ok_or(MilestonesError::MilestoneNotFound)?;
.bind(repository.id)
.bind(milestone_id)
.fetch_optional(pool)
.await?
.ok_or(MilestonesError::MilestoneNotFound)?;
let summary = summary_from_row(row, &repository)?;
let description_html = render_markdown(
Some(pool),
Expand Down
4 changes: 1 addition & 3 deletions crates/api/src/domain/organizations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1524,9 +1524,7 @@ fn patch_text_required(
Ok(normalized)
}

fn deserialize_patch_value<'de, D>(
deserializer: D,
) -> Result<Option<serde_json::Value>, D::Error>
fn deserialize_patch_value<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
Expand Down
14 changes: 11 additions & 3 deletions crates/api/src/domain/packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,21 @@ pub struct PackageCapabilitySummary {
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", tag = "action")]
pub enum PackageSettingsMutation {
UpdateVisibility { visibility: String },
GrantAccess { username: String, role: String },
UpdateVisibility {
visibility: String,
},
GrantAccess {
username: String,
role: String,
},
RevokeAccess {
#[serde(rename = "userId")]
user_id: Uuid,
},
LinkRepository { owner: String, repo: String },
LinkRepository {
owner: String,
repo: String,
},
UnlinkRepository {
#[serde(rename = "repositoryId")]
repository_id: Uuid,
Expand Down
16 changes: 13 additions & 3 deletions crates/api/src/routes/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ use axum::{
routing::{delete, get, patch, post},
Json, Router,
};
use flate2::{write::GzEncoder, Compression};
use serde::Deserialize;
use serde_json::json;
use sqlx::Row;
use std::collections::HashMap;
use std::io::Write;
use uuid::Uuid;

use crate::{
Expand Down Expand Up @@ -1091,15 +1093,23 @@ async fn download_workflow_job_logs_route(

Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.header(header::CONTENT_TYPE, "application/gzip")
.header(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{filename}\""),
format!("attachment; filename=\"{filename}.gz\""),
)
.body(Body::from(body))
.body(Body::from(gzip_bytes(body)?))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep log-download tests in sync with gzip output

This now returns gzipped bytes from /api/repos/.../actions/jobs/.../logs/download for every caller, but the existing crates/api/tests/api_repository_actions_run_detail_contract.rs contract still calls get_text(...) on that endpoint and asserts a plain log substring. When that test runs, String::from_utf8 will receive gzip bytes and fail before the assertion, so the Rust test suite is broken unless that remaining consumer is converted to gunzip (or this route stays text/plain).

Useful? React with 👍 / 👎.

.map_err(|_| database_unavailable())
}

fn gzip_bytes(body: String) -> Result<Vec<u8>, (StatusCode, Json<ErrorEnvelope>)> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder
.write_all(body.as_bytes())
.map_err(|_| database_unavailable())?;
encoder.finish().map_err(|_| database_unavailable())
}

async fn download_workflow_run_log_archive_route(
State(state): State<AppState>,
headers: HeaderMap,
Expand Down
26 changes: 8 additions & 18 deletions crates/api/src/routes/repositories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4023,15 +4023,10 @@ async fn create_release(
) -> Result<(StatusCode, Json<serde_json::Value>), (StatusCode, Json<ErrorEnvelope>)> {
let pool = state.db.as_ref().ok_or_else(database_unavailable)?;
let actor = AuthenticatedUser::from_headers(&state, &headers).await?.0;
let release = create_repository_release_by_owner_name(
pool,
&owner,
&repo,
Some(actor.id),
request,
)
.await
.map_err(map_releases_error)?;
let release =
create_repository_release_by_owner_name(pool, &owner, &repo, Some(actor.id), request)
.await
.map_err(map_releases_error)?;

Ok((StatusCode::CREATED, Json(json!(release))))
}
Expand Down Expand Up @@ -4210,15 +4205,10 @@ async fn publish_release(
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorEnvelope>)> {
let pool = state.db.as_ref().ok_or_else(database_unavailable)?;
let actor = AuthenticatedUser::from_headers(&state, &headers).await?.0;
let release = publish_repository_release_by_owner_name(
pool,
&owner,
&repo,
release_id,
Some(actor.id),
)
.await
.map_err(map_releases_error)?;
let release =
publish_repository_release_by_owner_name(pool, &owner, &repo, release_id, Some(actor.id))
.await
.map_err(map_releases_error)?;

Ok(Json(json!(release)))
}
Expand Down
54 changes: 53 additions & 1 deletion crates/api/tests/api_repository_actions_job_log_contract.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use axum::{
body::{to_bytes, Body},
http::{header, Method, Request, StatusCode},
http::{header, HeaderMap, Method, Request, StatusCode},
};
use chrono::{Duration, Utc};
use flate2::read::GzDecoder;
use opengithub_api::{
auth::session,
config::{AppConfig, AuthConfig},
Expand All @@ -21,6 +22,7 @@ use opengithub_api::{
};
use serde_json::{json, Value};
use sqlx::{PgPool, Row};
use std::io::Read;
use tower::ServiceExt;
use url::Url;
use uuid::Uuid;
Expand Down Expand Up @@ -206,6 +208,34 @@ async fn get_text_with_content_type(
)
}

async fn get_bytes_with_headers(
app: axum::Router,
uri: &str,
cookie: Option<&str>,
) -> (StatusCode, HeaderMap, Vec<u8>) {
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,
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions crates/api/tests/owner_packages_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
5 changes: 4 additions & 1 deletion crates/api/tests/projects_workspace_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions crates/api/tests/repository_network_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
Loading