From 025e27b49634f713a5e94da21531442c962eb634 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 17 Jul 2026 14:06:12 +0800 Subject: [PATCH] Move bakery's own release workflow from .github to .forgejo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .github/workflows/release.yml built and published the bakery binary itself, but it lived under .github/ and targeted runs-on: [self-hosted, hestia] — a runner label only registered against Forgejo, never against GitHub Actions. It has therefore never run; get.sh has been pointing at dl.breadway.dev/bakery/... this whole time with nothing actually publishing there. Recreated the same logic as .forgejo/workflows/release-bakery.yml, matching the sibling release-bread-theme.yml in this repo (manual clone instead of actions/checkout, GH_RELEASE_TOKEN instead of the GitHub-provided GITHUB_TOKEN, same dormant-until-provisioned minisign signing step). Removed the dead .github copy. --- .forgejo/workflows/release-bakery.yml | 74 ++++++++++++++ .github/workflows/release.yml | 85 ---------------- docs/release-channels.md | 94 ++++++++++++++++++ scripts/doctor-channels.sh | 133 ++++++++++++++++++++++++++ 4 files changed, 301 insertions(+), 85 deletions(-) create mode 100644 .forgejo/workflows/release-bakery.yml delete mode 100644 .github/workflows/release.yml create mode 100644 docs/release-channels.md create mode 100755 scripts/doctor-channels.sh diff --git a/.forgejo/workflows/release-bakery.yml b/.forgejo/workflows/release-bakery.yml new file mode 100644 index 0000000..283153b --- /dev/null +++ b/.forgejo/workflows/release-bakery.yml @@ -0,0 +1,74 @@ +name: release bakery + +on: + push: + tags: ['v*'] + +jobs: + build: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: build + run: cd src && cargo build --release --locked -p bakery + + - name: test + run: cd src && cargo test --release --locked -p bakery + + - name: prepare artifacts + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/bakery/${VERSION}" + mkdir -p "${PKG_DIR}" + cp "src/target/release/bakery" "${PKG_DIR}/bakery-x86_64" + strip "${PKG_DIR}/bakery-x86_64" + sha256sum "${PKG_DIR}/bakery-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/bakery-x86_64.sha256" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/bakery/latest" + + # Signs the bakery binary itself with the shared bakery ecosystem signing + # key (same key that signs index.json and bread-theme — see + # release-bread-theme.yml). BAKERY_MINISIGN_SEC_KEY_PATH is a *path on + # this runner's disk* (hestia has persistent storage), not the key + # contents. Dormant (binary ships unsigned, as today) until that secret + # is provisioned. + - name: sign release binary + env: + MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/bakery/${VERSION}" + if [ -n "${MINISIGN_SEC_KEY:-}" ]; then + minisign -W -S -s "${MINISIGN_SEC_KEY}" -m "${PKG_DIR}/bakery-x86_64" \ + -x "${PKG_DIR}/bakery-x86_64.minisig" /dev/null || true + ASSETS="${PKG_DIR}/bakery-x86_64 ${PKG_DIR}/bakery-x86_64.sha256" + [ -f "${PKG_DIR}/bakery-x86_64.minisig" ] && ASSETS="${ASSETS} ${PKG_DIR}/bakery-x86_64.minisig" + gh release upload "${GITHUB_REF_NAME}" --repo Breadway/bread-ecosystem ${ASSETS} --clobber diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1f9675b..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: release - -on: - push: - tags: ["v*"] - -permissions: - contents: write - -env: - DL_DIR: /srv/breadway-dl - -jobs: - build: - runs-on: [self-hosted, hestia] - steps: - - uses: actions/checkout@v4 - - - name: build - run: cargo build --release --locked -p bakery - - - name: test - run: cargo test --locked --workspace - - - name: prepare artifacts - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/bakery/${VERSION}" - mkdir -p "${PKG_DIR}" - - cp target/release/bakery "${PKG_DIR}/bakery-x86_64" - strip "${PKG_DIR}/bakery-x86_64" - sha256sum "${PKG_DIR}/bakery-x86_64" | awk '{print $1}' \ - > "${PKG_DIR}/bakery-x86_64.sha256" - - cp bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${VERSION}" "${DL_DIR}/bakery/latest" - - # Signs the bakery binary itself with the same minisign key that signs - # index.json (get.sh pins the matching public key). Dormant until the - # BAKERY_MINISIGN_SEC_KEY secret is actually provisioned in this repo's - # Actions settings — until then this step logs a warning and the - # binary ships unsigned, exactly as it does today. - - name: sign release binary - env: - MINISIGN_SEC_KEY_CONTENTS: ${{ secrets.BAKERY_MINISIGN_SEC_KEY }} - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/bakery/${VERSION}" - if [ -n "${MINISIGN_SEC_KEY_CONTENTS}" ]; then - command -v minisign >/dev/null 2>&1 || { echo "::error::minisign not installed on runner"; exit 1; } - KEY_FILE="$(mktemp)" - trap 'shred -u "${KEY_FILE}" 2>/dev/null || rm -f "${KEY_FILE}"' EXIT - printf '%s' "${MINISIGN_SEC_KEY_CONTENTS}" > "${KEY_FILE}" - minisign -W -S -s "${KEY_FILE}" -m "${PKG_DIR}/bakery-x86_64" \ - -x "${PKG_DIR}/bakery-x86_64.minisig" /dev/null || rm -f "${KEY_FILE}"' EXIT - printf '%s' "${MINISIGN_SEC_KEY_CONTENTS}" > "${KEY_FILE}" - MINISIGN_SEC_KEY="${KEY_FILE}" bash "${GITHUB_WORKSPACE}/scripts/gen-index.sh" - else - bash "${GITHUB_WORKSPACE}/scripts/gen-index.sh" - fi - - - name: upload to GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/bakery/${VERSION}" - gh release create "${GITHUB_REF_NAME}" \ - --title "bakery v${VERSION}" --generate-notes 2>/dev/null || true - ASSETS="${PKG_DIR}/bakery-x86_64 ${PKG_DIR}/bakery-x86_64.sha256" - [ -f "${PKG_DIR}/bakery-x86_64.minisig" ] && ASSETS="${ASSETS} ${PKG_DIR}/bakery-x86_64.minisig" - gh release upload "${GITHUB_REF_NAME}" ${ASSETS} --clobber diff --git a/docs/release-channels.md b/docs/release-channels.md new file mode 100644 index 0000000..952c9c2 --- /dev/null +++ b/docs/release-channels.md @@ -0,0 +1,94 @@ +# Release channel policy + +There are two independent distribution channels in the bread ecosystem, plus +a third "neither" state for repos that aren't distributed yet. Every repo +under `Breadway/` should sit in exactly one of these three buckets, and its +`.forgejo/workflows/` directory + packaging metadata should match that +bucket exactly — no more files, no fewer. + +## The two channels + +**bakery channel** (`bakery install `, `curl .../get | sh`, or a raw +binary download from dl.breadway.dev / the GitHub release page). A repo is on +this channel if and only if **all** of the following are true: + +1. It has a `bakery.toml` at the root (or, for a multi-product repo like + bread-ecosystem, one per product directory). +2. It has an entry in `bread-ecosystem`'s `registry/bread-ecosystem.toml`. + `scripts/gen-index.sh` only ever looks at repos listed there — a + `bakery.toml` that isn't backed by a registry entry is inert. +3. It has a `.forgejo/workflows/release.yml` (or a product-specific name + like `release-bread-theme.yml` / `release-bakery.yml` for multi-product + repos) that builds the binary, drops it under `/srv/breadway-dl//`, + copies `bakery.toml` alongside it, regenerates `index.json` via + `bread-ecosystem/scripts/gen-index.sh`, and uploads the same artifacts to + a GitHub release as a fallback mirror. + +All three must be present together. Two out of three is a bug, not a +partial rollout — either finish the third piece or remove the other two. + +**pacman channel** (`pacman -S ` from the self-hosted `[breadway]` +repo, built via AUR-style `PKGBUILD`s). A repo is on this channel if and +only if: + +1. It has a `PKGBUILD` under `packaging/` (either `packaging/PKGBUILD` or + `packaging/arch/PKGBUILD` — both patterns exist in the wild, pick + whichever a sibling repo of the same shape already uses). +2. It has a `.forgejo/workflows/package.yml` that builds the package in an + `archlinux:latest` container and `curl -X PUT`s the resulting + `.pkg.tar.zst` to `https://git.breadway.dev/api/packages/Breadway/arch/os`. + +A repo can be on **both** channels (most GUI/daemon apps are — see +breadbar, breadbox, breadcrumbs, bread, breadpad, breadpaper), **bakery +only** (breadclip, breadmon, breadsearch, breadshot, bread-theme, bakery +itself), **pacman only** (breadlock, breadhelp — both are OS-integration +pieces where package-manager rigor matters more than a curl-script), or +**neither** (dev-only / not yet released; no bakery.toml, no PKGBUILD, no +release or package workflow — just the repo itself, e.g. breadarr today). + +`bos` is a fourth, deliberately special case: it ships as an ISO, not a +binary, via its own `release-iso.yml`. It is never on either channel and +should never carry a `bakery.toml` or `PKGBUILD`. + +## mirror.yml is not part of this policy + +Every repo previously carried its own `.forgejo/workflows/mirror.yml` doing +a `git clone --mirror` + push to GitHub with a per-repo `MIRROR_TOKEN` +secret. That pattern is being replaced ecosystem-wide by Forgejo's native +Push Mirror feature, provisioned centrally by +`bread-ecosystem/scripts/setup-push-mirrors.sh` against the live repo list +— see that script and `scripts/cleanup-old-mirror-workflows.sh`. Once the +migration is confirmed working, no repo should have a `mirror.yml` and this +document doesn't require one. Don't add `mirror.yml` to a repo that's +missing it; that gap is intentional and about to be moot everywhere. + +## Checklist for adding a repo to a channel + +- **Bakery**: write `bakery.toml`, add a `[[products]]` entry to + `bread-ecosystem/registry/bread-ecosystem.toml`, copy a sibling's + `release.yml` (prefer one with the same shape: single binary vs. binary + + systemd service — compare against `bread/release.yml` if there's a + service to install, `breadmon/release.yml` if not) and swap the repo + name / binary name / `PKG_DIR`. +- **Pacman**: write `packaging/PKGBUILD` (or `packaging/arch/PKGBUILD`), + copy a sibling's `package.yml` and swap the repo/package name and + `system_deps`→`pacman -Syu` package list. +- Never add either file type "just in case." An unused `bakery.toml` or + `PKGBUILD` is exactly the kind of drift this document exists to prevent + (see the breadlock/breadarr/bos-settings history in the audit that + produced this doc — two of those had a stray `bakery.toml` nothing + served, one was missing the registry entry + release.yml that would have + made an existing `bakery.toml` real). + +## Current state (as of this pass) + +| Repo | bakery | pacman | notes | +|---|---|---|---| +| bread-ecosystem (bakery product) | yes | yes | `release-bakery.yml` recovered from a dead `.github/workflows/release.yml` that referenced a `hestia` self-hosted runner GitHub never had registered | +| bread-ecosystem (bread-theme product) | yes | no | | +| bread, breadbar, breadbox, breadcrumbs, breadpad, breadpaper | yes | yes | complete, used as templates | +| breadclip, breadmon, breadsearch, breadshot | yes | no | complete | +| breadlock, breadhelp | no | yes | breadlock's `bakery.toml` was removed as orphaned; its README wrongly claimed it was a registry entry | +| bos-settings | yes | yes | was missing both the registry entry and `release.yml`; both added | +| bos | no | no | ISO-only via `release-iso.yml`; had an erroneous `bakery.toml` copy-pasted from bos-settings, removed | +| breadarr | no | no | had an orphaned `bakery.toml` with no registry entry and zero workflows; removed. Not yet assigned a channel — do that deliberately when it's ready to ship, don't infer it from a stray config file | diff --git a/scripts/doctor-channels.sh b/scripts/doctor-channels.sh new file mode 100755 index 0000000..9164565 --- /dev/null +++ b/scripts/doctor-channels.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# doctor-channels.sh — detect drift between a repo's declared distribution +# channel(s) and its actual .forgejo/workflows/ + packaging metadata. +# +# See docs/release-channels.md for the policy this checks against. +# +# Usage: +# scripts/doctor-channels.sh [BASE_DIR] +# +# BASE_DIR defaults to the parent of this repo checkout (i.e. run from a +# normal ~/Projects/bread-ecosystem checkout, it scans sibling ~/Projects/* +# repos). Point it at a directory of worktrees (e.g. ~/Projects, which is +# also where *-fix-worktree checkouts live) to check those instead: +# +# scripts/doctor-channels.sh ~/Projects +# +# Exits 0 if no drift found, 1 if any repo has drift (so it's CI-friendly). +# +# Requires: python3 (tomllib, stdlib since 3.11) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BASE_DIR="${1:-$(dirname "${SCRIPT_DIR}")}" +REGISTRY="${SCRIPT_DIR}/registry/bread-ecosystem.toml" + +if [[ ! -f "${REGISTRY}" ]]; then + echo "error: registry not found at ${REGISTRY}" >&2 + exit 2 +fi + +# repo (last path segment of registry `repo = "Breadway/x"`) -> 1 +mapfile -t registry_repos < <(python3 -c " +import tomllib +with open('${REGISTRY}', 'rb') as f: + d = tomllib.load(f) +for p in d['products']: + print(p['repo'].split('/')[-1]) +") + +is_in_registry() { + local name="$1" + for r in "${registry_repos[@]}"; do + [[ "${r}" == "${name}" ]] && return 0 + done + return 1 +} + +# Repos with a deliberately non-standard packaging shape that the +# single-PKGBUILD/single-package.yml heuristic below doesn't fit. Extend +# this if another repo grows a legitimately special-cased layout. +PACKAGE_CHECK_EXEMPT=("bos") # ships an ISO via release-iso.yml; its PKGBUILDs + # under packaging/*/ build bundled AUR deps + # (bibata, calamares, ...), each with its own + # dedicated workflow — not a pacman-channel package. + +is_package_check_exempt() { + local name="$1" + for r in "${PACKAGE_CHECK_EXEMPT[@]}"; do + [[ "${r}" == "${name}" ]] && return 0 + done + return 1 +} + +drift=0 +checked=0 + +for dir in "${BASE_DIR}"/*/; do + name="$(basename "${dir}")" + name="${name%-fix-worktree}" # normalize worktree checkouts back to the repo name + [[ -d "${dir}/.git" || -f "${dir}/.git" ]] || continue + # Skip bread-ecosystem itself — it's a multi-product repo the registry + # membership check above doesn't map 1:1, and it's already reviewed by + # hand above (bakery + bread-theme products). + [[ "${name}" == "bread-ecosystem" ]] && continue + + checked=$((checked + 1)) + has_bakery_toml=0 + [[ -f "${dir}/bakery.toml" ]] && has_bakery_toml=1 + + has_release_wf=0 + compgen -G "${dir}/.forgejo/workflows/release*.yml" >/dev/null 2>&1 && has_release_wf=1 + + in_registry=0 + is_in_registry "${name}" && in_registry=1 + + has_pkgbuild=0 + find "${dir}" -maxdepth 3 -iname 'PKGBUILD' -not -path '*/.git/*' 2>/dev/null \ + | grep -q . && has_pkgbuild=1 + + has_package_wf=0 + [[ -f "${dir}/.forgejo/workflows/package.yml" ]] && has_package_wf=1 + + issues=() + + if [[ "${has_bakery_toml}" == 1 && "${in_registry}" == 0 ]]; then + issues+=("has bakery.toml but no registry/bread-ecosystem.toml entry") + fi + if [[ "${in_registry}" == 1 && "${has_bakery_toml}" == 0 ]]; then + issues+=("registered in bread-ecosystem.toml but has no bakery.toml") + fi + if [[ "${in_registry}" == 1 && "${has_release_wf}" == 0 ]]; then + issues+=("registered + has bakery.toml but no release*.yml workflow") + fi + if [[ "${has_bakery_toml}" == 1 && "${in_registry}" == 0 && "${has_release_wf}" == 1 ]]; then + issues+=("has a release workflow for a product not in the registry (index.json will never include it)") + fi + if ! is_package_check_exempt "${name}"; then + if [[ "${has_pkgbuild}" == 1 && "${has_package_wf}" == 0 ]]; then + issues+=("has a PKGBUILD but no package.yml workflow") + fi + if [[ "${has_package_wf}" == 1 && "${has_pkgbuild}" == 0 ]]; then + issues+=("has package.yml but no PKGBUILD") + fi + fi + + if [[ ${#issues[@]} -gt 0 ]]; then + drift=1 + echo "${name}:" + for i in "${issues[@]}"; do + echo " - ${i}" + done + fi +done + +echo +echo "checked ${checked} repos under ${BASE_DIR}" +if [[ "${drift}" == 0 ]]; then + echo "no channel drift found" +else + echo "drift found — see docs/release-channels.md for the policy" +fi +exit "${drift}"