# 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 # : 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 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 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 '> "$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 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 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 ' /dev/null || code=$? case "$code" in 0) echo "the metainfo's newest release, $version, is released already: a push to master names a new 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 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 }}