CI (Namespace shadow) #272
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI (Namespace shadow) | |
| # Fire-and-forget dispatcher for the Namespace shadow benchmark (INFRA-3631). | |
| # | |
| # This workflow does NOT use `workflow_call`. Instead, it dispatches `ci.yml` | |
| # as a separate `workflow_dispatch` run with `runner_provider=namespace`. | |
| # | |
| # Why a dispatch instead of a call: | |
| # - `workflow_call` nests the called workflow's jobs into THIS workflow's | |
| # check suite on the PR. With the repo's merge queue ALLGREEN policy, | |
| # any shadow flake would block merges. | |
| # - `workflow_dispatch` runs live in the Actions tab only -- invisible to | |
| # PR checks. The shadow can fail freely; it never blocks the queue, | |
| # never duplicates commit statuses, never posts PR comments. | |
| # | |
| # Correlation back to the originating PR: | |
| # - The dispatched run's display name is set via `run-name` in ci.yml to | |
| # `ci [shadow PR #<num> @ <sha>]`, so it's identifiable in the Actions | |
| # tab at a glance. | |
| # - The dispatcher job posts a GitHub Actions step summary linking the PR | |
| # to the dispatched shadow run URL, reachable from the PR Checks tab. | |
| # | |
| # Authentication: Token Exchange Service (same pattern as triage-forwarder.yml | |
| # and shared-services-workflows deploy.yml — OIDC → POST /api/exchange/token). | |
| # | |
| # Prerequisites: | |
| # - Repo/org Actions variable: TOKEN_EXCHANGE_URL (already used elsewhere in | |
| # metamask-mobile, e.g. triage-forwarder). | |
| # - Rego policy in token-exchange-service: explicit policy | |
| # mm-metamask-mobile-namespace-shadow-ci-token-exchange (matches OIDC | |
| # `workflow_ref` claim for .github/workflows/ci-namespace-shadow.yml). | |
| # | |
| # Fork PRs: the job `if` below skips the entire job for fork head repos, so OIDC | |
| # exchange never runs for untrusted forks (same model as not exposing secrets). | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| paths-ignore: | |
| - "docs/**" | |
| - "**/*.md" | |
| - ".github/CODEOWNERS" | |
| push: | |
| branches: [main] | |
| schedule: | |
| - cron: "0 * * * *" | |
| workflow_dispatch: | |
| concurrency: | |
| group: ns-shadow-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| id-token: write | |
| jobs: | |
| dispatch-shadow: | |
| name: "[shadow] Dispatch" | |
| runs-on: ubuntu-latest | |
| # Fork PRs use head.repo != github.repository — skip (no shadow, no token exchange). | |
| if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} | |
| steps: | |
| - name: Get OIDC token for token-exchange-service | |
| id: oidc | |
| run: | | |
| set -euo pipefail | |
| OIDC_TOKEN=$(curl -sSf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ | |
| "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=api://token-exchange-service" | jq -r '.value') | |
| echo "::add-mask::$OIDC_TOKEN" | |
| echo "oidc_token=$OIDC_TOKEN" >> "$GITHUB_OUTPUT" | |
| - name: Exchange for installation token (scoped permissions) | |
| id: exchange | |
| env: | |
| OIDC_TOKEN: ${{ steps.oidc.outputs.oidc_token }} | |
| TOKEN_EXCHANGE_URL: ${{ vars.TOKEN_EXCHANGE_URL }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${TOKEN_EXCHANGE_URL}" ]; then | |
| echo "::error::TOKEN_EXCHANGE_URL Actions variable is not set. Configure it at org or repo level (see triage-forwarder.yml)." | |
| exit 1 | |
| fi | |
| RESPONSE=$(curl -sSf -X POST "${TOKEN_EXCHANGE_URL}/api/exchange/token" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$(jq -cn \ | |
| --arg oidcToken "$OIDC_TOKEN" \ | |
| --arg targetRepo "${{ github.repository }}" \ | |
| '{oidcToken: $oidcToken, targetRepo: $targetRepo, requested_permissions: {metadata: "read", contents: "read", actions: "write"}}')") | |
| STATUS=$(echo "$RESPONSE" | jq -r '.status // "ok"') | |
| if [[ "$STATUS" == "fail" ]]; then | |
| MSG=$(echo "$RESPONSE" | jq -r '.message // "unknown"') | |
| echo "::error::Token exchange failed: ${MSG}" | |
| echo "::notice::Ensure token-exchange-service policy mm-metamask-mobile-namespace-shadow-ci-token-exchange is deployed (INFRA-3631)." | |
| exit 1 | |
| fi | |
| TOKEN=$(echo "$RESPONSE" | jq -r '.token // empty') | |
| if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then | |
| echo "::error::Token exchange returned no token" | |
| exit 1 | |
| fi | |
| echo "::add-mask::$TOKEN" | |
| echo "token=$TOKEN" >> "$GITHUB_OUTPUT" | |
| - name: Dispatch ci.yml on Namespace | |
| id: dispatch | |
| env: | |
| GH_TOKEN: ${{ steps.exchange.outputs.token }} | |
| REPO: ${{ github.repository }} | |
| REF: ${{ github.head_ref || github.ref_name }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || '' }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| run: | | |
| set -euo pipefail | |
| echo "Dispatching ci.yml on ref='${REF}' (PR='${PR_NUMBER:-none}', sha='${HEAD_SHA}')" | |
| gh workflow run ci.yml \ | |
| --repo "$REPO" \ | |
| --ref "$REF" \ | |
| -f runner_provider=namespace \ | |
| -f pr_number="$PR_NUMBER" \ | |
| -f head_sha="$HEAD_SHA" | |
| # gh workflow run returns immediately with no run ID. Find the run | |
| # we just created so we can link to it from the step summary. | |
| # Best-effort: poll briefly for a queued/in_progress dispatch on this ref. | |
| RUN_URL="" | |
| for _ in $(seq 1 10); do | |
| RUN_URL=$(gh run list \ | |
| --repo "$REPO" \ | |
| --workflow ci.yml \ | |
| --event workflow_dispatch \ | |
| --branch "$REF" \ | |
| --limit 1 \ | |
| --json url,createdAt \ | |
| --jq '.[0].url' 2>/dev/null || true) | |
| if [ -n "$RUN_URL" ] && [ "$RUN_URL" != "null" ]; then | |
| break | |
| fi | |
| sleep 2 | |
| done | |
| echo "run_url=${RUN_URL}" >> "$GITHUB_OUTPUT" | |
| - name: Step summary | |
| env: | |
| PR_NUMBER: ${{ github.event.pull_request.number || '' }} | |
| PR_URL: ${{ github.event.pull_request.html_url || '' }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| REF: ${{ github.head_ref || github.ref_name }} | |
| RUN_URL: ${{ steps.dispatch.outputs.run_url }} | |
| run: | | |
| { | |
| echo "## Namespace shadow CI dispatched" | |
| echo | |
| echo "| Field | Value |" | |
| echo "|---|---|" | |
| if [ -n "$PR_NUMBER" ]; then | |
| echo "| PR | [#${PR_NUMBER}](${PR_URL}) |" | |
| fi | |
| echo "| Ref | \`${REF}\` |" | |
| echo "| Head SHA | \`${HEAD_SHA}\` |" | |
| if [ -n "${RUN_URL}" ]; then | |
| echo "| Shadow run | ${RUN_URL} |" | |
| else | |
| echo "| Shadow run | (not yet visible — check the Actions tab) |" | |
| fi | |
| echo | |
| echo "_Shadow CI runs **fire-and-forget**: it does not appear in this PR's checks and never blocks the merge queue. Benchmark data is collected by \`scripts/namespace-benchmark.sh\`._" | |
| } >> "$GITHUB_STEP_SUMMARY" |