Skip to content

ci: reconcile ambiguous release commits and resume partial publication - #316

Merged
ioannisj merged 5 commits into
mainfrom
ci/release-resume-partial-publication
Nov 11, 2026
Merged

ioannisj merged 5 commits into
mainfrom
ci/release-resume-partial-publication

Conversation

@ioannisj

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Release run 43015053422 published PHP 4.23.1 and then stopped halfway through Laravel 0.4.1. GitHub applied the createCommitOnBranch mutation for the distribution commit and answered with an HTTP 502, so scripts/commit_release.py treated it as a failure and the job never reached the two Laravel releases. The commit itself is on posthog-laravel main, signed, and byte-identical to the approved archive.

There was no way to finish it by hand. Rerunning the job fails the freshness check because main now carries the version bump commit, and the tag rulesets on both repos let only the releaser App create release tags.

Makes the workflow able to finish such a run on its own:

  • scripts/commit_release.py reconciles after a failed mutation. It re-reads the destination branch and accepts the new head only if it is a GitHub-verified, single-parent child of the expected head whose diff is exactly the requested file changes. The mutation is never sent twice.
  • release.yml gets a resume_run_id dispatch input. A read-only job validates the resumed run and its candidate artifact, finds the shared version commit (source tree plus release.patch, App-signed) and reports which refs already exist. A job under the Release environment then completes only what is missing, check-then-create, with the same three App tokens and the same revoke-before-Composer order. Existing tags and releases are verified, never recreated or moved.
  • scripts/check-release-refs.sh gets a resume mode. The default mode is unchanged.
  • RELEASING.md documents the resume and the manual App-token fallback for when the artifact has expired.

Two things I left for follow-ups: the resume job repeats about a dozen steps of the release job and could share a composite action, and the reconciliation branch has no in-repo test yet.

💚 How did you test it?

  • python3 -m py_compile on both release scripts, actionlint with shellcheck on release.yml, shellcheck on the three shell scripts.
  • A stub gh suite against the real 0.4.1 candidate and real clones, 10 of 10 pass. A 502 with a matching landed head reuses it, in both distribution and source mode. A changed tree, an unchanged head, an unverified head, and a head that is not a child of the expected head all fail with the original error.
  • I ran the workflow's shell blocks read-only against the live repos: run validation, candidate digests, scripts/find-version-commit.sh returns the real version commit 82cd1d0, the refs check reports core present and both Laravel refs missing, the Packagist wait plus composer install --no-dev pass, and a fresh posthog-laravel clone takes the no-diff path and reuses 15105e02 without a mutation.
  • Not exercised end to end: the resume dispatch itself. The first real run is gh workflow run release.yml --ref main -f resume_run_id=43015053422 after this merges, before the candidate artifact expires on 2026-11-16.

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm change to generate a change intent file

🤖 Agent context

DRI: @ioannisj
Autonomy: Human-driven (agent-assisted)

@ioannisj ioannisj self-assigned this Nov 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

posthog-php-fork_curl Compliance Report

Date: 2026-11-11T13:40:52.007665+00:00
Duration: 116584ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 38ms
Format Validation.Event Has Uuid ✅ 531ms
Format Validation.Event Has Lib Properties ✅ 532ms
Format Validation.Distinct Id Is String ✅ 533ms
Format Validation.Token Is Present ✅ 533ms
Format Validation.Custom Properties Preserved ✅ 532ms
Format Validation.Event Has Timestamp ✅ 531ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 533ms
Retry Behavior.Retries On 503 ✅ 5854ms
Retry Behavior.Does Not Retry On 400 ✅ 2538ms
Retry Behavior.Does Not Retry On 401 ✅ 2536ms
Retry Behavior.Respects Retry After Header ❌ 6350ms
Retry Behavior.Implements Backoff ✅ 16280ms
Retry Behavior.Retries On 500 ✅ 5649ms
Retry Behavior.Retries On 502 ✅ 5651ms
Retry Behavior.Retries On 504 ✅ 5649ms
Retry Behavior.Max Retries Respected ✅ 16280ms
Deduplication.Generates Unique Uuids ✅ 542ms
Deduplication.Preserves Uuid On Retry ✅ 5647ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10866ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5650ms
Deduplication.No Duplicate Events In Batch ✅ 542ms
Deduplication.Different Events Have Different Uuids ✅ 534ms
Compression.Sends Gzip When Enabled ✅ 535ms
Batch Format.Uses Proper Batch Structure ✅ 532ms
Batch Format.Flush With No Events Sends Nothing ✅ 520ms
Batch Format.Multiple Events Batched Together ✅ 522ms
Error Handling.Does Not Retry On 403 ✅ 2535ms
Error Handling.Does Not Retry On 413 ✅ 2537ms
Error Handling.Retries On 408 ✅ 5650ms

