Move bakery's own release workflow from .github to .forgejo

.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.
This commit is contained in:
Breadway 2026-07-17 14:06:12 +08:00
parent 6ae7edb83c
commit 025e27b496
4 changed files with 301 additions and 85 deletions

View file

@ -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
echo "signed bakery-x86_64"
else
echo "::warning::BAKERY_MINISIGN_SEC_KEY_PATH not set — shipping bakery-x86_64 UNSIGNED"
fi
- name: regenerate index.json
env:
MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }}
run: cd src && bash scripts/gen-index.sh
- name: upload to GitHub Release
env:
GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
PKG_DIR="/srv/breadway-dl/bakery/${VERSION}"
gh release create "${GITHUB_REF_NAME}" --repo Breadway/bread-ecosystem \
--title "bakery ${GITHUB_REF_NAME}" --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}" --repo Breadway/bread-ecosystem ${ASSETS} --clobber

View file

@ -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
echo "signed bakery-x86_64"
else
echo "::warning::BAKERY_MINISIGN_SEC_KEY secret not set — shipping bakery-x86_64 UNSIGNED"
fi
- name: regenerate index.json
env:
MINISIGN_SEC_KEY_CONTENTS: ${{ secrets.BAKERY_MINISIGN_SEC_KEY }}
run: |
if [ -n "${MINISIGN_SEC_KEY_CONTENTS}" ]; then
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_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

94
docs/release-channels.md Normal file
View file

@ -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 <name>`, `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/<name>/`,
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 <name>` 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 |

133
scripts/doctor-channels.sh Executable file
View file

@ -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}"