chore(release): prepare v0.19.0 (#53)
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:
+55
-35
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
Executable
+362
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user