Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 152 additions & 10 deletions .github/workflows/ci-namespace-shadow.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,168 @@
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
Comment thread
bsgrigorov marked this conversation as resolved.
# 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'
- "docs/**"
- "**/*.md"
- ".github/CODEOWNERS"
push:
branches: [main]
schedule:
- cron: '0 * * * *'
- cron: "0 * * * *"
workflow_dispatch:

concurrency:
group: ns-shadow-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
id-token: write

jobs:
shadow-ci:
name: '[shadow] CI'
uses: ./.github/workflows/ci.yml
with:
runner_provider: namespace
secrets: inherit
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
Comment thread
bsgrigorov marked this conversation as resolved.
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"
29 changes: 26 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: ci

run-name: >-
${{ inputs.pr_number && format('ci [shadow PR #{0} @ {1}]', inputs.pr_number, inputs.head_sha) || '' }}

on:
push:
branches: [main]
Expand All @@ -19,6 +22,14 @@ on:
type: string
required: false
default: current
pr_number:
description: "PR number (shadow correlation, optional)"
type: string
required: false
head_sha:
description: "PR head SHA (shadow correlation, optional)"
type: string
required: false
workflow_dispatch:
inputs:
runner_provider:
Expand All @@ -29,6 +40,14 @@ on:
- current
- namespace
default: current
pr_number:
description: "PR number (shadow correlation, optional)"
required: false
type: string
head_sha:
description: "PR head SHA (shadow correlation, optional)"
required: false
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }}
Expand Down Expand Up @@ -124,6 +143,7 @@ jobs:
retry_wait_seconds: 30
command: yarn install --immutable
- name: Post build-source-hash commit status
if: ${{ inputs.runner_provider != 'namespace' }}
id: publish
uses: ./.github/actions/post-build-source-hash
with:
Expand Down Expand Up @@ -466,7 +486,7 @@ jobs:
EOF

- name: Post iOS JS bundle size to commit status
if: ${{ github.ref == 'refs/heads/main' }}
if: ${{ github.ref == 'refs/heads/main' && inputs.runner_provider != 'namespace' }}
env:
GITHUB_TOKEN: ${{ github.token }}
shell: bash
Expand Down Expand Up @@ -515,7 +535,9 @@ jobs:
name: Ship JS bundle size check
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
needs: [js-bundle-size-check]
if: ${{ github.ref == 'refs/heads/main' }}
# Skip on Namespace shadow runs: hourly cron against main would otherwise
# push duplicate entries to the external mobile_bundlesize_stats repo.
if: ${{ github.ref == 'refs/heads/main' && inputs.runner_provider != 'namespace' }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
Expand Down Expand Up @@ -975,7 +997,7 @@ jobs:
github-token: ${{ github.token }}
pr-number: ${{ github.event.pull_request.number }}
repository: ${{ github.repository }}
post-comment: 'true'
post-comment: ${{ inputs.runner_provider != 'namespace' }}
base-ref: ${{ github.event.pull_request.base.ref }}

build-android-apks:
Expand Down Expand Up @@ -1115,6 +1137,7 @@ jobs:
path: fixture-results/

- name: Report results
if: ${{ inputs.runner_provider != 'namespace' }}
env:
RESULTS_PATH: fixture-results
VALIDATION_RESULT: ${{ needs.validate-e2e-fixtures.result }}
Expand Down
Loading