Crafter.Build/.forgejo/workflows/ci.yaml
Jorijn van der Graaf 0cbded4a3d
All checks were successful
Flatpak / flatpak (pull_request) Successful in 7m20s
Flatpak / flatpak (push) Successful in 11m12s
fix(ci): run on a label only the Crafter.Build runner carries
Master's last two runs (#368 and #370) failed in the Build step with

    The state dir (/root/.cache/flatpak-builder) is not on the same
    filesystem as the target dir (/workspace/Catcrafts/Crafter.Build)

while #369, the 2.2.0 tag run on the same commit with the same
flatpak-builder, passed. The difference was the runner. Since 14 October
the Hetzner box has a second privileged runner, cx33-org-flatpak, which
serves every Catcrafts repository under the label `flatpak` so their
package jobs can run crafter-build as a Flatpak. It mounts no host
volumes, deliberately, since any repository in the organisation can send
it jobs. Whenever it was idle first it took this job too, logged our four
volumes as "not a valid volume, will be ignored", and left the
flatpak-builder state on the container's overlay while the workspace sat
on a Docker volume. Even past that check it would have had no build
cache, no web root and no signing key.

The org runner's label is right for the jobs it was made for, and giving
it our volumes would hand the signing key to every repository in the
organisation. So the job asks for a label only its own runner carries:
`crafter-flatpak`, after the volumes and key directory that runner alone
allows. The runner on the host is relabelled to match
(/etc/forgejo-runner/config-flatpak.yaml, previous copy kept as
config-flatpak.yaml.bak-2026-10-31-label), so until this reaches master
a push there with the old `runs-on: flatpak` goes only to the org runner
and fails the same way.

AGENTS.md now says the label must stay unique and quotes the error a job
on the wrong runner stops with, so the next collision is found from the
log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:25:44 +01:00

208 lines
9.4 KiB
YAML

