Files
antisocial/.gitea/workflows/publish.yml
T
thatguygriffandClaude Opus 5 503d8a8dec Move the working version on after a release
package.json has sat at 0.1.0 since the first commit, through three releases,
because nothing read it. That is fine right up until something does — an image
label, a health endpoint, a bug report quoting a version — at which point the
tree claims to be a version that shipped long ago.

A new `bump` job takes the tag that was just published, works out the next
patch from it, and commits that to main. After 1.2.0 the tree says 1.2.1: not
a version that exists, which is the point. A build from main is then legible
as "after 1.2.0" rather than as 1.2.0 itself.

It sits in publish.yml rather than a workflow of its own so that it can say
`needs: build`. A version that failed to publish has not been released, and
moving past it would say that it had. Prereleases are skipped for the same
reason -- 1.2.3-rc1 is a candidate for a version that has not shipped, so
there is nothing yet to move past.

The bump goes through `npm version` rather than editing the file. The version
is in the lockfile too, in two places, and a tree where those disagree is worse
than one that is merely out of date.

Three smaller things. The patch arithmetic forces base ten, because a patch
number written 08 is otherwise read as octal and kills the job. The commit
carries `[skip ci]`, or pushing it starts another build of the image that was
just published. And the committer is a name that is not a person at a reserved
address that can never become one, so nothing here names the instance it runs
on.

Pushing to main needs a token that may write to the repository. The Actions
task token can where the instance allows it; where it does not, setting a
VERSION_BUMP_TOKEN secret overrides it. A push that is refused fails the job
with both of those as the suggestion rather than a bare 403.

package.json goes to 1.2.1 here, which is where the job would have left it had
it existed when 1.2.0 went out.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017nMQ2eDKnqALYhAibpTKTu
2026-08-27 13:19:05 -03:00

241 lines
9.1 KiB
YAML

