Skip to content
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ CHANGELOG
replaces the 1.5.0 import library that PHP publishes for Windows builds.
The default is unchanged: without the flag, the extension links against a
system libmaxminddb as before. GitHub #265.
* The `conflict` constraint on `ext-maxminddb` in `composer.json` is again
updated when a release is cut. The substitution that maintains it stopped
matching in April 2024, when the constraint's separator changed from a comma
to `||`, so the constraint has read `<1.11.1` through four releases. Users of
the C extension should note the effect of reviving it: `ext-maxminddb` is a
Composer platform package, so this release conflicts with an older compiled
extension and `composer update` will require upgrading the two together.
GitHub #266.

1.13.1 (2025-11-21)
-------------------
Expand Down
250 changes: 180 additions & 70 deletions dev-bin/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ if ! git ls-remote origin &>/dev/null; then
exit 1
fi

# The extension release is a handoff: this script pushes a tag and the
# extension repository's release.yml does the rest. If that workflow is not on
# its default branch, the tag push triggers nothing -- and the tag guard
# further down then refuses every retry, while `gh workflow run` cannot help
# either, because workflow_dispatch also resolves the workflow from the default
# branch. Checked here, before anything has been published.
if ! gh workflow view release.yml --repo maxmind/MaxMind-DB-Reader-php-ext &>/dev/null; then
echo "Error: release.yml is not on the extension repository's default branch."
echo "Pushing a tag there would build nothing, and it cannot be retried."
echo "Merge maxmind/MaxMind-DB-Reader-php-ext#2 first. Nothing has been published yet."
exit 1
fi

check_command perl
check_command php
check_command phpize
Expand Down Expand Up @@ -75,6 +88,15 @@ version="${BASH_REMATCH[1]}"
date="${BASH_REMATCH[3]}"
notes="$(echo "${BASH_REMATCH[4]}" | sed -n -E '/^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?/,$!p')"

# The notes become this repository's release body, package.xml's <notes>, and
# the extension tag's annotation, which is the extension release's body. An
# unusual heading layout that made the filter above yield nothing would publish
# all four empty without complaint -- `git tag -a -m ""` exits 0.
if [ -z "${notes//[[:space:]]/}" ]; then
echo "Error: extracted empty release notes from CHANGELOG.md."
exit 1
fi

if [[ "$date" != "$(date +"%Y-%m-%d")" ]]; then
echo "$date is not today!"
exit 1
Expand All @@ -89,13 +111,62 @@ fi

rm -fr vendor

perl -pi -e "s{(?<=php composer\.phar require maxmind-db/reader:).+}{^$version}g" README.md
perl -pi -e "s/(?<=#define PHP_MAXMINDDB_VERSION \")\d+\.\d+\.\d+(?=\")/$version/" ext/php_maxminddb.h
perl -pi -e "s/(?<=\"ext-maxminddb\": \"<)\d+.\d+.\d+(?=,)/$version/" composer.json
perl -pi -e "s/(?<=<(?:api)>)\d+\.\d+\.\d+(?=<)/$version/" package.xml
perl -pi -e "s/(?<=<(?:release)>)\d+\.\d+\.\d+(?=<)/$version/" package.xml
perl -0777 -pi -e "s{(?<=<notes>).*(?=</notes>)}{$notes}sm" package.xml
perl -pi -e "s/(?<=<date>)\d{4}-\d{2}-\d{2}(?=<)/$date/" package.xml
# Every substitution below is asserted to match something.
#
# `perl -pi` exits 0 whether or not the pattern matched, and exits 0 on a
# missing file too, printing only to stderr -- so a substitution can quietly do
# nothing and the release proceeds. That is not hypothetical: the
# ext-maxminddb floor in composer.json stopped being updated in April 2024,
# when the constraint's separator changed from a comma to " || " and left the
# old anchor matching nothing, and four releases shipped a stale floor before
# anyone noticed. The suppressor is the `git status --porcelain` test further
# down, which treats "nothing changed" as normal when it is only ever a bug.
#
# Counted in a separate read-only pass because the substituting run cannot
# report it: with -i, perl has already renamed the rewritten file into place by
# the time END could inspect a counter.
subst() { # <file> <s/// expression> [extra perl flags...]
local file="$1" expr="$2"
shift 2
if [ ! -f "$file" ]; then
echo "Error: $file is missing; the release tooling cannot update it."
exit 1
fi
local matches
matches="$(perl "$@" -ne "\$n += $expr; END { print \$n + 0 }" "$file")"
if [ "$matches" -eq 0 ]; then
echo "Error: nothing in $file matched, so its version would not be updated."
echo "The file's format has probably changed. Pattern: $expr"
exit 1
fi
perl "$@" -pi -e "$expr" "$file"
}

