probe: release.yml with a stubbed release job and a red harness step (throwaway, #232)

This commit is contained in:
torben committed 2026-10-14 13:51:44 +00:00
1 parent 86ead61f83
commit add6b3d43a
1 file changed
+7 -284
+7 -284
View File
@@ -1,48 +1,10 @@
# Publish a release when VERSION moves on main.
#
# The release artifact is exactly a `dist export` tree, packed with a top-level
# directory: unpack it, run instructions/setup-instance.md, and there is a
# working wiki instance - no checkout of this repo required. CI already proved
# that path works before this workflow ever runs. Beside it the release carries
# tools/preflight.sh and tools/preflight.ps1 as assets, which download and unpack
# that tarball themselves, and the two instructions an agent reads before there is
# a tree to read them in: setup-instance.md, which the installation sentence in
# INSTALL.md points at, and the preflight.md its first step leads to.
#
# The tag is created here, by CI, and never by an agent: AGENTS.md invariant 5
# ("never call raw git commit/push") stays intact because nothing in a session
# has to tag anything.
#
# Auth is `${{ gitea.token }}` - the short-lived per-job token this Gitea
# instance issues - not a 1Password secret. Nothing here reaches outside the
# instance, so the Zero-Trust secret path that the container-build workflows
# use has nothing to carry.
#
# `wiki-mirror` is the second job: once `release` has published a release, it
# mirrors that tree's docs into the Gitea wiki (.gitea/scripts/wiki_mirror.py
# converts, wiki-push.sh pushes; wiki-mirror.yml does the same by hand for any
# tag). It is a separate job so that a failing mirror leaves the release and its
# tag standing: the release is the product, the wiki a view of it. It does not
# run for a `-beta.N` candidate, for which `release` sets `skip`.
#
# `converters` is the first job: the converter tests against the real documents of the private
# repository `torben/chemenu-testfiles` (converters.yml has the reasoning and is the reference for
# the steps; the two are kept in step by hand, because this instance's support for calling one
# workflow from another is not something this repository relies on). `release` waits for it with
# `needs:`, so a red harness stops the run before a release or a tag exists. For a candidate it
# decides `skip` the way `release` does and ends after its first steps.
# THROWAWAY PROBE for #232 - lives only on branch gate-probe-232, never merged.
# Same job layout as the real release.yml (converters -> needs -> release); the release job is
# a stub that only prints REACHED, so no release or tag can come out of this run.
name: Release
on:
push:
branches: [main]
paths:
- VERSION
# A release whose job failed after VERSION had already moved cannot be
# retried by a push - the version is not raised again - and a re-run uses the
# workflow file of the failed commit. Dispatching on main runs the current
# file; the "already exists" check below still refuses a second release.
workflow_dispatch:
jobs:
@@ -72,7 +34,6 @@ jobs:
set -eu
case "$(tr -d '[:space:]' < VERSION)" in
*-beta.*)
echo "VERSION is a running candidate - the converter tests run for a release only."
echo "skip=true" >> "$GITHUB_OUTPUT"
;;
*)
@@ -98,255 +59,17 @@ jobs:
- name: Real documents
if: steps.version.outputs.skip != 'true'
# Red on purpose: no test matches the keyword, so pytest exits 5.
run: |
set -eu
cd tools
.venv/bin/python -m pytest -c pytest.ini ../testfiles/harness -q --durations=5
.venv/bin/python -m pytest -c pytest.ini ../testfiles/harness -q -k does_not_exist_probe
release:
needs: converters
runs-on: linux-docker
container:
image: debian:trixie-slim
permissions:
contents: write
outputs:
skip: ${{ steps.version.outputs.skip }}
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
env:
WIKITOOL_SESSION_ID: release-${{ github.run_id }}
WIKI_TRACE_DIR: /tmp/wikitool-trace
BUILD_DIR: /tmp/build
# The address a *reader* uses. `github.server_url` is whatever the runner
# registered against (an internal one here), which is right for the API
# call below and wrong for a URL baked into every distributed instance.
PUBLIC_BASE_URL: https://gitea.nehmer.net
steps:
- name: System dependencies
# `nodejs` is for act_runner, not for us - see the note in ci.yml.
run: |
set -eu
apt-get update -qq
apt-get install -y --no-install-recommends \
python3 python3-venv git nodejs ripgrep ca-certificates curl jq tar gzip
rm -rf /var/lib/apt/lists/*
- uses: actions/checkout@v7
- name: Tool environment
run: |
set -eu
git config --global --add safe.directory "$GITHUB_WORKSPACE"
tools/preflight.sh
- name: Resolve the version and refuse to re-release it
id: version
env:
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TOKEN: ${{ gitea.token }}
run: |
set -eu
version="$(cat VERSION | tr -d '[:space:]')"
# A running candidate (`X.Y.Z-beta.N`) is never released - betas are
# a dev-checkout state, not a distributed one (see
# instructions/dev/version-parts.md). This guard sits *before* the
# API query below: without it, every `version bump` on a candidate
# would push VERSION and trigger a wasted round-trip against the
# releases API for a tag that was never going to be created. Ending
# the job cleanly here (not `exit 1`) is what keeps a beta bump a
# normal, unremarkable push rather than a failing CI run - skipping
# every later step is what "cleanly" means in Actions: mark this one
# skip and gate the rest on it.
case "$version" in
*-beta.*)
echo "VERSION is a running candidate (${version}) - nothing to release. Skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
echo "skip=false" >> "$GITHUB_OUTPUT"
tag="v${version}"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
status="$(curl -s -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${TOKEN}" "${API}/releases/tags/${tag}")"
if [ "$status" = "200" ]; then
echo "Release ${tag} already exists. VERSION was touched without being raised;"
echo 'bump it with `tools/wikitool version bump` instead of re-releasing.'
exit 1
fi
echo "No release ${tag} yet - proceeding."
- name: Release notes from CHANGES.md
# `version notes` fails when the changelog has no entry for this
# version, which is the last place that mistake can still be caught.
#
# The footer below settles Gitea #47's second side-finding: a release
# note is written once, at tag time, and a later correction to
# CHANGES.md never reaches it - `gitea-mcp` has no release-edit method,
# and delete-and-recreate would destroy the attached tarball assets that
# INSTALL.md and `version check` point at. That happened for real to
# v4.4.0, whose note carried a fact that the corpus had already
# corrected. Rather than build a correction path for a text nobody can
# edit, the snapshot says it is one and names where the maintained
# version lives. A stale note then costs a reader one click instead of
# a wrong belief. Appended here rather than inside `version notes`,
# which is a general-purpose extractor whose other callers (a local
# preview, a pipe) should not inherit a release-page footer.
if: steps.version.outputs.skip != 'true'
run: |
set -eu
tools/wikitool docs verify
tools/wikitool version notes > /tmp/release-notes.md
cat >> /tmp/release-notes.md <<'EOF'
---
*This note is a snapshot of the `CHANGES.md` entry as it stood when the tag was cut, and
is never edited afterwards. The maintained version of this text - including any later
correction - is the entry for this version in `CHANGES.md` in the repository.*
EOF
cat /tmp/release-notes.md
# Gitea keeps a release note in a TEXT column, which on this instance's
# MySQL holds 65535 bytes. A longer note fails the API call after the
# tarball is built; refusing here keeps the margin visible and the tag
# uncreated. The fix is a shorter CHANGES.md entry, not a cut note.
size="$(wc -c < /tmp/release-notes.md)"
if [ "$size" -gt 60000 ]; then
echo "Release notes are ${size} bytes; Gitea stores at most 65535 on MySQL."
echo "Shorten this version's CHANGES.md entry, then dispatch this workflow on main."
exit 1
fi
- name: Build the distribution tarball
id: build
if: steps.version.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
run: |
set -eu
name="chemenu-stack-${VERSION}"
mkdir -p "$BUILD_DIR"
tools/wikitool dist export "${BUILD_DIR}/${name}" \
--source-repo "${PUBLIC_BASE_URL}/${GITHUB_REPOSITORY}" \
--source-commit "${GITHUB_SHA}" \
--release-url "${PUBLIC_BASE_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG}" \
--update-url "${PUBLIC_BASE_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/latest"
tar -czf "${BUILD_DIR}/${name}.tar.gz" -C "$BUILD_DIR" "$name"
( cd "$BUILD_DIR" && sha256sum "${name}.tar.gz" > "${name}.tar.gz.sha256" )
cat "${BUILD_DIR}/${name}.tar.gz.sha256"
echo "name=${name}" >> "$GITHUB_OUTPUT"
# The two preflight scripts are attached to the release as well: the first
# thing a new user runs, before there is any tree to run it from. Each copy
# is the tree's script with the download address of *this* release written
# into its two placeholder lines (the tree copy keeps them empty, which is
# how a script knows it is not a release asset). The address is the public
# one, for the same reason as the URLs in `dist export` above.
download="${PUBLIC_BASE_URL}/${GITHUB_REPOSITORY}/releases/download/${TAG}"
sed \
-e "s|^RELEASE_ARCHIVE_URL=''|RELEASE_ARCHIVE_URL='${download}/${name}.tar.gz'|" \
-e "s|^RELEASE_CHECKSUM_URL=''|RELEASE_CHECKSUM_URL='${download}/${name}.tar.gz.sha256'|" \
tools/preflight.sh > "${BUILD_DIR}/preflight.sh"
sed \
-e "s|^\$ReleaseArchiveUrl = ''|\$ReleaseArchiveUrl = '${download}/${name}.tar.gz'|" \
-e "s|^\$ReleaseChecksumUrl = ''|\$ReleaseChecksumUrl = '${download}/${name}.tar.gz.sha256'|" \
tools/preflight.ps1 > "${BUILD_DIR}/preflight.ps1"
# A placeholder that did not match would ship a script that refuses to run.
grep -qF "RELEASE_ARCHIVE_URL='${download}/${name}.tar.gz'" "${BUILD_DIR}/preflight.sh"
grep -qF "RELEASE_CHECKSUM_URL='${download}/${name}.tar.gz.sha256'" "${BUILD_DIR}/preflight.sh"
grep -qF "ReleaseArchiveUrl = '${download}/${name}.tar.gz'" "${BUILD_DIR}/preflight.ps1"
grep -qF "ReleaseChecksumUrl = '${download}/${name}.tar.gz.sha256'" "${BUILD_DIR}/preflight.ps1"
chmod +x "${BUILD_DIR}/preflight.sh"
# The two instructions as the tarball carries them (dist export has already
# stripped them), not as this checkout holds them.
for doc in setup-instance.md preflight.md; do
cp "${BUILD_DIR}/${name}/instructions/${doc}" "${BUILD_DIR}/${doc}"
done
- name: Publish the release
if: steps.version.outputs.skip != 'true'
env:
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TOKEN: ${{ gitea.token }}
TAG: ${{ steps.version.outputs.tag }}
NAME: ${{ steps.build.outputs.name }}
run: |
set -eu
# Creating the release creates the tag, pinned to this commit.
# The payload goes through a file: as one argument it is bounded by
# Linux's 128 KiB per-argument limit, which the 8.0.0 notes exceeded
# ("curl: Argument list too long").
jq -n \
--arg tag "$TAG" \
--arg target "$GITHUB_SHA" \
--arg name "$TAG" \
--rawfile body /tmp/release-notes.md \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body,
draft: false, prerelease: false}' > /tmp/release-payload.json
release="$(curl -sS -f -X POST "${API}/releases" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @/tmp/release-payload.json)"
id="$(printf '%s' "$release" | jq -r '.id')"
echo "Created release ${TAG} (id ${id})."
for asset in "${NAME}.tar.gz" "${NAME}.tar.gz.sha256" preflight.sh preflight.ps1 setup-instance.md preflight.md; do
curl -sS -f -X POST "${API}/releases/${id}/assets?name=${asset}" \
-H "Authorization: token ${TOKEN}" \
-F "attachment=@${BUILD_DIR}/${asset}" > /dev/null
echo "Uploaded ${asset}."
done
echo "Done: ${PUBLIC_BASE_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
wiki-mirror:
# Runs only after `release` succeeded and created a release (a candidate
# sets skip=true). `release` never depends on this job.
needs: release
if: needs.release.outputs.skip != 'true'
runs-on: linux-docker
container:
image: debian:trixie-slim
permissions:
contents: write
wiki: write
env:
VERSION: ${{ needs.release.outputs.version }}
TAG: ${{ needs.release.outputs.tag }}
PUBLIC_BASE_URL: https://gitea.nehmer.net
steps:
- name: System dependencies
run: |
set -eu
apt-get update -qq
apt-get install -y --no-install-recommends python3 git nodejs ca-certificates curl jq
rm -rf /var/lib/apt/lists/*
- uses: actions/checkout@v7
- name: Mirror the docs into the wiki
env:
WIKI_TOKEN: ${{ gitea.token }}
run: |
set -eu
python3 .gitea/scripts/wiki_mirror.py --source . --out /tmp/wiki-out \
--version "$VERSION" --repo-url "${PUBLIC_BASE_URL}/${GITHUB_REPOSITORY}"
.gitea/scripts/wiki-push.sh /tmp/wiki-out "$VERSION"
- name: Say which half failed
if: failure()
run: |
echo "Release ${TAG} is published; only the wiki mirror failed."
echo "The release and its tag stand. Dispatch wiki-mirror.yml with tag ${TAG} to retry the mirror."
- name: Stub
run: echo "REACHED - the release job started"