name: Publish
# Builds the application image and pushes it to a container registry.
#
# push to main -> :main and :sha-<short>
# tag 1.2.3 -> :1.2.3, :1.2, :1 and :latest
# pull request -> builds without pushing, so a broken Dockerfile is
# caught before it can move a published tag
#
# Configure with repository variables and secrets:
#
# vars.REGISTRY required, e.g. registry.example.com
# vars.IMAGE_NAME optional, defaults to this repository's owner/name
# vars.REGISTRY_USER optional, defaults to the actor running the workflow
# secrets.REGISTRY_TOKEN required to push
#
# Point REGISTRY at a host the runner reaches directly, without an intermediate
# proxy that caps request bodies: a browser image has layers well over 100MB,
# and such a proxy rejects them mid-push with `413 Payload Too Large`.
#
# If that host serves plain HTTP, the builder's Docker daemon also needs it in
# `insecure-registries` — that is daemon configuration, not something a
# workflow can set.
on:
push:
branches:
- main
tags:
# Glob, not regex: `[0-9]` is one digit and `*` is the rest, so these
# match 1.2.3 and 1.2.3-rc1 while ignoring tags that are not versions.
# A `+` here would be read as a literal plus.
- '[0-9]*.[0-9]*.[0-9]*'
- 'v[0-9]*.[0-9]*.[0-9]*'
pull_request:
paths:
- 'Dockerfile'
- 'package.json'
- 'package-lock.json'
- '.gitea/workflows/publish.yml'
workflow_dispatch:
jobs:
build:
name: Build and push
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check the runner can build images
run: |
if ! docker info >/dev/null 2>&1; then
echo "No usable Docker daemon in the job container." >&2
exit 1
fi
docker version --format 'client {{.Client.Version}} / server {{.Server.Version}} / arch {{.Server.Arch}}'
# Images are built natively, so each carries the architecture of the
# runner that built it. A runner of a different architecture joining the
# pool would overwrite these tags with its own arch, at which point this
# needs buildx and a manifest list.
- name: Work out the tags
id: meta
env:
REGISTRY: ${{ vars.REGISTRY }}
IMAGE_NAME: ${{ vars.IMAGE_NAME }}
run: |
set -euo pipefail
if [ -z "${REGISTRY}" ]; then
echo "The REGISTRY repository variable is not set." >&2
echo "Set it to the registry host to publish to, e.g. registry.example.com" >&2
exit 1
fi
image="${REGISTRY}/$(echo "${IMAGE_NAME:-${{ github.repository }}}" | tr '[:upper:]' '[:lower:]')"
tags=""
if [ "${{ github.ref_type }}" = "tag" ]; then
version="${{ github.ref_name }}"
version="${version#v}"
tags="${version}"
# Only a final release moves the rolling aliases; a prerelease is
# published under its own exact version and nothing else.
case "${version}" in
*-*) ;;
*)
major="${version%%.*}"
minor="${version%.*}"
tags="${tags} ${minor} ${major} latest"
;;
esac
else
tags="main sha-$(git rev-parse --short HEAD)"
fi
args=""
for tag in ${tags}; do args="${args} --tag ${image}:${tag}"; done
{
echo "image=${image}"
echo "tags=${tags}"
echo "args=${args}"
} >> "$GITHUB_OUTPUT"
echo "Publishing ${image} as:${tags// /, :}"
# The Actions task token is rejected by some registries, so pushing uses
# a token that belongs to a real user.
- name: Log in to the container registry
if: github.event_name != 'pull_request'
run: |
if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
echo "REGISTRY_TOKEN is not set." >&2
exit 1
fi
if ! echo "${{ secrets.REGISTRY_TOKEN }}" \
| docker login "${{ vars.REGISTRY }}" -u "${{ vars.REGISTRY_USER || github.actor }}" --password-stdin
then
echo >&2
echo "If that failed with 'server gave HTTP response to HTTPS client', the" >&2
echo "registry is plain HTTP and the builder's Docker daemon has to be told" >&2
echo "to allow it: add ${{ vars.REGISTRY }} to insecure-registries in the" >&2
echo "daemon config on the runner. The workflow cannot configure that." >&2
exit 1
fi
- name: Build
run: docker build --pull ${{ steps.meta.outputs.args }} --file Dockerfile .
- name: Push
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
for tag in ${{ steps.meta.outputs.tags }}; do
docker push "${{ steps.meta.outputs.image }}:${tag}"
done
echo "Published ${{ steps.meta.outputs.image }} as: ${{ steps.meta.outputs.tags }}"
- name: Log out
if: always() && github.event_name != 'pull_request'
run: docker logout "${{ vars.REGISTRY }}" || true
# Once a release is out, the version in package.json has already shipped.
# Moving it on to the next patch means the working tree is never sitting on
# a number that is published and immutable, and that a build from main is
# always identifiable as "after 1.2.0" rather than "1.2.0, but not really".
#
# `needs: build` is the point of putting this here rather than in a workflow
# of its own: a version that failed to publish has not been released, and
# bumping past it would say it had.
bump:
name: Move the working version on
needs: build
# Tags only, and only final ones. A prerelease has not shipped the version
# it is a candidate for, so there is nothing yet to move past.
if: github.ref_type == 'tag' && !contains(github.ref_name, '-')
runs-on: ubuntu-latest
container:
image: node:22
steps:
# The tag names a commit in main's history, but the bump belongs on the
# branch, so this checks out main rather than the tag.
- uses: actions/checkout@v4
with:
ref: main
# The task token can push only if the instance allows Actions to
# write to the repository. Where it does not, set VERSION_BUMP_TOKEN
# to a personal access token with write access and it is used
# instead.
token: ${{ secrets.VERSION_BUMP_TOKEN || secrets.GITEA_TOKEN }}
- name: Work out the next patch version
id: next
run: |
set -euo pipefail
version="${{ github.ref_name }}"
version="${version#v}"
major="${version%%.*}"
rest="${version#*.}"
minor="${rest%%.*}"
patch="${rest##*.}"
# `10#` forces base ten: a patch number written 08 would otherwise be
# read as octal and fail to parse.
next="${major}.${minor}.$((10#${patch} + 1))"
echo "next=${next}" >> "$GITHUB_OUTPUT"
echo "Released ${version}; the working version becomes ${next}"
- name: Bump package.json
id: bump
env:
NEXT: ${{ steps.next.outputs.next }}
run: |
set -euo pipefail
current="$(node -p "require('./package.json').version")"
if [ "${current}" = "${NEXT}" ]; then
echo "package.json is already ${NEXT}; nothing to do."
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# npm rather than editing the file: the version is in the lockfile
# too, in more than one place, and they have to agree.
npm version "${NEXT}" --no-git-tag-version --allow-same-version >/dev/null
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "package.json ${current} -> ${NEXT}"
- name: Commit it to main
if: steps.bump.outputs.changed == 'true'
env:
NEXT: ${{ steps.next.outputs.next }}
run: |
set -euo pipefail
# A name that is not a person, and a reserved address that can never
# resolve to one. Nothing here names the instance it runs on.
git config user.name 'Release bot'
git config user.email '[email protected]'
git add package.json package-lock.json
# `[skip ci]` because this commit is a number and nothing else:
# without it the push to main starts another build of the very image
# that was just published.
git commit -m "Set the working version to ${NEXT} [skip ci]"
if ! git push origin HEAD:main; then
echo >&2
echo "Could not push the version bump to main. Either the Actions" >&2
echo "token has no write access to this repository, or main is" >&2
echo "protected against direct pushes. Set VERSION_BUMP_TOKEN to a" >&2
echo "token that may push to main, or allow that token past the" >&2
echo "branch protection." >&2
exit 1
fi