Repository navigation
224 lines (205 loc) · 9.98 KB
/
Copy pathrelease.yaml
File metadata and controls
224 lines (205 loc) · 9.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
name: Release
on:
# Fires when a tag is pushed via git with a workflow-triggering identity.
# This covers the hotfix tag and a human ad-hoc `git push vX.Y.Z`: both are
# push-only and have no dispatcher, so push: tags is their build path.
#
# The machine rc and final-release paths do NOT rely on this trigger. The
# orchestrate rc cut (generated in cascade's own-repo mode, see
# generate-workflow --own-repo) and the auto-promote final publish create
# their tag with the default GITHUB_TOKEN, whose actions deliberately do not
# start a workflow run, and then dispatch this workflow explicitly (below)
# with the release PAT. That keeps exactly one Release run per machine-cut
# tag while still cascading to fleet-e2e via workflow_run.
push:
tags:
- "v*"
# Backstop only: a release published outside the machine flow (for example a
# human publishing from the UI, or a PAT-created release) still builds and
# attaches assets. GoReleaser publishes with the default GITHUB_TOKEN, which
# does not re-fire this trigger, so it never loops on the machine path.
release:
types: [published, released]
# Canonical trigger for every machine path. orchestrate and auto-promote
# dispatch this workflow against the cut tag with the release PAT; this is
# immune to a CI-skip state commit on the tagged commit and to the
# API-release events GitHub does not reliably fire (#86).
workflow_dispatch:
# Least-privilege default: read-only at the top level. Only the release job,
# which cuts the GitHub Release and uploads built assets, is granted write.
permissions:
contents: read
# Single-flight: at most one Release run holds the pipeline at a time. The
# redundant triggers above are deliberate (a pushed candidate tag emits a real
# push event while orchestrate/promote/auto-promote also dispatch explicitly, so
# an API-created or CI-skipped tag still builds), but a pushed tag can fire two
# runs at once. A constant group, not one keyed on the tag, enforces the hard
# invariant that no two releases are ever in flight together. cancel-in-progress
# is false so an active publish always finishes; a queued duplicate then runs and
# no-ops through the idempotency guard in the release job. A pending duplicate can
# be superseded by a newer trigger, which is safe: the release job is idempotent
# and every dispatcher watches its run and can redispatch, so a final publish is
# never silently dropped.
concurrency:
group: release
cancel-in-progress: false
jobs:
# Integration protection: because Integration now gates merge to main via branch
# protection, any commit on main already passed Integration. A tag cut from main
# is therefore implicitly Integration-validated. We do not re-run Integration
# here (it would double the ~27min cost); the merge gate is the protection.
test:
name: Test
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
# actionlint must be on PATH so the emitted-workflow enforcement guard
# (TestActionlint_FeatureMatrix and the census-driven sweep) genuinely runs
# instead of skipping. Pinned to the same SHA the workflow-lint job uses so
# the guard and the repo's own workflow lint agree on the actionlint version.
- name: Install actionlint
run: go install github.com/rhysd/actionlint/cmd/actionlint@914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12
- name: Run tests
run: go test -v ./...
- name: Run linter
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.10.1
release:
name: Release
needs: test
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
# Idempotency guard for the deliberate double-trigger. When a candidate tag
# is both pushed and dispatched, two Release runs start; the single-flight
# concurrency group serialises them, and the loser lands here after the
# winner has published. If a live (non-draft) release with its checksum
# manifest already exists for this tag, skip the build and no-op green
# rather than driving GoReleaser into a 422 and leaving a stray draft. The
# decision core lives in a script so it is unit tested in isolation and
# reused by the post-GoReleaser belt below.
- name: Skip if already published
id: idem
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
SKIP="$(.github/scripts/release-idempotency.sh "$TAG")"
echo "skip=$SKIP" >> "$GITHUB_OUTPUT"
if [ "$SKIP" = "true" ]; then
echo "::notice::$TAG is already published with its checksum manifest; skipping the build (idempotent no-op)."
else
echo "$TAG not yet published; proceeding with the release build."
fi
- name: Set up Go
if: steps.idem.outputs.skip != 'true'
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
- name: Install cosign
if: steps.idem.outputs.skip != 'true'
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Import GPG key
if: steps.idem.outputs.skip != 'true'
env:
GPG_KEY_MATERIAL: ${{ secrets.CASCADE_RELEASE_GPG_KEY }}
run: |
mkdir -p ~/.gnupg
chmod 700 ~/.gnupg
echo "$GPG_KEY_MATERIAL" | gpg --batch --import
- name: Install syft
if: steps.idem.outputs.skip != 'true'
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
# Auto-promote tags the newest candidate's exact commit as the final
# vX.Y.Z, so a final tag and its last candidate (for example v0.8.0 and
# v0.8.0-rc.7) share one commit. GoReleaser, given only the current tag,
# auto-detects the previous tag as that candidate and produces an empty
# compare range, hence an empty changelog on the final release. For a
# final tag we pin GORELEASER_PREVIOUS_TAG to the previous stable release
# so the changelog spans the whole candidate cycle (v0.7.0..v0.8.0). For a
# candidate or dry-run tag we leave it unset so GoReleaser keeps its
# existing incremental changelog.
- name: Resolve previous stable tag for final changelog
if: steps.idem.outputs.skip != 'true'
env:
REF_NAME: ${{ github.ref_name }}
run: |
tag="$REF_NAME"
case "$tag" in
*-rc.*|*-dryrun.*)
echo "prerelease tag ${tag}; leaving GORELEASER_PREVIOUS_TAG unset"
;;
*)
git fetch --tags --force
prev="$(.github/scripts/previous-stable-tag.sh "$tag")"
if [ -n "$prev" ]; then
echo "final tag ${tag}; changelog spans ${prev}..${tag}"
echo "GORELEASER_PREVIOUS_TAG=${prev}" >> "$GITHUB_ENV"
else
echo "final tag ${tag}; no previous stable, leaving GORELEASER_PREVIOUS_TAG unset"
fi
;;
esac
# continue-on-error tolerates the one benign GoReleaser failure: two runs
# that both clear the pre-check and publish the same tag at once, so the
# loser hits `422 already_exists`. The next step converts that specific
# race into success and fails the job on any other error.
- name: Run GoReleaser
id: goreleaser
if: steps.idem.outputs.skip != 'true'
continue-on-error: true
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
GORELEASER_PREVIOUS_TAG: ${{ env.GORELEASER_PREVIOUS_TAG }}
GPG_FINGERPRINT: ${{ secrets.CASCADE_RELEASE_GPG_FINGERPRINT }}
# Belt for the rare simultaneous double-publish that the pre-check cannot
# catch: if GoReleaser succeeded we are done; if it failed only because a
# concurrent run already published this tag with its checksum manifest, the
# 422 is benign and the job succeeds; any other GoReleaser failure is real
# and fails the job.
- name: Confirm release published
if: steps.idem.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
GORELEASER_OUTCOME: ${{ steps.goreleaser.outcome }}
run: |
set -euo pipefail
if [ "$GORELEASER_OUTCOME" = "success" ]; then
echo "GoReleaser published $TAG."
exit 0
fi
if [ "$(.github/scripts/release-idempotency.sh "$TAG")" = "true" ]; then
echo "::notice::GoReleaser failed but $TAG is already published by a concurrent run; treating as an idempotent no-op success."
exit 0
fi
echo "::error::GoReleaser failed and no published release exists for $TAG."
exit 1
# Skip when the release was already published (idem no-op) or when this run
# lost the publish race, since only a successful GoReleaser leaves dist/*
# to attest and the winning run attested its own build.
- name: Attest build provenance
if: steps.idem.outputs.skip != 'true' && steps.goreleaser.outcome == 'success'
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: "dist/*"
github-token: ${{ secrets.GITHUB_TOKEN }}