name: Release
on:
workflow_dispatch:
push:
tags:
- "v*"
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
verify:
name: Verify tag
runs-on: ubuntu-latest
outputs:
version: ${{ steps.parse.outputs.version }}
steps:
- uses: actions/checkout@v7
- name: Parse version
id: parse
env:
TAG: ${{ github.ref_name }}
run: |
VERSION="${TAG#v}"
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Tag '$TAG' is not a valid semver tag (vX.Y.Z)"
exit 1
fi
CARGO_VERSION=$(grep '^version =' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
if [ "$VERSION" != "$CARGO_VERSION" ]; then
echo "::error::Tag version '$VERSION' does not match Cargo.toml version '$CARGO_VERSION'"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
build-binaries:
name: Binary (${{ matrix.target }})
needs: verify
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
os_name: linux
arch: amd64
artifact_name: carryctx-x86_64-unknown-linux-gnu
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04-arm
os_name: linux
arch: arm64
artifact_name: carryctx-aarch64-unknown-linux-gnu
- target: x86_64-apple-darwin
os: macos-latest
os_name: macos
arch: amd64
artifact_name: carryctx-x86_64-apple-darwin
- target: aarch64-apple-darwin
os: macos-latest
os_name: macos
arch: arm64
artifact_name: carryctx-aarch64-apple-darwin
- target: x86_64-pc-windows-msvc
os: windows-latest
os_name: windows
arch: amd64
artifact_name: carryctx-x86_64-pc-windows-msvc.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
- name: Build release
run: cargo build --release --locked --target ${{ matrix.target }}
- name: Rename artifact (Linux/macOS)
if: matrix.os_name != 'windows'
run: mv target/${{ matrix.target }}/release/carryctx ${{ matrix.artifact_name }}
- name: Rename artifact (Windows)
if: matrix.os_name == 'windows'
run: move target\${{ matrix.target }}\release\carryctx.exe ${{ matrix.artifact_name }}
- name: Package for Linux distributions (nfpm)
if: matrix.os_name == 'linux'
run: |
if [ "${{ matrix.arch }}" = "arm64" ]; then
NFPM_ARCH="arm64"
PKG_ARCH="aarch64"
else
NFPM_ARCH="x86_64"
PKG_ARCH="x86_64"
fi
curl -sL https://github.com/goreleaser/nfpm/releases/download/v2.37.1/nfpm_2.37.1_Linux_${NFPM_ARCH}.tar.gz | tar xz nfpm
sed -i "s/amd64/${NFPM_ARCH}/g" nfpm.yaml
sed -i "s/carryctx-linux-x86_64/${{ matrix.artifact_name }}/g" nfpm.yaml
./nfpm pkg --packager deb --target carryctx-linux-${PKG_ARCH}.deb
./nfpm pkg --packager rpm --target carryctx-linux-${PKG_ARCH}.rpm
./nfpm pkg --packager apk --target carryctx-linux-${PKG_ARCH}.apk
./nfpm pkg --packager archlinux --target carryctx-linux-${PKG_ARCH}.pkg.tar.zst
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: carryctx-${{ matrix.target }}
path: |
${{ matrix.artifact_name }}
*.deb
*.rpm
*.apk
*.pkg.tar.zst
if-no-files-found: warn
publish-cratesio:
name: Publish crates.io
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --locked || echo "crates.io publish skipped (version may already exist)"
publish-npm:
name: Publish npm (${{ matrix.target }})
needs: [verify, build-binaries]
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-apple-darwin
- aarch64-apple-darwin
- x86_64-pc-windows-msvc
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Download binary
uses: actions/download-artifact@v4
with:
name: carryctx-${{ matrix.target }}
path: target/${{ matrix.target }}/release/
- name: Package platform npm
env:
TARGET: ${{ matrix.target }}
VERSION: ${{ needs.verify.outputs.version }}
run: |
PLATFORM_DIR="npm/@carryctx/cli-${TARGET}"
mkdir -p "$PLATFORM_DIR/bin"
if [ -f "target/$TARGET/release/carryctx.exe" ]; then
cp "target/$TARGET/release/carryctx.exe" "$PLATFORM_DIR/bin/carryctx"
elif [ -f "target/$TARGET/release/carryctx" ]; then
cp "target/$TARGET/release/carryctx" "$PLATFORM_DIR/bin/carryctx"
fi
chmod +x "$PLATFORM_DIR/bin/carryctx" 2>/dev/null || true
# Determine os/cpu/libc from target triple
case "$TARGET" in
x86_64-unknown-linux-gnu) OS='["linux"]'; CPU='["x64"]'; LIBC='["glibc"]' ;;
x86_64-apple-darwin) OS='["darwin"]'; CPU='["x64"]' ;;
aarch64-apple-darwin) OS='["darwin"]'; CPU='["arm64"]' ;;
x86_64-pc-windows-msvc) OS='["win32"]'; CPU='["x64"]' ;;
*) echo "Unknown target: $TARGET"; exit 1 ;;
esac
LIBC_FIELD=""
[ -n "$LIBC" ] && LIBC_FIELD=',"libc":'$LIBC
cat > "$PLATFORM_DIR/package.json" <<- PKG_EOF
{
"name": "@carryctx/cli-${TARGET}",
"version": "${VERSION}",
"description": "CarryCtx CLI binary for ${TARGET}",
"os": $OS,
"cpu": $CPU$LIBC_FIELD,
"bin": { "carryctx": "bin/carryctx" }
}
PKG_EOF
cat "$PLATFORM_DIR/package.json"
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
cd "npm/@carryctx/cli-${{ matrix.target }}"
npm publish --access public 2>&1 | tail -5 || echo "npm publish failed (may already exist)"
publish-npm-root:
name: Publish npm (root)
needs: [verify, publish-npm]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Create root package
env:
VERSION: ${{ needs.verify.outputs.version }}
run: |
mkdir -p npm/carryctx/bin
cat > npm/carryctx/package.json <<- PKG_EOF
{
"name": "carryctx",
"version": "${VERSION}",
"description": "Persistent project context for coding agents",
"bin": { "carryctx": "./bin/carryctx.js" },
"optionalDependencies": {
"@carryctx/cli-linux-x64-gnu": "${VERSION}",
"@carryctx/cli-linux-arm64-gnu": "${VERSION}",
"@carryctx/cli-darwin-x64": "${VERSION}",
"@carryctx/cli-darwin-arm64": "${VERSION}",
"@carryctx/cli-win32-x64": "${VERSION}"
}
}
PKG_EOF
cat > npm/carryctx/bin/carryctx.js << 'LAUNCHER'
#!/usr/bin/env node
const { spawnSync } = require('child_process');
const { platform, arch } = process;
const osMap = { win32: 'win32', darwin: 'darwin', linux: 'linux' };
const archMap = { x64: 'x64', arm64: 'arm64' };
const pn = osMap[platform] || platform;
const an = archMap[arch] || arch;
const target = platform === 'linux' ? `${pn}-${an}-gnu` : `${pn}-${an}`;
try {
const binPath = require.resolve(`@carryctx/cli-${target}/bin/carryctx`);
const result = spawnSync(binPath, process.argv.slice(2), { stdio: 'inherit' });
process.exit(result.status ?? 1);
} catch (e) {
console.error(`CarryCtx: no binary found for ${target}`);
console.error('Install from crates.io, npm (platform package), or GitHub Releases.');
process.exit(1);
}
LAUNCHER
chmod +x npm/carryctx/bin/carryctx.js
- name: Publish root package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
cd npm/carryctx
npm publish --access public 2>&1 | tail -5 || echo "npm publish failed (may already exist)"
create-release:
name: GitHub Release
needs: [verify, build-binaries]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Download all binaries
uses: actions/download-artifact@v4
with:
pattern: carryctx-*
merge-multiple: true
- name: Create release
uses: softprops/action-gh-release@v3
with:
files: |
carryctx-x86_64-unknown-linux-gnu
carryctx-aarch64-unknown-linux-gnu
carryctx-x86_64-apple-darwin
carryctx-aarch64-apple-darwin
carryctx-x86_64-pc-windows-msvc.exe
*.deb
*.rpm
*.apk
*.pkg.tar.zst
name: v${{ needs.verify.outputs.version }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-aur:
name: Publish AUR
needs: [verify, build-binaries]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7
- name: Download Linux binaries
uses: actions/download-artifact@v4
with:
pattern: carryctx-*-unknown-linux-*
merge-multiple: true
- name: Compute SHA256 and generate PKGBUILD
env:
VERSION: ${{ needs.verify.outputs.version }}
run: |
sum_x86_64=$(sha256sum carryctx-x86_64-unknown-linux-gnu | cut -d' ' -f1)
cat > PKGBUILD <<- PKGEOF
# Maintainer: Xuepoo <xuepoo@carryctx.dev>
pkgname=carryctx
pkgver=${VERSION}
pkgrel=1
pkgdesc="Persistent project context for coding agents"
arch=('x86_64')
url="https://carryctx.dev"
license=('MIT')
depends=('glibc' 'gcc-libs')
source_x86_64=("https://github.com/Xuepoo/carryctx/releases/download/v${VERSION}/carryctx-x86_64-unknown-linux-gnu")
sha256sums_x86_64=('${sum_x86_64}')
package() {
install -Dm755 "\${srcdir}/carryctx-x86_64-unknown-linux-gnu" "\${pkgdir}/usr/bin/carryctx"
}
PKGEOF
echo "sum_x86_64=${sum_x86_64}" >> "$GITHUB_ENV"
echo "PKGBUILD generated for v${VERSION}"
cat PKGBUILD | head -5
- name: Push to AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
VERSION: ${{ needs.verify.outputs.version }}
run: |
mkdir -p ~/.ssh
echo "${AUR_SSH_PRIVATE_KEY}" > ~/.ssh/aur_key
chmod 600 ~/.ssh/aur_key
cat >> ~/.ssh/config <<- SSHCONF
Host aur.archlinux.org
IdentityFile ~/.ssh/aur_key
User aur
StrictHostKeyChecking accept-new
SSHCONF
git clone ssh://aur@aur.archlinux.org/carryctx.git /tmp/aur-repo
cp PKGBUILD /tmp/aur-repo/
cd /tmp/aur-repo
cat > .SRCINFO <<- SRCINFO
pkgbase = carryctx
pkgdesc = Persistent project context for coding agents
pkgver = ${VERSION}
pkgrel = 1
url = https://carryctx.dev
arch = x86_64
license = MIT
depends = glibc
depends = gcc-libs
source_x86_64 = https://github.com/Xuepoo/carryctx/releases/download/v${VERSION}/carryctx-x86_64-unknown-linux-gnu
sha256sums_x86_64 = ${sum_x86_64}
SRCINFO
git add PKGBUILD .SRCINFO
git commit -m "chore: bump to v${VERSION}" --allow-empty --no-verify
git push origin master
publish-homebrew:
name: Publish Homebrew
needs: [verify, build-binaries]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7
- name: Download Linux and macOS binaries
uses: actions/download-artifact@v4
with:
pattern: carryctx-*
merge-multiple: true
- name: Compute SHA256 and update formula
env:
VERSION: ${{ needs.verify.outputs.version }}
run: |
sum_linux_x64=$(sha256sum carryctx-x86_64-unknown-linux-gnu | cut -d' ' -f1)
sum_macos_x64=$(sha256sum carryctx-x86_64-apple-darwin | cut -d' ' -f1)
sum_macos_arm64=$(sha256sum carryctx-aarch64-apple-darwin | cut -d' ' -f1)
cat > carryctx.rb <<- RBEOF
class Carryctx < Formula
desc "Persistent project context for coding agents"
homepage "https://carryctx.dev"
version "${VERSION}"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/Xuepoo/carryctx/releases/download/v${VERSION}/carryctx-aarch64-apple-darwin"
sha256 "${sum_macos_arm64}"
else
url "https://github.com/Xuepoo/carryctx/releases/download/v${VERSION}/carryctx-x86_64-apple-darwin"
sha256 "${sum_macos_x64}"
end
end
on_linux do
url "https://github.com/Xuepoo/carryctx/releases/download/v${VERSION}/carryctx-x86_64-unknown-linux-gnu"
sha256 "${sum_linux_x64}"
end
def install
if OS.mac?
if Hardware::CPU.arm?
bin.install "carryctx-aarch64-apple-darwin" => "carryctx"
else
bin.install "carryctx-x86_64-apple-darwin" => "carryctx"
end
elsif OS.linux?
bin.install "carryctx-x86_64-unknown-linux-gnu" => "carryctx"
end
end
test do
assert_match version.to_s, shell_output("\#{bin}/carryctx --version")
end
end
RBEOF
- name: Push to homebrew-tap
run: |
git clone https://github.com/Xuepoo/homebrew-tap.git /tmp/homebrew-tap
cp carryctx.rb /tmp/homebrew-tap/Formula/carryctx.rb
cd /tmp/homebrew-tap
git config user.name "CarryCtx Bot"
git config user.email "bot@carryctx.dev"
git add Formula/carryctx.rb
git commit -m "chore(carryctx): update to v${VERSION}" --no-verify
git push https://x-access-token:${GITHUB_TOKEN}@github.com/Xuepoo/homebrew-tap.git main
env:
GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
publish-scoop:
name: Publish Scoop
needs: [verify, build-binaries]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7
- name: Download Windows binary
uses: actions/download-artifact@v8
with:
pattern: carryctx-x86_64-pc-windows-msvc
merge-multiple: true
- name: Compute SHA256 and update manifest
env:
VERSION: ${{ needs.verify.outputs.version }}
run: |
hash=$(sha256sum carryctx-x86_64-pc-windows-msvc | cut -d' ' -f1)
cat > carryctx.json <<- JSONEOF
{
"version": "${VERSION}",
"description": "Persistent project context for coding agents",
"homepage": "https://carryctx.dev",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://github.com/Xuepoo/carryctx/releases/download/v${VERSION}/carryctx-x86_64-pc-windows-msvc.exe",
"hash": "${hash}",
"bin": [["carryctx-x86_64-pc-windows-msvc.exe", "carryctx"]]
}
},
"checkver": {
"github": "https://github.com/Xuepoo/carryctx"
},
"autoupdate": {
"architecture": {
"64bit": {
"url": "https://github.com/Xuepoo/carryctx/releases/download/v\$version/carryctx-x86_64-pc-windows-msvc.exe"
}
}
}
}
JSONEOF
- name: Push to scoop-bucket
run: |
git clone https://github.com/Xuepoo/scoop-bucket.git /tmp/scoop-bucket
cp carryctx.json /tmp/scoop-bucket/bucket/carryctx.json
cd /tmp/scoop-bucket
git config user.name "CarryCtx Bot"
git config user.email "bot@carryctx.dev"
git add bucket/carryctx.json
git commit -m "chore(carryctx): update to v${VERSION}" --no-verify
git push https://x-access-token:${GITHUB_TOKEN}@github.com/Xuepoo/scoop-bucket.git main
env:
GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}