Skip to content
Open
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
47 changes: 36 additions & 11 deletions .agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,22 +249,47 @@ mutating tools; the escape hatch for clients without permission-request support)

## Releasing

Version lives in `Cargo.toml`. The binary is published to six registries via separate workflows
(`release-crates`, `release-github`, `release-homebrew`, `release-npm`, `release-nuget`,
`release-pypi`); the `npm/`, `nuget/`, and `pypi/` dirs hold the wrapper-package templates. Update
`CHANGELOG.md` for releases.
Version lives in `Cargo.toml`. Update `CHANGELOG.md` for releases.

Publishing splits into two groups. **Language registries** each get their own tag-triggered
workflow: `release-crates`, `release-npm`, `release-nuget`, `release-pypi`. The `npm/`, `nuget/`,
and `pypi/` dirs hold their wrapper-package templates.

**OS package managers** all publish somewhere outside this repo and all need checksums from the
GitHub release, so `release-github` builds the binaries, attaches the assets, and then dispatches
`release-homebrew`, `release-scoop`, `release-winget`, and `release-aur`. A dispatch failure in one
is logged as a warning rather than failing the others, so an unconfigured channel does not block a
release. Their inputs live in `packaging/`.

Every release asset now carries a `.sha256` sidecar, not just the macOS Homebrew tarball. Scoop,
winget, and the AUR PKGBUILD each need one, and they consume the raw binaries rather than the
tarball. `release-github` also builds a `.deb` and `.rpm` per Linux target with nfpm
(`packaging/nfpm.yaml`), packaging the already-built binary rather than re-invoking cargo.

Three of these need credentials or a one-time manual step before they work:

- Scoop needs a `getsigit/scoop-bucket` repo and a `SCOOP_BUCKET_TOKEN` secret, mirroring the
Homebrew tap setup.
- winget needs a `WINGET_TOKEN` (PAT with `public_repo`) so `wingetcreate` can fork
`microsoft/winget-pkgs`. The workflow only handles *updates*; the first submission has to be made
by hand with `wingetcreate new`, since a package must exist before it can be updated.
- The AUR needs `AUR_USERNAME`, `AUR_EMAIL`, and `AUR_SSH_PRIVATE_KEY`. It publishes `sigit-bin`
(a prebuilt binary) so Arch users are not compiling the on-device inference stack to install a
CLI.

`[profile.release]` sets `strip = "symbols"` because binary size is a distribution constraint, not
just a nicety — see the NuGet note below.
just a nicety. See the NuGet note below.

The NuGet package (`SiGit.Code`, installed with `dotnet tool install --global SiGit.Code`) is the
odd one out: npm and PyPI publish one artifact per platform, but a .NET tool is a single package,
so `nuget/sigit/` bundles all six binaries under `native/<os>-<arch>/` and a small managed shim
(`Program.cs`) execs the right one. That shim leaves stdin/stdout/stderr unredirected on purpose,
since siGit Code chooses TUI or ACP mode by testing whether stdin is a TTY.
odd one out among the language registries: npm and PyPI publish one artifact per platform, but a
.NET tool is a single package, so `nuget/sigit/` bundles all six binaries under
`native/<os>-<arch>/` and a small managed shim (`Program.cs`) execs the right one. That shim leaves
stdin/stdout/stderr unredirected on purpose, since siGit Code chooses TUI or ACP mode by testing
whether stdin is a TTY. It also sets `RollForward=Major`: it targets `net8.0`, and without that a
machine carrying only the .NET 10 runtime installs a tool that refuses to start.

