ci: build on GitHub via the mirror, switchable with BUILD_ON #7
No files matched your search
+60
-11
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
# this directory exists; .github/workflows is GitHub's side of the switch below.
|
||||
#
|
||||
# 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
|
||||
@@ -12,12 +12,21 @@
|
||||
# links; Gitea attaches the tarballs to the Release itself, as GitHub did, so
|
||||
# the build and the release are one job and nothing is handed between jobs.
|
||||
#
|
||||
# e2e.yml is deliberately NOT ported. e2e/public.sh publishes 25, 80, 443,
|
||||
# 465, 993, 995 and 4190 on the machine it runs on, which on GitHub was a
|
||||
# throwaway VM and here would be the CI host -- where 80 and 443 are nginx
|
||||
# and the mail ports belong to the mail netns. It stays a manual check on a
|
||||
# disposable host until there is a runner that can safely be given those
|
||||
# ports.
|
||||
# BUILD ON GITHUB. The org variable BUILD_ON decides which forge builds.
|
||||
# Unset (or anything but 'github'), the test and release jobs below run here,
|
||||
# as they always have. Set to 'github', they skip and .github/workflows/ci.yml
|
||||
# does the same work on GitHub's runners -- GitHub holds a push mirror of this
|
||||
# repository, updated on every commit -- and reports back as the commit status
|
||||
# "github/ci (branch)" or "github/ci (tag)". The `github` job waits for that
|
||||
# status and passes or fails with it, so a run here still says whether the
|
||||
# commit is good. If GitHub is ever unavailable, unset BUILD_ON and everything
|
||||
# builds here again.
|
||||
#
|
||||
# The end-to-end test is deliberately NOT here. e2e/public.sh publishes 25,
|
||||
# 80, 443, 465, 993, 995 and 4190 on the machine it runs on, which here would
|
||||
# be the CI host -- where 80 and 443 are nginx and the mail ports belong to the
|
||||
# mail netns. It lives in .github/workflows/ci.yml as a job run by hand, on a
|
||||
# GitHub runner, which is a throwaway VM.
|
||||
name: ci
|
||||
|
||||
on:
|
||||
@@ -32,6 +41,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{ vars.BUILD_ON != 'github' }}
|
||||
runs-on: light
|
||||
container:
|
||||
image: golang:1.26-bookworm@sha256:a688600ca24f8a4d3ca77f95b0dd40704a9fc787c826660eb7ba0b641b8b175d # 1.26-bookworm
|
||||
@@ -45,7 +55,7 @@ jobs:
|
||||
- run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
if: ${{ vars.BUILD_ON != 'github' && startsWith(github.ref, 'refs/tags/') }}
|
||||
needs: [test]
|
||||
runs-on: light
|
||||
container:
|
||||
@@ -102,12 +112,51 @@ jobs:
|
||||
|| { curl --silent "${auth[@]}" -X DELETE "$API/releases/$id"; exit 1; }
|
||||
done
|
||||
|
||||
# Stands in for test and release while GitHub builds: waits for the status
|
||||
# GitHub's ci.yml posts on this commit and passes or fails with it. A pull
|
||||
# request is checked at its head commit, which is what GitHub built when
|
||||
# the branch reached the mirror. Two and a half hours covers a slow queue;
|
||||
# a timeout here with BUILD_ON=github usually means GitHub never got the
|
||||
# push -- check the mirror's last error in the repository settings.
|
||||
github:
|
||||
if: ${{ vars.BUILD_ON == 'github' }}
|
||||
runs-on: light
|
||||
timeout-minutes: 150
|
||||
container:
|
||||
image: golang:1.26-bookworm@sha256:a688600ca24f8a4d3ca77f95b0dd40704a9fc787c826660eb7ba0b641b8b175d # 1.26-bookworm
|
||||
steps:
|
||||
- shell: bash
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get update -qq && apt-get install -y -qq --no-install-recommends jq >/dev/null
|
||||
ctx="github/ci (branch)"; [ "$IS_TAG" = true ] && ctx="github/ci (tag)"
|
||||
echo "waiting for '$ctx' on $SHA"
|
||||
while :; do
|
||||
s="$(curl -fsS -H "Authorization: token $TOKEN" \
|
||||
"$CI_SERVER_INTERNAL/api/v1/repos/$REPO/commits/$SHA/statuses?limit=50" \
|
||||
| jq -c --arg c "$ctx" '[.[] | select(.context == $c)] | sort_by(.id) | last // empty')" || s=""
|
||||
state="$(printf '%s' "$s" | jq -r '.status // .state // empty')"
|
||||
case "$state" in
|
||||
success) echo "GitHub: success"; exit 0 ;;
|
||||
failure|error) echo "GitHub: $state -- $(jq -r '.target_url' <<<"$s")"; exit 1 ;;
|
||||
esac
|
||||
sleep 20
|
||||
done
|
||||
|
||||
# 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.
|
||||
# 'on: release' never fires for it -- so announce it from here. When GitHub
|
||||
# builds, the Release is made there and published only once its files are
|
||||
# attached; this waits for GitHub's success and announces then. The action
|
||||
# makes one topic per tag, so a second announce for the same tag is a no-op.
|
||||
announce:
|
||||
needs: [release]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: [release, github]
|
||||
if: ${{ always() && startsWith(github.ref, 'refs/tags/') && (needs.release.result == 'success' || needs.github.result == 'success') }}
|
||||
runs-on: light
|
||||
steps:
|
||||
- uses: coffey-labs/actions/discourse-release@e9293996e2efa770839121fa8f8da93083f216be
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# CI on GitHub Actions, for the GitHub copy of this repository.
|
||||
#
|
||||
# The repository lives on the self-hosted Gitea; GitHub holds a push mirror
|
||||
# that Gitea updates on every commit. Nothing is merged here -- pull requests
|
||||
# happen on Gitea, and their branches reach GitHub as ordinary pushes, which is
|
||||
# why this workflow runs on `push` and not on `pull_request`.
|
||||
#
|
||||
# Which forge does the building is one switch, the variable BUILD_ON, set on
|
||||
# both forges at the organization level:
|
||||
#
|
||||
# BUILD_ON=github every job here runs; .gitea/workflows/ci.yml skips its own
|
||||
# test and release jobs and waits for the status this
|
||||
# workflow reports back instead.
|
||||
# unset / other every job here skips; Gitea builds as it always has.
|
||||
#
|
||||
# The result goes back to Gitea as one commit status, "github/ci (branch)" or
|
||||
# "github/ci (tag)", which is what Gitea's `github` job waits on.
|
||||
#
|
||||
# Tags do what the Gitea release job does: check the tag is on main, build
|
||||
# the reproducible archives, and attach them to the Gitea Release -- Gitea is
|
||||
# where the install guide points, so that is where the files go. The Release
|
||||
# is created as a draft, filled, then published, so it is never visible with
|
||||
# assets missing; if an upload fails, a Release this run created is deleted.
|
||||
# Gitea announces the release once this run has reported success.
|
||||
#
|
||||
# Go cross-compiles both architectures from one amd64 runner, so there is no
|
||||
# arm64 job.
|
||||
#
|
||||
# The end-to-end test (e2e job) runs only when this workflow is started by
|
||||
# hand. e2e/public.sh publishes 25, 80, 443, 465, 993, 995 and 4190 on the
|
||||
# machine it runs on, so it needs a throwaway VM -- which a GitHub runner is and
|
||||
# the self-hosted runners are not. It used to run every Monday on its own
|
||||
# schedule; schedules belong to Gitea now, so for the moment it is a manual
|
||||
# check: run this workflow from main after ihasmail's weekly release.
|
||||
#
|
||||
# Configuration, all at the organization level on GitHub: variables BUILD_ON,
|
||||
# GITEA_URL; secret GITEA_TOKEN (write:repository on Gitea).
|
||||
#
|
||||
# Every `uses:` is pinned to a full commit SHA, with the release it was in the
|
||||
# trailing comment. Nothing schedules here: schedules belong to Gitea.
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags: ['**']
|
||||
# For a run GitHub queued and then orphaned, and for the end-to-end test.
|
||||
# Run it from a tag to redo that tag's release; attaching skips any file
|
||||
# already on the Release.
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_type != 'tag' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL }}
|
||||
CONTEXT: github/ci (${{ github.ref_type }})
|
||||
|
||||
jobs:
|
||||
# Tells Gitea a result is on its way, so a status that is still missing
|
||||
# reads as "running" rather than "never started".
|
||||
pending:
|
||||
if: vars.BUILD_ON == 'github'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \
|
||||
"$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
|
||||
-d "$(jq -n --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
|
||||
'{state:"pending", context:$c, target_url:$u, description:"GitHub Actions"}')"
|
||||
|
||||
test:
|
||||
if: vars.BUILD_ON == 'github'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: go vet ./...
|
||||
- run: go test ./...
|
||||
# `@latest` on purpose: a vulnerability check should use today's
|
||||
# database, not a pinned copy of last month's.
|
||||
- run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||
|
||||
release:
|
||||
if: vars.BUILD_ON == 'github' && github.ref_type == 'tag'
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
# Full history: the ancestry check cannot be answered from a shallow
|
||||
# clone.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
# A release can never describe code that was not reviewed onto main.
|
||||
- run: |
|
||||
git fetch --quiet origin main
|
||||
git merge-base --is-ancestor "$(git rev-parse "${TAG}^{commit}")" origin/main \
|
||||
|| { echo "::error::$TAG is not on main"; exit 1; }
|
||||
# SOURCE_DATE_EPOCH makes the tarballs reproducible: two builds of one
|
||||
# tag give byte-identical archives.
|
||||
- run: |
|
||||
SOURCE_DATE_EPOCH="$(git log -1 --format=%ct "$TAG")" scripts/build-release.sh "$TAG" dist
|
||||
sha256sum dist/*.tar.gz
|
||||
- env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
API="$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
auth=(-H "Authorization: token $GITEA_TOKEN")
|
||||
# Reuse the Release if the tag already has one (a re-run), else make a
|
||||
# draft of our own.
|
||||
created=0
|
||||
id="$(curl -fsS "${auth[@]}" "$API/releases/tags/$TAG" 2>/dev/null | jq -r '.id // empty' || true)"
|
||||
if [ -z "$id" ]; then
|
||||
id="$(curl -fsS "${auth[@]}" -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --arg t "$TAG" '{tag_name:$t, name:$t, draft:true,
|
||||
body:"Binaries for linux/amd64 and linux/arm64. Verify with SHA256SUMS."}')" \
|
||||
"$API/releases" | jq -r '.id // empty')"
|
||||
[ -n "$id" ] || { echo "::error::could not create the release"; exit 1; }
|
||||
created=1
|
||||
fi
|
||||
have="$(curl -fsS "${auth[@]}" "$API/releases/$id/assets" | jq -r '.[].name')"
|
||||
for f in dist/*; do
|
||||
n="$(basename "$f")"
|
||||
if grep -qxF "$n" <<<"$have"; then echo "already attached: $n"; continue; fi
|
||||
echo "uploading $n"
|
||||
curl -fsS -o /dev/null "${auth[@]}" --form "attachment=@$f" "$API/releases/$id/assets?name=$n" || {
|
||||
[ "$created" = 1 ] && curl -sS -o /dev/null "${auth[@]}" -X DELETE "$API/releases/$id"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
if [ "$created" = 1 ]; then
|
||||
curl -fsS -o /dev/null "${auth[@]}" -H 'Content-Type: application/json' -X PATCH \
|
||||
-d '{"draft":false}' "$API/releases/$id"
|
||||
fi
|
||||
|
||||
# A deploy with no --ihasmail-image takes whatever ihasmail release is
|
||||
# newest, so this is what notices when a new release stops working with the
|
||||
# Stalwart and Caddy versions pinned here. Pebble stands in for Let's
|
||||
# Encrypt and a DNS stub answers every name: nothing but images comes from
|
||||
# the internet.
|
||||
e2e:
|
||||
if: vars.BUILD_ON == 'github' && github.event_name == 'workflow_dispatch'
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: e2e/public.sh
|
||||
|
||||
report:
|
||||
if: always() && vars.BUILD_ON == 'github'
|
||||
needs: [pending, test, release, e2e]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
STATE: ${{ (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && 'failure' || 'success' }}
|
||||
run: |
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \
|
||||
"$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
|
||||
-d "$(jq -n --arg s "$STATE" --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
|
||||
'{state:$s, context:$c, target_url:$u, description:"GitHub Actions"}')"
|
||||
@@ -1,41 +0,0 @@
|
||||
# The end-to-end test, every week, against ihasmail's newest release.
|
||||
#
|
||||
# A deploy with no --ihasmail-image takes whatever ihasmail release is newest
|
||||
# when it runs, rather than one this repository pinned. That is only safe if
|
||||
# something notices when a new release stops working with the Stalwart and
|
||||
# Caddy versions pinned here -- this is that something. It runs on Mondays
|
||||
# three hours after ihasmail's weekly release (09:00 UTC), so a release that
|
||||
# breaks the one shot shows up the same day, before most people deploy it.
|
||||
#
|
||||
# e2e/public.sh needs nothing from the internet but images: Pebble stands in for
|
||||
# Let's Encrypt and a DNS stub answers every name, and it publishes 25, 80, 443,
|
||||
# 465, 993, 995 and 4190 on the runner while it runs.
|
||||
name: End-to-end
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: e2e
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
public:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Vet and test
|
||||
run: |
|
||||
go vet ./...
|
||||
go test ./...
|
||||
- name: Deploy and check a public stack
|
||||
run: e2e/public.sh
|
||||
@@ -1,69 +0,0 @@
|
||||
# Publish a release when a version tag is pushed: test, check for known
|
||||
# vulnerabilities, build the Linux binaries, attach them with their checksums.
|
||||
#
|
||||
# Tags are the date of the commit, as ihasmail's are: v2026.9.13, and
|
||||
# v2026.9.13.1 for a second release on the same day.
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
# For a tag whose run never started. GitHub has queued and then orphaned
|
||||
# runs before, and a pushed tag has no other way to trigger this again.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Existing tag to release, e.g. v2026.9.13"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.event.inputs.tag || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Check the tag names a commit on main
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --quiet origin main
|
||||
git merge-base --is-ancestor "$(git rev-parse "$TAG^{commit}")" origin/main \
|
||||
|| { echo "::error::$TAG is not on main"; exit 1; }
|
||||
|
||||
- name: Vet and test
|
||||
run: |
|
||||
go vet ./...
|
||||
go test ./...
|
||||
|
||||
- name: Known vulnerabilities
|
||||
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
SOURCE_DATE_EPOCH="$(git log -1 --format=%ct "$TAG")" scripts/build-release.sh "$TAG" dist
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
gh release upload "$TAG" dist/* --clobber
|
||||
else
|
||||
gh release create "$TAG" dist/* --verify-tag --title "$TAG" --generate-notes
|
||||
fi
|
||||
Reference in new issue
Block a user