-
-
Notifications
You must be signed in to change notification settings - Fork 14
376 lines (337 loc) · 14.2 KB
/
Copy pathrelease.yml
File metadata and controls
376 lines (337 loc) · 14.2 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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# Manual distribution workflow. Push a v* tag first; `Release Prepare` runs the
# test suite and builds the server binaries. Then run this workflow with that
# tag and the successful prepare run ID.
#
# NOTHING HERE COMPILES. Every stage consumes the prepare run's artifacts, so a
# failure in one stage is retried by re-dispatching with only that stage
# enabled — the tests, the binaries, and the Docker image are never rebuilt to
# fix a Discord webhook or a crates.io timeout.
#
# Stages, in dependency order:
# publish_crates -> crates.io, dependency-ordered (graph tsort)
# docker -> per-arch images + multi-arch manifest (full releases only)
# github_release -> GitHub Release with the binary tarballs
# notify -> Discord announcement (full releases only)
#
# Each stage is idempotent: crates already on crates.io are skipped, image tags
# and the release are overwritten in place. Re-running everything is safe; the
# toggles exist to save time, not to protect against double-publishing.
#
# Required secrets: CARGO_REGISTRY_TOKEN, DOCKERHUB_USERNAME, DOCKERHUB_TOKEN,
# DISCORD_RELEASE_WEBHOOK_URL.
name: Release
run-name: Release ${{ inputs.tag }}
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag to publish, e.g. v0.2.0"
required: true
type: string
prepare_run_id:
description: "Successful Release Prepare run ID holding the binaries"
required: true
type: string
ref:
description: "Ref to take workflow/Dockerfile/scripts from. Defaults to the tag; override to pick up a distribution-only fix without re-tagging."
required: false
type: string
publish_crates:
description: "Publish crates to crates.io"
type: boolean
default: true
docker:
description: "Build and push Docker images"
type: boolean
default: true
github_release:
description: "Create the GitHub Release"
type: boolean
default: true
notify:
description: "Announce on Discord"
type: boolean
default: true
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
# Always runs. Cheap, and it guarantees the dispatched tag still means what
# the prepare run assumed it meant.
validate-version:
uses: ./.github/workflows/release-validate.yml
with:
ref: ${{ inputs.tag }}
# ── crates.io ────────────────────────────────────────────────────────────────
# All 8 tiers in one job, with is_published checks and wait_for polling, so a
# re-run never double-publishes an already-indexed crate and never races the
# index for a dependency it just pushed.
publish-crates:
name: Publish to crates.io
needs: validate-version
if: inputs.publish_crates
runs-on: ubuntu-latest
environment: crates.io
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install system deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
cmake clang libclang-dev pkg-config protobuf-compiler perl \
libcurl4-openssl-dev libsasl2-dev
- name: Set version from tag
run: bash scripts/ci/stamp_version.sh "${{ needs.validate-version.outputs.version }}"
- name: Publish crates
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
# Publish order is derived from the actual dependency graph, never
# hand-maintained tiers — a `cargo metadata` + `tsort` topological sort
# so a dependency is always on the index before any crate that needs it.
# Add/re-wire a crate and the order self-updates; nothing here to edit.
META=$(cargo metadata --no-deps --format-version=1)
# Publishable workspace crates only (drop `publish = false` helpers such
# as test-support / cluster-tests, whose `.publish` is []).
mapfile -t PUBLISHABLE < <(
jq -r '.packages[]
| select(.publish == null or (.publish | length > 0))
| .name' <<<"$META"
)
SET=$(printf '%s\n' "${PUBLISHABLE[@]}" | jq -R . | jq -s .)
# Edges "<dependency> <dependent>" over internal normal/build deps only
# (dev-deps are path-only and stripped on publish; including them could
# introduce cycles tsort can't order). tsort emits deps before dependents.
EDGES=$(jq -r --argjson P "$SET" '
.packages[]
| select(.name as $n | $P | index($n))
| .name as $dependent
| .dependencies[]
| select(.kind == null or .kind == "build")
| select(.name as $d | $P | index($d))
| "\(.name) \($dependent)"
' <<<"$META")
ORDER=$(tsort <<<"$EDGES")
# Isolated crates (no internal deps, nothing depends on them) never
# appear in an edge — prepend any that tsort dropped.
for crate in "${PUBLISHABLE[@]}"; do
grep -qxF "$crate" <<<"$ORDER" || ORDER=$(printf '%s\n%s' "$crate" "$ORDER")
done
echo "Publish order:"
printf ' %s\n' $ORDER
is_published() {
curl -sf \
-H "User-Agent: nodedb-ci (github.com/NodeDB-Lab/nodedb)" \
"https://crates.io/api/v1/crates/$1/$2" > /dev/null 2>&1
}
wait_for() {
local crate="$1" version="$2"
echo -n " Waiting for $crate@$version..."
for i in $(seq 1 30); do
if is_published "$crate" "$version"; then
echo " ready"
return 0
fi
sleep 5
done
echo " timed out!"
return 1
}
for crate in $ORDER; do
VERSION=$(jq -r --arg name "$crate" \
'.packages[] | select(.name == $name) | .version' <<<"$META")
if is_published "$crate" "$VERSION"; then
echo " $crate@$VERSION already published — skipping"
else
echo " Publishing $crate@$VERSION..."
cargo publish -p "$crate" --allow-dirty --no-verify
# Wait for the index before the next (dependent) crate publishes.
wait_for "$crate" "$VERSION"
fi
done
# ── Docker (full releases only) ──────────────────────────────────────────────
# Native runner per arch (no QEMU). The image is assembled from the binary the
# prepare run already built: `--build-context binary=` overrides the Dockerfile
# stage of the same name, so Buildx prunes the compile stages entirely and the
# build is a two-layer copy.
docker-build:
name: Docker build (${{ matrix.label }})
needs: validate-version
if: inputs.docker && needs.validate-version.outputs.is_full_release == 'true'
runs-on: ${{ matrix.runs-on }}
permissions:
contents: read
actions: read
strategy:
fail-fast: false
matrix:
include:
- runs-on: ubuntu-latest
platform: linux/amd64
label: amd64
binary: binary-linux-x64
- runs-on: ubuntu-24.04-arm
platform: linux/arm64
label: arm64
binary: binary-linux-arm64
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || inputs.tag }}
- name: Download prebuilt binary
uses: actions/download-artifact@v8
with:
name: ${{ matrix.binary }}
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: ./binctx
- name: Make binary executable
run: chmod +x ./binctx/nodedb
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push platform image
uses: docker/build-push-action@v7
with:
context: .
build-contexts: binary=./binctx
platforms: ${{ matrix.platform }}
push: true
tags: farhansyah/nodedb:${{ needs.validate-version.outputs.version }}-${{ matrix.label }}
sbom: true
provenance: mode=max
# The binary is now compiled on the runner (glibc 2.39) rather than inside
# the Debian builder stage, so the runtime base must be able to load it.
# Exercise the pushed per-arch tag before it can reach the multi-arch
# manifest: `latest` is only ever built from images that have run.
- name: Smoke-test pushed image
run: |
docker run --rm --pull=always \
farhansyah/nodedb:${{ needs.validate-version.outputs.version }}-${{ matrix.label }} \
--version
docker-manifest:
name: Docker manifest
needs: [validate-version, docker-build]
if: inputs.docker && needs.validate-version.outputs.is_full_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push multi-arch manifest
env:
VERSION: ${{ needs.validate-version.outputs.version }}
run: |
docker buildx imagetools create \
--tag farhansyah/nodedb:${VERSION} \
--tag farhansyah/nodedb:latest \
farhansyah/nodedb:${VERSION}-amd64 \
farhansyah/nodedb:${VERSION}-arm64
# ── GitHub Release ───────────────────────────────────────────────────────────
# Deliberately independent of publish-crates and docker: a crates.io outage
# must not block cutting the release, and re-running this stage alone is the
# fix for a bad release body.
github-release:
name: Create GitHub Release
needs: validate-version
if: inputs.github_release
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
- name: Download binary artifacts
uses: actions/download-artifact@v8
with:
pattern: "server-*"
run-id: ${{ inputs.prepare_run_id }}
github-token: ${{ github.token }}
path: ./artifacts
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ inputs.tag }}
name: NodeDB ${{ needs.validate-version.outputs.version }}
generate_release_notes: true
draft: false
prerelease: ${{ needs.validate-version.outputs.is_full_release != 'true' }}
files: artifacts/*
fail_on_unmatched_files: true
# ── Discord (full releases only) ─────────────────────────────────────────────
notify-discord:
name: Notify Discord
needs: [validate-version, publish-crates, github-release]
if: >-
always() &&
inputs.notify &&
needs.validate-version.outputs.is_full_release == 'true' &&
needs.publish-crates.result != 'failure' &&
needs.publish-crates.result != 'cancelled' &&
needs.github-release.result != 'failure' &&
needs.github-release.result != 'cancelled'
runs-on: ubuntu-latest
# NOTE: this is the ONE non-idempotent stage — every run POSTs a fresh Discord
# message (crates/docker/github-release all overwrite or skip in place). It is
# guarded only by the `notify` input: leave `notify: false` when re-dispatching
# already-announced releases, or you will double-post.
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Build changelog and post to Discord
env:
DISCORD_RELEASE_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}
VERSION: ${{ needs.validate-version.outputs.version }}
IS_FULL: ${{ needs.validate-version.outputs.is_full_release }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
TAG="v${VERSION}"
PREV_TAG=$(python3 .github/scripts/prev_tag.py "$TAG")
if [[ -n "$PREV_TAG" ]]; then
RANGE="${PREV_TAG}..${TAG}"
COMPARE_URL="https://github.com/${REPO}/compare/${PREV_TAG}...${TAG}"
else
RANGE="${TAG}"
COMPARE_URL="https://github.com/${REPO}/releases/tag/${TAG}"
fi
COMMITS=$(git log "$RANGE" --pretty=format:'- %s (%h)' --no-merges --max-count=25)
[[ -z "$COMMITS" ]] && COMMITS="- (no commits found)"
COUNT=$(git rev-list --count --no-merges "$RANGE" 2>/dev/null || echo 0)
if [[ "$IS_FULL" == "true" ]]; then
COLOR=3066993 # green
TITLE="🚀 New Release: NodeDB ${VERSION}"
else
COLOR=15844367 # amber
TITLE="🚧 New Pre-Release: NodeDB ${VERSION}"
fi
DESC=$(printf '**Changes since %s** (%s commits)\n%s\n\n[Full changelog](%s) · [Release notes](https://github.com/%s/releases/tag/%s)' \
"${PREV_TAG:-start}" "$COUNT" "$COMMITS" "$COMPARE_URL" "$REPO" "$TAG")
PAYLOAD=$(jq -n \
--arg title "$TITLE" \
--arg desc "$DESC" \
--arg url "https://github.com/${REPO}/releases/tag/${TAG}" \
--argjson color "$COLOR" \
'{embeds:[{title:$title,url:$url,description:$desc,color:$color}]}')
curl -fsS -H "Content-Type: application/json" \
-X POST -d "$PAYLOAD" "$DISCORD_RELEASE_WEBHOOK_URL"