Files
bench/release.sh
T
istosandClaude Fable 5 74adc09566 Merge origin/main into task/15: releases meet the AGENTS.md world and the LICENSE
Resolutions honouring both sides:
- CLAUDE.md: main's pointer form wins; this branch's release-era wording
  (tree diagram line, Updating section) ported into AGENTS.md where the
  brief now lives.
- update.sh: this branch's release-based rewrite wins; main's additions
  to the old file list (AGENTS.md from task 13, LICENSE) are carried by
  the release-manifest instead: added copy AGENTS.md and copy LICENSE.
- Starter local/: manifest and release.sh now seed the split pair
  (AGENTS.md notes + pointer CLAUDE.md) mirroring task 13's layout;
  artifact test updated to match.
- tests/test_update_round_trip.py (main's, aimed at the removed
  git-clone mechanism) retired; its two 13-specific intents ported into
  test_update_from_release.py as
  test_agents_brief_replaces_an_old_vendor_named_copy, its third already
  covered there. 147 tests green.
- release.sh's lost executable bit restored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 08:15:28 +02:00

162 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build and publish a bench release from this repo.
#
# ./release.sh # tag v<VERSION> and publish
# ./release.sh --tarball <out> [--source o/r] # build the artifact only
#
# The artifact is built from manager/core/release-manifest — a curated
# tarball that never contained bench's own cards, local/ content, state,
# tests or .claude/ in the first place. Its contents sit at the tarball
# root and the asset name is stable (bench.tar.gz): both are what the
# README's tokenless releases/latest/download/ one-liner depends on.
#
# Publishing refuses on a dirty tree and on an already-existing tag, so
# tag v<VERSION> always names exactly one committed tree. Notes default
# to the core version; set BENCH_RELEASE_NOTES to say more.
#
# This script is bench-repo tooling — the manifest deliberately leaves it
# out of the artifact, so installs never carry it.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MANIFEST="$ROOT/manager/core/release-manifest"
ASSET="bench.tar.gz"
# The starter manager/local/ notes a fresh install unpacks — bench's own
# repo has its real ones, so these are generated, never copied. AGENTS.md
# is the content; CLAUDE.md is the compatibility pointer, mirroring the
# root pair (task 13).
seed_local_agents_md() {
cat <<'MD'
# Project-specific workflow notes
This file is yours — updates never touch manager/local/. Put here what an
agent or teammate needs that the core doc cannot know: post-merge chores,
what the driver assumes, what each local command is for.
Settings live in manager/local/.env (gitignored); every option and its
default is documented in manager/core/.env.example. A `checks` file here
replaces core/checks as the Focus view's definition-of-done panel.
MD
}
seed_local_claude_md() {
cat <<'MD'
<!-- Compatibility pointer, load-bearing: the project notes live in AGENTS.md
(the vendor-neutral name all coding agents read); this file makes Claude Code
CLIs without native AGENTS.md support load it via the import below. -->
@AGENTS.md
MD
}
# origin's URL as "owner/repo" — what gets stamped into the shipped
# update.sh so a fresh install can update with zero configuration.
source_repo() {
local url
url="$(git -C "$ROOT" remote get-url origin 2>/dev/null)" || return 1
url="${url#ssh://}"
url="${url#git@github.com:}"
url="${url#https://github.com/}"
url="${url#http://github.com/}"
url="${url#github.com/}"
url="${url%.git}"
url="${url%/}"
[ -n "$url" ] || return 1
printf '%s\n' "$url"
}
build_tarball() { # build_tarball <out.tar.gz> <source-repo-or-empty>
local out="$1" source="$2" stage kind path
stage="$(mktemp -d)"
# shellcheck disable=SC2064 — expand $stage now, it is gone by EXIT
trap "rm -rf '$stage'" RETURN
while read -r kind path _; do
case "$kind" in
copy|once)
[ -f "$ROOT/$path" ] || { echo "manifest names a missing file: $path" >&2; return 1; }
mkdir -p "$stage/$(dirname "$path")"
cp "$ROOT/$path" "$stage/$path"
;;
tree)
[ -d "$ROOT/$path" ] || { echo "manifest names a missing directory: $path" >&2; return 1; }
mkdir -p "$stage/$path"
rsync -a --exclude='__pycache__/' --exclude='.DS_Store' \
"$ROOT/$path/" "$stage/$path/"
;;
keep)
mkdir -p "$stage/$path"
touch "$stage/$path/.gitkeep"
;;
seed)
case "$path" in
manager/local/AGENTS.md)
mkdir -p "$stage/$(dirname "$path")"
seed_local_agents_md > "$stage/$path"
;;
manager/local/CLAUDE.md)
mkdir -p "$stage/$(dirname "$path")"
seed_local_claude_md > "$stage/$path"
;;
*) echo "manifest seeds a file this script cannot generate: $path" >&2; return 1;;
esac
;;
''|'#'*) ;;
*) echo "manifest has an unknown entry class: $kind $path" >&2; return 1;;
esac
done < "$MANIFEST"
if [ -n "$source" ]; then
sed "s|^BENCH_SOURCE_DEFAULT=.*|BENCH_SOURCE_DEFAULT=\"$source\"|" \
"$stage/update.sh" > "$stage/update.sh.tmp"
mv "$stage/update.sh.tmp" "$stage/update.sh"
fi
chmod +x "$stage"/start.sh "$stage"/stop.sh "$stage"/update.sh
find "$stage/manager/core/adapters" \( -name run -o -name wire \) \
-exec chmod +x {} + 2>/dev/null || true
# Contents at the tarball root — extraction into .task-manager/ must
# not land one directory too deep. COPYFILE_DISABLE keeps macOS tar
# from smuggling AppleDouble ._* entries into the artifact.
COPYFILE_DISABLE=1 tar -czf "$out" -C "$stage" .
}
# --tarball mode: just build, no git/gh — for tests and inspection.
if [ "${1:-}" = "--tarball" ]; then
out="${2:?usage: ./release.sh --tarball <out.tar.gz> [--source owner/repo]}"
source=""
[ "${3:-}" = "--source" ] && source="${4:?--source needs owner/repo}"
[ -n "$source" ] || source="$(source_repo || true)"
build_tarball "$out" "$source"
echo "Built $out (source stamp: ${source:-none})."
exit 0
fi
# Publish mode from here on: clean committed tree, fresh tag, gh.
version="$(cat "$ROOT/manager/core/VERSION")"
tag="v$version"
if [ -n "$(git -C "$ROOT" status --porcelain)" ]; then
echo "Working tree is dirty — commit (or stash) first; a release must be reproducible from its tag." >&2
exit 1
fi
if git -C "$ROOT" rev-parse -q --verify "refs/tags/$tag" >/dev/null \
|| [ -n "$(git -C "$ROOT" ls-remote --tags origin "refs/tags/$tag" 2>/dev/null)" ]; then
echo "Tag $tag already exists — bump manager/core/VERSION before releasing." >&2
exit 1
fi
source="$(source_repo)" || { echo "No origin remote to publish to." >&2; exit 1; }
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
build_tarball "$tmp/$ASSET" "$source"
git -C "$ROOT" tag "$tag"
git -C "$ROOT" push origin "$tag"
gh release create "$tag" --repo "$source" --verify-tag \
--title "bench $tag" \
--notes "${BENCH_RELEASE_NOTES:-bench core version $version}" \
"$tmp/$ASSET"
echo "Published $tag ($source): asset $ASSET."