feat(repos): add manifest parser and validation for repos.yaml #5005
Workflow file for this run
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: E2E Tests | |
| # PR-triggered e2e uses pull_request_target so fork PRs receive secrets. | |
| # Authorization runs in a separate gate job (base checkout only) before the e2e | |
| # job checks out the PR head — see gate/e2e job comments for why this is split. | |
| permissions: {} | |
| on: | |
| push: | |
| branches: [main] | |
| # SYNC-WITH: grep in "Check for e2e-relevant changes" (e2e job) and | |
| # "Check for behaviour-relevant changes" (behaviour job). push.paths is the | |
| # union of both filters (plus **/*.go); each job grep may be narrower. | |
| paths: | |
| - '**/*.go' | |
| - 'go.mod' | |
| - 'go.sum' | |
| - 'e2e/**' | |
| - 'internal/scaffold/fullsend-repo/**' | |
| - 'internal/security/hooks/**' | |
| - 'internal/dispatch/gcf/mintsrc/**' | |
| - 'internal/sentencetoken/english.json' | |
| - 'internal/runtime/**' | |
| - 'internal/sandbox/**' | |
| - 'internal/config/**' | |
| - 'internal/cli/github.go' | |
| - 'internal/cli/run.go' | |
| - 'internal/layers/**' | |
| - 'internal/forge/**' | |
| - 'internal/harness/**' | |
| - 'internal/dispatch/**' | |
| - 'internal/mintclient/**' | |
| - 'cmd/fullsend/**' | |
| - 'Makefile' | |
| - '.github/scripts/**' | |
| - '.github/workflows/e2e.yml' | |
| - 'action.yml' | |
| - '.github/actions/check-e2e-authorization/**' | |
| - 'scripts/check-e2e-authorization.sh' | |
| pull_request_target: | |
| types: [opened, synchronize, reopened, labeled] | |
| merge_group: | |
| workflow_dispatch: | |
| concurrency: | |
| group: >- | |
| ${{ github.event_name == 'pull_request_target' | |
| && format('e2e-{0}', github.event.pull_request.number) | |
| || format('{0}-{1}', github.workflow, github.ref) }} | |
| cancel-in-progress: >- | |
| ${{ github.event_name == 'pull_request_target' | |
| || github.ref != 'refs/heads/main' }} | |
| jobs: | |
| gate: | |
| # Separate job (not steps in e2e) so pull-requests: write stays out of the | |
| # job that checks out fork head and runs make e2e-test with secrets. | |
| # Never checkout github.event.pull_request.head.sha here. | |
| if: >- | |
| github.event_name == 'pull_request_target' && | |
| (github.event.action != 'labeled' || github.event.label.name == 'ok-to-test') | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| outputs: | |
| authorized: ${{ steps.auth.outputs.authorized }} | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.sha }} # Base branch only — never checkout PR head in gate | |
| - name: Check PR authorization | |
| id: auth | |
| uses: ./.github/actions/check-e2e-authorization | |
| with: | |
| pr_number: ${{ github.event.pull_request.number }} | |
| repository: ${{ github.repository }} | |
| pr_updated_at: ${{ github.event.pull_request.updated_at }} | |
| event_action: ${{ github.event.action }} | |
| pr_author_association: ${{ github.event.pull_request.author_association }} | |
| pr_author_login: ${{ github.event.pull_request.user.login }} | |
| e2e: | |
| # For pull_request_target, runs only when gate sets authorized=true. | |
| # Do not treat a skipped gate as authorized (e.g. labeled events for non-ok-to-test labels). | |
| # This job checks out untrusted PR head code — no pull-requests: write here. | |
| needs: gate | |
| if: >- | |
| !cancelled() && | |
| (github.event_name != 'pull_request_target' || needs.gate.outputs.authorized == 'true') | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Check for e2e-relevant changes | |
| id: changes | |
| if: github.event_name == 'pull_request_target' || github.event_name == 'merge_group' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| MERGE_GROUP_BASE: ${{ github.event.merge_group.base_sha }} | |
| MERGE_GROUP_HEAD: ${{ github.event.merge_group.head_sha }} | |
| # SYNC-WITH: push.paths filter above | |
| run: | | |
| if [ "$EVENT_NAME" = "merge_group" ]; then | |
| FILES=$(gh api "repos/${REPO}/compare/${MERGE_GROUP_BASE}...${MERGE_GROUP_HEAD}" --jq '.files[].filename') || { | |
| echo "::warning::Failed to fetch merge group files — running e2e tests as a precaution" | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| } | |
| FILE_COUNT=$(echo "$FILES" | wc -l) | |
| if [ "$FILE_COUNT" -ge 300 ]; then | |
| echo "::warning::Compare API returned $FILE_COUNT files (possible truncation at 300) — running e2e tests as a precaution" | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| else | |
| FILES=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename') || { | |
| echo "::warning::Failed to fetch PR files — running e2e tests as a precaution" | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| } | |
| fi | |
| if echo "$FILES" | grep -qE '\.go$|^go\.(mod|sum)$|^e2e/|^internal/scaffold/fullsend-repo/|^internal/security/hooks/|^internal/dispatch/gcf/mintsrc/|^internal/sentencetoken/english\.json$|^Makefile$|^\.github/scripts/|^\.github/workflows/e2e\.yml$|^action\.yml$|^\.github/actions/check-e2e-authorization/|^scripts/check-e2e-authorization\.sh$'; then | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::notice::No e2e-relevant files changed — skipping tests" | |
| echo "relevant=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| if: steps.changes.outputs.relevant != 'false' | |
| with: | |
| ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} | |
| persist-credentials: false | |
| # checkout@v7 blocks fork PR head checkouts on pull_request_target by default. | |
| # Safe here: gate job authorizes before this job runs; no pull-requests: write. | |
| allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }} | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | |
| if: steps.changes.outputs.relevant != 'false' | |
| with: | |
| go-version-file: go.mod | |
| - name: Authenticate to GCP | |
| if: steps.changes.outputs.relevant != 'false' | |
| uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 | |
| with: | |
| workload_identity_provider: ${{ secrets.E2E_GCP_WIF_PROVIDER }} | |
| service_account: ${{ secrets.E2E_GCP_SERVICE_ACCOUNT }} | |
| - name: Run e2e tests | |
| if: steps.changes.outputs.relevant != 'false' | |
| run: make e2e-test | |
| env: | |
| E2E_SCREENSHOT_DIR: ${{ runner.temp }}/e2e-screenshots | |
| E2E_GCP_PROJECT_ID: ${{ secrets.E2E_GCP_PROJECT_ID }} | |
| - name: Upload debug screenshots | |
| if: always() && steps.changes.outputs.relevant != 'false' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: e2e-screenshots-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.run_id }} | |
| path: ${{ runner.temp }}/e2e-screenshots/ | |
| if-no-files-found: ignore | |
| retention-days: 5 | |
| behaviour: | |
| # Same gate authorization as e2e — checks out untrusted PR head with secrets. | |
| needs: gate | |
| if: >- | |
| !cancelled() && | |
| (github.event_name != 'pull_request_target' || needs.gate.outputs.authorized == 'true') | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Check for behaviour-relevant changes | |
| id: changes | |
| if: github.event_name == 'pull_request_target' || github.event_name == 'merge_group' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| MERGE_GROUP_BASE: ${{ github.event.merge_group.base_sha }} | |
| MERGE_GROUP_HEAD: ${{ github.event.merge_group.head_sha }} | |
| # SYNC-WITH: push.paths behaviour entries above | |
| run: | | |
| if [ "$EVENT_NAME" = "merge_group" ]; then | |
| FILES=$(gh api "repos/${REPO}/compare/${MERGE_GROUP_BASE}...${MERGE_GROUP_HEAD}" --jq '.files[].filename') || { | |
| echo "::warning::Failed to fetch merge group files — running behaviour tests as a precaution" | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| } | |
| FILE_COUNT=$(echo "$FILES" | wc -l) | |
| if [ "$FILE_COUNT" -ge 300 ]; then | |
| echo "::warning::Compare API returned $FILE_COUNT files (possible truncation at 300) — running behaviour tests as a precaution" | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| else | |
| FILES=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename') || { | |
| echo "::warning::Failed to fetch PR files — running behaviour tests as a precaution" | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| } | |
| fi | |
| # e2e/admin: behaviour suite reuses exported admin test helpers | |
| if echo "$FILES" | grep -qE '^e2e/behaviour/|^e2e/admin/|^internal/runtime/|^internal/sandbox/|^internal/config/|^internal/cli/|^internal/layers/|^internal/scaffold/fullsend-repo/|^internal/forge/|^internal/harness/|^internal/dispatch/|^internal/security/hooks/|^internal/mintclient/|^cmd/fullsend/|^go\.(mod|sum)$|^Makefile$|^\.github/workflows/e2e\.yml$|^\.github/actions/check-e2e-authorization/|^scripts/check-e2e-authorization\.sh$'; then | |
| echo "relevant=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::notice::No behaviour-relevant files changed — skipping behaviour tests" | |
| echo "relevant=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| if: steps.changes.outputs.relevant != 'false' | |
| with: | |
| ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} | |
| persist-credentials: false | |
| allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }} | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | |
| if: steps.changes.outputs.relevant != 'false' | |
| with: | |
| go-version-file: go.mod | |
| - name: Authenticate to GCP | |
| if: steps.changes.outputs.relevant != 'false' | |
| uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 | |
| with: | |
| workload_identity_provider: ${{ secrets.E2E_GCP_WIF_PROVIDER }} | |
| service_account: ${{ secrets.E2E_GCP_SERVICE_ACCOUNT }} | |
| - name: Run behaviour tests | |
| if: steps.changes.outputs.relevant != 'false' | |
| run: make behaviour-test | |
| env: | |
| BEHAVIOUR_SCM: github | |
| BEHAVIOUR_CI: githubactions | |
| BEHAVIOUR_INSTALL_MODE: per-repo | |
| BEHAVIOUR_ARTIFACT_DIR: ${{ runner.temp }}/behaviour-artifacts | |
| E2E_GCP_PROJECT_ID: ${{ secrets.E2E_GCP_PROJECT_ID }} | |
| E2E_GCP_WIF_PROVIDER: ${{ secrets.E2E_GCP_WIF_PROVIDER }} | |
| - name: Upload behaviour debug artifacts | |
| if: failure() && steps.changes.outputs.relevant != 'false' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: behaviour-artifacts-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.run_id }} | |
| path: ${{ runner.temp }}/behaviour-artifacts/ | |
| if-no-files-found: ignore | |
| retention-days: 5 |