Skip to content

Merge pull request #144 from cbusillo/fix/homebrew-link-canonical-cle… #58

Merge pull request #144 from cbusillo/fix/homebrew-link-canonical-cle…

Merge pull request #144 from cbusillo/fix/homebrew-link-canonical-cle… #58

Workflow file for this run

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'
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
id-token: write
issues: write
pull-requests: write
statuses: write
jobs:
preflight-tests:
name: Preflight Tests (Linux fast E2E)
runs-on: ubuntu-24.04
env:
CARGO_HOME: ${{ github.workspace }}/.cargo-home
CARGO_TARGET_DIR: ${{ github.workspace }}/.cargo-target
RUSTUP_HOME: ${{ github.workspace }}/.rustup-home
steps:
- name: Prepare cargo target dir on data disk
shell: bash
run: |
set -euo pipefail
mkdir -p "$CARGO_TARGET_DIR"
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read Rust toolchain channel
id: rust_toolchain
shell: bash
run: |
set -euo pipefail
TOOLCHAIN=$(python3 -c "import sys, pathlib; p=pathlib.Path('code-rs/rust-toolchain.toml').read_text();
try:
import tomllib as tl
except ModuleNotFoundError:
import tomli as tl
print(tl.loads(p)['toolchain']['channel'])")
echo "channel=$TOOLCHAIN" >> "$GITHUB_OUTPUT"
echo "RUST_TOOLCHAIN=$TOOLCHAIN" >> "$GITHUB_ENV"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ steps.rust_toolchain.outputs.channel }}
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: v5-rust
shared-key: code-preflight-${{ steps.rust_toolchain.outputs.channel }}
workspaces: |
code-rs -> target
cache-targets: false
cache-workspace-crates: true
cache-on-failure: false
- name: Build CLI (dev-fast)
shell: bash
run: |
set -euo pipefail
cd code-rs
cargo build --locked --profile dev-fast --bin code
- name: CLI smokes (skip duplicated cargo tests)
shell: bash
env:
SKIP_CARGO_TESTS: "1"
CI_CLI_BIN: ${{ env.CARGO_TARGET_DIR }}/dev-fast/code
run: bash scripts/ci-tests.sh
- name: Drop dev-fast artifacts before workspace tests
shell: bash
run: |
set -euo pipefail
rm -rf "$CARGO_TARGET_DIR"/dev-fast || true
df -h
- name: Install cargo-nextest
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest
- name: Workspace tests (nextest)
shell: bash
run: |
cd code-rs
cargo nextest run --no-fail-fast --locked
- name: Free disk after tests
if: always()
shell: bash
run: |
echo "Disk usage before cleanup" && df -h
rm -rf ~/.cache/sccache || true
rm -rf "$CARGO_TARGET_DIR" || true
echo "Disk usage after cleanup" && df -h
determine-version:
name: Determine Version
runs-on: [self-hosted, Linux, X64, chris-testing]
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'
- name: Determine next GitHub Release version
id: version
working-directory: codex-cli
shell: bash
run: |
set -euo pipefail
CURRENT_VERSION=$(node -p "require('./package.json').version")
latest_tag=$(git tag --list 'v*' --sort=-v:refname | head -n 1 | sed 's/^v//' || true)
latest_tag="${latest_tag:-0.0.0}"
CANDIDATE=$(printf '%s\n%s\n' "$CURRENT_VERSION" "$latest_tag" | sort -V | tail -n1)
if git rev-parse "v${CANDIDATE}" >/dev/null 2>&1; then
IFS='.' read -ra V <<< "$CANDIDATE"
CANDIDATE="${V[0]}.${V[1]}.$((${V[2]} + 1))"
fi
while git rev-parse "v${CANDIDATE}" >/dev/null 2>&1; do
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: ${{ fromJson(matrix.runs_on) }}
env:
CARGO_HOME: ${{ github.workspace }}/.cargo-home
RUSTUP_HOME: ${{ github.workspace }}/.rustup-home
strategy:
matrix:
include:
# Linux builds
- os: ubuntu-24.04
runs_on: '["ubuntu-24.04"]'
target: x86_64-unknown-linux-musl
artifact: code-x86_64-unknown-linux-musl
- os: ubuntu-24.04-arm
runs_on: '["ubuntu-24.04-arm"]'
target: aarch64-unknown-linux-musl
artifact: code-aarch64-unknown-linux-musl
# GNU variants omitted to reduce asset duplication.
# macOS builds
- os: macos-14
runs_on: '["macos-14"]'
target: x86_64-apple-darwin
artifact: code-x86_64-apple-darwin
- os: macos-14
runs_on: '["macos-14"]'
target: aarch64-apple-darwin
artifact: code-aarch64-apple-darwin
# Windows build
- os: windows-latest
runs_on: '["windows-latest"]'
target: x86_64-pc-windows-msvc
artifact: code-x86_64-pc-windows-msvc.exe
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read Rust toolchain channel
id: rust_toolchain
shell: bash
run: |
set -euo pipefail
TOOLCHAIN=$(python3 -c "import sys, pathlib; p=pathlib.Path('code-rs/rust-toolchain.toml').read_text();
try:
import tomllib as tl
except ModuleNotFoundError:
import tomli as tl
print(tl.loads(p)['toolchain']['channel'])")
echo "channel=$TOOLCHAIN" >> "$GITHUB_OUTPUT"
echo "RUST_TOOLCHAIN=$TOOLCHAIN" >> "$GITHUB_ENV"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ steps.rust_toolchain.outputs.channel }}
targets: ${{ 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-${{ steps.rust_toolchain.outputs.channel }}
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/sccache-action@v0.0.9
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: runner.os == 'Linux' && contains(matrix.target, 'gnu')
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
SUDO=(sudo)
elif [[ ${EUID:-$(id -u)} -eq 0 ]]; then
SUDO=()
else
echo "apt-get is available but neither sudo nor root is available; using preinstalled GNU build packages." >&2
SUDO=(false)
fi
if [[ ${SUDO[0]:-} != false ]]; then
"${SUDO[@]}" apt-get update || true
"${SUDO[@]}" apt-get install -y libssl-dev pkg-config mold || true
fi
fi
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: runner.os == 'Linux' && contains(matrix.target, 'musl')
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1 && ! command -v musl-gcc >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
SUDO=(sudo)
elif [[ ${EUID:-$(id -u)} -eq 0 ]]; then
SUDO=()
else
echo "musl-gcc is required but this runner cannot install packages without sudo/root." >&2
exit 1
fi
"${SUDO[@]}" apt-get update
"${SUDO[@]}" apt-get install -y musl-tools pkg-config
fi
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: |
(runner.os == 'Linux' && 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: Install zstd (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
if command -v zstd >/dev/null 2>&1; then
exit 0
fi
if command -v sudo >/dev/null 2>&1; then
SUDO=(sudo)
elif [[ ${EUID:-$(id -u)} -eq 0 ]]; then
SUDO=()
else
echo "zstd is required but this runner cannot install packages without sudo/root." >&2
exit 1
fi
"${SUDO[@]}" apt-get update -qq
"${SUDO[@]}" apt-get install -y zstd
- name: Compress artifacts (Linux dual-format)
if: runner.os == 'Linux'
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 GitHub Release
needs: [determine-version, build-binaries, preflight-tests]
runs-on: [self-hosted, Linux, X64, chris-testing]
if: "!contains(github.event.head_commit.message, '[skip ci]')"
timeout-minutes: 30
env:
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'
- 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: Update package.json version
id: version
working-directory: codex-cli
shell: bash
run: |
git config --local user.email "action@github.com"
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"
git add package.json
if git diff --staged --quiet; then
echo "skip_push=true" >> "$GITHUB_OUTPUT"
else
git commit -m "chore(release): ${NEW_VERSION}"
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"
# 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 docs/release-notes/RELEASE_NOTES.md
- name: Normalize release assets for changelog generation
if: runner.os == 'Linux' && env.OPENAI_API_KEY != ''
shell: bash
run: |
set -euo pipefail
if command -v zstd >/dev/null 2>&1; then
exit 0
fi
shopt -s nullglob
for zst in release-assets/*.zst; do
fallback="${zst%.zst}.tar.gz"
if [ -f "$fallback" ]; then
echo "zstd is unavailable; using fallback $(basename "$fallback") for changelog generation"
rm -f "$zst"
else
echo "zstd is unavailable and no fallback exists for $(basename "$zst")" >&2
exit 1
fi
done
- 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 "action@github.com"
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 --list 'v*' --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"
if [ -f CHANGELOG.md ] && [ -f docs/release-notes/RELEASE_NOTES.md ] \
&& grep -qF "## [${NEW_VERSION}]" CHANGELOG.md 2>/dev/null \
&& grep -qF "## @just-every/code v${NEW_VERSION}" docs/release-notes/RELEASE_NOTES.md 2>/dev/null; then
echo "CHANGELOG.md and docs/release-notes/RELEASE_NOTES.md already contain v${NEW_VERSION}; skipping regeneration."
exit 0
fi
# Extract a Linux x86_64 Code binary from artifacts and make it runnable.
mkdir -p .code-bin docs/release-notes
: > docs/release-notes/RELEASE_NOTES.md
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)" > docs/release-notes/context.md
git log --no-color --format='* %h %s (%an)' --abbrev=8 --no-merges $RANGE >> docs/release-notes/context.md || true
# Build the task prompt (with variables expanded by bash).
cat > docs/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 docs/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 (docs/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:
gh release download v${NEW_VERSION} --repo ${GITHUB_REPOSITORY}
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}
Context (git log excerpt follows):
PROMPT
cat docs/release-notes/context.md >> docs/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 < docs/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 docs/release-notes/RELEASE_NOTES.md ]; then
node - <<'JS'
const fs = require('fs');
const p = 'docs/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 docs/release-notes/RELEASE_NOTES.md ]; then
awk -v v="${NEW_VERSION}" '/^## /{p=($2==("v"v)||$2==v)} p{print}' CHANGELOG.md > docs/release-notes/RELEASE_NOTES.md || true
fi
if [ ! -s docs/release-notes/RELEASE_NOTES.md ]; then
printf "## @just-every/code v%s\n\nSee CHANGELOG.md for details." "${NEW_VERSION}" > docs/release-notes/RELEASE_NOTES.md
fi
# Commit CHANGELOG changes if any.
if ! git diff --quiet -- CHANGELOG.md; then
git add CHANGELOG.md docs/release-notes/RELEASE_NOTES.md || true
git commit -m "docs(changelog): update for v${NEW_VERSION}" || true
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 docs/release-notes
if [ -f CHANGELOG.md ]; then
awk -v v="${NEW_VERSION}" '/^## /{p=($2==("v"v)||$2==v)} p{print}' CHANGELOG.md > docs/release-notes/RELEASE_NOTES.md || true
fi
if [ ! -s docs/release-notes/RELEASE_NOTES.md ]; then
printf "## @just-every/code v%s\n\nSee CHANGELOG.md for details." "${NEW_VERSION}" > docs/release-notes/RELEASE_NOTES.md
fi
- name: Assert release notes exist
shell: bash
run: |
test -s docs/release-notes/RELEASE_NOTES.md || { echo "docs/release-notes/RELEASE_NOTES.md missing" >&2; exit 1; }
- name: Open release metadata pull request
if: steps.version.outputs.skip_push != 'true'
shell: bash
env:
GH_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
NEW_VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
release_branch="release/v${NEW_VERSION}-${GITHUB_RUN_ID}"
git add codex-cli/package.json CHANGELOG.md docs/release-notes/RELEASE_NOTES.md 2>/dev/null || true
if ! git diff --staged --quiet; then
git commit -m "chore(release): prepare v${NEW_VERSION}"
fi
# Keep the release handoff branch clean; downloaded artifacts and
# model context files are recreated by each release run.
rm -rf .code-bin .github/auto artifacts release-assets docs/release-notes/context.md docs/release-notes/prompt.expanded.txt || true
git switch -c "$release_branch"
git push -u origin "$release_branch"
if gh pr create \
--base main \
--head "$release_branch" \
--title "chore(release): prepare v${NEW_VERSION}" \
--body "This automated release PR contains the version and release-notes changes for v${NEW_VERSION}. Merge this PR to let the protected main-branch Release workflow create the tag and GitHub Release assets from main."; then
:
else
echo "::warning::GitHub Actions could not create the release PR; create it from branch ${release_branch}."
gh pr view "$release_branch" --json url --jq .url || true
fi
echo "Release metadata changes require PR review because main is protected."
echo "Merge the generated PR, then the next main Release run will publish the GitHub Release."
# Defer tag and GitHub Release publication until release metadata has landed on main.
- name: Push tag
if: steps.version.outputs.skip_push == 'true'
shell: bash
run: git push origin "v${{ steps.version.outputs.version }}" || true
- name: Verify release notes header matches version
if: steps.version.outputs.skip_push == 'true'
shell: bash
run: |
set -euo pipefail
scripts/check-release-notes-version.sh
- name: Generate update manifest
if: steps.version.outputs.skip_push == 'true'
shell: bash
env:
NEW_VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
scripts/release/generate-update-manifest.sh \
--version "$NEW_VERSION" \
--commit "$GITHUB_SHA" \
--repository "$GITHUB_REPOSITORY" \
--assets-dir release-assets \
--output release-assets/update-manifest.json
- name: Create GitHub Release
if: steps.version.outputs.skip_push == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.version.outputs.version }}
name: Release v${{ steps.version.outputs.version }}
body_path: docs/release-notes/RELEASE_NOTES.md
files: |
release-assets/*
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}