Files
forgefirm/scripts/release.sh
T
ScottW514 c3da9fa1bd Catalog: setup.account-login, motion.cancel-abort, forgectrl.settings-bounds and exthost.service leave; the rootfs lint takes image.health's file checks
The catalog is 43. Each test leaves for a host proof that its
component's CI runs at the pins:

- setup.account-login: the login rules are host_routes.sh's (forgectrl
  6c2bb38); the render is test_image.py's new AccountRenderTests, which
  runs the layer's forgefirm-users.init with reload in a private mount
  namespace as root (a fresh /run, the scratch account files over /etc,
  the scratch data directory, root's, over /data): the data directory
  found 0700 is 0711 after the render, and as the account's own uid the
  home is entered and written while listing the data directory and
  reading the users record are EACCES. A control without the script's
  chmod go+x fails it (700 != 711). forgectrl.auth's cover widens to
  src/ui/login.*, since login.html had no other cover.
- motion.cancel-abort: laser_lifecycle_test.py's new rule 17 on the
  null-sink controller: 0x85 stops a jog short of its target, Idle; ^X
  mid-move lands in Alarm with the position kept and no return to the
  job start; $X recovers to Idle and the next jog ships steps (counted
  in GFSINK_DUMP). It passes on the driver at its pin, badc832.
  motion.deadman requires motion.pacing in its place.
- forgectrl.settings-bounds: host_routes.sh refuses the out-of-range and
  over-length values with the settings file byte for byte the same, and
  the lid lamp under GF_SYSFS_ROOT follows lid_lamp_idle (forgectrl
  6c2bb38).
- exthost.service: forgeext's host tests at the pinned bb75b23 -
  sandbox_test (the confinement from inside, the pulse device, the
  settings file and the setup record refused), run_test.py (the API,
  safe mode, the master switch, and the killed host under the init
  script's own wrapper), install_test.py and sdk_test.py.
  exthost.armed-freeze requires exthost.platform in its place.

HOST_TESTED rows for each path the lint named once exthost.service
left: forgectrl src/logs.* to logs_test.c and
docs/advisories/extensions.md to host_setup.sh; forgeext src/api.*,
caps.*, evfeed.*, holdkeep.*, httpreq.*, manifest.*, quota.*,
sandbox.*, settings.*, state.* to their unit tests, netrules.* to
netrules_test.py, call.* to sdk_test.py, install.*, pkg.* and index.* to
install_test.py, and main.c, fflog.* and CMakeLists.txt to run_test.py.
test_manifest's behavioral-path case is unaffected.

The rootfs lint takes image.health's file checks (rules init-order,
logging, watchdog, read-only, dhcpv6, and on a release image factory),
each proved in RootfsLintTests by a drift that fails it. image.health
keeps the live half: the rendered issue and motd against the running
manifest, one host and no pool process, watchdog0, no userspace
watchdog running, the mounts, the dev image's factory slots read-only,
no DHCPv6 client running, the DHCP client's -b and parent, ifup, the
getty, and IPv6. release.sh drops its PermitRootLogin and
PermitEmptyPasswords greps, its ROOTFS_READ_ONLY check and its /factory
fstab check, which the lint holds; it keeps the empty root shadow
field, / mounted ro, and the three HostKey lines under /data.

setup.py registers no test now: its helpers the deleted tests alone
called go (tls_base, tcp_open, mode, press_status, wiz_summary,
wait_settled, the login constants), and test_setup_suite.py keeps the
cases of what remains.

Fingerprints: image.health's moves (its text), every test in
exthost.py, forgectrl.py and motion.py moves with its module text, and
the layer content hash moves with rootfs-lint.sh: the next image
inherits no campaign.

Proof: the coverage lint over the tree manifest from the pins finds 0
uncovered paths and 0 host-tested row problems (43 tests); pyflakes on
the touched files is clean; forgetest's unit suite under Linux passes
(334), test_image.py's 28 among them. The lint passes image
20261015142442's release tree as release and its dev tree as dev, and
refuses the dev tree as release (sshd, factory).
laser_lifecycle_test.py passes whole on the null-sink build of badc832.
2026-10-15 14:02:20 -04:00

454 lines
20 KiB
Bash

#!/bin/bash
# Copyright 2020-2026 514 LLC d/b/a OpenGlow
# Written by Scott Wiederhold
# https://community.openglow.org
# SPDX-License-Identifier: MIT
#
# ForgeFIRM release pipeline (runs on the Yocto build host).
#
# release.sh <version> [--publish] full release: gates, build, pack,
# sign, checksums, stage, publish cmd
# (the acceptance gate reads
# releases/v<version>/acceptance.json)
# release.sh --dev build + pack a dev-signed .fw of the dev image for
# the GUI upload path; no staging
#
# Environment:
# FWUP host fwup for packing (default: fwup in PATH)
# FWUP_COMPAT factory-era fwup 0.14.2 binary; when set, the
# packed archive is verified with it (raw-format
# key), replicating the factory-compat guarantee
# FORGEFIRM_SIGNING_KEY private key for release mode (REQUIRED - no
# default, so key choice is always deliberate)
# FORGEFIRM_DEV_KEY private key for --dev mode (REQUIRED for --dev)
# RELEASE_STAGING_DIR where release assets are staged
# (default: <repo>/release-staging)
# FORGEFIRM_ACCEPTANCE_SKIP set to 1 to bypass the acceptance gate
# deliberately (never the default; see the site,
# Developers, "Acceptance")
# FORGEFIRM_SOURCE_SKIP set to 1 to build a release without the source
# bundle. The licenses of the software in the image
# make source necessary, so this is never the
# default, and it is REFUSED with --publish: a
# release that conveys the binaries without the
# corresponding source is a license violation, which
# no release flag cures. Building without publishing
# is allowed; the staged release is marked a
# prerelease and carries NO-SOURCE.txt.
# FORGEFIRM_DOCS_DIR the forgefirm-docs checkout to tag with this
# release (default: <repo>/../forgefirm-docs). The
# firmware and the documentation that describes it
# share a tag, so the documentation that agrees with
# a machine can be found from its version.
# FORGEFIRM_DOCS_SKIP set to 1 to release without tagging the
# documentation. Never the default.
#
# The source bundle: a release build merges kas/source-bundle.yml, so the
# build writes the source of every recipe of the image beside the image.
# scripts/source-bundle.py packs that source, the license manifests, the
# license texts and the ForgeFIRM layers into
# forgefirm-source-v<version>.tar.gz, and refuses to pack a bundle in which
# a recipe that needs source has none.
#
# Version contract: <version> == FORGEFIRM_RELEASE in forgefirm-release.inc
# == /etc/forgefirm-version ("v<version>") in the built rootfs == .fw
# meta-version ("v<version>") == release tag ("v<version>").
set -euo pipefail
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))
die () { echo "RELEASE FAILED: $*" >&2; exit 1; }
warn () { echo "WARNING: $*" >&2; }
VERSION=""
MODE=release
PUBLISH=0
for ARG in "$@"; do
case "$ARG" in
--dev) MODE=dev ;;
--publish) PUBLISH=1 ;;
-*) die "unknown option $ARG" ;;
*)
[ -z "$VERSION" ] \
|| die "multiple versions given ('$VERSION' and '$ARG')"
VERSION="$ARG"
;;
esac
done
# The source bundle belongs to a release. A --dev archive goes to one bench
# and publishes nothing, so it builds without the archiver.
SOURCE_BUNDLE=0
if [ "$MODE" = release ] && [ -z "${FORGEFIRM_SOURCE_SKIP:-}" ]; then
SOURCE_BUNDLE=1
fi
# Decided first, and refused before any tool or key is looked for, because it
# needs neither. A rootfs no campaign authorized is a quality problem that the
# prerelease flag can label; conveying the GPL binaries of this image with no
# corresponding source is a license violation, and no release flag cures one.
if [ "$MODE" = release ] && [ "$SOURCE_BUNDLE" != 1 ] && [ "$PUBLISH" = 1 ]; then
die "FORGEFIRM_SOURCE_SKIP cannot be combined with --publish: a published release must carry its source"
fi
FWUP="${FWUP:-fwup}"
command -v "$FWUP" >/dev/null || die "fwup not found (set FWUP=)"
command -v kas >/dev/null || die "kas not found on PATH"
build_images () {
CFG="kas/forgefirm-glowforge.yml"
TARGETS="forgefirm-image forgefirm-image-dev"
if [ "$SOURCE_BUNDLE" = 1 ]; then
# The archiver rides the release build, so the source that the bundle
# publishes is the source that this image is built from. The overlay
# adds tasks and nothing else: no file of the root filesystem and no
# component changes, so the image manifest and the acceptance result
# are the same with it and without it. The boot loader, the kernel and
# the kernel module reach the machine outside the root filesystem, so
# they are named as targets as well (scripts/source-bundle.py,
# BUILD_TARGETS).
CFG="$CFG:kas/source-bundle.yml"
TARGETS="$TARGETS u-boot virtual/kernel kernel-module-glowforge"
fi
echo "== building images ($CFG) =="
( cd "$REPO" && kas shell "$CFG" -c "bitbake $TARGETS" ) \
|| die "bitbake failed"
}
resolve_ext4 () {
EXT4=$(readlink -f "$DEPLOY/forgefirm-image-glowforge.rootfs.ext4")
[ -s "$EXT4" ] || die "release ext4 not found in $DEPLOY"
}
check_size () {
SZ=$(stat -c%s "$EXT4")
[ "$SZ" -lt "$FAIL_BYTES" ] \
|| die "rootfs is $SZ bytes - too close to the 200 MiB slot"
if [ "$SZ" -ge "$WARN_BYTES" ]; then
warn "rootfs is $((SZ / 1048576)) MiB - $(( (FAIL_BYTES - SZ) / 1048576 )) MiB of margin left before the release gate"
fi
}
# --- dev mode -----------------------------------------------------------------
if [ "$MODE" = "dev" ]; then
KEY="${FORGEFIRM_DEV_KEY:?set FORGEFIRM_DEV_KEY to the dev signing key}"
build_images
resolve_ext4
# The dev archive carries the dev image (forgetest, the bench tools), not
# the release rootfs: what the bench installs is what the bench runs.
#
# No size gate here. The dev rootfs is about 368 MiB - forgetest and the
# bench tools - so it does not fit a 200 MiB eMMC slot and is never
# installed into one. It goes on an SD card, which the boot selector
# carries as a first-class location, either as the .wic.gz written to the
# card or by pointing fwup at the card rather than a slot. check_size
# guards the slot, so it applies to the release rootfs alone.
EXT4="${EXT4/forgefirm-image-glowforge/forgefirm-image-dev-glowforge}"
[ -f "$EXT4" ] || die "dev rootfs not found: $EXT4"
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"
echo "== dev archive ready: $OUT ($DEVVER) =="
exit 0
fi
# --- release mode -------------------------------------------------------------
[ -n "$VERSION" ] || die "usage: release.sh <version> [--publish] | release.sh --dev"
KEY="${FORGEFIRM_SIGNING_KEY:?set FORGEFIRM_SIGNING_KEY to the release signing key}"
PUB="${KEY%.priv}.pub"
[ -f "$KEY" ] || die "signing key '$KEY' not found"
[ -f "$PUB" ] || die "public key '$PUB' not found"
echo "== gates =="
# Repo state (informational when the build tree is not a git checkout).
if git -C "$REPO" rev-parse --git-dir >/dev/null 2>&1; then
[ -z "$(git -C "$REPO" status --porcelain)" ] \
|| die "working tree is dirty - release from a clean tree"
if ! git -C "$REPO" diff --quiet "@{upstream}" 2>/dev/null; then
warn "HEAD differs from upstream - push before publishing"
fi
else
warn "$REPO is not a git checkout - repo-state gates skipped (rsynced build tree)"
fi
# Version single-source check.
BB_REL=$(sed -n 's/^FORGEFIRM_RELEASE ?= "\(.*\)"/\1/p' "$RELEASE_INC")
[ "$BB_REL" = "$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
# version at or above 0.1.0 is a mistake, not a release.
if grep -q 'ForgeFIRM is in beta' "$REPO/README.md"; then
MAJOR=${VERSION%%.*}; REST=${VERSION#*.}; MINOR=${REST%%.*}
case "$MAJOR.$MINOR" in
0.0) ;;
*) die "version $VERSION is not a beta number, and the README says ForgeFIRM is in beta (0.0.x only)" ;;
esac
fi
# The installer must embed the pubkey matching the signing key, or every
# install will refuse the published archive.
INST_HEX=$(sed -n "s/^PUBKEY='\(.*\)'$/\1/p" "$INSTALLER" | tr -d '\\x')
KEY_HEX=$(base64 -d "$PUB" | xxd -p | tr -d '\n')
[ -n "$INST_HEX" ] || die "cannot extract the embedded pubkey from the installer"
[ "$INST_HEX" = "$KEY_HEX" ] \
|| die "installer's embedded pubkey does not match the signing key - update install-forgefirm.sh"
build_images
resolve_ext4
check_size
# Rootfs version stamp.
STAMP=$(debugfs -R "cat /etc/forgefirm-version" "$EXT4" 2>/dev/null)
[ "$STAMP" = "v$VERSION" ] \
|| die "rootfs stamp is '$STAMP', expected 'v$VERSION'"
# Acceptance gate: the committed acceptance artifact must authorize THIS
# build. scripts/acceptance-gate.py recomputes every catalog test's domain
# fingerprint from the manifest inside the release rootfs and requires the
# recorded PASS to match (https://docs.forgefirm.org/developers/acceptance/).
# A release is never signed
# without it; FORGEFIRM_ACCEPTANCE_SKIP=1 bypasses deliberately and loudly.
ART="$REPO/releases/v$VERSION/acceptance.json"
GATED=1
if [ -n "${FORGEFIRM_ACCEPTANCE_SKIP:-}" ]; then
warn "acceptance gate SKIPPED by FORGEFIRM_ACCEPTANCE_SKIP - this release carries no acceptance proof"
GATED=0
else
[ -f "$ART" ] \
|| die "no acceptance artifact at releases/v$VERSION/acceptance.json - run the campaign on the bench, export, commit"
REL_MANIFEST=$(mktemp)
debugfs -R "cat /etc/forgefirm-manifest.json" "$EXT4" > "$REL_MANIFEST" 2>/dev/null
[ -s "$REL_MANIFEST" ] \
|| { rm -f "$REL_MANIFEST"; die "release rootfs carries no /etc/forgefirm-manifest.json"; }
python3 "$REPO/scripts/acceptance-gate.py" "$ART" "$REL_MANIFEST" --machine glowforge \
|| { rm -f "$REL_MANIFEST"; die "acceptance gate refused this build (see the table above)"; }
rm -f "$REL_MANIFEST"
echo "acceptance gate OK ($ART)"
fi
# Root policy gate. The release image ships root WITHOUT a password (the
# serial console is the recovery path): root's shadow field must be empty.
# sshd's refusal of root and of empty passwords is the rootfs lint's
# (meta-forgefirm/recipes-forgefirm/images/rootfs-lint.sh), which fails
# the build. Read the built file, not the recipes: this catches a drift
# however it got in (recipe, local.conf, an inherited class).
SSHD_CONFIG=$(debugfs -R "cat /etc/ssh/sshd_config" "$EXT4" 2>/dev/null)
[ -n "$SSHD_CONFIG" ] \
|| die "release rootfs carries no /etc/ssh/sshd_config"
ROOT_SHADOW=$(debugfs -R "cat /etc/shadow" "$EXT4" 2>/dev/null \
| awk -F: '$1=="root"{print; exit}')
[ -n "$ROOT_SHADOW" ] \
|| die "release rootfs has no root entry in /etc/shadow"
ROOT_PW=$(printf '%s\n' "$ROOT_SHADOW" | awk -F: '{print $2}')
[ -z "$ROOT_PW" ] \
|| die "release rootfs has a non-empty root password field: the policy is an empty field (empty-root-password in forgefirm-image.bb); a build drift"
echo "root policy gate OK (root field empty)"
# Read-only rootfs gate. The release rootfs mounts read-only: the root
# line of the built fstab carries ro, and sshd keeps its host keys on
# /data, where the read-only rootfs cannot hold them. rcS's
# ROOTFS_READ_ONLY and the fstab with no factory slot are the rootfs
# lint's.
FSTAB=$(debugfs -R "cat /etc/fstab" "$EXT4" 2>/dev/null)
[ -n "$FSTAB" ] \
|| die "release rootfs carries no /etc/fstab"
printf '%s\n' "$FSTAB" | awk '$1 == "/dev/root" && $2 == "/" { print $4 }' \
| grep -Eq '(^|,)ro(,|$)' \
|| die "release fstab does not mount / read-only (base-files fstab or read-only-rootfs drift?)"
[ "$(printf '%s\n' "$SSHD_CONFIG" | grep -c '^HostKey /data/forgefirm/ssh/')" = 3 ] \
|| die "release sshd_config does not keep the host keys under /data/forgefirm/ssh (recipes-connectivity/openssh drift?)"
echo "read-only rootfs gate OK (/ ro, host keys on /data)"
# Config-level guard: debug-tweaks must not sit in the shared kas config,
# where it would apply to every target including the release image.
if ( cd "$REPO" && kas dump kas/forgefirm-glowforge.yml 2>/dev/null ) \
| grep -q 'debug-tweaks'; then
die "debug-tweaks appears in the resolved kas config - it must live only in forgefirm-image-dev.bb"
fi
echo "kas config gate OK (no debug-tweaks in the shared config)"
echo "== pack + sign =="
STAGE="${RELEASE_STAGING_DIR:-$REPO/release-staging}/v$VERSION"
mkdir -p "$STAGE"
"$REPO/scripts/mkfw.sh" "$EXT4" "v$VERSION" "$STAGE/forgefirm.fw" "$KEY"
# Factory-era compat verification (fwup 0.14.2 wants raw 32-byte keys).
if [ -n "${FWUP_COMPAT:-}" ]; then
RAW=$(mktemp)
base64 -d "$PUB" > "$RAW"
"$FWUP_COMPAT" -V -i "$STAGE/forgefirm.fw" -p "$RAW" \
|| { rm -f "$RAW"; die "factory-era fwup rejects the archive"; }
rm -f "$RAW"
echo "factory-era fwup verification OK"
elif [ "$MODE" = release ] && [ -z "${FWUP_COMPAT_SKIP:-}" ]; then
# The factory-compat guarantee is a release property: a public release
# must not skip it silently. FWUP_COMPAT_SKIP=1 bypasses deliberately.
die "FWUP_COMPAT not set - factory-era verification is required for a release (set FWUP_COMPAT_SKIP=1 to bypass deliberately)"
else
warn "FWUP_COMPAT not set - factory-era verification skipped"
fi
echo "== stage assets =="
cp -L "$DEPLOY/forgefirm-image-glowforge.rootfs.wic.gz" "$STAGE/forgefirm-image-glowforge.rootfs.wic.gz"
# The acceptance artifact travels with the release (see the site,
# Developers, "Acceptance") - only when the gate accepted it for THIS
# rootfs. A skipped gate ships no artifact: an acceptance.json next to a
# rootfs it never authorized would read as proof. The release says so
# instead, and goes out as a prerelease.
ASSETS="forgefirm.fw sha256sums.txt forgefirm-image-glowforge.rootfs.wic.gz"
PRERELEASE=""
rm -f "$STAGE/acceptance.json" "$STAGE/acceptance.md" "$STAGE/NO-ACCEPTANCE.txt"
if [ "$GATED" = 1 ] && [ -f "$ART" ]; then
cp "$ART" "$STAGE/acceptance.json"
ASSETS="$ASSETS acceptance.json"
if [ -f "${ART%.json}.md" ]; then
cp "${ART%.json}.md" "$STAGE/acceptance.md"
ASSETS="$ASSETS acceptance.md"
fi
else
cat > "$STAGE/NO-ACCEPTANCE.txt" <<NOTE
ForgeFIRM v$VERSION was signed with the acceptance gate skipped
(FORGEFIRM_ACCEPTANCE_SKIP). No acceptance campaign authorized this
rootfs. Treat it as a prerelease.
NOTE
ASSETS="$ASSETS NO-ACCEPTANCE.txt"
PRERELEASE="--prerelease"
fi
# The source bundle. It is packed from the license manifests of THIS
# rootfs, and source-bundle.py stops the release when a recipe of the image
# has no source (see the site, Developers, "Release flow").
rm -f "$STAGE"/forgefirm-source-v*.tar.gz "$STAGE/NO-SOURCE.txt"
if [ "$SOURCE_BUNDLE" = 1 ]; then
echo "== source bundle =="
python3 "$REPO/scripts/source-bundle.py" "$VERSION" \
--deploy "$DEPLOY_ROOT" --image-name "$(basename "$EXT4" .ext4)" \
--out "$STAGE" \
|| die "the source bundle failed"
ASSETS="$ASSETS forgefirm-source-v$VERSION.tar.gz"
else
warn "source bundle SKIPPED by FORGEFIRM_SOURCE_SKIP - this release publishes no source"
cat > "$STAGE/NO-SOURCE.txt" <<NOTE
ForgeFIRM v$VERSION was built with the source bundle skipped
(FORGEFIRM_SOURCE_SKIP). The image contains GPL and other copyleft
software whose licenses require the corresponding source to accompany
the binaries, and this staging directory does not carry it.
Do not publish these assets as they stand. Rebuild without
FORGEFIRM_SOURCE_SKIP, or attach the matching source bundle before the
binaries reach anyone.
NOTE
ASSETS="$ASSETS NO-SOURCE.txt"
PRERELEASE="--prerelease"
fi
# Every attached file is bound to the release by the sums, the artifact
# included.
( cd "$STAGE" && sha256sum $(echo "$ASSETS" | tr ' ' '\n' | grep -v '^sha256sums.txt$') > sha256sums.txt )
ls -la "$STAGE"
# --- the documentation tag ----------------------------------------------------
#
# Firmware on a machine needs the documentation that agrees with it, so the
# docs repository carries the same tag as the release. The tag is made here and
# pushed with the release, never before: a tag on documentation that never
# shipped is worse than no tag at all.
DOCS_TAG_CMD=""
if [ -n "${FORGEFIRM_DOCS_SKIP:-}" ]; then
warn "docs tag SKIPPED by FORGEFIRM_DOCS_SKIP - this release ships no matching documentation tag"
else
DOCS_DIR="${FORGEFIRM_DOCS_DIR:-$REPO/../forgefirm-docs}"
[ -d "$DOCS_DIR/.git" ] \
|| die "no forgefirm-docs checkout at $DOCS_DIR (set FORGEFIRM_DOCS_DIR, or FORGEFIRM_DOCS_SKIP=1 to release without one)"
[ -z "$(git -C "$DOCS_DIR" status --porcelain)" ] \
|| die "forgefirm-docs has uncommitted changes; commit them before a release"
if git -C "$DOCS_DIR" rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null 2>&1; then
echo "docs: tag v$VERSION already exists in $DOCS_DIR"
else
git -C "$DOCS_DIR" tag -a "v$VERSION" -m "ForgeFIRM v$VERSION" \
|| die "cannot tag forgefirm-docs"
echo "docs: tagged $DOCS_DIR at v$VERSION"
fi
echo "docs: v$VERSION -> $(git -C "$DOCS_DIR" rev-parse --short HEAD)"
DOCS_TAG_CMD="git -C $DOCS_DIR push origin v$VERSION"
fi
# The release notes: releases/v<version>/notes.md when the release carries
# one (staged beside the assets, not uploaded as one), GitHub's generated
# notes appended otherwise.
#
# Every release ends with the same Installation Information block. GPLv3
# section 6 asks that it be provided WITH the object code, not merely be
# possible, and the release page is where the object code is conveyed. The
# source bundle's README carries the same text for the source route.
NOTES_OPT="--notes-file notes.md"
if [ -f "$REPO/releases/v$VERSION/notes.md" ]; then
cp "$REPO/releases/v$VERSION/notes.md" "$STAGE/notes.md"
echo "notes: releases/v$VERSION/notes.md"
else
# No hand-written notes: GitHub's generated changelog carries the release,
# and gh appends it to the body the notes file provides.
printf 'ForgeFIRM v%s. Installation, usage, and safety are documented at\nhttps://docs.forgefirm.org/.\n' \
"$VERSION" > "$STAGE/notes.md"
NOTES_OPT="$NOTES_OPT --generate-notes"
echo "notes: generated by GitHub, with the installation block"
fi
cat >> "$STAGE/notes.md" <<'EOF'
## Installing your own build
Full instructions: <https://docs.forgefirm.org/install/updating/#installing-your-own-build>.
Building the image is documented at
<https://docs.forgefirm.org/developers/building/>.
EOF
# Only a release that carries the source bundle may point at it. The
# no-source path cannot be published at all, but its staged notes must not
# claim an asset that is not there either.
if [ "$SOURCE_BUNDLE" = 1 ]; then
cat >> "$STAGE/notes.md" <<'EOF'
The source of everything in this image is the `forgefirm-source-v*.tar.gz`
asset on this release.
EOF
fi
cat <<EOF
== release v$VERSION staged ==
Pre-publish checklist (docs.forgefirm.org, Developers, "Release flow"):
- meta-openglow pushed; kas config flipped to the pinned-remote block
- kas lock refreshed
- forgefirm-docs current for this release (the currency rule) and pushed
Publish (from a directory with an authenticated gh):
cd "$STAGE"
gh release create "v$VERSION" --repo openglow-org/forgefirm \\
--title "ForgeFIRM v$VERSION" $NOTES_OPT $PRERELEASE \\
$ASSETS
Push the documentation tag with it:
$DOCS_TAG_CMD
EOF
if [ "$PUBLISH" = "1" ]; then
command -v gh >/dev/null || die "--publish requested but gh is not on PATH"
( cd "$STAGE" && gh release create "v$VERSION" --repo openglow-org/forgefirm \
--title "ForgeFIRM v$VERSION" $NOTES_OPT $PRERELEASE \
$ASSETS ) \
|| die "gh release create failed"
echo "== published v$VERSION =="
fi