Bundling every target means the package is large, so `release-nuget.yml` fails the pack job if the
`.nupkg` crosses nuget.org's 250 MB limit. At v1.5.1 it lands around 160 MB. If a future release
trips that check, the fix is not to drop targets but to split into RID-specific tool packages
(.NET 10's `DotnetToolRidPackage`), which ship one binary per platform the way npm already does
that also needs a fallback package for pre-.NET-10 SDKs.
(.NET 10's `DotnetToolRidPackage`), which ship one binary per platform the way npm already does.
That also needs a fallback package for pre-.NET-10 SDKs.
99 changes: 99 additions & 0 deletions .github/workflows/release-aur.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: AUR Release

# Dispatched by release-github.yml once the release and its checksums exist.
#
# Publishes the `sigit-bin` package: a prebuilt binary rather than a source
# build, so Arch users are not compiling the whole dependency tree (and the
# on-device inference stack in particular) to install a CLI.
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag (e.g. v1.5.2)"
required: true

permissions:
contents: read

env:
REPO: getsigit/sigit

jobs:
publish-aur-package:
name: Publish sigit-bin to the AUR
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6
with:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitactions/checkout@v6 does not exist yet (latest stable is v4); this will fail at runtime when the action is resolved. Use actions/checkout@v4.

ref: ${{ github.event.inputs.tag }}

- name: Resolve tag and version
id: release
shell: bash
run: |
TAG="${{ github.event.inputs.tag }}"
VERSION="${TAG#v}"

echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"

- name: Read SHA256 checksums from release
id: sha
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
mkdir -p artifacts

gh release download "${{ steps.release.outputs.tag }}" \
--repo "${{ env.REPO }}" \
--pattern "sigit-linux-*.sha256" \
--dir artifacts/

X86_64_SHA=$(cat artifacts/sigit-linux-amd64.sha256)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical — The .sha256 sidecars contain the checksum of the full binary asset, but PKGBUILD's source_x86_64 downloads that same binary — so the sha must match. Confirm the sidecar hashes the raw sigit-linux-amd64 binary and not a renamed/archived variant, otherwise makepkg validation fails.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning — The SHA read here comes from the .sha256 of the release asset, but the PKGBUILD's source_x86_64 downloads sigit-linux-amd64 (a different URL/file than what winget/scoop reference). Confirm the released asset is literally named sigit-linux-amd64 (no arch suffix mismatch) so the checksum matches what makepkg fetches.

AARCH64_SHA=$(cat artifacts/sigit-linux-arm64.sha256)

echo "x86_64=${X86_64_SHA}" >> "$GITHUB_OUTPUT"
echo "aarch64=${AARCH64_SHA}" >> "$GITHUB_OUTPUT"

echo "x86_64 SHA256: ${X86_64_SHA}"
echo "aarch64 SHA256: ${AARCH64_SHA}"

- name: Render PKGBUILD
shell: bash
run: |
mkdir -p aur-build

sed \
-e "s/@VERSION@/${{ steps.release.outputs.version }}/g" \
-e "s/@SHA256_X86_64@/${{ steps.sha.outputs.x86_64 }}/g" \
-e "s/@SHA256_AARCH64@/${{ steps.sha.outputs.aarch64 }}/g" \
packaging/aur/PKGBUILD.in > aur-build/PKGBUILD

if grep -q '@[A-Z0-9_]*@' aur-build/PKGBUILD; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning — The placeholder grep @[A-Z0-9_]*@ matches @@ (empty), so a completely-failed substitution leaving a bare @@ would still trip it, but a value containing @ (e.g. an unexpected sha) wouldn't be caught — minor, but also note this grep runs before bash -n which is good.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — The placeholder-detection regex @[A-Z0-9_]*@ won't catch a placeholder that was partially substituted and left something like @sha256_X86_64@ (wrong case). The PKGBUILD template uses uppercase, which matches, but if the template ever gains a mixed-case placeholder this guard silently passes. A broader pattern like @[A-Za-z0-9_]*@ would be safer.

echo "::error::PKGBUILD still contains unsubstituted placeholders" >&2
grep -n '@[A-Z0-9_]*@' aur-build/PKGBUILD >&2
exit 1
fi

# Catches an unbalanced quote or paren before it reaches the AUR.
bash -n aur-build/PKGBUILD

echo "Rendered PKGBUILD:"
cat aur-build/PKGBUILD

# The action runs makepkg in an Arch container to generate .SRCINFO and
# pushes over SSH. Generating .SRCINFO by hand is possible but drifts
# from the PKGBUILD the moment a field is added.
- name: Publish to the AUR
uses: KSXGitHub/github-actions-deploy-aur@v3
with:
pkgname: sigit-bin
pkgbuild: aur-build/PKGBUILD
commit_username: ${{ secrets.AUR_USERNAME }}
commit_email: ${{ secrets.AUR_EMAIL }}
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: "Update sigit-bin to ${{ steps.release.outputs.version }}"
updpkgsums: false
87 changes: 77 additions & 10 deletions .github/workflows/release-github.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,54 @@ jobs:
echo "Archive: ${ARCHIVE_NAME}"
echo "SHA256: $(cat "${ARCHIVE_NAME}.sha256")"

# Debian and RPM packages are built from the binary staged above rather
# than by re-invoking cargo, so each Linux target compiles once and gets
# packaged twice.
- name: Build Linux packages
if: contains(matrix.target, 'linux')
shell: bash
env:
NFPM_VERSION: "2.43.0"
run: |
case "$(uname -m)" in
x86_64) nfpm_arch="x86_64"; export PKG_ARCH="amd64" ;;
aarch64) nfpm_arch="arm64"; export PKG_ARCH="arm64" ;;
*) echo "Unsupported Linux build host $(uname -m)" >&2; exit 1 ;;
esac