# Passed through the environment rather than interpolated into perl source by
# the shell. $notes is free text from the changelog, and perl would re-read a
# double-quoted replacement as code: "$reader" and "@args" -- ordinary words in
# a PHP project's release notes -- become variable lookups and vanish.
export RELEASE_VERSION="$version" RELEASE_DATE="$date" RELEASE_NOTES="$notes"

# shellcheck disable=SC2016 # $ENV{...} is perl source; the shell must not expand it
{
subst README.md 's{(?<=php composer\.phar require maxmind-db/reader:).+}{^$ENV{RELEASE_VERSION}}g'
subst ext/php_maxminddb.h 's/(?<=#define PHP_MAXMINDDB_VERSION ")\d+\.\d+\.\d+(?=")/$ENV{RELEASE_VERSION}/'
# Matched by what ends the version rather than by the version's own shape.
# This line is the one that broke: the constraint was written
# "<1.11.1,>=2.0.0" until April 2024 and "<1.11.1 || >=2.0.0" after, and an
# anchor tied to the separator stopped matching. Tying it to the digits
# instead just moves the problem -- \d+\.\d+\.\d+ does not match
# "1.14.0-beta1", which the changelog regex explicitly permits. Consuming
# everything up to a space, comma, pipe or quote handles every shape the
# file has had, a prerelease, and a `composer normalize` that collapses the
# spaces.
subst composer.json 's/(?<="ext-maxminddb": "<)[^ ,|"]+/$ENV{RELEASE_VERSION}/'
subst package.xml 's/(?<=<(?:api)>)\d+\.\d+\.\d+(?=<)/$ENV{RELEASE_VERSION}/'
subst package.xml 's/(?<=<(?:release)>)\d+\.\d+\.\d+(?=<)/$ENV{RELEASE_VERSION}/'
subst package.xml 's{(?<=<notes>).*(?=</notes>)}{$ENV{RELEASE_NOTES}}sm' -0777
subst package.xml 's/(?<=<date>)\d{4}-\d{2}-\d{2}(?=<)/$ENV{RELEASE_DATE}/'
}

pushd ext
phpize
Expand Down Expand Up @@ -151,9 +222,7 @@ echo "==================================================================="
if [ ! -d "$ext_repo_dir" ]; then
echo "Extension repository not found at: $ext_repo_dir"
echo "Cloning extension repository..."
git clone --recurse-submodules "$ext_repo_url" "$ext_repo_dir"

if [ $? -ne 0 ]; then
if ! git clone --recurse-submodules "$ext_repo_url" "$ext_repo_dir"; then
echo "ERROR: Failed to clone extension repository"
echo "Please clone manually: git clone --recurse-submodules $ext_repo_url $ext_repo_dir"
exit 1
Expand Down Expand Up @@ -184,11 +253,23 @@ git pull origin main

# Update submodule to the new tag
echo "Updating submodule to $tag..."

# .ext is only cloned when it is absent, so a pre-existing clone made without
# --recurse-submodules leaves this an empty directory. git's repository
# discovery walks *up*, so the fetch and checkout below would then run against
# .ext itself and detach its HEAD at its own same-named tag -- and succeed, so
# set -e never fires and the branch reports contentment.
if [ ! -e MaxMind-DB-Reader-php/.git ]; then
echo "ERROR: $ext_repo_dir/MaxMind-DB-Reader-php is not a git checkout."
echo "The clone was probably made without --recurse-submodules. Run:"
echo " git -C $ext_repo_dir submodule update --init"
popd >/dev/null
exit 1
fi

cd MaxMind-DB-Reader-php
git fetch --tags origin
git checkout "$tag"

