diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 0984183..1cc8a97 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -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"