|
| 1 | +# backport-dispatch.yml — OPTIONAL latency optimization for DEV-352's |
| 2 | +# auto-backport bot. Authored in atlas (release/public path), ships to the |
| 3 | +# PUBLIC repo via the snapshot per invariant I0 — this workflow only ever |
| 4 | +# runs on the public repo, never on atlas (see the repo guard below). |
| 5 | +# |
| 6 | +# NOT YET ARMED (2026-07-15): the atlas-side auto-backport.yml has a |
| 7 | +# scheduled SWEEP (every 30 min) that scans merged public PRs anonymously |
| 8 | +# and backports anything unabsorbed — the whole pipeline is fully |
| 9 | +# functional with ZERO public-repo credentials via that path alone. This |
| 10 | +# workflow instantly notifies atlas the moment a PR merges instead of |
| 11 | +# waiting for the next sweep (seconds vs. up to 30 minutes) — a nice-to-have, |
| 12 | +# not a requirement. |
| 13 | +# |
| 14 | +# Arming it means placing a GitHub App credential with contents:write on |
| 15 | +# the PRIVATE atlas repo into THIS PUBLIC repo's secrets — a real trust |
| 16 | +# decision (a compromised public-repo secret store would let an attacker |
| 17 | +# push to atlas) that is deliberately left to the operator, not decided by |
| 18 | +# this file. Until TESTSPRITE_HOB_APP_ID/TESTSPRITE_HOB_PRIVATE_KEY (or the |
| 19 | +# ALFHEIM_AGENT_* fallback) exist on the PUBLIC repo with contents:write on |
| 20 | +# atlas, this workflow no-ops cleanly (see the "not armed" step) — it does |
| 21 | +# NOT fail loud, so it stays quiet for every contributor watching the |
| 22 | +# Actions tab until the operator decides to enable it. |
| 23 | +# |
| 24 | +# SECURITY: this workflow reads ONLY event metadata (PR number, merge SHA, |
| 25 | +# base ref) — it never checks out the merged PR's code. That is what makes |
| 26 | +# `pull_request_target` safe here: the base-repo context (secrets, a |
| 27 | +# write-capable token) is required because a fork-originated merged PR gets |
| 28 | +# ZERO secrets under a plain `pull_request` trigger, even for the `closed` |
| 29 | +# activity type — but nothing in this job ever runs untrusted fork code, so |
| 30 | +# the classic pull_request_target RCE/secret-exfiltration risk (checking |
| 31 | +# out and executing the fork's own head ref under a privileged context) |
| 32 | +# does not apply. Do not add actions/checkout to this workflow. |
| 33 | +name: Notify atlas of a merged public PR |
| 34 | + |
| 35 | +on: |
| 36 | + pull_request_target: |
| 37 | + types: [closed] |
| 38 | + |
| 39 | +permissions: |
| 40 | + contents: read |
| 41 | + |
| 42 | +jobs: |
| 43 | + notify: |
| 44 | + if: github.repository == 'TestSprite/testsprite-cli' && github.event.pull_request.merged == true |
| 45 | + runs-on: ubuntu-latest |
| 46 | + steps: |
| 47 | + - id: app-token |
| 48 | + continue-on-error: true |
| 49 | + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 |
| 50 | + with: |
| 51 | + app-id: ${{ secrets.TESTSPRITE_HOB_APP_ID || secrets.ALFHEIM_AGENT_APP_ID }} |
| 52 | + private-key: ${{ secrets.TESTSPRITE_HOB_PRIVATE_KEY || secrets.ALFHEIM_AGENT_PRIVATE_KEY }} |
| 53 | + owner: TestSprite |
| 54 | + repositories: testsprite-cli-atlas |
| 55 | + # Minimum required for POST /repos/{owner}/{repo}/dispatches (verified |
| 56 | + # against docs.github.com/en/rest/using-the-rest-api/permissions-required-for-github-apps — |
| 57 | + # the endpoint is gated on Contents:write, not Contents:read). |
| 58 | + permission-contents: write |
| 59 | + |
| 60 | + - name: Dispatch to atlas |
| 61 | + if: steps.app-token.outputs.token != '' |
| 62 | + env: |
| 63 | + GH_TOKEN: ${{ steps.app-token.outputs.token }} |
| 64 | + # These two fields are fork-influenced (merge_commit_sha/base.ref come |
| 65 | + # from a merged PR's event payload) — routed through env instead of |
| 66 | + # spliced into the run: string to avoid the GHA script-injection class |
| 67 | + # (a maliciously-crafted value could otherwise break out of the -F |
| 68 | + # argument and inject arbitrary shell). PR_NUMBER is left inline: it is |
| 69 | + # a GitHub-assigned integer, not attacker-authorable content. |
| 70 | + MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} |
| 71 | + BASE_REF: ${{ github.event.pull_request.base.ref }} |
| 72 | + run: | |
| 73 | + set -euo pipefail |
| 74 | + gh api repos/TestSprite/testsprite-cli-atlas/dispatches \ |
| 75 | + -f event_type=public-pr-merged \ |
| 76 | + -F 'client_payload[pr_number]=${{ github.event.pull_request.number }}' \ |
| 77 | + -F "client_payload[merge_commit_sha]=$MERGE_SHA" \ |
| 78 | + -F "client_payload[base_ref]=$BASE_REF" |
| 79 | +
|
| 80 | + - name: Not armed yet — the atlas sweep will pick this up |
| 81 | + if: steps.app-token.outputs.token == '' |
| 82 | + run: | |
| 83 | + echo "::notice::No cross-repo credential configured on this repo yet — atlas's scheduled sweep (auto-backport.yml, every 30 min) will pick up this merge instead of an instant dispatch. See DEV-352 / DEV-348 / DEV-350 for the operator decision to arm this." |
0 commit comments