Skip to content
Draft
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
102 changes: 102 additions & 0 deletions .github/scripts/publish/freebsd-prepare.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Substitute the release version into the FreeBSD port's Makefile and
# regenerate its distinfo (per-arch SHA256 + SIZE) from the release's
# published assets — the `.sha256` companion gives the digest and the
# release asset metadata gives the byte size. This avoids running
# `make makesum` on a host that has no ports tree (and no need to download
# the multi-MB tarballs just to size them).
#
# Usage: freebsd-prepare.sh <version-without-leading-v>
# Requires: GH_TOKEN, GITHUB_REPOSITORY (provided by Actions).
set -euo pipefail

version="${1:?usage: freebsd-prepare.sh <version>}"
port_dir="freebsd/runner"
makefile="${port_dir}/Makefile"
distinfo="${port_dir}/distinfo"

# Reject anything that isn't strict semver (with optional prerelease) before
# touching files. The downstream `sed -i` substitution would otherwise be at
# the mercy of `&`, `/`, `\`, and newlines in whatever the workflow handed
# us. Keeping the alphabet to [0-9A-Za-z.-] guarantees a byte-for-byte
# literal substitution.
if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "error: version '${version}' does not match semver (X.Y.Z or X.Y.Z-prerelease)" >&2
exit 1
fi

for f in "${makefile}" "${distinfo}"; do
if [[ ! -f "${f}" ]]; then
echo "error: ${f} not found" >&2
exit 1
fi
done

# Fresh upstream version → DISTVERSION bump. PORTREVISION (if any) is reset
# by simply not carrying one in the checked-in Makefile. The literal tab
# keeps the value-column alignment used throughout the Makefile.
tab="$(printf '\t')"
sed -i -E "s/^DISTVERSION=.*/DISTVERSION=${tab}${version}/" "${makefile}"

# CARCH -> Rust triple. Keep in lockstep with RUST_TARGET_* in the Makefile
# and the freebsd-* entries in npm/targets.json. amd64 is the tier-2
# release-blocking build and is mandatory here; aarch64 is the tier-3
# `experimental` build (continue-on-error in release.yml), so its asset may
# legitimately be missing for a given release — we skip it rather than abort,
# matching how the npm publish treats experimental packages as optional.
declare -A triples=(
[amd64]='x86_64-unknown-freebsd'
[aarch64]='aarch64-unknown-freebsd'
)
declare -A required=([amd64]=1 [aarch64]=0)

# distinfo entry for one arch, emitted to stdout. Returns non-zero (without
# printing) when the arch's assets are absent.
emit_arch() {
local carch="$1" triple="$2" distfile sum size
distfile="runner-v${version}-${triple}.tar.gz"

# `.sha256` asset is "<hash> <filename>"; field 1 is the digest.
if ! sum="$(gh release download "v${version}" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "runner-v${version}-${triple}.sha256" \
--output - 2>/dev/null | awk 'NR==1{print $1}')" || [[ -z "${sum}" ]]; then
return 1
fi
if [[ ! "${sum}" =~ ^[0-9a-f]{64}$ ]]; then
echo "error: bad sha256 for ${distfile}: '${sum}'" >&2
exit 1
fi

# Byte size from the release asset metadata (no tarball download).
size="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/v${version}" \
--jq ".assets[] | select(.name == \"${distfile}\") | .size")"
if [[ ! "${size}" =~ ^[0-9]+$ ]]; then
echo "error: bad size for ${distfile}: '${size}'" >&2
exit 1
fi

echo "SHA256 (${distfile}) = ${sum}"
echo "SIZE (${distfile}) = ${size}"
}

# Regenerate distinfo from scratch so a dropped arch can never leave a stale
# entry behind. TIMESTAMP mirrors what `make makesum` would stamp.
{
echo "TIMESTAMP = $(date +%s)"
for carch in amd64 aarch64; do
if ! emit_arch "${carch}" "${triples[$carch]}"; then
if [[ "${required[$carch]}" == 1 ]]; then
echo "error: required ${carch} freebsd asset missing for v${version}" >&2
exit 1
fi
echo "note: skipping ${carch} — no freebsd asset for v${version}" >&2
fi
done
} >"${distinfo}.tmp"
mv "${distinfo}.tmp" "${distinfo}"

echo "--- prepared ${makefile} ---"
grep -E '^(PORTNAME|DISTVERSION|PKGNAMESUFFIX)=' "${makefile}"
echo "--- prepared ${distinfo} ---"
cat "${distinfo}"
91 changes: 91 additions & 0 deletions .github/workflows/freebsd-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: freebsd-release
run-name: >-
${{ format('freebsd publish {0}', case(github.event_name == 'release',
github.event.release.tag_name,
inputs.tag)) }}

