11// This code has been adapted from uv under https://github.com/astral-sh/uv/blob/c5caf92edf539a9ebf24d375871178f8f8a0ab93/crates/uv-publish/src/trusted_publishing.rs
22// The original code is dual-licensed under Apache-2.0 and MIT
33
4- //! Trusted publishing (via OIDC) with GitHub actions .
4+ //! Trusted publishing (via OIDC) with GitHub Actions, GitLab CI, and Google Cloud .
55
66use reqwest:: StatusCode ;
77use reqwest_middleware:: ClientWithMiddleware ;
@@ -12,24 +12,57 @@ use std::ffi::OsString;
1212use thiserror:: Error ;
1313use url:: Url ;
1414
15- use crate :: utils:: console_utils:: github_action_runner;
15+ use crate :: utils:: console_utils:: { github_action_runner, gitlab_ci_runner , google_cloud_runner } ;
1616use crate :: utils:: consts;
1717
18+ /// Represents the CI provider being used for trusted publishing.
19+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
20+ pub enum CiProvider {
21+ GitHubActions ,
22+ GitLabCI ,
23+ GoogleCloud ,
24+ }
25+
26+ impl std:: fmt:: Display for CiProvider {
27+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
28+ match self {
29+ CiProvider :: GitHubActions => write ! ( f, "GitHub Actions" ) ,
30+ CiProvider :: GitLabCI => write ! ( f, "GitLab CI" ) ,
31+ CiProvider :: GoogleCloud => write ! ( f, "Google Cloud" ) ,
32+ }
33+ }
34+ }
35+
36+ /// Detects which CI provider is being used, if any.
37+ pub fn detect_ci_provider ( ) -> Option < CiProvider > {
38+ if github_action_runner ( ) {
39+ Some ( CiProvider :: GitHubActions )
40+ } else if gitlab_ci_runner ( ) {
41+ Some ( CiProvider :: GitLabCI )
42+ } else if google_cloud_runner ( ) {
43+ Some ( CiProvider :: GoogleCloud )
44+ } else {
45+ None
46+ }
47+ }
48+
1849/// If applicable, attempt obtaining a token for trusted publishing.
1950pub async fn check_trusted_publishing (
2051 client : & ClientWithMiddleware ,
2152 prefix_url : & Url ,
2253) -> TrustedPublishResult {
23- // If we aren't in GitHub Actions, we can't use trusted publishing.
24- if ! github_action_runner ( ) {
25- return TrustedPublishResult :: Skipped ;
26- }
27- // We could check for credentials from the keyring or netrc the auth middleware first, but
28- // given that we are in GitHub Actions we check for trusted publishing first.
54+ // Check which CI provider we're running on
55+ let provider = match detect_ci_provider ( ) {
56+ Some ( p ) => p ,
57+ None => return TrustedPublishResult :: Skipped ,
58+ } ;
59+
2960 tracing:: debug!(
30- "Running on GitHub Actions without explicit credentials, checking for trusted publishing"
61+ "Running on {} without explicit credentials, checking for trusted publishing" ,
62+ provider
3163 ) ;
32- match get_token ( client, prefix_url) . await {
64+
65+ match get_token ( client, prefix_url, provider) . await {
3366 Ok ( token) => TrustedPublishResult :: Configured ( token) ,
3467 Err ( err) => {
3568 tracing:: debug!( "Could not obtain trusted publishing credentials, skipping: {err}" ) ;
@@ -63,6 +96,14 @@ pub enum TrustedPublishingError {
6396 "Prefix.dev returned error code {0}, is trusted publishing correctly configured?\n Response: {1}"
6497 ) ]
6598 PrefixDev ( StatusCode , String ) ,
99+ #[ error( "GitLab CI OIDC token not found. Make sure you have configured `id_tokens` in your .gitlab-ci.yml:\n \n \
100+ job_name:\n \
101+ id_tokens:\n \
102+ PREFIX_ID_TOKEN:\n \
103+ aud: prefix.dev\n ") ]
104+ GitLabOidcTokenNotFound ,
105+ #[ error( "Google Cloud OIDC token retrieval failed. Make sure you are running in a Google Cloud environment (Cloud Build, Cloud Run, GCE, or GKE) with a service account attached." ) ]
106+ GoogleCloudOidcTokenNotFound ,
66107}
67108
68109impl TrustedPublishingError {
@@ -100,42 +141,115 @@ struct MintTokenRequest {
100141pub async fn get_token (
101142 client : & ClientWithMiddleware ,
102143 prefix_url : & Url ,
144+ provider : CiProvider ,
103145) -> Result < TrustedPublishingToken , TrustedPublishingError > {
104- // If this fails, we can skip the audience request.
105- let oidc_token_request_token =
106- env:: var ( consts:: ACTIONS_ID_TOKEN_REQUEST_TOKEN ) . map_err ( |err| {
107- TrustedPublishingError :: from_var_err ( consts:: ACTIONS_ID_TOKEN_REQUEST_TOKEN , err)
108- } ) ?;
146+ // Get the OIDC token based on the CI provider
147+ let oidc_token = match provider {
148+ CiProvider :: GitHubActions => {
149+ // If this fails, we can skip the audience request.
150+ let oidc_token_request_token = env:: var ( consts:: ACTIONS_ID_TOKEN_REQUEST_TOKEN )
151+ . map_err ( |err| {
152+ TrustedPublishingError :: from_var_err (
153+ consts:: ACTIONS_ID_TOKEN_REQUEST_TOKEN ,
154+ err,
155+ )
156+ } ) ?;
109157
110- // Request 1: Get the OIDC token from GitHub.
111- let oidc_token = get_oidc_token ( & oidc_token_request_token, client) . await ?;
158+ // Request 1: Get the OIDC token from GitHub.
159+ get_github_oidc_token ( & oidc_token_request_token, client) . await ?
160+ }
161+ CiProvider :: GitLabCI => {
162+ // Get the OIDC token from GitLab CI environment variable
163+ get_gitlab_oidc_token ( ) ?
164+ }
165+ CiProvider :: GoogleCloud => {
166+ // Get the OIDC token from Google Cloud metadata server
167+ get_google_cloud_oidc_token ( client) . await ?
168+ }
169+ } ;
112170
113171 // Request 2: Get the publishing token from prefix.dev.
114172 let publish_token = get_publish_token ( & oidc_token, prefix_url, client) . await ?;
115173
116- tracing:: info!( "Received token, using trusted publishing" ) ;
174+ tracing:: info!( "Received token from {} , using trusted publishing" , provider ) ;
117175
118- // Tell GitHub Actions to mask the token in any console logs.
119- if github_action_runner ( ) {
120- println ! ( "::add-mask::{}" , & publish_token. secret( ) ) ;
176+ // Mask the token in CI logs
177+ match provider {
178+ CiProvider :: GitHubActions => {
179+ println ! ( "::add-mask::{}" , & publish_token. secret( ) ) ;
180+ }
181+ CiProvider :: GitLabCI => {
182+ // GitLab CI doesn't have a built-in mask mechanism like GitHub Actions,
183+ // but the token should be short-lived anyway
184+ tracing:: debug!( "Token obtained via GitLab CI trusted publishing" ) ;
185+ }
186+ CiProvider :: GoogleCloud => {
187+ // Google Cloud doesn't have a built-in mask mechanism,
188+ // but the token should be short-lived anyway
189+ tracing:: debug!( "Token obtained via Google Cloud trusted publishing" ) ;
190+ }
121191 }
122192
123193 Ok ( publish_token)
124194}
125195
126- /// Get raw OIDC token for attestation generation
196+ /// Get raw OIDC token for attestation generation.
197+ /// Returns the OIDC token from the current CI provider.
127198pub async fn get_raw_oidc_token (
128199 client : & ClientWithMiddleware ,
129200) -> Result < String , TrustedPublishingError > {
130- let oidc_token_request_token =
131- env:: var ( consts:: ACTIONS_ID_TOKEN_REQUEST_TOKEN ) . map_err ( |err| {
132- TrustedPublishingError :: from_var_err ( consts:: ACTIONS_ID_TOKEN_REQUEST_TOKEN , err)
133- } ) ?;
201+ match detect_ci_provider ( ) {
202+ Some ( CiProvider :: GitHubActions ) => {
203+ let oidc_token_request_token = env:: var ( consts:: ACTIONS_ID_TOKEN_REQUEST_TOKEN )
204+ . map_err ( |err| {
205+ TrustedPublishingError :: from_var_err (
206+ consts:: ACTIONS_ID_TOKEN_REQUEST_TOKEN ,
207+ err,
208+ )
209+ } ) ?;
210+ get_github_oidc_token ( & oidc_token_request_token, client) . await
211+ }
212+ Some ( CiProvider :: GitLabCI ) => get_gitlab_oidc_token ( ) ,
213+ Some ( CiProvider :: GoogleCloud ) => get_google_cloud_oidc_token ( client) . await ,
214+ None => Err ( TrustedPublishingError :: MissingEnvVar (
215+ "GITHUB_ACTIONS, GITLAB_CI, or CLOUD_BUILD_ID/K_SERVICE" ,
216+ ) ) ,
217+ }
218+ }
134219
135- get_oidc_token ( & oidc_token_request_token, client) . await
220+ /// Get the OIDC token from GitLab CI.
221+ /// GitLab CI provides the token via the `PREFIX_ID_TOKEN` environment variable
222+ /// when configured with `id_tokens` in the `.gitlab-ci.yml` file.
223+ fn get_gitlab_oidc_token ( ) -> Result < String , TrustedPublishingError > {
224+ // GitLab CI provides OIDC tokens via the `id_tokens` keyword in .gitlab-ci.yml
225+ // The user should configure their job like this:
226+ //
227+ // job_name:
228+ // id_tokens:
229+ // PREFIX_ID_TOKEN:
230+ // aud: prefix.dev
231+ // script:
232+ // - rattler upload ...
233+ //
234+ // The token is then available as the PREFIX_ID_TOKEN environment variable.
235+ match env:: var ( consts:: PREFIX_ID_TOKEN ) {
236+ Ok ( token) if !token. is_empty ( ) => {
237+ tracing:: info!( "Found GitLab CI OIDC token in PREFIX_ID_TOKEN" ) ;
238+ Ok ( token)
239+ }
240+ Ok ( _) => {
241+ tracing:: warn!( "PREFIX_ID_TOKEN is set but empty" ) ;
242+ Err ( TrustedPublishingError :: GitLabOidcTokenNotFound )
243+ }
244+ Err ( _) => {
245+ tracing:: debug!( "PREFIX_ID_TOKEN not found in environment" ) ;
246+ Err ( TrustedPublishingError :: GitLabOidcTokenNotFound )
247+ }
248+ }
136249}
137250
138- async fn get_oidc_token (
251+ /// Get the OIDC token from GitHub Actions.
252+ async fn get_github_oidc_token (
139253 oidc_token_request_token : & str ,
140254 client : & ClientWithMiddleware ,
141255) -> Result < String , TrustedPublishingError > {
@@ -162,6 +276,57 @@ async fn get_oidc_token(
162276 Ok ( oidc_token. value )
163277}
164278
279+ /// Get the OIDC token from Google Cloud metadata server.
280+ /// Works in Cloud Build, Cloud Run, GCE, and GKE with Workload Identity.
281+ /// Respects the `GCE_METADATA_HOST` environment variable for custom metadata server hostnames.
282+ async fn get_google_cloud_oidc_token (
283+ client : & ClientWithMiddleware ,
284+ ) -> Result < String , TrustedPublishingError > {
285+ // Use GCE_METADATA_HOST if set, otherwise use the default hostname
286+ let metadata_host = env:: var ( consts:: GCE_METADATA_HOST )
287+ . unwrap_or_else ( |_| consts:: GCP_METADATA_HOST_DEFAULT . to_string ( ) ) ;
288+
289+ let metadata_url = format ! (
290+ "http://{}{}?audience=prefix.dev" ,
291+ metadata_host,
292+ consts:: GCP_METADATA_IDENTITY_PATH
293+ ) ;
294+ let url = Url :: parse ( & metadata_url) ?;
295+
296+ tracing:: info!(
297+ "Querying the trusted publishing OIDC token from Google Cloud metadata server at {}" ,
298+ metadata_host
299+ ) ;
300+
301+ let response = client
302+ . get ( url. clone ( ) )
303+ . header ( "Metadata-Flavor" , "Google" )
304+ . send ( )
305+ . await
306+ . map_err ( |err| TrustedPublishingError :: ReqwestMiddleware ( url. clone ( ) , err) ) ?;
307+
308+ if !response. status ( ) . is_success ( ) {
309+ tracing:: warn!(
310+ "Google Cloud metadata server returned status {}" ,
311+ response. status( )
312+ ) ;
313+ return Err ( TrustedPublishingError :: GoogleCloudOidcTokenNotFound ) ;
314+ }
315+
316+ let token = response
317+ . text ( )
318+ . await
319+ . map_err ( |err| TrustedPublishingError :: Reqwest ( url. clone ( ) , err) ) ?;
320+
321+ if token. is_empty ( ) {
322+ tracing:: warn!( "Google Cloud metadata server returned empty token" ) ;
323+ return Err ( TrustedPublishingError :: GoogleCloudOidcTokenNotFound ) ;
324+ }
325+
326+ tracing:: info!( "Successfully obtained OIDC token from Google Cloud" ) ;
327+ Ok ( token)
328+ }
329+
165330async fn get_publish_token (
166331 oidc_token : & str ,
167332 prefix_url : & Url ,
0 commit comments