From bd0a14132ffea0218fd0a973793d99f1b317d9d6 Mon Sep 17 00:00:00 2001 From: Marco Falkenberg Date: Wed, 2 Sep 2026 10:47:38 +0200 Subject: [PATCH] ci: run every suite on Dependabot PRs and deploy their review apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps on a Dependabot PR, both rooted in the same fact: `secrets.*` in a Dependabot-triggered run resolves against the repository's *Dependabot* secret store, not the Actions one. **Review apps.** The images were always built and pushed; only the deploy job was skipped, and `cleanup-previews.yml` failed on every Dependabot merge ("Missing required environment variables", after the merge, where nobody looks). Both halves now run in a `workflow_run` from the default branch, which has the secrets — the same mechanism `dependabot-auto-merge.yml` uses. Neither new workflow ever checks out the PR's code: the PR reaches them only as the two images its build job pushed. The actor conditions are the exact complement of the guards in the two ordinary preview workflows, so a push a person makes onto a `dependabot/*` branch is still deployed there and not a second time here. The cleanup workflow skips its *steps* rather than its job for that actor: a job-level `if` concludes the run `skipped`, and only a run that completes is a dependable trigger. **Tests.** The full cross-version suite (both harnesses, every published version) was the one suite a Dependabot PR never ran — it was label-only, while visual has run unconditionally for a while. A dependency bump is exactly the change that moves rendered structure without touching a component, so it now runs there too, and `dependabot-auto-merge.yml` waits for both suites. Neither is a required status check, so this workflow is the only thing between a red run and a merge. The gate reads the verdicts back per workflow instead of trusting the payload, ordered by run `id` with `cancelled`/`skipped` dropped. One commit carries four to five runs of the same suite: Dependabot attaches its own labels right after opening the PR, each `labeled` event starts a run, and the concurrency group cancels the rest — with `created_at` equal to the second, and the surviving run not necessarily the highest id. Also corrects the rationale several of these workflows carried. They claimed a Dependabot-triggered run gets a read-only `GITHUB_TOKEN`; the run log says `PullRequests: write`, and the ghcr push works. Only the secrets are withheld. CONTRIBUTE.md now records both halves. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-previews.yml | 10 +- .../workflows/cleanup-previews-dependabot.yml | 107 ++++++++++++ .github/workflows/cleanup-previews.yml | 16 ++ .github/workflows/dependabot-auto-merge.yml | 61 ++++++- .../workflows/deploy-previews-dependabot.yml | 159 ++++++++++++++++++ .../workflows/test-cross-version-label.yml | 26 ++- .github/workflows/test-visual-label.yml | 15 +- AGENTS.md | 3 +- CONTRIBUTE.md | 42 ++++- .../remote-react-components/CONTRIBUTE.md | 5 + 10 files changed, 416 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/cleanup-previews-dependabot.yml create mode 100644 .github/workflows/deploy-previews-dependabot.yml diff --git a/.github/workflows/build-previews.yml b/.github/workflows/build-previews.yml index 8a00185cbf..2e9d875d72 100644 --- a/.github/workflows/build-previews.yml +++ b/.github/workflows/build-previews.yml @@ -130,9 +130,13 @@ jobs: packages: read pull-requests: write - # Preview deploy needs the MITTWALD_* secrets, which GitHub withholds from - # Dependabot-triggered runs and from forks. Skip there so those PRs don't - # fail on missing secrets (the build job above still validates the images). + # Preview deploy needs the MITTWALD_* secrets, which a Dependabot-triggered + # run resolves against the Dependabot secret store (empty there) and a fork + # does not get at all. Skip both so those PRs don't fail on missing secrets + # (the build job above still validates the images). + # A Dependabot PR is deployed all the same — by + # `deploy-previews-dependabot.yml`, which this run triggers via + # `workflow_run` and which does get the secrets. A fork's is not. if: > github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' && diff --git a/.github/workflows/cleanup-previews-dependabot.yml b/.github/workflows/cleanup-previews-dependabot.yml new file mode 100644 index 0000000000..1c6fffd27c --- /dev/null +++ b/.github/workflows/cleanup-previews-dependabot.yml @@ -0,0 +1,107 @@ +# Tears down the preview apps of a Dependabot PR. +# +# The counterpart to `deploy-previews-dependabot.yml`, and it exists for the +# same reason: Dependabot merges its own PRs, so the `closed` event is a +# Dependabot-triggered run, where `secrets.*` comes from the Dependabot store +# and the `MITTWALD_*` credentials are empty. That is not hypothetical — it is +# what every Dependabot merge did until this pair existed, failing on "Missing +# required environment variables" after the merge, where nobody looks. +# `cleanup-previews.yml` now skips its steps for that actor and completes green +# — this workflow is what its completion triggers, from the default branch, with +# the secrets. +# +# Unlike the deploy side there is no conclusion to filter on: the cleanup run +# reaches here having done nothing at all, which is the point. +name: Cleanup Preview Apps (Dependabot) + +on: + workflow_run: + workflows: ["Cleanup Preview Apps"] + types: + - completed + +# See the deploy workflow: the two are keyed the same so that queuing this +# cleanup cancels a deploy still in flight for the branch. +concurrency: + group: preview-dependabot-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + +jobs: + cleanup: + runs-on: ubuntu-latest + timeout-minutes: 10 + + # The actor test is the exact complement of `CLEAN_UP_HERE` in + # `cleanup-previews.yml`: a PR a person closes is cleaned up there, and only + # what Dependabot closed itself is left for this run. + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.actor.login == 'dependabot[bot]' && + github.event.workflow_run.head_repository.full_name == github.repository && + startsWith(github.event.workflow_run.head_branch, 'dependabot/') + + permissions: + contents: read + pull-requests: read # resolving the PR number for the branch + + steps: + - name: Resolve the pull request + id: pr + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ github.token }} + script: | + const run = context.payload.workflow_run; + const { owner, repo } = context.repo; + + // `workflow_run.pull_requests` covers open PRs; by the time a + // cleanup is triggered the PR is closed, so the lookup is the normal + // path here rather than the fallback it is on the deploy side. + let number = run.pull_requests?.[0]?.number; + if (!number) { + const { data } = await github.rest.pulls.list({ + owner, + repo, + state: "closed", + head: `${owner}:${run.head_branch}`, + sort: "created", + direction: "desc", + }); + number = data[0]?.number; + } + if (!number) { + const reason = "no closed PR found for this branch"; + core.info(`nothing to clean up: ${reason}`); + core.summary.addRaw(`Nothing to clean up for \`${run.head_branch}\`: ${reason}.`).write(); + return; + } + + core.setOutput("number", String(number)); + + - name: Checkout the default branch + if: steps.pr.outputs.number != '' + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - name: Install pnpm + if: steps.pr.outputs.number != '' + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - name: Setup Node.js + if: steps.pr.outputs.number != '' + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: "24" + cache: pnpm + + - name: Install dependencies + if: steps.pr.outputs.number != '' + run: pnpm install --frozen-lockfile + + - name: Cleanup preview environment + if: steps.pr.outputs.number != '' + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + MITTWALD_PROJECT_ID: ${{ secrets.MITTWALD_PROJECT_ID }} + MITTWALD_API_TOKEN: ${{ secrets.MITTWALD_API_TOKEN }} + run: | + pnpm tsx dev/cleanup-review.ts diff --git a/.github/workflows/cleanup-previews.yml b/.github/workflows/cleanup-previews.yml index 5ad77a0179..ea292a3e74 100644 --- a/.github/workflows/cleanup-previews.yml +++ b/.github/workflows/cleanup-previews.yml @@ -30,23 +30,39 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 + env: + # Dependabot merges its own PRs, so the `closed` event is a + # Dependabot-triggered run — where `secrets.*` resolves against the + # Dependabot store and the MITTWALD_* credentials arrive empty. The + # cleanup for them happens in + # `cleanup-previews-dependabot.yml`, which this run triggers via + # `workflow_run`. The steps are skipped rather than the job: a job-level + # `if` concludes the whole run `skipped`, and only a run that actually + # completes is a dependable trigger. + CLEAN_UP_HERE: ${{ github.actor != 'dependabot[bot]' }} + steps: - name: Checkout repository + if: env.CLEAN_UP_HERE == 'true' uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Install pnpm + if: env.CLEAN_UP_HERE == 'true' uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Setup Node.js + if: env.CLEAN_UP_HERE == 'true' uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: "24" cache: pnpm - name: Install dependencies + if: env.CLEAN_UP_HERE == 'true' run: pnpm install --frozen-lockfile - name: Cleanup preview environment + if: env.CLEAN_UP_HERE == 'true' env: PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 3ad11a274a..5a78c6cfdb 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -1,9 +1,10 @@ -# Auto-merges Dependabot PRs once the visual regression suite is green. +# Auto-merges Dependabot PRs once the visual regression and cross-version suites +# are green. # # Why this hangs off `workflow_run` instead of `pull_request`: # -# - `pull_request` on a Dependabot PR gets a READ-ONLY `GITHUB_TOKEN`, so it -# cannot comment. +# - `pull_request` fires when the PR changes, not when a suite finishes, so it +# never sees the verdict it is supposed to act on. # - `pull_request_target` would give a write token to a run that checks out # dependency code. This workflow never checks anything out. # - `workflow_run` runs from the default branch with a write token and only @@ -20,7 +21,10 @@ name: Dependabot auto-merge on: workflow_run: - workflows: ["Run Visual Regression Tests"] + # Both suites run unconditionally on a Dependabot PR, and either completing + # lands here — the one that arrives second is the run that finds them both + # green. + workflows: ["Run Visual Regression Tests", "Run Cross-Version Tests"] types: - completed @@ -28,9 +32,11 @@ permissions: {} jobs: auto-merge: - # Visual is the gate this workflow owns. The required `main` check is - # enforced by Dependabot when it processes the merge command, and by the - # ruleset, which does NOT grant Dependabot a bypass on status checks. + # Visual and cross-version are the gates this workflow owns: neither is a + # required status check, so nothing else stops a merge over a red one. The + # required `main` check is enforced by Dependabot when it processes the merge + # command, and by the ruleset, which does NOT grant Dependabot a bypass on + # status checks. if: > github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' && @@ -43,6 +49,7 @@ jobs: cancel-in-progress: false permissions: + actions: read # reads the gate suites' runs for the PR head contents: read pull-requests: write # comments the merge command @@ -124,6 +131,46 @@ jobs: return decline("the PR carries a major version update"); } + // The completion that triggered this run says nothing about the + // other suite, so check both against the PR head — including the one + // that just finished, whose verdict is read here rather than trusted + // from the payload. + const gates = ["Run Visual Regression Tests", "Run Cross-Version Tests"]; + const runsForHead = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { + owner, + repo, + head_sha: pr.head.sha, + }); + + for (const gate of gates) { + // One commit carries several runs of the same workflow: Dependabot + // applies its labels right after opening the PR, and every + // `labeled` event starts one that the concurrency group then + // cancels. Four of them, `created_at` equal to the second — so + // order by `id`, which is monotonic, and drop the superseded ones + // rather than let a cancelled sibling outvote the run that did + // the work. What is left is the newest run that means something: + // still going (wait for its own completion to land here), + // successful, or failed. + const [latest] = runsForHead + .filter((candidate) => candidate.name === gate) + .filter( + (candidate) => + candidate.conclusion !== "cancelled" && candidate.conclusion !== "skipped", + ) + .sort((a, b) => b.id - a.id); + + if (!latest) { + return decline(`${gate} has not run for ${pr.head.sha.slice(0, 7)}`); + } + if (latest.status !== "completed") { + return decline(`${gate} is still ${latest.status}`); + } + if (latest.conclusion !== "success") { + return decline(`${gate} concluded ${latest.conclusion}`); + } + } + // Every rebase produces a fresh run, so only skip a command that was // already issued for THIS head commit. const command = "@dependabot squash and merge"; diff --git a/.github/workflows/deploy-previews-dependabot.yml b/.github/workflows/deploy-previews-dependabot.yml new file mode 100644 index 0000000000..c9d874998a --- /dev/null +++ b/.github/workflows/deploy-previews-dependabot.yml @@ -0,0 +1,159 @@ +# Deploys the preview apps for Dependabot PRs. +# +# `build-previews.yml` builds and pushes both images on a Dependabot PR just +# like on any other, but its deploy job cannot run there: in a +# Dependabot-triggered run `secrets.*` resolves against the repository's +# *Dependabot* secret store, not the Actions one, so `MITTWALD_PROJECT_ID` and +# `MITTWALD_API_TOKEN` arrive empty and `deploy-review.ts` exits on the missing +# credentials. (The `GITHUB_TOKEN` itself is not the problem here — this +# repository does grant Dependabot runs the permissions a job requests.) A +# `workflow_run` runs from the default branch in an ordinary Actions context, +# where the secrets are what they are everywhere else — the same mechanism +# `dependabot-auto-merge.yml` uses. +# +# It only ever checks out the default branch. The PR's code reaches this run +# exclusively as the two images the build job already pushed, which is what +# makes the write token safe to hold here. Note the consequence: like every +# `workflow_run` workflow, this one is inert until it lands on `main`, so it +# cannot be exercised from the PR that introduces it. +name: Deploy Preview Apps (Dependabot) + +on: + workflow_run: + workflows: ["Build & Deploy Preview Apps"] + types: + - completed + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# Shared with `cleanup-previews-dependabot.yml`, the pair that matters for a +# Dependabot PR: queuing the cleanup cancels a deploy still running for the same +# branch, so the deploy can no longer create preview resources after the cleanup +# has already run. Keyed on the branch, not the PR number, because the cleanup +# side runs on a closed PR — where `workflow_run.pull_requests` comes back empty +# and no number is available to a concurrency expression. A Dependabot branch +# name is unique per PR. +concurrency: + group: preview-dependabot-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 15 + + # No images to deploy unless the build succeeded, and nothing to do unless + # the build was the one that could not deploy itself. The actor test is the + # exact complement of `github.actor != 'dependabot[bot]'` over there, so a + # push a person makes onto a `dependabot/*` branch is deployed by that job + # and not, a second time, by this one. + if: > + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.actor.login == 'dependabot[bot]' && + github.event.workflow_run.head_repository.full_name == github.repository && + startsWith(github.event.workflow_run.head_branch, 'dependabot/') + + permissions: + contents: read + packages: read + pull-requests: write # deploy-review.ts comments the preview URLs + + steps: + - name: Resolve the pull request + id: pr + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ github.token }} + script: | + const run = context.payload.workflow_run; + const { owner, repo } = context.repo; + + const skip = (reason) => { + core.info(`not deploying: ${reason}`); + core.summary.addRaw(`Not deploying \`${run.head_branch}\`: ${reason}`).write(); + }; + + // Populated for same-repo branches, which Dependabot branches are — + // the lookup is only a fallback. + let number = run.pull_requests?.[0]?.number; + if (!number) { + const { data } = await github.rest.pulls.list({ + owner, + repo, + state: "open", + head: `${owner}:${run.head_branch}`, + }); + number = data[0]?.number; + } + if (!number) { + return skip("no open PR found for this branch"); + } + + const { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: number, + }); + + // The branch prefix is not proof of authorship: anyone with write + // access can push a `dependabot/*` branch, and this run holds a + // write token. + if (pr.user.login !== "dependabot[bot]") { + return skip(`PR #${number} was opened by ${pr.user.login}`); + } + + // A merge or close may have landed while the build was running. + // Deploying now would orphan the preview: the cleanup already ran. + if (pr.state !== "open") { + return skip(`PR #${number} is ${pr.state}`); + } + + core.setOutput("number", String(number)); + + # Deliberately no `ref:` — a `workflow_run` checkout defaults to the + # default branch, and running the PR's own tooling is exactly what this + # workflow must not do. + - name: Checkout the default branch + if: steps.pr.outputs.number != '' + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - name: Install pnpm + if: steps.pr.outputs.number != '' + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - name: Setup Node.js + if: steps.pr.outputs.number != '' + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: "24" + cache: pnpm + + - name: Install dependencies + if: steps.pr.outputs.number != '' + run: pnpm install --frozen-lockfile + + - name: Deploy preview environment + if: steps.pr.outputs.number != '' + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + MITTWALD_PROJECT_ID: ${{ secrets.MITTWALD_PROJECT_ID }} + MITTWALD_API_TOKEN: ${{ secrets.MITTWALD_API_TOKEN }} + MITTWALD_TLS_CERTIFICATE_ID_DOCS: + ${{ vars.MITTWALD_TLS_CERTIFICATE_ID_DOCS }} + MITTWALD_TLS_CERTIFICATE_ID_STORYBOOK: + ${{ vars.MITTWALD_TLS_CERTIFICATE_ID_STORYBOOK }} + # The refs the build job pushed (`type=ref,event=pr` => `pr-`), + # derived the same way `build-previews.yml` derives them. + DOCS_IMAGE_TAG: + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/docs:pr-${{ + steps.pr.outputs.number }} + STORYBOOK_IMAGE_TAG: + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/storybook:pr-${{ + steps.pr.outputs.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + pnpm tsx dev/deploy-review.ts diff --git a/.github/workflows/test-cross-version-label.yml b/.github/workflows/test-cross-version-label.yml index 0613a7f67c..1521469792 100644 --- a/.github/workflows/test-cross-version-label.yml +++ b/.github/workflows/test-cross-version-label.yml @@ -5,15 +5,25 @@ on: pull_request: types: - labeled + # Dependabot PRs run both harnesses unconditionally rather than on a + # label, for the same two reasons as the visual suite: it is a gate + # `dependabot-auto-merge.yml` waits for, and labelling from a workflow + # would not work anyway — events created with `GITHUB_TOKEN` do not start + # new workflow runs. A dependency bump is exactly the change that moves + # rendered structure without touching a component. + - opened + - reopened + - synchronize jobs: run-cross-version-tests: runs-on: ubuntu-latest timeout-minutes: 45 if: > - github.event_name == 'workflow_dispatch' || (github.event.label.name == - 'run-cross-version-tests' && - github.head_ref != github.event.repository.default_branch) + github.event_name == 'workflow_dispatch' || + (github.head_ref != github.event.repository.default_branch && + (github.event.label.name == 'run-cross-version-tests' || + github.event.pull_request.user.login == 'dependabot[bot]')) # one at a time per branch; re-labeling supersedes an in-flight run concurrency: @@ -26,7 +36,10 @@ jobs: steps: - name: Remove label if exists - if: github.event_name == 'pull_request' + # Only the run that the label started has a label to remove. Not every + # `labeled` event does: Dependabot attaches its own labels right after + # opening a PR, and each of those starts a run of this workflow too. + if: github.event.label.name == 'run-cross-version-tests' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ github.token }} @@ -154,7 +167,10 @@ jobs: cat "$RUNNER_TEMP/cross-version-summary.md" >> "$GITHUB_STEP_SUMMARY" - name: Comment summary on PR - if: always() && github.event_name == 'pull_request' + # The comment answers a request. Nobody asked on a Dependabot PR — there + # the check run and its step summary are the signal, and + # `dependabot-auto-merge.yml` is what acts on the verdict. + if: always() && github.event.label.name == 'run-cross-version-tests' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ github.token }} diff --git a/.github/workflows/test-visual-label.yml b/.github/workflows/test-visual-label.yml index 6c17ad0b0b..20db3f099b 100644 --- a/.github/workflows/test-visual-label.yml +++ b/.github/workflows/test-visual-label.yml @@ -39,9 +39,11 @@ jobs: steps: - name: Remove label if exists - # Only the labelled path has a label to remove — and only that path may - # write: a `pull_request` event on a Dependabot PR gets a read-only - # `GITHUB_TOKEN`, so this call would 403 and fail the whole job. + # Only the labelled path has a label to remove. Note that a Dependabot + # PR takes this path as well: Dependabot attaches its own labels right + # after opening the PR, and each of those `labeled` events starts a run + # here too. Removing a label that is not there 404s, which the script + # swallows. if: github.event.action == 'labeled' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -175,9 +177,10 @@ jobs: cat "$RUNNER_TEMP/visual-summary.md" >> "$GITHUB_STEP_SUMMARY" - name: Comment summary on PR - # Same read-only-token reason as the label removal above. On a Dependabot - # PR the check run and its step summary are the signal; nothing - # auto-merges while it is red. + # Same path as the label removal above. The `synchronize` run of a + # Dependabot PR posts nothing — there the check run and its step summary + # are the signal, and `dependabot-auto-merge.yml` is what acts on the + # verdict. if: github.event.action == 'labeled' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: diff --git a/AGENTS.md b/AGENTS.md index 5619c94860..bf703e483c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,7 +190,8 @@ and commit the results. - **New dependencies:** pnpm enforces a `minimumReleaseAge` of one week (exempt: `@mittwald/*`) — brand-new versions won't resolve. - **Dependency updates run themselves.** Dependabot opens four grouped npm PRs a - week and merges them itself once CI and the visual suite are green — see + week and merges them itself once CI and the full visual and cross-version + suites are green — see [CONTRIBUTE.md § Dependency updates](CONTRIBUTE.md#dependency-updates). A deliberate hold belongs in `.github/dependabot.yml` as an `ignore` entry; a closed PR only makes it come back next week. diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index 8888d42b4b..0c4879f4f9 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -816,14 +816,44 @@ deliberately by bumping it manually. The moving parts: exempt). Nothing is left ungrouped on purpose: every merge invalidates `pnpm-lock.yaml` in every other open PR, which makes Dependabot rebase it and CI run again, so the number of open PRs is what drives that cascade. -- The **visual regression suite runs on every Dependabot PR**, not on a label. A - `playwright`, `vitest` or `react-aria` bump moves snapshots, and the label - route is not even available to a workflow: events created with `GITHUB_TOKEN` - do not start new workflow runs. +- The **visual regression and cross-version suites run on every Dependabot PR**, + not on a label. A `playwright`, `vitest` or `react-aria` bump moves snapshots + and rendered structure, and the label route is not even available to a + workflow: events created with `GITHUB_TOKEN` do not start new workflow runs. + Both are the full suites a human gets from the `run-visual-tests` / + `run-cross-version-tests` labels — for cross-version that means both harnesses + against every published version, not just the iframe smoke test `test.yml` + runs on all PRs. - [`dependabot-auto-merge.yml`](.github/workflows/dependabot-auto-merge.yml) - waits for that suite, re-checks the PR, and comments + waits for **both** suites, re-checks the PR, and comments `@dependabot squash and merge`. Dependabot merges once the required `main` - check is green. + check is green. Neither suite is a required status check, so this workflow is + the only thing standing between a red one and a merge. +- **Preview apps are deployed** for a Dependabot PR too, by + [`deploy-previews-dependabot.yml`](.github/workflows/deploy-previews-dependabot.yml) + and torn down by + [`cleanup-previews-dependabot.yml`](.github/workflows/cleanup-previews-dependabot.yml). + Both hang off a `workflow_run` for the same reason the auto-merge does: GitHub + withholds the `MITTWALD_*` secrets from Dependabot-triggered runs, so the + deploy and cleanup jobs in the two ordinary preview workflows skip that actor + and these two do the work from the default branch. They never check out the + PR's code — only the images its build job already pushed. + +**What a Dependabot-triggered run does and does not get.** Both halves are worth +knowing before you add a workflow that touches these PRs, and only one matches +the usual folklore: + +- **`secrets.*` resolves against the repository's _Dependabot_ secret store**, + not the Actions one. An Actions-only secret arrives as an empty string, and + the step fails wherever the script validates its environment. Put a secret a + Dependabot PR genuinely needs into the Dependabot store (`PUBLISH_PAT` is + there); otherwise move the work into a `workflow_run` that runs from the + default branch. +- **The `GITHUB_TOKEN` is not read-only here.** A job gets the permissions it + requests — `pull-requests: write` and `packages: write` both work on a + Dependabot PR in this repository, which is why the visual summary comment and + the ghcr push land. Don't design around a restriction that isn't in force, and + don't rely on it staying that way for anything that matters. **When one goes red.** Nothing merges. Find the culprit in the group, then either fix the code or park the dependency with diff --git a/packages/remote-react-components/CONTRIBUTE.md b/packages/remote-react-components/CONTRIBUTE.md index 2f093abc60..951f7c6b2b 100644 --- a/packages/remote-react-components/CONTRIBUTE.md +++ b/packages/remote-react-components/CONTRIBUTE.md @@ -406,3 +406,8 @@ structural divergence, so the version gates may need adjusting — a `minVersion`/`skipVersions` entry in `scenarioVersionSupport.ts` (see [Missing components and legitimate divergences](#missing-components-and-legitimate-divergences)). The label lets you find and fix that on the PR instead of after merge. + +**Dependabot PRs run both harnesses unconditionally** — no label, and a red run +blocks the auto-merge (`dependabot-auto-merge.yml`). A `react-aria` or +`react-dom` bump changes rendered structure without touching a component, which +is precisely the divergence class the PR job's single harness does not see.