diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 56b6970..dfc5187 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -2,24 +2,31 @@ # SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® name: Flatpak -# Crafter.Build's workflow, for this app, with one change: every push to -# master is a release (2026-11-02: no rolling `beta` or `latest` builds). -# One job, two outcomes: -# pull request -> build the Flatpak, run the suite inside it, smoke-test it -# push master -> also 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. +# 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 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. +# 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 @@ -43,60 +50,40 @@ env: WEB_DIR: /srv/flatpak jobs: - flatpak: - runs-on: crafter-flatpak + check: + if: "!(github.event_name != 'pull_request' && github.ref == 'refs/heads/master')" + runs-on: 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 + 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 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 + persist-credentials: false - - name: Version and channel + - name: Version id: meta run: | set -eu - # The metainfo's newest release is the version and its notes. On - # master 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 - fi echo "version=$version" >> "$GITHUB_OUTPUT" - echo "branch=stable" >> "$GITHUB_OUTPUT" - echo "version $version, flatpak branch stable" + echo "version $version" - name: Build # The manifest is rewritten beside the original so its relative @@ -107,9 +94,9 @@ jobs: 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 }}" \ + flatpak-builder --force-clean --disable-rofiles-fuse \ + --state-dir=.flatpak-builder \ + --repo=repo --default-branch=stable \ build-dir "$CI_MANIFEST" - name: Smoke test @@ -158,18 +145,123 @@ jobs: 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. + 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 - if: github.ref == 'refs/heads/master' run: | mkdir -p dist - flatpak build-bundle repo "dist/crafter-review-${{ steps.meta.outputs.version }}.flatpak" "$APP_ID" "${{ steps.meta.outputs.branch }}" + 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 - if: github.ref == 'refs/heads/master' uses: actions/upload-artifact@v3 with: name: crafter-review-${{ steps.meta.outputs.version }} @@ -177,7 +269,6 @@ jobs: if-no-files-found: error - name: Publish to the Catcrafts repo - if: github.ref == 'refs/heads/master' run: | set -eu if [ ! -s /run/crafter-flatpak/signing-key.asc ] || [ ! -s /run/crafter-flatpak/key-id ]; then @@ -195,7 +286,7 @@ jobs: [ -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 }}" + "$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. @@ -204,7 +295,6 @@ jobs: flock -u 9 - name: Tag the release - if: github.ref == 'refs/heads/master' run: | # The release's tag at the pushed commit. Never forced: a tag that # appeared since the version check fails the push. @@ -212,7 +302,6 @@ jobs: git push origin "refs/tags/${{ steps.meta.outputs.version }}" - name: Release notes - if: github.ref == 'refs/heads/master' run: | # The newest block's description as Markdown, the way # Crafter.Build's workflow writes its notes. @@ -243,7 +332,6 @@ jobs: 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 == 'refs/heads/master' uses: https://code.forgejo.org/actions/forgejo-release@v2 with: direction: upload diff --git a/AGENTS.md b/AGENTS.md index 2f3cafb..82934b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -240,7 +240,7 @@ Runs and Commands drop every `REVIEW_GATES_*`, `CRAFTER_REVIEW_*`, `SEATS_ENV_FI `.forgejo/workflows/ci.yaml` is Crafter.Build's workflow for this app. - **Builds.** A pull request builds the Flatpak, runs the suite inside it and smoke-tests the app as it ships: the version and home, every library resolved and no RUNPATH outside `/app`, the bundled tools, and a freeze and a dry run on a scratch repository. - **Publishing.** Every push to master is a release (2026-11-02, Jorijn: no rolling `beta` or `latest` builds): CI publishes `stable`, tags the pushed commit with the version of the metainfo's newest `` and creates the release with its notes. A push to master therefore adds a new ``; one whose version is released already fails the job before anything is built or published. -- **The runner.** It runs on the `crafter-flatpak` runner, registered to Crafter.Build alone today: until this repository has such a runner, the workflow does not run, and it is unverified on the forge. Its smoke test passes when run by hand against a local build. +- **The runner.** It runs on the Catcrafts organisation's privileged `flatpak` runner, in two jobs by trust, as imsd-messages' workflow does: `check` (a pull request, a run by hand on another branch) builds cold with no host volume; `publish` (master) mounts the crafter-build installation the publish jobs share (`crafter-flatpak-org-system`), the published repository and its signing key. flatpak-builder's state stays in the workspace, so every run builds cold. The workflow is unverified on the forge until its first run; its smoke test passes when run by hand against a local build. - **The shared repo.** Both apps publish into one repo. The publish step holds `/srv/flatpak/.publish.lock`, which protects anything only once Crafter.Build's workflow takes the same lock. This one writes its own flatpakref and never the shared `catcrafts.flatpakrepo`. Pins, moved together: