Files
inbuxa-admin/.gitlab-ci.yml
T
jcoffey-dev 25e33767af Release weekly and publish the image from GitLab
Ports release.yml and publish.yml, the two workflows left behind when the
GitHub account was suspended; nothing has released INBUXA Admin since.

weekly-release keeps release.yml's decision unchanged: on a schedule that
sets RELEASE_WEEKLY=1, count the commits on main since the newest published
release, stop if there are none, and otherwise pick today's version
(YYYY.M.D, .2 and on for a second release in a day), commit it to main in
inbuxa-version.json and cut the release at that commit. DRY_RUN=1 stops
after the decision.

What changes is how it writes. GitHub's job pushed the bump with
GITHUB_TOKEN; here the bump goes through the commits API with RELEASE_TOKEN,
and last_commit_id pins it to the commit the job counted from, so a main
that moved underneath makes the job fail rather than release something the
notes don't describe. The release and its tag go through the releases API
with the same token, which as an ordinary push starts a tag pipeline --
replacing publish.yml's workflow_call, which only existed because a
GITHUB_TOKEN release raises no event.

publish runs in that tag pipeline, after the build job has built and
tested the tag again. It pushes a two-architecture image (arm64 under QEMU,
as ihasmail's does) to the project registry,
registry.coffeylabs.org/inbuxa/inbuxa-admin, tagged with the version and
latest. Only date tags publish, not the inherited v1.0.x, and a tag whose
commit's inbuxa-version.json says anything else is refused, so an image
never reports a version other than its tag.

Tag lookups use git show-ref, after ihasmail's port found rev-parse
--verify reading some tag names as describe output on the git in these
images.

Needs, on the project: a RELEASE_TOKEN variable (project access token,
Maintainer, api scope, protected and masked) whose role may push to main,
and a pipeline schedule on main, Mondays 09:37 UTC, setting
RELEASE_WEEKLY=1.
2026-09-21 10:31:09 -07:00

211 lines
10 KiB
YAML

# CI on the self-hosted GitLab, ported from .github/workflows/ when the GitHub
# account was suspended on 2026-09-20. The Actions files stay in the tree: they
# are the reference this was written from and work unchanged if the appeal
# succeeds.
#
# Every `image:` is pinned by digest, with its tag in the trailing comment --
# the replacement for the workflows' SHA-pinned actions, since GitLab has no
# action allowlist. Read the comment for the version; the digest is what runs.
#
# The runner is the inbuxa group runner on Web_Host, with the host docker
# socket bound in, as ihasmail's is. Jobs reach GitLab and its registry on the
# internal network, never through https://git.coffeylabs.org, which is
# Cloudflare-proxied and caps request bodies at 100 MB.
#
# Not ported:
# * cleanup.yml pruned GHCR with dataaxiom/ghcr-cleanup-action; on GitLab
# that belongs in the project's container registry cleanup policy, not in
# a pipeline.
stages: [build, publish, release]
default:
interruptible: true
build:
stage: build
image: node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9 # 22-bookworm-slim
variables:
NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.npm"
cache:
key:
files: [package-lock.json]
paths: [.npm/]
script:
- npm ci --ignore-scripts
- npm run typecheck
- npm run lint
- npm test
- npm run build
rules:
- if: $RELEASE_WEEKLY == "1"
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# A release tag is built and tested again before its image is published.
- if: $CI_COMMIT_TAG
# ------------------------------------------------------------- publish ------
# Port of publish.yml, to the project's own registry now that GHCR went with
# the GitHub account: registry.coffeylabs.org/inbuxa/inbuxa-admin.
#
# Tag-driven. publish.yml was called from release.yml because a release cut
# with GITHUB_TOKEN raises no event; GitLab has no such rule, so the tag the
# weekly release creates starts a tag pipeline, and this builds from it.
#
# Only date tags publish (v2026.9.21, v2026.9.21.2). The repository still
# carries the inherited v1.0.x tags, and a tag of any other shape pushed by
# hand is not a release.
#
# The tag must agree with inbuxa-version.json at the commit it names -- the
# property release.yml was built around: the tree a tag points at reports the
# version the tag claims. A tag placed beside an unbumped file fails here
# rather than publishing an image that reports the wrong version.
#
# Both architectures build under QEMU on this amd64 host, where publish.yml
# had a native arm64 runner. That is slow -- tens of minutes for npm ci and
# the Vite build through instruction translation -- and tolerable for a weekly
# tag, which is why this is tag-only. If arm64 starts timing out, the fix is
# an arm64 runner, not dropping the platform.
publish:
stage: publish
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
needs: [build]
variables:
DOCKER_BUILDKIT: "1"
IMAGE: $CI_REGISTRY_IMAGE
before_script:
- apk add --no-cache jq >/dev/null
- |
set -eu
VERSION="$(jq -er .version inbuxa-version.json)"
if [ "$CI_COMMIT_TAG" != "v$VERSION" ]; then
echo "Tag $CI_COMMIT_TAG names a commit whose inbuxa-version.json says $VERSION." >&2
echo "Refusing to publish an image that would report the wrong version." >&2
exit 1
fi
echo "VERSION=$VERSION" > version.env
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- docker run --privileged --rm tonistiigi/binfmt --install arm64
- docker buildx create --use --name ci-builder --driver docker-container || docker buildx use ci-builder
script:
- . ./version.env
# Attestations are off, as they were in publish.yml: they add manifests of
# their own to the index.
- |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--provenance=false --sbom=false \
--tag "$IMAGE:$VERSION" \
--tag "$IMAGE:latest" \
--push .
- docker buildx imagetools inspect "$IMAGE:$VERSION"
after_script:
- docker logout "$CI_REGISTRY" || true
rules:
- if: $CI_COMMIT_TAG =~ /^v[0-9]{4}\.[0-9]+\.[0-9]+(\.[0-9]+)?$/
# ------------------------------------------------------- weekly release -----
# Port of release.yml: cut a release once a week, but only if there is
# something in it. The decision is the workflow's, unchanged -- count the
# commits on main since the newest published release, and skip the week if
# there are none. A release with nothing in it moves :latest to an identical
# build, spends a version number, and notifies everybody about nothing.
#
# The version is the date, YYYY.M.D unpadded, with a .N suffix from 2 for a
# second release on one day. It is committed to main in inbuxa-version.json
# and the tag names that commit, so the commit is the release.
#
# It runs from a pipeline schedule (Mondays 09:37 UTC, as release.yml did)
# that sets RELEASE_WEEKLY=1. GitLab keeps schedules on the project, not in
# this file, so the schedule and this job only work as a pair. Run it by hand
# with RELEASE_WEEKLY=1, adding DRY_RUN=1 to see the decision and stop.
#
# Everything that writes uses RELEASE_TOKEN, a project access token
# (Maintainer, `api` scope; protected, masked), never CI_JOB_TOKEN, which can
# neither commit nor raise a tag pipeline:
# * the bump is committed through the commits API, with last_commit_id set
# to the commit this job counted from. If main moved meanwhile the API
# refuses and the job fails; run it again. Otherwise the notes and the
# count would describe a different commit from the one released.
# * the release -- and with it the tag -- is created through the releases
# API. A tag made that way is an ordinary push, so it starts the tag
# pipeline and `publish` builds the image.
# The token's role must be allowed to push to main. The token expires; when
# it does this fails loudly at the first API call, and a new one goes in the
# same variable.
weekly-release:
stage: release
image: node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9 # 22-bookworm-slim
# One at a time: two overlapping runs would race to write the same version
# and create the same tag.
resource_group: weekly-release
variables:
GIT_DEPTH: "0"
before_script:
- apt-get update -qq && apt-get install -y -qq --no-install-recommends git curl jq ca-certificates >/dev/null
# The build directory is shared between jobs, and a checkout owned by
# another user makes git refuse with "detected dubious ownership".
- git config --global --add safe.directory "$CI_PROJECT_DIR"
script:
- |
set -euo pipefail
API="http://gitlab/api/v4/projects/${CI_PROJECT_ID}"
auth=(--header "PRIVATE-TOKEN: ${RELEASE_TOKEN}")
git fetch -q --tags origin
sha="$(git rev-parse HEAD)"
# The newest published release, or empty on a project that has never
# had one -- in which case everything counts as new.
previous="$(curl -fsS "${auth[@]}" "${API}/releases?order_by=released_at&sort=desc&per_page=1" | jq -r '.[0].tag_name // ""')"
# A release can outlive its tag; falling back to the whole history
# over-counts, which cuts a release that was due anyway. Tag lookups use
# show-ref, which matches an exact ref: rev-parse --verify on this git
# can read some tag names as describe output and "find" a tag that
# isn't there (see ihasmail's port).
if [ -n "$previous" ] && git show-ref --verify --quiet "refs/tags/${previous}"; then
count="$(git rev-list --count "${previous}..HEAD")"; range="${previous}..HEAD"
else
count="$(git rev-list --count HEAD)"; range="HEAD"
fi
if [ "$count" -eq 0 ]; then
echo "Nothing to release: no commits since ${previous}."; exit 0
fi
today="$(date -u +%Y.%-m.%-d)"
version="$today"; n=2
while git show-ref --verify --quiet "refs/tags/v${version}"; do
version="${today}.${n}"; n=$((n + 1))
done
tag="v${version}"
echo "Releasing ${tag} -- ${count} commit(s) since ${previous:-the beginning}, from ${sha}."
if [ "${DRY_RUN:-0}" = "1" ]; then echo "DRY_RUN=1: stopping here."; exit 0; fi
# The bump, written with a JSON parser rather than sed: a version put
# into JSON by string substitution is one stray quote from a file
# nothing can read.
VERSION="$version" node -e '
const fs = require("fs");
const f = "inbuxa-version.json";
const j = JSON.parse(fs.readFileSync(f, "utf8"));
j.version = process.env.VERSION;
fs.writeFileSync(f, JSON.stringify(j, null, 2) + "\n");
'
jq -n --arg msg "Version ${version}" --arg sha "$sha" --rawfile content inbuxa-version.json \
'{branch:"main", commit_message:$msg, last_commit_id:$sha,
actions:[{action:"update", file_path:"inbuxa-version.json", content:$content}]}' > commit.json
bump="$(curl -fsS "${auth[@]}" --header "Content-Type: application/json" \
--data @commit.json "${API}/repository/commits" | jq -er .id)"
echo "committed the bump as ${bump}"
# Notes bounded to what is new: one line per change on main's
# first-parent history, which is what GitHub's generated notes listed.
notes="$(git log --first-parent --format='- %s' "$range")"
jq -n --arg tag "$tag" --arg ref "$bump" --arg name "INBUXA Admin ${version}" \
--arg desc "$(printf '%s commit(s) since %s.\n\n%s' "$count" "${previous:-the beginning}" "$notes")" \
'{tag_name:$tag, ref:$ref, name:$name, description:$desc}' > release.json
curl -fsS "${auth[@]}" --header "Content-Type: application/json" \
--data @release.json "${API}/releases" | jq -r '"created release " + .tag_name'
rules:
- if: $RELEASE_WEEKLY == "1" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH