Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .claude/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down
119 changes: 97 additions & 22 deletions bin/create-split-repos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
Usage: $(basename "$0") [--dry-run] [package-name ...]

Creates split repos under \${GITHUB_ORG} (default: marko-php) for Marko packages.

If no package names are given, every directory under packages/ is checked.
If package names are given (without the "marko-" prefix), only those are checked.

The script fetches the org's existing repos with a single API call and
compares locally, so a no-op run completes in well under a second even
with 70+ packages.

Options:
--dry-run Print what would be created without making any API calls.
-h, --help Show this message.

Examples:
$(basename "$0") # check all packages
$(basename "$0") inertia-react inertia-vue # check just those two
$(basename "$0") --dry-run # preview only

EOF
}

# Parse args
DRY_RUN=false
PACKAGES=()
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
-h|--help) usage; exit 0 ;;
--*) echo "Error: unknown flag '$arg'" >&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
9 changes: 9 additions & 0 deletions bin/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ set -euo pipefail

VERSION="${1:?Usage: ./bin/release.sh <version> (e.g., 0.1.0)}"
TAG="${VERSION}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

echo "Preparing release ${TAG}..."

Expand Down Expand Up @@ -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

Expand Down