curl -sSfL \
"https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_Linux_${nfpm_arch}.tar.gz" \
| tar -xz -C /tmp nfpm

export PKG_VERSION="${RELEASE_VERSION}"
export PKG_BINARY="./release/${PROJECT_NAME}-${{ matrix.name }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warningPKG_BINARY references ${{ matrix.name }} but the checksum step and upload later glob release/*; ensure the staged binary name (${PROJECT_NAME}-${matrix.name}) matches the sigit-linux-amd64/sigit-linux-arm64 names the AUR/Scoop workflows download, or the sidecars won't be found.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warningPKG_BINARY points at ./release/${PROJECT_NAME}-${{ matrix.name }}, but nfpm runs with cwd unspecified; ensure this step's working directory contains release/, LICENSE, and README.md (referenced as ./LICENSE in nfpm.yaml) or the package build fails to find them.


/tmp/nfpm package --config packaging/nfpm.yaml --packager deb --target ./release/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warningnfpm package --target ./release/ lets nfpm choose the output filename, but the earlier step already placed a raw binary there named sigit-linux-amd64. nfpm will produce something like sigit_1.5.2_amd64.deb; the subsequent checksum loop will hash it correctly, but the AUR workflow later downloads sigit-linux-amd64.sha256 (the raw binary's sidecar), not the .deb's. This is intentional, but the .deb and .rpm files will also get .sha256 sidecars whose names are whatever nfpm chose—make sure downstream consumers (README, release notes) reference the correct filenames.

/tmp/nfpm package --config packaging/nfpm.yaml --packager rpm --target ./release/

ls -la ./release/

# Homebrew has always had a checksum because the tap needs one. Scoop,
# winget, and the AUR PKGBUILD each need one too, and they consume the
# raw binaries rather than the macOS tarball, so every asset gets a
# sidecar. Windows runners use bash from Git for Windows, which has
# sha256sum; macOS only has shasum.
- name: Checksum release assets
shell: bash
run: |
cd release

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning — The checksum step iterates release/ including the freshly built .deb/.rpm, but on the Windows/macOS matrix legs those files won't exist; harmless, though note the AUR/Scoop workflows depend on specific asset names (sigit-linux-amd64 etc.) which must match the staged binary names from matrix.name — verify those line up.

for asset in *; do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning — The checksum loop runs on every matrix job but only Linux jobs produce .deb/.rpm; ensure the nfpm packages are staged into release/ before this step and that .deb/.rpm sidecars are actually wanted (they are checksummed but no channel consumes them).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning — The glob for asset in * will also match directories if any exist under release/. Consider for asset in *; do [ -f "$asset" ] || continue; … to avoid computing a checksum for a directory entry and producing a nonsensical .sha256 file.

case "${asset}" in *.sha256) continue ;; esac

if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${asset}" | awk '{print $1}' > "${asset}.sha256"
else
shasum -a 256 "${asset}" | awk '{print $1}' > "${asset}.sha256"
fi

echo "${asset}: $(cat "${asset}.sha256")"
done

- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -171,17 +219,36 @@ jobs:
tag_name: ${{ steps.tag.outputs.tag }}
files: release/*

- name: Trigger Homebrew release
# These channels all publish somewhere outside this repo (a tap, a Scoop
# bucket, microsoft/winget-pkgs, the AUR) and every one of them reads
# checksums off the release created above, so they hang off this job
# rather than firing on the tag directly.
- name: Trigger OS package manager releases
uses: actions/github-script@v7
with:
script: |
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'release-homebrew.yml',
ref: 'main',
inputs: {
tag: '${{ steps.tag.outputs.tag }}'
const tag = '${{ steps.tag.outputs.tag }}'
const workflows = [
'release-homebrew.yml',
'release-scoop.yml',
'release-winget.yml',
'release-aur.yml',
]

for (const workflow_id of workflows) {
try {
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id,
ref: 'main',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning — Dispatches use ref: 'main', so the workflow definition run is whatever is on main, not the released tag; if a release is cut from a branch other than main this dispatches stale workflow logic. Intentional but worth confirming.

inputs: { tag },
})
console.log(`Dispatched ${workflow_id} for tag ${tag}`)
} catch (error) {
// One packaging channel being unconfigured (a missing secret,
// a bucket repo that does not exist yet) should not take the
// rest of the fan-out down with it.
core.warning(`Failed to dispatch ${workflow_id}: ${error.message}`)
}
})
console.log('Dispatched release-homebrew.yml for tag ${{ steps.tag.outputs.tag }}')
}
Loading