ci: keep release tooling and cache keys stable across jobs

This commit is contained in:
ranxi2001
2026-09-20 16:54:06 +08:00
parent 0892ef3a9a
commit d2416a9b68
4 changed files with 67 additions and 16 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__/
+2 -2
View File
@@ -4,9 +4,9 @@ The release workflow builds the frontend once, then runs each configured Go targ
Each build uses GoReleaser OSS in snapshot mode with the selected release version and one target. Archive naming, bundled files, Go flags and release templates remain in the existing GoReleaser configurations. Every archive is accompanied by its source commit, target, version and SHA256. The publishing job verifies the complete matrix before building images or publishing. It uses GoReleaser's `extra_files` support to publish existing archives and checksums, with builds disabled. No Pro license is needed.
Go caches are isolated by target and refreshed on each source commit, with fallback to the preceding target cache. Matrix jobs upload uniquely named artifacts. The publishing job extracts only the regular Linux binary from each verified archive and restores its executable permission before constructing Docker contexts. QEMU remains limited to runtime-image instructions. DockerHub images are omitted when its credentials are absent; GHCR is always retained. Simple mode still publishes only the amd64 GHCR image and the simple release description.
Go caches are isolated by target and refreshed on each source commit, with fallback to the preceding target cache. Save uses the original restore key, even if a build hook changes `go.sum`. Matrix jobs upload uniquely named artifacts. The publishing job extracts only the regular Linux binary from each verified archive and restores its executable permission before constructing Docker contexts. QEMU remains limited to runtime-image instructions. DockerHub images are omitted when its credentials are absent; GHCR is always retained. Simple mode still publishes only the amd64 GHCR image and the simple release description.
All build jobs use the commit resolved by `prepare`, including a manual release's selected tag. The workflow serializes release runs to prevent simultaneous updates to moving image tags.
All build jobs use the commit resolved by `prepare`, including a manual release's selected tag. Helper scripts come from the workflow revision and are passed as a run-local artifact, so older application tags do not need to contain the new scripts. The workflow serializes release runs to prevent simultaneous updates to moving image tags.
## Validate without publication
@@ -159,5 +159,33 @@ class ReleaseMatrixTest(unittest.TestCase):
self.assertIn('ghcr.io/exampleowner/sub2api', log)
def test_published_full_and_simple_image_tags(self):
fake_bin = Path('bin')
fake_bin.mkdir()
docker = fake_bin / 'docker'
docker.write_text('#!/bin/sh\nprintf "%s\\n" "$*" >> "$DOCKER_LOG"\n')
docker.chmod(0o755)
for simple in (False, True):
with self.subTest(simple=simple):
log_path = Path(f'docker-{simple}.log').resolve()
env = {**os.environ, 'PATH': str(fake_bin.resolve()) + os.pathsep + os.environ['PATH'],
'DOCKER_LOG': str(log_path), 'RUNNER_TEMP': self.temp.name,
'RELEASE_VERSION': '9.8.7', 'RELEASE_SHA': 'a' * 40, 'GITHUB_REPOSITORY': 'ExampleOwner/sub2api',
'DRY_RUN': 'false', 'SIMPLE_RELEASE': str(simple).lower(), 'DOCKERHUB_USERNAME': 'fixturehub'}
subprocess.run(['bash', str(ROOT / '.github/release-tools/release-images.sh')], env=env, check=True)
log = log_path.read_text()
self.assertIn('--push', log)
self.assertEqual(log.count('buildx build'), 1 if simple else 2)
if simple:
self.assertNotIn('fixturehub', log)
self.assertNotIn('imagetools', log)
self.assertIn('ghcr.io/exampleowner/sub2api:latest', log)
else:
self.assertEqual(log.count('imagetools create'), 2)
self.assertIn('fixturehub/sub2api:9.8', log)
self.assertIn('ghcr.io/exampleowner/sub2api:9', log)
if __name__ == '__main__':
unittest.main()
+36 -14
View File
@@ -45,14 +45,21 @@ jobs:
simple: ${{ steps.plan.outputs.simple }}
dry_run: ${{ steps.plan.outputs.dry_run }}
steps:
- uses: actions/checkout@v6
- name: Checkout workflow tooling
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
- name: Preserve tooling for releases of older tags
run: cp -R .github/release-tools "$RUNNER_TEMP/release-tools"
- name: Checkout selected application source
uses: actions/checkout@v6
with:
ref: ${{ inputs.tag || github.ref }}
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- run: python -m pip install -r .github/release-tools/requirements-release.txt
- run: python -m pip install -r "$RUNNER_TEMP/release-tools/requirements-release.txt"
- name: Resolve one source commit and target matrix
id: plan
env:
@@ -61,7 +68,12 @@ jobs:
args=(--ref "$REQUESTED_REF")
if [[ "$SIMPLE_RELEASE" == true ]]; then args+=(--simple); fi
if [[ "$DRY_RUN" == true ]]; then args+=(--dry-run); fi
python .github/release-tools/release_matrix.py plan "${args[@]}"
python "$RUNNER_TEMP/release-tools/release_matrix.py" plan "${args[@]}"
- uses: actions/upload-artifact@v7
with:
name: release-tooling
path: ${{ runner.temp }}/release-tools/
retention-days: 1
- uses: actions/upload-artifact@v7
with:
name: version-file
@@ -107,6 +119,10 @@ jobs:
with:
ref: ${{ needs.prepare.outputs.sha }}
fetch-depth: 0
- uses: actions/download-artifact@v8
with:
name: release-tooling
path: ${{ runner.temp }}/release-tools/
- uses: actions/download-artifact@v8
with:
name: version-file
@@ -136,17 +152,17 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- run: python -m pip install -r .github/release-tools/requirements-release.txt
- run: python -m pip install -r "$RUNNER_TEMP/release-tools/requirements-release.txt"
- name: Select one GoReleaser target
env:
TARGET_OS: ${{ matrix.goos }}
TARGET_ARCH: ${{ matrix.goarch }}
run: python .github/release-tools/release_matrix.py config build --goos "$TARGET_OS" --goarch "$TARGET_ARCH" --output .goreleaser.matrix.yaml
run: python "$RUNNER_TEMP/release-tools/release_matrix.py" config build --goos "$TARGET_OS" --goarch "$TARGET_ARCH" --output "$RUNNER_TEMP/goreleaser.matrix.yaml"
- name: Build and archive without publishing
uses: goreleaser/goreleaser-action@v7
with:
version: '~> v2'
args: release --snapshot --clean --skip=validate,publish,docker,announce --config=.goreleaser.matrix.yaml
args: release --snapshot --clean --skip=validate,publish,docker,announce --config=${{ runner.temp }}/goreleaser.matrix.yaml
env:
GITHUB_REPO_OWNER: ${{ github.repository_owner }}
GITHUB_REPO_OWNER_LOWER: ${{ needs.prepare.outputs.owner_lower }}
@@ -158,7 +174,7 @@ jobs:
TARGET_OS: ${{ matrix.goos }}
TARGET_ARCH: ${{ matrix.goarch }}
RELEASE_SHA: ${{ needs.prepare.outputs.sha }}
run: python .github/release-tools/release_matrix.py collect --version "$RELEASE_VERSION" --sha "$RELEASE_SHA" --goos "$TARGET_OS" --goarch "$TARGET_ARCH" --output matrix-output
run: python "$RUNNER_TEMP/release-tools/release_matrix.py" collect --version "$RELEASE_VERSION" --sha "$RELEASE_SHA" --goos "$TARGET_OS" --goarch "$TARGET_ARCH" --output matrix-output
- uses: actions/upload-artifact@v7
with:
name: release-binary-${{ matrix.goos }}-${{ matrix.goarch }}
@@ -172,7 +188,7 @@ jobs:
path: |
~/go/pkg/mod
~/.cache/go-build
key: release-go-${{ runner.os }}-${{ runner.arch }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('backend/go.mod', 'backend/go.sum') }}-${{ needs.prepare.outputs.sha }}
key: ${{ steps.go-cache.outputs.cache-primary-key }}
release:
needs: [prepare, build-binaries]
@@ -181,6 +197,8 @@ jobs:
contents: write
packages: write
env:
SIMPLE_RELEASE: ${{ needs.prepare.outputs.simple }}
DRY_RUN: ${{ needs.prepare.outputs.dry_run }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
RELEASE_SHA: ${{ needs.prepare.outputs.sha }}
@@ -189,6 +207,10 @@ jobs:
with:
ref: ${{ needs.prepare.outputs.sha }}
fetch-depth: 0
- uses: actions/download-artifact@v8
with:
name: release-tooling
path: ${{ runner.temp }}/release-tools/
- uses: actions/download-artifact@v8
with:
pattern: release-binary-*
@@ -197,12 +219,12 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- run: python -m pip install -r .github/release-tools/requirements-release.txt
- run: python -m pip install -r "$RUNNER_TEMP/release-tools/requirements-release.txt"
- name: Verify complete matrix and prepare Docker contexts
run: |
args=(--version "$RELEASE_VERSION" --sha "$RELEASE_SHA" --input release-input --output .release-context)
if [[ "$SIMPLE_RELEASE" == true ]]; then args+=(--simple); fi
python .github/release-tools/release_matrix.py contexts "${args[@]}"
python "$RUNNER_TEMP/release-tools/release_matrix.py" contexts "${args[@]}"
- uses: actions/setup-go@v6
with:
go-version-file: backend/go.mod
@@ -229,7 +251,7 @@ jobs:
- name: Build images and publish manifests
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'skip' }}
run: bash .github/release-tools/release-images.sh
run: bash "$RUNNER_TEMP/release-tools/release-images.sh"
- name: Get annotated tag notes
id: tag_message
run: |
@@ -242,14 +264,14 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: Prepare publication-only GoReleaser config
run: |
args=(publish --output .goreleaser.publisher.yaml)
args=(publish --output "$RUNNER_TEMP/goreleaser.publisher.yaml")
if [[ "$SIMPLE_RELEASE" == true ]]; then args+=(--simple); fi
python .github/release-tools/release_matrix.py config "${args[@]}"
python "$RUNNER_TEMP/release-tools/release_matrix.py" config "${args[@]}"
- name: Publish existing archives and release notes
uses: goreleaser/goreleaser-action@v7
with:
version: '~> v2'
args: release --clean --skip=validate,before,docker ${{ env.DRY_RUN == 'true' && '--snapshot --skip=publish,announce' || '' }} --config=.goreleaser.publisher.yaml
args: release --clean --skip=validate,before,docker ${{ env.DRY_RUN == 'true' && '--snapshot --skip=publish,announce' || '' }} --config=${{ runner.temp }}/goreleaser.publisher.yaml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_CURRENT_TAG: ${{ needs.prepare.outputs.tag }}