Skip to content

Commit ef37289

Browse files
committed
no-mistakes(review): record source commit per asset and gate release on agreement
1 parent 6771c2f commit ef37289

3 files changed

Lines changed: 195 additions & 23 deletions

File tree

scripts/build-linux-arm64.sh

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
# Usage:
4444
# ./scripts/build-linux-arm64.sh # build + verify + stage
4545
# ./scripts/build-linux-arm64.sh --no-stage # build + verify only
46+
#
47+
# CODEGRAPH_ALLOW_DIRTY=1 ./scripts/build-linux-arm64.sh
48+
# Build from whatever is in the tree, skipping the commit check below.
49+
# Implies --no-stage: a binary nobody can trace must not reach the
50+
# staging directory, where it would look exactly like a release build.
4651

4752
set -euo pipefail
4853

@@ -74,7 +79,9 @@ CHECK_PROVENANCE=1
7479
if [ "${CODEGRAPH_ALLOW_DIRTY:-0}" = "1" ]; then
7580
CHECK_PROVENANCE=0
7681
else
77-
EXPECTED_GIT_SHORT="$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true)"
82+
# An explicit length rather than `--short`, whose default the host user's
83+
# core.abbrev can change while the container's git has no such setting.
84+
EXPECTED_GIT_SHORT="$(git -C "$REPO_ROOT" rev-parse --short=12 HEAD 2>/dev/null || true)"
7885
if [ -z "$EXPECTED_GIT_SHORT" ]; then
7986
echo "ERROR: $REPO_ROOT is not a git checkout, so the engine cannot say what" >&2
8087
echo "produced it. Build from a checkout, or set CODEGRAPH_ALLOW_DIRTY=1 for a" >&2
@@ -222,7 +229,16 @@ if [ "$CHECK_PROVENANCE" = "1" ]; then
222229
echo "and cannot be traced back to a released commit." >&2
223230
exit 1 ;;
224231
esac
225-
if [ "$GOT_GIT" != "$EXPECTED_GIT_SHORT" ]; then
232+
if [ "${#GOT_GIT}" -lt 7 ]; then
233+
echo "ERROR: the binary's stamp '$GOT_GIT' is too short to name a commit." >&2
234+
exit 1
235+
fi
236+
# By prefix: the container's git and the host's git abbreviate independently,
237+
# so the same commit can legitimately come back at two different lengths.
238+
same_commit=0
239+
case "$EXPECTED_GIT_SHORT" in "$GOT_GIT"*) same_commit=1 ;; esac
240+
case "$GOT_GIT" in "$EXPECTED_GIT_SHORT"*) same_commit=1 ;; esac
241+
if [ "$same_commit" -eq 0 ]; then
226242
echo "ERROR: the binary claims commit $GOT_GIT, but the host is at" >&2
227243
echo "$EXPECTED_GIT_SHORT. The release assets would disagree on their source." >&2
228244
exit 1
@@ -235,9 +251,15 @@ CONTAINER
235251
BUILT="$BUILD_DIR/release/codegraph-server"
236252
[ -f "$BUILT" ] || { echo "ERROR: no binary at $BUILT" >&2; exit 1; }
237253

238-
if [ "${1:-}" = "--no-stage" ]; then
254+
if [ "${1:-}" = "--no-stage" ] || [ "$CHECK_PROVENANCE" -eq 0 ]; then
239255
echo
240256
echo "Built (not staged): $BUILT"
257+
if [ "$CHECK_PROVENANCE" -eq 0 ]; then
258+
echo
259+
echo "Not staged: CODEGRAPH_ALLOW_DIRTY=1 skipped the commit check, and an"
260+
echo "unverified binary in $STAGE_DIR would be indistinguishable"
261+
echo "from a release build. Commit the tree and re-run to stage."
262+
fi
241263
exit 0
242264
fi
243265

@@ -249,5 +271,8 @@ echo "Staged: $STAGE_DIR/$ASSET"
249271

250272
# Provenance is recorded where it is known. publish-release-assets.sh treats an
251273
# unstamped binary in the staging directory as stale, because vscode/bin/ is not
252-
# cleaned between releases.
253-
"$REPO_ROOT/scripts/stamp-binary.sh" "$ASSET" "$VERSION"
274+
# cleaned between releases, and refuses a set of assets whose commits disagree.
275+
# The commit is passed rather than read back from the binary: an x86_64 host
276+
# cannot execute what it just cross-built, and the container already proved this
277+
# binary agrees with this checkout.
278+
"$REPO_ROOT/scripts/stamp-binary.sh" "$ASSET" "$VERSION" "$EXPECTED_GIT_SHORT"