if [ $? -ne 0 ]; then
if ! git checkout "$tag"; then
echo "ERROR: Failed to checkout tag $tag in submodule"
popd >/dev/null
exit 1
Expand All @@ -201,9 +282,7 @@ git add MaxMind-DB-Reader-php

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR-body correction (no better line to anchor to): the shellcheck claim is wrong in the direction that overstates the cleanup. Measured with shellcheck 0.11.0:

  • main: 5 findings, 2 classes — SC2181 ×4 (lines 156, 191, 221, 260) and SC2035 ×1 (line 235)
  • this branch: 2 findings, 1 class — SC2181 ×2 (lines 156, 191)

So one fewer class and three fewer findings, not "two fewer classes". The parenthetical correctly names three findings but calls them classes, which reads as though the $? class was eliminated — it was not.

Separately, there is no shell linting in CI (lint.yml is PHP-only, and there are five scripts in dev-bin/), so this evidence is a one-time local observation that nothing preserves.

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, and I will correct the PR body — "two fewer classes" overstates
it; it is one fewer class and three fewer findings, and the parenthetical
conflates the two words.

Both SC2181 instances are now gone too, so the claim is about to change again in
the direction it originally implied.

Your closing point stands and is not addressed: there is still no shell linting
in CI, so all of this is a local observation nothing preserves. Worth a follow-up
given there are five scripts in dev-bin/.

🤖 Claude, replying on behalf of Greg.

# Check if there are actual changes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out-of-diff (lines 187-189): an uninitialised submodule makes these commands operate on the wrong repository, and they succeed, so set -e never fires.

.ext is only cloned when the directory is absent (line 151). If a pre-existing .ext was cloned without --recurse-submodules, MaxMind-DB-Reader-php/ is an empty directory; cd into it works and git's repo discovery walks up, so git fetch --tags origin and git checkout "$tag" run against .ext itself and detach its HEAD at its own same-named tag. This branch then reports contentment.

Worth asserting the submodule is a real checkout before entering it ([ -e MaxMind-DB-Reader-php/.git ]).

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — [ -e MaxMind-DB-Reader-php/.git ] before entering, with a message
pointing at git submodule update --init.

That git's repository discovery walks up, so both commands succeed against
.ext itself and set -e never fires, is the part that makes this dangerous
rather than merely wrong. Thank you for flagging it despite being out of diff.

🤖 Claude, replying on behalf of Greg.