Failures

retry_behavior.respects_retry_after_header

Retry delay too short: 811ms < 2500ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 524ms
Request Payload.Flags Request Uses V2 Query Param ✅ 524ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 523ms
Request Payload.Flags Request Omits Authorization Header ✅ 524ms
Request Payload.Token In Flags Body Matches Init ✅ 523ms
Request Payload.Groups Round Trip ✅ 524ms
Request Payload.Groups Default To Empty Object ✅ 523ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 523ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 524ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 523ms
Request Lifecycle.No Flags Request On Init Alone ✅ 519ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 517ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 526ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 525ms
Retry Behavior.Retries Flags On 502 ✅ 625ms
Retry Behavior.Retries Flags On 504 ✅ 626ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 534ms

@github-actions

Copy link
Copy Markdown
Contributor

posthog-php-lib_curl Compliance Report

Date: 2026-11-11T13:41:02.711789+00:00
Duration: 116436ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 24ms
Format Validation.Event Has Uuid ✅ 521ms
Format Validation.Event Has Lib Properties ✅ 521ms
Format Validation.Distinct Id Is String ✅ 522ms
Format Validation.Token Is Present ✅ 522ms
Format Validation.Custom Properties Preserved ✅ 522ms
Format Validation.Event Has Timestamp ✅ 522ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 521ms
Retry Behavior.Retries On 503 ✅ 5831ms
Retry Behavior.Does Not Retry On 400 ✅ 2524ms
Retry Behavior.Does Not Retry On 401 ✅ 2524ms
Retry Behavior.Respects Retry After Header ❌ 6331ms
Retry Behavior.Implements Backoff ✅ 16244ms
Retry Behavior.Retries On 500 ✅ 5631ms
Retry Behavior.Retries On 502 ✅ 5626ms
Retry Behavior.Retries On 504 ✅ 5633ms
Retry Behavior.Max Retries Respected ✅ 17045ms
Deduplication.Generates Unique Uuids ✅ 228ms
Deduplication.Preserves Uuid On Retry ✅ 5629ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10837ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5632ms
Deduplication.No Duplicate Events In Batch ✅ 526ms
Deduplication.Different Events Have Different Uuids ✅ 523ms
Compression.Sends Gzip When Enabled ✅ 522ms
Batch Format.Uses Proper Batch Structure ✅ 523ms
Batch Format.Flush With No Events Sends Nothing ✅ 518ms
Batch Format.Multiple Events Batched Together ✅ 510ms
Error Handling.Does Not Retry On 403 ✅ 2524ms
Error Handling.Does Not Retry On 413 ✅ 2524ms
Error Handling.Retries On 408 ✅ 5629ms

Failures

retry_behavior.respects_retry_after_header

Retry delay too short: 803ms < 2500ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 522ms
Request Payload.Flags Request Uses V2 Query Param ✅ 520ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 520ms
Request Payload.Flags Request Omits Authorization Header ✅ 520ms
Request Payload.Token In Flags Body Matches Init ✅ 520ms
Request Payload.Groups Round Trip ✅ 520ms
Request Payload.Groups Default To Empty Object ✅ 519ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 520ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 520ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 520ms
Request Lifecycle.No Flags Request On Init Alone ✅ 516ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 508ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 522ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 521ms
Retry Behavior.Retries Flags On 502 ✅ 623ms
Retry Behavior.Retries Flags On 504 ✅ 622ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 521ms

@github-actions

Copy link
Copy Markdown
Contributor

posthog-php-socket Compliance Report

