# builds.sr.ht manifest for spec.sr.ht. One linear pipeline: install the cache
# helper, assemble the shared SCSS, stamp a version, restore caches, start a
# Postgres in the VM, test, package with abuild, publish, save caches, upload
# this build's own coverage and benchmarks to cov.sr.ht and bench.sr.ht.
#
# The reasoning behind every task lives in docs/ci.md, not here: builds.sr.ht
# stores the submitted manifest in a varchar(16384), so a manifest over 16 KiB
# cannot be submitted at all — and the failure is a branch with no CI, not a red
# build. Add paragraphs to docs/ci.md and a pointer here.
image: alpine/edge
packages:
- abuild
- curl
- go
- git
- rclone
- sassc
- minify
# For the database suites, not for the package — see docs/ci.md#packages.
- postgresql
- postgresql-client
secrets:
# File secret `apk-ci-s3`, installed at ~/.apk-ci.env, containing
# APK_CI_S3_ACCESS_KEY / APK_CI_S3_SECRET_KEY for the Garage `repo` bucket.
- apk-ci-s3
# S3 credentials for the cacher CI cache (Garage `docker-cache` bucket),
# same pair the bencher and ci-cacher builds use.
- 7dde4219-0783-4581-a67d-c94749de3600 # ~/.s3-cache-key-id
- 0e5b3530-6f19-4f30-9b73-9339dd382e46 # ~/.s3-cache-key-secret
# One tokens.sr.ht working token, the same one the siblings mount, carrying
# artifacts:upload, cov:upload and bench:upload. See docs/ci.md#secrets.
- c7968415-1a6d-4ca0-a188-150fb7f57b65 # ~/.srht-token
sources:
- https://git.srht.bigb.es/~bigbes/sr-ht-spec
environment:
REPO: sr-ht-spec
APK_REPO: alpine/v3.22/bigbes/x86_64
S3_BUCKET: repo
S3_ENDPOINT: https://s3.bigb.es
# The second destination of the same apk: one channel of artifacts.sr.ht
# holds the packages of every sibling service, so a consumer adds one
# repository line. See docs/ci.md#publish_artifacts.
ARTIFACTS_ENDPOINT: https://artifacts.srht.bigb.es
ARTIFACTS_CHANNEL: "~bigbes/main"
ARTIFACTS_DIST: v3.22
# Must track the srht deployment's SRHT_CORE_VER, or this service's theme
# drifts from the rest of the instance. BOOTSTRAP_REV is the submodule commit
# core.sr.ht pins at that tag; bump the two together.
CORE_VER: "0.84.5"
BOOTSTRAP_REV: 779ad9f174ea5ab7e755f6df0ec9e5912d67dd16
# Dogfooding. Both repo names are the `sources:` line read as ~owner/repo.
# docs/ci.md#coverage, docs/ci.md#bench.
COVER_ORIGIN: https://cov.srht.bigb.es
COVER_REPO: "~bigbes/sr-ht-spec"
BENCH_ORIGIN: https://bench.srht.bigb.es
BENCH_REPO: "~bigbes/sr-ht-spec"
# Literal paths relative to $HOME, and not a fallback. docs/ci.md#artifacts.
artifacts:
- cover.out
- bench.txt
submitter:
git.sr.ht:
allow-refs:
- refs/heads/master
# Tags build too, now that the version task reads them: pushing v0.9.0 is
# what produces the 0.9.0 apk. See docs/ci.md#version.
- "refs/tags/v*"
tasks:
# S3-backed CI cache helper; installed first so scss can already use it.
# install.sh's PATH export goes to ~/.buildenv, which only the NEXT task
# sources — hence two tasks and not one. See docs/ci.md#cacher.
- cacher_install: |
curl -fsSL https://bigbes.pages.srht.bigb.es/ci-cacher/install.sh | sh
- cacher_init: |
cacher init \
--endpoint https://s3.bigb.es \
--region garage \
--bucket docker-cache \
--prefix sr-ht-spec/deps \
--key-file ~/.s3-cache-key-id \
--secret-file ~/.s3-cache-key-secret
- scss: |
# Assemble the shared sourcehut partials no apk ships, the way
# core.sr.ht's `make install-scss` would, cached by the two pins so an
# outage at git.sr.ht or github.com can't fail us. --exec runs on a miss
# and seeds the cache after; it sees exported vars only, hence the inline
# key and the single quotes. See docs/ci.md#scss.
cacher dir download "scss/${CORE_VER}-${BOOTSTRAP_REV}.tar.zst" ~/scss --exec '
git clone --depth 1 --branch "$CORE_VER" \
https://git.sr.ht/~sircmpwn/core.sr.ht /tmp/core
mkdir -p ~/scss/bootstrap
cp /tmp/core/scss/*.scss /tmp/core/scss/*.css ~/scss/
git init -q /tmp/bootstrap
git -C /tmp/bootstrap remote add origin https://github.com/twbs/bootstrap
git -C /tmp/bootstrap fetch -q --depth 1 origin "$BOOTSTRAP_REV"
git -C /tmp/bootstrap checkout -q FETCH_HEAD
cp -r /tmp/bootstrap/scss ~/scss/bootstrap/scss
'
sudo mkdir -p /usr/share/sourcehut
sudo cp -r ~/scss /usr/share/sourcehut/scss
- keygen: |
# Throwaway signing key, and -i is not optional: docs/ci.md#keygen.
SUDO=sudo abuild-keygen -a -n -i -q
- version: |
# ONE `git describe` decides the apk pkgver: a tag, else tag_git<n>, else
# the family's commit count. The raw describe output is not a legal
# pkgver and _git sorts AFTER the release: docs/ci.md#version.
#
# EXPORTED rather than sed-ed into the APKBUILD (which reads $PKGVER)
# because rewriting a tracked file would flip the VCS stamp Go records in
# the binary to dirty — do not "tidy" it back into a sed. The tree is
# printed because this is the last moment it is provably clean.
cd "$REPO"
desc=$(git describe --tags --always --dirty)
base=${desc%-dirty}
case "$base" in
v*-g*) n=${base%-g*}; ver="${n%-*}"; ver="${ver#v}_git${n##*-}" ;;
v*) ver="${base#v}" ;;
*) ver="0.0.$(git rev-list --count HEAD)" ;;
esac
echo "export PKGVER=$ver" >> ~/.buildenv
echo "building $ver from $desc"
git status --porcelain
- cache_restore: |
# Restore the Go module and build caches, both keyed by go.sum: the
# dependency tree dominates compile time, and it only changes when go.sum
# does. A miss is just a cold build, never an error.
KEY_MOD=$(cacher key "gomod/{hash}.tar.zst" --hash-from "$REPO/go.sum")
KEY_GOC=$(cacher key "gocache/{hash}.tar.zst" --hash-from "$REPO/go.sum")
echo "export KEY_MOD=$KEY_MOD KEY_GOC=$KEY_GOC" >> ~/.buildenv
# abuild redirects the Go caches into its throwaway $tmpdir (and an
# upstream typo slaves GOMODCACHE to GOCACHE), so env exports here can't
# stick — the APKBUILD's build() re-pins both to these home locations.
# --optional makes a miss a cold build, not an error.
cacher dir download "$KEY_MOD" ~/go/pkg/mod --optional
cacher dir download "$KEY_GOC" ~/.cache/go-build --optional
# Repair block for the HALF-restored module cache — the normal failure
# here, not a freak one, and it reads like a code bug. Do not remove and
# do not soften to `|| true`:
# docs/ci.md#the-half-restored-module-cache.
cd "$REPO"
chmod -R u+w ~/go/pkg/mod 2>/dev/null || true
if ! go mod verify >/dev/null 2>&1; then
echo "restored module cache did not verify — discarding it"
rm -rf ~/go/pkg/mod
fi
# `go mod download` and NOT `go mod download all`: the `all` pattern
# resolves the whole module graph, test dependencies of dependencies
# included, and APPENDS their hashes to the tracked go.sum — 170 lines on
# this tree, measured. A modified tracked file is a "-dirty" apk, which is
# the failure this whole commit is about. docs/ci.md#cache_restore.
go mod download
go mod verify
# And the tree is printed because the two lines above are the last thing
# that touches it before abuild does. docs/ci.md#cache_restore.
git status --porcelain
- postgres: |
# A real Postgres in the VM. Without it 67 tests of db/, service/ and
# cmd/specsrht-migrate/ skip themselves and the build goes green having
# exercised none of the persistence layer — the migration-agreement check
# included. Every flag below is load-bearing: docs/ci.md#postgres.
sudo install -d -o postgres -g postgres /run/postgresql /var/lib/postgresql/data
sudo -u postgres initdb -D /var/lib/postgresql/data
sudo -u postgres pg_ctl -D /var/lib/postgresql/data -l /tmp/pg.log -w start \
-o "-k /run/postgresql -h 127.0.0.1 \
-c fsync=off -c full_page_writes=off -c synchronous_commit=off"
sudo -u postgres createuser -s "$(id -un)"
sudo -u postgres createdb -O "$(id -un)" specsrht_test
echo "export SPECSRHT_TEST_PG='postgresql://$(id -un)@127.0.0.1/specsrht_test?sslmode=disable'" \
>> ~/.buildenv
- test: |
cd "$REPO"
# An empty DSN would skip every Postgres-backed suite and leave the build
# green over untested code — and `options="!check"` in the APKBUILD means
# this task is the only place the suites run at all. It also catches a
# reordering of the two tasks. docs/ci.md#test.
if [ -z "$SPECSRHT_TEST_PG" ]; then
echo "SPECSRHT_TEST_PG is unset: the postgres task did not export it," >&2
echo "so every database suite would skip and this build would lie." >&2
exit 1
fi
test -z "$(gofmt -l .)" || { gofmt -l .; echo "gofmt: files above need formatting" >&2; exit 1; }
go vet ./...
# `make cover`, not a bare `go test ./...`: the Makefile names the -timeout
# and the coverage flags, and it is the suites `make test` runs, so the
# profile is a by-product of the gate. docs/ci.md#test.
make cover COVERPROFILE="$HOME/cover.out"
- build: |
cd "$REPO"
# -d: makedepends come from `packages:`. The APKBUILD runs `make css`
# before `make build` and asserts the result with `make check-css`.
# See docs/ci.md#build.
REPODEST=$HOME/packages abuild -d
find "$HOME/packages" -name '*.apk'
- publish: |
# The gate is the honest answer to a build that was handed no secrets, not
# a fallback: with ~/.apk-ci.env absent every earlier task has still run
# and a signed apk is sitting in $HOME/packages. On a push the secret is
# there and this publishes. See docs/ci.md#publish.
if [ ! -r ~/.apk-ci.env ]; then
echo "no ~/.apk-ci.env: this build has no apk repo credentials"
echo "the package was built and signed, and is not published"
exit 0
fi
set +x # never echo the S3 credentials into the build log
. ~/.apk-ci.env
export RCLONE_CONFIG_GARAGE_TYPE=s3
export RCLONE_CONFIG_GARAGE_PROVIDER=Other
export RCLONE_CONFIG_GARAGE_ENDPOINT="$S3_ENDPOINT"
export RCLONE_CONFIG_GARAGE_REGION=garage
export RCLONE_CONFIG_GARAGE_FORCE_PATH_STYLE=true
export RCLONE_CONFIG_GARAGE_ACCESS_KEY_ID="$APK_CI_S3_ACCESS_KEY"
export RCLONE_CONFIG_GARAGE_SECRET_ACCESS_KEY="$APK_CI_S3_SECRET_KEY"
set -x
# Upload only; never delete. Old versions stay so a pinned deployment can
# always be rebuilt — the same reason the upstream mirror is append-only.
find "$HOME/packages" -name '*.apk' -print | while read -r f; do
rclone copyto "$f" "garage:$S3_BUCKET/$APK_REPO/$(basename "$f")"
echo "uploaded $(basename "$f")"
done
echo "published; apk-mirror on phoebe re-indexes within 15 minutes"
- publish_artifacts: |
# The same apk into the artifacts.sr.ht channel, beside the S3 copy above.
# The two destinations are independent on purpose: S3 is the road phoebe
# still walks, this one is the road being opened. Its failure is its own,
# and it never rolls back the upload that already succeeded.
# See docs/ci.md#publish_artifacts.
if [ ! -r ~/.srht-token ]; then
echo "no ~/.srht-token: nothing was published to artifacts.sr.ht"
exit 0
fi
count=$(find "$HOME/packages" -name '*.apk' | wc -l)
test "$count" -gt 0 || { echo "no .apk under $HOME/packages" >&2; exit 1; }
url="$ARTIFACTS_ENDPOINT/api/v1/pkg/$ARTIFACTS_CHANNEL/apk/$ARTIFACTS_DIST"
# A `find | while read` would run the body in a subshell under ash and
# lose $failed with it, so the loop reads a word list instead.
failed=
for file in $(find "$HOME/packages" -name '*.apk'); do
set +x # the token must not reach the log
code=$(curl -sS -o /tmp/artifacts.out -w '%{http_code}' -X PUT \
-H "Authorization: Bearer $(cat ~/.srht-token)" \
--data-binary "@$file" "$url")
set -x
case "$code" in
200|201)
echo "published $(basename "$file") -> $code"
;;
409)
# Same version, other bytes. abuild stamps mtimes into the archive,
# so resubmitting one commit builds a byte-different apk under the
# same pkgver; the published copy stands and the build stays green.
echo "WARNING: $(basename "$file") already published with other bytes; kept the published copy"
;;
*)
echo "FAILED $(basename "$file") -> $code" >&2
cat /tmp/artifacts.out >&2
echo >&2
failed=1
;;
esac
done
test -z "$failed" || exit 1
echo "index: $ARTIFACTS_ENDPOINT/$ARTIFACTS_CHANNEL/apk/$ARTIFACTS_DIST/x86_64/APKINDEX.tar.gz"
- cache_save: |
# AFTER publish so an S3 hiccup cannot strand a good apk, and fatal on
# purpose. Without --force an upload skips a key already there, so no
# `cacher exists ||` guard is needed. See docs/ci.md#cache_save.
cacher dir upload "$KEY_MOD" ~/go/pkg/mod
cacher dir upload "$KEY_GOC" ~/.cache/go-build
- coverage: |
# Dogfooding: the profile the test task wrote, POSTed to this instance's
# own cov.sr.ht. Before bench, whose run is minutes. docs/ci.md#coverage.
cd "$REPO"
# Missing or empty is a 400 about a body rather than about the build.
test -s "$HOME/cover.out" || { echo "no ~/cover.out" >&2; exit 1; }
if [ ! -r ~/.srht-token ]; then
echo "no ~/.srht-token: no cov.sr.ht credentials in this build"
echo "the profile is this build's cover.out artifact and is not lost"
exit 0
fi
# Both ref prefixes stripped (this builds tags too), key is the idempotency
# key, no Content-Type (the service sniffs), set +x so the header stays out
# of the log, --fail-with-body so a rejection is loud and readable.
# docs/ci.md#the-two-requests.
ref="${GIT_REF#refs/heads/}"; ref="${ref#refs/tags/}"
url="$COVER_ORIGIN/api/v1/repos/$COVER_REPO/reports"
url="$url?commit=$(git rev-parse HEAD)&ref=$ref&key=$JOB_ID&job_url=$JOB_URL"
echo "uploading cover.out to $url"
set +x
curl -sS --fail-with-body -X POST \
-H "Authorization: Bearer $(cat ~/.srht-token)" \
--data-binary "@$HOME/cover.out" \
"$url"
echo
- bench: |
# Dogfooding: this service's own benchmarks, to this instance's own
# bench.sr.ht. Last and its own task on purpose, and this VM measures a
# shape rather than a number. docs/ci.md#bench.
cd "$REPO"
# -s so the recipe is not echoed into the body, and a redirect and a cat
# and NOT `| tee` — tee's exit status would let a failed run pass.
make -s bench > "$HOME/bench.txt"
cat "$HOME/bench.txt"
# `go test -bench` matching nothing prints `ok` and exits 0, and an empty
# body is valid benchfmt, so the names are checked. docs/ci.md#the-two-greps
grep -q '^BenchmarkCompare' "$HOME/bench.txt"
grep -q '^BenchmarkLinkPass' "$HOME/bench.txt"
if [ ! -r ~/.srht-token ]; then
echo "no ~/.srht-token: no bench.sr.ht credentials in this build"
echo "the run is above and is this build's bench.txt artifact"
exit 0
fi
# The coverage request's shape, plus visibility= — which acts only on the
# POST that creates $BENCH_REPO.
ref="${GIT_REF#refs/heads/}"; ref="${ref#refs/tags/}"
url="$BENCH_ORIGIN/api/v1/repos/$BENCH_REPO/runs"
url="$url?commit=$(git rev-parse HEAD)&ref=$ref&key=$JOB_ID&job_url=$JOB_URL"
url="$url&visibility=public"
echo "uploading bench.txt to $url"
set +x
curl -sS --fail-with-body -X POST \
-H "Authorization: Bearer $(cat ~/.srht-token)" \
--data-binary "@$HOME/bench.txt" \
"$url"
echo