scripts/publish-release-assets.sh

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,42 @@ echo
120120
#
121121
# Provenance is recorded where it is knowable rather than guessed here: each
122122
# binary is stamped into MANIFEST by the build that produced it, on the host
123-
# that could actually run `--version`. Scraping version strings out of a
123+
# that could actually run `--info`. Scraping version strings out of a
124124
# cross-platform image was tried and is not reliable - the engine does not
125125
# store its version as a standalone string on every target, so good binaries
126126
# were reported as stale.
127127
MANIFEST="$VSCODE_BIN/BUILD-MANIFEST"
128128

129+
# The binary name is the last field, so a line whose second field is that name
130+
# is one written before the manifest carried a commit at all.
131+
manifest_line() {
132+
[ -f "$MANIFEST" ] || return 0
133+
awk -v b="$1" '$NF == b { print; exit }' "$MANIFEST"
134+
}
135+
136+
manifest_version() {
137+
local line ver
138+
line="$(manifest_line "$1")"
139+
if [ -n "$line" ]; then
140+
read -r ver _ <<< "$line"
141+
printf '%s\n' "$ver"
142+
fi
143+
}
144+
145+
manifest_commit() {
146+
local line ver commit
147+
line="$(manifest_line "$1")"
148+
if [ -n "$line" ]; then
149+
read -r ver commit _ <<< "$line"
150+
if [ "$commit" != "$1" ]; then
151+
printf '%s\n' "$commit"
152+
fi
153+
fi
154+
}
155+
129156
missing=0
130157
stale=0
158+
COMMITS=()
131159
for bin in "${BINARIES[@]}" "$WINDOWS_SIDECAR"; do
132160
if [ ! -f "$VSCODE_BIN/$bin" ]; then
133161
printf ' ✗ %-36s MISSING\n' "$bin"
@@ -143,12 +171,19 @@ for bin in "${BINARIES[@]}" "$WINDOWS_SIDECAR"; do
143171
continue
144172
fi
145173

146-
if [ -f "$MANIFEST" ] && grep -qxF "$VERSION $bin" "$MANIFEST"; then
147-
printf ' ✓ %-36s %s (%s)\n' "$bin" "$size" "$VERSION"
148-
else
149-
recorded="$(grep -F " $bin" "$MANIFEST" 2>/dev/null | awk '{print $1}' | tr '\n' ' ' || true)"
150-
printf ' ✗ %-36s %s NOT STAMPED %s\n' "$bin" "$size" "${recorded:+(manifest says: $recorded)}"
174+
recorded_version="$(manifest_version "$bin")"
175+
recorded_commit="$(manifest_commit "$bin")"
176+
177+
if [ "$recorded_version" != "$VERSION" ]; then
178+
printf ' ✗ %-36s %s NOT STAMPED %s\n' \
179+
"$bin" "$size" "${recorded_version:+(manifest says: $recorded_version)}"
151180
stale=1
181+
elif [ -z "$recorded_commit" ]; then
182+
printf ' ✗ %-36s %s NO COMMIT RECORDED\n' "$bin" "$size"
183+
stale=1
184+
else
185+
printf ' ✓ %-36s %s (%s @ %s)\n' "$bin" "$size" "$VERSION" "$recorded_commit"
186+
COMMITS+=("$recorded_commit")
152187
fi
153188
done
154189

@@ -168,20 +203,65 @@ fi
168203
if [ "$stale" -ne 0 ]; then
169204
cat >&2 <<EOF
170205
171-
ERROR: at least one binary is not stamped as $VERSION in
206+
ERROR: at least one binary is not stamped as $VERSION with a source commit in
172207
$VSCODE_BIN/BUILD-MANIFEST.
173208
174209
vscode/bin/ is not cleaned between releases, so an unstamped binary is assumed
175210
to be left over from an earlier one. Rebuild it and record it with:
176211
177-
./scripts/stamp-binary.sh <name> <version>
212+
./scripts/stamp-binary.sh <name> <version> [commit]
213+
214+
A binary stamped with no commit was recorded before the manifest carried one;
215+
re-stamp it, from the host that can run it or with the commit stated, so the
216+
set can be checked for agreement.
178217
179218
Publishing an unverified binary would produce a release whose checksums are
180219
perfectly valid for the wrong build - the hardest kind of mistake to notice.
181220
EOF
182221
exit 1
183222
fi
184223

