diff --git a/.github/workflows/mirror-release.yml b/.github/workflows/mirror-release.yml index f3db0bf..09bb0d1 100644 --- a/.github/workflows/mirror-release.yml +++ b/.github/workflows/mirror-release.yml @@ -1,8 +1,6 @@ name: Mirror signed release on: - release: - types: [published] workflow_dispatch: inputs: tag: @@ -31,7 +29,7 @@ jobs: LC_ALL: C GH_HOST: github.com GH_PROMPT_DISABLED: '1' - TAG: ${{ github.event.release.tag_name || inputs.tag }} + TAG: ${{ inputs.tag }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} TRUSTED_WORKFLOW_SHA: ${{ github.workflow_sha }} MIRROR_BASE_URL: https://dl.ll.cd/linux-temp-admin @@ -46,9 +44,12 @@ jobs: run: | set -Eeuo pipefail [[ "$GITHUB_REPOSITORY" == xxvcc/linux-temp-admin ]] + [[ "$GITHUB_EVENT_NAME" == workflow_dispatch ]] [[ "$DEFAULT_BRANCH" == main ]] + [[ "$GITHUB_REF" == "refs/heads/$DEFAULT_BRANCH" ]] [[ "$GITHUB_WORKFLOW_REF" == "$GITHUB_REPOSITORY/.github/workflows/mirror-release.yml@refs/heads/$DEFAULT_BRANCH" ]] [[ "$TRUSTED_WORKFLOW_SHA" =~ ^[0-9a-f]{40}$ ]] + [[ "$GITHUB_SHA" == "$TRUSTED_WORKFLOW_SHA" ]] [[ "$MIRROR_ENVIRONMENT_CONFIGURED" == true ]] [[ "$TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-([0-9A-Za-z]+([.-][0-9A-Za-z]+)*))?$ ]] (( ${#BASH_REMATCH[1]} > 1 || 10#${BASH_REMATCH[1]} >= 2 )) diff --git a/CHANGELOG.md b/CHANGELOG.md index b06edb9..8f7a081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project are documented here. +## Unreleased + +- Run mirror synchronization only from an explicit protected-`main` dispatch. + GitHub binds a `release` event workflow to the released tag, so the automatic + trigger could not satisfy the default-branch trust check and must not receive + the mirror deployment credential. + ## v2.8.3 - 2026-07-27 - Run the release artifact handoff on the official actions' native Node.js 24 diff --git a/docs/releasing.md b/docs/releasing.md index 64e08e1..3b3bd58 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -744,7 +744,20 @@ timeout and uses a private, transient configuration directory. Publishing the GitHub Release is not the end of the release. The [`Mirror signed release`](../.github/workflows/mirror-release.yml) workflow must -finish before announcement. It accepts only an immutable public GitHub Release +be explicitly dispatched from the protected default branch and finish before +announcement: + +```bash +gh workflow run mirror-release.yml --repo xxvcc/linux-temp-admin \ + --ref main -f tag=v2.8.3 +gh run list --repo xxvcc/linux-temp-admin \ + --workflow mirror-release.yml --event workflow_dispatch --limit 1 +``` + +The explicit default-branch dispatch is a credential boundary: a GitHub +`release` event runs with the released tag as its workflow ref, so it must not +receive the `release-mirror` Environment credential. The workflow accepts only +an immutable public GitHub Release with the exact five-asset release set, rechecks the checksum manifest and both ed25519 signatures against the trusted keyring, verifies the released binaries, and copies one complete release into @@ -758,8 +771,8 @@ verifies the stable installer hash and fails if either client reports that it used the GitHub fallback. Create a protected GitHub Environment named `release-mirror` with no required -reviewers. Disable administrator bypass, and allow only protected `v*` tags plus -the protected default branch used for an explicit recovery dispatch. Enable +reviewers. Disable administrator bypass, and allow only the protected default +branch used for normal publication and recovery dispatches. Enable immutable Releases for the repository; synchronization fails closed when the selected GitHub Release is mutable. Configure exactly these environment values: diff --git a/internal/selfmanage/release_pipeline_test.go b/internal/selfmanage/release_pipeline_test.go index c3514ad..c237349 100644 --- a/internal/selfmanage/release_pipeline_test.go +++ b/internal/selfmanage/release_pipeline_test.go @@ -112,12 +112,16 @@ func TestMirrorReleaseWorkflowPublishesVerifiedImmutableContentFailClosed(t *tes mirror := readReleaseFile(t, "../../.github/workflows/mirror-release.yml") installer := readReleaseFile(t, "../../scripts/install.sh") for _, required := range []string{ - "types: [published]", + "workflow_dispatch:", "group: linux-temp-admin-release-mirror-stable", "cancel-in-progress: false", "environment: release-mirror", "LTA_RELEASE_MIRROR_ENVIRONMENT_CONFIGURED", + `TAG: ${{ inputs.tag }}`, + `[[ "$GITHUB_EVENT_NAME" == workflow_dispatch ]]`, + `[[ "$GITHUB_REF" == "refs/heads/$DEFAULT_BRANCH" ]]`, `$GITHUB_REPOSITORY/.github/workflows/mirror-release.yml@refs/heads/$DEFAULT_BRANCH`, + `[[ "$GITHUB_SHA" == "$TRUSTED_WORKFLOW_SHA" ]]`, "GH_HOST: github.com", "GH_PROMPT_DISABLED: '1'", "timeout -k 5 60 gh api", @@ -149,6 +153,8 @@ func TestMirrorReleaseWorkflowPublishesVerifiedImmutableContentFailClosed(t *tes } } for _, prohibited := range []string{ + "\n release:", + "github.event.release.tag_name", "types: [published, edited]", "contents: write", "--clobber",