From d898b5659d0328eac49f183f4b59acd46f015c0a Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Wed, 9 Sep 2026 18:12:03 -0400 Subject: [PATCH] Give the release version its own file, outside the layer content hash Setting the release number was a platform change. FORGEFIRM_RELEASE sat in forgefirm-image.bb, the recipe hashes as content of meta-forgefirm, and a change to the content of a layer invalidates every acceptance result. So a version bump threw away the campaign that was meant to authorize that very release, and the number therefore had to be decided before the image the campaign ran on. Nothing said so: the release-flow page went straight from the kas configuration to the artifact and the pipeline, while the gate quietly required the recipe value, the rootfs stamp, the archive's meta-version and the tag to agree. v0.0.1 was cut on a tree whose number happened to be right; the next one would have cost a second campaign to discover the rule. The number moves to forgefirm-release.inc, which carries it and nothing else, and the manifest leaves that file out of the layer content hash exactly as it leaves out the component pin files (FORGEFIRM_MANIFEST_VERSION_SUFFIX, and the same list in scripts/manifest-from-tree.py, which computes the identity on a workstation and must agree byte for byte). release.sh reads the number from the new file. The version is metadata, not platform content, and this only makes the manifest say what it already meant: the version string was already outside the identity hash, and it was the file carrying it that defeated that. Nothing is weakened. release.sh still requires the number to equal the rootfs stamp, the .fw meta-version and the release tag, and image.health still compares the stamp on the running machine with the manifest's. Proven: the tree manifest is byte-identical across a bump from 0.0.1 to 0.0.2 (identity a64e51b8e5ecca0af683d4f0 either way, the meta-forgefirm layer hash unchanged), where before the two differed. bitbake resolves FORGEFIRM_RELEASE=0.0.1 and FORGEFIRM_VERSION_STRING=v0.0.1 for the release image through the new require, and the dev image still overrides the string with its build timestamp. --- .../classes/forgefirm-image-manifest.bbclass | 10 +++++++++- .../images/forgefirm-image.bb | 7 ++++++- .../images/forgefirm-release.inc | 18 ++++++++++++++++++ scripts/manifest-from-tree.py | 7 +++++-- scripts/release.sh | 11 +++++++---- 5 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 meta-forgefirm/recipes-forgefirm/images/forgefirm-release.inc diff --git a/meta-forgefirm/classes/forgefirm-image-manifest.bbclass b/meta-forgefirm/classes/forgefirm-image-manifest.bbclass index 5118cb5..70e4667 100644 --- a/meta-forgefirm/classes/forgefirm-image-manifest.bbclass +++ b/meta-forgefirm/classes/forgefirm-image-manifest.bbclass @@ -44,6 +44,13 @@ FORGEFIRM_MANIFEST_DIR ?= "${sysconfdir}/forgefirm-manifest.d" FORGEFIRM_MANIFEST_CONTENT_LAYERS ?= "meta-forgefirm meta-glowforge-bsp meta-openglow-core" FORGEFIRM_MANIFEST_PIN_SUFFIX ?= "-pin.inc" +# The release version file. Like a pin file it is metadata, not platform +# content: it carries FORGEFIRM_RELEASE and nothing else, the version +# string is already outside the identity hash (below), and the release +# gate proves the number against the rootfs stamp, the .fw meta-version +# and the tag. Hashing it would make every version bump a platform +# change and invalidate the campaign that authorizes the release. +FORGEFIRM_MANIFEST_VERSION_SUFFIX ?= "forgefirm-release.inc" do_rootfs[depends] += "virtual/kernel:do_deploy kernel-module-glowforge:do_deploy" @@ -88,7 +95,8 @@ def forgefirm_manifest_layers(d): and dirty flag of every layer checkout (informational).""" import os, subprocess content_layers = (d.getVar('FORGEFIRM_MANIFEST_CONTENT_LAYERS') or '').split() - skip = ('.md',) + tuple((d.getVar('FORGEFIRM_MANIFEST_PIN_SUFFIX') or '').split()) + skip = (('.md',) + tuple((d.getVar('FORGEFIRM_MANIFEST_PIN_SUFFIX') or '').split()) + + tuple((d.getVar('FORGEFIRM_MANIFEST_VERSION_SUFFIX') or '').split())) identity, build = {}, {} for layer in (d.getVar('BBLAYERS') or '').split(): name = os.path.basename(layer.rstrip('/')) diff --git a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb index 9287c22..62b33c5 100644 --- a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb +++ b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb @@ -113,7 +113,12 @@ IMAGE_ROOTFS_MAXSIZE = "204800" # Release images carry the release version; the dev image overrides the # string with the build timestamp (the same DATETIME as the artifact # name) plus a dev tag. -FORGEFIRM_RELEASE ?= "0.0.1" +# +# FORGEFIRM_RELEASE lives in its own file, which the manifest leaves out +# of the layer content hash: the version is metadata, and a bump must not +# read as a platform change and invalidate a campaign +# (forgefirm-release.inc). +require forgefirm-release.inc FORGEFIRM_VERSION_STRING ?= "v${FORGEFIRM_RELEASE}" write_forgefirm_version() { diff --git a/meta-forgefirm/recipes-forgefirm/images/forgefirm-release.inc b/meta-forgefirm/recipes-forgefirm/images/forgefirm-release.inc new file mode 100644 index 0000000..1b958c9 --- /dev/null +++ b/meta-forgefirm/recipes-forgefirm/images/forgefirm-release.inc @@ -0,0 +1,18 @@ +# The release version, and nothing else. +# +# This file is metadata, not platform content: the image manifest leaves +# it out of the layer content hash the way it leaves out the component +# pin files (forgefirm-image-manifest.bbclass, +# FORGEFIRM_MANIFEST_VERSION_SUFFIX). A version bump therefore changes +# the version and invalidates nothing, so the number can be set at the +# moment a release is cut rather than before the campaign that authorizes +# it. Setting it inside forgefirm-image.bb instead would hash as layer +# content and invalidate every acceptance result, which is what it did +# until v0.0.1. +# +# Nothing else belongs here. The version is still proven end to end: +# scripts/release.sh reads FORGEFIRM_RELEASE from this file and requires +# it to equal the rootfs stamp, the .fw meta-version and the release tag, +# and the acceptance test image.health compares the stamp on the running +# machine with the manifest's. +FORGEFIRM_RELEASE ?= "0.0.1" diff --git a/scripts/manifest-from-tree.py b/scripts/manifest-from-tree.py index fd27912..c0c5af1 100644 --- a/scripts/manifest-from-tree.py +++ b/scripts/manifest-from-tree.py @@ -52,8 +52,11 @@ CONTENT_LAYERS = {"meta-forgefirm": ("forgefirm", "meta-forgefirm"), "meta-glowforge-bsp": ("meta-openglow", "meta-glowforge-bsp"), "meta-openglow-core": ("meta-openglow", "meta-openglow-core")} # Left out of a layer's content, as in forgefirm-image-manifest.bbclass -# (FORGEFIRM_MANIFEST_PIN_SUFFIX): documentation and the component pin files. -LAYER_SKIP_SUFFIXES = (".md", "-pin.inc") +# (FORGEFIRM_MANIFEST_PIN_SUFFIX, FORGEFIRM_MANIFEST_VERSION_SUFFIX): +# documentation, the component pin files, and the release version file. +# All three are metadata; hashing them would turn a pin bump or a version +# bump into a platform change and invalidate every acceptance result. +LAYER_SKIP_SUFFIXES = (".md", "-pin.inc", "forgefirm-release.inc") def git(args, cwd=None, input=None): diff --git a/scripts/release.sh b/scripts/release.sh index 8dcd9ab..99e9a44 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -45,7 +45,7 @@ # forgefirm-source-v.tar.gz, and refuses to pack a bundle in which # a recipe that needs source has none. # -# Version contract: == FORGEFIRM_RELEASE in forgefirm-image.bb +# Version contract: == FORGEFIRM_RELEASE in forgefirm-release.inc # == /etc/forgefirm-version ("v") in the built rootfs == .fw # meta-version ("v") == release tag ("v"). @@ -55,6 +55,9 @@ REPO="$(cd "$(dirname "$0")/.." && pwd)" DEPLOY_ROOT="$REPO/build/tmp/deploy" DEPLOY="$DEPLOY_ROOT/images/glowforge" IMAGE_BB="$REPO/meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb" +# The release version, in its own file so a bump is not a platform change +# (the manifest leaves it out of the layer content hash). +RELEASE_INC="$REPO/meta-forgefirm/recipes-forgefirm/images/forgefirm-release.inc" INSTALLER="$REPO/scripts/install-forgefirm.sh" WARN_BYTES=$((170 * 1024 * 1024)) FAIL_BYTES=$((195 * 1024 * 1024)) @@ -134,7 +137,7 @@ if [ "$MODE" = "dev" ]; then EXT4="${EXT4/forgefirm-image-glowforge/forgefirm-image-dev-glowforge}" [ -f "$EXT4" ] || die "dev rootfs not found: $EXT4" check_size - REL=$(sed -n 's/^FORGEFIRM_RELEASE ?= "\(.*\)"/\1/p' "$IMAGE_BB") + REL=$(sed -n 's/^FORGEFIRM_RELEASE ?= "\(.*\)"/\1/p' "$RELEASE_INC") DEVVER="v${REL}-dev-$(date +%Y%m%d%H%M%S)" OUT="$DEPLOY/forgefirm-dev.fw" "$REPO/scripts/mkfw.sh" "$EXT4" "$DEVVER" "$OUT" "$KEY" @@ -163,9 +166,9 @@ else fi # Version single-source check. -BB_REL=$(sed -n 's/^FORGEFIRM_RELEASE ?= "\(.*\)"/\1/p' "$IMAGE_BB") +BB_REL=$(sed -n 's/^FORGEFIRM_RELEASE ?= "\(.*\)"/\1/p' "$RELEASE_INC") [ "$BB_REL" = "$VERSION" ] \ - || die "FORGEFIRM_RELEASE in forgefirm-image.bb is '$BB_REL', not '$VERSION'" + || die "FORGEFIRM_RELEASE in forgefirm-release.inc is '$BB_REL', not '$VERSION'" # The beta rule: every release below 0.1.0 is a beta, and 0.1.0 is the # first release that is not. While the README carries the beta banner, a