Skip to content

Commit 9a2c2c4

Browse files
ci(INFRA-3631): shadow dispatch via token exchange OIDC
Replace GitHub App token with TOKEN_EXCHANGE_URL exchange aligned to TES policy. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b594287 commit 9a2c2c4

1 file changed

Lines changed: 55 additions & 18 deletions

File tree

.github/workflows/ci-namespace-shadow.yml

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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

3036
on:
3137
pull_request:
@@ -46,29 +52,60 @@ concurrency:
4652

4753
permissions:
4854
contents: read
55+
id-token: write
4956

5057
jobs:
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

Comments
 (0)