ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 1m30s
ci / announce (pull_request) Skipped
The repository moved from inbuxa/ihasmail-inbuxa to inbuxa/inbuxa-webmail, matching inbuxa-server and inbuxa-admin. Point the source links, the image name and the package link at the new name. The OAuth client id stays ihasmail-inbuxa, since that is what the server registers.
248 lines
13 KiB
YAML
248 lines
13 KiB
YAML
# 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 is the GitHub side, below.
|
|
#
|
|
# WHERE THE BUILD RUNS. Gitea push-mirrors this repository to GitHub, and the
|
|
# org variable BUILD_ON picks which forge does the heavy work:
|
|
# * unset (or anything but `github`): every job here runs, as it always did,
|
|
# and GitHub's workflow skips all of its jobs.
|
|
# * `github`: the test, build and publish jobs here are skipped, GitHub
|
|
# Actions runs .github/workflows/ci.yml on its hosted runners (native
|
|
# arm64, no QEMU), and the `github` job below waits for the commit status
|
|
# that run posts back, passing or failing with it. So this run's result is
|
|
# still the one that counts, for a PR's checks as for anything that merges
|
|
# on green CI. The variable is set on both forges, and must agree.
|
|
# If GitHub is ever unavailable, unsetting BUILD_ON here is the whole
|
|
# fallback: the jobs below take over again unchanged.
|
|
#
|
|
# Releases are cut by pushing a tag named `inbuxa-v<version>`, where
|
|
# <version> is what scripts/version.mjs says for the tagged commit with the
|
|
# `+` turned into `-` (e.g. inbuxa-v2026.9.22-g1a2b3c4). The prefix matters:
|
|
# this repository carries upstream ihasmail's own `v...` tags, on commits it
|
|
# shares with upstream, and a publish keyed on `v*` would ship plain ihasmail
|
|
# under the INBUXA name the moment one arrived. Only `inbuxa-v` tags publish.
|
|
# A tag publishes only if it names its own commit's version and that commit is
|
|
# on main. There is no release schedule yet; tags are cut by hand.
|
|
|
|
# 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. Do not "simplify" one back to a bare tag.
|
|
#
|
|
# 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.
|
|
name: ci
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
tags: ['**']
|
|
pull_request:
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
# -------------------------------------------------------------- test ------
|
|
node:
|
|
if: ${{ vars.BUILD_ON != 'github' }}
|
|
runs-on: light
|
|
container:
|
|
image: node:26-bookworm-slim@sha256:582460f614631b59b824ac6020533b9bf339c7fdf3a6d7db31abb6b4065f0212 # 26-bookworm-slim
|
|
env:
|
|
NPM_CONFIG_CACHE: ${{ github.workspace }}/.npm
|
|
steps:
|
|
# version.test.ts shells out to git to resolve a build version, and the
|
|
# slim image ships without it; the checkout action installs it when it
|
|
# is missing, so it is there for the tests too. Full history, because
|
|
# the version is computed from it.
|
|
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
|
|
with:
|
|
fetch-depth: 0
|
|
# config.test.ts chmods a directory to 0555 and expects the write to be
|
|
# refused. Root ignores the permission bits, so as root that assertion
|
|
# can never hold. The tests run as the image's unprivileged `node` user
|
|
# for that reason; -p keeps the environment.
|
|
#
|
|
# imageproxy.test.ts needs IPv6 as well, which is not set here but on the
|
|
# runner: jobs run on the `ci-net` docker network, created with --ipv6.
|
|
# Without a non-loopback IPv6 address on the container, getaddrinfo's
|
|
# AI_ADDRCONFIG drops ::1 from the results entirely, localhost resolves
|
|
# to IPv4 only, and the test's control case connects to a port nothing
|
|
# is listening on. That is a runner property, so it cannot be fixed from
|
|
# this file -- if these tests ever fail again with ECONNREFUSED on
|
|
# 127.0.0.1, check that the runner still puts jobs on an IPv6-enabled
|
|
# network.
|
|
- run: chown -R node:node "$GITHUB_WORKSPACE"
|
|
- run: su node -p -c "npm ci --ignore-scripts"
|
|
- run: su node -p -c "npm run typecheck"
|
|
- run: su node -p -c "npm test"
|
|
- run: su node -p -c "npm run build"
|
|
|
|
# ------------------------------------------------------------- build ------
|
|
# Proves the Dockerfile still builds on every change, without pushing. The
|
|
# equivalent of ci.yml's final `docker build -t ihasmail:ci .` step. The
|
|
# Dockerfile builds everything itself; `needs` only keeps the order.
|
|
docker-build:
|
|
if: ${{ vars.BUILD_ON != 'github' && !startsWith(github.ref, 'refs/tags/') }}
|
|
needs: [node]
|
|
runs-on: docker
|
|
container:
|
|
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
steps:
|
|
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
|
|
- run: |
|
|
tag="ihasmail:ci-$(echo "$GITHUB_SHA" | cut -c1-8)"
|
|
docker build -t "$tag" .
|
|
docker image rm "$tag"
|
|
|
|
# ----------------------------------------------------------- release ------
|
|
# Only for `inbuxa-v` tags (see the top of this file). The tag has to name
|
|
# its own commit's version, so the image, the release and the About screen
|
|
# all agree, and the commit has to be on main, so a release never describes
|
|
# code that was not reviewed onto the default branch.
|
|
version:
|
|
if: ${{ vars.BUILD_ON != 'github' && startsWith(github.ref, 'refs/tags/inbuxa-v') }}
|
|
runs-on: light
|
|
container:
|
|
image: node:26-bookworm-slim@sha256:582460f614631b59b824ac6020533b9bf339c7fdf3a6d7db31abb6b4065f0212 # 26-bookworm-slim
|
|
outputs:
|
|
version: ${{ steps.v.outputs.VERSION }}
|
|
docker_tag: ${{ steps.v.outputs.DOCKER_TAG }}
|
|
steps:
|
|
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
|
|
with:
|
|
fetch-depth: 0
|
|
- id: v
|
|
shell: bash
|
|
env:
|
|
TAG: ${{ github.ref_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
V="$(node scripts/version.mjs)"
|
|
want="inbuxa-v${V/+/-}"
|
|
[ "$TAG" = "$want" ] || { echo "!! $TAG does not name this commit's version; expected $want"; exit 1; }
|
|
git merge-base --is-ancestor "$(git rev-parse "${TAG}^{commit}")" origin/main \
|
|
|| { echo "!! $TAG is not on main"; exit 1; }
|
|
echo "VERSION=$V" >> "$GITHUB_OUTPUT"
|
|
echo "DOCKER_TAG=${V/+/-}" >> "$GITHUB_OUTPUT"
|
|
echo "VERSION=$V DOCKER_TAG=${V/+/-}"
|
|
|
|
# Multi-arch image at <REGISTRY>/inbuxa/inbuxa-webmail, then the release.
|
|
# arm64 is built under QEMU on this amd64 host, which is slow but fine for
|
|
# a hand-cut release. PACKAGE_TOKEN (jcoffey-dev, write:package) logs in:
|
|
# the job's own token is refused by the container registry. The release is
|
|
# created last, so a release on the page always has its image behind it.
|
|
publish:
|
|
if: ${{ vars.BUILD_ON != 'github' && startsWith(github.ref, 'refs/tags/inbuxa-v') }}
|
|
needs: [node, version]
|
|
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 }}
|
|
VERSION: ${{ needs.version.outputs.version }}
|
|
DOCKER_TAG: ${{ needs.version.outputs.docker_tag }}
|
|
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
|
steps:
|
|
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
|
|
- run: |
|
|
test -n "$REGISTRY" && test -n "$VERSION" && test -n "$DOCKER_TAG"
|
|
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
|
|
- run: |
|
|
docker buildx build \
|
|
--platform linux/amd64,linux/arm64 \
|
|
--build-arg IHASMAIL_VERSION="$VERSION" \
|
|
--provenance=false --sbom=false \
|
|
--tag "$IMAGE:$DOCKER_TAG" \
|
|
--tag "$IMAGE:latest" \
|
|
--push .
|
|
docker buildx imagetools inspect "$IMAGE:$DOCKER_TAG"
|
|
# Show the package on the repository's Packages tab. Idempotent.
|
|
- run: |
|
|
apk add --no-cache -q curl
|
|
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"
|
|
# The release, on the internal address. The job's own token may create
|
|
# releases; a tag it creates would not start a workflow, but this one
|
|
# already exists.
|
|
- env:
|
|
TAG: ${{ github.ref_name }}
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
body="INBUXA webmail $VERSION.\n\nImage: \`$IMAGE:$DOCKER_TAG\` (linux/amd64, linux/arm64), also tagged \`latest\`."
|
|
curl -fsS -o /dev/null -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
--data "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"$body\"}" \
|
|
"$CI_SERVER_INTERNAL/api/v1/repos/$GITHUB_REPOSITORY/releases"
|
|
echo "release $TAG created"
|
|
- if: always()
|
|
run: docker logout "$REGISTRY" || true
|
|
|
|
# The release above is made with the job's own token, and Gitea starts no
|
|
# workflow for events the Actions bot causes -- announce.yml's
|
|
# 'on: release' never fires for it -- so announce it from here. With
|
|
# BUILD_ON=github the release is created by GitHub's run instead, and this
|
|
# follows the `github` job. Announcing stays on Gitea either way; the
|
|
# action posts once per tag, so a second attempt is a no-op.
|
|
announce:
|
|
needs: [publish, github]
|
|
if: ${{ always() && startsWith(github.ref, 'refs/tags/inbuxa-v') && (needs.publish.result == 'success' || needs.github.result == 'success') }}
|
|
runs-on: light
|
|
steps:
|
|
- uses: coffey-labs/actions/discourse-release@e9293996e2efa770839121fa8f8da93083f216be
|
|
with:
|
|
api-key: ${{ secrets.DISCOURSE_RELEASE_KEY }}
|
|
discord-webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
|
tag: ${{ github.ref_name }}
|
|
|
|
# ------------------------------------------------------------ github ------
|
|
# With BUILD_ON=github, the work above happens in GitHub Actions, which
|
|
# posts one commit status back here when it finishes: "github/ci (branch)"
|
|
# for a branch push, "github/ci (tag)" for a tag. This job waits for that
|
|
# status on the commit under test -- the PR's head for a pull request -- and
|
|
# passes or fails with it. Nothing arriving within the timeout means GitHub
|
|
# never built the commit (a mirror that failed to sync, or GitHub being
|
|
# down): check the mirror, or unset BUILD_ON to build here.
|
|
github:
|
|
if: ${{ vars.BUILD_ON == 'github' }}
|
|
# Its own runner label with plenty of slots: this job only polls, but holds a slot
|
|
# for as long as the GitHub build takes, and must not starve the build runners.
|
|
runs-on: wait
|
|
timeout-minutes: 150
|
|
container:
|
|
image: node:26-bookworm-slim@sha256:582460f614631b59b824ac6020533b9bf339c7fdf3a6d7db31abb6b4065f0212 # 26-bookworm-slim
|
|
env:
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
CONTEXT: github/ci (${{ github.ref_type == 'tag' && format('tag {0}', github.ref_name) || 'branch' }})
|
|
steps:
|
|
- run: apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates curl jq >/dev/null
|
|
- shell: bash
|
|
run: |
|
|
set -uo pipefail
|
|
url="$CI_SERVER_INTERNAL/api/v1/repos/$GITHUB_REPOSITORY/commits/$SHA/statuses?limit=50"
|
|
echo "waiting for '$CONTEXT' on $SHA"
|
|
while :; do
|
|
state="$(curl -fsS -H "Authorization: token $TOKEN" "$url" \
|
|
| jq -r --arg c "$CONTEXT" '[.[] | select(.context == $c)] | sort_by(.id) | last | .status // empty')"
|
|
case "$state" in
|
|
success) echo "GitHub reported success"; exit 0 ;;
|
|
failure|error) echo "GitHub reported $state -- see the status's link for the run" >&2; exit 1 ;;
|
|
esac
|
|
sleep 20
|
|
done
|