if [ -z "$(git status --porcelain)" ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out-of-diff (lines 156-160 and 191-195): two dead if [ $? -ne 0 ] blocks remain, the same idiom this PR removed in the two paths below.

Under set -eu -o pipefail, git clone (154) and git checkout "$tag" (189) abort the script before the test runs, so Please clone manually: ... and ERROR: Failed to checkout tag $tag in submodule can never print. shellcheck flags exactly these two lines (SC2181) and nothing else in the file.

The cost is the lost guidance, not the dead branch — converting them to if ! git clone ...; then would keep the messages. Half-cleaning the pattern also leaves the file teaching two contradictory lessons about it.

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — both converted to if ! cmd; then, keeping their messages.

Your point that half-cleaning the pattern leaves the file teaching two
contradictory lessons is the reason these went in this pass rather than being
left as pre-existing.

🤖 Claude, replying on behalf of Greg.

echo "No changes needed in extension repository (already at $tag)"
popd >/dev/null
echo "Extension repository is up to date"
echo "No commit needed in extension repository (submodule already at $tag)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch can be reached with an unpushed local commit, and then line 249 tags a commit that is on no remote branch.

The cleanliness check at line 167 is git status --porcelain, which says nothing about ahead-ness. If the persistent .ext clone has a local-only commit that already bumped the submodule to $tag, this branch is taken, nothing is pushed, and the tag is pushed pointing at a commit GitHub has on no branch — so the released extension is built from an unreachable commit.

Note this is a behaviour change: the old gh release create --repo ...-ext tagged the remote default-branch tip. Suggest verifying git rev-parse HEAD against git rev-parse origin/main before tagging.

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, and by the same change as line 251 — pushing main atomically with the
tag means the tag can only ever point at something reachable from main, and a
main that does not fast-forward now fails the whole push rather than half of
it.

Your note that this is a behaviour change from the old gh release create --repo
form, which tagged the remote tip, is the part that made it clear an explicit fix
was needed rather than just documentation.

🤖 Claude, replying on behalf of Greg.

else

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR-body / commit-message correction: the already-bumped-submodule case did not exit 0 silently — it exited 1. main pops the directory stack inside this branch and again after fi (3 pushd against 9 popd). Reproduced:

No changes needed in extension repository (already at v9.9.9)
Extension repository is up to date
release.sh: line 95: popd: directory stack empty
exit=1

The net effect (no extension release) and the fix are both right; only the diagnosis is off. Worth correcting so the next reader isn't hunting a silent-exit bug that was never there — "exited 1 on a dir-stack underflow" and "exited 0" have different fixes.

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you — I will correct the PR body and the commit message. "Exited 1 on a
dir-stack underflow" and "exited 0 silently" really do have different fixes, and
someone reading the latter would go looking for a missing error check that was
never the problem.

The 3-pushd-against-9-popd count is the detail that makes it verifiable.

🤖 Claude, replying on behalf of Greg.

# Commit submodule update
echo "Committing submodule update..."
Expand All @@ -213,65 +292,87 @@ This updates the submodule reference to track the $tag release.

Release notes from main repository:
$notes"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verification suggestion. The tag block is currently testable only by duplicating it — the PR's scratch-repo testing was done on an extracted copy, which validates control flow but not the data flowing through it (the #-stripping and empty-notes issues above are both invisible to that method, and a copy cannot catch divergence from the shipping block).

dev-bin/test-gate-extension.sh on this same branch is the precedent, and its header comment argues the general case — the gate "is only ever observed succeeding". That applies here with almost no modification: this block will be observed succeeding once per release, at the worst possible moment to discover it is wrong.

Two cheap changes would make it durable: extract the block into a function or script taking (remote, tag, notes) so a test can invoke the real code, and add RELEASE_DRY_RUN=1 gating the three mutating operations, which is the single largest reduction in "unexercised until someone cuts a real release".

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not done, and I think you are right that it is the highest-value thing left in
this file.

Recording the argument so it does not get lost: this block will be observed
succeeding exactly once per release, at the worst possible moment to discover it
is wrong, and the scratch-repo method validates control flow but not the data
flowing through it — which is precisely why the #-stripping and empty-notes
issues were invisible to it.

Some of the pressure is off: the # stripping, the empty notes, the
non-atomic push and the unpushed-commit case are all fixed, so the specific
things that testing would have caught are caught. But the general point holds,
and RELEASE_DRY_RUN=1 around the three mutating operations is the cheap version.

For what it is worth, the extension PR now has the equivalent for its Windows
gate — dev-bin/test-gate-extension.sh, 16 cases — which came directly from your
comment there.

🤖 Claude, replying on behalf of Greg.


# Push changes
echo "Pushing to origin..."
git push origin main

if [ $? -ne 0 ]; then
echo "ERROR: Failed to push to extension repository"
popd >/dev/null
exit 1
fi

# Create pre-packaged source tarball for PIE
# PIE needs this because it doesn't handle git submodules automatically
echo "Creating pre-packaged source tarball for PIE..."
pie_tarball="maxminddb-${tag}.tgz"

# Create tarball with files at root level (PIE requirement)
# Note: naming must be {extension-name}-v{version}.tgz
pushd MaxMind-DB-Reader-php/ext >/dev/null
tar -czf "../../$pie_tarball" *
# Refuse to re-push a tag that is already there. A tag push is what starts the
# release, and pushing a tag that already exists raises no event, so the run
# would build nothing while this script reported success.
if ! ext_remote_tag="$(git ls-remote --tags origin "refs/tags/$tag")"; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment placement: this paragraph (220-225) explains why tagging is necessary and why it must happen even when the commit was a no-op, but it is attached to the git ls-remote existence check. The reasoning for the guard itself lives in the echo strings at 233-239 instead. A reader arriving here gets a paragraph about workflow triggering followed by an unrelated existence check.

Suggest moving the paragraph onto the git tag at 248 and giving the guard a one-line comment of its own.

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the paragraph about why tagging is necessary now sits on the git tag,
and the existence check has a one-line comment of its own about what it is
actually for.

🤖 Claude, replying on behalf of Greg.

echo "ERROR: Could not list tags in the extension repository remote"
popd >/dev/null
exit 1
fi

if [ ! -f "$pie_tarball" ]; then
echo "ERROR: Failed to create source tarball"
popd >/dev/null
exit 1
fi
if [ -n "$ext_remote_tag" ]; then
echo "ERROR: Tag $tag already exists in the extension repository."
echo "Pushing it again raises no event, so the release workflow would not"
echo "run. Check whether the release is already there:"
echo "https://github.com/maxmind/MaxMind-DB-Reader-php-ext/releases/tag/$tag"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This recovery command does not work in the case it is printed for.

The message says the tag already exists, then advises gh workflow run ... -f tag=$tag to rebuild assets. But draft-release refuses outright once the release is no longer a draft (release.yml:213-216):

::error::Release $TAG exists and is already published; refusing to stage assets onto it.

The dispatch only helps while the release is still a draft or absent. Worth saying so, otherwise the operator burns a run to find out.

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the message now says the dispatch helps while the release is still a
draft or absent, and that it will not help once published, because
draft-release refuses to stage onto a published release.

Worth the extra two lines given the alternative is burning a run to discover
it.

🤖 Claude, replying on behalf of Greg.

echo "If it is still a draft, or absent, you can rebuild its assets by hand:"
echo "gh workflow run release.yml --repo maxmind/MaxMind-DB-Reader-php-ext -f tag=$tag"
echo "That will not help once the release is published: the workflow refuses"
echo "to stage assets onto a published release, and there is nothing to be"
echo "done about one except cut a new version."
popd >/dev/null
exit 1
fi

echo "Created $pie_tarball"
# Tagging the extension repository is what triggers its release workflow, which
# builds the pre-packaged source tarball and the precompiled binaries, uploads
# them all, and publishes the release. That has to happen even when the
# submodule commit above turned out to be unnecessary -- on a re-run, or after
# someone bumped the submodule by hand, the commit is a no-op but the tag may
# still not be on the remote, and it is the tag that starts the release.
#
# --cleanup=verbatim because git's default for -m is --cleanup=strip, which
# removes every line beginning with '#'. The annotation is the extension
# release's notes, and '#' begins a Markdown heading and a "#123" issue
# reference, so the default would silently publish notes that differ from the
# ones this repository's release carries.
echo "Tagging $tag in extension repository..."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty notes produce an empty annotation without complaint. git tag -f -a t -m "" exits 0 with empty contents, so an unusual CHANGELOG heading layout that makes the sed filter at line 76 yield an empty string would publish an extension release with an empty body. A one-line guard after the parse would close it:

if [ -z "$(echo "$notes" | tr -d '[:space:]')" ]; then
    echo "Error: extracted empty release notes from CHANGELOG.md."
    exit 1
fi

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, with the guard placed at the parse rather than after it, so it covers
every consumer of $notes — this repository's release body, package.xml, and
the extension tag's annotation.

🤖 Claude, replying on behalf of Greg.

git tag -f -a --cleanup=verbatim "$tag" -m "$notes"

# One transaction. Pushed separately, a failure between them leaves the branch
# public and the tag missing -- the half-done state this whole change exists to
# remove, and one that cannot be repaired by re-running, because release.sh
# dies far earlier at `gh release create` and never reaches this block again.
#
# Pushing main here also settles what the tag names. The cleanliness check
# above says nothing about ahead-ness, so a local-only commit in a
# pre-existing .ext clone could otherwise be tagged and pushed while the commit
# itself stayed on no branch GitHub knows about. Sending both together means
# the tag can only ever point at something reachable from main -- and a main
# that does not fast-forward now fails the whole push instead of half of it.
echo "Pushing main and tag $tag..."
git push --atomic origin main "refs/tags/$tag"

# Create corresponding release in extension repo with same tag
echo "Creating release $tag in extension repository..."
gh release create "$tag" \
echo ""
echo "✓ Extension repository tagged $tag"

# The script's last real action was that push; everything after it used to be a
# printed promise. Confirm the workflow actually picked the tag up, because the
# only remedy is manual and the main repository's release is already public by
# now.
echo "Waiting for the extension release workflow to start..."
ext_run=""
for _ in $(seq 1 12); do
ext_run="$(gh run list --workflow=release.yml \
--repo maxmind/MaxMind-DB-Reader-php-ext \
--title "$version" \
--notes "Extension release for MaxMind-DB-Reader-php $version

This release tracks the $tag tag of the main repository.

## Release notes from main repository

$notes" \
"$pie_tarball"

if [ $? -ne 0 ]; then
echo "ERROR: Failed to create release in extension repository"
echo "You may need to create it manually at:"
echo "https://github.com/maxmind/MaxMind-DB-Reader-php-ext/releases/new?tag=$tag"
popd >/dev/null
exit 1
fi

# Clean up tarball
rm -f "$pie_tarball"

--branch "$tag" --limit 1 --json databaseId --jq '.[0].databaseId // empty')"
[ -n "$ext_run" ] && break
sleep 10
done

