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