Crafter.Review/.forgejo/workflows/ci.yaml
Jorijn van der Graaf 0d3d8ed2d2
All checks were successful
Flatpak / check (push) Has been skipped
Flatpak / publish (push) Successful in 5m20s
ci: the release carries the keyed flatpakref
The Bundle step copied the checkout's flatpakref into the release before
the publish step wrote the keyed one to the web root, so 1.0.0's asset
said GPGKey=GPG_KEY_BASE64_PLACEHOLDER and installing from it failed. A
step after publishing copies the web root's copy into the release files
and writes their checksums. Released as 1.0.1.
2026-11-02 12:53:03 +01:00

352 lines
17 KiB
YAML

# SPDX-License-Identifier: AGPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
name: Flatpak
# Every push to master is a release (2026-11-02: no rolling `beta` or
# `latest` builds). Two jobs, by trust, the way imsd-messages' workflow
# splits them, both on the Catcrafts organisation's privileged `flatpak`
# runner (bubblewrap needs the privilege):
# check -> a pull request or a run by hand on another branch: build the
# Flatpak, run the suite inside it, smoke-test it - cold, with
# no host volume, since the job runs as root in a privileged
# container and must reach neither the shared installation nor
# the published repository or its key
# publish -> master, pushed or run by hand: the same, then publish it as
# `stable` and cut release X.Y.Z, the metainfo's newest
# <release>: its tag at the pushed commit and its notes. A
# version that is released already fails the job before
# anything is built or published. It mounts the volumes
# imsd-messages' publish job mounts but its cache: the
# crafter-build installation the publish jobs share
# (crafter-flatpak-org-system: crafter-build //stable and its
# runtime), /var/www/flatpak (the published repository) and
# /etc/crafter-flatpak (its signing key, which never enters the
# forge). flatpak-builder's state stays in the workspace, so
# every run builds cold.
#
# It builds on the Crafter.Build Flatpak (`base:` in the manifest), whose
# crafter-build and LLVM compile this project, so a job installs that base
# from the same repo it publishes into.
#
# 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]
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:
check:
if: "!(github.event_name != 'pull_request' && github.ref == 'refs/heads/master')"
runs-on: flatpak
container:
image: archlinux:latest
steps:
- name: Install tools
# nodejs: the checkout, upload and release steps are Node actions.
run: |
pacman-key --init
pacman-key --populate archlinux
pacman -Sy --noconfirm --needed archlinux-keyring
pacman -Syu --noconfirm --needed flatpak flatpak-builder git jq gnupg go-yq nodejs ostree python util-linux
git config --global --add safe.directory '*'
- name: Sdk and base app
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"
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Version
id: meta
run: |
set -eu
version=$(grep -m1 -oP '<release version="\K[^"]+' "$METAINFO")
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "version $version"
- 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 --disable-rofiles-fuse \
--state-dir=.flatpak-builder \
--repo=repo --default-branch=stable \
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'
publish:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
runs-on: flatpak
container:
image: archlinux:latest
options: >-
-v crafter-flatpak-org-system:/var/lib/flatpak
-v /var/www/flatpak:/srv/flatpak
-v /etc/crafter-flatpak:/run/crafter-flatpak:ro
steps:
- name: Install tools
# nodejs: the checkout, upload and release steps are Node actions.
run: |
pacman-key --init
pacman-key --populate archlinux
pacman -Sy --noconfirm --needed archlinux-keyring
pacman -Syu --noconfirm --needed flatpak flatpak-builder git jq gnupg go-yq nodejs ostree python util-linux
git config --global --add safe.directory '*'
- name: Sdk and base app
# A no-op once the shared installation holds them; `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
id: meta
run: |
set -eu
# The metainfo's newest release is the version and its notes. It
# must be new: its tag must not exist yet, and a lookup that cannot
# tell stops the job as well.
version=$(grep -m1 -oP '<release version="\K[^"]+' "$METAINFO")
code=0
git ls-remote --exit-code --tags origin "refs/tags/$version" > /dev/null || code=$?
case "$code" in
0) echo "the metainfo's newest release, $version, is released already: a push to master names a new <release> in $METAINFO" >&2; exit 1 ;;
2) ;;
*) echo "git ls-remote exited $code: cannot tell whether $version is released" >&2; exit 1 ;;
esac
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "version $version"
- 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 --disable-rofiles-fuse \
--state-dir=.flatpak-builder \
--repo=repo --default-branch=stable \
build-dir "$CI_MANIFEST"
- name: Smoke test
# As the check job's.
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'
- name: Bundle
run: |
mkdir -p dist
flatpak build-bundle repo "dist/crafter-review-${{ steps.meta.outputs.version }}.flatpak" "$APP_ID" stable
- name: Upload bundle
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
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/stable"
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: Release files
# The flatpakref as published, with the repo's key - the checkout's
# carries a placeholder - and the checksums of what the release carries.
run: |
set -eu
cp "$WEB_DIR/net.catcrafts.CrafterReview.flatpakref" dist/
(cd dist && sha256sum * > SHA256SUMS)
ls -la dist
- name: Tag the release
run: |
# The release's tag at the pushed commit. Never forced: a tag that
# appeared since the version check fails the push.
git tag "${{ steps.meta.outputs.version }}" "$GITHUB_SHA"
git push origin "refs/tags/${{ steps.meta.outputs.version }}"
- name: Release notes
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
uses: https://code.forgejo.org/actions/forgejo-release@v2
with:
direction: upload
url: ${{ github.server_url }}
repo: ${{ github.repository }}
tag: ${{ steps.meta.outputs.version }}
title: crafter-review ${{ steps.meta.outputs.version }}
release-notes-file: release-notes.md
release-dir: dist
token: ${{ secrets.GITHUB_TOKEN }}