Files
inbuxa-webmail/.github/workflows/ci.yml
T
jcoffey-dev ddb6ce4611
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 2m30s
ci / announce (pull_request) Skipped
ci: a cancelled GitHub run no longer reports failure to Gitea
The mirror can push one commit twice in quick succession. GitHub then
starts two runs and cancels the older, and that run's report job posted
"failure" for the commit. Gitea's github job, seeing the newest status,
failed the check while the surviving run was still building and later
passed.

A cancelled run now posts nothing and leaves the result to the run that
superseded it. A real failure still reports failure.
2026-09-30 00:26:40 -07:00

262 lines
12 KiB
YAML

# CI and publishing on GitHub Actions, for a repository whose source of truth
# is the self-hosted Gitea. Gitea push-mirrors every commit and tag here, and
# this workflow does the heavy work on GitHub's hosted runners -- native arm64
# included -- then reports the result back to Gitea as a commit status.
#
# THE SWITCH. Every job here runs only when the org variable BUILD_ON is
# `github`. Gitea's .gitea/workflows/ci.yml reads the same variable (set on
# the Gitea org too): with it set, Gitea skips its own build jobs and waits for
# the status this workflow posts; without it, Gitea builds everything itself,
# exactly as before, and every job here is skipped. If GitHub is ever
# unavailable, unsetting BUILD_ON on Gitea is the whole fallback.
#
# There is no pull_request trigger: pull requests live on Gitea. A PR's branch
# arrives here as an ordinary push, and the status lands on its head commit,
# which is where Gitea's PR looks for it.
#
# Releases are cut by pushing a tag named `inbuxa-v<version>` (see
# .gitea/workflows/ci.yml for why the prefix matters: this repository carries
# upstream ihasmail's own `v...` tags, and only `inbuxa-v` tags publish).
#
# Org configuration, not in this file:
# vars.BUILD_ON `github` to build here
# vars.REGISTRY the Gitea container registry's DNS-only name
# vars.GITEA_URL Gitea's public URL, for statuses, releases and packages
# secrets.GITEA_TOKEN jcoffey-dev, write:repository + write:package
#
# Every `uses:` is pinned to a full commit SHA with the release in the
# trailing comment. A tag is a mutable pointer, so trusting `@v7` is trusting
# every future version of that action. Do not "simplify" a pin back to a tag.
name: ci
on:
push:
branches: ['**']
tags: ['**']
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GITEA_URL: ${{ vars.GITEA_URL }}
REGISTRY: ${{ vars.REGISTRY }}
# A registry path must be lowercase; the repository name already is.
IMAGE: ${{ vars.REGISTRY }}/inbuxa/ihasmail-inbuxa
STATUS_CONTEXT: github/ci (${{ github.ref_type }})
jobs:
# Tells Gitea a result is on its way, so a PR shows the check as running
# rather than missing.
pending:
if: ${{ vars.BUILD_ON == 'github' }}
runs-on: ubuntu-latest
steps:
- env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
jq -n --arg c "$STATUS_CONTEXT" \
--arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
'{state:"pending", context:$c, target_url:$u, description:"GitHub Actions"}' \
| curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" --data @- \
"$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA"
# -------------------------------------------------------------- test ------
# version.test.ts shells out to git to resolve a build version from the
# history, so the checkout is a full one. The hosted runner runs as an
# unprivileged user, so config.test.ts's read-only directory holds here
# without the `su node` Gitea needs.
node:
if: ${{ vars.BUILD_ON == 'github' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 26
cache: npm
# --ignore-scripts: a postinstall script in any transitive dependency
# would otherwise run with the job's credentials in its environment.
- run: npm ci --ignore-scripts
- run: npm run typecheck
- run: npm test
- run: npm run build
# ------------------------------------------------------------- build ------
# Proves the Dockerfile still builds on every change, without pushing.
docker-build:
if: ${{ vars.BUILD_ON == 'github' && github.ref_type == 'branch' }}
needs: [node]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: docker build -t "ihasmail:ci-${GITHUB_SHA::8}" .
# ----------------------------------------------------------- release ------
# Only for `inbuxa-v` tags. 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: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
docker_tag: ${{ steps.v.outputs.docker_tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 26
- id: v
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
V="$(node scripts/version.mjs)"
want="inbuxa-v${V/+/-}"
[ "$TAG" = "$want" ] || { echo "::error::$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 "::error::$TAG is not on main"; exit 1; }
echo "version=$V" >> "$GITHUB_OUTPUT"
# A Docker tag may not contain '+', so build metadata becomes '-'.
echo "docker_tag=${V/+/-}" >> "$GITHUB_OUTPUT"
echo "version $V -> tag ${V/+/-}"
# Each architecture on its own native runner, pushed as an untagged image by
# digest; `publish` joins the two digests into one multi-arch tag.
build:
if: ${{ vars.BUILD_ON == 'github' && startsWith(github.ref, 'refs/tags/inbuxa-v') }}
needs: [node, version]
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ vars.REGISTRY }}
username: jcoffey-dev
password: ${{ secrets.GITEA_TOKEN }}
- name: Build and push by digest
id: push
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
platforms: ${{ matrix.platform }}
build-args: IHASMAIL_VERSION=${{ needs.version.outputs.version }}
# Attestations add manifests of their own to the index, and
# `imagetools create` below expects the two entries pushed here.
provenance: false
sbom: false
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Save the digest
env:
DIGEST: ${{ steps.push.outputs.digest }}
run: |
mkdir -p /tmp/digests
# Bare hash as the filename; the prefix is put back when joining.
touch "/tmp/digests/${DIGEST#sha256:}"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digest-${{ strategy.job-index }}
path: /tmp/digests/*
retention-days: 1
if-no-files-found: error
# Joins the digests into `:<version>` and `:latest`, links the package to
# the repository on Gitea, then creates the release there -- last, so a
# release on the page always has its image behind it. The release is made
# with GITEA_TOKEN, a user's token, so Gitea's announce.yml fires for it;
# Gitea's ci.yml announces as well once this run's status arrives, and the
# announce action posts once per tag whichever gets there first.
publish:
if: ${{ vars.BUILD_ON == 'github' && startsWith(github.ref, 'refs/tags/inbuxa-v') }}
needs: [version, build]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ vars.REGISTRY }}
username: jcoffey-dev
password: ${{ secrets.GITEA_TOKEN }}
- name: Create the manifest
env:
DOCKER_TAG: ${{ needs.version.outputs.docker_tag }}
run: |
refs=()
for f in /tmp/digests/*; do refs+=("${IMAGE}@sha256:$(basename "$f")"); done
docker buildx imagetools create -t "${IMAGE}:${DOCKER_TAG}" -t "${IMAGE}:latest" "${refs[@]}"
docker buildx imagetools inspect "${IMAGE}:${DOCKER_TAG}"
# Shows the package on the repository's Packages tab. Idempotent.
- env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/v1/packages/inbuxa/container/ihasmail-inbuxa/-/link/ihasmail-inbuxa" \
|| echo "package already linked (or link refused); not fatal"
- env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.version.outputs.version }}
DOCKER_TAG: ${{ needs.version.outputs.docker_tag }}
run: |
set -eu
API="$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/releases"
# A re-run finds the release already there.
if curl -fsS -o /dev/null -H "Authorization: token $GITEA_TOKEN" "$API/tags/$TAG"; then
echo "release $TAG already exists"; exit 0
fi
body="$(printf 'INBUXA webmail %s.\n\nImage: `%s:%s` (linux/amd64, linux/arm64), also tagged `latest`.' "$VERSION" "$IMAGE" "$DOCKER_TAG")"
jq -n --arg tag "$TAG" --arg body "$body" '{tag_name:$tag, name:$tag, body:$body}' \
| curl -fsS -o /dev/null -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
--data @- "$API"
echo "release $TAG created"
# One commit status on Gitea for the whole run: what Gitea's ci.yml waits
# for, and what a Gitea PR shows.
report:
if: ${{ always() && vars.BUILD_ON == 'github' }}
needs: [pending, node, docker-build, version, build, publish]
runs-on: ubuntu-latest
steps:
- env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
STATE: ${{ contains(needs.*.result, 'failure') && 'failure' || (contains(needs.*.result, 'cancelled') && 'cancelled' || 'success') }}
run: |
# A cancelled run was superseded by a newer run for the same commit (the
# mirror can push one commit twice); that run reports. Posting "failure"
# here would fail the Gitea check while the real build is still going.
if [ "$STATE" = cancelled ]; then echo "cancelled: leaving the result to the newer run"; exit 0; fi
jq -n --arg s "$STATE" --arg c "$STATUS_CONTEXT" \
--arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
'{state:$s, context:$c, target_url:$u, description:"GitHub Actions"}' \
| curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" --data @- \
"$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA"
echo "reported $STATE as '$STATUS_CONTEXT'"