From 10f50f155cd6f2869febe48e69c9b88ea981819f Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Thu, 25 Jun 2026 14:07:18 +0000 Subject: [PATCH] publish: deploy tags via a workflow_dispatch trampoline (fix tag-after-merge wedge) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A release tagged on the merge commit shares the default branch's just-deployed SHA. actions/deploy-pages stamps pages_build_version = GITHUB_SHA, which can't be changed (no input; a custom value 404s, server-validated against the OIDC commit claim — actions/deploy-pages#383). The Pages backend then silently DROPS a second deploy of an already-deployed SHA on a non-workflow_dispatch event: it reports success and flips the deployment record active, but the origin keeps serving the first artifact. So an inline tag deploy "succeeds" while the site stays on the pre-tag build — the recurring tag-after-merge wedge. Confirmed 2026-06-25 + actions/deploy-pages#383's controlled matrix (same SHA, identical pages_build_version): a workflow_dispatch deploy of that SHA is the one event type that FORCES the origin to re-serve. It's event-type, not timing. Fix: tags don't deploy inline. - ci.yml: the `publish` job now excludes tags (ref_type != 'tag'); a new `publish-tag` job (needs [release], actions: write) re-triggers publish.yml via `gh workflow run --ref ` (workflow_dispatch). The dispatched run re-gathers from durable sources (incl. the new release) and deploys, re-serving. - publish.yml: revert to stock actions/deploy-pages (the env-override on main was a no-op — GITHUB_SHA is protected; the direct-API custom build version 404s). Keep the post-deploy origin Verify step as the backstop. - main/internal-PR deploys are the first of their SHA, so they serve fine inline and keep PR/commit visibility. Downstream: consumers can't dispatch publish.yml cross-repo, so they trampoline via a small publish-dispatch.yml wrapper (also the fork-preview opt-in). Docs updated: tutorial (required wrapper + publish-tag job), architecture (the mechanism), workflow reference, and CLAUDE.md. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 47 ++++++++++++--- .github/workflows/publish.yml | 34 +++++------ CLAUDE.md | 75 +++++++++++++++++------- docs/explanations/architecture.md | 48 +++++++++++---- docs/reference/workflows.md | 11 +++- docs/tutorials/adding-to-a-fresh-repo.md | 62 ++++++++++++++------ 6 files changed, 199 insertions(+), 78 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e312085..f214072 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,10 +5,12 @@ name: CI # publish the release assets. It uploads each build's `docs` artifact. # # Publishing is nested here (the `publish` job → publish.yml) so its status is -# visible on the PR / commit, but ONLY for internal events: a fork PR's build runs -# with a read-only token and must never deploy, so the `publish` job is skipped for -# it (the `docs` reusable workflow points a maintainer at the manual opt-in -# instead). See the architecture explanation in docs/. +# visible on the PR / commit, but ONLY for internal NON-TAG events: a fork PR's build +# runs with a read-only token and must never deploy, so the `publish` job is skipped +# for it (the `docs` reusable workflow points a maintainer at the manual opt-in +# instead). TAGS deploy via the `publish-tag` trampoline (workflow_dispatch) instead +# of inline — a same-SHA tag deploy would otherwise be silently dropped by Pages. See +# the architecture explanation in docs/. on: pull_request: push: @@ -36,14 +38,17 @@ jobs: permissions: contents: write # create the GitHub Release + attach assets - # Internal events only: an internal PR, or a push to main/tag, has a same-repo - # build we can trust + deploy. publish.yml reconstructs the whole site and - # injects this build's `docs` artifact for `docs.version-name`. Fork PRs (head - # repo ≠ this repo) are excluded — docs.yml's build job warns them instead. + # Internal NON-TAG events (an internal PR or a push to main): a same-repo build we + # can trust + deploy inline, with the publish status visible on the PR/commit. These + # are the FIRST deploy of their SHA, so the Pages backend serves them fine. + # publish.yml reconstructs the whole site and injects this build's `docs` artifact + # for `docs.version-name`. Fork PRs (head repo ≠ this repo) are excluded — docs.yml's + # build job warns them instead. TAGS are excluded too: see publish-tag below. publish: needs: [lint, test, docs] if: >- github.repository == 'DiamondLightSource/myst-version-switcher-plugin' && + github.ref_type != 'tag' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository ) uses: ./.github/workflows/publish.yml @@ -55,3 +60,29 @@ jobs: pages: write id-token: write statuses: write + + # Tags can't deploy inline. A release tag is cut on the merge commit, so it shares + # the default branch's just-deployed SHA — and the Pages backend silently DROPS a + # second same-SHA deploy on a non-`workflow_dispatch` event (reports success, keeps + # serving the old artifact; see publish.yml). The escape hatch is that a + # `workflow_dispatch` deploy of that SHA forces a re-serve. So instead of nesting + # publish.yml inline, re-trigger it as a workflow_dispatch (after `release`, so the + # dispatched run gathers the new release's docs.zip). The dispatched run does the + # deploy + verify; its status shows on its own run rather than this tag's CI, which + # is fine — a tag has no PR to annotate. (Consumers do the same by dispatching their + # fork-preview wrapper instead — see the how-to.) + publish-tag: + needs: [lint, test, docs, release] + if: >- + github.repository == 'DiamondLightSource/myst-version-switcher-plugin' && + github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: + actions: write # dispatch publish.yml + steps: + - name: Re-deploy via workflow_dispatch (tag trampoline) + env: + GH_TOKEN: ${{ github.token }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + gh workflow run publish.yml --repo "$GITHUB_REPOSITORY" --ref "$DEFAULT_BRANCH" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2bef352..393355c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -146,31 +146,31 @@ jobs: with: path: ${{ steps.site.outputs.dir }} - # deploy-pages stamps the deployment with pages_build_version = GITHUB_SHA - # (src/internal/context.js: `buildVersion: process.env.GITHUB_SHA`). The Pages - # backend time-windows-dedups on that build version: a deploy reusing a build - # version already deployed within the window is acked but DROPPED — the origin - # keeps the prior artifact while deploy-pages still reports success. A release - # tagged on the merge commit reuses the just-deployed default-branch SHA within - # that window, so its (correct) artifact is silently dropped — the "publish - # wedged after tag-after-merge" failure, confirmed 2026-06-25 (same SHA across - # main/tag/manual deploys; only the tag, ~2.5min behind main, wedged). Override - # GITHUB_SHA for this step alone so each run posts a UNIQUE build version — no - # collision, immediate serve. context.js reads GITHUB_SHA ONLY for buildVersion, - # so nothing else in the action is affected. + # Deploy the uploaded artifact. deploy-pages stamps the deployment with + # pages_build_version = GITHUB_SHA and has no input to change it; the value is + # server-validated against the OIDC commit claim, so it CANNOT be overridden + # (a custom value 404s — actions/deploy-pages#383). The Pages backend silently + # DROPS a *second* deploy of an already-deployed SHA on a non-`workflow_dispatch` + # event (it reports success but the origin keeps serving the first artifact), + # EXCEPT a `workflow_dispatch` deploy of that SHA, which forces a re-serve. A + # release tag is cut on the merge commit, so it shares the default branch's + # just-deployed SHA → an inline tag deploy would be dropped. The fix lives in the + # CALLER: tags don't deploy inline — they re-trigger this workflow via + # `workflow_dispatch` (see ci.yml's tag trampoline / the consumer how-to), which + # re-serves. main/internal-PR deploys are the FIRST of their SHA, so they serve + # fine inline. The Verify step below backstops any residual stale-origin case. - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 - env: - GITHUB_SHA: ${{ github.sha }}-${{ github.run_id }} # deploy-pages reports success once the deployment RECORD flips active — even # if the Pages backend silently fails to update the SERVED origin (a wedged # site can serve a stale artifact indefinitely; see the 2026-06 incident where # a gh-pages→Actions cutover froze the origin while every deploy reported OK). - # The unique build version above prevents the tag-after-merge dedup wedge; this - # is the backstop that turns any *other* stale-origin failure into a red check - # instead of silently serving old docs. Confirm the live switcher.json + # The caller's tag trampoline (deploy via workflow_dispatch) prevents the + # tag-after-merge drop; this is the backstop that turns any *other* stale-origin + # failure into a red check instead of silently serving old docs. Confirm the + # live switcher.json # byte-matches the one we just assembled, polling up to 60s for the origin/CDN # to propagate; on failure, dump the origin headers (a frozen etag/last-modified # is the wedge signature). diff --git a/CLAUDE.md b/CLAUDE.md index ab01a6a..b1c6c0e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,8 +16,8 @@ scripts/migrate.sh # operator gh-pages → durable-s test/ # npm test suite (node, no framework) docs/ # this repo's own docs (dogfoods the plugin) .github/workflows/docs.yml # PUBLIC reusable: build at the versioned BASE_URL → pack docs.zip → upload `docs` artifact (workflow_call; build-command input) -.github/workflows/publish.yml # PUBLIC reusable: run assemble/ (checked out at job.workflow_sha) + deploy to Pages (PRIVILEGED): workflow_call (internal) + workflow_dispatch (fork-PR opt-in) -.github/workflows/ci.yml # this repo's own entry: _lint / _test / docs.yml / _release, then nests publish.yml for internal events +.github/workflows/publish.yml # PUBLIC reusable: run assemble/ (checked out at job.workflow_sha) + deploy to Pages (PRIVILEGED): workflow_call (internal non-tag) + workflow_dispatch (tag trampoline + fork-PR opt-in) +.github/workflows/ci.yml # this repo's own entry: _lint / _test / docs.yml / _release, then nests publish.yml for internal non-tag events (tags trampoline via publish-tag → workflow_dispatch) ``` ## Two halves, different lifecycles @@ -66,15 +66,19 @@ gathered (a merged/closed PR, a deleted release) is correctly dropped — no ### Split build (unprivileged) from publish (privileged), but nest publish for visibility `ci.yml` builds + uploads the `docs` artifact for every event including fork PRs. It then nests the privileged `publish.yml` (`workflow_call`) as a `publish` job — -but **only for internal events** (an internal PR, or a push to main/tag): the job's -`if` excludes fork PRs (`head.repo.full_name != github.repository`), whose builds -run with a read-only token and must never deploy. So publish status is **visible on -the PR/commit** for internal work, while untrusted fork-PR code still never reaches -a write token. A fork PR instead gets a warning step in `docs.yml`'s build job that -links the manual opt-in (`publish.yml`'s `workflow_dispatch`). - -Because publish now runs **inside the build's own CI run**, the current build isn't -a *completed* successful run the gather can discover (and for a main/tag push the +but **only for internal, non-tag events** (an internal PR, or a push to main): the +job's `if` excludes fork PRs (`head.repo.full_name != github.repository`), whose +builds run with a read-only token and must never deploy. So publish status is +**visible on the PR/commit** for internal work, while untrusted fork-PR code still +never reaches a write token. A fork PR instead gets a warning step in `docs.yml`'s +build job that links the manual opt-in (`publish.yml`'s `workflow_dispatch`). +**Tags don't nest** — a tag shares the merge commit's SHA and GitHub Pages drops a +second same-SHA deploy unless it's a `workflow_dispatch` (deploy-pages#383), so the +`publish-tag` job re-triggers `publish.yml` via `workflow_dispatch` instead (see +the Publish flow below). + +Because the inline publish runs **inside the build's own CI run**, the current build +isn't a *completed* successful run the gather can discover (and for a main push the gather would find the *previous* build). So `ci.yml` passes `publish.yml` the build's version name (`needs.docs.outputs.version-name`); `publish.yml` hands it to assemble as `ARTIFACT_VERSION_NAME`. `publish.yml` downloads the same-run `docs` artifact and @@ -147,9 +151,15 @@ One entry workflow (`ci.yml`) that nests the privileged publish: head.repo.full_name == github.repository)`); grants `pages`/`id-token`/`statuses` perms to the call and passes `version-name: needs.docs.outputs.version-name`. The fork-PR warning is a step inside `docs.yml`'s build job (not a separate ci.yml - job), keeping the top-level workflow lean. + job), keeping the top-level workflow lean. The `publish` job's `if` also excludes + **tags** (`ref_type != 'tag'`) — a separate `publish-tag` job (tags only, `needs: + [_release]`, `actions: write`) re-triggers `publish.yml` via + `gh workflow run publish.yml --ref ` (`workflow_dispatch`), because + a same-SHA tag deploy is silently dropped by Pages unless it's a dispatch event + (deploy-pages#383). - `publish.yml` — **assemble + deploy, privileged.** `workflow_call` (the nested - internal path) + `workflow_dispatch` (the fork-PR opt-in). Sparse-checks-out this + internal non-tag path) + `workflow_dispatch` (the tag trampoline + the fork-PR + opt-in). Sparse-checks-out this repo's `assemble/` at `job.workflow_sha` (so the scripts match the ref the consumer pinned `publish.yml@` to — see "Self-referencing the assemble scripts" below), runs `assemble.sh`, then `upload-pages-artifact` + `deploy-pages`, carrying the @@ -175,16 +185,25 @@ Sub-workflows of `ci.yml`: - `_release.yml` — attaches `version-switcher.mjs` + the tag's `docs.zip` (the `docs` artifact, verbatim — no repack) as Release assets (tag-only). -**Publish flow.** Internal PRs, `main`, and tags publish as part of the same CI run -once lint/test/docs pass (`ci.yml`'s `publish` job → `publish.yml` injects this -build + `assemble` gathers `main`, releases, and every other open PR → deploy), so -the deploy is a visible check on the PR/commit. An **external fork PR** builds + +**Publish flow.** Internal PRs and `main` publish as part of the same CI run once +lint/test/docs pass (`ci.yml`'s `publish` job → `publish.yml` injects this build + +`assemble` gathers `main`, releases, and every other open PR → deploy), so the deploy +is a visible check on the PR/commit. **Tags are the exception:** a release tag shares +the merge commit's SHA, and GitHub Pages silently drops a second deploy of an +already-deployed SHA *unless* the event is `workflow_dispatch` +([`actions/deploy-pages#383`](https://github.com/actions/deploy-pages/issues/383)) — +so the `publish-tag` job (after `_release`) re-triggers `publish.yml` via +`gh workflow run` (`workflow_dispatch`), which re-gathers from durable sources (incl. +the new release) and deploys with the origin actually updating. The dispatched run +carries the deploy status (not the tag's CI run). An **external fork PR** builds + verifies in CI but does **not** auto-publish (its `publish` job is skipped); a maintainer runs `publish.yml` with the PR number (`workflow_dispatch` → `pr`), which sets a `preview-approved` commit status pinned to that **head SHA** and assembles — so a later push to the PR (new SHA) drops the preview until re-approved. `assemble` gathers an open PR's artifact via its head SHA; internal PRs always, -fork PRs only when the SHA carries that status. +fork PRs only when the SHA carries that status. Consumers can't dispatch `publish.yml` +cross-repo, so they trampoline via a small `publish-dispatch.yml` wrapper (which also +serves the fork-preview opt-in) — see the how-to. `mystmd` is pinned at `1.10.1` (not `latest`). @@ -245,23 +264,35 @@ jobs: uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/docs.yml@ with: build-command: make docs # or: tox -e docs / npx … myst build / npm ci && npm run docs - publish: # internal events only (fork PRs excluded by the if) + publish: # internal, non-tag events (fork PRs + tags excluded by the if) needs: [docs] if: >- github.repository == 'ORG/REPO' && + github.ref_type != 'tag' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository ) uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/publish.yml@ with: version-name: ${{ needs.docs.outputs.version-name }} permissions: { pages: write, id-token: write, contents: read, actions: read, statuses: write } + publish-tag: # tags: re-deploy via workflow_dispatch (see how-to) + needs: [docs, release] + if: github.repository == 'ORG/REPO' && github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: { actions: write } + steps: + - env: { GH_TOKEN: "${{ github.token }}", DEFAULT_BRANCH: "${{ github.event.repository.default_branch }}" } + run: gh workflow run publish-dispatch.yml --repo "$GITHUB_REPOSITORY" --ref "$DEFAULT_BRANCH" ``` Add a `_release.yml`-style job that attaches each tag's built `docs.zip` (bare `html/` root) + the plugin as Release assets so `assemble` can reconstruct released -versions, and a small `workflow_dispatch` wrapper around `publish.yml` for the -fork-PR opt-in (reusable workflows can't be dispatched cross-repo). See the full -how-to + snippets in `docs/`. +versions, and a small `publish-dispatch.yml` `workflow_dispatch` wrapper around +`publish.yml` (reusable workflows can't be dispatched cross-repo). The wrapper is the +**tag trampoline** target (`publish-tag` dispatches it so the deploy runs as a +`workflow_dispatch` — required for a release's same-SHA deploy to actually go live) +**and** the fork-PR preview opt-in (pass `pr`). See the full how-to + snippets in +`docs/`. ## Upstreaming diff --git a/docs/explanations/architecture.md b/docs/explanations/architecture.md index 272594e..73aa910 100644 --- a/docs/explanations/architecture.md +++ b/docs/explanations/architecture.md @@ -116,23 +116,49 @@ manual opt-in. The privileged `publish.yml` is therefore reachable two ways, bot trusted: `workflow_call` (nested, internal) and `workflow_dispatch` (the maintainer fork opt-in). -The cost of nesting is an environment-policy change: because internal PRs and tags -now deploy from **their own ref**, the `github-pages` environment's deployment-branch -policy must allow those refs. The alternative — triggering publish via -`workflow_run` after CI completes — keeps deploys on the default branch only, but at -the price of the deploy being invisible on the PR. This project chose visibility. +The cost of nesting is an environment-policy change: because internal PRs now deploy +from **their own ref**, the `github-pages` environment's deployment-branch policy must +allow those refs. The alternative — triggering publish via `workflow_run` after CI +completes — keeps deploys on the default branch only, but at the price of the deploy +being invisible on the PR. This project chose visibility. (Tags are the exception — +they deploy from the default-branch ref via the trampoline below, not their own ref.) + +### Why tags deploy via a `workflow_dispatch` trampoline + +A release tag is cut on the merge commit, so it shares the default branch's +just-deployed SHA. `deploy-pages` stamps every deployment with +`pages_build_version = GITHUB_SHA` — it has no input to change it, and the value is +server-validated against the deploy OIDC token's commit claim, so a unique value +can't be forced (it 404s; see [`actions/deploy-pages#383`](https://github.com/actions/deploy-pages/issues/383)). +The Pages backend then silently **drops a second deploy of an already-deployed SHA** +on a non-`workflow_dispatch` event — it reports success and flips the deployment +record active, but the origin keeps serving the *first* artifact. So an inline tag +deploy would "succeed" while the site stayed on the pre-tag build. + +The one documented escape hatch: a `workflow_dispatch` deploy of that same SHA +*forces* a re-serve. So tags don't deploy inline. `ci.yml`'s `publish-tag` job (after +`release`, so the new `docs.zip` asset exists) re-triggers `publish.yml` as a +`workflow_dispatch`; that run re-gathers from durable sources (now including the new +release) and deploys, and because its event is `workflow_dispatch` the origin updates. +`main` and internal-PR deploys are the *first* of their SHA, so they serve fine inline +and keep their PR/commit visibility. Consumers do the same by dispatching their own +`publish-dispatch.yml` wrapper (a reusable workflow can't be dispatched cross-repo). +The post-deploy origin-verify step in `publish.yml` backstops any residual stale +origin by failing the run instead of serving stale docs silently. ### Why the current build is *injected* -Because publish now runs *inside* the build's own CI run, that run isn't a +Because the inline publish runs *inside* the build's own CI run, that run isn't a *completed* successful run yet — so `assemble`'s normal gather can't discover it. For -a `main` or tag push it would be worse than missing: the gather would find the -**previous** successful run and publish a build behind by one commit. So `ci.yml` -passes the build's version name to `publish.yml`, which hands it to `assemble` as +a `main` push it would be worse than missing: the gather would find the **previous** +successful run and publish a build behind by one commit. So `ci.yml` passes the +build's version name to `publish.yml`, which hands it to `assemble` as `ARTIFACT_VERSION_NAME`; `publish.yml` downloads this run's `docs` artifact and `assemble` unzips + stages it directly, **skipping the re-gather of that version**. -Everything else still comes from durable sources. The fork opt-in path passes no current build — there the fork is -gathered from durable sources via its approved head SHA's successful run. +Everything else still comes from durable sources. The dispatch paths (the tag +trampoline and the fork opt-in) pass no current build — they gather entirely from +durable sources, the tag from its just-attached `docs.zip` Release asset and a fork +from its approved head SHA's successful run. ## The bash / JS split inside `assemble` diff --git a/docs/reference/workflows.md b/docs/reference/workflows.md index 51a9019..2b3e25c 100644 --- a/docs/reference/workflows.md +++ b/docs/reference/workflows.md @@ -42,8 +42,15 @@ preinstalled Node, so `build-command` can be `make` / `npx` / `tox` / `npm` driv Reconstructs the whole site and deploys it to Pages. Carries the `github-pages` environment, `concurrency: pages`, and `pages`/`id-token`/`statuses` write permissions. Reachable two ways: `workflow_call` (nested by the consumer's `ci.yml` for internal -events) and `workflow_dispatch` (the maintainer fork-PR opt-in, for this repo's own -copy). The canonical-repo guard lives in the **caller**, so the workflow stays generic. +**non-tag** events) and `workflow_dispatch`. The dispatch path covers both the tag +trampoline and the fork-PR opt-in — **tags must deploy via `workflow_dispatch`**, since +GitHub Pages silently drops a second deploy of an already-deployed SHA (a release tag +shares the merge commit's SHA) unless the event is `workflow_dispatch` +([`actions/deploy-pages#383`](https://github.com/actions/deploy-pages/issues/383); see +the [architecture explanation](../explanations/architecture.md)). Consumers dispatch a +small `publish-dispatch.yml` wrapper (a reusable workflow can't be dispatched +cross-repo). The canonical-repo guard lives in the **caller**, so the workflow stays +generic. It self-checks-out this repo's `assemble/` scripts at `job.workflow_sha` / `job.workflow_repository` (the `job` context resolves to the reusable file, not the diff --git a/docs/tutorials/adding-to-a-fresh-repo.md b/docs/tutorials/adding-to-a-fresh-repo.md index 2f2df05..9489446 100644 --- a/docs/tutorials/adding-to-a-fresh-repo.md +++ b/docs/tutorials/adding-to-a-fresh-repo.md @@ -57,9 +57,14 @@ repo — you **call** this project's two reusable workflows by full path, pinned `_sources/` copy, unlike the default branch), so without it a release drops on the next deploy. - **`publish`** — `publish.yml` reconstructs the whole versioned site and deploys it, - nested so its status shows on the PR/commit, but it runs **only for internal events** - (the canonical-repo + non-fork guard). Fork PRs build to verify but never reach a - write token. + nested so its status shows on the PR/commit, but it runs **only for internal, + non-tag events** (the canonical-repo + non-fork guard). Fork PRs build to verify but + never reach a write token. +- **`publish-tag`** — tags can't deploy inline (a release tag shares the merge commit's + SHA, and GitHub Pages silently drops a second same-SHA deploy unless it's a + `workflow_dispatch`). So this tag-only job re-triggers the deploy as a dispatch of + your `publish-dispatch.yml` wrapper (below), which forces a re-serve. **Required for + releases to go live.** ```yaml # .github/workflows/ci.yml @@ -85,10 +90,11 @@ jobs: steps: # ↓ paste the two steps from the literalinclude below ↓ - publish: + publish: # internal, non-tag events only needs: [docs] if: >- github.repository == 'ORG/REPO' && + github.ref_type != 'tag' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository ) uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/publish.yml@ @@ -100,6 +106,18 @@ jobs: pages: write id-token: write statuses: write + + publish-tag: # tags: re-deploy via workflow_dispatch + needs: [docs, release] + if: github.repository == 'ORG/REPO' && github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: + actions: write # dispatch the wrapper + steps: + - env: + GH_TOKEN: ${{ github.token }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: gh workflow run publish-dispatch.yml --repo "$GITHUB_REPOSITORY" --ref "$DEFAULT_BRANCH" ``` The `release` job's `steps:` are exactly what this repo's own `_release.yml` runs (minus @@ -119,26 +137,35 @@ root, the `docs` artifact name), and `publish.yml` runs the site reconstruction internally — it checks out this project's `assemble` scripts at the same `` via `job.workflow_sha`, so there is no action or script for you to wire. -### (optional) Fork-PR preview opt-in +### The `publish-dispatch.yml` wrapper (required) + +A reusable workflow can't be `workflow_dispatch`'d cross-repo, so you need a tiny +wrapper in your own repo that calls `publish.yml`. It serves **two** purposes through +one optional `pr` input: -Internal PRs publish automatically; fork PRs build and verify but never auto-deploy. -To let a maintainer manually publish a fork PR's preview, add a small -`workflow_dispatch` wrapper — a reusable workflow can't be dispatched cross-repo, so -the wrapper lives in your repo and passes the PR number through `workflow_call`: +- **Tag trampoline (no `pr`)** — `publish-tag` above dispatches it on a tag. Deploying + as a `workflow_dispatch` event is what makes a same-SHA tag deploy actually go live + (an inline tag deploy is silently dropped — see the [architecture + explanation](../explanations/architecture.md)). **This is why the wrapper is + required, not optional.** +- **Fork-PR preview (with `pr`)** — internal PRs publish automatically; fork PRs build + and verify but never auto-deploy. A maintainer runs this from the Actions tab with a + PR number to approve that fork's current head SHA and deploy a preview (a later push + to the PR drops it until re-run). ```yaml -# .github/workflows/preview-fork.yml -name: Preview fork PR +# .github/workflows/publish-dispatch.yml +name: Publish (dispatch) on: workflow_dispatch: inputs: - pr: { description: Fork PR number to approve + preview, required: true } + pr: { description: "Fork PR number to approve + preview (leave empty to just re-deploy)", required: false, default: "" } jobs: - preview: + publish: uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/publish.yml@ with: - version-name: "" # no in-run build to inject → pure durable gather - pr: ${{ inputs.pr }} # pins the fork's head SHA as preview-approved, then assembles + version-name: "" # no in-run build to inject → pure durable gather + pr: ${{ inputs.pr }} # empty → plain re-deploy; set → pin that fork head SHA as preview-approved permissions: contents: read actions: read @@ -147,9 +174,8 @@ jobs: statuses: write ``` -Run it from the Actions tab with the PR number. It approves that fork PR's current -head commit and deploys a preview; a later push to the PR (new SHA) drops the preview -until you re-run it. +If you point `docs.yml`'s fork-preview warning at this file, also set +`preview-workflow: publish-dispatch.yml` on the `docs` job. ## 4. Allow the deploying refs in the `github-pages` environment