12 Commits
Author SHA1 Message Date
jcoffey-dev 0ea86b6be0 Merge pull request 'Point links at git.coffeylabs.org after the move from GitHub' (#10) from fix/links-after-move into main
ci / build (push) Successful in 1m4s
ci / publish (push) Skipped
2026-09-22 16:11:23 +00:00
jcoffey-dev 3e7bb80f73 Point links at git.coffeylabs.org after the move from GitHub
ci / build (pull_request) Successful in 1m6s
ci / publish (pull_request) Skipped
GitHub took the organization's repos and GHCR offline on 2026-09-20. Repo,
release, raw-file and clone links now go to Gitea at git.coffeylabs.org,
container images to registry.coffeylabs.org, and GitLab-style /-/blob paths
to Gitea's /src/branch form. Go module paths are identifiers and stay as
they are; links to GitHub issues and pull requests are left as history.
2026-09-22 09:08:22 -07:00
jcoffey-dev 4eb838042c Merge pull request 'ci: run socket-free jobs on the light label' (#9) from ci/light-runner into main
ci / build (push) Successful in 1m6s
ci / publish (push) Skipped
2026-09-22 13:54:26 +00:00
jcoffey-dev 14ccb05e52 ci: run socket-free jobs on the light label
ci / build (pull_request) Successful in 1m55s
ci / publish (pull_request) Skipped
Both runners carry `light` (host1, and host2 over the wg-hosts link), so
these jobs run on whichever host is free. Jobs that mount the docker socket
keep `runs-on: docker`, which only host1 has.
2026-09-22 06:45:44 -07:00
jcoffey-dev 7d5f2273ae Merge branch 'ci/gitea-actions' into 'main'
ci / build (push) Successful in 4m23s
ci / publish (push) Skipped
ci: add Gitea Actions workflow

See merge request inbuxa/inbuxa-admin!8
2026-09-22 00:00:36 -07:00
jcoffey-dev de58de765a ci: fail clearly when PACKAGE_TOKEN is missing
ci / build (pull_request) Successful in 6m2s
ci / publish (pull_request) Skipped
ci / build (push) Successful in 3m14s
ci / publish (push) Failing after 46s
2026-09-21 23:20:50 -07:00
jcoffey-dev 1be7b5c60b ci: add Gitea Actions workflows ported from .gitlab-ci.yml 2026-09-21 22:52:11 -07:00
jcoffey-dev 2cd73c6705 Merge branch 'ci/registry-token-host' into 'main'
Fetch the registry token from the public address, not the runner's

See merge request inbuxa/inbuxa-admin!7
2026-09-21 18:20:30 -07:00
jcoffey-dev 7a455e7b6a Fetch the registry token from the public address, not the runner's
The builder on the host's network (the last change here) didn't help: the
next publish failed exactly as before. Looking on the host showed why.
Both builders resolve git.coffeylabs.org publicly; the token isn't fetched
by the builder at all. buildx fetches registry tokens on the client side,
in the job container, and on ci-net the name git.coffeylabs.org belongs to
the gitlab container itself (172.30.0.2) -- which is how the runner clones
over plain HTTP, and which has nothing on 443. So every push asked
https://git.coffeylabs.org/jwt/auth for a token and was refused. The login
before it worked because the host's daemon does the login, and the host
resolves the name publicly.

For the publish job only, the name now points at its public address in the
job's /etc/hosts, looked up from a public resolver, as the host sees it.
/etc/hosts wins over Docker's DNS, and nothing else in the job is affected:
the checkout is done, and image layers go to the registry's own DNS-only
name, not this one. The builder goes back to the shared ci-builder; its
network was never the problem.

The lookup and the /etc/hosts write were tried in the job's own image
(docker:28-cli, same digest): it picks the first public IPv4 address and
getent then returns it.
2026-09-21 16:48:43 -07:00
weekly-release ff38c7a5e8 Version 2026.9.21.2 2026-09-21 23:35:57 +00:00
jcoffey-dev 0454528e8a Merge branch 'ci/buildx-host-network' into 'main'
Publish with a builder on the host's network

See merge request inbuxa/inbuxa-admin!6
2026-09-21 16:33:30 -07:00
jcoffey-dev 9967f9cd7f Publish with a builder on the host's network
The first release's image never reached the registry. Both platforms
built, then the push failed:

  failed to fetch oauth token: Post "https://git.coffeylabs.org/jwt/auth":
  dial tcp 172.30.0.2:443: connect: connection refused

buildx's docker-container builder is a container of its own on the host's
daemon, and it does the push, token and all. On the network it was
created on, git.coffeylabs.org resolves to an internal address with
nothing listening on 443. The job's own `docker login` worked because it
goes through the host daemon, which resolves the name publicly.
ihasmail's publish failed the same way this morning (job 513), so this is
the runners, not this pipeline.

The builder now runs on the host's network, so it resolves the name as
the login does. Only the token request goes to git.coffeylabs.org; image
layers still go to registry.coffeylabs.org, the registry's DNS-only name.
It gets a new name, ci-builder-host: `ci-builder` is a long-lived
container shared between jobs, and `create || use` would keep reusing it
on its old network.
2026-09-21 16:31:01 -07:00
6 changed files with 278 additions and 3 deletions
+127
View File
@@ -0,0 +1,127 @@
# CI on the self-hosted Gitea, ported from .gitlab-ci.yml during the move off
# GitLab (2026-09-22). Gitea reads .gitea/workflows and ignores .github/ once
# this directory exists; .github/workflows stays as it was for GitHub.
#
# Every job runs in an image pinned by digest (tag in the trailing comment),
# and the only action used is coffey-labs/actions/checkout pinned by SHA. The
# instance resolves short `uses:` against itself, never GitHub, so nothing
# unreviewed can be pulled in. Read the comment for the version; the digest is
# what runs.
#
# Jobs run on the runner's `ci-net` network and clone from Gitea's internal
# address, never through the Cloudflare-proxied public name, which caps
# request bodies at 100 MB. Images go to the registry's own DNS-only name
# (vars.REGISTRY, an org variable).
#
# The weekly release is its own workflow, weekly-release.yml.
#
# Not ported:
# * cleanup.yml pruned GHCR with dataaxiom/ghcr-cleanup-action; on Gitea
# that belongs in the package cleanup rules (owner settings -> Packages),
# not in a workflow.
name: ci
on:
push:
branches: [main]
# 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.
tags:
- 'v[0-9][0-9][0-9][0-9].[0-9]+.[0-9]+'
- 'v[0-9][0-9][0-9][0-9].[0-9]+.[0-9]+.[0-9]+'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# A release tag is built and tested again before its image is published.
build:
runs-on: light
container:
image: node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9 # 22-bookworm-slim
env:
NPM_CONFIG_CACHE: ${{ github.workspace }}/.npm
steps:
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
- run: npm ci --ignore-scripts
- run: npm run typecheck
- run: npm run lint
- run: npm test
- run: npm run build
# ----------------------------------------------------------- publish ------
# Port of publish.yml, to the owner's own registry now that GHCR went with
# the GitHub account: <REGISTRY>/inbuxa/inbuxa-admin, the same path the
# GitLab registry used.
#
# Tag-driven. A release cut with the job's own token raises no event on
# Gitea (as on GitHub), so weekly-release.yml creates its release with
# RELEASE_TOKEN; the tag that makes is an ordinary push, and starts this.
#
# 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.
#
# The push logs in with PACKAGE_TOKEN (jcoffey-dev, write:package): Gitea's
# per-job token is refused by the container registry. The registry hands out
# its push tokens from its own name, so unlike on GitLab nothing here has to
# be pointed at a public address.
publish:
if: ${{ startsWith(github.ref, 'refs/tags/') }}
needs: [build]
runs-on: docker
container:
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
env:
DOCKER_BUILDKIT: "1"
REGISTRY: ${{ vars.REGISTRY }}
IMAGE: ${{ vars.REGISTRY }}/${{ github.repository }}
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
steps:
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
- run: |
set -eu
apk add --no-cache -q jq curl
VERSION="$(jq -er .version inbuxa-version.json)"
if [ "$GITHUB_REF_NAME" != "v$VERSION" ]; then
echo "Tag $GITHUB_REF_NAME 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" >> "$GITHUB_ENV"
- run: |
test -n "$REGISTRY"
test -n "$PACKAGE_TOKEN" || { echo "PACKAGE_TOKEN secret is not set on this repository" >&2; exit 1; }
echo "$PACKAGE_TOKEN" | docker login -u jcoffey-dev --password-stdin "$REGISTRY"
docker run --privileged --rm tonistiigi/binfmt --install arm64
docker buildx create --use --name gitea-builder --driver docker-container || docker buildx use gitea-builder
# Attestations are off, as they were in publish.yml: they add manifests
# of their own to the index.
- run: |
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"
# Gitea keeps a container package on its owner; linking it shows it on
# the repository's Packages tab. Idempotent.
- run: |
curl -fsS -o /dev/null -X POST -H "Authorization: token $PACKAGE_TOKEN" \
"$CI_SERVER_INTERNAL/api/v1/packages/${GITHUB_REPOSITORY%%/*}/container/${GITHUB_REPOSITORY#*/}/-/link/${GITHUB_REPOSITORY#*/}" \
|| echo "package already linked (or link refused); not fatal"
- if: always()
run: docker logout "$REGISTRY" || true
+135
View File
@@ -0,0 +1,135 @@
# Weekly release, ported from the weekly-release job in .gitlab-ci.yml (itself
# a port of release.yml): cut a release once a week, but only if there is
# something in it. The decision is 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.
#
# Mondays 09:37 UTC, as release.yml did. Run it by hand from the Actions tab
# (workflow_dispatch); dry_run defaults to true, so a manual run shows the
# decision and stops unless you untick it.
#
# SIDE-BY-SIDE PERIOD: until the GitLab cutover, GitLab's own schedule is
# still live and still cuts the real release, and its bump commit and tag
# reach this copy through the sync. Two releasers would race to write the same
# version, so this workflow only ever dry-runs unless the variable
# RELEASE_LIVE is '1'. Set RELEASE_LIVE=1 (repo or org Actions variable) at
# cutover, when GitLab's schedule is switched off -- not before.
#
# Reads use the job's own token. Everything that writes uses RELEASE_TOKEN
# (jcoffey-dev, write:repository), because a tag Gitea creates for the job
# token raises no event (checked 2026-09-22) and the tag must start ci.yml's
# publish job:
# * the bump is committed through the contents API. Gitea's API has no
# "only if the branch is still at X" guard like GitLab's last_commit_id,
# so the job checks main's head immediately before writing and refuses if
# it moved since the commit it counted from; run it again. Otherwise the
# notes and the count would describe a different commit from the one
# released. (The API does refuse if the file itself changed, via its blob
# sha.)
# * 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 ci.yml and
# `publish` builds the image.
# The token's owner must be allowed to push to main.
name: weekly-release
on:
schedule:
- cron: '37 9 * * 1'
workflow_dispatch:
inputs:
dry_run:
description: Show the decision and stop
type: boolean
default: true
# One at a time: two overlapping runs would race to write the same version and
# create the same tag.
concurrency:
group: weekly-release
cancel-in-progress: false
jobs:
weekly-release:
runs-on: light
container:
image: node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9 # 22-bookworm-slim
env:
READ_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
# Live only with RELEASE_LIVE=1 AND either the schedule or a manual run
# with dry_run unticked.
DRY_RUN: ${{ (vars.RELEASE_LIVE == '1' && (github.event_name == 'schedule' || inputs.dry_run == false || inputs.dry_run == 'false')) && '0' || '1' }}
steps:
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
with:
fetch-depth: 0
- run: apt-get update -qq && apt-get install -y -qq --no-install-recommends curl jq ca-certificates >/dev/null
- shell: bash
run: |
set -euo pipefail
API="${CI_SERVER_INTERNAL}/api/v1/repos/${GITHUB_REPOSITORY}"
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 -H "Authorization: token ${READ_TOKEN}" "${API}/releases?draft=false&pre-release=false&limit=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" = "1" ]; then echo "Dry run (RELEASE_LIVE='${{ vars.RELEASE_LIVE }}'): stopping here."; exit 0; fi
auth=(-H "Authorization: token ${RELEASE_TOKEN}")
# 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");
'
head="$(curl -fsS "${auth[@]}" "${API}/branches/main" | jq -er .commit.id)"
if [ "$head" != "$sha" ]; then
echo "main moved from ${sha} to ${head} since this run counted; run it again." >&2
exit 1
fi
blob="$(curl -fsS "${auth[@]}" "${API}/contents/inbuxa-version.json?ref=${sha}" | jq -er .sha)"
jq -n --arg msg "Version ${version}" --arg blob "$blob" \
--arg content "$(base64 -w0 inbuxa-version.json)" \
'{branch:"main", message:$msg, sha:$blob, content:$content}' > commit.json
bump="$(curl -fsS "${auth[@]}" -X PUT -H "Content-Type: application/json" \
--data @commit.json "${API}/contents/inbuxa-version.json" | jq -er .commit.sha)"
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 body "$(printf '%s commit(s) since %s.\n\n%s' "$count" "${previous:-the beginning}" "$notes")" \
'{tag_name:$tag, target_commitish:$ref, name:$name, body:$body}' > release.json
curl -fsS "${auth[@]}" -H "Content-Type: application/json" \
--data @release.json "${API}/releases" | jq -r '"created release " + .tag_name'
+13
View File
@@ -87,6 +87,19 @@ publish:
echo "VERSION=$VERSION" > version.env echo "VERSION=$VERSION" > version.env
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- docker run --privileged --rm tonistiigi/binfmt --install arm64 - docker run --privileged --rm tonistiigi/binfmt --install arm64
# The registry hands out push tokens from https://git.coffeylabs.org/jwt/auth,
# and buildx fetches them here, in the job, not in its builder. On ci-net
# that name is the gitlab container itself (172.30.0.2), which serves
# plain HTTP to the runner and nothing on 443, so every push failed at the
# last step with "connection refused". The login above works because the
# host's daemon does it, and the host resolves the name publicly. So, for
# this job only, point the name at its public address the same way. Only
# the token request uses it; layers go to the registry's own DNS-only name.
- |
public="$(nslookup "$CI_SERVER_HOST" 1.1.1.1 2>/dev/null | awk '/^Address: / && $2 !~ /:/ { print $2; exit }')"
if [ -z "$public" ]; then echo "Could not resolve $CI_SERVER_HOST publicly" >&2; exit 1; fi
echo "$public $CI_SERVER_HOST" >> /etc/hosts
echo "$CI_SERVER_HOST -> $public for the registry token"
- docker buildx create --use --name ci-builder --driver docker-container || docker buildx use ci-builder - docker buildx create --use --name ci-builder --driver docker-container || docker buildx use ci-builder
script: script:
- . ./version.env - . ./version.env
+1 -1
View File
@@ -32,6 +32,6 @@ session.
You'll get an acknowledgement within a few days. A report that turns out to You'll get an acknowledgement within a few days. A report that turns out to
affect the mail server rather than this interface will be moved to affect the mail server rather than this interface will be moved to
[inbuxa-server](https://github.com/inbuxa/inbuxa-server), and one that affects [inbuxa-server](https://git.coffeylabs.org/inbuxa/inbuxa-server), and one that affects
upstream Stalwart's web interface will be passed to Stalwart Labs with credit upstream Stalwart's web interface will be passed to Stalwart Labs with credit
to you. to you.
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"version": "2026.9.21" "version": "2026.9.21.2"
} }
+1 -1
View File
@@ -11,4 +11,4 @@
* this fork. The version shown beside the link names the build, which is what * this fork. The version shown beside the link names the build, which is what
* makes the offer something a person can act on. * makes the offer something a person can act on.
*/ */
export const SOURCE_URL = 'https://github.com/inbuxa/inbuxa-admin'; export const SOURCE_URL = 'https://git.coffeylabs.org/inbuxa/inbuxa-admin';