@@ -0,0 +1,919 @@
name : Release
on :
push :
branches : [ main ]
# Ignore common non-release paths, but DO NOT ignore release.yml so that
# edits to this workflow can intentionally trigger a new release run.
paths-ignore :
- '.github/workflows/issue-triage.yml'
- '.github/workflows/preview-build.yml'
- '.github/workflows/upstream-merge.yml'
- 'examples/**'
- '**/*.test.ts'
- 'test/**'
- '*.md'
- 'CHANGELOG.md'
- 'release-notes/**'
- 'codex-cli/package.json'
concurrency :
group : ci-${{ github.ref }}
cancel-in-progress : true
permissions :
contents : write
jobs :
preflight-tests :
name : Preflight Tests (Linux fast E2E)
runs-on : ubuntu-24.04
steps :
- name : Checkout code
uses : actions/checkout@v4
- name : Install Rust (1.90)
shell : bash
run : |
rustup set profile minimal
rustup toolchain install 1.90.0 --profile minimal
rustup default 1.90.0
- name : Setup Rust Cache
uses : Swatinem/rust-cache@v2
with :
prefix-key : v5-rust
shared-key : code-preflight-1.90
workspaces : |
code-rs -> target
cache-targets : true
cache-workspace-crates : true
cache-on-failure : true
- name : Build (fast profile)
shell : bash
run : ./build-fast.sh
- name : Curated tests + CLI smokes
shell : bash
run : bash scripts/ci-tests.sh
determine-version :
name : Determine Version
runs-on : ubuntu-latest
outputs :
version : ${{ steps.version.outputs.version }}
steps :
- name : Checkout code
uses : actions/checkout@v4
with : { fetch-depth : 0 }
- name : Setup Node.js
uses : actions/setup-node@v4
with :
node-version : '20'
registry-url : 'https://registry.npmjs.org'
- name : Determine next version (across all npm packages; ensure unique)
id : version
working-directory : codex-cli
shell : bash
run : |
set -euo pipefail
CURRENT_VERSION=$(node -p "require('./package.json').version")
PKGS=(
"@just-every/code"
"@just-every/code-darwin-arm64"
"@just-every/code-darwin-x64"
"@just-every/code-linux-x64-musl"
"@just-every/code-linux-arm64-musl"
"@just-every/code-win32-x64"
)
# Find the highest published version across all packages
MAX_PUBLISHED="0.0.0"
for p in "${PKGS[@]}"; do
v=$(npm view "$p" version 2>/dev/null || echo "0.0.0")
if [ "$(printf '%s\n%s\n' "$MAX_PUBLISHED" "$v" | sort -V | tail -n1)" != "$MAX_PUBLISHED" ]; then
MAX_PUBLISHED="$v"
fi
done
# Start with whichever is higher: CURRENT_VERSION or MAX_PUBLISHED
CANDIDATE=$(printf '%s\n%s\n' "$CURRENT_VERSION" "$MAX_PUBLISHED" | sort -V | tail -n1)
# If equal to MAX_PUBLISHED, bump patch once
if [ "$CANDIDATE" = "$MAX_PUBLISHED" ]; then
IFS='.' read -ra V <<< "$CANDIDATE"; CANDIDATE="${V[0]}.${V[1]}.$((${V[2]} + 1))"
fi
# Ensure candidate is globally unused; keep bumping until unique
while :; do
used=false
for p in "${PKGS[@]}"; do
if [ "$(npm view "$p@$CANDIDATE" version 2>/dev/null || true)" = "$CANDIDATE" ]; then
used=true; break
fi
done
if [ "$used" = false ]; then break; fi
IFS='.' read -ra V <<< "$CANDIDATE"; CANDIDATE="${V[0]}.${V[1]}.$((${V[2]} + 1))"
done
NEW_VERSION="$CANDIDATE"
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
build-binaries :
name : Build ${{ matrix.target }}
needs : [ determine-version]
runs-on : ${{ matrix.os }}
strategy :
matrix :
include :
# Linux builds
- os : ubuntu-24.04
target : x86_64-unknown-linux-musl
artifact : code-x86_64-unknown-linux-musl
- os : ubuntu-24.04-arm
target : aarch64-unknown-linux-musl
artifact : code-aarch64-unknown-linux-musl
# (GNU variants omitted to reduce asset duplication; npm defaults to musl)
# macOS builds
- os : macos-14
target : x86_64-apple-darwin
artifact : code-x86_64-apple-darwin
- os : macos-14
target : aarch64-apple-darwin
artifact : code-aarch64-apple-darwin
# Windows build
- os : windows-latest
target : x86_64-pc-windows-msvc
artifact : code-x86_64-pc-windows-msvc.exe
steps :
- name : Checkout code
uses : actions/checkout@v4
- name : Install Rust (minimal)
shell : bash
run : |
rustup set profile minimal
rustup toolchain install 1.90.0 --profile minimal
rustup default 1.90.0
rustup target add ${{ matrix.target }}
# Keep target/ across runs so Cargo can no-op when code hasn't changed
- id : rust_cache
name : Setup Rust Cache (target + registries)
uses : Swatinem/rust-cache@v2
with :
prefix-key : v5-rust
shared-key : code-${{ matrix.target }}-toolchain-1.90
workspaces : |
code-rs -> target
cache-targets : true
cache-workspace-crates : true
cache-on-failure : true
# sccache: skip compiles when possible (doesn't skip linking)
- name : Setup sccache (GHA backend)
uses : mozilla-actions/[email protected]
with :
version : v0.10.0
token : ${{ secrets.GITHUB_TOKEN }}
- name : Enable sccache
shell : bash
run : |
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
echo "SCCACHE_IDLE_TIMEOUT=1800" >> "$GITHUB_ENV"
echo "SCCACHE_CACHE_SIZE=10G" >> "$GITHUB_ENV"
# -------- Platform tuning (minimal, proven) --------
# Linux GNU: use mold if available; prefer system OpenSSL
- name : Linux (gnu) tuning
if : contains(matrix.os, 'ubuntu') && contains(matrix.target, 'gnu')
shell : bash
run : |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y libssl-dev pkg-config mold || true
if command -v clang >/dev/null 2>&1; then
echo 'CC=sccache clang' >> "$GITHUB_ENV"
echo 'CXX=sccache clang++' >> "$GITHUB_ENV"
else
echo 'CC=sccache gcc' >> "$GITHUB_ENV"
echo 'CXX=sccache g++' >> "$GITHUB_ENV"
fi
echo 'OPENSSL_NO_VENDOR=1' >> "$GITHUB_ENV"
echo 'RUSTFLAGS=-Awarnings -C link-arg=-fuse-ld=mold -C debuginfo=0 -C strip=symbols -C panic=abort' >> "$GITHUB_ENV"
# Linux MUSL: reliable static build via musl-gcc (no glibc symbol leaks)
- name : Linux (musl) tuning
if : contains(matrix.os, 'ubuntu') && contains(matrix.target, 'musl')
shell : bash
run : |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y musl-tools pkg-config
echo 'CC=musl-gcc' >> "$GITHUB_ENV"
case "${{ matrix.target }}" in
x86_64-unknown-linux-musl) echo 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc' >> "$GITHUB_ENV" ;;
aarch64-unknown-linux-musl) echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc' >> "$GITHUB_ENV" ;;
esac
echo 'PKG_CONFIG_ALLOW_CROSS=1' >> "$GITHUB_ENV"
echo 'OPENSSL_STATIC=1' >> "$GITHUB_ENV"
echo 'RUSTFLAGS=-Awarnings -C debuginfo=0 -C strip=symbols -C panic=abort' >> "$GITHUB_ENV"
# macOS: stick with Apple toolchain to avoid brew overhead; still cache C via sccache
- name : macOS tuning
if : startsWith(matrix.os, 'macos-')
shell : bash
run : |
echo 'CC=sccache clang' >> "$GITHUB_ENV"
echo 'CXX=sccache clang++' >> "$GITHUB_ENV"
echo 'RUSTFLAGS=-Awarnings -C debuginfo=0 -C strip=symbols -C panic=abort' >> "$GITHUB_ENV"
# Windows: vcpkg not needed (rustls + pure Rust deps)
# Windows: use SChannel (no OpenSSL) + fast linker flags
- name : Windows TLS backend (SChannel) + linker flags
if : matrix.os == 'windows-latest'
shell : pwsh
run : |
# Force libgit2 to use the native Windows TLS stack
"LIBGIT2_SYS_USE_SCHANNEL=1" >> $env:GITHUB_ENV
# If anything in your graph uses curl-sys, prefer SChannel there too
"CURL_SSL_BACKEND=schannel" >> $env:GITHUB_ENV
# Prefer lld-link if present; otherwise MSVC link with good opts
if (Get-Command lld-link -ErrorAction SilentlyContinue) {
"RUSTFLAGS=-Awarnings -Clinker=lld-link -C codegen-units=16 -C debuginfo=0 -C strip=symbols -C panic=abort -C link-arg=/OPT:REF -C link-arg=/OPT:ICF -C link-arg=/DEBUG:NONE" >> $env:GITHUB_ENV
} else {
"RUSTFLAGS=-Awarnings -C codegen-units=16 -C debuginfo=0 -C strip=symbols -C panic=abort -C link-arg=/OPT:REF -C link-arg=/OPT:ICF -C link-arg=/DEBUG:NONE" >> $env:GITHUB_ENV
}
# Prefetch deps so --frozen works even with git deps
- name : Prefetch dependencies (git + registry)
working-directory : code-rs
env :
CARGO_NET_GIT_FETCH_WITH_CLI : "true"
run : cargo fetch --locked
# Inject the display version without touching Cargo manifests
- name : Export CODE_VERSION for Rust build
shell : bash
run : echo "CODE_VERSION=${{ needs.determine-version.outputs.version }}" >> "$GITHUB_ENV"
- name : Build binaries (with timings)
shell : bash
env :
CARGO_INCREMENTAL : "0" # keep off in CI; release builds + sccache
RUST_BACKTRACE : "1"
run : |
cd code-rs
cargo build --release --frozen --locked --timings --target ${{ matrix.target }} --bin code
- name : Post-build smoke (run binary) [Unix]
if : |
(contains(matrix.os, 'ubuntu') && matrix.target == 'x86_64-unknown-linux-musl') ||
(matrix.os == 'ubuntu-24.04-arm' && matrix.target == 'aarch64-unknown-linux-musl') ||
(startsWith(matrix.os, 'macos-') && matrix.target == 'aarch64-apple-darwin')
shell : bash
run : |
set -euo pipefail
exe="code-rs/target/${{ matrix.target }}/release/code"
"$exe" --version
"$exe" completion bash > /dev/null
- name : Post-build smoke (run binary) [Windows]
if : matrix.os == 'windows-latest' && matrix.target == 'x86_64-pc-windows-msvc'
shell : pwsh
run : |
$exe = "code-rs/target/${{ matrix.target }}/release/code.exe"
& $exe --version | Out-Null
& $exe completion bash | Out-Null
- name : sccache stats
shell : bash
run : sccache --show-stats || true
- name : Prepare artifacts
shell : bash
run : |
mkdir -p artifacts
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
cp code-rs/target/${{ matrix.target }}/release/code.exe artifacts/${{ matrix.artifact }}
else
cp code-rs/target/${{ matrix.target }}/release/code artifacts/${{ matrix.artifact }}
fi
- name : Compress artifacts (Windows)
if : matrix.os == 'windows-latest'
shell : pwsh
run : |
Get-ChildItem artifacts -File | ForEach-Object {
$src = $_.FullName
$dst = "$src.zip"
Compress-Archive -Path $src -DestinationPath $dst -Force
Remove-Item $src -Force
}
- name : Compress artifacts (Linux dual-format)
if : contains(matrix.os, 'ubuntu')
shell : bash
run : |
shopt -s nullglob
for f in artifacts/*; do
# Only process regular files; skip any directories
[ -f "$f" ] || continue
base=$(basename "$f")
# .zst (size-optimized)
zstd -T0 -19 --force -o "artifacts/${base}.zst" "$f"
# .tar.gz fallback for users without zstd
tar -C artifacts -czf "artifacts/${base}.tar.gz" "$base"
rm -f "$f"
done
- name : Compress artifacts (macOS dual-format)
if : startsWith(matrix.os, 'macos-')
shell : bash
run : |
shopt -s nullglob
for f in artifacts/*; do
# Only process regular files; skip any directories
[ -f "$f" ] || continue
base=$(basename "$f")
# .zst (size-optimized)
zstd -T0 -19 --force -o "artifacts/${base}.zst" "$f"
# .tar.gz fallback for users without zstd
tar -C artifacts -czf "artifacts/${base}.tar.gz" "$base"
rm -f "$f"
done
- name : Upload binaries (compressed)
uses : actions/upload-artifact@v4
with :
name : binaries-${{ matrix.target }}
path : artifacts/
compression-level : 0
- name : Upload cargo timings
uses : actions/upload-artifact@v4
with :
name : cargo-timings-${{ matrix.target }}
path : code-rs/target/cargo-timings/*.html
if-no-files-found : ignore
compression-level : 0
release :
name : Publish to npm
needs : [ determine-version, build-binaries, preflight-tests]
runs-on : ubuntu-latest
if : "!contains(github.event.head_commit.message, '[skip ci]')"
timeout-minutes : 30
env :
NPM_TOKEN : ${{ secrets.NPM_TOKEN }}
OPENAI_API_KEY : ${{ secrets.OPENAI_API_KEY }}
steps :
- name : Checkout code
uses : actions/checkout@v4
with :
fetch-depth : 0
token : ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
- name : Setup Node.js
uses : actions/setup-node@v4
with :
node-version : '20'
registry-url : 'https://registry.npmjs.org'
- name : Start local OpenAI proxy for release (hardened)
if : env.OPENAI_API_KEY != ''
env :
OPENAI_API_KEY : ${{ secrets.OPENAI_API_KEY }}
run : |
set -euo pipefail
mkdir -p .github/auto
PORT=5058 LOG_DEST=stdout EXIT_ON_5XX=1 RESPONSES_BETA="responses=v1" node scripts/openai-proxy.js > .github/auto/openai-proxy.log 2>&1 &
for i in {1..30}; do if nc -z 127.0.0.1 5058; then break; else sleep 0.2; fi; done || true
- name : Download all artifacts
uses : actions/download-artifact@v4
with :
path : artifacts/
- name : Prepare release assets
shell : bash
run : |
set -euo pipefail
mkdir -p release-assets
shopt -s nullglob
# Gather all built "code-*" files (zst/tar.gz/zip or raw) recursively under artifacts/
while IFS= read -r -d '' f; do
cp "$f" release-assets/
done < <(find artifacts -type f -name 'code-*' -print0)
# Show what we collected
ls -la release-assets/ || true
- name : Build per-target npm binary packages
shell : bash
env :
NEW_VERSION : ${{ needs.determine-version.outputs.version }}
run : |
set -euo pipefail
stage_root="npm-binaries"
rm -rf "$stage_root" && mkdir -p "$stage_root"
map_target() {
case "$1" in
aarch64-apple-darwin) echo "os=darwin cpu=arm64 pkg=@just-every/code-darwin-arm64" ;;
x86_64-apple-darwin) echo "os=darwin cpu=x64 pkg=@just-every/code-darwin-x64" ;;
x86_64-unknown-linux-musl) echo "os=linux cpu=x64 pkg=@just-every/code-linux-x64-musl" ;;
aarch64-unknown-linux-musl) echo "os=linux cpu=arm64 pkg=@just-every/code-linux-arm64-musl" ;;
x86_64-pc-windows-msvc.exe) echo "os=win32 cpu=x64 pkg=@just-every/code-win32-x64" ;;
*) echo "" ;;
esac
}
decompress_to() {
local input="$1" outdir="$2" base
base="$(basename "$input")"
mkdir -p "$outdir"
case "$base" in
*.zst)
# decompress to raw binary
local raw="${outdir}/${base%.zst}"
zstd -d -q --force "$input" -o "$raw"
echo "$raw"
;;
*.tar.gz)
tar -xzf "$input" -C "$outdir"
echo "$outdir/${base%.tar.gz}"
;;
*.zip)
unzip -q -o "$input" -d "$outdir"
echo "$outdir/${base%.zip}"
;;
*)
# already raw
cp "$input" "$outdir/"
echo "$outdir/$base"
;;
esac
}
for f in release-assets/*; do
b="$(basename "$f")"
# Derive target triple from the filename while preserving optional .exe (Windows):
# code-<triple>[.exe].(zst|tar.gz|zip) -> <triple>[.exe]
case "$b" in
*.tar.gz) base="${b%.tar.gz}" ;;
*.zst) base="${b%.zst}" ;;
*.zip) base="${b%.zip}" ;;
*) base="$b" ;;
esac
t="${base#code-}"
meta="$(map_target "$t")" || true
if [ -z "$meta" ]; then
echo "Skipping unrecognized artifact: $b"
continue
fi
eval "$meta" # sets os, cpu, pkg
workdir="$stage_root/${pkg##*/}"
rm -rf "$workdir" && mkdir -p "$workdir/bin"
rawbin="$(decompress_to "$f" "$workdir/bin")"
chmod +x "$rawbin" || true
# package.json
printf '{\n "name": "%s",\n "version": "%s",\n "license": "Apache-2.0",\n "description": "Platform binary for @just-every/code (%s)",\n "os": ["%s"],\n "cpu": ["%s"],\n "files": ["bin/"],\n "private": false\n}\n' \
"$pkg" "$NEW_VERSION" "$t" "$os" "$cpu" > "$workdir/package.json"
done
ls -R "$stage_root"
- name : Publish per-target npm binary packages (disabled; moved to end)
if : ${{ false }}
env :
NODE_AUTH_TOKEN : ${{ env.NPM_TOKEN }}
shell : bash
run : |
set -euo pipefail
for dir in npm-binaries/*; do
echo "Publishing $(basename "$dir")"
(cd "$dir" && npm publish --access public)
done
- name : Sync README for npm
shell : bash
run : cp README.md codex-cli/README.md
- name : Update package.json version
id : version
working-directory : codex-cli
shell : bash
run : |
git config --local user.email "[email protected] "
git config --local user.name "GitHub Action"
NEW_VERSION="${{ needs.determine-version.outputs.version }}"
npm version "$NEW_VERSION" --no-git-tag-version --allow-same-version
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
# Inject optionalDependencies pointing to per-target packages
# Note: These are platform-filtered in the target packages, so npm will fetch only one.
tmp=$(mktemp)
jq '.optionalDependencies = {
"@just-every/code-darwin-arm64": "'"$NEW_VERSION"'",
"@just-every/code-darwin-x64": "'"$NEW_VERSION"'",
"@just-every/code-linux-x64-musl": "'"$NEW_VERSION"'",
"@just-every/code-linux-arm64-musl": "'"$NEW_VERSION"'",
"@just-every/code-win32-x64": "'"$NEW_VERSION"'"
}' package.json > "$tmp" && mv "$tmp" package.json
git add package.json
if git diff --staged --quiet; then
echo "skip_push=true" >> "$GITHUB_OUTPUT"
else
git commit -m "chore(release): ${NEW_VERSION} [skip ci]"
echo "skip_push=false" >> "$GITHUB_OUTPUT"
fi
if ! git rev-parse "v${NEW_VERSION}" >/dev/null 2>&1; then
git tag "v${NEW_VERSION}"
fi
echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT"
- name : Verify ESM bin integrity (no require in coder.js)
shell : bash
working-directory : codex-cli
run : |
set -euo pipefail
tgz=$(npm pack --json | jq -r '.[0].filename')
mkdir -p .pack && tar -xzf "$tgz" -C .pack
BIN=".pack/package/bin/coder.js"
test -f "$BIN" || { echo "coder.js missing in packed tarball" >&2; exit 1; }
if grep -q "require(\"fs\").createWriteStream" "$BIN"; then
echo "Found CommonJS require in coder.js; expected ESM. Failing." >&2
sed -n '1,60p' "$BIN" >&2 || true
exit 1
fi
# Sanity: ensure ESM import of fs functions exists
if ! grep -q "import .*from \"fs\"" "$BIN"; then
echo "Missing ESM import from fs in coder.js; unexpected content." >&2
sed -n '1,80p' "$BIN" >&2 || true
exit 1
fi
echo "ESM integrity OK for coder.js"
# Generate CHANGELOG and release notes by running our own `code` CLI in headless mode.
# It will:
# - Review changes between the previous tag and the new version
# - Update CHANGELOG.md with a new section for vNEW_VERSION
# - Write rich release notes to release-notes/RELEASE_NOTES.md
- name : Generate CHANGELOG + release notes (Code)
if : env.OPENAI_API_KEY != ''
shell : bash
env :
NEW_VERSION : ${{ needs.determine-version.outputs.version }}
run : |
set -euo pipefail
git config --local user.email "[email protected] "
git config --local user.name "GitHub Action"
# Determine previous tag (the most recent tag before the new one).
# If none exists, use the initial commit as the base.
PREV_TAG=$(git tag --sort=-v:refname | sed -n '2p' || true)
if [ -z "$PREV_TAG" ]; then
BASE=$(git rev-list --max-parents=0 HEAD | tail -n1)
RANGE="$BASE..HEAD"
else
RANGE="$PREV_TAG..HEAD"
fi
echo "Previous tag: ${PREV_TAG:-<none>}"
echo "Range: $RANGE"
# Extract a Linux x86_64 Code binary from artifacts and make it runnable.
mkdir -p .code-bin release-notes
LNX_ZST=$(ls -1 release-assets/code-x86_64-unknown-linux-musl.* 2>/dev/null | head -n1 || true)
if [ -z "$LNX_ZST" ]; then
echo "Could not find linux x86_64 Code artifact in release-assets/" >&2
exit 1
fi
case "$LNX_ZST" in
*.zst)
zstd -d -q --force "$LNX_ZST" -o .code-bin/code ;;
*.tar.gz)
tar -xzf "$LNX_ZST" -C .code-bin && mv .code-bin/code-x86_64-unknown-linux-musl .code-bin/code ;;
*.zip)
unzip -q -o "$LNX_ZST" -d .code-bin && mv .code-bin/code-x86_64-unknown-linux-musl .code-bin/code ;;
*)
cp "$LNX_ZST" .code-bin/code ;;
esac
chmod +x .code-bin/code
# Prepare context for the model.
DATE=$(date -u +%Y-%m-%d)
echo "# Commit log ($RANGE)" > release-notes/context.md
git log --no-color --format='* %h %s (%an)' --abbrev=8 --no-merges $RANGE >> release-notes/context.md || true
# Build the task prompt (with variables expanded by bash).
cat > release-notes/prompt.expanded.txt <<PROMPT
You are Code running headless in CI to prepare a new release.
Inputs
- Version: v${NEW_VERSION}
- Date (UTC): ${DATE}
- Previous tag: ${PREV_TAG:-none}
- Commit range: ${RANGE}
- Working directory: repo root (CHANGELOG.md lives at top-level)
Primary Tasks
1) Update CHANGELOG.md with a new entry for this version using the EXACT house style below.
2) Generate GitHub release notes at release-notes/RELEASE_NOTES.md (concise, user‑ facing), derived from the same changes.
CHANGELOG.md House Style (strict)
- File header stays as-is ("Changelog"). Do not rewrite older sections.
- Insert the new section at the top (above previous versions), with this header format exactly:
## [${NEW_VERSION}] - ${DATE}
- Include 2– 5 bullets (no more, no fewer), each a single line, focusing on user-visible features, fixes, UX, performance, or stability.
- Keep bullets concise and scannable; avoid long prose. Use present tense.
- When helpful, start bullets with a short scope label like "TUI:", "CLI:", or "Core:".
- At the end of each bullet, include abbreviated commit SHA(s) in parentheses, using 7– 8 hex chars, comma‑ separated when multiple, like: (abc1234, def5678).
- Map changes from the git commit log in ${RANGE}; ignore pure chores/merges unless user‑ visible.
- Do NOT add links, tables, code blocks, or subheadings. Do NOT include PR author attributions in the changelog.
- Do NOT add any extra headers inside the changelog entry; only bullets under the version header.
- Idempotent: if a section for ${NEW_VERSION} already exists, replace only that section’ s body with the newly generated bullets and keep the header intact.
Release Notes (release-notes/RELEASE_NOTES.md)
- Write exactly these sections in order; include the optional Thanks section only when applicable:
1) Title: ## @just-every/code v${NEW_VERSION}
2) One brief intro sentence (1– 2 lines max).
3) Section header: ### Changes
- The same 2– 5 bullets as in the changelog (you may omit SHAs).
4) Section header: ### Install
Code block with exactly:
npm install -g @just-every/code@latest
code
5) Optional section header: ### Thanks
- Include ONE line like: "Thanks to @alice and @bob for contributions!"
- Only include if at least one merged PR in ${RANGE} is authored by an external contributor.
- External contributors are any GitHub users other than: @zemaj, @andrej-griniuk, and NOT upstream contributors.
- Treat a username as upstream if it matches the regex /-oai$/i, or clearly belongs to the upstream org (e.g., OpenAI maintainers). Exclude such users from Thanks.
- Derive usernames from merge commits, Co-authored-by trailers, or PR references in commit messages. Deduplicate and prefer "@username" form.
- Keep notes concise; no walls of text. Do not add any other sections beyond the optional Thanks.
- Optional final line (only if a previous tag exists):
Compare: https://github.com/${GITHUB_REPOSITORY}/compare/${PREV_TAG}...v${NEW_VERSION}
Rules
- Use the provided git log as source of truth; summarize responsibly.
- Explore codebase directly if commit messages are unclear or need additional context.
- Keep formatting minimal (headers + list bullets). No emojis ever! Basic markdown only.
- Never reorder older versions. Only touch the section for v${NEW_VERSION}.
- After writing files, stage and commit with message: docs(changelog): update for v${NEW_VERSION} [skip ci]
Context (git log excerpt follows):
PROMPT
cat release-notes/context.md >> release-notes/prompt.expanded.txt
# Run Code in fully automated exec mode against the repo root.
# Note: the working-directory flag is `--cd` on the `exec` subcommand.
OPENAI_BASE_URL="http://127.0.0.1:5058/v1" \
./.code-bin/code exec --cd "$GITHUB_WORKSPACE" --full-auto --skip-git-repo-check < release-notes/prompt.expanded.txt | tee .github/auto/RELEASE_AGENT_OUT.txt || {
echo "Code exec returned non-zero; continuing to check for outputs..." >&2
}
# Assert no fatal streaming/server errors if proxy in use
if [ -s .github/auto/RELEASE_AGENT_OUT.txt ]; then
if rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/RELEASE_AGENT_OUT.txt >/dev/null 2>&1; then
echo "Agent reported a fatal error (stream/server). Failing job." >&2
rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/RELEASE_AGENT_OUT.txt || true
exit 1
fi
fi
# Post-process: scrub upstream contributors from the Thanks section (e.g., usernames ending with -oai)
if [ -s release-notes/RELEASE_NOTES.md ]; then
node - <<'JS'
const fs = require('fs');
const p = 'release-notes/RELEASE_NOTES.md';
if (!fs.existsSync(p)) process.exit(0);
const src = fs.readFileSync(p,'utf8');
const lines = src.split(/\r?\n/);
const start = lines.findIndex(l => /^###\s+Thanks\s*$/i.test(l));
if (start === -1) process.exit(0);
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) { if (/^###\s+/.test(lines[i])) { end = i; break; } }
const body = lines.slice(start + 1, end).join(' ');
const seen = new Set();
const keep = [];
for (const m of body.matchAll(/@([A-Za-z0-9](?:[A-Za-z0-9_-]{0,38}))/g)) {
const u = m[1];
const uname = '@' + u;
if (seen.has(uname)) continue;
seen.add(uname);
const lower = u.toLowerCase();
// Local maintainers we never thank in Thanks
if (lower === 'zemaj' || lower === 'andrej-griniuk') continue;
// Exclude upstream-style usernames: suffix -oai (case-insensitive)
if (/-oai$/i.test(u)) continue;
keep.push(uname);
}
const out = lines.slice();
if (keep.length === 0) {
// Remove entire Thanks section
out.splice(start, end - start);
} else {
const msg = `Thanks to ${keep.join(' and ')} for contributions!`;
out.splice(start + 1, end - (start + 1), msg);
}
fs.writeFileSync(p, out.join('\n'));
JS
fi
# If the agent forgot to write release notes, synthesize a minimal one from CHANGELOG.
if [ ! -s release-notes/RELEASE_NOTES.md ]; then
awk -v v="${NEW_VERSION}" '/^## /{p=($2==("v"v)||$2==v)} p{print}' CHANGELOG.md > release-notes/RELEASE_NOTES.md || true
fi
# Commit CHANGELOG changes if any.
if ! git diff --quiet -- CHANGELOG.md; then
git add CHANGELOG.md release-notes/RELEASE_NOTES.md || true
git commit -m "docs(changelog): update for v${NEW_VERSION} [skip ci]" || true
fi
- name : Enforce [skip ci] on notes-only commit
shell : bash
run : |
set -euo pipefail
# If HEAD only touched notes files and lacks [skip ci], amend the subject to include it.
mapfile -t files < <(git show --name-only --pretty="" HEAD)
notes_only=true
for f in "${files[@]}"; do
[ -z "$f" ] && continue
case "$f" in
CHANGELOG.md|release-notes/RELEASE_NOTES.md) ;; # allowed
*) notes_only=false ;;
esac
done
if [ "$notes_only" = true ]; then
subj=$(git log -1 --pretty=%s || true)
if ! grep -q '\[skip ci\]' <<<"$subj"; then
body=$(git log -1 --pretty=%b || true)
git commit --amend -m "${subj} [skip ci]" -m "$body"
fi
fi
- name : Fallback release notes from CHANGELOG (no OPENAI_API_KEY)
if : env.OPENAI_API_KEY == ''
shell : bash
env :
NEW_VERSION : ${{ needs.determine-version.outputs.version }}
run : |
set -euo pipefail
mkdir -p release-notes
if [ -f CHANGELOG.md ]; then
awk -v v="${NEW_VERSION}" '/^## /{p=($2==("v"v)||$2==v)} p{print}' CHANGELOG.md > release-notes/RELEASE_NOTES.md || true
fi
if [ ! -s release-notes/RELEASE_NOTES.md ]; then
printf "## @just-every/code v%s\n\nSee CHANGELOG.md for details." "${NEW_VERSION}" > release-notes/RELEASE_NOTES.md
fi
- name : Assert release notes exist
shell : bash
run : |
test -s release-notes/RELEASE_NOTES.md || { echo "release-notes/RELEASE_NOTES.md missing" >&2; exit 1; }
- name : Check if we should publish
id : should_publish
shell : bash
run : |
if [ -z "${{ secrets.NPM_TOKEN }}" ]; then
echo "publish=false" >> "$GITHUB_OUTPUT"
else
echo "publish=true" >> "$GITHUB_OUTPUT"
fi
# Defer npm publish until after pushing changes and tags; platform packages first
- name : Push tag
shell : bash
run : git push origin "v${{ steps.version.outputs.version }}" || true
- name : Push changes (fast-forward or policy merge)
if : steps.version.outputs.skip_push != 'true'
shell : bash
run : |
set -euo pipefail
# First attempt a simple push
if git push origin main; then
git push origin --tags || true
exit 0
fi
echo "Push rejected (non-fast-forward). Attempting fetch + fast-forward..."
git fetch origin --tags
# Try fast-forward only; succeeds when our HEAD is descendant of origin/main
if git merge --ff-only origin/main; then
echo "Fast-forward merge succeeded; retrying push."
git push origin main
git push origin --tags || true
exit 0
fi
echo "Non-FF merge required; applying merge-with-policy fallback."
# Non-interactive merge that prefers our current changes, but adopts remote for workflow files.
git merge --no-ff --no-commit origin/main || true
if git rev-parse -q --verify MERGE_HEAD >/dev/null; then
# Default to ours for all files
git checkout --ours . || true
# Adopt remote version for workflow files (release-ignore path)
# Only adjust if those files are in conflict/unmerged
while IFS= read -r f; do
[ -z "$f" ] && continue
case "$f" in
.github/workflows/*) git checkout --theirs -- "$f" || true ;;
esac
done < <(git ls-files --unmerged | cut -f2 | sort -u)
git add -A
git commit -m "Merge origin/main: adopt remote workflow changes; keep release changes [skip ci]"
fi
echo "Retrying push after policy merge..."
git push origin main
git push origin --tags || true
- name : Create GitHub Release
uses : softprops/action-gh-release@v1
with :
tag_name : v${{ steps.version.outputs.version }}
name : Release v${{ steps.version.outputs.version }}
body_path : release-notes/RELEASE_NOTES.md
files : |
release-assets/*
env :
GITHUB_TOKEN : ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
# --- Homebrew Tap Update -------------------------------------------------
- name : Generate Homebrew formula (Code.rb)
shell : bash
run : |
set -euo pipefail
bash scripts/generate-homebrew-formula.sh
test -s Formula/Code.rb || { echo "Formula/Code.rb missing" >&2; exit 1; }
- name : Publish Homebrew formula to just-every/homebrew-tap
shell : bash
env :
GH_PAT : ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
run : |
set -euo pipefail
if [ -z "${GH_PAT:-}" ]; then
echo "GH_PAT/GITHUB_TOKEN not available; skipping tap publish" >&2
exit 0
fi
tmpdir=$(mktemp -d)
git clone "https://x-access-token:${GH_PAT}@github.com/just-every/homebrew-tap.git" "$tmpdir"
mkdir -p "$tmpdir/Formula"
cp -f Formula/Code.rb "$tmpdir/Formula/Code.rb"
cd "$tmpdir"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git add -A
if git diff --staged --quiet; then
echo "No tap changes to commit." >&2
exit 0
fi
ver="${{ steps.version.outputs.version }}"
git commit -m "chore(homebrew): bump Code formula to v${ver}"
# Ensure default branch exists
if ! git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then
git branch -M main || true
fi
git push -u origin HEAD:main
- name : Publish per-target npm binary packages (last)
if : ${{ env.NPM_TOKEN != '' }}
env :
NODE_AUTH_TOKEN : ${{ env.NPM_TOKEN }}
shell : bash
run : |
set -euo pipefail
shopt -s nullglob
for dir in npm-binaries/*; do
name=$(jq -r '.name' "$dir/package.json")
version=$(jq -r '.version' "$dir/package.json")
echo "Preparing to publish $name@$version"
existing=$(npm view "$name@$version" version 2>/dev/null || true)
if [ "$existing" = "$version" ]; then
echo "Skip: $name@$version already published"
continue
fi
echo "Publishing $(basename \"$dir\")"
(cd "$dir" && npm publish --access public)
done
- name : Publish main npm package (last)
if : steps.should_publish.outputs.publish == 'true'
working-directory : codex-cli
env :
NODE_AUTH_TOKEN : ${{ secrets.NPM_TOKEN }}
shell : bash
run : |
set -euo pipefail
name="@just-every/code"
version=$(jq -r '.version' package.json)
echo "Preparing to publish $name@$version"
existing=$(npm view "$name@$version" version 2>/dev/null || true)
if [ "$existing" = "$version" ]; then
echo "Skip: $name@$version already published"
exit 0
fi
npm publish --access public