name: Flatpak
# One job, three outcomes:
# pull request -> build the Flatpak and run the suite inside it
# push master -> also publish the result as the `beta` branch of the
# Catcrafts repo and refresh the rolling `latest` release
# tag X.Y.Z -> publish as `stable` and create release X.Y.Z
#
# The `crafter-flatpak` runner label is the privileged runner on the Hetzner
# box (bubblewrap needs it) with four volumes allowed: the Sdk install, the
# flatpak-builder state (ccache and the LLVM module, so only crafter-build
# itself rebuilds on a normal run), the published repo's web root, and the
# repo signing key, which lives on the runner host and never in the forge.
# No other runner carries that label. Plain `flatpak` also matches the
# organisation's runner, which mounts none of those volumes; a job it picks
# up fails before building anything.
on:
pull_request:
branches: [master]
push:
branches: [master]
# Bare semver tags. Spelled with globs so it means the same whether the
# forge reads it as a workflow pattern or as a plain glob; `latest`, the
# rolling tag this workflow moves itself, does not start with a digit.
tags: ['[0-9]*.[0-9]*.[0-9]*']
workflow_dispatch:
env:
APP_ID: net.catcrafts.CrafterBuild
MANIFEST: packaging/flatpak/net.catcrafts.CrafterBuild.yaml
CI_MANIFEST: packaging/flatpak/ci-manifest.yaml
RUNTIME: org.freedesktop.Sdk//26.08
REPO_DIR: /srv/flatpak/repo
WEB_DIR: /srv/flatpak
jobs:
flatpak:
runs-on: crafter-flatpak
container:
image: archlinux:latest
options: >-
-v crafter-flatpak-system:/var/lib/flatpak
-v crafter-flatpak-builder:/root/.cache/flatpak-builder
-v /var/www/flatpak:/srv/flatpak
-v /etc/crafter-flatpak:/run/crafter-flatpak:ro
steps:
- name: Install tools
run: |
pacman-key --init
pacman-key --populate archlinux
pacman -Sy --noconfirm --needed archlinux-keyring
pacman -Syu --noconfirm --needed flatpak flatpak-builder ccache git nodejs jq gnupg go-yq ostree python
git config --global --add safe.directory '*'
- name: Sdk
# A no-op once the volume is warm; `update` follows point releases.
run: |
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install -y --noninteractive flathub "$RUNTIME"
flatpak update -y --noninteractive
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
- name: Version and channel
id: meta
run: |
set -eu
if [ "${GITHUB_REF_TYPE}" = tag ]; then
version="$GITHUB_REF_NAME"
branch=stable
# The metainfo's newest release is the release notes and must be
# the version being tagged.
top=$(grep -m1 -oP '<release version="\K[^"]+' packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml)
if [ "$top" != "$version" ]; then
echo "metainfo lists $top as the newest release, but the tag is $version" >&2
exit 1
fi
else
# A master build names itself after the release it follows, the
# way a local checkout does: 2.0.0-3-g21746b1 is three commits
# past 2.0.0. Before the first tag there is nothing to describe
# from and the bare hash gets a placeholder in front.
described=$(git describe --tags --always --exclude=latest)
case "$described" in
[0-9]*.[0-9]*.[0-9]*) version="$described" ;;
*) version="0.0.0-dev+$described" ;;
esac
branch=beta
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
echo "version $version, flatpak branch $branch"
- name: Build
# The manifest is rewritten beside the original so its relative
# source paths still resolve. The suite runs inside this build.
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -eu
yq '(.modules[] | select(.name == "crafter-build") | ."build-options".env.CRAFTER_BUILD_VERSION) = strenv(VERSION)' "$MANIFEST" > "$CI_MANIFEST"
grep -n 'CRAFTER_BUILD_VERSION' "$CI_MANIFEST"
flatpak-builder --force-clean --ccache --disable-rofiles-fuse \
--state-dir=/root/.cache/flatpak-builder \
--repo=repo --default-branch="${{ steps.meta.outputs.branch }}" \
build-dir "$CI_MANIFEST"
- name: Smoke test
run: |
flatpak-builder --run build-dir "$CI_MANIFEST" crafter-build --version
flatpak-builder --run build-dir "$CI_MANIFEST" sh -c 'cd examples/hello-world && crafter-build && ls bin/*/hello*'
# Everything below publishes and only runs on master and on tags.
- name: Bundle
if: github.event_name != 'pull_request'
run: |
mkdir -p dist
flatpak build-bundle repo "dist/crafter-build-${{ steps.meta.outputs.version }}.flatpak" "$APP_ID" "${{ steps.meta.outputs.branch }}"
cp packaging/flatpak/net.catcrafts.CrafterBuild.flatpakref dist/
(cd dist && sha256sum * > SHA256SUMS)
ls -la dist
- name: Upload bundle
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v3
with:
name: crafter-build-${{ steps.meta.outputs.version }}
path: dist/*.flatpak
if-no-files-found: error
- name: Publish to the Catcrafts repo
if: github.event_name != 'pull_request'
run: |
set -eu
# The signing key is generated once on the runner host
# (/etc/crafter-flatpak: signing-key.asc and key-id) and mounted
# read-only; it is imported into a throwaway keyring per run.
if [ ! -s /run/crafter-flatpak/signing-key.asc ] || [ ! -s /run/crafter-flatpak/key-id ]; then
echo "no signing key under /etc/crafter-flatpak on the runner host; generate it as described in AGENTS.md (CI and publishing)" >&2
exit 1
fi
export GNUPGHOME=$(mktemp -d)
gpg --batch --quiet --import /run/crafter-flatpak/signing-key.asc
FLATPAK_GPG_KEY_ID=$(cat /run/crafter-flatpak/key-id)
# The live repo is the canonical copy; builds are promoted into it,
# never built into it.
# Keyed on the repo's config file, not the directory: the web root
# may exist as an empty directory before the first publish.
[ -f "$REPO_DIR/config" ] || ostree init --repo="$REPO_DIR" --mode=archive
flatpak build-commit-from --src-repo=repo \
--gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" \
"$REPO_DIR" "app/$APP_ID/x86_64/${{ steps.meta.outputs.branch }}"
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" \
--generate-static-deltas --prune "$REPO_DIR"
# The repo description and the app ref carry the public key.
key=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 --wrap=0)
sed "s|GPGKey=.*|GPGKey=$key|" packaging/flatpak/catcrafts.flatpakrepo > "$WEB_DIR/catcrafts.flatpakrepo"
sed "s|GPGKey=.*|GPGKey=$key|" packaging/flatpak/net.catcrafts.CrafterBuild.flatpakref > "$WEB_DIR/net.catcrafts.CrafterBuild.flatpakref"
- name: Rolling 'latest' release
if: github.event_name != 'pull_request' && github.ref_type != 'tag'
run: |
git config user.email "ci@catcrafts.net"
git config user.name "Crafter Build CI"
git tag -f latest
git push origin latest --force
- name: Publish rolling 'latest' release
if: github.event_name != 'pull_request' && github.ref_type != 'tag'
uses: https://code.forgejo.org/actions/forgejo-release@v2
with:
direction: upload
url: ${{ github.server_url }}
repo: ${{ github.repository }}
tag: latest
title: Latest master build
prerelease: true
override: true
release-dir: dist
token: ${{ secrets.GITHUB_TOKEN }}
- name: Release notes
if: github.ref_type == 'tag'
run: |
# The newest <release> block's description, as plain text.
python3 - > release-notes.md <<'EOF'
import re, html
text = open("packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml").read()
block = re.search(r"<release [^>]*>(.*?)</release>", text, re.S).group(1)
paras = re.findall(r"<p>(.*?)</p>", block, re.S)
print("\n\n".join(html.unescape(" ".join(p.split())) for p in paras))
print("\nInstall:\n\n flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo\n flatpak install catcrafts net.catcrafts.CrafterBuild\n")
EOF
- name: Publish release
if: github.ref_type == 'tag'
uses: https://code.forgejo.org/actions/forgejo-release@v2
with:
direction: upload
url: ${{ github.server_url }}
repo: ${{ github.repository }}
tag: ${{ github.ref_name }}
title: crafter-build ${{ github.ref_name }}
release-notes-file: release-notes.md
release-dir: dist
token: ${{ secrets.GITHUB_TOKEN }}