diff --git a/.claude/release-process.md b/.claude/release-process.md index 3a55e1fb..1a42a425 100644 --- a/.claude/release-process.md +++ b/.claude/release-process.md @@ -123,10 +123,11 @@ The script does everything else, in order: 6. Runs the full test suite **including** the `integration-destructive` group (`vendor/bin/pest --parallel`). Must pass. 7. Generates release notes deterministically from git history: walks `git log PREV_TAG..HEAD`, extracts every `#NN` reference, fetches each PR by number via `gh api repos/.../pulls/N`, buckets by label per `.github/release.yml`, and computes new contributors via the GitHub search API. We do **not** use `gh api releases/generate-notes` — that endpoint matches PRs to the tag range by their stored `merge_commit_sha`, which can go stale during rapid concurrent merges and silently drop PRs. Walking git ourselves removes that dependency entirely. 8. Prepends a new `## [VERSION] - YYYY-MM-DD` section to `CHANGELOG.md` directly under the insertion marker, then commits `chore: changelog for VERSION` on `main` -9. Pushes `main` (changelog commit included) -10. Creates annotated tag `VERSION` and pushes it -11. Creates the GitHub Release using the same notes file (single source of truth — `CHANGELOG.md` and the GitHub Release body match exactly) -12. Switches back to `develop`, merges `main`, and pushes `develop` +9. Invokes `bin/create-split-repos.sh` to create any missing split repos for newly-added packages (idempotent; uses a single batched API call so a no-op run completes in ~1s). This is what prevents the "split workflow fails because the split repo doesn't exist yet" failure mode that previously required a manual fix-up after the fact. +10. Pushes `main` (changelog commit included) +11. Creates annotated tag `VERSION` and pushes it +12. Creates the GitHub Release using the same notes file (single source of truth — `CHANGELOG.md` and the GitHub Release body match exactly) +13. Switches back to `develop`, merges `main`, and pushes `develop` After the tag is pushed, everything else is automatic: - GitHub Actions split workflow runs: https://github.com/marko-php/marko/actions @@ -216,8 +217,9 @@ Runtime env vars for bin scripts (not stored as secrets — passed at the comman - Regenerate PAT at: https://github.com/settings/tokens if needed **Split repo does not exist for a package** -- Run: `GITHUB_ORG=marko-php ./bin/create-split-repos.sh` -- Script is idempotent — safe to re-run, skips repos that already exist +- `bin/release.sh` invokes `create-split-repos.sh` automatically before pushing, so this should rarely happen for new releases. If it still does (e.g. a package added directly to `develop` after the release script ran), run: `GITHUB_ORG=marko-php ./bin/create-split-repos.sh` (no args = check all packages) or scope it to specific packages: `./bin/create-split-repos.sh inertia-react inertia-vue`. +- Use `--dry-run` to preview without making API calls. +- Script is idempotent — safe to re-run, skips repos that already exist via a single batched API call. **Packagist not updating after a release** - Verify the webhook is configured on the split repo: `https://github.com/marko-php/marko-{name}/settings/hooks` diff --git a/bin/create-split-repos.sh b/bin/create-split-repos.sh index 04ea6a62..f09275cf 100755 --- a/bin/create-split-repos.sh +++ b/bin/create-split-repos.sh @@ -5,39 +5,114 @@ GITHUB_ORG="${GITHUB_ORG:-marko-php}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(dirname "$SCRIPT_DIR")" +usage() { + cat <&2; usage >&2; exit 1 ;; + *) PACKAGES+=("$arg") ;; + esac +done + # Prerequisites -command -v gh >/dev/null 2>&1 || { echo "Error: gh CLI not installed. https://cli.github.com"; exit 1; } -command -v jq >/dev/null 2>&1 || { echo "Error: jq not installed. brew install jq"; exit 1; } -gh auth status >/dev/null 2>&1 || { echo "Error: Not authenticated with gh. Run: gh auth login"; exit 1; } +command -v gh >/dev/null 2>&1 || { echo "Error: gh CLI not installed. https://cli.github.com" >&2; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "Error: jq not installed. brew install jq" >&2; exit 1; } +gh auth status >/dev/null 2>&1 || { echo "Error: Not authenticated with gh. Run: gh auth login" >&2; exit 1; } + +# Default to every package directory if no args given. +if [ ${#PACKAGES[@]} -eq 0 ]; then + for pkg_dir in "$REPO_ROOT"/packages/*/; do + PACKAGES+=("$(basename "$pkg_dir")") + done +fi -echo "Creating split repos under ${GITHUB_ORG}..." +# Single API call to list every repo in the org. Compared locally below — much +# faster than N sequential `gh repo view` calls (the previous behavior). +echo "Fetching existing repos under ${GITHUB_ORG}..." +EXISTING_REPOS=$(gh repo list "$GITHUB_ORG" --limit 1000 --json name --jq '.[].name') + +if [ "$DRY_RUN" = true ]; then + echo "(dry run — no repos will be created)" +fi + +CREATED=0 +SKIPPED=0 +MISSING_DIR=0 +for pkg in "${PACKAGES[@]}"; do + repo_name="marko-${pkg}" + full_repo="${GITHUB_ORG}/${repo_name}" + + # Already exists — skip silently to keep output focused on actual work. + if echo "$EXISTING_REPOS" | grep -qx "$repo_name"; then + SKIPPED=$((SKIPPED + 1)) + continue + fi + + pkg_dir="$REPO_ROOT/packages/$pkg" + if [ ! -d "$pkg_dir" ]; then + echo " ⚠ packages/${pkg} does not exist — skipping" >&2 + MISSING_DIR=$((MISSING_DIR + 1)) + continue + fi -for pkg_dir in "$REPO_ROOT"/packages/*/; do - pkg=$(basename "$pkg_dir") - repo="${GITHUB_ORG}/marko-${pkg}" description="[READ-ONLY] Subtree split of marko/${pkg}. Issues and PRs at https://github.com/marko-php/marko" - # Get description from composer.json if [[ -f "$pkg_dir/composer.json" ]]; then pkg_description=$(jq -r '.description // empty' "$pkg_dir/composer.json") [[ -n "$pkg_description" ]] && description="[READ-ONLY] ${pkg_description}. Issues and PRs at https://github.com/marko-php/marko" fi - if gh repo view "$repo" >/dev/null 2>&1; then - echo " ✓ ${repo} already exists, skipping" - else - echo " Creating ${repo}..." - gh repo create "$repo" \ - --public \ - --description "$description" \ - --disable-issues \ - --disable-wiki + if [ "$DRY_RUN" = true ]; then + echo " Would create ${full_repo}" + echo " description: ${description}" + CREATED=$((CREATED + 1)) + continue + fi + + echo " Creating ${full_repo}..." + gh repo create "$full_repo" \ + --public \ + --description "$description" \ + --disable-issues \ + --disable-wiki - # Sync repo settings and workflow - "$SCRIPT_DIR/sync-split-repo-config.sh" "marko-${pkg}" + "$SCRIPT_DIR/sync-split-repo-config.sh" "${repo_name}" - echo " ✓ Created ${repo}" - fi + echo " ✓ Created ${full_repo}" + CREATED=$((CREATED + 1)) done -echo "Done! All split repos created under ${GITHUB_ORG}." +echo "" +if [ "$DRY_RUN" = true ]; then + echo "Dry run done: ${CREATED} would be created, ${SKIPPED} already exist, ${MISSING_DIR} skipped (no local dir)." +else + echo "Done: ${CREATED} created, ${SKIPPED} already existed, ${MISSING_DIR} skipped (no local dir)." +fi diff --git a/bin/release.sh b/bin/release.sh index e1c0d1a5..3903a4f8 100755 --- a/bin/release.sh +++ b/bin/release.sh @@ -3,6 +3,7 @@ set -euo pipefail VERSION="${1:?Usage: ./bin/release.sh (e.g., 0.1.0)}" TAG="${VERSION}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" echo "Preparing release ${TAG}..." @@ -280,6 +281,14 @@ echo "Committing changelog..." git add "$CHANGELOG_FILE" git commit -m "chore: changelog for ${VERSION}" +# Ensure split repos exist for any newly-added packages BEFORE pushing main or +# the tag. The split workflow fires on both pushes and will fail loudly if a +# package's split repo doesn't exist yet (this is what bit us during 0.5.0). +# The script is idempotent and uses a single batched API call, so a no-op run +# completes in ~1s. +echo "Verifying split repos exist for all packages..." +"${SCRIPT_DIR}/create-split-repos.sh" + echo "Pushing main branch..." git push origin main