chore(release): prepare v0.19.0 (#53)
CI / security (push) Successful in 1m11s
CI / lint (push) Successful in 21s
CI / ai-review (push) Skipped

Version 0.19.0 across version.py, the Void template and the synced docs; changelog entry; Gitea project URLs in pyproject.toml; per-backend ladder thresholds measured in #19; the xbps release workflow is manual-only, and packaging/void/publish-release.sh publishes a locally built, verified repository to the Gitea releases.
This commit was merged in pull request #53.
This commit is contained in:
2026-11-06 20:23:14 +00:00
parent 85cbc37842
commit 731b919865
25 changed files with 1408 additions and 189 deletions
+55 -35
View File
@@ -10,7 +10,8 @@ repository first.
```
packaging/void/
├── README.md this file
├── build-release.sh the release path: gate, sdist, signed repo, install smoke
├── build-release.sh the release path: gate, clean checkout, sdist, signed repo, install smoke
├── publish-release.sh publishes a built out-dir to the Gitea release API (dry run first)
├── container-release.sh the container half: build, sign, install (one per phase)
├── render-voculinux.sh pins version, _tag and checksum in the build tree only
├── smoke.sh ticket #28 gate: the release path with a throwaway key
@@ -161,7 +162,7 @@ xbps versions cannot contain `-`. The xbps spelling of a release is
| `__version__` (tag `v` + it) | xbps `version` | distfile and release asset |
|---|---|---|
| `0.18.5` | `0.18.5` | `voculinux-0.18.5.tar.gz` |
| `0.19.0` | `0.19.0` | `voculinux-0.19.0.tar.gz` |
| `0.19.0-alpha` | `0.19.0alpha` | `voculinux-0.19.0alpha.tar.gz` |
| `0.19.0-beta` | `0.19.0beta` | `voculinux-0.19.0beta.tar.gz` |
| `0.19.0-rc1` | `0.19.0rc1` | `voculinux-0.19.0rc1.tar.gz` |
@@ -186,7 +187,7 @@ refuses to continue unless exactly one `checksum=` line exists and reads back as
the computed digest, and it refuses if the committed template changed.
After a release, the digest of the published sdist is recorded in the template
by a **pull request**: the workflow log and the release notes both print
by a **pull request**: the build output and the published release notes both carry
`checksum=<sha256>`, and the pull request copies it into
`packaging/void/srcpkgs/voculinux/template`. Nothing commits to `main`.
@@ -194,9 +195,10 @@ by a **pull request**: the workflow log and the release notes both print
```sh
just void-smoke # the gate: build, sign with a throwaway key, install, smoke
just void-release v0.18.5 # a dry run of the release, with XBPS_SIGNING_KEY_FILE set
just void-release v0.19.0 # build the release set, with XBPS_SIGNING_KEY_FILE set
packaging/void/build-release.sh names v0.19.0-beta # offline: the names a tag maps to
packaging/void/build-release.sh check-tag v0.19.0-beta # offline: the tag gate
packaging/void/publish-release.sh v0.19.0 --out-dir DIR --dry-run # every check, the planned calls, no request
```
Podman only, never Docker. The base image digest and the void-packages commit
@@ -230,13 +232,17 @@ every `srcpkgs` template.
## Signed repository
`packaging/void/build-release.sh release TAG --key-file FILE --out-dir DIR`
is the whole release, and the only path that signs anything. It is the path the
tag workflow runs, and `just void-release` runs it locally.
is the whole release, and the only path that signs anything. The maintainer runs
it on the release host (see "Releasing" below); `just void-release` runs the same
command with `publish/` as the out-dir.
Fail-closed, in this order, before any container starts:
- the tag must be exactly `v` plus `__version__`, and the committed template must
name that version and tag;
- the checkout must be clean: no uncommitted or untracked change. The sdist is built
from the working tree, so anything else is not the tag. The commit it is built from
is recorded in `BUILT_FROM`;
- the key must be non-empty, valid, RSA 4096, and its public half must equal
`repository-key.plist` byte for byte (a different key never publishes);
- the canonical sdist must build with Python 3.11 and uv 0.12/0.13. Only its
@@ -256,8 +262,9 @@ The committed templates must be unchanged after the build; the run fails if they
are not. `DIR` must be absent or empty, and the run writes `DIR/sdist/`,
`DIR/xbps/repo/` (packages, signatures, `x86_64-repodata`), `DIR/xbps/keys/`
(the client `.plist` named by its fingerprint, and `FINGERPRINT`) and
`DIR/SDIST_SHA256`. A final check accepts only that set: nothing from musl, no
other architecture, every package signed, the key the pinned one.
`DIR/SDIST_SHA256` and `DIR/BUILT_FROM` (the tag and the commit it was built
from). A final check accepts only that set: nothing from musl, no other
architecture, every package signed, the key the pinned one.
### The repository key
@@ -273,8 +280,10 @@ fa:d1:00:32:ce:b1:b9:fd:73:05:05:76:75:54:37:25
`tests/test_void_signed_repo.py` derives the fingerprint from this file
and fails if either README shows a different one.
- **Private half**: never committed, never in the repository, never on a
build host's disk beyond the run. It exists as the `XBPS_SIGNING_KEY`
CI secret (PEM text, newlines intact) and in the maintainer's own backup.
build host's disk beyond the run. It is kept in the maintainer's own backup,
and the release reads it as a file on tmpfs (`/dev/shm`) that is removed
afterwards. The `XBPS_SIGNING_KEY` CI secret holds the same PEM text for the
workflow, which is manual dispatch only.
- **Generating a new key** (only if the key is ever replaced; clients then
have to re-pin, so this is a release-notes event):
@@ -292,35 +301,47 @@ openssl rsa -in xbps-sign-key.pem -pubout 2>/dev/null | openssl md5
python3 -c "import plistlib,sys; sys.stdout.buffer.write(plistlib.load(open('packaging/void/repository-key.plist','rb'))['public-key'])" | openssl md5
```
### Releasing: the Gitea workflow
### Releasing: the local sequence
A human pushes a version tag; `.gitea/workflows/xbps-release.yml` does the
rest. It never runs on a schedule or a manual dispatch.
Pushing a tag starts nothing. `.gitea/workflows/xbps-release.yml` is manual dispatch
only, kept for a future x86_64 host-mode runner; its one runner today is ARM64 and
cannot build this release. The sequence is:
1. **Tag gate.** `build-release.sh check-tag` (see above). Nothing else runs first.
2. **Key gate.** The CI secret `XBPS_SIGNING_KEY` must be non-empty. The key is
written with noclobber to a `0600` file on tmpfs (`/dev/shm`), never to disk.
3. **Runner check.** The tools in [Runner requirements](#runner-requirements)
must be present, or the job stops and names what is missing.
4. **Release.** `build-release.sh release`, with the key file (above).
5. **Publish.** The staged set goes to the tag's release and to the rolling
`latest` release, which the client registration URL serves. Assets:
`sdist/voculinux-<version>.tar.gz`, the packages, their `.sig2` signatures,
`x86_64-repodata` (last), `FINGERPRINT` and the `.plist`. Nothing is built or
uploaded by hand.
6. **Cleanup.** The key file is removed by a step that always runs.
1. **Merge the release pull request** to `main`. `origin/main` is then the commit to release.
2. **Create and push the annotated tag** on that commit:
`git tag -a vX.Y.Z <sha> -m "Voculinux X.Y.Z"`, then `git push origin refs/tags/vX.Y.Z`.
Never `git push --tags`: the clone holds upstream tags.
3. **Build in a clean checkout of the tag:** `git worktree add /tmp/voculinux-vX.Y.Z vX.Y.Z`,
then, in that worktree,
`packaging/void/build-release.sh release vX.Y.Z --key-file /dev/shm/xbps-sign-key.pem --out-dir /path/to/out`.
Remove the key file when the run ends.
4. **Dry-run the publish:** `packaging/void/publish-release.sh vX.Y.Z --out-dir /path/to/out --dry-run`.
It checks that origin's `vX.Y.Z` peels to the commit in `BUILT_FROM`, that the sdist
matches `SDIST_SHA256`, and that the client key is the committed one, then prints the
calls it would make. It sends no request and reads no token.
5. **Publish:** `packaging/void/publish-release.sh vX.Y.Z --out-dir /path/to/out`. The tag's
release gets the sdist, packages, signatures, `FINGERPRINT` and the client `.plist`, and
the rolling `latest` release that the client registration URL serves is replaced, with the
repodata last. The token comes from `GITEA_TOKEN`, or from the `tea` login for
`https://git.bongbetic.com`. It goes into a 0600 header file that is removed on exit and is
never printed.
6. **Post-release pull request:** record the `checksum=` the build printed in the template
(section 3.5 of `docs/RELEASE_PROCESS.md`).
Then the template's checksum is recorded by a pull request (above), and the
release notes show the sdist digest to copy from.
`publish-release.sh` and the workflow's publish step make the same API calls: the sdist,
packages, signatures and client key, then the repodata, to the tag's release and then to
`latest`. Any non-2xx response stops the publish. Publishing an older tag refreshes `latest`
with the older repository, so publish in version order.
Secret setup, once: Gitea repository settings, Actions, Secrets, add
`XBPS_SIGNING_KEY` with the PEM private key. The job token must be allowed to
create releases and upload release assets.
Secret setup for the workflow, once: Gitea repository settings, Actions, Secrets, add
`XBPS_SIGNING_KEY` with the PEM private key. The job token must be allowed to create
releases and upload release assets.
### Runner requirements
### Runner requirements (the release host)
The job runs on the `bongbetic-ci` label as a host job. It is not a `container:`
job, and it never uses Docker. It needs:
The release runs on the maintainer's x86_64 Void host. The workflow's `runs-on` label
(`bongbetic-ci`) is ARM64 today and cannot run it. Requirements, which a future x86_64
host-mode runner would also need. It never uses Docker:
| requirement | why | check |
|---|---|---|
@@ -333,8 +354,7 @@ job, and it never uses Docker. It needs:
| about 25 GiB free disk | the void-packages bootstrap and the whisper.cpp build | `df -h` |
| egress to `github.com`, `repo-default.voidlinux.org`, `ghcr.io`, `pypi.org`, `files.pythonhosted.org`, `git.bongbetic.com` | clone, bootstrap, base image, locked tools, distfiles, Gitea API | the build phase |
The job's `timeout-minutes` is 240. A cold build takes most of that: the
whisper.cpp Vulkan build is the long step.
A cold build is long: the whisper.cpp Vulkan build is the long step.
## Client setup
+26 -3
View File
@@ -12,9 +12,11 @@
# src/vocalinux/version.py, and the committed Void template must already
# name that version and tag. Exits 1 otherwise.
# build-release.sh release TAG --key-file FILE --out-dir DIR
# the whole release: gate, canonical sdist, signed repository,
# completeness check. Writes DIR/sdist and DIR/xbps. Publishes nothing;
# the workflow publishes DIR. DIR must be absent or empty.
# the whole release: gate, clean-checkout gate, canonical sdist, signed
# repository, completeness check. Writes DIR/sdist, DIR/xbps,
# DIR/SDIST_SHA256 and DIR/BUILT_FROM (the tag and the commit built from).
# Publishes nothing; packaging/void/publish-release.sh publishes DIR.
# DIR must be absent or empty, and the checkout must be clean.
# build-release.sh smoke
# the same pipeline for the current version, signed with a throwaway
# RSA4096 key in a temporary directory. Proves the path works; nothing is
@@ -127,6 +129,19 @@ check_template() {
fail "committed template _tag is '$have', not '$TAG'; update packaging/void/srcpkgs/voculinux/template"
}
# release_checkout: a release is built from a clean git checkout, and the commit
# it is built from is recorded in DIR/BUILT_FROM. The sdist is built from the
# working tree (tracked edits and non-ignored new files included), so anything
# but a clean tree is not the tag. Sets SOURCE_COMMIT.
release_checkout() {
git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1 ||
fail "a release is built from a git checkout of $TAG, and records its commit; $ROOT is not one"
[ -z "$(git -C "$ROOT" status --porcelain --untracked-files=normal)" ] ||
fail "the working tree has uncommitted or untracked changes; build $TAG from a clean checkout"
SOURCE_COMMIT=$(git -C "$ROOT" rev-parse --verify HEAD) ||
fail "could not read the commit HEAD"
}
# check_key KEY_FILE: RSA 4096, and for a release the public half must be the
# committed repository-key.plist key. Sets FINGERPRINT and writes the client
# key to $WORK/client.plist.
@@ -281,6 +296,9 @@ verify_set() {
run_pipeline() {
derive "$1"
check_template
if [ "$MODE" = release ]; then
release_checkout
fi
WORK=$(mktemp -d "${TMPDIR:-/tmp}/voculinux-release.XXXXXX")
cleanup() {
status=$?
@@ -317,6 +335,11 @@ run_pipeline() {
phase_install
verify_set
printf '%s\n' "$SDIST_SHA256" >"$OUT/SDIST_SHA256"
if [ "$MODE" = release ]; then
# The commit this set was built from: publish-release.sh refuses to publish
# unless origin's tag peels to it.
printf 'tag=%s\ncommit=%s\n' "$TAG" "$SOURCE_COMMIT" >"$OUT/BUILT_FROM"
fi
echo "template bump for the post-release pull request: checksum=$SDIST_SHA256"
}
+362
View File
@@ -0,0 +1,362 @@
#!/usr/bin/env bash
# Publish an already-built Void release to the fork's Gitea release API (spec #16).
#
# A release is built and verified by packaging/void/build-release.sh, locally on
# the maintainer's x86_64 Void host (packaging/void/README.md, "Releasing"). This
# script publishes that out-dir and nothing else:
#
# publish-release.sh TAG --out-dir DIR [--dry-run]
#
# Before any API request it checks that:
# 1. TAG passes the tag gate (build-release.sh check-tag): exactly v plus
# __version__, and the committed template names this release;
# 2. DIR/BUILT_FROM names TAG and the commit the set was built from;
# 3. origin's refs/tags/TAG resolves to that commit. An annotated tag is compared
# by the commit it peels to; a lightweight tag is compared directly;
# 4. the sdist matches DIR/SDIST_SHA256, the client key is the committed
# packaging/void/repository-key.plist, and the repository has its packages,
# signatures and index.
# It then publishes the staged set the way the Gitea workflow's publish step does
# (.gitea/workflows/xbps-release.yml): the tag's release first, then the rolling
# `latest` release, with the repodata uploaded last.
#
# --dry-run runs every check above, reads no token, makes no API call, and prints
# the calls it would make, in order.
#
# Token: GITEA_TOKEN when it is set. Otherwise the token of the tea login for
# GITEA_HOST in $TEA_CONFIG (default ${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml).
# The token is written to a 0600 header file in a private temporary directory,
# which is removed on exit. It is never printed.
#
# Environment: GITEA_HOST, GITEA_API (the repository's API root), REMOTE (default
# origin), TEA_CONFIG. Tests point them at a stub; a release uses the defaults.
set -euo pipefail
CDPATH=""
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
ROOT=$(cd -- "$HERE/../.." && pwd)
PLIST="$HERE/repository-key.plist"
GITEA_HOST=${GITEA_HOST:-https://git.bongbetic.com}
GITEA_API=${GITEA_API:-https://git.bongbetic.com/api/v1/repos/xavierk/voculinux}
REMOTE=${REMOTE:-origin}
TEA_CONFIG=${TEA_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml}
usage() {
echo "usage: publish-release.sh TAG --out-dir DIR [--dry-run]" >&2
exit 2
}
fail() {
echo "publish-release.sh: $*" >&2
exit 1
}
plan() {
printf ' %s\n' "$*"
}
urlencode() {
local s=$1 out="" c i
for ((i = 0; i < ${#s}; i++)); do
c=${s:i:1}
case $c in
[a-zA-Z0-9._~-]) out+=$c ;;
*) out+=$(printf '%%%02X' "'$c") ;;
esac
done
printf '%s' "$out"
}
# Reads the token of the tea login for GITEA_HOST and writes the header file
# named by argv[3]. Prints nothing else; a failure names the config, never a token.
TEA_READER=$(cat <<'PY'
import re
import sys
config, host, out = sys.argv[1:4]
try:
with open(config, encoding="utf-8") as handle:
text = handle.read()
except OSError:
sys.exit(f"no tea config at {config}")
# tea keeps a YAML list of logins; each block opens with "- name:".
for block in re.split(r"^\s*-\s+name:", text, flags=re.M)[1:]:
fields = {}
for line in block.splitlines():
match = re.match(r"\s*(url|token):\s*(.*?)\s*$", line)
if match:
fields[match.group(1)] = match.group(2).strip("'\"")
if fields.get("url", "").rstrip("/") == host and fields.get("token"):
with open(out, "w", encoding="utf-8") as handle:
handle.write("Authorization: token " + fields["token"] + "\n")
sys.exit(0)
sys.exit(f"no tea login for {host} with a token in {config}")
PY
)
TAG=""
OUT=""
DRY_RUN=0
[ "$#" -ge 1 ] || usage
TAG=$1
shift
while [ "$#" -gt 0 ]; do
case $1 in
--out-dir)
[ "$#" -ge 2 ] || usage
OUT=$2
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
*) usage ;;
esac
done
[ -n "$OUT" ] || usage
# Gate 1: the tag gate, offline. Exactly v plus __version__, and the template names it.
"$HERE/build-release.sh" check-tag "$TAG" >/dev/null ||
fail "tag $TAG fails the tag gate (build-release.sh check-tag); refusing to publish"
names=$("$HERE/build-release.sh" names "$TAG") || fail "could not derive the names of $TAG"
SDIST_NAME=$(printf '%s\n' "$names" | sed -n 's/^SDIST_NAME=//p')
# Gate 2: the out-dir is the set build-release.sh built for this tag.
[ -d "$OUT" ] || fail "$OUT is not a directory"
OUT=$(cd -- "$OUT" && pwd)
[ -f "$OUT/BUILT_FROM" ] ||
fail "$OUT has no BUILT_FROM record: it was not written by build-release.sh release"
BUILT_TAG=$(sed -n 's/^tag=//p' "$OUT/BUILT_FROM")
BUILT_COMMIT=$(sed -n 's/^commit=//p' "$OUT/BUILT_FROM")
[ "$BUILT_TAG" = "$TAG" ] || fail "$OUT was built for '$BUILT_TAG', not '$TAG'"
printf '%s\n' "$BUILT_COMMIT" | grep -Eq '^[0-9a-f]{40}$' ||
fail "BUILT_FROM in $OUT names no commit"
shopt -s nullglob
[ -f "$OUT/sdist/$SDIST_NAME" ] && [ "$(find "$OUT/sdist" -mindepth 1 -maxdepth 1 | wc -l)" -eq 1 ] ||
fail "$OUT/sdist must hold exactly $SDIST_NAME"
[ -f "$OUT/SDIST_SHA256" ] || fail "$OUT has no SDIST_SHA256"
want_digest=$(cat "$OUT/SDIST_SHA256")
got_digest=$(sha256sum "$OUT/sdist/$SDIST_NAME" | cut -d' ' -f1)
[ "$got_digest" = "$want_digest" ] || fail "sdist/$SDIST_NAME does not match SDIST_SHA256"
[ -f "$OUT/xbps/keys/FINGERPRINT" ] || fail "$OUT has no FINGERPRINT record"
FINGERPRINT=$(sed -n 's/^fingerprint: //p' "$OUT/xbps/keys/FINGERPRINT")
[ -n "$FINGERPRINT" ] || fail "FINGERPRINT in $OUT names no fingerprint"
cmp -s "$PLIST" "$OUT/xbps/keys/$FINGERPRINT.plist" ||
fail "the client key in $OUT is not the committed packaging/void/repository-key.plist"
packages=("$OUT"/xbps/repo/*.x86_64.xbps)
signatures=("$OUT"/xbps/repo/*.sig2)
index=("$OUT"/xbps/repo/*-repodata*)
[ "${#packages[@]}" -gt 0 ] || fail "$OUT holds no packages"
[ "${#signatures[@]}" -eq "${#packages[@]}" ] || fail "every package needs exactly one signature"
[ "${#index[@]}" -gt 0 ] || fail "$OUT has no repository index (x86_64-repodata)"
# Gate 3: origin's tag is the commit the set was built from. Both refs are asked
# for in one call: the tag object, and the commit it peels to (`^{}`).
remote_refs=$(git -C "$ROOT" ls-remote --tags "$REMOTE" "refs/tags/$TAG" "refs/tags/$TAG^{}") ||
fail "could not read the tags of $REMOTE"
direct=$(printf '%s\n' "$remote_refs" | awk -v ref="refs/tags/$TAG" '$2 == ref { print $1 }')
peeled=$(printf '%s\n' "$remote_refs" | awk -v ref="refs/tags/$TAG^{}" '$2 == ref { print $1 }')
[ -n "$direct" ] ||
fail "tag $TAG is not on $REMOTE; push the annotated tag first (packaging/void/README.md)"
[ "$(printf '%s\n' "$direct" | wc -l)" -eq 1 ] || fail "$REMOTE has more than one $TAG"
if [ -n "$peeled" ]; then
REMOTE_COMMIT=$peeled
else
REMOTE_COMMIT=$direct
fi
[ "$REMOTE_COMMIT" = "$BUILT_COMMIT" ] ||
fail "$REMOTE's $TAG points at $REMOTE_COMMIT, but $OUT was built from $BUILT_COMMIT; refusing to publish"
echo "gates passed: $TAG on $REMOTE points at the commit $OUT was built from ($BUILT_COMMIT)"
# The staged set, in upload order: the sdist, packages, signatures and the client
# key first; the repodata last, so no client sees an index for packages not yet up.
cd "$OUT"
FILES=(
sdist/*.tar.gz
xbps/repo/*.x86_64.xbps
xbps/repo/*.sig2
xbps/keys/FINGERPRINT
xbps/keys/*.plist
xbps/repo/*-repodata*
)
NAMES=$(for f in "${FILES[@]}"; do basename "$f"; done)
SDIST_DIGEST=$want_digest
NOTES=$(printf '%s\n\n%s\n%s\n' \
"Signed xbps repository, Void Linux glibc x86_64, built from ${TAG} by packaging/void/build-release.sh and published by packaging/void/publish-release.sh. This is a third-party repository published by this project, not by upstream Void Linux. Registration: README.md." \
"$(cat xbps/keys/FINGERPRINT)" \
"sdist sha256: ${SDIST_DIGEST} (voculinux-*.tar.gz, the source the packages are built from)")
# Every request goes through api(), which is the only place curl runs. In a dry
# run each helper prints its planned call instead and no request is made.
api() {
local method=$1 url=$2 code
shift 2
code=$(curl -sS -o "$BODY" -w '%{http_code}' -X "$method" -H "@$AUTH" "$@" "$url") ||
fail "request failed: $method $url"
case $code in
2[0-9][0-9]) ;;
*)
cat "$BODY" >&2
echo >&2
fail "$method $url returned HTTP $code"
;;
esac
}
json_field() {
python3 -I -c 'import json,sys; print(json.load(open(sys.argv[1]))[sys.argv[2]])' "$BODY" "$1"
}
release_payload() {
python3 -I -c 'import json,sys; print(json.dumps({"tag_name": sys.argv[1], "target_commitish": sys.argv[2], "name": sys.argv[3], "body": sys.argv[4], "draft": False, "prerelease": False}))' "$@"
}
# release_id TAG: sets REL_ID to the release's id, or to "" when the tag has no
# release. Sets a variable rather than printing: a failure inside $(...) in an
# argument position would otherwise be swallowed.
release_id() {
local code
REL_ID=""
code=$(curl -sS -o "$BODY" -w '%{http_code}' -H "@$AUTH" \
"$GITEA_API/releases/tags/$(urlencode "$1")") || fail "request failed: GET releases/tags/$1"
case $code in
200) REL_ID=$(json_field id) ;;
404) ;;
*)
cat "$BODY" >&2
echo >&2
fail "GET releases/tags/$1 returned HTTP $code"
;;
esac
}
# ensure_release TAG NAME NOTES: sets REL_ID, creating the release at the built
# commit when the tag has none.
ensure_release() {
local payload
if [ "$DRY_RUN" -eq 1 ]; then
plan "GET $GITEA_API/releases/tags/$1"
plan "POST $GITEA_API/releases only when $1 has no release: tag_name=$1 target_commitish=$BUILT_COMMIT"
REL_ID="<$1 release id>"
return 0
fi
release_id "$1"
if [ -z "$REL_ID" ]; then
payload=$(release_payload "$1" "$BUILT_COMMIT" "$2" "$3")
api POST "$GITEA_API/releases" -H 'Content-Type: application/json' -d "$payload"
REL_ID=$(json_field id)
fi
}
# delete_assets ID MODE: MODE is "all", or "same" for only the assets whose names
# this run uploads again (a rerun on a tag release leaves nothing stale behind).
delete_assets() {
local listing aid aname
if [ "$DRY_RUN" -eq 1 ]; then
plan "GET $GITEA_API/releases/$1/assets"
if [ "$2" = all ]; then
plan "DELETE $GITEA_API/releases/$1/assets/<asset id> for every asset"
else
plan "DELETE $GITEA_API/releases/$1/assets/<asset id> for each asset this set uploads again"
fi
return 0
fi
api GET "$GITEA_API/releases/$1/assets"
listing=$(python3 -I -c 'import json,sys; [print(a["id"], a["name"]) for a in json.load(open(sys.argv[1]))]' "$BODY")
[ -n "$listing" ] || return 0
while read -r aid aname; do
if [ "$2" = all ] || printf '%s\n' "$NAMES" | grep -qxF -- "$aname"; then
api DELETE "$GITEA_API/releases/$1/assets/$aid"
echo "deleted: $aname"
fi
done <<<"$listing"
}
upload_assets() {
local f name
for f in "${FILES[@]}"; do
name=$(basename "$f")
if [ "$DRY_RUN" -eq 1 ]; then
plan "POST $GITEA_API/releases/$1/assets?name=$(urlencode "$name") attachment=$f"
continue
fi
api POST "$GITEA_API/releases/$1/assets?name=$(urlencode "$name")" \
-F "attachment=@${f};filename=${name}"
echo "uploaded: $name"
done
}
# verify_assets ID: every file this run uploaded is on the release.
verify_assets() {
local have missing
if [ "$DRY_RUN" -eq 1 ]; then
plan "GET $GITEA_API/releases/$1/assets (every uploaded name must be listed)"
return 0
fi
api GET "$GITEA_API/releases/$1/assets"
have=$(python3 -I -c 'import json,sys; [print(a["name"]) for a in json.load(open(sys.argv[1]))]' "$BODY" | sort)
missing=$(printf '%s\n' "$NAMES" | sort | comm -23 - <(printf '%s\n' "$have"))
[ -z "$missing" ] || fail "release $1 is missing uploaded assets: $missing"
}
if [ "$DRY_RUN" -eq 1 ]; then
echo "dry run: no request is sent and no token is read. Planned calls, in order:"
echo " Authorization: token <redacted>, API $GITEA_API"
echo "tag release $TAG (commit $BUILT_COMMIT):"
REL_ID=""
BODY=""
ensure_release "$TAG" "$TAG" "$NOTES"
TAG_ID=$REL_ID
delete_assets "$TAG_ID" same
upload_assets "$TAG_ID"
verify_assets "$TAG_ID"
echo "latest release (rolling, the registration URL serves it):"
ensure_release latest latest "$NOTES"
LATEST_ID=$REL_ID
plan "PATCH $GITEA_API/releases/$LATEST_ID name=latest, body=the same notes, with ${TAG} named"
delete_assets "$LATEST_ID" all
upload_assets "$LATEST_ID"
verify_assets "$LATEST_ID"
echo "dry run complete: nothing was published"
exit 0
fi
# Token: GITEA_TOKEN first (the workflow's job token), then the tea login.
AUTH_DIR=$(mktemp -d "${TMPDIR:-/tmp}/publish-release.XXXXXX")
trap 'rm -rf "$AUTH_DIR"' EXIT
AUTH="$AUTH_DIR/authorization.h"
BODY="$AUTH_DIR/body.json"
umask 077
if [ -n "${GITEA_TOKEN:-}" ]; then
printf 'Authorization: token %s\n' "$GITEA_TOKEN" >"$AUTH"
else
python3 -I -c "$TEA_READER" "$TEA_CONFIG" "$GITEA_HOST" "$AUTH" ||
fail "no tea login for $GITEA_HOST with a token (set GITEA_TOKEN, or log in with tea)"
fi
echo "publishing ${TAG} (built from ${BUILT_COMMIT}) to ${GITEA_API}"
echo "== tag release ${TAG}"
ensure_release "$TAG" "$TAG" "$NOTES"
TAG_ID=$REL_ID
delete_assets "$TAG_ID" same
upload_assets "$TAG_ID"
verify_assets "$TAG_ID"
# The rolling release the registration URL points at. The tag it was created
# with stays where it was first made; its assets are what clients download.
echo "== latest release"
LATEST_NOTES=$(printf '%s\n\n%s\n' \
"Rolling signed xbps repository, currently built from ${TAG} (${BUILT_COMMIT})." \
"$NOTES")
ensure_release latest latest "$LATEST_NOTES"
LATEST_ID=$REL_ID
PATCH_PAYLOAD=$(python3 -I -c 'import json,sys; print(json.dumps({"name": sys.argv[1], "body": sys.argv[2]}))' latest "$LATEST_NOTES")
api PATCH "$GITEA_API/releases/$LATEST_ID" -H 'Content-Type: application/json' -d "$PATCH_PAYLOAD"
delete_assets "$LATEST_ID" all
upload_assets "$LATEST_ID"
verify_assets "$LATEST_ID"
echo "OK: signed repository published to ${TAG} and latest"
+2 -2
View File
@@ -1,11 +1,11 @@
# Template file for 'voculinux'
pkgname=voculinux
version=0.18.5
version=0.19.0
revision=1
# The release tag this template names: v + __version__, with the pre-release
# hyphen kept (the tag is v0.19.0-beta while version is 0.19.0beta). The release
# gate (packaging/void/build-release.sh check-tag) refuses any mismatch.
_tag=v0.18.5
_tag=v0.19.0
# The release sdist ships no setup.py (setuptools pyproject-only), so the
# build goes through the PEP 517 wheel (cf. Solaar, PhotoCollage).
build_style=python3-pep517