No description
  • JavaScript 81.4%
  • Dockerfile 10.3%
  • Shell 8.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Gilles Fritz 2818b84a36
All checks were successful
ci / build (push) Successful in 5m0s
publish-release: send a publish report to Argus (#15)
Closes #14

Monitoring plan, phase 5 (argus/backend#22, PR argus/backend#42).

## Changes
- `publish-release`: after the release (numbered or devel), if `ARGUS_UPLOAD_TOKEN` is set, POSTs a `publish` report (JSON) to `${ARGUS_URL:-https://reports.stationmyr.net}/api/projects/<repo>/reports/`:
  - version (`devel-<sha7>` for the rolling snapshot), kind, date;
  - artifacts: each `--asset` (size from disk, download URL from the release re-read after attaching) + `--artifact "name|bytes|url"` (e.g. a Docker image);
  - links: the release page + `--link "label|url"`;
  - dependencies: `--dependencies FILE` (`name==v`, `name@v` incl. scoped `@a/b@v`, `name v`; `#` comments);
  - ref = `GITHUB_REF_NAME` (default master), commit = HEAD, run URL, actor.
- No token: logged and skipped. Upload failure: `:⚠️:` only, the release stays done.
- README; tests (`node --test release-notes/*.test.mjs`: 27 passed).

Needs argus/backend#42 deployed before a repo sets the token (older Argus refuses `report_type=publish`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014BSmBtGWeFBD9z4mYm9SjA
Reviewed-on: #15
2026-10-29 23:25:19 +00:00
.forgejo/workflows Prune jobs: announce expect_every for Argus silent-source alerts (#13) 2026-10-28 18:33:30 +00:00
release-notes publish-release: send a publish report to Argus (#15) 2026-10-29 23:25:19 +00:00
build.sh bugfix: add libatomic1 to python variant, fixes node 24 crash (#11) 2026-10-28 08:34:00 +00:00
deploy.Dockerfile Update alpine Docker tag to v3.24 (#2) 2026-09-30 06:50:26 +00:00
docker.Dockerfile Add publish-release: idempotent numbered releases, devel stays a draft 2026-10-24 10:03:46 +02:00
golang.Dockerfile Add publish-release: idempotent numbered releases, devel stays a draft 2026-10-24 10:03:46 +02:00
node.Dockerfile Let Renovate handle the build-arg Dockerfiles properly 2026-10-24 21:18:28 +02:00
python.Dockerfile bugfix: add libatomic1 to python variant, fixes node 24 crash (#11) 2026-10-28 08:34:00 +00:00
README.md publish-release: send a publish report to Argus (#15) 2026-10-29 23:25:19 +00:00
renovate.json Let Renovate handle the build-arg Dockerfiles properly 2026-10-24 21:18:28 +02:00

ci-base

ci image sizes pnpm-store docker-host buildx

Shared Forgejo Actions job base images for the other repos on git.stationmyr.net — avoids every job reinstalling git/node/curl/jq from scratch on every run, since Forgejo Actions job containers are ephemeral (nothing persists between runs) and the stock upstream images (python:*-slim, docker:*-cli, node:*-alpine, golang:*) don't ship git or node, both needed by actions/checkout (a JS action).

Variants

Tag Base Adds Used by
git.stationmyr.net/guardians_of_the_galaxy/ci-base:python-3.10 … :python-3.15 python:3.1x-slim git, nodejs, npm, pnpm, curl, jq, tox, build, twine poulpinou-django/vieuxhibou-django's test matrix (one leg per Python version)
git.stationmyr.net/guardians_of_the_galaxy/ci-base:python same as :python-3.15 (floating alias) git, nodejs, npm, pnpm, curl, jq, tox, build, twine jobs that just need "a" python and don't care which version (ws-backend, ws-frontend, poulpinou-django/vieuxhibou-django's lint/publish, vieuxhibou-gin's release job)
git.stationmyr.net/guardians_of_the_galaxy/ci-base:docker docker:29-cli git, nodejs, curl, jq ws-backend's docker job
git.stationmyr.net/guardians_of_the_galaxy/ci-base:node-22, :node-26 node:2x-alpine git, curl, jq, pnpm ws-frontend's unit job, bigb-vue's publish/cleanup jobs
git.stationmyr.net/guardians_of_the_galaxy/ci-base:node same as :node-26 (floating alias) git, curl, jq, pnpm jobs that just need "a" node and don't care which version
git.stationmyr.net/guardians_of_the_galaxy/ci-base:golang golang:1.27 git, nodejs, curl, jq vieuxhibou-gin's test job
git.stationmyr.net/guardians_of_the_galaxy/ci-base:deploy alpine:3.20 openssh-client ws-backend's/vieuxhibou's deploy job

Every variant above the deploy row is expected to have git, node, curl and jq at minimum — build.sh (the only thing ci.yml calls) is the single source of truth for that baseline, and fails the build if a variant is missing one of them rather than letting a consumer repo find out at the point of use. curl/jq were missing from node/golang until 2026-10-10 (nothing checked for their presence until this script existed) — bigb-vue's CI hit curl: not found on :node first. deploy is the deliberate exception: it never runs actions/checkout at all (no repo needed, just SSH to the target server), so its own baseline is just ssh.

Base image versions are kept current by Renovate (see guardians_of_the_galaxy/renovate-config), which proposes PRs bumping the FROM lines in each *.Dockerfile as new upstream versions ship. node.Dockerfile and python.Dockerfile are the exception: their Python/Node versions are build args from build.sh's per-version loops, so adding or dropping one (a new Python, a new Node LTS) is a manual edit there, and Renovate's Dockerfile manager is turned off for those two files (renovate.json). The one real pin they have, the Node major copied into every python-* image (ARG NODE_MAJOR), carries a # renovate: comment and is bumped by a custom manager, to even (LTS) majors only. pnpm's own pin (npm install -g pnpm@x.y.z in node.Dockerfile/ python.Dockerfile) is not a FROM line, so Renovate won't touch it — bump it by hand when needed. Node 22 is required everywhere now (pnpm 11+ needs it), which is also why python.Dockerfile installs Node from NodeSource instead of Debian's apt package — see that file's comments.

Not covered: ws-backend's e2e job (mcr.microsoft.com/playwright:*, tightly version-pinned to match the installed browsers — folding it into this repo would couple a Playwright bump to a ci-base rebuild).

How this builds itself

No snapshot/release split like the application repos — these are pure infra images, versioned by content only. Every push to master rebuilds and overwrites every tag (the docker/golang/deploy variants, each :python-3.1x/:python, and each :node-2x/:node). Nothing downstream should pin to a specific build; if a breaking change is needed, bump the variant's own tag name instead (e.g. introduce :python-2) rather than assuming callers will notice a silent behavior change.

Each build also uploads an image-sizes report to Argus (docker image inspect's real byte size per variant, not docker images' own human-rounded column) — purely informational, for watching the trend over time; nothing here gates on it.

Release notes

Every variant except deploy ships release-notes (release-notes/release-notes.mjs, zero-dependency Node — node is the one interpreter every variant already has). It prints Markdown release notes for everything since the previous v* tag: merged PRs (title, author, labels), the issues they close (Fixes #N, Closes #N, …), closed issues in a milestone named X.Y.Z or vX.Y.Z, and commits pushed straight to master without a PR. Entries are grouped by label first, then by conventional-commit type/scope (feat:, fix(api):, chore(deps):, a ! or BREAKING CHANGE: footer), then by the first word of an untyped title (Add …, Fix …). Dependency updates are folded into a <details> block.

release-notes --version 1.2.0   # notes for v1.2.0 (previous v* tag → HEAD)
release-notes                   # unreleased changes since the last tag

--repo/--server default to Forgejo Actions' GITHUB_REPOSITORY/ GITHUB_SERVER_URL; --from/--to override the range. Private repos need RELEASE_NOTES_TOKEN (read access). If the API is unreachable it warns once and falls back to git history alone — a release never fails over its notes. A shallow clone is a hard error: the checkout needs fetch-depth: 0.

In a consumer workflow's numbered-release step (jq --rawfile builds the JSON, so Markdown needs no escaping):

      - uses: actions/checkout@v7
        with:
          fetch-depth: 0
      # ...
      - name: Tag and publish the release
        if: ${{ inputs.version != '' }}
        env:
          PACKAGES_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
          RELEASE_NOTES_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
        run: |
          release-notes --version "${{ inputs.version }}" > /tmp/notes.md
          # ... git tag + push as before ...
          jq -n --arg tag "v${{ inputs.version }}" --rawfile body /tmp/notes.md \
              '{tag_name: $tag, name: $tag, body: $body, draft: false, prerelease: false}' \
            | curl -fsS -X POST \
                -H "Authorization: token $PACKAGES_TOKEN" \
                -H "Content-Type: application/json" \
                -d @- "https://git.stationmyr.net/api/v1/repos/${{ github.repository }}/releases"

The rolling devel draft can carry release-notes without --version the same way — a running preview of the next release's notes.

Publishing: publish-release

Also in every variant but deploy: tags HEAD and creates or updates its release, in place of hand-rolled git+curl+jq:

publish-release --version 1.2.0 --body-file /tmp/notes.md [--asset dist/x.tar.gz]...
publish-release --devel --body-file /tmp/notes.md [--name "devel (latest snapshot)"]

Env: GITHUB_REPOSITORY/GITHUB_SERVER_URL/GITHUB_ACTOR (Forgejo Actions sets them) and RELEASE_TOKEN or PACKAGES_TOKEN (write:repository).

  • --version is safe to re-run after a failure halfway: an existing vX.Y.Z tag on HEAD is kept (on another commit: hard error, pick another version), an existing release gets its body updated, an already-attached asset is skipped.

  • --devel fixes the rolling draft coming out published about half the time: pushing a tag makes Forgejo sync that tag's release from a queue worker, asynchronously, and the sync clears its draft flag (services/repository/push.go, pushUpdateAddTags). It makes sure the release exists before moving the tag, waits for the sync (which stamps the release with the commit's time), then marks it a draft.

  • Publish report to Argus (argus/backend#22): when ARGUS_UPLOAD_TOKEN is set (the repo's Argus upload token), a publish report is sent afterwards -- version (devel-<sha> for the snapshot), date, release page, every --asset with its size and download URL, plus:

    --artifact "git.stationmyr.net/o/r:1.2.0|52428800|https://..."  # not a release asset
    --link "Package|https://git.stationmyr.net/o/-/packages/..."
    --dependencies deps.txt   # "name==1.0", "name@1.0" or "name 1.0" per line
    

    ARGUS_URL defaults to https://reports.stationmyr.net. A failed upload only warns: the release itself is already done. Without the token, nothing is sent.

Per-repo configuration

Optional .forgejo/release-notes.json (--config for another path). Each top-level key replaces the default; unknown keys are an error.

Key Default
include_direct_commits true commits that aren't any PR's merge/squash commit
include_milestone_issues true closed issues in milestone X.Y.Z/vX.Y.Z not already linked
exclude_patterns ^Merge branch , ^Merge remote-tracking branch regexes on commit subject / PR or issue title
exclude_labels skip-changelog, no-changelog PR/issue labels that drop the entry
exclude_authors [] Forgejo logins or git author names whose changes are dropped
bot_authors renovate-bot, renovate[bot] changes listed, but not credited under contributors
skip_markers [skip changelog], [no changelog] anywhere in a direct commit's message
show_authors / contributors true / true "by @x" per line / contributors footer
hide_scopes deps, security scopes not printed as a **scope:** prefix
tag_prefix v
sections breaking, security, features, fixes, docs, dependencies, CI & maintenance ordered, first match wins — see below
other_title 📦 Other changes everything no section matched

A section is {"title", "labels", "types", "scopes", "prefixes", "breaking", "collapsed"} (all but title optional), e.g. a repo that only wants PRs and a single feature/fix split:

{
  "include_direct_commits": false,
  "sections": [
    { "title": "Features", "labels": ["feature"], "types": ["feat"] },
    { "title": "Fixes", "labels": ["bug"], "types": ["fix"] }
  ]
}

Labels

The default sections key on eight labels: breaking, security, feature, bug, documentation, dependencies (Renovate puts it on its PRs, see renovate-config — it only assigns labels that already exist), ci and skip-changelog. release-notes/sync-labels.mjs creates them, or fixes their color/description, in the repos it's given — by hand, not from CI; safe to rerun:

RELEASE_NOTES_TOKEN=... node release-notes/sync-labels.mjs [--dry-run] Gilles/new-repo

Tests: node --test release-notes/*.test.mjs (run by ci.yml before any image is built); build.sh also runs release-notes once in every variant against a scratch repo.

Requirements

PACKAGES_TOKEN secret on this repo (or the guardians_of_the_galaxy org), scoped at least write:package — same secret used across the other repos for pushing to the container/PyPI registries.

Shared pnpm store

Consumer workflows can mount a shared named volume to reuse pnpm's content-addressable store across CI runs instead of re-downloading every package on every job:

container:
  image: git.stationmyr.net/guardians_of_the_galaxy/ci-base:node
  volumes:
    - pnpm-store:/pnpm-store
steps:
  - run: pnpm config set store-dir /pnpm-store
  - run: pnpm install --frozen-lockfile

Requires pnpm-store to be in the runner's valid_volumes allowlist (runner-config.yml, tracked in vps-garion — see that repo for the server-side setup). Docker creates the named volume automatically on first use; nothing to provision by hand, and losing it (fresh volume, server reinstall) is a non-event — it's a performance cache only, never a correctness dependency.

Size is capped by .forgejo/workflows/prune-pnpm-store.yml, a weekly scheduled job that measures the volume and wipes it entirely if it's grown past the threshold defined in that file. It wipes rather than selectively prunes: pnpm store prune's "unreferenced package" check only knows about projects actually checked out in the same job, which a dedicated maintenance job never has — running it here would be equivalent to always wiping anyway, so the explicit size-based wipe is the real mechanism, not a fallback.

Each run uploads a report to Argus (size before purge, threshold, whether it wiped) instead of a standalone Forgejo Actions artifact — Argus's own Project.history_retention_count keeps a bounded history and the report is a page, not a downloadable HTML file.

Docker host cleanup

.forgejo/workflows/prune-docker-host.yml — a separate weekly job that prunes the runner host's own Docker disk usage: dangling images, unused volumes (excluding pnpm-store, which looks "unused" between runs by design — see above), and build cache older than 7 days. Added after dangling images alone reached 67GB, mostly from :devel-style tags (no snapshot/release split, see e.g. ws-backend) being rebuilt and overwritten on every push without the old layers ever being cleaned up.

Same reporting as prune-pnpm-store: an Argus report with the before/after size per docker system df category and the total space reclaimed.

Orphaned buildx builders

.forgejo/workflows/prune-buildx.yml — a third weekly job, staggered an hour after the two above, that removes any leftover buildx_buildkit_* containers: docker buildx build (used by several consumer repos' own image-build workflows, not this repo's own build.sh) creates a fresh ephemeral builder whenever none is passed explicitly, and nothing removes it afterwards. Added after 8 had piled up over about two weeks — each one its own idle buildkitd daemon, contributing to a real memory exhaustion incident on garion. Considered switching to one shared persistent builder instead; rejected, since a corrupted shared builder would then be a single point of failure for every repo's builds — this job is the cleanup side of staying with ephemeral ones, not a workaround for avoiding the choice.

Same reporting as the other two: an Argus report with how many orphaned builders were found and removed — status: warn instead of ok when that count is non-zero, since it means some other workflow is leaking builders again.