224+
# ------------------------------------------------------ one commit, all assets
225+
# A shared version number is not a shared source. Each platform is built on its
226+
# own host, so five binaries can all be stamped $VERSION and still come from
227+
# five different trees - and then a bug reported against $VERSION on Linux and
228+
# one reported against $VERSION on macOS describe different software under the
229+
# same name. Compared by prefix: each host's git abbreviates commits to its own
230+
# length, so the same commit legitimately appears as 7 and 12 hex digits.
231+
ref_commit=""
232+
for c in "${COMMITS[@]}"; do
233+
if [ -z "$ref_commit" ] || [ "${#c}" -lt "${#ref_commit}" ]; then
234+
ref_commit="$c"
235+
fi
236+
done
237+
238+
commit_mismatch=0
239+
for c in "${COMMITS[@]}"; do
240+
case "$c" in "$ref_commit"*) ;; *) commit_mismatch=1 ;; esac
241+
done
242+
243+
if [ "$commit_mismatch" -ne 0 ]; then
244+
{
245+
echo
246+
echo "ERROR: the staged binaries were not all built from the same commit."
247+
echo
248+
for bin in "${BINARIES[@]}"; do
249+
printf ' %-36s %s\n' "$bin" "$(manifest_commit "$bin")"
250+
done
251+
cat <<EOF
252+
253+
Everything in one release has to come from one tree, or the tag names a build
254+
that never existed as a whole. Rebuild the assets that disagree from the commit
255+
being released - see cross-platform-builds.md for the per-platform hosts - and
256+
re-stamp them with ./scripts/stamp-binary.sh.
257+
EOF
258+
} >&2
259+
exit 1
260+
fi
261+
262+
echo
263+
printf ' all %d engine binaries built from %s\n' "${#COMMITS[@]}" "$ref_commit"
264+
185265
# ---------------------------------------------------------------- checksums
186266
# An engine binary runs on the user's machine with their permissions, so the
187267
# client verifies what it downloaded. TLS alone does not cover a mirror, a

scripts/stamp-binary.sh

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,39 @@
22
# Copyright 2026 Andrey Vasilevsky <anvanster@gmail.com>
33
# SPDX-License-Identifier: Apache-2.0
44
#
5-
# Record which version a staged engine binary was built from.
5+
# Record which version and which commit a staged engine binary was built from.
66
#
77
# vscode/bin/ is a staging directory that is not cleaned between releases, so a
88
# binary left over from an earlier version is indistinguishable from a fresh
99
# one by inspection. Scraping the version out of the image does not work
1010
# reliably across targets - the engine does not store it as a standalone string
1111
# everywhere, and good binaries get reported as stale.
1212
#
13-
# So provenance is recorded at the point where it is actually known: whoever
14-
# stages a binary states what produced it. publish-release-assets.sh refuses to
15-
# publish anything not stamped for the version being released.
13+
# The version alone is not provenance. Five binaries built on five different
14+
# hosts can all be 0.20.1 and still come from five different trees, which is a
15+
# release whose assets disagree with each other and with the tag it was cut
16+
# from. So the commit is recorded beside the version, and
17+
# publish-release-assets.sh refuses a set that does not agree on one.
18+
#
19+
# The commit is taken from the binary itself wherever the staging host can run
20+
# it: the engine bakes it in at build time and prints it from `--info`, which
21+
# makes it a measurement rather than a claim. A cross-built asset cannot be
22+
# executed on the host that stages it, so there it has to be stated.
1623
#
1724
# Usage:
18-
# ./scripts/stamp-binary.sh codegraph-server-linux-x64 0.20.0
25+
# ./scripts/stamp-binary.sh codegraph-server-darwin-arm64 0.20.1
26+
# ./scripts/stamp-binary.sh codegraph-server-linux-x64 0.20.1 a1b2c3d4e5f6
1927
#
2028
set -euo pipefail
2129

