docker build . stamps the git tag or short commit, not dev (closes #154)
check / check (push) Failing after 56s

script/build writes the version script/version prints into
dist/package.json, which quak --version reports: the VERSION environment
variable or build arg when one is given, otherwise git describe --tags
--always, otherwise package.json's version. A checkout with .git whose
version comes out empty, dev or unknown fails the build.

.dockerignore no longer leaves out .git, and ARG VERSION has no default,
so a plain docker build . of a clone stamps its commit. script/docker and
script/cibuild still pass the host's version. make build-bin bundles the
built dist/, so the single binary reports the same version.

Model: opus-5-5
This commit is contained in:
2026-10-02 00:44:56 +00:00
parent 10afa7a7f4
commit 3c8377cb2c
11 changed files with 265 additions and 27 deletions
+23 -9
View File
@@ -1,7 +1,7 @@
#!/bin/sh
# script/build: compile the TypeScript sources into dist/, then verify that
# the artifacts package.json advertises are among the files the compiler
# actually wrote. tsc reports success by exit status alone and knows nothing
# script/build: compile the TypeScript sources into dist/, stamp the version
# script/version prints into it, then verify that the artifacts package.json
# advertises are among the files the compiler actually wrote. tsc reports success by exit status alone and knows nothing
# about the manifest, so without this step a green build can still ship a
# package whose main, types or bin resolve to nothing. Our own extension to
# scripts-to-rule-them-all.
@@ -46,13 +46,24 @@ for (const bin of bins) {
}
# src/index.ts imports ../package.json for the version, which tsc copies to
# dist/package.json. Running the built CLI proves that import resolves from
# dist/ and reports the version package.json declares.
# dist/package.json. The version script/version prints is written into that
# copy only; the repo's own package.json is left as it is.
stamp_version() {
node -e '
const { readFileSync, writeFileSync } = require("node:fs");
const pkg = JSON.parse(readFileSync("dist/package.json", "utf-8"));
pkg.version = process.argv[1];
writeFileSync("dist/package.json", JSON.stringify(pkg, null, 4) + "\n");
' "$1"
}
# Running the built CLI proves the import resolves from dist/ and reports
# the stamped version.
verify_version() {
built="$(node dist/bin/quak.js --version)"
declared="$(node -p 'require("./package.json").version')"
if [ "$built" != "$declared" ]; then
echo "build: dist/bin/quak.js reports $built, package.json declares $declared" >&2
if [ "$built" != "$1" ]; then
echo "build: dist/bin/quak.js reports $built, the build stamped $1" >&2
exit 1
fi
echo "build: dist/bin/quak.js reports version $built"
@@ -60,9 +71,12 @@ verify_version() {
main() {
cd "$ROOT"
# Own line, so that a failing script/version stops the build.
version="$("$ROOT/script/version")"
yarn run tsc
stamp_version "$version"
verify_entrypoints
verify_version
verify_version "$version"
}
main "$@"
+3 -3
View File
@@ -15,9 +15,9 @@ main() {
cd "$ROOT"
# Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an
# empty constant. VERSION is computed here because .dockerignore
# excludes .git, so `git describe` in a build stage yields an empty
# version without failing.
# empty constant. The version resolved here goes in as the VERSION
# build arg, which takes precedence over what the build would derive
# from the .git in its context.
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown"
docker build --no-cache \
+3 -3
View File
@@ -12,9 +12,9 @@ main() {
cd "$ROOT"
# Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an
# empty constant. VERSION is computed here because .dockerignore
# excludes .git, so `git describe` in a build stage yields an empty
# version without failing.
# empty constant. The version resolved here goes in as the VERSION
# build arg, which takes precedence over what the build would derive
# from the .git in its context.
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown"
docker build --no-cache \
Executable
+39
View File
@@ -0,0 +1,39 @@
#!/bin/sh
# script/version: print the version script/build stamps into the built
# package. Our own extension to scripts-to-rule-them-all.
#
# Order of precedence:
#
# 1. $VERSION, if set and not empty: an explicit value, such as the
# Dockerfile's VERSION build arg.
# 2. If this checkout has .git, `git describe --tags --always`: the tag
# on a tagged commit, otherwise the short commit.
# 3. Otherwise, as in a source tarball, the version package.json declares.
#
# A checkout with .git whose version still comes out empty, dev or unknown
# fails: git is missing or could not read the checkout, and the build could
# not be traced back to its commit.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
version="${VERSION:-}"
if [ -e .git ]; then
if [ -z "$version" ]; then
version="$(git describe --tags --always || true)"
fi
case "$version" in
"" | dev | unknown)
echo "version: $ROOT has .git, but the version came out '$version'" >&2
exit 1
;;
esac
elif [ -z "$version" ]; then
version="$(node -p 'require("./package.json").version')"
fi
echo "$version"
}
main "$@"