Crafter.Review/.forgejo/workflows/ci.yaml
Jorijn van der Graaf 85dfdc5c08
All checks were successful
Flatpak / check (push) Has been skipped
Flatpak / publish (push) Successful in 6m37s
ci: nodejs in the job containers, for the Node actions
The first publish run stopped at Checkout: actions/checkout, and the
upload and release steps after it, are Node actions, and the Arch
container had no `node`. Both jobs install nodejs with their tools, as
imsd-messages' workflow does.
2026-11-02 11:27:38 +01:00

346 lines
16 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
cp packaging/flatpak/net.catcrafts.CrafterReview.flatpakref dist/
(cd dist && sha256sum * > SHA256SUMS)
ls -la dist
- 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: 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 }}