diff --git a/CLAUDE.md b/CLAUDE.md index 60e2bdcd..389c0546 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ make update-apps APPS="tenant redis" make show-target RELEASE_TAG=v1.3.0 # prints the resolved DOC_VERSION/BRANCH ``` -Required tools: Hugo extended v0.164.0, Go 1.23+, Node 22+, yq v4+ (for version lifecycle targets). +Required tools: Hugo extended v0.164.0, Go 1.23+, Node 22+, yq v4+ (for version lifecycle targets), jq (for `hack/download_openapi.sh` in the production build and `make update-all` without `RELEASE_TAG`). ## Architecture diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f61ca0d0..cdb10371 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -226,7 +226,9 @@ make show-target RELEASE_TAG=v1.3.0 # prints resolved DOC_VERSION / BRANCH ``` Required tools: Hugo extended 0.164.0, Go 1.23+, Node 22+, `yq` v4+ (for the -version lifecycle targets and Makefile routing). +version lifecycle targets and Makefile routing), `jq` (for +`hack/download_openapi.sh` in the production build and `make update-all` +without `RELEASE_TAG`). ## Where the architecture is implemented diff --git a/data/versions/next.yaml b/data/versions/next.yaml index 66ed53c2..5436f40d 100644 --- a/data/versions/next.yaml +++ b/data/versions/next.yaml @@ -4,7 +4,7 @@ # next/ trunk track upstream cozystack/cozystack@main: # # talos / talos_minor ← packages/core/talos/images/talos/profiles/installer.yaml @ main -# cozystack_version / _tag ← latest final release tag (the upcoming release's +# cozystack_version / _tag ← latest published release (the upcoming release's # own tag/assets don't exist until it is cut; # hack/release_next.sh overrides these from # RELEASE_TAG when next/ is promoted). diff --git a/data/versions/v1.6.yaml b/data/versions/v1.6.yaml index 829350c0..ca8db7b8 100644 --- a/data/versions/v1.6.yaml +++ b/data/versions/v1.6.yaml @@ -1,18 +1,13 @@ -# AUTOGENERATED by hack/update_versions.sh — do not edit by hand. +# Pinned upstream-tool versions for the Cozystack v1.6 docs. # -# Regenerated by 'make update-all' so the {{< version-pin >}} values in the -# next/ trunk track upstream cozystack/cozystack@main: -# -# talos / talos_minor ← packages/core/talos/images/talos/profiles/installer.yaml @ main -# cozystack_version / _tag ← latest final release tag (the upcoming release's -# own tag/assets don't exist until it is cut; -# hack/release_next.sh overrides these from -# RELEASE_TAG when next/ is promoted). +# Snapshotted from data/versions/next.yaml by hack/release_next.sh when v1.6 +# was released. 'make update-all' does not rewrite it; patch releases +# update it by hand (hack/release-checklist.md). # Cozystack release the docs are pinned to. cozystack_version: "1.6.0" cozystack_tag: "v1.6.0" -# Talos version shipped by the Cozystack installer for this trunk. +# Talos version shipped by the Cozystack installer for this minor. talos: "v1.13.6" talos_minor: "v1.13" # the docs minor used by talos.dev URLs diff --git a/hack/release_next.sh b/hack/release_next.sh index 1344edf0..c0892786 100755 --- a/hack/release_next.sh +++ b/hack/release_next.sh @@ -121,8 +121,10 @@ fi # 5. Snapshot data/versions/next.yaml → data/versions/$DOC_VERSION.yaml so the # {{< version-pin >}} shortcode in the released docs keeps resolving to the -# values that were true at the cut. next.yaml is unchanged; update it -# separately for the next development cycle. +# values that were true at the cut. next.yaml is unchanged. The upstream +# promote workflow (cozystack/cozystack promote-rc.yaml) regenerates it from +# the release staging branch before calling release-next, so the snapshot +# carries that release's Talos pins. VERSIONS_DIR="data/versions" NEXT_DATA="${VERSIONS_DIR}/next.yaml" TARGET_DATA="${VERSIONS_DIR}/${DOC_VERSION}.yaml" @@ -130,11 +132,22 @@ if [[ -f "$NEXT_DATA" ]]; then if [[ -e "$TARGET_DATA" ]]; then echo "! $TARGET_DATA already exists; leaving it as-is." >&2 else - cp "$NEXT_DATA" "$TARGET_DATA" + # Replace the trunk header (the leading comment block) and the trunk wording + # in section comments: both describe next.yaml, not a frozen snapshot. + { + echo "# Pinned upstream-tool versions for the Cozystack ${DOC_VERSION} docs." + echo "#" + echo "# Snapshotted from ${NEXT_DATA} by hack/release_next.sh when ${DOC_VERSION}" + echo "# was released. 'make update-all' does not rewrite it; patch releases" + echo "# update it by hand (hack/release-checklist.md)." + echo "" + awk 'h && /^#/ {next} h && /^$/ {h=0; next} {h=0; print}' h=1 "$NEXT_DATA" \ + | sed 's|for this trunk\.|for this minor.|' + } > "$TARGET_DATA" # Pin the release-coupled Cozystack version from RELEASE_TAG so a stale # next.yaml can't silently freeze the wrong version into the snapshot — # this is exactly how v1.5.yaml once inherited next.yaml's v1.3.0 values. - # Talos pins are left as snapshotted; they're refreshed manually per cycle. + # Talos pins are left as snapshotted. VERSION_BARE="${RELEASE_TAG#v}" sed -i.bak \ -e "s|^\(cozystack_version:[[:space:]]*\).*|\1\"${VERSION_BARE}\"|" \ diff --git a/hack/test_version_pins.sh b/hack/test_version_pins.sh new file mode 100755 index 00000000..994cb5f9 --- /dev/null +++ b/hack/test_version_pins.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Offline self-check for the data/versions/*.yaml pin pipeline +# (hack/update_versions.sh, hack/release_next.sh). +# Run from the repo root: hack/test_version_pins.sh +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +fail=0 +check() { # check + if [[ "$2" == "$3" ]]; then + echo "ok $1" + else + echo "FAIL $1: expected '$2', got '$3'"; fail=1 + fi +} + +# shellcheck source=hack/update_versions.sh +source hack/update_versions.sh + +# GitHub lists releases newest-created first. +check "resolver skips a draft and a prerelease" "v1.6.3" "$(latest_published_tag <<'EOF' +[ + {"tag_name": "v1.6.4", "draft": true, "prerelease": false}, + {"tag_name": "v1.7.0-rc.1", "draft": false, "prerelease": true}, + {"tag_name": "v1.6.3", "draft": false, "prerelease": false}, + {"tag_name": "v1.6.2", "draft": false, "prerelease": false} +] +EOF +)" + +check "resolver picks the highest version, not the newest release" "v1.6.3" "$(latest_published_tag <<'EOF' +[ + {"tag_name": "v1.5.9", "draft": false, "prerelease": false}, + {"tag_name": "v1.6.3", "draft": false, "prerelease": false}, + {"tag_name": "v1.5.8", "draft": false, "prerelease": false} +] +EOF +)" + +check "resolver prints nothing when no release is published" "" "$(latest_published_tag <<<'[{"tag_name": "v1.6.4", "draft": true, "prerelease": false}]')" + +sandbox="$(mktemp -d)" +trap 'rm -rf "$sandbox"' EXIT + +# update_versions.sh must send GITHUB_TOKEN on curl's stdin, never in argv +# where any process listing shows it. A PATH stub stands in for curl. +mkdir -p "$sandbox/bin" +cat > "$sandbox/bin/curl" <<'EOF' +#!/usr/bin/env bash +echo "argv: $*" >> "$CURL_LOG" +case "$*" in + *api.github.com*) + if [[ "$*" == *"--header @-"* ]]; then sed 's/^/stdin: /' >> "$CURL_LOG"; fi + echo '[{"tag_name": "v1.6.3", "draft": false, "prerelease": false}]' ;; + *) echo 'version: v1.13.6' ;; +esac +EOF +chmod +x "$sandbox/bin/curl" +run_update() { + CURL_LOG="$sandbox/curl.log" PATH="$sandbox/bin:$PATH" \ + hack/update_versions.sh --dest "$sandbox/pin.yaml" >/dev/null +} +GITHUB_TOKEN=s3cr3t run_update +check "token stays out of curl argv" "0" "$(grep --count '^argv: .*s3cr3t' "$sandbox/curl.log" || true)" +check "token is sent as a header on stdin" "1" "$(grep --count '^stdin: Authorization: token s3cr3t$' "$sandbox/curl.log" || true)" +check "pin file takes the resolved release" '"v1.6.3"' "$(awk '/^cozystack_tag:/{print $2}' "$sandbox/pin.yaml")" +rm "$sandbox/curl.log" +(unset GITHUB_TOKEN GH_TOKEN; run_update) +check "no token, plain request" "argv: -fsSL https://api.github.com/repos/cozystack/cozystack/releases?per_page=100" "$(grep 'api.github.com' "$sandbox/curl.log")" + +# Without jq the default tag cannot be resolved: the error must say so rather +# than claim upstream has no published release. An explicit tag needs no jq. +mkdir -p "$sandbox/nojq" +for tool in bash awk sed grep sort tail mkdir dirname cat; do + ln -s "$(type -P "$tool")" "$sandbox/nojq/$tool" +done +ln -s "$sandbox/bin/curl" "$sandbox/nojq/curl" +run_nojq() { + CURL_LOG="$sandbox/curl.log" PATH="$sandbox/nojq" \ + hack/update_versions.sh --dest "$sandbox/pin.yaml" "$@" 2>&1 +} +echo keep > "$sandbox/pin.yaml" +rc=0; out="$(run_nojq)" || rc=$? +check "no jq, no tag: exits non-zero" "1" "$rc" +check "no jq, no tag: error names jq" "1" "$(grep --count 'jq is required' <<<"$out" || true)" +check "no jq, no tag: pin file untouched" "keep" "$(cat "$sandbox/pin.yaml")" +run_nojq --cozystack-tag v1.6.2 >/dev/null +check "no jq, explicit tag: pins it" '"v1.6.2"' "$(awk '/^cozystack_tag:/{print $2}' "$sandbox/pin.yaml")" + +# release_next.sh must give the snapshot a released-version header while +# copying every value line except the release-coupled cozystack pins. +mkdir -p "$sandbox/hack" "$sandbox/data/versions" "$sandbox/content/en/docs/next" +cp hugo.yaml "$sandbox/" +cp hack/release_next.sh hack/register_version.sh "$sandbox/hack/" +cp data/versions/next.yaml "$sandbox/data/versions/" +printf -- '---\ntitle: "Next"\n---\n' > "$sandbox/content/en/docs/next/_index.md" +(cd "$sandbox" && ./hack/release_next.sh --release-tag v9.9.0 >/dev/null) +snapshot="$sandbox/data/versions/v9.9.yaml" +check "snapshot header no longer mentions the trunk" "0" "$(grep --count --ignore-case 'trunk' "$snapshot" || true)" +check "snapshot header names the released version" "1" "$(grep --count '^# .*Cozystack v9.9 docs' "$snapshot" || true)" +check "snapshot pins cozystack_tag to the release" '"v9.9.0"' "$(awk '/^cozystack_tag:/{print $2}' "$snapshot")" +values() { grep --invert-match --extended-regexp '^(#|$|cozystack_version:|cozystack_tag:)' "$1"; } +check "snapshot keeps every other value line" "$(values data/versions/next.yaml)" "$(values "$snapshot")" + +exit "$fail" diff --git a/hack/update_versions.sh b/hack/update_versions.sh index 2527b670..4a289f90 100755 --- a/hack/update_versions.sh +++ b/hack/update_versions.sh @@ -12,16 +12,17 @@ Sources of truth: * talos / talos_minor ← packages/core/talos/images/talos/profiles/installer.yaml at --branch (always the version main/the tag ships). * cozystack_version/_tag ← --cozystack-tag if given (e.g. an upcoming release - tag); otherwise the latest final upstream release - tag. Used as the `next` trunk's resolvable default - until the real release is cut. + tag); otherwise the highest published (non-draft, + non-prerelease) GitHub release, so every + releases/download// URL resolves. Used as the + `next` trunk's default until the real release is cut. Options: --dest PATH data/versions/.yaml file to (re)generate (required) --branch REF Git ref in cozystack/cozystack to read the Talos installer from (default: main) --cozystack-tag TAG Pin cozystack_tag to this vX.Y.Z tag instead of the latest - release (optional) + published release (optional) -h, --help Show this help and exit Examples: @@ -30,6 +31,18 @@ Examples: EOF } +# Reads a GitHub releases-list JSON array on stdin and prints the highest +# published final vX.Y.Z tag. A tag exists before its release is published, so +# the tag list alone can name a version whose assets are still missing. Sorted +# by version, not creation time: a patch of an older minor can be the newest. +latest_published_tag() { + jq -r '.[] | select(.draft == false and .prerelease == false) | .tag_name' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1 || true +} + +# Sourced by hack/test_version_pins.sh for latest_published_tag only. +[[ "${BASH_SOURCE[0]}" != "$0" ]] && return 0 + SOURCE_REPO="cozystack/cozystack" DEST="" BRANCH="main" @@ -73,11 +86,30 @@ talos_minor="${talos%.*}" # v1.13.0 -> v1.13 # -------------------- 2. Cozystack release tag -------------------- if [[ -z "$COZYSTACK_TAG" ]]; then - # Latest final release: top-level refs/tags/vX.Y.Z only (excludes the - # api/apps/v1alpha1/* submodule tags and any -rc/-beta pre-releases). - COZYSTACK_TAG="$(git ls-remote --tags --refs "https://github.com/${SOURCE_REPO}.git" 'v*.*.*' \ - | awk -F/ '/refs\/tags\/v[0-9]+\.[0-9]+\.[0-9]+$/{print $NF}' \ - | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1 || true)" + if ! command -v jq >/dev/null; then + echo "Error: jq is required to resolve the default cozystack tag; install jq or pass --cozystack-tag." >&2 + exit 1 + fi + # Optional token for the higher API rate limit. + token="${GITHUB_TOKEN:-${GH_TOKEN:-}}" + RELEASES_URL="https://api.github.com/repos/${SOURCE_REPO}/releases?per_page=100" + fetch_releases() { + if [[ -n "$token" ]]; then + # Header on stdin (curl >= 7.55.0): in argv the token shows in ps. + curl -fsSL --header @- "$RELEASES_URL" <<<"Authorization: token ${token}" + else + curl -fsSL "$RELEASES_URL" + fi + } + if ! releases="$(fetch_releases)"; then + echo "Error: GitHub releases API request failed: $RELEASES_URL (set GITHUB_TOKEN if rate-limited, or pass --cozystack-tag)." >&2 + exit 1 + fi + COZYSTACK_TAG="$(latest_published_tag <<<"$releases")" + if [[ -z "$COZYSTACK_TAG" ]]; then + echo "Error: no published vX.Y.Z release found at $RELEASES_URL." >&2 + exit 1 + fi fi if [[ ! "$COZYSTACK_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Error: could not determine a cozystack release tag (got '${COZYSTACK_TAG:-}')." >&2 @@ -94,7 +126,7 @@ cat > "$DEST" <