#!/usr/bin/env bash # Promote a tag's tree from the source repo into the PUBLIC repo working dir as one clean release # commit + tag. Excludes .git (via git archive) and .github (private CI stays private). The public # repo's own .git is preserved. The PUSH is done by the calling workflow (it holds the credentials). # usage: promote.sh set -euo pipefail src="${1:?src repo required}" tag="${2:?tag required}" pub="${3:?public repo dir required}" msg="${4:?commit message required}" git -C "$src" rev-parse "$tag" >/dev/null 2>&1 || { echo "unknown tag: $tag" >&2; exit 1; } # Fail fast BEFORE mutating the public tree if this release was already promoted — re-promoting a # published release must be deliberate, not a silent half-commit left on the runner. if git -C "$pub" rev-parse -q --verify "refs/tags/$tag" >/dev/null 2>&1; then echo "tag already promoted in public repo: $tag" >&2 exit 1 fi # Clear the public working tree but KEEP its .git. find "$pub" -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + # Extract exactly the tag's tree (git archive omits .git automatically). git -C "$src" archive --format=tar "$tag" | tar -x -C "$pub" # Private CI never goes public. rm -rf "${pub:?}/.github" # Leak guard — refuse to publish a tree that still carries an internal-only doc. These paths are # export-ignored on current trees, so their presence means the tag predates the leak-safety fixes # (such a tree would also ship the old private URLs + dev notes). Refuse rather than leak the # dev/staging repos. Checking for the docs (not grepping for URL strings) keeps this guard free of # the very identifiers it protects — so it never trips on its own source or on legitimate content. for f in CLAUDE.md rules.md handoff.md kickoff-prompt.md docs; do if [ -e "$pub/$f" ]; then echo "leak-guard: REFUSING to publish $tag — internal path '$f' present; promote a tag cut after the leak-safety fixes (>= v0.9.58)." >&2 exit 1 fi done git -C "$pub" add -A # --allow-empty: stable promotion re-tags the same source commit as the last beta, so the # extracted tree is byte-identical and there is nothing to stage — still produce a release commit. git -C "$pub" commit -q --allow-empty -m "$msg" git -C "$pub" tag "$tag"