on:
release: { types: [published] }
workflow_dispatch:
inputs:
tag:
description: Release tag to publish (e.g. v0.12.0)
required: true
type: string
dry-run:
description: Prepare + build the port in the FreeBSD VM but do not upload
required: true
default: false
type: boolean

permissions: { contents: read }

jobs:
publish:
name: build + attach freebsd pkg
runs-on: ubuntu-latest
# Serialize so a manual dispatch and a release-published run can't race
# the same release's asset upload.
concurrency: { group: freebsd-publish, cancel-in-progress: false }
permissions: { contents: write }
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ case(github.event_name == 'release', github.event.release.tag_name, inputs.tag) }}
DRY_RUN: ${{ inputs.dry-run || 'false' }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with: { persist-credentials: false }

# Bump DISTVERSION + regenerate distinfo from the published release
# assets, so the VM build below fetches and checksums the tarballs for
# exactly this tag (not the checked-in reference snapshot).
- name: prepare port (version + distinfo)
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
bash "${GITHUB_WORKSPACE}/.github/scripts/publish/freebsd-prepare.sh" "${version}"

# Build the port the canonical way — `make package` runs the staging
# QA + plist checks, so a drifted PLIST_FILES fails here instead of
# shipping a broken package. We only need the ports `Mk/` infra, so a
# blobless, sparse clone keeps the checkout tiny.
- name: build pkg in FreeBSD VM
uses: vmactions/freebsd-vm@a6de9343ef5747433d9c25784c90e84998b9d69a # v1.4.6
with:
usesh: true
prepare: |
pkg install -y git
run: |
set -eu
ws="$(pwd)"
git clone --filter=blob:none --depth 1 --sparse \
https://github.com/freebsd/freebsd-ports /usr/ports
cd /usr/ports
git sparse-checkout set Mk Keywords Templates
mkdir -p /usr/ports/devel
cp -R "${ws}/freebsd/runner" /usr/ports/devel/runner
cd /usr/ports/devel/runner
# check-plist fails on drift in either direction (a listed file
# that wasn't staged, or a staged file that isn't listed).
make BATCH=yes stage check-plist package
pkgfile="$(make -V PKGFILE)"
echo "built ${pkgfile}"
mkdir -p "${ws}/freebsd/pkgout"
cp "${pkgfile}" "${ws}/freebsd/pkgout/runner-freebsd-amd64.pkg"

- name: checksum
run: |
set -euo pipefail
cd freebsd/pkgout
sha256sum runner-freebsd-amd64.pkg > runner-freebsd-amd64.pkg.sha256
ls -l
cat runner-freebsd-amd64.pkg.sha256

- name: upload pkg to release
if: env.DRY_RUN != 'true'
run: |
set -euo pipefail
gh release upload "${RELEASE_TAG}" \
freebsd/pkgout/runner-freebsd-amd64.pkg \
freebsd/pkgout/runner-freebsd-amd64.pkg.sha256 \
--repo "${GITHUB_REPOSITORY}" --clobber
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,33 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic

### Added

- FreeBSD distribution channel. A prebuilt port (`freebsd/runner`,
package name `runner-bin`) installs the `runner` + `run` binaries from
the GitHub release `*-unknown-freebsd` tarballs the `release.yml` matrix
already publishes — no recompile — for `amd64` and `aarch64`. Since
FreeBSD has no AUR-style push-to-git remote, the channel ships an
installable amd64 `.pkg` attached to each release:
`pkg add https://github.com/kjanat/runner/releases/latest/download/runner-freebsd-amd64.pkg`.
- FreeBSD completions shipped by the port and auto-loaded from the
canonical `${LOCALBASE}` dirs: bash at
`share/bash-completion/completions/{runner,run}`, zsh at
`share/zsh/site-functions/{_runner,_run}`, fish at
`share/fish/vendor_completions.d/{runner,run}.fish`. PowerShell has no
autoload convention, so the pwsh script is installed at
`share/runner/runner.ps1` for users to dot-source from their `$PROFILE`.
- `.github/workflows/freebsd-release.yml` builds and attaches the `.pkg`
on every `release: published` event (with manual `workflow_dispatch` +
`dry-run` for validation). It runs the real ports build (`make package`)
inside a FreeBSD VM (`vmactions/freebsd-vm`) against a blobless, sparse
checkout of the ports `Mk/` infrastructure, so the staging QA and plist
checks gate every release. Third-party `uses:` pinned to commit SHAs.
- `.github/scripts/publish/freebsd-prepare.sh` rewrites `DISTVERSION` and
regenerates `distinfo` (per-arch `SHA256` + `SIZE`) from the release's
published `.sha256` companion assets and asset metadata. The mandatory
`amd64` entry aborts on a missing asset; the `experimental` `aarch64`
build is skipped with a warning when absent. Strict semver regex on the
version input refuses anything containing `&`, `/`, `\`, or newlines
before any `sed` runs.
- AUR distribution channel. Two packages on the Arch User Repository:
`runner-run-bin` (prebuilt binaries for `x86_64`, `aarch64`, `armv7h`)
and `runner-run` (source build for `x86_64`, `aarch64`). `-bin`
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ Or on Arch Linux:
yay -S runner-run-bin
```

Or on FreeBSD:

```sh
pkg add https://github.com/kjanat/runner/releases/latest/download/runner-freebsd-amd64.pkg
```

<details>
<summary><i>Other install methods</i></summary>

Expand All @@ -197,6 +203,12 @@ cargo install --path .
yay -S runner-run
```

```sh
# FreeBSD/aarch64: build the prebuilt port locally (distinfo already
# carries the aarch64 distfile checksum), or fetch the amd64 .pkg above.
cd /usr/ports/devel/runner && make package
```

```sh
curl -fsSLO https://raw.githubusercontent.com/kjanat/runner/master/install.sh
bash install.sh
Expand Down
86 changes: 86 additions & 0 deletions freebsd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# FreeBSD packaging

A single prebuilt [FreeBSD port][ports] under `runner/`:

| Port | Installs | Arches |
| ------------ | --------------------------------- | -------------- |
| `runner-bin` | prebuilt from GitHub release tars | amd64, aarch64 |

It mirrors the AUR `runner-run-bin` package: no compile, just the upstream
release binaries (`runner` + `run`) plus bash, zsh and fish completions.
The release tarballs are the same `*-unknown-freebsd` assets that the main
`release.yml` build matrix already publishes (see the `freebsd-x64` /
`freebsd-arm64` entries in `npm/targets.json`), so this port reuses them
rather than recompiling.

There is no FreeBSD equivalent of the AUR's push-to-git remote — the
official Ports Collection lands through Bugzilla review. So the
self-serviceable channel is a binary `.pkg` attached to each GitHub
release, installable with `pkg add`:

```sh
pkg add https://github.com/kjanat/runner/releases/latest/download/runner-freebsd-amd64.pkg
```

Completions auto-load from the canonical `${LOCALBASE}` dirs (no `eval`
line needed in a user's rc):

- bash: `${LOCALBASE}/share/bash-completion/completions/{runner,run}`
- zsh: `${LOCALBASE}/share/zsh/site-functions/{_runner,_run}`
- fish: `${LOCALBASE}/share/fish/vendor_completions.d/{runner,run}.fish`

PowerShell has no system autoload dir, so the pwsh script is installed at
`${LOCALBASE}/share/runner/runner.ps1` for users to dot-source from their
`$PROFILE`:

```powershell
if (Test-Path /usr/local/share/runner/runner.ps1) { . /usr/local/share/runner/runner.ps1 }
```

Completions are clap-dynamic (the shell shells out to the binary for
candidates), so tab-completing in a project picks up the *current* task
list from `package.json` / `turbo.json` / `Justfile` / etc., not a static
snapshot.

## Automation

`.github/workflows/freebsd-release.yml` builds and attaches the `.pkg` on
every `release: published` event (and via manual `workflow_dispatch` with
a `tag` input). Per release it:

1. Rewrites `DISTVERSION` in the `Makefile` and regenerates `distinfo`
(per-arch `SHA256` + `SIZE`) from the release's published `.sha256`
companion assets and asset metadata
(`.github/scripts/publish/freebsd-prepare.sh`).
2. Inside a FreeBSD VM ([`vmactions/freebsd-vm`]) builds the port through
the standard ports framework (`make package`) against a sparse checkout
of the ports `Mk/` infrastructure. `make package` runs the staging QA
checks and the plist check, so a drifted `PLIST_FILES` fails the build
loudly instead of shipping a broken package.
3. Uploads the resulting amd64 `.pkg` to the GitHub release as
`runner-freebsd-amd64.pkg` (+ a `.sha256` companion).

The checked-in `Makefile` / `distinfo` values are a reference snapshot; CI
overwrites them before building, so they need not be bumped by hand.

aarch64 users build the port locally — `distinfo` already carries the
aarch64 distfile checksum, so `make package` on an aarch64 host produces
the matching `.pkg` with no edits.

## Validation

Cut a release as usual, or dry-run first:

- **Validate without uploading**: Actions → `freebsd-release` → Run
workflow, set `tag` and tick `dry-run`. This prepares the port and runs
the full `make package` build in the FreeBSD VM, but skips the release
upload.
- **Local build** (on a FreeBSD box, with the port dropped into a ports
tree at `devel/runner`):

```sh
cd /usr/ports/devel/runner && make stage check-plist package
```

[ports]: https://docs.freebsd.org/en/books/porters-handbook/
[`vmactions/freebsd-vm`]: https://github.com/vmactions/freebsd-vm
Loading