Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a032ceed7 | ||
|
|
d7be002c19 | ||
|
|
67aab8015a |
@@ -0,0 +1,170 @@
|
|||||||
|
# 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. 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. 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.
|
||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['**']
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# -------------------------------------------------------------- test ------
|
||||||
|
node:
|
||||||
|
runs-on: docker
|
||||||
|
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, so nothing is handed over from the
|
||||||
|
# node job; `needs` only keeps the order.
|
||||||
|
docker-build:
|
||||||
|
if: ${{ !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"
|
||||||
|
|
||||||
|
# ----------------------------------------------------------- publish ------
|
||||||
|
# Tag-driven. GitHub needed a release -> publish workflow_call chain because
|
||||||
|
# a release cut with GITHUB_TOKEN raises no event -- and Gitea behaves the
|
||||||
|
# same way, which is why weekly-release.yml cuts its release with
|
||||||
|
# RELEASE_TOKEN: a tag made with that token is an ordinary push, and starts
|
||||||
|
# this workflow.
|
||||||
|
#
|
||||||
|
# The version the image is built with, computed the way publish.yml did it:
|
||||||
|
# scripts/version.mjs, which needs node and the full history. The build is
|
||||||
|
# *told* the real form (IHASMAIL_VERSION, what About and /api/health
|
||||||
|
# report); the Docker tag gets the same string with '+' turned into '-',
|
||||||
|
# because a tag may not contain '+'. Leaving the build arg out would ship an
|
||||||
|
# image reporting itself unversioned -- which is exactly what
|
||||||
|
# version.test.ts calls looking wrong.
|
||||||
|
version:
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
|
runs-on: docker
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
V="$(node scripts/version.mjs)"
|
||||||
|
echo "VERSION=$V" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "DOCKER_TAG=${V/+/-}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "VERSION=$V DOCKER_TAG=${V/+/-}"
|
||||||
|
|
||||||
|
# arm64 is built under QEMU on this amd64 host, not on a native runner as
|
||||||
|
# GitHub's free `ubuntu-24.04-arm` did. It is slow -- tens of minutes for the
|
||||||
|
# npm install and Vite build through instruction translation -- which is
|
||||||
|
# tolerable for a weekly tag and would not be for every push. That is why
|
||||||
|
# this job is tag-only. If arm64 ever starts timing out, the fix is an arm64
|
||||||
|
# runner, not dropping the platform: TrueNAS and Unraid users pull it.
|
||||||
|
#
|
||||||
|
# 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: [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"
|
||||||
|
# Gitea keeps a container package on its owner; linking it shows it 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"
|
||||||
|
- if: always()
|
||||||
|
run: docker logout "$REGISTRY" || true
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Weekly release, ported from the weekly-release job in .gitlab-ci.yml (itself
|
||||||
|
# a port of .github/workflows/release.yml): cut a release once a week, but
|
||||||
|
# only when 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 or if the tag already exists (the version comes from the
|
||||||
|
# commit, so an unchanged commit is an existing tag).
|
||||||
|
#
|
||||||
|
# Mondays 09:17 UTC, the same odd minute as before. 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 tags reach this copy
|
||||||
|
# through the sync. Two releasers would race to create the same tag, 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. The release -- and with it the tag -- is
|
||||||
|
# created with 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 has to start ci.yml's version and publish jobs.
|
||||||
|
name: weekly-release
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '17 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 create the same tag.
|
||||||
|
concurrency:
|
||||||
|
group: weekly-release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
weekly-release:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: node:26-bookworm-slim@sha256:582460f614631b59b824ac6020533b9bf339c7fdf3a6d7db31abb6b4065f0212 # 26-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 >/dev/null
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Internal address, as for everything else CI does: never through the proxy.
|
||||||
|
API="${CI_SERVER_INTERNAL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
|
# 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;
|
||||||
|
# under-counting would skip one that was.
|
||||||
|
# Tag lookups use show-ref, which matches an exact ref and nothing
|
||||||
|
# else. `rev-parse --verify refs/tags/<name>` does not: on the git in
|
||||||
|
# this image (2.39) a name ending in -g<hex> falls back to being read
|
||||||
|
# as git-describe output, resolves to that commit, and so "exists"
|
||||||
|
# whether or not the tag does. Every commit not merged through a pull
|
||||||
|
# request has a -g<hex> version, so that check reported every such
|
||||||
|
# week as already released.
|
||||||
|
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
|
||||||
|
version="$(node scripts/version.mjs)"
|
||||||
|
# A Docker tag may not contain '+', and neither should the git tag,
|
||||||
|
# so the two always agree about what to call a build.
|
||||||
|
tag="v${version/+/-}"
|
||||||
|
title="v${version%%+*}"
|
||||||
|
sha="$(git rev-parse HEAD)"
|
||||||
|
if [ "$count" -eq 0 ]; then
|
||||||
|
echo "Nothing to release: no commits since ${previous}."; exit 0
|
||||||
|
fi
|
||||||
|
if git show-ref --verify --quiet "refs/tags/${tag}"; then
|
||||||
|
echo "Nothing to release: tag ${tag} already exists."; exit 0
|
||||||
|
fi
|
||||||
|
echo "Releasing ${tag} -- ${count} commit(s) since ${previous:-the beginning}, at ${sha}."
|
||||||
|
if [ "$DRY_RUN" = "1" ]; then echo "Dry run (RELEASE_LIVE='${{ vars.RELEASE_LIVE }}'): stopping here."; exit 0; fi
|
||||||
|
# Notes bounded to what is new, from the first-parent history of
|
||||||
|
# main -- one line per merge, which is what GitHub's generated notes
|
||||||
|
# listed.
|
||||||
|
notes="$(git log --first-parent --format='- %s' "$range")"
|
||||||
|
jq -n --arg tag "$tag" --arg ref "$sha" --arg name "$title" \
|
||||||
|
--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 -H "Authorization: token ${RELEASE_TOKEN}" -H "Content-Type: application/json" \
|
||||||
|
--data @release.json "${API}/releases" | jq -r '"created release " + .tag_name'
|
||||||
Reference in New Issue
Block a user