From a325520a52f6baa2ef118ceda2f65a410c9b6518 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 4 Jul 2025 15:35:35 +0200 Subject: [PATCH 1/7] database: Add `trustpub_data` columns to the `trustpub_tokens` and `versions` tables --- crates/crates_io_database/Cargo.toml | 2 +- crates/crates_io_database/src/models/mod.rs | 1 + .../src/models/trustpub/data.rs | 60 +++++++++++++++++++ .../src/models/trustpub/mod.rs | 2 + .../src/models/trustpub/token.rs | 2 + .../crates_io_database/src/models/version.rs | 4 +- crates/crates_io_database/src/schema.rs | 4 ++ .../crates_io_database_dump/src/dump-db.toml | 3 + .../down.sql | 5 ++ .../up.sql | 7 +++ .../trustpub/tokens/exchange/mod.rs | 1 + .../trustpub/tokens/revoke/tests.rs | 1 + src/tests/github_secret_scanning.rs | 1 + src/tests/krate/publish/trustpub.rs | 1 + src/worker/jobs/trustpub/delete_tokens.rs | 2 + 15 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 crates/crates_io_database/src/models/trustpub/data.rs create mode 100644 migrations/2025-07-04-102806_add_trustpub_data_columns/down.sql create mode 100644 migrations/2025-07-04-102806_add_trustpub_data_columns/up.sql diff --git a/crates/crates_io_database/Cargo.toml b/crates/crates_io_database/Cargo.toml index 1647914aad5..d1f6723bd82 100644 --- a/crates/crates_io_database/Cargo.toml +++ b/crates/crates_io_database/Cargo.toml @@ -31,5 +31,5 @@ utoipa = { version = "=5.4.0", features = ["chrono"] } claims = "=0.8.0" crates_io_test_db = { path = "../crates_io_test_db" } googletest = "=0.14.2" -insta = "=1.43.1" +insta = { version = "=1.43.1", features = ["json"] } tokio = { version = "=1.46.1", features = ["macros", "rt"] } diff --git a/crates/crates_io_database/src/models/mod.rs b/crates/crates_io_database/src/models/mod.rs index e04985b9885..25c7726ecfa 100644 --- a/crates/crates_io_database/src/models/mod.rs +++ b/crates/crates_io_database/src/models/mod.rs @@ -14,6 +14,7 @@ pub use self::krate::{Crate, CrateName, NewCrate, RecentCrateDownloads}; pub use self::owner::{CrateOwner, Owner, OwnerKind}; pub use self::team::{NewTeam, Team}; pub use self::token::ApiToken; +pub use self::trustpub::TrustpubData; pub use self::user::{NewUser, User}; pub use self::version::{NewVersion, TopVersions, Version}; diff --git a/crates/crates_io_database/src/models/trustpub/data.rs b/crates/crates_io_database/src/models/trustpub/data.rs new file mode 100644 index 00000000000..9b35ece8dc0 --- /dev/null +++ b/crates/crates_io_database/src/models/trustpub/data.rs @@ -0,0 +1,60 @@ +use diesel::deserialize::{self, FromSql}; +use diesel::pg::{Pg, PgValue}; +use diesel::serialize::{self, Output, ToSql}; +use diesel::sql_types::Jsonb; +use diesel::{AsExpression, FromSqlRow}; +use serde::{Deserialize, Serialize}; + +/// Data structure containing trusted publisher information extracted from JWT claims +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromSqlRow, AsExpression)] +#[diesel(sql_type = Jsonb)] +#[serde(tag = "provider")] +pub enum TrustpubData { + #[serde(rename = "github")] + GitHub { + /// Repository (e.g. "octo-org/octo-repo") + repository: String, + /// Workflow run ID + run_id: String, + /// SHA of the commit + sha: String, + }, +} + +impl ToSql for TrustpubData { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { + let json = serde_json::to_value(self)?; + >::to_sql(&json, &mut out.reborrow()) + } +} + +impl FromSql for TrustpubData { + fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { + let json = >::from_sql(bytes)?; + Ok(serde_json::from_value(json)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::assert_json_snapshot; + + #[test] + fn test_serialization() { + let data = TrustpubData::GitHub { + repository: "octo-org/octo-repo".to_string(), + run_id: "example-run-id".to_string(), + sha: "example-sha".to_string(), + }; + + assert_json_snapshot!(data, @r#" + { + "provider": "github", + "repository": "octo-org/octo-repo", + "run_id": "example-run-id", + "sha": "example-sha" + } + "#); + } +} diff --git a/crates/crates_io_database/src/models/trustpub/mod.rs b/crates/crates_io_database/src/models/trustpub/mod.rs index 6a2ad6357b4..5452b368d08 100644 --- a/crates/crates_io_database/src/models/trustpub/mod.rs +++ b/crates/crates_io_database/src/models/trustpub/mod.rs @@ -1,7 +1,9 @@ +mod data; mod github_config; mod token; mod used_jti; +pub use self::data::TrustpubData; pub use self::github_config::{GitHubConfig, NewGitHubConfig}; pub use self::token::NewToken; pub use self::used_jti::NewUsedJti; diff --git a/crates/crates_io_database/src/models/trustpub/token.rs b/crates/crates_io_database/src/models/trustpub/token.rs index 80e6fcf5c84..6d29224aecf 100644 --- a/crates/crates_io_database/src/models/trustpub/token.rs +++ b/crates/crates_io_database/src/models/trustpub/token.rs @@ -1,3 +1,4 @@ +use crate::models::TrustpubData; use crate::schema::trustpub_tokens; use chrono::{DateTime, Utc}; use diesel::prelude::*; @@ -9,6 +10,7 @@ pub struct NewToken<'a> { pub expires_at: DateTime, pub hashed_token: &'a [u8], pub crate_ids: &'a [i32], + pub trustpub_data: Option<&'a TrustpubData>, } impl NewToken<'_> { diff --git a/crates/crates_io_database/src/models/version.rs b/crates/crates_io_database/src/models/version.rs index 8ca1a2edfa2..477e8f51d77 100644 --- a/crates/crates_io_database/src/models/version.rs +++ b/crates/crates_io_database/src/models/version.rs @@ -7,7 +7,7 @@ use diesel::prelude::*; use diesel_async::{AsyncPgConnection, RunQueryDsl}; use serde::Deserialize; -use crate::models::{Crate, User}; +use crate::models::{Crate, TrustpubData, User}; use crate::schema::{readme_renderings, users, versions}; // Queryable has a custom implementation below @@ -36,6 +36,7 @@ pub struct Version { pub homepage: Option, pub documentation: Option, pub repository: Option, + pub trustpub_data: Option, } impl Version { @@ -103,6 +104,7 @@ pub struct NewVersion<'a> { repository: Option<&'a str>, categories: Option<&'a [&'a str]>, keywords: Option<&'a [&'a str]>, + trustpub_data: Option<&'a TrustpubData>, } impl NewVersion<'_> { diff --git a/crates/crates_io_database/src/schema.rs b/crates/crates_io_database/src/schema.rs index 9d43048b2e1..e2f2f3cbea0 100644 --- a/crates/crates_io_database/src/schema.rs +++ b/crates/crates_io_database/src/schema.rs @@ -800,6 +800,8 @@ diesel::table! { hashed_token -> Bytea, /// Unique identifiers of the crates that can be published using this token crate_ids -> Array>, + /// JSONB data containing JWT claims from the trusted publisher (e.g., GitHub Actions context like repository, run_id, sha) + trustpub_data -> Nullable, } } @@ -1077,6 +1079,8 @@ diesel::table! { keywords -> Array>, /// JSONB representation of the version number for sorting purposes. semver_ord -> Nullable, + /// JSONB data containing JWT claims from the trusted publisher (e.g., GitHub Actions context like repository, run_id, sha) + trustpub_data -> Nullable, } } diff --git a/crates/crates_io_database_dump/src/dump-db.toml b/crates/crates_io_database_dump/src/dump-db.toml index c3c28ca558e..9394701a49c 100644 --- a/crates/crates_io_database_dump/src/dump-db.toml +++ b/crates/crates_io_database_dump/src/dump-db.toml @@ -206,6 +206,7 @@ created_at = "private" expires_at = "private" hashed_token = "private" crate_ids = "private" +trustpub_data = "private" [trustpub_used_jtis.columns] id = "private" @@ -280,6 +281,8 @@ documentation = "public" repository = "public" categories = "public" keywords = "public" +# The following column is private for now, until we can guarantee a stable data schema. +trustpub_data = "private" [versions_published_by.columns] version_id = "private" diff --git a/migrations/2025-07-04-102806_add_trustpub_data_columns/down.sql b/migrations/2025-07-04-102806_add_trustpub_data_columns/down.sql new file mode 100644 index 00000000000..3d555c3752d --- /dev/null +++ b/migrations/2025-07-04-102806_add_trustpub_data_columns/down.sql @@ -0,0 +1,5 @@ +-- Remove trustpub_data column from versions table +ALTER TABLE versions DROP COLUMN trustpub_data; + +-- Remove trustpub_data column from trustpub_tokens table +ALTER TABLE trustpub_tokens DROP COLUMN trustpub_data; diff --git a/migrations/2025-07-04-102806_add_trustpub_data_columns/up.sql b/migrations/2025-07-04-102806_add_trustpub_data_columns/up.sql new file mode 100644 index 00000000000..8613245bb47 --- /dev/null +++ b/migrations/2025-07-04-102806_add_trustpub_data_columns/up.sql @@ -0,0 +1,7 @@ +-- Add trustpub_data column to trustpub_tokens table +ALTER TABLE trustpub_tokens ADD COLUMN trustpub_data JSONB; +COMMENT ON COLUMN trustpub_tokens.trustpub_data IS 'JSONB data containing JWT claims from the trusted publisher (e.g., GitHub Actions context like repository, run_id, sha)'; + +-- Add trustpub_data column to versions table +ALTER TABLE versions ADD COLUMN trustpub_data JSONB; +COMMENT ON COLUMN versions.trustpub_data IS 'JSONB data containing JWT claims from the trusted publisher (e.g., GitHub Actions context like repository, run_id, sha)'; diff --git a/src/controllers/trustpub/tokens/exchange/mod.rs b/src/controllers/trustpub/tokens/exchange/mod.rs index 02398f9f037..7d5d5573495 100644 --- a/src/controllers/trustpub/tokens/exchange/mod.rs +++ b/src/controllers/trustpub/tokens/exchange/mod.rs @@ -134,6 +134,7 @@ pub async fn exchange_trustpub_token( expires_at: chrono::Utc::now() + chrono::Duration::minutes(30), hashed_token: &new_token.sha256(), crate_ids: &crate_ids, + trustpub_data: None, }; new_token_model.insert(conn).await?; diff --git a/src/controllers/trustpub/tokens/revoke/tests.rs b/src/controllers/trustpub/tokens/revoke/tests.rs index bee3c2017e5..c272ee23dc1 100644 --- a/src/controllers/trustpub/tokens/revoke/tests.rs +++ b/src/controllers/trustpub/tokens/revoke/tests.rs @@ -25,6 +25,7 @@ async fn new_token(conn: &mut AsyncPgConnection, crate_id: i32) -> QueryResult QueryResult Date: Fri, 4 Jul 2025 15:37:26 +0200 Subject: [PATCH 2/7] trustpub/exchange: Save JWT claims subset in the `trustpub_data` column --- .../crates_io_trustpub/src/github/claims.rs | 48 +++++++++++++++---- .../trustpub/tokens/exchange/mod.rs | 10 +++- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/crates/crates_io_trustpub/src/github/claims.rs b/crates/crates_io_trustpub/src/github/claims.rs index 225131f3621..f931080df1f 100644 --- a/crates/crates_io_trustpub/src/github/claims.rs +++ b/crates/crates_io_trustpub/src/github/claims.rs @@ -25,6 +25,8 @@ pub struct GitHubClaims { pub repository: String, pub workflow_ref: String, pub environment: Option, + pub run_id: String, + pub sha: String, } impl GitHubClaims { @@ -116,7 +118,9 @@ mod tests { "repository_owner_id": "65", "repository": "octo-org/octo-repo", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", - "environment": "prod" + "environment": "prod", + "run_id": "example-run-id", + "sha": "example-sha" } "#); @@ -132,6 +136,8 @@ mod tests { "aud": AUDIENCE, "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "exp": now + 30, @@ -148,7 +154,9 @@ mod tests { "repository_owner_id": "65", "repository": "octo-org/octo-repo", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", - "environment": null + "environment": null, + "run_id": "example-run-id", + "sha": "example-sha" } "#); @@ -163,6 +171,8 @@ mod tests { "aud": AUDIENCE, "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "exp": now + 30, @@ -170,7 +180,7 @@ mod tests { }))?; let error = GitHubClaims::decode(&jwt, AUDIENCE, &DECODING_KEY).unwrap_err(); - assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `jti`", line: 1, column: 251)))"#); + assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `jti`", line: 1, column: 297)))"#); Ok(()) } @@ -184,6 +194,8 @@ mod tests { "aud": "somebody-else", "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "exp": now + 30, @@ -205,6 +217,8 @@ mod tests { "aud": [AUDIENCE, "somebody-else"], "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "exp": now + 30, @@ -224,6 +238,8 @@ mod tests { "jti": "example-id", "aud": AUDIENCE, "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "exp": now + 30, @@ -231,7 +247,7 @@ mod tests { }))?; let error = GitHubClaims::decode(&jwt, AUDIENCE, &DECODING_KEY).unwrap_err(); - assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `repository`", line: 1, column: 236)))"#); + assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `repository`", line: 1, column: 282)))"#); Ok(()) } @@ -243,6 +259,8 @@ mod tests { "jti": "example-id", "aud": AUDIENCE, "repository": "octo-org/octo-repo", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "exp": now + 30, @@ -250,7 +268,7 @@ mod tests { }))?; let error = GitHubClaims::decode(&jwt, AUDIENCE, &DECODING_KEY).unwrap_err(); - assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `repository_owner_id`", line: 1, column: 243)))"#); + assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `repository_owner_id`", line: 1, column: 289)))"#); Ok(()) } @@ -263,13 +281,15 @@ mod tests { "aud": AUDIENCE, "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "iss": "https://token.actions.githubusercontent.com", "exp": now + 30, "iat": now, }))?; let error = GitHubClaims::decode(&jwt, AUDIENCE, &DECODING_KEY).unwrap_err(); - assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `workflow_ref`", line: 1, column: 185)))"#); + assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `workflow_ref`", line: 1, column: 231)))"#); Ok(()) } @@ -283,6 +303,8 @@ mod tests { "aud": AUDIENCE, "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "exp": now + 30, "iat": now, @@ -303,6 +325,8 @@ mod tests { "aud": AUDIENCE, "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://gitlab.com", "exp": now + 30, @@ -324,13 +348,15 @@ mod tests { "aud": AUDIENCE, "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "iat": now, }))?; let error = GitHubClaims::decode(&jwt, AUDIENCE, &DECODING_KEY).unwrap_err(); - assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `exp`", line: 1, column: 253)))"#); + assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `exp`", line: 1, column: 299)))"#); Ok(()) } @@ -344,6 +370,8 @@ mod tests { "aud": AUDIENCE, "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "exp": now - 3000, @@ -365,13 +393,15 @@ mod tests { "aud": AUDIENCE, "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "exp": now + 30, }))?; let error = GitHubClaims::decode(&jwt, AUDIENCE, &DECODING_KEY).unwrap_err(); - assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `iat`", line: 1, column: 253)))"#); + assert_compact_debug_snapshot!(error, @r#"Error(Json(Error("missing field `iat`", line: 1, column: 299)))"#); Ok(()) } @@ -385,6 +415,8 @@ mod tests { "aud": AUDIENCE, "repository": "octo-org/octo-repo", "repository_owner_id": "65", + "run_id": "example-run-id", + "sha": "example-sha", "workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", "iss": "https://token.actions.githubusercontent.com", "exp": now + 300, diff --git a/src/controllers/trustpub/tokens/exchange/mod.rs b/src/controllers/trustpub/tokens/exchange/mod.rs index 7d5d5573495..423521add41 100644 --- a/src/controllers/trustpub/tokens/exchange/mod.rs +++ b/src/controllers/trustpub/tokens/exchange/mod.rs @@ -2,7 +2,7 @@ use super::json; use crate::app::AppState; use crate::util::errors::{AppResult, bad_request, server_error}; use axum::Json; -use crates_io_database::models::trustpub::{NewToken, NewUsedJti}; +use crates_io_database::models::trustpub::{NewToken, NewUsedJti, TrustpubData}; use crates_io_database::schema::trustpub_configs_github; use crates_io_diesel_helpers::lower; use crates_io_trustpub::access_token::AccessToken; @@ -130,11 +130,17 @@ pub async fn exchange_trustpub_token( let new_token = AccessToken::generate(); + let trustpub_data = TrustpubData::GitHub { + repository: signed_claims.repository, + run_id: signed_claims.run_id, + sha: signed_claims.sha, + }; + let new_token_model = NewToken { expires_at: chrono::Utc::now() + chrono::Duration::minutes(30), hashed_token: &new_token.sha256(), crate_ids: &crate_ids, - trustpub_data: None, + trustpub_data: Some(&trustpub_data), }; new_token_model.insert(conn).await?; From 6d409001aa0f522ab2ea0dfd3197e15552134723 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 4 Jul 2025 15:38:06 +0200 Subject: [PATCH 3/7] trustpub/exchange: Save `trustpub_data` from the `trustpub_tokens` table to the `versions` table --- src/controllers/krate/publish.rs | 33 ++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/controllers/krate/publish.rs b/src/controllers/krate/publish.rs index 0aa70e17a94..c3de6d7cade 100644 --- a/src/controllers/krate/publish.rs +++ b/src/controllers/krate/publish.rs @@ -44,7 +44,7 @@ use crate::util::errors::{AppResult, BoxedAppError, bad_request, custom, forbidd use crate::views::{ EncodableCrate, EncodableCrateDependency, GoodCrate, PublishMetadata, PublishWarnings, }; -use crates_io_database::models::{User, versions_published_by}; +use crates_io_database::models::{TrustpubData, User, versions_published_by}; use crates_io_diesel_helpers::canon_crate_name; use crates_io_trustpub::access_token::AccessToken; @@ -57,20 +57,27 @@ const MAX_DESCRIPTION_LENGTH: usize = 1000; enum AuthType { Regular(Box), - TrustPub, + TrustPub(Option), } impl AuthType { fn user(&self) -> Option<&User> { match self { AuthType::Regular(auth) => Some(auth.user()), - AuthType::TrustPub => None, + AuthType::TrustPub(_) => None, } } fn user_id(&self) -> Option { self.user().map(|u| u.id) } + + fn trustpub_data(&self) -> Option<&TrustpubData> { + match self { + AuthType::Regular(_) => None, + AuthType::TrustPub(data) => data.as_ref(), + } + } } /// Publish a new crate/version. @@ -173,14 +180,15 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult> = trustpub_tokens::table - .filter(trustpub_tokens::hashed_token.eq(hashed_token.as_slice())) - .filter(trustpub_tokens::expires_at.gt(now)) - .select(trustpub_tokens::crate_ids) - .get_result(&mut conn) - .await - .optional()? - .ok_or_else(|| forbidden("Invalid authentication token"))?; + let (crate_ids, trustpub_data): (Vec>, Option) = + trustpub_tokens::table + .filter(trustpub_tokens::hashed_token.eq(hashed_token.as_slice())) + .filter(trustpub_tokens::expires_at.gt(now)) + .select((trustpub_tokens::crate_ids, trustpub_tokens::trustpub_data)) + .get_result(&mut conn) + .await + .optional()? + .ok_or_else(|| forbidden("Invalid authentication token"))?; if !crate_ids.contains(&Some(existing_crate.id)) { let name = &existing_crate.name; @@ -188,7 +196,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult EndpointScope::PublishUpdate, @@ -502,6 +510,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult Date: Fri, 4 Jul 2025 15:15:21 +0200 Subject: [PATCH 4/7] emails: Adjust publish notification to show "by GitHub Actions" if applicable --- ...rate__publish__trustpub__full_flow-11.snap | 2 +- src/worker/jobs/send_publish_notifications.rs | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-11.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-11.snap index 9cb1eb46aa3..5367f67f985 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-11.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-11.snap @@ -54,7 +54,7 @@ Content-Transfer-Encoding: quoted-printable Hello foo! -A new version of the package foo (1.1.0) was published at [0000-00-00T00:00:00Z]. +A new version of the package foo (1.1.0) was published by GitHub Actions (https://github.com/rust-lang/foo-rs/actions/runs/example-run-id) at [0000-00-00T00:00:00Z]. If you have questions or security concerns, you can contact us at help@crates.io. If you would like to stop receiving these security notifications, you can disable them in your account settings. diff --git a/src/worker/jobs/send_publish_notifications.rs b/src/worker/jobs/send_publish_notifications.rs index fea389271c6..6c4210da9e6 100644 --- a/src/worker/jobs/send_publish_notifications.rs +++ b/src/worker/jobs/send_publish_notifications.rs @@ -1,5 +1,5 @@ use crate::email::EmailMessage; -use crate::models::OwnerKind; +use crate::models::{OwnerKind, TrustpubData}; use crate::schema::{crate_owners, crates, emails, users, versions}; use crate::worker::Environment; use anyhow::anyhow; @@ -74,16 +74,25 @@ impl BackgroundJob for SendPublishNotificationsJob { let krate = &publish_details.krate; let version = &publish_details.version; - let publisher_info = match &publish_details.publisher { - Some(publisher) if publisher == recipient => &format!( + let publisher_info = match (&publish_details.publisher, &publish_details.trustpub_data) + { + (Some(publisher), _) if publisher == recipient => &format!( " by your account (https://{domain}/users/{publisher})", domain = ctx.config.domain_name ), - Some(publisher) => &format!( + (Some(publisher), _) => &format!( " by {publisher} (https://{domain}/users/{publisher})", domain = ctx.config.domain_name ), - None => "", + ( + _, + Some(TrustpubData::GitHub { + repository, run_id, .. + }), + ) => &format!( + " by GitHub Actions (https://github.com/{repository}/actions/runs/{run_id})", + ), + _ => "", }; let email = EmailMessage::from_template( @@ -154,6 +163,8 @@ struct PublishDetails { publish_time: DateTime, #[diesel(select_expression = users::columns::gh_login.nullable())] publisher: Option, + #[diesel(select_expression = versions::columns::trustpub_data.nullable())] + trustpub_data: Option, } impl PublishDetails { From 2ff1b63d7f77e702acb45e0d36746d6042698462 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 4 Jul 2025 14:25:59 +0200 Subject: [PATCH 5/7] Expose `trustpub_data` field on version APIs --- ..._io__openapi__tests__openapi_snapshot-2.snap | 7 +++++++ ...e__publish__edition__edition_is_saved-4.snap | 1 + ...ublish__links__crate_with_links_field-3.snap | 1 + ...te__publish__manifest__boolean_readme-4.snap | 1 + ..._publish__manifest__lib_and_bin_crate-4.snap | 1 + ...__krate__publish__trustpub__full_flow-9.snap | 6 ++++++ ...e__yanking__patch_version_yank_unyank-2.snap | 3 ++- ...e__yanking__patch_version_yank_unyank-3.snap | 3 ++- ...e__yanking__patch_version_yank_unyank-4.snap | 3 ++- ...e__yanking__patch_version_yank_unyank-5.snap | 3 ++- ...e__yanking__patch_version_yank_unyank-6.snap | 3 ++- ...ate__yanking__patch_version_yank_unyank.snap | 3 ++- ...crates__read__include_default_version-2.snap | 1 + ...io__tests__routes__crates__read__show-2.snap | 3 +++ ...routes__crates__read__show_all_yanked-2.snap | 2 ++ ..._not_included_in_reverse_dependencies-2.snap | 1 + ...se_dependencies__reverse_dependencies-2.snap | 1 + ...cludes_published_by_user_when_present-2.snap | 2 ++ ...ery_supports_u64_version_number_parts-2.snap | 1 + ...ld_version_doesnt_depend_but_new_does-2.snap | 1 + ..._not_included_in_reverse_dependencies-2.snap | 1 + ...tes__crates__versions__list__versions-2.snap | 3 +++ ...y_crate_name_and_semver_no_published_by.snap | 1 + ...s__read__show_by_crate_name_and_version.snap | 1 + src/views.rs | 17 ++++++++++++++++- 25 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap index f0112dcd4d4..88413740453 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap @@ -1148,6 +1148,13 @@ expression: response.json() "null" ] }, + "trustpub_data": { + "description": "Information about the trusted publisher that published this version, if any.\n\nStatus: **Unstable**\n\nThis field is filled if the version was published via trusted publishing\n(e.g., GitHub Actions) rather than a regular API token.\n\nThe exact structure of this field depends on the `provider` field\ninside it.", + "type": [ + "object", + "null" + ] + }, "updated_at": { "description": "The date and time this version was last updated (i.e. yanked or unyanked).", "example": "2019-12-13T13:46:41Z", diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap index 722a178fba1..3850557bc57 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap @@ -49,6 +49,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo/1.0.0/readme", "repository": null, "rust_version": "1.0", + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap index f10a4c4b3d6..8f18952f4ec 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap @@ -49,6 +49,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo/1.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap index 5a1b329b9e1..5fe5f2e0f12 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap @@ -49,6 +49,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo/1.0.0/readme", "repository": null, "rust_version": "1.69", + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap index 4b3a96d7071..606721884a8 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap @@ -52,6 +52,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo/1.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap index c9a9656f3c1..1ad21be16e3 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap @@ -31,6 +31,12 @@ expression: response.json() "readme_path": "/api/v1/crates/foo/1.1.0/readme", "repository": null, "rust_version": null, + "trustpub_data": { + "provider": "github", + "repository": "rust-lang/foo-rs", + "run_id": "example-run-id", + "sha": "example-sha" + }, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap index 3bf4b8099bc..9782ad19f8e 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap @@ -62,6 +62,7 @@ expression: json "description": "description", "homepage": null, "documentation": null, - "repository": null + "repository": null, + "trustpub_data": null } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap index 41189ca5afa..3024140b782 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap @@ -73,6 +73,7 @@ expression: json "description": "description", "homepage": null, "documentation": null, - "repository": null + "repository": null, + "trustpub_data": null } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap index 41189ca5afa..3024140b782 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap @@ -73,6 +73,7 @@ expression: json "description": "description", "homepage": null, "documentation": null, - "repository": null + "repository": null, + "trustpub_data": null } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap index 28066f4633d..8e3a9965e2a 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap @@ -84,6 +84,7 @@ expression: json "description": "description", "homepage": null, "documentation": null, - "repository": null + "repository": null, + "trustpub_data": null } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap index 28066f4633d..8e3a9965e2a 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap @@ -84,6 +84,7 @@ expression: json "description": "description", "homepage": null, "documentation": null, - "repository": null + "repository": null, + "trustpub_data": null } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap index 3bf4b8099bc..9782ad19f8e 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap @@ -62,6 +62,7 @@ expression: json "description": "description", "homepage": null, "documentation": null, - "repository": null + "repository": null, + "trustpub_data": null } } diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap index c7b079c37a4..c5d5669a769 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap @@ -71,6 +71,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo_default_version/0.5.1/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap index 75c2cd698be..1db5c8d3bcc 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap @@ -84,6 +84,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo_show/0.5.1/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false @@ -122,6 +123,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo_show/0.5.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false @@ -154,6 +156,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo_show/1.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap index 30269ad6920..b24044455e9 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap @@ -83,6 +83,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo_show/0.5.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": true @@ -121,6 +122,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo_show/1.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": true diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap index 27e4773acf2..a28a59217da 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap @@ -55,6 +55,7 @@ expression: response.json() "readme_path": "/api/v1/crates/c3/1.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap index 7ca7deae51e..5a6e2f0bc53 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap @@ -55,6 +55,7 @@ expression: response.json() "readme_path": "/api/v1/crates/c2/1.1.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap index 94dde205c95..95e0eefb230 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap @@ -67,6 +67,7 @@ expression: response.json() "readme_path": "/api/v1/crates/c3/3.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false @@ -99,6 +100,7 @@ expression: response.json() "readme_path": "/api/v1/crates/c2/2.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap index 6850cb4856b..2f07702d00c 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap @@ -55,6 +55,7 @@ expression: response.json() "readme_path": "/api/v1/crates/c2/1.0.18446744073709551615/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap index 6e17a2fbc8f..0b9b5db1cf4 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap @@ -55,6 +55,7 @@ expression: response.json() "readme_path": "/api/v1/crates/c2/2.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap index 6e17a2fbc8f..0b9b5db1cf4 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap @@ -55,6 +55,7 @@ expression: response.json() "readme_path": "/api/v1/crates/c2/2.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap index d0546ef9c91..92ea2d62e20 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap @@ -36,6 +36,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo_versions/1.0.0/readme", "repository": null, "rust_version": "1.64", + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false @@ -74,6 +75,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo_versions/0.5.1/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false @@ -112,6 +114,7 @@ expression: response.json() "readme_path": "/api/v1/crates/foo_versions/0.5.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap index ea818781dfc..3e9fd9dbe94 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap @@ -31,6 +31,7 @@ expression: json "readme_path": "/api/v1/crates/foo_vers_show_no_pb/1.0.0/readme", "repository": null, "rust_version": null, + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap index 6940293e5ca..f174100a440 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap @@ -37,6 +37,7 @@ expression: json "readme_path": "/api/v1/crates/foo_vers_show/2.0.0/readme", "repository": null, "rust_version": "1.64", + "trustpub_data": null, "updated_at": "[datetime]", "yank_message": null, "yanked": false diff --git a/src/views.rs b/src/views.rs index 60ad1a710b2..481e3ec50f5 100644 --- a/src/views.rs +++ b/src/views.rs @@ -1,7 +1,7 @@ use crate::external_urls::remove_blocked_urls; use crate::models::{ ApiToken, Category, Crate, Dependency, DependencyKind, Keyword, Owner, ReverseDependency, Team, - TopVersions, User, Version, VersionDownload, VersionOwnerAction, + TopVersions, TrustpubData, User, Version, VersionDownload, VersionOwnerAction, }; use chrono::{DateTime, Utc}; use crates_io_github as github; @@ -912,6 +912,18 @@ pub struct EncodableVersion { /// The URL to the crate's repository, if set. #[schema(example = "https://github.com/serde-rs/serde")] pub repository: Option, + + /// Information about the trusted publisher that published this version, if any. + /// + /// Status: **Unstable** + /// + /// This field is filled if the version was published via trusted publishing + /// (e.g., GitHub Actions) rather than a regular API token. + /// + /// The exact structure of this field depends on the `provider` field + /// inside it. + #[schema(value_type = Option)] + pub trustpub_data: Option, } impl EncodableVersion { @@ -942,6 +954,7 @@ impl EncodableVersion { homepage, documentation, repository, + trustpub_data, .. } = version; @@ -976,6 +989,7 @@ impl EncodableVersion { homepage, documentation, repository, + trustpub_data, published_by: published_by.map(User::into), audit_actions: audit_actions .into_iter() @@ -1119,6 +1133,7 @@ mod tests { .unwrap() .and_utc(), }], + trustpub_data: None, }; let json = serde_json::to_string(&ver).unwrap(); assert_some!(json.as_str().find(r#""updated_at":"2017-01-06T14:23:11Z""#)); From ac617fe292792788fad1785e76b61cfd7179f8ea Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 4 Jul 2025 14:54:37 +0200 Subject: [PATCH 6/7] msw: Add `trustpub_data` field on `version` model --- packages/crates-io-msw/handlers/crates/downloads.test.js | 2 ++ packages/crates-io-msw/handlers/crates/get.test.js | 5 +++++ .../handlers/crates/reverse-dependencies.test.js | 2 ++ .../crates-io-msw/handlers/versions/follow-updates.test.js | 1 + packages/crates-io-msw/handlers/versions/get.test.js | 1 + packages/crates-io-msw/handlers/versions/list.test.js | 3 +++ packages/crates-io-msw/handlers/versions/patch.test.js | 2 ++ packages/crates-io-msw/models/dependency.test.js | 1 + packages/crates-io-msw/models/version-download.test.js | 1 + packages/crates-io-msw/models/version.js | 2 ++ packages/crates-io-msw/models/version.test.js | 1 + 11 files changed, 21 insertions(+) diff --git a/packages/crates-io-msw/handlers/crates/downloads.test.js b/packages/crates-io-msw/handlers/crates/downloads.test.js index d1c3d9a3aca..e588f787de7 100644 --- a/packages/crates-io-msw/handlers/crates/downloads.test.js +++ b/packages/crates-io-msw/handlers/crates/downloads.test.js @@ -99,6 +99,7 @@ test('includes related versions', async function () { published_by: null, readme_path: '/api/v1/crates/rand/1.0.0/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yank_message: null, yanked: false, @@ -120,6 +121,7 @@ test('includes related versions', async function () { published_by: null, readme_path: '/api/v1/crates/rand/1.0.1/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yank_message: null, yanked: false, diff --git a/packages/crates-io-msw/handlers/crates/get.test.js b/packages/crates-io-msw/handlers/crates/get.test.js index c3b2a51ef09..2dfa077b331 100644 --- a/packages/crates-io-msw/handlers/crates/get.test.js +++ b/packages/crates-io-msw/handlers/crates/get.test.js @@ -64,6 +64,7 @@ test('returns a crate object for known crates', async function () { published_by: null, readme_path: '/api/v1/crates/rand/1.0.0-beta.1/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, @@ -128,6 +129,7 @@ test('works for non-canonical names', async function () { published_by: null, readme_path: '/api/v1/crates/foo-bar/1.0.0-beta.1/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, @@ -165,6 +167,7 @@ test('includes related versions', async function () { published_by: null, readme_path: '/api/v1/crates/rand/1.2.0/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, @@ -186,6 +189,7 @@ test('includes related versions', async function () { published_by: null, readme_path: '/api/v1/crates/rand/1.1.0/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, @@ -207,6 +211,7 @@ test('includes related versions', async function () { published_by: null, readme_path: '/api/v1/crates/rand/1.0.0/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, diff --git a/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js b/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js index af19abf58aa..88c6ec1642d 100644 --- a/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js +++ b/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js @@ -84,6 +84,7 @@ test('returns a paginated list of crate versions depending to the specified crat published_by: null, readme_path: '/api/v1/crates/baz/1.0.1/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, @@ -105,6 +106,7 @@ test('returns a paginated list of crate versions depending to the specified crat published_by: null, readme_path: '/api/v1/crates/bar/1.0.0/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, diff --git a/packages/crates-io-msw/handlers/versions/follow-updates.test.js b/packages/crates-io-msw/handlers/versions/follow-updates.test.js index 26029165ee3..49d4ff3c9b6 100644 --- a/packages/crates-io-msw/handlers/versions/follow-updates.test.js +++ b/packages/crates-io-msw/handlers/versions/follow-updates.test.js @@ -41,6 +41,7 @@ test('returns latest versions of followed crates', async function () { published_by: null, readme_path: '/api/v1/crates/foo/1.2.3/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, diff --git a/packages/crates-io-msw/handlers/versions/get.test.js b/packages/crates-io-msw/handlers/versions/get.test.js index 6c116c90a0c..5a37d3eb422 100644 --- a/packages/crates-io-msw/handlers/versions/get.test.js +++ b/packages/crates-io-msw/handlers/versions/get.test.js @@ -42,6 +42,7 @@ test('returns a version object for known version', async function () { published_by: null, readme_path: '/api/v1/crates/rand/1.0.0-beta.1/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yank_message: null, yanked: false, diff --git a/packages/crates-io-msw/handlers/versions/list.test.js b/packages/crates-io-msw/handlers/versions/list.test.js index 263d0f0d007..01b733d6bf7 100644 --- a/packages/crates-io-msw/handlers/versions/list.test.js +++ b/packages/crates-io-msw/handlers/versions/list.test.js @@ -47,6 +47,7 @@ test('returns all versions belonging to the specified crate', async function () published_by: null, readme_path: '/api/v1/crates/rand/1.2.0/readme', rust_version: '1.69', + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, @@ -74,6 +75,7 @@ test('returns all versions belonging to the specified crate', async function () }, readme_path: '/api/v1/crates/rand/1.1.0/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, @@ -95,6 +97,7 @@ test('returns all versions belonging to the specified crate', async function () published_by: null, readme_path: '/api/v1/crates/rand/1.0.0/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yanked: false, yank_message: null, diff --git a/packages/crates-io-msw/handlers/versions/patch.test.js b/packages/crates-io-msw/handlers/versions/patch.test.js index a3ee8a368af..829c29c16e8 100644 --- a/packages/crates-io-msw/handlers/versions/patch.test.js +++ b/packages/crates-io-msw/handlers/versions/patch.test.js @@ -72,6 +72,7 @@ test('yanks the version', async function () { published_by: null, readme_path: '/api/v1/crates/foo/1.0.0/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yank_message: 'some reason', yanked: true, @@ -102,6 +103,7 @@ test('yanks the version', async function () { published_by: null, readme_path: '/api/v1/crates/foo/1.0.0/readme', rust_version: null, + trustpub_data: null, updated_at: '2017-02-24T12:34:56Z', yank_message: null, yanked: false, diff --git a/packages/crates-io-msw/models/dependency.test.js b/packages/crates-io-msw/models/dependency.test.js index 7476b00339b..b9ad66d9b8c 100644 --- a/packages/crates-io-msw/models/dependency.test.js +++ b/packages/crates-io-msw/models/dependency.test.js @@ -76,6 +76,7 @@ test('happy path', ({ expect }) => { "publishedBy": null, "readme": null, "rust_version": null, + "trustpub_data": null, "updated_at": "2017-02-24T12:34:56Z", "yank_message": null, "yanked": false, diff --git a/packages/crates-io-msw/models/version-download.test.js b/packages/crates-io-msw/models/version-download.test.js index 76a495470ef..0dd48be6bca 100644 --- a/packages/crates-io-msw/models/version-download.test.js +++ b/packages/crates-io-msw/models/version-download.test.js @@ -46,6 +46,7 @@ test('happy path', ({ expect }) => { "publishedBy": null, "readme": null, "rust_version": null, + "trustpub_data": null, "updated_at": "2017-02-24T12:34:56Z", "yank_message": null, "yanked": false, diff --git a/packages/crates-io-msw/models/version.js b/packages/crates-io-msw/models/version.js index 90b367e774f..bf0b4ec22f9 100644 --- a/packages/crates-io-msw/models/version.js +++ b/packages/crates-io-msw/models/version.js @@ -18,6 +18,7 @@ export default { crate_size: Number, readme: nullable(String), rust_version: nullable(String), + trustpub_data: nullable(Object), crate: oneOf('crate'), publishedBy: nullable(oneOf('user')), @@ -34,6 +35,7 @@ export default { applyDefault(attrs, 'crate_size', () => (((attrs.id + 13) * 42) % 13) * 54_321); applyDefault(attrs, 'readme', () => null); applyDefault(attrs, 'rust_version', () => null); + applyDefault(attrs, 'trustpub_data', () => null); if (!attrs.crate) { throw new Error(`Missing \`crate\` relationship on \`version:${attrs.num}\``); diff --git a/packages/crates-io-msw/models/version.test.js b/packages/crates-io-msw/models/version.test.js index e5d6e3c002f..8d19e77f28d 100644 --- a/packages/crates-io-msw/models/version.test.js +++ b/packages/crates-io-msw/models/version.test.js @@ -41,6 +41,7 @@ test('happy path', ({ expect }) => { "publishedBy": null, "readme": null, "rust_version": null, + "trustpub_data": null, "updated_at": "2017-02-24T12:34:56Z", "yank_message": null, "yanked": false, From 1870ecfb005b1c5ec95cf76fdf5b1ea614297db7 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 4 Jul 2025 15:45:44 +0200 Subject: [PATCH 7/7] Show "via GitHub" on version list if version was published by GitHub Actions --- app/components/version-list/row.hbs | 17 +++++++++++++++++ app/models/version.js | 24 ++++++++++++++++++++++++ e2e/acceptance/versions.spec.ts | 9 ++++++++- tests/acceptance/versions-test.js | 9 ++++++++- 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/app/components/version-list/row.hbs b/app/components/version-list/row.hbs index 97a3bc2c2bb..7d90edbed89 100644 --- a/app/components/version-list/row.hbs +++ b/app/components/version-list/row.hbs @@ -45,6 +45,23 @@ {{or @version.published_by.name @version.published_by.login}} + {{else if @version.trustpubPublisher}} + + via + {{#if @version.trustpubUrl}} + + {{#if (eq @version.trustpub_data.provider "github")}} + {{svg-jar "github"}} + {{/if}} + {{@version.trustpubPublisher}} + + {{else}} + {{#if (eq @version.trustpub_data.provider "github")}} + {{svg-jar "github"}} + {{/if}} + {{@version.trustpubPublisher}} + {{/if}} + {{/if}}