From d4ceb66e0217466d02ab62976452563e452bfa73 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Mon, 3 Aug 2026 13:44:44 -0700 Subject: [PATCH] ci(release): refuse a stable tag unless dig-constants is single and current MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dig-constants is the ecosystem's one source for ports, paths and shared values, and both ways that guarantee fails are SILENT — neither is a build error: stale a CHANGED (not renamed) value means this node disagrees with every component that is current, and nothing reports it. duplicated cargo cannot unify semver-incompatible 0.x minors, so several copies link into ONE binary, each serving a different subsystem. The second is the one no review catches, because every individual manifest looks reasonable. This repo is shipping it right now — the lock carries FOUR dig-constants against a published 0.9.0: 0.1.0 <- dig-clvm 0.4.0 <- dig-gossip, dig-node-core, dig-node-service 0.5.1 <- dig-nat, digstore-chain 0.8.0 <- dig-download The gate reads the LOCK, not the manifest range: `dig-constants = "0.4"` is "satisfied" by a 0.4.0 lock forever, and only the lock says what compiles in. It fails closed when crates.io cannot be read, because a gate that passes on a network error is bypassable by causing one. Placed before version resolution in the stable job so no tag exists to deploy — releasing is the moment drift escapes the repo. Verified against today's tree: reports all four versions and the staleness, and exits 1. NOT wired as a PR check yet, deliberately: dig-constants is stale today, so a required check would block every open PR and the release it is meant to protect. Promote it once #2072 lands. Refs #2072 Co-Authored-By: Claude --- .github/workflows/nightly-release.yml | 20 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- scripts/check-dig-constants-current.sh | 89 ++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 scripts/check-dig-constants-current.sh diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/nightly-release.yml index 705f9be..b4288fc 100644 --- a/.github/workflows/nightly-release.yml +++ b/.github/workflows/nightly-release.yml @@ -104,6 +104,26 @@ jobs: fetch-depth: 0 token: ${{ secrets.RELEASE_TOKEN }} + # Refuse to cut a stable tag while dig-constants is stale or duplicated. + # + # dig-constants is the ecosystem's one source for ports, paths and shared values, and both of + # its failure modes are SILENT — neither is a build error: + # • stale — a changed (not renamed) value means this node disagrees with every component + # that is current, and nothing says so. + # • duplicated — cargo cannot unify semver-incompatible 0.x minors, so several copies link + # into one binary, each serving a different subsystem. On 2026-08-03 this repo + # shipped FOUR (0.1.0/0.4.0/0.5.1/0.8.0) against a published 0.9.0. + # + # Placed BEFORE version resolution deliberately: the point is that no tag exists to deploy, not + # that a bad build is caught later. Releasing is the moment the drift escapes the repo. + # + # The script fails closed if crates.io cannot be read — a gate that passes on a network error + # is bypassable by causing one. + - name: Require dig-constants to be single and current + if: steps.token.outputs.present == 'true' + shell: bash + run: ./scripts/check-dig-constants-current.sh + - name: Resolve version + skip if already tagged if: steps.token.outputs.present == 'true' id: ver diff --git a/Cargo.lock b/Cargo.lock index 33fddcd..b3568ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2281,7 +2281,7 @@ dependencies = [ [[package]] name = "dig-node-service" -version = "0.93.8" +version = "0.93.9" dependencies = [ "async-trait", "axum", diff --git a/Cargo.toml b/Cargo.toml index 77737d5..b33ccfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ edition = "2021" # the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a # release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet) # keep their own independent versions — only the released binary tracks the workspace version. -version = "0.93.8" +version = "0.93.9" # Release hardening, matching digstore: keep integer-overflow checks ON in release. # The node parses untrusted serialized input and does offset/length arithmetic over diff --git a/scripts/check-dig-constants-current.sh b/scripts/check-dig-constants-current.sh new file mode 100644 index 0000000..a675754 --- /dev/null +++ b/scripts/check-dig-constants-current.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# check-dig-constants-current.sh — refuse a release unless dig-constants is SINGLE and LATEST. +# +# WHY THIS EXISTS +# +# dig-constants is the ecosystem's one source for ports, paths, schemes and shared values. Its whole +# purpose is that two components cannot disagree about a canonical value. Two ways that guarantee +# fails silently, and neither shows up as a build error: +# +# 1. STALE — the node compiles against an old copy while the value has since changed. A renamed +# constant fails loudly; a CHANGED one does not. The node simply uses the wrong port/path and +# disagrees with every component that is current. +# +# 2. DUPLICATED — cargo cannot unify semver-incompatible 0.x minors, so a graph can carry several +# dig-constants versions AT ONCE, each linked into a different subsystem of the same binary. +# On 2026-08-03 dig-node's lock held FOUR (0.1.0 via dig-clvm, 0.4.0 via dig-gossip + +# dig-node-core, 0.5.1 via dig-nat + digstore-chain, 0.8.0 via dig-download) while 0.9.0 was +# published. That is the exact drift the crate exists to prevent, happening inside one process. +# +# Duplication is the more dangerous of the two and the one a version-bump review never catches, +# because every individual manifest looks reasonable. +# +# WHAT IT CHECKS — the LOCK, not the manifest range. `dig-constants = "0.4"` is "satisfied" by a lock +# at 0.4.0 forever; only the lock says what actually compiles in. +# +# FAILS CLOSED. A release gate that passes when it cannot reach crates.io is bypassable by inducing a +# network error. If the index cannot be read, this refuses and says so. +# +# Usage: scripts/check-dig-constants-current.sh [path/to/Cargo.lock] +# Exit: 0 = single version, and it is the latest published; 1 = anything else. + +set -uo pipefail + +LOCK="${1:-Cargo.lock}" +CRATE="dig-constants" +UA="dig-node-ci/1.0 (https://github.com/DIG-Network/dig-node; release gate)" + +[ -f "$LOCK" ] || { echo "::error::$LOCK not found"; exit 1; } + +# Every version of the crate present in the resolved graph. +mapfile -t FOUND < <(awk -v c="$CRATE" ' + /^\[\[package\]\]/ { name=""; ver="" } + /^name = / { gsub(/^name = "|"$/,""); name=$0 } + /^version = / { gsub(/^version = "|"$/,""); ver=$0; if (name==c) print ver } +' "$LOCK" | sort -u) + +if [ "${#FOUND[@]}" -eq 0 ]; then + echo "::error::$CRATE does not appear in $LOCK at all. If dig-node genuinely no longer depends on it, delete this gate deliberately rather than letting it pass silently." + exit 1 +fi + +# The published tip, from the sparse index. A bare curl 403s here — the descriptive User-Agent is +# mandatory, not decoration. +name_len=${#CRATE} +if [ "$name_len" -le 2 ]; then path="$name_len/$CRATE" +elif [ "$name_len" -eq 3 ]; then path="3/${CRATE:0:1}/$CRATE" +else path="${CRATE:0:2}/${CRATE:2:2}/$CRATE" +fi + +body="$(curl -sS --max-time 30 -A "$UA" "https://index.crates.io/$path" 2>/dev/null)" || body="" +LATEST="$(printf '%s' "$body" | grep -v '"yanked":true' | sed -n 's/.*"vers":"\([^"]*\)".*/\1/p' | tail -1)" + +if [ -z "$LATEST" ]; then + echo "::error::could not read the crates.io sparse index for $CRATE. Refusing rather than assuming current — a gate that passes on a network error is not a gate." + exit 1 +fi + +echo "published tip : $LATEST" +echo "in this lock : ${FOUND[*]}" + +rc=0 + +if [ "${#FOUND[@]}" -gt 1 ]; then + echo "::error::$CRATE resolves to ${#FOUND[@]} DIFFERENT versions in one binary: ${FOUND[*]}" + echo "::error::cargo cannot unify semver-incompatible 0.x minors, so each is linked into a different subsystem and they can disagree about a value that is supposed to be canonical by construction. Find the consumers with: cargo tree -i $CRATE" + rc=1 +fi + +for v in "${FOUND[@]}"; do + if [ "$v" != "$LATEST" ]; then + echo "::error::$CRATE $v is behind the published $LATEST. Bump the consumer that pins it — a 0.x minor gap is semver-BREAKING, so the caret range will never resolve forward on its own." + rc=1 + fi +done + +if [ "$rc" -eq 0 ]; then + echo "OK: exactly one $CRATE ($LATEST), matching the published tip." +fi +exit "$rc"