Date: 2026-11-11T13:41:04.512087+00:00
Duration: 115950ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 25ms
Format Validation.Event Has Uuid ✅ 520ms
Format Validation.Event Has Lib Properties ✅ 522ms
Format Validation.Distinct Id Is String ✅ 522ms
Format Validation.Token Is Present ✅ 522ms
Format Validation.Custom Properties Preserved ✅ 522ms
Format Validation.Event Has Timestamp ✅ 523ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 522ms
Retry Behavior.Retries On 503 ✅ 5830ms
Retry Behavior.Does Not Retry On 400 ✅ 2524ms
Retry Behavior.Does Not Retry On 401 ✅ 2524ms
Retry Behavior.Respects Retry After Header ❌ 6330ms
Retry Behavior.Implements Backoff ✅ 16245ms
Retry Behavior.Retries On 500 ✅ 5631ms
Retry Behavior.Retries On 502 ✅ 5630ms
Retry Behavior.Retries On 504 ✅ 5628ms
Retry Behavior.Max Retries Respected ✅ 16244ms
Deduplication.Generates Unique Uuids ✅ 528ms
Deduplication.Preserves Uuid On Retry ✅ 5630ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10836ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5633ms
Deduplication.No Duplicate Events In Batch ✅ 526ms
Deduplication.Different Events Have Different Uuids ✅ 524ms
Compression.Sends Gzip When Enabled ✅ 522ms
Batch Format.Uses Proper Batch Structure ✅ 522ms
Batch Format.Flush With No Events Sends Nothing ✅ 519ms
Batch Format.Multiple Events Batched Together ✅ 511ms
Error Handling.Does Not Retry On 403 ✅ 2523ms
Error Handling.Does Not Retry On 413 ✅ 2523ms
Error Handling.Retries On 408 ✅ 5632ms

Failures

retry_behavior.respects_retry_after_header

Retry delay too short: 802ms < 2500ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 522ms
Request Payload.Flags Request Uses V2 Query Param ✅ 520ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 520ms
Request Payload.Flags Request Omits Authorization Header ✅ 520ms
Request Payload.Token In Flags Body Matches Init ✅ 520ms
Request Payload.Groups Round Trip ✅ 520ms
Request Payload.Groups Default To Empty Object ✅ 520ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 521ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 520ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 520ms
Request Lifecycle.No Flags Request On Init Alone ✅ 517ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 508ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 523ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 521ms
Retry Behavior.Retries Flags On 502 ✅ 623ms
Retry Behavior.Retries Flags On 504 ✅ 622ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 523ms

@ioannisj
ioannisj marked this pull request as ready for review November 11, 2026 13:43
@ioannisj
ioannisj requested a review from a team as a code owner November 11, 2026 13:43
@veria-ai

veria-ai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

PR overview

No security issues were found in this pull request.

Reviews

@marandaneto marandaneto left a comment

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.

Approved to unblock but left a few comments

- '.changeset/*.md'
workflow_dispatch:
inputs:
resume_run_id:

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.

I think we should use the version instead, wdyt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 An AI agent wrote this on behalf of @ioannisj.

Keeping the run id. The approved candidate only exists as an artifact of the failed run, so that's what the resume downloads, and the run URL is already in the failure Slack message and the RELEASING.md recipe.

A version would need an extra job that lists Release runs and pulls candidates until one matches, and it'd have to say which version too (PHP 4.23.1 or Laravel 0.4.1). I think we can add a resume_version input as a follow-up convenience.

name: Resume publication
needs: resume-check
runs-on: ubuntu-latest
environment: Release

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.

Does this require another approval?

Should we instead patch the release to allow rerunning the failed task, we do this for other sdks, i wonder why this one is a separate workflow

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 An AI agent wrote this on behalf of @ioannisj.

Yes, one Release approval, the same gate the publish job already has. It's not a separate workflow either, resume-publication sits in the same release.yml and is only reached through the resume_run_id input.

Rerunning the failed job doesn't work here because the recheck at .github/workflows/release.yml:312 requires main to still equal the original source SHA, and the job's own version commit 82cd1d0 moved main, so a rerun fails before publishing anything. iOS gets away with gh run rerun --failed because the environment is only on version-bump and the publish steps are separate jobs. Here the two-repo publish is one job on purpose so PHP and Laravel land together.

Splitting it like iOS for example is a bigger refactor and gives up that atomicity, so I went with the smaller fix given the artifact expires on the 16th. Open to the split as a follow-up if it's worth it.

@ioannisj
ioannisj merged commit 8235861 into main Nov 11, 2026
36 checks passed
@ioannisj
ioannisj deleted the ci/release-resume-partial-publication branch November 11, 2026 14:19
@ioannisj ioannisj mentioned this pull request Nov 11, 2026
4 of 6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants