- JavaScript 81.4%
- Dockerfile 10.3%
- Shell 8.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci / build (push) Successful in 5m0s
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 |
||
| .forgejo/workflows | ||
| release-notes | ||
| build.sh | ||
| deploy.Dockerfile | ||
| docker.Dockerfile | ||
| golang.Dockerfile | ||
| node.Dockerfile | ||
| python.Dockerfile | ||
| README.md | ||
| renovate.json | ||
ci-base
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).
-
--versionis safe to re-run after a failure halfway: an existingvX.Y.Ztag 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. -
--develfixes 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_TOKENis set (the repo's Argus upload token), apublishreport is sent afterwards -- version (devel-<sha>for the snapshot), date, release page, every--assetwith 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 lineARGUS_URLdefaults tohttps://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.