22-
if [ $# -ne 2 ]; then
23-
echo "usage: $(basename "$0") <binary-name> <version>" >&2
30+
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
31+
echo "usage: $(basename "$0") <binary-name> <version> [commit]" >&2
2432
exit 2
2533
fi
2634

2735
BINARY="$1"
2836
VERSION="$2"
37+
COMMIT="${3:-}"
2938

3039
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
3140
BIN_DIR="${CODEGRAPH_BIN_DIR:-$REPO_ROOT/vscode/bin}"
@@ -36,14 +45,72 @@ if [ ! -f "$BIN_DIR/$BINARY" ]; then
3645
exit 1
3746
fi
3847

48+
# Two abbreviations of the same commit, made by different git versions on
49+
# different hosts, differ in length rather than content.
50+
same_commit() {
51+
case "$1" in "$2"*) return 0 ;; esac
52+
case "$2" in "$1"*) return 0 ;; esac
53+
return 1
54+
}
55+
56+
# Failure here is the ordinary cross-built case, not an error: a linux binary
57+
# staged on macOS cannot execute, which is exactly why [commit] exists.
58+
SELF_REPORTED=""
59+
if [ -x "$BIN_DIR/$BINARY" ]; then
60+
info="$("$BIN_DIR/$BINARY" --info 2>/dev/null || true)"
61+
SELF_REPORTED="$(printf '%s\n' "$info" | sed -n '1s/.*(\(.*\)).*/\1/p')"
62+
fi
63+
64+
if [ -n "$SELF_REPORTED" ] && [ -n "$COMMIT" ] && ! same_commit "$COMMIT" "$SELF_REPORTED"; then
65+
echo "ERROR: $BINARY reports commit $SELF_REPORTED, but $COMMIT was given." >&2
66+
echo "The binary is the only witness of what actually built it, so the" >&2
67+
echo "argument cannot override it. Stage the binary you meant to stage." >&2
68+
exit 1
69+
fi
70+
71+
# They agree by here, so keep whichever abbreviation names the commit more
72+
# precisely.
73+
if [ "${#SELF_REPORTED}" -gt "${#COMMIT}" ]; then
74+
COMMIT="$SELF_REPORTED"
75+
fi
76+
77+
if [ -z "$COMMIT" ]; then
78+
echo "ERROR: no commit recorded for $BINARY." >&2
79+
echo "This host cannot run it, so the commit it was built from has to be" >&2
80+
echo "stated. From the checkout it was built on:" >&2
81+
echo >&2
82+
echo " $(basename "$0") $BINARY $VERSION \$(git rev-parse --short=12 HEAD)" >&2
83+
exit 1
84+
fi
85+
86+
case "$COMMIT" in
87+
unknown*)
88+
echo "ERROR: $BINARY reports commit '$COMMIT' - it was built somewhere git" >&2
89+
echo "could not be read, so nothing can say which source produced it." >&2
90+
exit 1 ;;
91+
*-dirty)
92+
echo "ERROR: $BINARY reports commit '$COMMIT' - it was built from a modified" >&2
93+
echo "tree and cannot be traced back to a released commit." >&2
94+
exit 1 ;;
95+
*[!0-9a-f]*)
96+
echo "ERROR: '$COMMIT' is not a commit hash." >&2
97+
exit 1 ;;
98+
esac
99+
100+
if [ "${#COMMIT}" -lt 7 ]; then
101+
echo "ERROR: commit '$COMMIT' is too short to name one commit; use at least 7" >&2
102+
echo "hex digits, as \`git rev-parse --short\` produces." >&2
103+
exit 1
104+
fi
105+
39106
touch "$MANIFEST"
40107
# One line per binary: re-stamping replaces the previous entry rather than
41108
# appending, so the manifest can never claim two versions for one file.
42109
tmp="$(mktemp)"
43110
grep -vF " $BINARY" "$MANIFEST" > "$tmp" 2>/dev/null || true
44-
printf '%s %s\n' "$VERSION" "$BINARY" >> "$tmp"
45-
sort -k2 "$tmp" > "$MANIFEST"
111+
printf '%s %s %s\n' "$VERSION" "$COMMIT" "$BINARY" >> "$tmp"
112+
sort -k3 "$tmp" > "$MANIFEST"
46113
rm -f "$tmp"
47114

48-
echo "Stamped $BINARY as $VERSION"
115+
echo "Stamped $BINARY as $VERSION ($COMMIT)"
49116
cat "$MANIFEST"

0 commit comments

Comments
 (0)