feat(packaging): add verified Omarchy AUR path

Render codewhale-bin PKGBUILD and .SRCINFO metadata from the workspace version and checksum-verified x64/arm64 release archives. Preserve the Arch runtime dependency contract, reject placeholders and tampered assets, and validate package contents deterministically.\n\nWire the renderer into CI and release-candidate smoke without publishing to AUR, and document Omarchy install/update plus the separately authorized release handoff.
This commit is contained in:
CodeWhale Bot
2026-08-23 23:07:39 -07:00
parent 4baa8b5214
commit d3d349deb6
9 changed files with 470 additions and 1 deletions

View File

@@ -124,7 +124,7 @@ jobs:
scripts/release/npm-wrapper-smoke.js|scripts/mobile-smoke.sh|scripts/check-provider-registry.py)
heavy=true
;;
docs/*|*.md|.github/PULL_REQUEST_TEMPLATE.md|.github/ISSUE_TEMPLATE/*|.github/scripts/agent-task-metadata.test.sh|.github/workflows/agent-task-labels.yml|.github/workflows/auto-tag.yml|.github/workflows/stale.yml|.github/workflows/triage.yml|scripts/release/check-versions.sh|scripts/release/check-ohos-deps.sh|scripts/release/install-dogfood.sh|scripts/release/install-dogfood.test.sh|scripts/release/prepare-release.sh|scripts/release/prepare-release.test.sh|scripts/check-coauthor-trailers.py|scripts/dev-cache.sh|scripts/dev-cache.test.sh|scripts/dev-cargo.sh|scripts/dev-test.sh)
docs/*|*.md|packaging/aur/*|.github/PULL_REQUEST_TEMPLATE.md|.github/ISSUE_TEMPLATE/*|.github/scripts/agent-task-metadata.test.sh|.github/workflows/agent-task-labels.yml|.github/workflows/auto-tag.yml|.github/workflows/stale.yml|.github/workflows/triage.yml|scripts/release/check-versions.sh|scripts/release/check-ohos-deps.sh|scripts/release/install-dogfood.sh|scripts/release/install-dogfood.test.sh|scripts/release/prepare-release.sh|scripts/release/prepare-release.test.sh|scripts/check-coauthor-trailers.py|scripts/dev-cache.sh|scripts/dev-cache.test.sh|scripts/dev-cargo.sh|scripts/dev-test.sh)
;;
*)
heavy=true
@@ -190,6 +190,7 @@ jobs:
bash scripts/release/require-release-tag-checkout.test.sh
bash scripts/release/validate-crate-publish-order.test.sh
bash scripts/release/verify-remote-tag.test.sh
bash packaging/aur/render.test.sh
sh scripts/dev-cache.test.sh
bash .github/scripts/update-homebrew-tap.test.sh
node .github/scripts/release-workflows.test.js

View File

@@ -402,6 +402,8 @@ jobs:
run: |
node --test scripts/release/assemble-release-assets.test.js
node --test npm/codewhale/test/artifacts.test.js npm/codewhale/test/release-assets.test.js
- name: Render AUR metadata from candidate Linux archives
run: bash packaging/aur/render.sh release-assets "${RUNNER_TEMP}/codewhale-bin"
- name: Smoke packed npm wrapper against candidate assets
env:
CODEWHALE_SMOKE_ASSETS_DIR: ${{ github.workspace }}/release-assets

View File

@@ -422,6 +422,27 @@ Install into a NixOS module:
---
## Omarchy / AUR
On Omarchy, install the prebuilt AUR package:
```bash
omarchy pkg aur add codewhale-bin
codewhale --version
```
`codewhale-bin` packages the same checksum-pinned Linux release archives as the
other binary install paths and provides both `codewhale` and `codew`. It does
not carry a separate Codewhale version. Package updates arrive through
`omarchy update`; the in-app updater leaves the pacman-owned binary to Omarchy.
The AUR update follows the matching Codewhale tag and release assets, so it may
appear after the GitHub release while its generated `PKGBUILD` and `.SRCINFO`
are validated. Release-maintainer instructions live in
[`packaging/aur/README.md`](../packaging/aur/README.md).
---
## Homebrew
The formula is `codewhale`. The tap GitHub repo is still

View File

@@ -323,6 +323,27 @@ manifest omits a required row; or the assets predate the matching release
workflow run. If the command fails, rerun or repair `release.yml`; do not
publish Cargo or npm against stale assets.
## AUR / Omarchy Package
`codewhale-bin` is a downstream package of the same Linux release, not a new
Codewhale semantic version. After the public asset gate above passes, render
its AUR metadata from the verified release directory:
```bash
./packaging/aur/render.sh /path/to/release-assets /tmp/codewhale-bin
```
The renderer reads the workspace version and extracts the x64/arm64 archive
hashes only after both release checksum manifests agree with the actual files.
It emits no `SKIP` checksums or source-controlled per-release values. Follow
[`packaging/aur/README.md`](../packaging/aur/README.md) for the clean Arch build,
`.SRCINFO` comparison, and package-content checks.
The GitHub release workflows only verify that the AUR metadata can be rendered
from their candidate assets. They do not publish to AUR. AUR publication is a
separate, explicitly authorized maintainer action after the matching tag and
assets are public.
## npm Wrapper Release
`release.yml` publishes `codewhale` through npm Trusted Publishing after the

View File

@@ -0,0 +1,40 @@
# shellcheck shell=bash
# shellcheck disable=SC2034,SC2154
# makepkg consumes the metadata variables and defines CARCH/srcdir/pkgdir.
pkgname=codewhale-bin
pkgver=@PKGVER@
pkgrel=@PKGREL@
pkgdesc='Provider-neutral coding agent for the terminal (prebuilt binary)'
arch=('x86_64' 'aarch64')
url='https://codewhale.net'
license=('MIT')
depends=('glibc' 'gcc-libs' 'dbus')
provides=("codewhale=${pkgver}" 'codewhale-tui' 'deepseek' 'deepseek-tui')
conflicts=('codewhale' 'codewhale-tui' 'deepseek' 'deepseek-tui' 'deepseek-tui-bin')
replaces=('deepseek-tui-bin')
options=('!strip')
_release_url="https://github.com/Hmbown/CodeWhale/releases/download/v${pkgver}"
source=("LICENSE-${pkgver}::https://raw.githubusercontent.com/Hmbown/CodeWhale/v${pkgver}/LICENSE")
sha256sums=('@LICENSE_SHA256@')
source_x86_64=("${pkgname}-${pkgver}-x86_64.tar.gz::${_release_url}/codewhale-linux-x64.tar.gz")
sha256sums_x86_64=('@X86_64_SHA256@')
source_aarch64=("${pkgname}-${pkgver}-aarch64.tar.gz::${_release_url}/codewhale-linux-arm64.tar.gz")
sha256sums_aarch64=('@AARCH64_SHA256@')
package() {
local release_arch
case "${CARCH}" in
x86_64) release_arch='x64' ;;
aarch64) release_arch='arm64' ;;
*) printf 'unsupported architecture: %s\n' "${CARCH}" >&2; return 1 ;;
esac
install -Dm755 "${srcdir}/codewhale-linux-${release_arch}/codewhale" \
"${pkgdir}/usr/bin/codewhale"
install -Dm755 "${srcdir}/codewhale-linux-${release_arch}/codew" \
"${pkgdir}/usr/bin/codew"
install -Dm644 "${srcdir}/LICENSE-${pkgver}" \
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}

73
packaging/aur/README.md Normal file
View File

@@ -0,0 +1,73 @@
# AUR packaging for Codewhale
`codewhale-bin` installs the prebuilt `codewhale` runtime and its `codew`
convenience command from the same Linux archives published for each Codewhale
release. It does not compile a fork or carry a separate Codewhale version.
The AUR repository is a separate publication destination. This directory is
the upstream source of its `PKGBUILD` and `.SRCINFO`, but the files with those
final names are generated only after the matching release archives and
checksum manifests exist. Nothing here publishes to AUR.
## Render a release update
From a checkout whose `Cargo.toml` has the released `X.Y.Z` workspace version,
obtain the complete, verified `codewhale-release-assets` directory and run:
```bash
./packaging/aur/render.sh /path/to/release-assets /tmp/codewhale-bin
```
The renderer:
- reads `pkgver` from `[workspace.package]` rather than accepting a second
product version;
- requires the x64 and arm64 Linux archives under their canonical release
names;
- requires both release checksum manifests to agree with each other and with
the archive bytes;
- inserts real SHA-256 values for both archives and the tagged MIT license;
- rejects unresolved placeholders, `SKIP`, malformed archives, and non-empty
output directories; and
- compares the rendered `.SRCINFO` with `makepkg --printsrcinfo` when `makepkg`
is available.
The initial AUR revision for each upstream release is `pkgrel=1`. A recipe-only
correction may use a higher Arch package revision without changing Codewhale's
semantic version:
```bash
./packaging/aur/render.sh /path/to/release-assets /tmp/codewhale-bin 2
```
Do not invent checksums before release artifacts exist, use `SKIP`, or copy
hashes from an older release.
## Validate on Arch or Omarchy
Review the generated files, then validate them in an unprivileged clean build
environment:
```bash
cd /tmp/codewhale-bin
makepkg --verifysource
makepkg --printsrcinfo | cmp - .SRCINFO
makepkg --cleanbuild
namcap PKGBUILD codewhale-bin-*.pkg.tar.zst
```
Inspect the package contents before installation. They should contain
`/usr/bin/codewhale`, `/usr/bin/codew`, and the MIT license, with no legacy
third command. After a test install, verify both entrypoints report `X.Y.Z`.
Publishing the generated files to the `codewhale-bin` AUR repository requires
separate authorization from an AUR maintainer. Regenerate `.SRCINFO` whenever
package metadata changes, and never publish this recipe before the matching
Codewhale tag, archives, and checksum manifests are public and verified.
Arch's `PKGBUILD(5)` contract defines architecture-specific sources and
checksums, and the AUR requires `.SRCINFO` to accompany metadata changes:
- <https://man.archlinux.org/man/PKGBUILD.5.en>
- <https://wiki.archlinux.org/title/.SRCINFO>
- <https://wiki.archlinux.org/title/AUR_submission_guidelines>

View File

@@ -0,0 +1,30 @@
pkgbase = codewhale-bin
pkgdesc = Provider-neutral coding agent for the terminal (prebuilt binary)
pkgver = @PKGVER@
pkgrel = @PKGREL@
url = https://codewhale.net
arch = x86_64
arch = aarch64
license = MIT
depends = glibc
depends = gcc-libs
depends = dbus
provides = codewhale=@PKGVER@
provides = codewhale-tui
provides = deepseek
provides = deepseek-tui
conflicts = codewhale
conflicts = codewhale-tui
conflicts = deepseek
conflicts = deepseek-tui
conflicts = deepseek-tui-bin
replaces = deepseek-tui-bin
options = !strip
source = LICENSE-@PKGVER@::https://raw.githubusercontent.com/Hmbown/CodeWhale/v@PKGVER@/LICENSE
sha256sums = @LICENSE_SHA256@
source_x86_64 = codewhale-bin-@PKGVER@-x86_64.tar.gz::https://github.com/Hmbown/CodeWhale/releases/download/v@PKGVER@/codewhale-linux-x64.tar.gz
sha256sums_x86_64 = @X86_64_SHA256@
source_aarch64 = codewhale-bin-@PKGVER@-aarch64.tar.gz::https://github.com/Hmbown/CodeWhale/releases/download/v@PKGVER@/codewhale-linux-arm64.tar.gz
sha256sums_aarch64 = @AARCH64_SHA256@
pkgname = codewhale-bin

166
packaging/aur/render.sh Executable file
View File

@@ -0,0 +1,166 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "usage: $0 RELEASE_ASSETS_DIR OUTPUT_DIR [PKGREL]" >&2
}
if [[ $# -lt 2 || $# -gt 3 ]]; then
usage
exit 2
fi
assets_dir="$1"
output_dir="$2"
pkgrel="${3:-1}"
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
if [[ ! -d "${assets_dir}" ]]; then
echo "release assets directory does not exist: ${assets_dir}" >&2
exit 1
fi
assets_dir="$(cd "${assets_dir}" && pwd)"
if [[ -e "${output_dir}" && ! -d "${output_dir}" ]]; then
echo "output path is not a directory: ${output_dir}" >&2
exit 1
fi
mkdir -p "${output_dir}"
if [[ -n "$(find "${output_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
echo "output directory must be empty: ${output_dir}" >&2
exit 1
fi
output_dir="$(cd "${output_dir}" && pwd)"
workspace_version="$(
grep -E '^version = "' "${repo_root}/Cargo.toml" \
| head -n 1 \
| sed -E 's/^version = "([^"]+)".*/\1/'
)"
if [[ ! "${workspace_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "workspace version must be X.Y.Z, got: ${workspace_version:-<missing>}" >&2
exit 1
fi
if [[ ! "${pkgrel}" =~ ^[1-9][0-9]*(\.[1-9][0-9]*)?$ ]]; then
echo "PKGREL must be a positive integer or positive x.y value, got: ${pkgrel}" >&2
exit 2
fi
artifact_manifest="${assets_dir}/codewhale-artifacts-sha256.txt"
bundle_manifest="${assets_dir}/codewhale-bundles-sha256.txt"
for manifest in "${artifact_manifest}" "${bundle_manifest}"; do
if [[ ! -f "${manifest}" ]]; then
echo "release assets are missing checksum manifest: ${manifest}" >&2
exit 1
fi
done
sha256_file() {
local path="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${path}" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "${path}" | awk '{print $1}'
else
echo "sha256sum or shasum is required" >&2
return 1
fi
}
manifest_sha() {
local manifest="$1"
local asset="$2"
local matches match_count checksum
matches="$(awk -v asset="${asset}" '$2 == asset { print $1 }' "${manifest}")"
match_count="$(printf '%s\n' "${matches}" | awk 'NF { count++ } END { print count + 0 }')"
if [[ "${match_count}" -ne 1 ]]; then
echo "$(basename "${manifest}") must contain exactly one checksum for ${asset}" >&2
return 1
fi
checksum="$(printf '%s\n' "${matches}" | awk 'NF { print; exit }')"
if [[ ! "${checksum}" =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "invalid checksum for ${asset} in $(basename "${manifest}"): ${checksum}" >&2
return 1
fi
printf '%s' "${checksum}" | tr 'A-F' 'a-f'
}
verified_archive_sha() {
local asset="$1"
local archive="${assets_dir}/${asset}"
if [[ ! -f "${archive}" ]]; then
echo "release assets are missing ${asset}" >&2
return 1
fi
local artifact_sha bundle_sha actual_sha
artifact_sha="$(manifest_sha "${artifact_manifest}" "${asset}")"
bundle_sha="$(manifest_sha "${bundle_manifest}" "${asset}")"
if [[ "${artifact_sha}" != "${bundle_sha}" ]]; then
echo "checksum manifests disagree for ${asset}" >&2
return 1
fi
actual_sha="$(sha256_file "${archive}")"
if [[ "${artifact_sha}" != "${actual_sha}" ]]; then
echo "release asset checksum mismatch for ${asset}" >&2
return 1
fi
local release_arch="${asset#codewhale-linux-}"
release_arch="${release_arch%.tar.gz}"
local listing
listing="$(tar -tzf "${archive}")"
for entry in \
"codewhale-linux-${release_arch}/codewhale" \
"codewhale-linux-${release_arch}/codew"; do
if ! grep -Fqx "${entry}" <<<"${listing}"; then
echo "${asset} is missing required archive entry: ${entry}" >&2
return 1
fi
done
printf '%s' "${actual_sha}"
}
x86_64_sha="$(verified_archive_sha 'codewhale-linux-x64.tar.gz')"
aarch64_sha="$(verified_archive_sha 'codewhale-linux-arm64.tar.gz')"
license_sha="$(sha256_file "${repo_root}/LICENSE")"
render_template() {
local source="$1"
local destination="$2"
local content
content="$(<"${source}")"
content="${content//@PKGVER@/${workspace_version}}"
content="${content//@PKGREL@/${pkgrel}}"
content="${content//@LICENSE_SHA256@/${license_sha}}"
content="${content//@X86_64_SHA256@/${x86_64_sha}}"
content="${content//@AARCH64_SHA256@/${aarch64_sha}}"
printf '%s\n' "${content}" > "${destination}"
}
render_template "${repo_root}/packaging/aur/PKGBUILD.template" "${output_dir}/PKGBUILD"
render_template "${repo_root}/packaging/aur/SRCINFO.template" "${output_dir}/.SRCINFO"
for rendered in "${output_dir}/PKGBUILD" "${output_dir}/.SRCINFO"; do
if grep -Eq '@(PKGVER|PKGREL|LICENSE_SHA256|X86_64_SHA256|AARCH64_SHA256)@|SKIP' "${rendered}"; then
echo "rendered AUR metadata contains an unresolved or insecure checksum token: ${rendered}" >&2
exit 1
fi
done
bash -n "${output_dir}/PKGBUILD"
if command -v makepkg >/dev/null 2>&1; then
generated_srcinfo="$(mktemp)"
trap 'rm -f "${generated_srcinfo}"' EXIT
(cd "${output_dir}" && makepkg --printsrcinfo) > "${generated_srcinfo}"
if ! cmp -s "${generated_srcinfo}" "${output_dir}/.SRCINFO"; then
echo "rendered .SRCINFO does not match makepkg --printsrcinfo" >&2
diff -u "${output_dir}/.SRCINFO" "${generated_srcinfo}" >&2 || true
exit 1
fi
fi
echo "Rendered codewhale-bin ${workspace_version}-${pkgrel} from verified release archives:"
echo " ${output_dir}/PKGBUILD"
echo " ${output_dir}/.SRCINFO"

115
packaging/aur/render.test.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env bash
# shellcheck disable=SC1091,SC2034,SC2329
# The test sources a generated PKGBUILD whose makepkg variables are dynamic.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
temp_root="$(mktemp -d)"
trap 'rm -rf "${temp_root}"' EXIT
assets_dir="${temp_root}/release-assets"
stage_dir="${temp_root}/stage"
mkdir -p "${assets_dir}" "${stage_dir}"
sha256_file() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}
for release_arch in x64 arm64; do
archive_root="codewhale-linux-${release_arch}"
mkdir -p "${stage_dir}/${archive_root}"
printf '#!/usr/bin/env sh\necho codewhale-%s\n' "${release_arch}" \
> "${stage_dir}/${archive_root}/codewhale"
printf '#!/usr/bin/env sh\necho codew-%s\n' "${release_arch}" \
> "${stage_dir}/${archive_root}/codew"
chmod 0755 \
"${stage_dir}/${archive_root}/codewhale" \
"${stage_dir}/${archive_root}/codew"
COPYFILE_DISABLE=1 tar -czf "${assets_dir}/${archive_root}.tar.gz" \
-C "${stage_dir}" "${archive_root}"
done
for manifest in codewhale-artifacts-sha256.txt codewhale-bundles-sha256.txt; do
{
printf '%s %s\n' \
"$(sha256_file "${assets_dir}/codewhale-linux-x64.tar.gz")" \
'codewhale-linux-x64.tar.gz'
printf '%s %s\n' \
"$(sha256_file "${assets_dir}/codewhale-linux-arm64.tar.gz")" \
'codewhale-linux-arm64.tar.gz'
} > "${assets_dir}/${manifest}"
done
first_output="${temp_root}/first"
second_output="${temp_root}/second"
revision_output="${temp_root}/revision"
bash "${repo_root}/packaging/aur/render.sh" "${assets_dir}" "${first_output}"
bash "${repo_root}/packaging/aur/render.sh" "${assets_dir}" "${second_output}"
bash "${repo_root}/packaging/aur/render.sh" "${assets_dir}" "${revision_output}" 2
cmp "${first_output}/PKGBUILD" "${second_output}/PKGBUILD"
cmp "${first_output}/.SRCINFO" "${second_output}/.SRCINFO"
workspace_version="$(
grep -E '^version = "' "${repo_root}/Cargo.toml" \
| head -n 1 \
| sed -E 's/^version = "([^"]+)".*/\1/'
)"
grep -Fqx "pkgver=${workspace_version}" "${first_output}/PKGBUILD"
grep -Fqx "depends=('glibc' 'gcc-libs' 'dbus')" "${first_output}/PKGBUILD"
for dependency in glibc gcc-libs dbus; do
grep -Fqx $'\tdepends = '"${dependency}" "${first_output}/.SRCINFO"
done
grep -Fqx 'pkgrel=2' "${revision_output}/PKGBUILD"
grep -Fqx $'\tpkgrel = 2' "${revision_output}/.SRCINFO"
grep -Fq "/releases/download/v${workspace_version}/codewhale-linux-x64.tar.gz" \
"${first_output}/.SRCINFO"
grep -Fq "/releases/download/v${workspace_version}/codewhale-linux-arm64.tar.gz" \
"${first_output}/.SRCINFO"
if grep -R -Eq 'SKIP|@[A-Z0-9_]+@' "${first_output}"; then
echo "rendered metadata retained a placeholder or SKIP checksum" >&2
exit 1
fi
for arch_case in 'x86_64:x64' 'aarch64:arm64'; do
carch="${arch_case%%:*}"
release_arch="${arch_case#*:}"
package_src="${temp_root}/package-src-${carch}"
package_root="${temp_root}/package-root-${carch}"
mkdir -p "${package_src}" "${package_root}"
tar -xzf "${assets_dir}/codewhale-linux-${release_arch}.tar.gz" -C "${package_src}"
cp "${repo_root}/LICENSE" "${package_src}/LICENSE-${workspace_version}"
(
source "${first_output}/PKGBUILD"
install() {
local mode="${1#-Dm}"
command mkdir -p "$(dirname "$3")"
command cp "$2" "$3"
command chmod "${mode}" "$3"
}
CARCH="${carch}"
srcdir="${package_src}"
pkgdir="${package_root}"
package
)
cmp \
"${stage_dir}/codewhale-linux-${release_arch}/codewhale" \
"${package_root}/usr/bin/codewhale"
cmp \
"${stage_dir}/codewhale-linux-${release_arch}/codew" \
"${package_root}/usr/bin/codew"
cmp "${repo_root}/LICENSE" \
"${package_root}/usr/share/licenses/codewhale-bin/LICENSE"
done
printf 'tampered\n' >> "${assets_dir}/codewhale-linux-x64.tar.gz"
if bash "${repo_root}/packaging/aur/render.sh" \
"${assets_dir}" "${temp_root}/tampered-output" >/dev/null 2>&1; then
echo "renderer accepted a release archive that no longer matched its manifests" >&2
exit 1
fi
echo "AUR render contract passed"