Compare commits
9
Commits
v2026.9.21.2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ea86b6be0 | ||
|
|
3e7bb80f73 | ||
|
|
4eb838042c | ||
|
|
14ccb05e52 | ||
|
|
7d5f2273ae | ||
|
|
de58de765a | ||
|
|
1be7b5c60b | ||
|
|
2cd73c6705 | ||
|
|
7a455e7b6a |
@@ -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
|
||||||
@@ -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'
|
||||||
+14
-10
@@ -87,16 +87,20 @@ 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 builder runs as a container on the host's daemon and does the push
|
# The registry hands out push tokens from https://git.coffeylabs.org/jwt/auth,
|
||||||
# itself, including fetching a registry token from git.coffeylabs.org.
|
# and buildx fetches them here, in the job, not in its builder. On ci-net
|
||||||
# On the runner's network that name resolves to an internal address
|
# that name is the gitlab container itself (172.30.0.2), which serves
|
||||||
# (172.30.0.2) with nothing on 443, so the token request was refused and
|
# plain HTTP to the runner and nothing on 443, so every push failed at the
|
||||||
# every push failed at the last step -- here and in ihasmail alike. On the
|
# last step with "connection refused". The login above works because the
|
||||||
# host's network the name resolves as it does for `docker login` above.
|
# host's daemon does it, and the host resolves the name publicly. So, for
|
||||||
# Only the token request uses it; layers still go to the registry's own
|
# this job only, point the name at its public address the same way. Only
|
||||||
# DNS-only name. A new name, because `ci-builder` is a long-lived container
|
# the token request uses it; layers go to the registry's own DNS-only name.
|
||||||
# shared between jobs and would keep whatever network it was created on.
|
- |
|
||||||
- docker buildx create --use --name ci-builder-host --driver docker-container --driver-opt network=host || docker buildx use ci-builder-host
|
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
|
||||||
script:
|
script:
|
||||||
- . ./version.env
|
- . ./version.env
|
||||||
# Attestations are off, as they were in publish.yml: they add manifests of
|
# Attestations are off, as they were in publish.yml: they add manifests of
|
||||||
|
|||||||
+1
-1
@@ -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.
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user