Skip to content

Commit dac29be

Browse files
committed
ci(release): derive crate publish order from the dependency graph
Replace the hand-maintained 8-tier publish list with a cargo metadata + tsort topological sort, so adding or re-wiring a crate no longer requires manually updating tier membership. Also gate the Discord notify job on publish-crates success/cancellation, not just the GitHub release step.
1 parent f0f0517 commit dac29be

1 file changed

Lines changed: 60 additions & 50 deletions

File tree

.github/workflows/release.yml

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# fix a Discord webhook or a crates.io timeout.
99
#
1010
# Stages, in dependency order:
11-
# publish_crates -> crates.io, 8 tiers
11+
# publish_crates -> crates.io, dependency-ordered (graph tsort)
1212
# docker -> per-arch images + multi-arch manifest (full releases only)
1313
# github_release -> GitHub Release with the binary tarballs
1414
# notify -> Discord announcement (full releases only)
@@ -105,22 +105,46 @@ jobs:
105105
env:
106106
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
107107
run: |
108-
# Tier 1: no internal NodeDB dependencies
109-
TIER1="nodedb-codec nodedb-wal nodedb-raft"
110-
# Tier 2: depends on tier 1 only (nodedb-types depends on nodedb-codec)
111-
TIER2="nodedb-types"
112-
# Tier 3: depends on tier 2 only (nodedb-mem + crates that need only nodedb-types/nodedb-raft)
113-
TIER3="nodedb-mem nodedb-bridge nodedb-crdt nodedb-strict nodedb-client nodedb-cluster nodedb-array"
114-
# Tier 4: depends on nodedb-mem (nodedb-fts, nodedb-vector, nodedb-graph, nodedb-spatial, nodedb-columnar)
115-
TIER4="nodedb-fts nodedb-vector nodedb-graph nodedb-spatial nodedb-columnar"
116-
# Tier 5: depends on tier 4 (nodedb-query depends on nodedb-spatial, nodedb-fts)
117-
TIER5="nodedb-query"
118-
# Tier 6: depends on tier 5 (nodedb-sql depends on nodedb-query, nodedb-spatial)
119-
TIER6="nodedb-sql"
120-
# Tier 7: depends on tier 6 (nodedb-physical depends on nodedb-sql, -query, -graph, -array)
121-
TIER7="nodedb-physical"
122-
# Tier 8: depends on tier 7 (nodedb depends on all crates including nodedb-physical)
123-
TIER8="nodedb"
108+
set -euo pipefail
109+
110+
# Publish order is derived from the actual dependency graph, never
111+
# hand-maintained tiers — a `cargo metadata` + `tsort` topological sort
112+
# so a dependency is always on the index before any crate that needs it.
113+
# Add/re-wire a crate and the order self-updates; nothing here to edit.
114+
META=$(cargo metadata --no-deps --format-version=1)
115+
116+
# Publishable workspace crates only (drop `publish = false` helpers such
117+
# as test-support / cluster-tests, whose `.publish` is []).
118+
mapfile -t PUBLISHABLE < <(
119+
jq -r '.packages[]
120+
| select(.publish == null or (.publish | length > 0))
121+
| .name' <<<"$META"
122+
)
123+
SET=$(printf '%s\n' "${PUBLISHABLE[@]}" | jq -R . | jq -s .)
124+
125+
# Edges "<dependency> <dependent>" over internal normal/build deps only
126+
# (dev-deps are path-only and stripped on publish; including them could
127+
# introduce cycles tsort can't order). tsort emits deps before dependents.
128+
EDGES=$(jq -r --argjson P "$SET" '
129+
.packages[]
130+
| select(.name as $n | $P | index($n))
131+
| .name as $dependent
132+
| .dependencies[]
133+
| select(.kind == null or .kind == "build")
134+
| select(.name as $d | $P | index($d))
135+
| "\(.name) \($dependent)"
136+
' <<<"$META")
137+
138+
ORDER=$(tsort <<<"$EDGES")
139+
# Isolated crates (no internal deps, nothing depends on them) never
140+
# appear in an edge — prepend any that tsort dropped.
141+
for crate in "${PUBLISHABLE[@]}"; do
142+
grep -qxF "$crate" <<<"$ORDER" || ORDER=$(printf '%s\n%s' "$crate" "$ORDER")
143+
done
144+
145+
echo "Publish order:"
146+
printf ' %s\n' $ORDER
147+
124148
is_published() {
125149
curl -sf \
126150
-H "User-Agent: nodedb-ci (github.com/NodeDB-Lab/nodedb)" \
@@ -141,38 +165,18 @@ jobs:
141165
return 1
142166
}
143167
144-
publish_tier() {
145-
local tier_name="$1"; shift
146-
local crates=("$@")
147-
local need_wait=()
148-
149-
echo "::group::Tier: $tier_name"
150-
for crate in "${crates[@]}"; do
151-
VERSION=$(cargo metadata --no-deps --format-version=1 \
152-
| jq -r --arg name "$crate" '.packages[] | select(.name == $name) | .version')
153-
if is_published "$crate" "$VERSION"; then
154-
echo " $crate@$VERSION already published — skipping"
155-
else
156-
echo " Publishing $crate@$VERSION..."
157-
cargo publish -p "$crate" --allow-dirty --no-verify
158-
need_wait+=("$crate:$VERSION")
159-
fi
160-
done
161-
162-
for entry in "${need_wait[@]}"; do
163-
wait_for "${entry%%:*}" "${entry##*:}"
164-
done
165-
echo "::endgroup::"
166-
}
167-
168-
publish_tier "1 (no internal deps)" $TIER1
169-
publish_tier "2 (depends on tier 1)" $TIER2
170-
publish_tier "3 (depends on tier 2)" $TIER3
171-
publish_tier "4 (depends on tier 3)" $TIER4
172-
publish_tier "5 (depends on tier 4)" $TIER5
173-
publish_tier "6 (depends on tier 5)" $TIER6
174-
publish_tier "7 (depends on tier 6)" $TIER7
175-
publish_tier "8 (depends on tier 7)" $TIER8
168+
for crate in $ORDER; do
169+
VERSION=$(jq -r --arg name "$crate" \
170+
'.packages[] | select(.name == $name) | .version' <<<"$META")
171+
if is_published "$crate" "$VERSION"; then
172+
echo " $crate@$VERSION already published — skipping"
173+
else
174+
echo " Publishing $crate@$VERSION..."
175+
cargo publish -p "$crate" --allow-dirty --no-verify
176+
# Wait for the index before the next (dependent) crate publishes.
177+
wait_for "$crate" "$VERSION"
178+
fi
179+
done
176180
177181
# ── Docker (full releases only) ──────────────────────────────────────────────
178182
# Native runner per arch (no QEMU). The image is assembled from the binary the
@@ -306,14 +310,20 @@ jobs:
306310
# ── Discord (full releases only) ─────────────────────────────────────────────
307311
notify-discord:
308312
name: Notify Discord
309-
needs: [validate-version, github-release]
313+
needs: [validate-version, publish-crates, github-release]
310314
if: >-
311315
always() &&
312316
inputs.notify &&
313317
needs.validate-version.outputs.is_full_release == 'true' &&
318+
needs.publish-crates.result != 'failure' &&
319+
needs.publish-crates.result != 'cancelled' &&
314320
needs.github-release.result != 'failure' &&
315321
needs.github-release.result != 'cancelled'
316322
runs-on: ubuntu-latest
323+
# NOTE: this is the ONE non-idempotent stage — every run POSTs a fresh Discord
324+
# message (crates/docker/github-release all overwrite or skip in place). It is
325+
# guarded only by the `notify` input: leave `notify: false` when re-dispatching
326+
# already-announced releases, or you will double-post.
317327
steps:
318328
- uses: actions/checkout@v7
319329
with:

0 commit comments

Comments
 (0)