# 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. # # 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. name: Release on: push: branches: [main] paths: - VERSION jobs: release: runs-on: linux-docker container: image: debian:trixie-slim permissions: contents: write 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" python3 -m venv tools/.venv tools/.venv/bin/pip install --quiet --upgrade pip tools/.venv/bin/pip install --quiet -r tools/requirements.txt - 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 - 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" - 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. payload="$(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}')" release="$(curl -sS -f -X POST "${API}/releases" \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ -d "$payload")" id="$(printf '%s' "$release" | jq -r '.id')" echo "Created release ${TAG} (id ${id})." for asset in "${NAME}.tar.gz" "${NAME}.tar.gz.sha256"; 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}"