if [ -n "$ext_run" ]; then
echo "✓ Its release workflow is running:"
echo " https://github.com/maxmind/MaxMind-DB-Reader-php-ext/actions/runs/$ext_run"
else
echo ""
echo "✓ Extension repository updated successfully!"
echo "✓ Release created: https://github.com/maxmind/MaxMind-DB-Reader-php-ext/releases/tag/$tag"
echo "✓ Pre-packaged source uploaded: $pie_tarball"
echo "WARNING: no release workflow run appeared for $tag after two minutes."
echo "This repository's $tag release is already published, so this needs a"
echo "human. Check for a run, and start one by hand if none arrived:"
echo " https://github.com/maxmind/MaxMind-DB-Reader-php-ext/actions/workflows/release.yml"
echo " gh workflow run release.yml --repo maxmind/MaxMind-DB-Reader-php-ext -f tag=$tag"
fi

popd >/dev/null
Expand All @@ -283,9 +384,18 @@ echo "==================================================================="
echo ""
echo "Main repository: https://github.com/maxmind/MaxMind-DB-Reader-php/releases/tag/$tag"
echo "Extension repository: https://github.com/maxmind/MaxMind-DB-Reader-php-ext/releases/tag/$tag"
echo " (published by CI once the release workflow finishes)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing confirms the handoff actually started. The script's last real action is the tag push; it then asserts success and exits 0. The old code at least verified its own work — the tarball had to exist and gh release create had to succeed. That verification is now a printed promise plus a manual action item.

