Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,82 @@ jobs:
exit 1
fi
echo "payload verified: binary, man page, three completion files"

# The Windows half of packaging verification. Exercises the Scoop manifest template,
# renders it against the last released tag's real GitHub Release .exe assets, and
# installs it through a local Scoop bucket on windows-latest.
scoop:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0

- name: Scoop manifest template is valid (offline)
shell: bash
run: python scripts/scoop_check.py --self-test && python scripts/scoop_check.py

- name: Render the Scoop manifest for the last released tag
shell: bash
run: |
set -euo pipefail
TAG=$(git describe --tags --abbrev=0 2>/dev/null || true)
if [ -z "$TAG" ]; then
echo "::error::no tags in this checkout — cannot render a Scoop manifest to install"
exit 1
fi
VER="${TAG#v}"
BASE_URL="https://github.com/l1a/retch/releases/download/${TAG}"
echo "last released tag: $TAG"
WORK="$(mktemp -d)"
curl -sfL --retry 3 -o "$WORK/retch-windows-x86_64.exe" "$BASE_URL/retch-windows-x86_64.exe" \
|| { echo "::error::cannot fetch $BASE_URL/retch-windows-x86_64.exe"; exit 1; }
curl -sfL --retry 3 -o "$WORK/retch-windows-arm64.exe" "$BASE_URL/retch-windows-arm64.exe" \
|| { echo "::error::cannot fetch $BASE_URL/retch-windows-arm64.exe"; exit 1; }
SHA_X64=$(python -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$WORK/retch-windows-x86_64.exe")
SHA_ARM=$(python -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$WORK/retch-windows-arm64.exe")
echo "computed x86_64 sha256: $SHA_X64"
echo "computed arm64 sha256: $SHA_ARM"
python scripts/render_packaging.py --target scoop --version "$VER" \
--sha256-x86-64 "$SHA_X64" --sha256-arm64 "$SHA_ARM" \
--out "$RUNNER_TEMP/retch.json"
python - "$RUNNER_TEMP/retch.json" "$VER" "$SHA_X64" "$SHA_ARM" <<'PY'
import json, pathlib, sys
path, ver, sha_x64, sha_arm = sys.argv[1:]
data = json.loads(pathlib.Path(path).read_text(encoding="utf-8"))
assert data["version"] == ver, f"version {data['version']!r} != {ver!r}"
assert data["architecture"]["64bit"]["hash"] == sha_x64
assert data["architecture"]["arm64"]["hash"] == sha_arm
assert data["architecture"]["64bit"]["url"].endswith(f"/v{ver}/retch-windows-x86_64.exe#/retch.exe")
assert data["architecture"]["arm64"]["url"].endswith(f"/v{ver}/retch-windows-arm64.exe#/retch.exe")
PY
echo "RENDERED_VER=$VER" >> "$GITHUB_ENV"
rm -rf "$WORK"
echo "render verified against the real $TAG Windows release binaries"

- name: Install via Scoop from a staged local bucket and verify binary
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) {
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
$env:PATH = "$HOME\scoop\shims;$env:PATH"
}
# Scoop resolves `<bucket>/<app>` from `~\scoop\buckets\<bucket>\bucket\<app>.json`
# (see `Find-BucketDirectory` in Scoop's `lib/buckets.ps1`), whereas `scoop bucket add`
# rejects a Windows drive-letter path in `Convert-RepositoryUri`. Staging directly
# under `~\scoop\buckets\local-test\bucket\` mirrors the `brew` job's `brew tap-new --no-git`.
$bucketDir = Join-Path $HOME "scoop\buckets\local-test\bucket"
New-Item -ItemType Directory -Force -Path $bucketDir | Out-Null
Copy-Item "$env:RUNNER_TEMP\retch.json" (Join-Path $bucketDir "retch.json")
scoop install local-test/retch
if ($LASTEXITCODE -ne 0) {
throw "scoop install local-test/retch failed with exit code $LASTEXITCODE"
}
$verOut = & retch --version
Write-Host "retch --version: $verOut"
if ($verOut -notmatch [regex]::Escape($env:RENDERED_VER)) {
throw "Expected retch --version to contain $($env:RENDERED_VER), got: $verOut"
}
& retch --fields os,cpu --no-logo
35 changes: 19 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,31 +309,34 @@ Clone `https://github.com/l1a/retch.wiki.git`, edit the relevant pages, and push

### 4.9 Packaging
- [ ] **There is no post-release packaging PR, and a release requires no version bump.**
`packaging/aur/PKGBUILD`, `packaging/copr/retch.spec` and
`packaging/homebrew/retch.rb` are **templates** carrying `@VERSION@` / `@SHA256@`;
`packaging/aur/PKGBUILD`, `packaging/copr/retch.spec`,
`packaging/homebrew/retch.rb` and `packaging/scoop/retch.json` are **templates**
carrying `@VERSION@` / `@SHA256@` (or `@SHA256_X86_64@` / `@SHA256_ARM64@`);
`scripts/render_packaging.py` fills those in at publish time, from the tag. So a
release is: tag → `just publish` → `just aur-publish <version>` →
`just brew-publish <version>` (COPR rebuilds itself off the tag), and nothing is
committed afterwards. `main` sits **at** the released version until the next feature
PR bumps it — which is what makes `just publish` on `main` correct, and its
HEAD-is-the-tag guard is what enforces that rather than trusting it.
`just brew-publish <version>` → `just scoop-publish <version>` (COPR rebuilds
itself off the tag), and nothing is committed afterwards. `main` sits **at** the
released version until the next feature PR bumps it — which is what makes
`just publish` on `main` correct, and its HEAD-is-the-tag guard is what enforces
that rather than trusting it.
- [ ] If you are touching packaging, the guards run offline in `just check`
(`render-check`, `aur-check`, `copr-check`, `brew-check`) and assert the templates
still record nothing. Do not "fix" one by writing a version back into a template.
(`render-check`, `aur-check`, `copr-check`, `brew-check`, `scoop-check`) and assert
the templates still record nothing. Do not "fix" one by writing a version back into a
template.
- [ ] `packaging/nixpkgs/package.nix` is the one target still pinned by hand, deliberately:
that channel is undecided and its CI job is `if: false`. Check whether it needs
`just nix-update` only if a nixpkgs submission is actually happening.
- [ ] **Channel text is part of publishing each channel.** `packaging/metadata.toml` holds the
one-line summary, the Homebrew `desc`, the licence, the GitHub description and topics,
and the COPR project-page files; `just check` (`metadata-check`) fails when
`Cargo.toml`, the PKGBUILD, the spec, the formula or the Nix files drift from it. Text
an artifact carries (crates.io description and README, AUR `pkgdesc`, RPM
`%description`, the tap's `desc`) ships with that channel's publish step. The COPR
project page is pushed by `copr.yml` on the tag, and GitHub's About box by
`just github-metadata` after it. **Before tagging, read the READMEs and those files
for claims that are no longer true** — the guard proves the copies agree, not that
they are right; the v0.17.9 review found a README flag that does not exist and a
crates.io example that did not compile.
`Cargo.toml`, the PKGBUILD, the spec, the formula, the Scoop manifest or the Nix files
drift from it. Text an artifact carries (crates.io description and README, AUR `pkgdesc`,
RPM `%description`, the tap's `desc`, the Scoop bucket's `description`) ships with that
channel's publish step. The COPR project page is pushed by `copr.yml` on the tag, and
GitHub's About box by `just github-metadata` after it. **Before tagging, read the
READMEs and those files for claims that are no longer true** — the guard proves the
copies agree, not that they are right; the v0.17.9 review found a README flag that does
not exist and a crates.io example that did not compile.
- [ ] History, for anyone reading an old commit: releases up to v0.9.10 committed the
packaging bump straight to `main` believing the gate made a PR impossible (it did
not); v0.9.12 turned it into a gated PR via `just post-release`; and the bump itself
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = [

[package]
name = "retch-cli"
version = "0.20.35"
version = "0.20.36"
edition = "2021"
authors = ["Ken Tobias"]
description = "A fast, feature-rich system information fetcher written in Rust (similar to fastfetch or neofetch)"
Expand Down
111 changes: 104 additions & 7 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ fmt:
lint:
cargo clippy --workspace -- -D warnings

# The renderer is the single implementation behind all three packaging targets, so its
# The renderer is the single implementation behind all four packaging targets, so its
# self-test is a `check` dependency like theirs. It is the piece that must refuse: a
# substitution matching nothing, a sentinel surviving, a placeholder checksum.
#
Expand All @@ -136,7 +136,7 @@ render-check:
@{{PY}} scripts/render_packaging.py --self-test

# Run strict checks (formatting and linting) as done in CI
check: standard-check render-check aur-check copr-check brew-check metadata-check text-check wip-check bench-check
check: standard-check render-check aur-check copr-check brew-check scoop-check metadata-check text-check wip-check bench-check
cargo fmt -- --check
cargo clippy --workspace -- -D warnings
# Also lint retch-cli WITHOUT its default `graphics` feature (base64/image/icy_sixel).
Expand Down Expand Up @@ -585,6 +585,18 @@ brew-check:
@{{PY}} scripts/brew_check.py --self-test
@{{PY}} scripts/brew_check.py

# ===== SCOOP =====
#
# packaging/scoop/retch.json is the SOURCE for the Windows Scoop bucket at l1a/scoop-retch.
# Like the AUR PKGBUILD, COPR spec and Homebrew formula, it is a template carrying
# @VERSION@, @SHA256_X86_64@ and @SHA256_ARM64@; scripts/render_packaging.py fills them in
# at publish time from the released GitHub Release .exe assets.

# Verify the Scoop manifest is still a template recording no version (offline)
scoop-check:
@{{PY}} scripts/scoop_check.py --self-test
@{{PY}} scripts/scoop_check.py

# Every channel's description and licence agrees with packaging/metadata.toml (offline)
metadata-check:
@{{PY}} scripts/metadata_check.py --self-test
Expand Down Expand Up @@ -789,6 +801,90 @@ brew-publish VERSION:
pass "pushed retch $VER to $TAP_REPO"
echo "Verify: brew tap l1a/retch && brew install retch"

#
# Takes the version for the same reason `aur-publish` and `brew-publish` do: the manifest is a
# template, and this is the moment the released version and its two binary checksums come into
# existence.
#
# Render the Scoop manifest for a released tag and push it to the bucket at l1a/scoop-retch
scoop-publish VERSION:
#!/usr/bin/env bash
set -euo pipefail
BOLD='\033[1m'; GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m'
pass() { echo -e "${GREEN}[✓]${NC} $1"; }
fail() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
info() { echo -e "${YELLOW}[→]${NC} $1"; }

BUCKET_REPO="git@github.com:l1a/scoop-retch.git"
VER="{{VERSION}}"; VER="${VER#v}"
[ -n "$VER" ] || fail "scoop-publish needs a version, e.g. just scoop-publish 0.20.28"

{{PY}} scripts/scoop_check.py || fail "packaging/scoop/retch.json is not a valid template"
pass "Scoop manifest template is valid"

# Both Windows architecture binaries are downloaded from the GitHub Release for v$VER
# and checksummed before rendering -- never copied from a committed field.
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
BASE_URL="https://github.com/l1a/retch/releases/download/v${VER}"
info "Downloading and checksumming the v$VER Windows release binaries..."
curl -sfL -o "$WORK/retch-windows-x86_64.exe" "$BASE_URL/retch-windows-x86_64.exe" \
|| fail "no x86_64 binary at $BASE_URL/retch-windows-x86_64.exe — tag and wait for the v$VER release build first"
curl -sfL -o "$WORK/retch-windows-arm64.exe" "$BASE_URL/retch-windows-arm64.exe" \
|| fail "no arm64 binary at $BASE_URL/retch-windows-arm64.exe — tag and wait for the v$VER release build first"
SHA_X64=$({{PY}} -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$WORK/retch-windows-x86_64.exe")
SHA_ARM=$({{PY}} -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$WORK/retch-windows-arm64.exe")
pass "v$VER x86_64: $(wc -c < "$WORK/retch-windows-x86_64.exe" | tr -d ' ') bytes, sha256 $SHA_X64"
pass "v$VER arm64: $(wc -c < "$WORK/retch-windows-arm64.exe" | tr -d ' ') bytes, sha256 $SHA_ARM"

{{PY}} scripts/render_packaging.py --target scoop --version "$VER" \
--sha256-x86-64 "$SHA_X64" --sha256-arm64 "$SHA_ARM" \
--out "$WORK/retch.json"
MANIFEST="$WORK/retch.json"
pass "rendered Scoop manifest for v$VER"

echo
echo -e "${BOLD}About to publish retch $VER to the Scoop bucket.${NC}"
echo "This is public and immediate: $BUCKET_REPO"
echo ""
if [ -n "${SCOOP_CONFIRM:-}" ]; then
CONFIRM="$SCOOP_CONFIRM"
echo "Type 'yes' to continue: $CONFIRM (answered by SCOOP_CONFIRM)"
elif [ -t 0 ]; then
echo -n "Type 'yes' (or 'y') to continue: "; read -r CONFIRM
else
read -r -t 10 CONFIRM || CONFIRM=""
echo "$CONFIRM"
[ -n "$CONFIRM" ] || fail "no terminal and nothing on stdin. Re-run with SCOOP_CONFIRM=yes"
fi
case "$CONFIRM" in
y|Y|yes|YES) ;;
*) echo -e "${RED}Aborted.${NC}"; exit 1 ;;
esac

info "Cloning the bucket..."
git clone -q "$BUCKET_REPO" "$WORK/bucket" || fail "could not clone $BUCKET_REPO — does the repo exist, and is the SSH key registered?"
mkdir -p "$WORK/bucket/bucket"
cp "$MANIFEST" "$WORK/bucket/bucket/retch.json"

if ! git -C "$WORK/bucket" rev-parse --verify -q HEAD >/dev/null 2>&1; then
git -C "$WORK/bucket" symbolic-ref HEAD refs/heads/main
info "empty bucket: default branch pinned to main"
fi

git -C "$WORK/bucket" config user.name "$(git -C "{{justfile_directory()}}" config user.name)"
git -C "$WORK/bucket" config user.email "$(git -C "{{justfile_directory()}}" config user.email)"

git -C "$WORK/bucket" add bucket/retch.json
if git -C "$WORK/bucket" diff --cached --quiet; then
pass "bucket already has this exact manifest — nothing to push"
exit 0
fi
git -C "$WORK/bucket" commit -q -m "retch $VER"
git -C "$WORK/bucket" push -q -u origin HEAD
pass "pushed retch $VER to $BUCKET_REPO"
echo "Verify: scoop bucket add l1a https://github.com/l1a/scoop-retch && scoop install retch"

# ===== RELEASING =====
#
# A release needs NO VERSION BUMP, and that is the point of the shape below.
Expand Down Expand Up @@ -819,16 +915,17 @@ brew-publish VERSION:
# 3. just publish # crates.io; refuses unless HEAD is the tag for X
# 4. just aur-publish X # renders + pushes to the AUR (needs podman)
# just brew-publish X # renders + pushes to the tap
# just scoop-publish X # renders + pushes to the Scoop bucket
# 5. just github-metadata # the repo's About box, from packaging/metadata.toml
#
# There is no step that edits a file, and therefore nothing to commit afterwards. Steps 3
# and 4 still ask before acting: they are public and irreversible.
#
# CHANNEL TEXT TRAVELS WITH EACH STEP. crates.io, the AUR, COPR's RPM and the tap read their
# description from the files they publish, and `just check` (metadata-check) keeps those in
# step with packaging/metadata.toml. Only the COPR project page and GitHub's About box live on
# the service rather than in a published file -- nothing ever updated them, which is how both
# went stale -- so they get steps of their own (2 and 5).
# CHANNEL TEXT TRAVELS WITH EACH STEP. crates.io, the AUR, COPR's RPM, the tap and the Scoop
# bucket read their description from the files they publish, and `just check` (metadata-check)
# keeps those in step with packaging/metadata.toml. Only the COPR project page and GitHub's
# About box live on the service rather than in a published file -- nothing ever updated them,
# which is how both went stale -- so they get steps of their own (2 and 5).

# Merge the active PR, switch to main, pull, delete the branch, and update WIP.md (requires gh)
#
Expand Down
26 changes: 24 additions & 2 deletions NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ information gathering without any dependency on `clap` or the CLI.
just publish # crates.io. Refuses unless HEAD is the tag.
just aur-publish X # renders + pushes to the AUR (needs podman; see §7.4). AUR_CONFIRM=y
just brew-publish X # renders + pushes to the tap. BREW_CONFIRM=y
just scoop-publish X # renders + pushes to the Scoop bucket. SCOOP_CONFIRM=y
just github-metadata # About box from packaging/metadata.toml
```
`retch-sysinfo` publishes before `retch-cli`, which pins `=0.1.x`; `just publish` orders them
Expand All @@ -150,9 +151,9 @@ information gathering without any dependency on `clap` or the CLI.

---

## Current State (v0.20.35)
## Current State (v0.20.36)

`main` carries **`retch-cli` 0.20.35** / **`retch-sysinfo` 0.1.100**. Newest released tag is
`main` carries **`retch-cli` 0.20.36** / **`retch-sysinfo` 0.1.100**. Newest released tag is
**`v0.20.34`**.

The fastfetch feature gap (§6) is closed on Linux, macOS and Windows (except for the
Expand All @@ -161,6 +162,27 @@ and §6c.

Recent work worth knowing about, beyond what `git log` says:

- **v0.20.36: Windows Scoop packaging (`packaging/scoop/retch.json`, `scripts/scoop_check.py`,
`just scoop-publish <version>`, and `l1a/scoop-retch` bucket).**
- **Template manifest (`packaging/scoop/retch.json`):** Follows the same zero-drift template
architecture as `packaging/aur/PKGBUILD`, `packaging/copr/retch.spec`, and
`packaging/homebrew/retch.rb`. Records `@VERSION@`, `@SHA256_X86_64@`, and `@SHA256_ARM64@`
sentinels in-repo; both `64bit` (`retch-windows-x86_64.exe`) and `arm64`
(`retch-windows-arm64.exe`) asset URLs append `#/retch.exe` so Scoop renames the downloaded
binary to `retch.exe` on install and exposes it through a single `"bin": "retch.exe"` shim.
- **Renderer & guards (`scripts/render_packaging.py`, `scripts/scoop_check.py`,
`scripts/metadata_check.py`):** Extended `render_packaging.py --target scoop` with
`--sha256-x86-64` and `--sha256-arm64`, added offline guard `scoop_check.py` (wired into
`just check` via `scoop-check`), and extended `metadata_check.py` to enforce that the
manifest's `description` and `license` match `packaging/metadata.toml`.
- **Publishing & CI (`just scoop-publish <version>`, `.github/workflows/packaging.yml`):**
`just scoop-publish <version>` downloads both `.exe` assets from the GitHub Release for
`v<version>`, computes their SHA-256 digests, renders `bucket/retch.json`, and pushes to
`git@github.com:l1a/scoop-retch.git` (gated by `SCOOP_CONFIRM`). The `scoop` job in
`packaging.yml` runs `scoop_check.py`, renders the manifest against the latest released tag's
real GitHub Release `.exe` assets on `windows-latest`, stages a local bucket, and verifies
`scoop install` + `retch --version`.

- **v0.20.35: Dynamic Linux DE/compositor/daemon `wallpaper` detection and removal of stale
cross-DE GNOME fallback.** Verified on `corrino` (Dell Inspiron 16 7630 2-in-1, Fedora 44,
Umbriel + Noctalia Shell):
Expand Down
Loading
Loading