mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
ci(release): add preflight E2E tests and per-target post-build smokes; fix duplicate workflow block
This commit is contained in:
@@ -870,822 +870,3 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
npm publish --access public
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/**'
|
||||
- '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:
|
||||
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.89.0 --profile minimal --target ${{ matrix.target }}
|
||||
rustup default 1.89.0
|
||||
|
||||
# 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: codex-${{ matrix.target }}-toolchain-1.89
|
||||
workspaces: |
|
||||
codex-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: codex-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 codex-rs
|
||||
cargo build --release --frozen --locked --timings --target ${{ matrix.target }} --bin code
|
||||
|
||||
- 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 codex-rs/target/${{ matrix.target }}/release/code.exe artifacts/${{ matrix.artifact }}
|
||||
else
|
||||
cp codex-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: codex-rs/target/cargo-timings/*.html
|
||||
if-no-files-found: ignore
|
||||
compression-level: 0
|
||||
|
||||
release:
|
||||
name: Publish to npm
|
||||
needs: [determine-version, build-binaries]
|
||||
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: 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"
|
||||
|
||||
# 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.
|
||||
./.code-bin/code exec --cd "$GITHUB_WORKSPACE" --full-auto --skip-git-repo-check < release-notes/prompt.expanded.txt || {
|
||||
echo "Code exec returned non-zero; continuing to check for outputs..." >&2
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "[ci-tests] Running curated integration tests..."
|
||||
pushd codex-rs >/dev/null
|
||||
|
||||
cargo test -p codex-login --test all -q
|
||||
cargo test -p codex-chatgpt --test all -q
|
||||
cargo test -p codex-apply-patch --test all -q
|
||||
cargo test -p codex-execpolicy --tests -q
|
||||
cargo test -p mcp-types --tests -q
|
||||
|
||||
popd >/dev/null
|
||||
|
||||
echo "[ci-tests] CLI smokes with host binary..."
|
||||
BIN=./codex-rs/target/dev-fast/code
|
||||
"${BIN}" --version >/dev/null
|
||||
"${BIN}" completion bash >/dev/null
|
||||
"${BIN}" doctor >/dev/null || true
|
||||
|
||||
echo "[ci-tests] Done."
|
||||
Reference in New Issue
Block a user