Consider confirming a run appeared rather than asserting it will:

run_id="$(gh run list --workflow=release.yml --repo maxmind/MaxMind-DB-Reader-php-ext \
    --branch "$tag" --limit 1 --json databaseId --jq '.[0].databaseId')"

and failing loudly, noting that the main repo release is already public, if it stays empty.

🤖 Comment by Claude (Claude Code) on behalf of Will.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The script now polls for the run and prints its URL, and if none appears
within two minutes it says plainly that this repository's release is already
public and the situation needs a person.

"The old code at least verified its own work" is the right way to put it — the
change traded a verified action for an asserted one, and that is a regression
even though the new design is better.

🤖 Claude, replying on behalf of Greg.

echo ""
echo "Action items:"
echo "1. Upload PECL package to pecl.php.net: https://pecl.php.net/package-new.php"
echo "1. Watch the extension repository's release workflow and confirm that it"
echo " published the release with all of its assets:"
echo " https://github.com/maxmind/MaxMind-DB-Reader-php-ext/actions/workflows/release.yml"
echo " It builds the pre-packaged source tarball and the precompiled binaries,"
echo " checks them, and only then un-drafts the release, so the release stays"
echo " a draft until its publish job's last step."
echo " Its final job smoke tests 'pie install' *after* that step, so a red"
echo " workflow can still mean an already-published release. That one needs a"
echo " human rather than a re-run."
echo "2. Upload PECL package to pecl.php.net: https://pecl.php.net/package-new.php"
echo " File: $package"
echo "2. Verify PIE installation: pie install maxmind-db/reader-ext:^$version"
echo "3. Announce release"
Loading