Crafter.Review/.forgejo/workflows/ci.yaml
Jorijn van der Graaf d5e54ffeb3
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
The config defines the modes (modes.<name>): the kind of artifact the mode
reviews - reply, series or commits, the three shipped partitions; a kind
is a mode of its own name - the lenses it runs, in its kind's gate order,
and the effort it runs at, else the kind's default (max for a reply or a
series, xhigh for a commit range). `run --mode` names any of them, the
dump records mode and kind, and everything that decided by the mode's
name - the partition and hash, the prompts, the schemas, the disposition
and reuse-key rules, what the CLI requires - decides by the kind.

Every lens stays off until a mode lists it; lenses.<name>.run is retired
with a warning that says so, and a lens of another kind, a name that is
no lens, a lens listed twice and an unknown effort are config problems.
The defaults run no lens; the template ships the three kinds as modes
with every lens of their kind at the kind's effort, to trim (the owner's
ruling of 2026-11-01), and init says so as it writes it - a kept config
is the repository's own, and only one that lists no lens gets a step.

The readers of the retired key follow: the CI smoke config, the shipped
skill, the procedure (its description of the config as well as its
mechanics), the manifest template, the run's messages and the comments
that named the three modes. The run lock, the run numbers and the dump
names are the kind's, so one full run per hash holds across the modes
of a kind and their rounds count on together; the dump says which mode
ran. The database broker starts for a lens the mode runs, as every other
reader of the lenses off reads them. A named mode runs through the CLI
in the kernel-modes test.
2026-11-02 00:29:00 +01:00

274 lines
12 KiB
YAML

