forked from pterodactyl/wings
-
Notifications
You must be signed in to change notification settings - Fork 0
298 lines (277 loc) · 12.4 KB
/
Copy pathrelease.yaml
File metadata and controls
298 lines (277 loc) · 12.4 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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
name: Release
# Automatic releases. The version is never ours to choose: it is always the
# version of the upstream pterodactyl/wings release that `develop` contains.
#
# push to develop
# └─ prepare resolves the newest upstream tag that is an ancestor of develop,
# refuses to continue without a matching CHANGELOG.md section,
# and opens a DRAFT release
# ├─ binary.yaml builds linux amd64/arm64 + SHA256SUMS onto the draft
# ├─ docker.yaml builds and pushes the versioned image
# └─ publish verifies the assets, moves :latest by digest, un-drafts
#
# Why draft-until-verified: nodes resolve GET /releases/latest and the :latest
# image. GitHub excludes drafts from /releases/latest, so a failure anywhere
# before `publish` leaves both pointing at the previous release rather than at a
# half-built one. Nothing becomes visible until the final job.
#
# Why binary/docker are `uses:` jobs in THIS run rather than `on: release`
# listeners: a release published with GITHUB_TOKEN does not trigger new workflow
# runs. docker.yaml used to listen on `release: published`, which only ever
# worked because a human clicked Publish. Automating that click without this
# change would have silently stopped every image build.
#
# NOTE: pushing a `v*` tag by hand no longer releases anything. Releases are cut
# from develop, by this workflow, and nowhere else.
#
# RECOVERY: a failed run leaves a DRAFT release behind. `prepare` treats an
# existing draft as "resume", so "Re-run failed jobs" on the original run picks
# up where it failed. Only a PUBLISHED release blocks a new run, which is why a
# fresh push to develop will not quietly re-cut a release that already shipped.
#
# RE-RELEASING a version: run this workflow manually with `force: true`. It
# deletes the published release AND its tag, then cuts the version again from
# the current develop. Deleting a release by hand does NOT re-trigger anything —
# that is deliberate, so an accidental deletion cannot resurrect itself with
# whatever happens to be on develop at the time.
#
# FORK-ONLY RELEASES: when we need to ship without an upstream bump, dispatch
# with `fork_revision` (1, 2, ...). The version becomes <upstream>.<revision>,
# e.g. v1.13.3.1 — upstream's number stays visible in front, and the release
# sorts after v1.13.3. The automatic push path never does this on its own.
on:
push:
branches:
- develop
workflow_dispatch:
inputs:
force:
description: "Re-release the version even if it is already published (deletes the existing release and its tag)"
type: boolean
default: false
fork_revision:
description: "Fork-only release: append this revision to the upstream version (1 -> v1.13.3.1). Leave empty for a normal release."
type: string
default: ""
permissions:
contents: write
packages: write
concurrency:
group: release
cancel-in-progress: false
jobs:
prepare:
name: Prepare release
runs-on: ubuntu-24.04
outputs:
should_release: ${{ steps.decide.outputs.should_release }}
version: ${{ steps.upstream.outputs.version }}
tag: ${{ steps.upstream.outputs.tag }}
sha: ${{ github.sha }}
steps:
- name: Code checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Ancestry checks need the whole history, not the default shallow clone.
fetch-depth: 0
- name: Resolve the upstream version contained in develop
id: upstream
env:
FORK_REVISION: ${{ inputs.fork_revision }}
run: |
set -euo pipefail
git remote add upstream https://github.com/pterodactyl/wings.git
# Fetched into a private namespace on purpose: our fork carries tags
# with the SAME names pointing at our own merge commits (our v1.13.1
# is f7ba42d, upstream's is e771816), so a plain `fetch --tags` is
# rejected as "would clobber existing tag".
git fetch --quiet upstream 'refs/tags/v*:refs/upstream-tags/v*'
best=""
while read -r ref; do
version="${ref#refs/upstream-tags/v}"
# Upstream tags release candidates too (v1.11.0-rc.1). We only ever
# ship stable versions.
case "$version" in *-*) continue ;; esac
# `^{commit}` dereferences annotated tags to the commit they point at.
git merge-base --is-ancestor "$(git rev-parse "${ref}^{commit}")" HEAD || continue
if [ -z "$best" ] || [ "$(printf '%s\n%s\n' "$best" "$version" | sort -V | tail -n1)" = "$version" ]; then
best="$version"
fi
done < <(git for-each-ref --format='%(refname)' 'refs/upstream-tags/v*')
if [ -z "$best" ]; then
echo "::error::no stable upstream tag is an ancestor of develop — cannot determine a version"
exit 1
fi
version="$best"
if [ -n "$FORK_REVISION" ]; then
case "$FORK_REVISION" in
''|*[!0-9]*|0*) echo "::error::fork_revision must be a positive integer, got '${FORK_REVISION}'"; exit 1 ;;
esac
version="${best}.${FORK_REVISION}"
echo "develop contains upstream v${best}; cutting fork revision v${version}"
else
echo "develop contains upstream v${best}"
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "tag=v${version}" >> "$GITHUB_OUTPUT"
- name: Decide whether to release
id: decide
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ steps.upstream.outputs.tag }}
FORCE: ${{ inputs.force }}
run: |
set -euo pipefail
state="$(gh release view "$TAG" --json isDraft \
--jq 'if .isDraft then "draft" else "published" end' 2>/dev/null || echo none)"
if [ "$FORCE" = "true" ] && [ "$state" != "none" ]; then
echo "::warning::${TAG} already exists (${state}) — force re-release requested, deleting it"
gh release delete "$TAG" --yes --cleanup-tag
state=none
fi
case "$state" in
none)
echo "no release for ${TAG} yet — cutting it"
;;
draft)
echo "a draft for ${TAG} exists — resuming it"
;;
published)
echo "${TAG} is already published — nothing to do"
echo "should_release=false" >> "$GITHUB_OUTPUT"
{
echo "### No release needed"
echo ""
echo "develop is on upstream \`${TAG}\`, which is already published."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
;;
esac
echo "should_release=true" >> "$GITHUB_OUTPUT"
- name: Extract the release notes
if: steps.decide.outputs.should_release == 'true'
env:
TAG: ${{ steps.upstream.outputs.tag }}
run: |
set -euo pipefail
# Everything between this version's heading and the next one. The `$`
# anchor matters: without it `## v1.13.2` also matches `## v1.13.20`.
sed -n "/^## ${TAG}\$/,/^## /{/^## /b;p}" CHANGELOG.md > RELEASE_CHANGELOG
if ! grep -q '[^[:space:]]' RELEASE_CHANGELOG; then
echo "::error::CHANGELOG.md has no '## ${TAG}' section — refusing to publish a release with empty notes"
exit 1
fi
cat RELEASE_CHANGELOG
- name: Open the draft release
if: steps.decide.outputs.should_release == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ steps.upstream.outputs.tag }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
# No git tag is created here. A draft only records tag_name and
# target_commitish; GitHub creates the tag when the release is
# published. That keeps an aborted run from leaving a tag behind.
if gh release view "$TAG" >/dev/null 2>&1; then
gh release edit "$TAG" --notes-file RELEASE_CHANGELOG --target "$SHA"
else
gh release create "$TAG" --draft --title "$TAG" --notes-file RELEASE_CHANGELOG --target "$SHA"
fi
{
echo "### Draft ${TAG} opened"
echo ""
echo "Building the binaries and the image now. The release is published"
echo "once both are attached and verified."
} >> "$GITHUB_STEP_SUMMARY"
binary:
name: Binary
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
uses: ./.github/workflows/binary.yaml
permissions:
contents: write
with:
ref: ${{ needs.prepare.outputs.sha }}
release-tag: ${{ needs.prepare.outputs.tag }}
version: ${{ needs.prepare.outputs.version }}
image:
name: Image
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
uses: ./.github/workflows/docker.yaml
permissions:
contents: read
packages: write
with:
ref: ${{ needs.prepare.outputs.sha }}
version: ${{ needs.prepare.outputs.version }}
# Runs only once BOTH the binaries and the image succeeded. This is the single
# point where anything becomes visible: :latest moves and the release leaves
# draft state. Until then a failure anywhere leaves the previous release and
# the previous :latest completely untouched.
publish:
name: Publish release
needs: [prepare, binary, image]
if: needs.prepare.outputs.should_release == 'true'
runs-on: ubuntu-24.04
permissions:
contents: write
packages: write
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Verify first: read-only, cheap, and independent of the retag. Doing it
# before anything moves means the only thing that can fail after :latest
# has moved is a single idempotent call, which a re-run repairs.
- name: Verify the release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
names="$(gh release view "$TAG" --json assets --jq '.assets[].name')"
echo "attached assets:"; echo "$names"
for asset in wings_linux_amd64 wings_linux_arm64 SHA256SUMS; do
printf '%s\n' "$names" | grep -qxF "$asset" || {
echo "::error::release ${TAG} is missing asset '${asset}' — refusing to publish"
exit 1
}
done
# Retag by digest — no rebuild, and it cannot accidentally point at a
# different image than the one the `image` job just verified. The previous
# digest is logged so a manual rollback needs no archaeology.
- name: Move :latest to the verified release image
env:
IMAGE: ghcr.io/${{ github.repository }}
DIGEST: ${{ needs.image.outputs.digest }}
run: |
set -euo pipefail
test -n "$DIGEST" || { echo "::error::image job produced no digest"; exit 1; }
echo "previous :latest -> $(docker buildx imagetools inspect --format '{{.Manifest.Digest}}' "${IMAGE}:latest" 2>/dev/null || echo '(none)')"
docker buildx imagetools create --tag "${IMAGE}:latest" "${IMAGE}@${DIGEST}"
echo ":latest -> ${DIGEST}"
- name: Publish the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
# Publishing is what finally creates the git tag, at the draft's
# target_commitish.
gh release edit "$TAG" --draft=false --latest
{
echo "### Published ${TAG}"
echo ""
echo "- Binaries: \`wings_linux_amd64\`, \`wings_linux_arm64\`, \`SHA256SUMS\`"
echo "- Image: \`ghcr.io/${{ github.repository }}:${VERSION}\` (also \`:${TAG}\` and \`:latest\`)"
} >> "$GITHUB_STEP_SUMMARY"