@@ -20,12 +20,18 @@ name: CI (Namespace shadow)
2020# - The dispatcher job posts a GitHub Actions step summary linking the PR
2121# to the dispatched shadow run URL, reachable from the PR Checks tab.
2222#
23- # Authentication (GitHub App, not a PAT):
24- # - Repo variable: SHADOW_CI_DISPATCH_GITHUB_APP_CLIENT_ID (App "Client ID")
25- # - Repo secret: SHADOW_CI_DISPATCH_GITHUB_APP_PRIVATE_KEY (PEM private key)
26- # - App must be installed on MetaMask/metamask-mobile with at least:
27- # Metadata: Read, Contents: Read, Actions: Read and write (installation
28- # permissions must cover the token's permission-* inputs below).
23+ # Authentication: Token Exchange Service (same pattern as triage-forwarder.yml
24+ # and shared-services-workflows deploy.yml — OIDC → POST /api/exchange/token).
25+ #
26+ # Prerequisites:
27+ # - Repo/org Actions variable: TOKEN_EXCHANGE_URL (already used elsewhere in
28+ # metamask-mobile, e.g. triage-forwarder).
29+ # - Rego policy in token-exchange-service: explicit policy
30+ # mm-metamask-mobile-namespace-shadow-ci-token-exchange (matches OIDC
31+ # `workflow_ref` claim for .github/workflows/ci-namespace-shadow.yml).
32+ #
33+ # Fork PRs: the job `if` below skips the entire job for fork head repos, so OIDC
34+ # exchange never runs for untrusted forks (same model as not exposing secrets).
2935
3036on :
3137 pull_request :
@@ -46,29 +52,60 @@ concurrency:
4652
4753permissions :
4854 contents : read
55+ id-token : write
4956
5057jobs :
5158 dispatch-shadow :
5259 name : " [shadow] Dispatch"
5360 runs-on : ubuntu-latest
54- # Skip dispatch for fork PRs: App credentials are not exposed to fork workflows,
55- # and we don't want untrusted PRs consuming Namespace capacity.
61+ # Fork PRs use head.repo != github.repository — skip (no shadow, no token exchange).
5662 if : ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
5763 steps :
58- - name : Mint GitHub App installation token
59- id : shadow-dispatch-token
60- uses : actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
61- with :
62- client-id : ${{ vars.SHADOW_CI_DISPATCH_GITHUB_APP_CLIENT_ID }}
63- private-key : ${{ secrets.SHADOW_CI_DISPATCH_GITHUB_APP_PRIVATE_KEY }}
64- permission-metadata : read
65- permission-contents : read
66- permission-actions : write
64+ - name : Get OIDC token for token-exchange-service
65+ id : oidc
66+ run : |
67+ set -euo pipefail
68+ OIDC_TOKEN=$(curl -sSf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
69+ "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=api://token-exchange-service" | jq -r '.value')
70+ echo "::add-mask::$OIDC_TOKEN"
71+ echo "oidc_token=$OIDC_TOKEN" >> "$GITHUB_OUTPUT"
72+
73+ - name : Exchange for installation token (scoped permissions)
74+ id : exchange
75+ env :
76+ OIDC_TOKEN : ${{ steps.oidc.outputs.oidc_token }}
77+ TOKEN_EXCHANGE_URL : ${{ vars.TOKEN_EXCHANGE_URL }}
78+ run : |
79+ set -euo pipefail
80+ if [ -z "${TOKEN_EXCHANGE_URL}" ]; then
81+ echo "::error::TOKEN_EXCHANGE_URL Actions variable is not set. Configure it at org or repo level (see triage-forwarder.yml)."
82+ exit 1
83+ fi
84+ RESPONSE=$(curl -sSf -X POST "${TOKEN_EXCHANGE_URL}/api/exchange/token" \
85+ -H "Content-Type: application/json" \
86+ -d "$(jq -cn \
87+ --arg oidcToken "$OIDC_TOKEN" \
88+ --arg targetRepo "${{ github.repository }}" \
89+ '{oidcToken: $oidcToken, targetRepo: $targetRepo, requested_permissions: {metadata: "read", contents: "read", actions: "write"}}')")
90+ STATUS=$(echo "$RESPONSE" | jq -r '.status // "ok"')
91+ if [[ "$STATUS" == "fail" ]]; then
92+ MSG=$(echo "$RESPONSE" | jq -r '.message // "unknown"')
93+ echo "::error::Token exchange failed: ${MSG}"
94+ echo "::notice::Ensure token-exchange-service policy mm-metamask-mobile-namespace-shadow-ci-token-exchange is deployed (INFRA-3631)."
95+ exit 1
96+ fi
97+ TOKEN=$(echo "$RESPONSE" | jq -r '.token // empty')
98+ if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
99+ echo "::error::Token exchange returned no token"
100+ exit 1
101+ fi
102+ echo "::add-mask::$TOKEN"
103+ echo "token=$TOKEN" >> "$GITHUB_OUTPUT"
67104
68105 - name : Dispatch ci.yml on Namespace
69106 id : dispatch
70107 env :
71- GH_TOKEN : ${{ steps.shadow-dispatch-token .outputs.token }}
108+ GH_TOKEN : ${{ steps.exchange .outputs.token }}
72109 REPO : ${{ github.repository }}
73110 REF : ${{ github.head_ref || github.ref_name }}
74111 PR_NUMBER : ${{ github.event.pull_request.number || '' }}
0 commit comments