# SPDX-License-Identifier: AGPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
name: Flatpak
# Crafter.Build's workflow, for this app. One job, three outcomes:
# pull request -> build the Flatpak, run the suite inside it, smoke-test it
# push master -> also publish it as the `beta` branch of the Catcrafts
# repo and refresh the rolling `latest` release
# tag X.Y.Z -> publish it as `stable` and create release X.Y.Z
#
# It builds on the Crafter.Build Flatpak (`base:` in the manifest), whose
# crafter-build and LLVM compile this project, so the runner installs that
# base from the same repo it publishes into.
#
# The `crafter-flatpak` label is the privileged runner on the Hetzner box
# (bubblewrap needs it) with the Sdk install, the flatpak-builder state, the
# repo's web root and the signing key mounted. It is registered to
# Crafter.Build alone today: until this repository has such a runner, this
# workflow does not run.
#
# Both apps publish into one repo: the publish step holds
# /srv/flatpak/.publish.lock, which protects anything once Crafter.Build's
# workflow takes it too. This one writes its own flatpakref and never the
# shared catcrafts.flatpakrepo, which Crafter.Build's workflow owns.
on:
pull_request:
branches: [master]
push:
branches: [master]
tags: ['[0-9]*.[0-9]*.[0-9]*']
workflow_dispatch:
env:
APP_ID: net.catcrafts.CrafterReview
MANIFEST: packaging/flatpak/net.catcrafts.CrafterReview.yaml
CI_MANIFEST: packaging/flatpak/ci-manifest.yaml
METAINFO: packaging/flatpak/net.catcrafts.CrafterReview.metainfo.xml
RUNTIME: org.freedesktop.Sdk//26.08
BASE: net.catcrafts.CrafterBuild//stable
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 jq gnupg go-yq ostree python util-linux
git config --global --add safe.directory '*'
- name: Sdk and base app
# A no-op once the volume is warm; `update` follows point releases
# and a new stable base.
run: |
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo
flatpak install -y --noninteractive flathub "$RUNTIME"
flatpak install -y --noninteractive catcrafts "$BASE"
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[^"]+' "$METAINFO")
if [ "$top" != "$version" ]; then
echo "metainfo lists $top as the newest release, but the tag is $version" >&2
exit 1
fi
else
# Named after the release it follows, as a checkout describes
# itself: 1.0.0-3-g21746b1 is three commits past 1.0.0.
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-review") | ."build-options".env.CRAFTER_REVIEW_VERSION) = strenv(VERSION)' "$MANIFEST" > "$CI_MANIFEST"
grep -n 'CRAFTER_REVIEW_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
# In the app as it ships: the version and the home, every library
# resolved and none through a RUNPATH outside /app, the bundled
# tools, and a freeze and a dry run on a scratch repository (a dry run
# joins no board and takes no lifetime lock, so it runs inside
# `flatpak build`; it exits 2 by design).
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -eu
shown=$(flatpak build build-dir crafter-review --version)
printf '%s\n' "$shown"
printf '%s\n' "$shown" | grep -qF "crafter-review $VERSION (prompt v"
flatpak build build-dir sh -euc '
for f in /app/bin/crafter-review /app/lib/node/bin/node; do
if ldd "$f" | grep "not found"; then echo "$f: a library is missing" >&2; exit 1; fi
bad=$(readelf -d "$f" | sed -n "s/.*(R[UN]*PATH).*\[\(.*\)\]/\1/p" | tr ":" "\n" | grep -v -e "^\$" -e "^/app" -e "^\\\$ORIGIN" || true)
if [ -n "$bad" ]; then echo "$f: a RUNPATH outside /app: $bad" >&2; exit 1; fi
done
rg --version | head -1
node --version
npx --version
git --version'
flatpak build build-dir sh -euc '
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
export GIT_AUTHOR_NAME=CI GIT_AUTHOR_EMAIL=ci@catcrafts.net GIT_COMMITTER_NAME=CI GIT_COMMITTER_EMAIL=ci@catcrafts.net
cd "$(mktemp -d)"
git init -q -b main .
git config user.email ci@catcrafts.net
printf ".review-gates/push/\n.review-gates/trees/\n" > .gitignore
mkdir .review-gates
printf "{\"modes\": {\"commits\": {\"lenses\": [\"repo-better-solution\", \"repo-bug-prover\", \"repo-security\", \"repo-facts\"]}}}\n" > .review-gates/config.json
git add -A
git commit -q -m base
base=$(git rev-parse HEAD)
printf "def hello(name):\n return name\n" > hello.py
git add hello.py
git commit -q -m "add hello"
crafter-review freeze --slug ci --base "$base"
hash=$(jq -r .hash .review-gates/push/ci/freeze.json)
code=0
crafter-review run --artifact .review-gates/push/ci --hash "$hash" --base "$base" --trees . \
--dispositions .review-gates/push/ci/gate-state/dispositions.json --scratch "$PWD/scratch" --dry-run || code=$?
if [ "$code" != 2 ]; then echo "the dry run exited $code, not 2" >&2; exit 1; fi
ls scratch/*-dry/prompts | head -3'
# 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-review-${{ steps.meta.outputs.version }}.flatpak" "$APP_ID" "${{ steps.meta.outputs.branch }}"
cp packaging/flatpak/net.catcrafts.CrafterReview.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-review-${{ 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
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 (Crafter.Build's 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)
# One publish at a time into the repo both apps share: a commit and
# the summary update of one workflow never interleave with the
# other's.
exec 9> "$WEB_DIR/.publish.lock"
flock 9
[ -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"
# This app's ref file only: catcrafts.flatpakrepo is Crafter.Build's.
key=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 --wrap=0)
sed "s|GPGKey=.*|GPGKey=$key|" packaging/flatpak/net.catcrafts.CrafterReview.flatpakref > "$WEB_DIR/net.catcrafts.CrafterReview.flatpakref"
flock -u 9
- 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 Review 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 Markdown, the way
# Crafter.Build's workflow writes its notes.
python3 - > release-notes.md <<'EOF'
import xml.etree.ElementTree as ET
desc = ET.parse("packaging/flatpak/net.catcrafts.CrafterReview.metainfo.xml").getroot().find("releases/release/description")
def text(el):
parts = [el.text or ""]
for child in el:
inner = text(child)
parts.append({"em": f"*{inner}*", "code": f"`{inner}`"}.get(child.tag, inner))
parts.append(child.tail or "")
return " ".join("".join(parts).split())
blocks = list(desc)
out = []
for i, el in enumerate(blocks):
heads_list = i + 1 < len(blocks) and blocks[i + 1].tag in ("ul", "ol")
if el.tag == "p":
out.append(f"## {text(el)}" if heads_list else text(el))
elif el.tag == "ul":
out.append("\n".join(f"- {text(li)}" for li in el))
elif el.tag == "ol":
out.append("\n".join(f"{n}. {text(li)}" for n, li in enumerate(el, 1)))
print("\n\n".join(out))
print("\nInstall:\n\n flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo\n flatpak install catcrafts net.catcrafts.CrafterReview\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-review ${{ github.ref_name }}
release-notes-file: release-notes.md
release-dir: dist
token: ${{ secrets.GITHUB_TOKEN }}