ci(ios): publish + prune per-PR XCFramework snapshot branches#495
ci(ios): publish + prune per-PR XCFramework snapshot branches#495
Conversation
XCFramework BuildThis PR's XCFramework is available for testing. Add to your .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/495")Built from d7830c9 |
For each PR commit, upload the signed XCFramework to S3 under `gutenbergkit/pr-builds/<n>/`, force-push a `pr-build/<n>` branch with `Package.swift` rewritten to consume that binary target, and post a sticky PR comment + Buildkite annotation pointing consumers at the branch. Mirrors the wordpress-rs flow. The existing trunk/tag publish step is gated on `build.pull_request.id == null` so we don't double-upload on PRs. Skips fork PRs explicitly to avoid `git push` 403s.
Each PR build force-pushes a `pr-build/<n>` snapshot branch. Nothing prunes them, so they accumulate. Add a Buildkite step that runs on trunk pushes, lists `pr-build/*` refs, queries each PR's state via the GitHub API, and deletes the refs whose PR is `closed` (covers both merged and rejected — GitHub collapses them). Skips a branch on any non-200 response so we never delete a ref we couldn't verify. Mirrors the wordpress-rs sweep step.
8027996 to
d7830c9
Compare
| exit 0 | ||
| fi | ||
|
|
||
| PR_NUMBER="$BUILDKITE_PULL_REQUEST" |
There was a problem hiding this comment.
Nitpick. Might as well have read BUILDKITE_PULL_REQUEST directly.
| #{XCFRAMEWORK_COMMENT_MARKER} | ||
| ## XCFramework Build | ||
|
|
||
| This PR's XCFramework is available for testing. Add to your `Package.swift`: |
There was a problem hiding this comment.
Nitpick.
| This PR's XCFramework is available for testing. Add to your `Package.swift`: | |
| This PR's XCFramework is available for testing. Add this to your `Package.swift`: |
or
| This PR's XCFramework is available for testing. Add to your `Package.swift`: | |
| This PR's XCFramework is available for testing. Add the following to your `Package.swift`: |
| ) | ||
| else | ||
| github_api( | ||
| server_url: 'https://api.github.com', |
There was a problem hiding this comment.
Nitpick. This URL is duplicated here and in the lane below. I could be extracted in a constant.
| IO.popen(['buildkite-agent', 'annotate', '--context', 'xcframework', '--style', 'info'], 'w') do |io| | ||
| io.write(body) | ||
| end |
There was a problem hiding this comment.
We have buildkite_annotate available in release-toolkit. Example usage in WordPress iOS.
buildkite_annotate(context: 'code-freeze-success', style: 'success', message: message) if is_ciSo, maybe here this would work
| IO.popen(['buildkite-agent', 'annotate', '--context', 'xcframework', '--style', 'info'], 'w') do |io| | |
| io.write(body) | |
| end | |
| buildkite_annotate(context: 'xcframework', style: 'info', message: body) |
There was a problem hiding this comment.
Pull request overview
This PR extends the iOS CI pipeline to publish per-PR signed XCFramework snapshots consumable via SwiftPM branch references, and adds a trunk-only cleanup sweep to delete stale pr-build/* branches for closed PRs. It fits into the existing release/publish automation by separating PR snapshot publishing from the existing trunk/tag S3 publish flow.
Changes:
- Add a
publish_pr_xcframeworkfastlane lane to upload PR artifacts to S3, force-push apr-build/<n>branch withPackage.swiftrewritten to use the binary target, and post/update a sticky PR comment + Buildkite annotation. - Update Buildkite pipeline to (a) gate the existing S3 publish step to non-PR builds, (b) add a PR-only publish step, and (c) add a trunk-only cleanup step.
- Add scripts to publish PR XCFramework artifacts and to prune
pr-build/*branches for PRs that are closed.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
fastlane/Fastfile |
Adds publish_pr_xcframework lane plus helpers to rewrite Package.swift, push pr-build/<n> branches, and upsert PR comments/annotations. |
.buildkite/publish-pr-xcframework.sh |
New PR-only wrapper script to download XCFramework artifacts and invoke the new fastlane lane (skips fork PRs). |
.buildkite/pipeline.yml |
Gates the existing S3 publish step to non-PR builds; adds PR publish and trunk cleanup steps. |
.buildkite/cleanup-pr-build-branches.sh |
New trunk-only cleanup script that queries GitHub PR state and deletes pr-build/* branches for closed PRs in batches. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| lane :publish_pr_xcframework do |options| | ||
| pr_number = options[:pr_number].to_s | ||
| UI.user_error!('pr_number is required') if pr_number.empty? | ||
|
|
||
| branch_name = "pr-build/#{pr_number}" | ||
| version = "pr-builds/#{pr_number}" | ||
|
|
||
| publish_to_s3(version: version) | ||
| push_xcframework_snapshot_branch(branch_name: branch_name, version: version, checksum: xcframework_checksum) |
| def xcframework_comment_body(branch_name:, commit_sha:) | ||
| short_sha = (commit_sha || 'unknown')[0, 8] | ||
| <<~MARKDOWN | ||
| #{XCFRAMEWORK_COMMENT_MARKER} | ||
| ## XCFramework Build | ||
|
|
||
| This PR's XCFramework is available for testing. Add to your `Package.swift`: | ||
|
|
||
| ```swift | ||
| .package(url: "https://github.com/#{GITHUB_REPO}", branch: "#{branch_name}") | ||
| ``` | ||
|
|
||
| <sub>Built from #{short_sha}</sub> | ||
| MARKDOWN |
| def post_buildkite_annotation(body:) | ||
| return unless ENV['BUILDKITE_AGENT_ACCESS_TOKEN'] | ||
|
|
||
| IO.popen(['buildkite-agent', 'annotate', '--context', 'xcframework', '--style', 'info'], 'w') do |io| | ||
| io.write(body) | ||
| end | ||
| end |
| if [[ "$http_code" != "200" ]]; then | ||
| echo "Skipping $branch (HTTP $http_code from GitHub)" | ||
| continue | ||
| fi | ||
|
|
||
| state=$(printf '%s' "$body" | jq -r '.state') | ||
|
|
mokagio
left a comment
There was a problem hiding this comment.
Works as advertised.
This approach will definitely make it easier to fetch builds from WIP branches. Overriding preceding builds on the same PR branch is a bit of a trade-off in term of reproducibility, but I think it's acceptable this is all in the context of PRs, when the build goes into trunk or a tag, it will no longer be removed (however, we are yet to implement the atomic Package.swift update system for those cases)
Have you considered running the pr-build/* cleanup job on a daily schedule rather than as an appendix of trunk builds? I don't know if it would change much and it might be simpler to manage having it as it is, but just wanted to put it out there. Having it on a schedule would feel neater to me. It somehow seems out of place for a trunk build to do cleanup.
Summary
s3://a8c-apps-public-artifacts/gutenbergkit/pr-builds/<n>/, force-push apr-build/<n>branch withPackage.swiftrewritten to consume the binary target, and post a sticky PR comment + Buildkite annotation pointing consumers at the branch.pr-build/*refs whose PR is closed (merged or rejected) so the branches don't accumulate.Fastfile:25–43) gated onbuild.pull_request.id == nullso PRs don't double-upload to the commit-SHA prefix.<!-- gutenbergkit-xcframework-build -->), same lane structure, same sweep approach.How a consumer uses it
The PR comment will look like:
The
pr-build/<n>branch points at the PR's HEAD commit withPackage.swift:9rewritten from.localto.release(version: "pr-builds/<n>", checksum: "<sha>"), so SPM resolves the resources via the binary target on CDN.Changes
Publish
.buildkite/pipeline.yml: Gate the existing:s3: Publish XCFramework to S3step on non-PR builds; add a:swift: :package: Publish PR XCFrameworkstep that runs only whenbuild.pull_request.id != null..buildkite/publish-pr-xcframework.sh(new): Skip fork PRs (no bot creds),source use-bot-for-git, download the build artifacts, and invoke the new fastlane lane.fastlane/Fastfile: Addpublish_pr_xcframeworklane that uploads to S3, rewritesPackage.swift, force-pushespr-build/<n>, posts/updates the sticky PR comment, and writes a Buildkite annotation. Helpers paginate the comments lookup so the marker isn't missed on PRs with >100 comments — small fix relative to the wordpress-rs prior art.Cleanup
.buildkite/cleanup-pr-build-branches.sh(new): Guards onBUILDKITE_BRANCH == "trunk", sourcesuse-bot-for-git, enumerates viagit ls-remote --heads origin 'refs/heads/pr-build/*', queriesGET /repos/wordpress-mobile/GutenbergKit/pulls/<n>withGITHUB_TOKEN, and deletes closed-PR refs with batchedgit push origin :refs/heads/...(chunks of 50). Skips on non-200 responses so we never delete a ref we couldn't verify..buildkite/pipeline.yml: New trunk-only:wastebasket: Clean up pr-build/*step.What we explored
.target. GutenbergKit's equivalent is the .html/.js bundle, but on the snapshot branch we're switchingGutenbergKitResourcesfrom a source target withresources: [.copy("Gutenberg")]to a.binaryTarget— the resources live inside the XCFramework on that path, so we don't need to commit them. Simpler than wordpress-rs.trunk-buildbranch keyed by SHA. Deferred to a follow-up — the existing trunkgutenbergkit/<commit-sha>/upload still runs unchanged, and the use case (downstream apps consuming a known-good trunk binary via SPM branch) isn't load-bearing yet..gitignore:200–202has a comment saying theios/Sources/GutenbergKitResources/Gutenberg/{assets,index.html}files are checked in temporarily until "this is published like Android in CI." Once consumers migrate to.release(...), those 61 files can finally be ignored — separate follow-up so consumers can flip first.Test plan
Publish
This PR exercises its own publish path, so several items are confirmed against build #2235.
Publish PR XCFrameworkstep runs and succeeds — confirmed:swift-package-publish-pr-xcframeworkSUCCESS in build #2235.pr-build/<n>branch appears on the remote withPackage.swift:9rewritten to.release(version: "pr-builds/<n>", checksum: "...")— confirmed:pr-build/495has.release(version: "pr-builds/495", checksum: "d2d8a91c...").<!-- gutenbergkit-xcframework-build -->comment posted with correct branch URL and short SHA — confirmed: comment bywpmobilebot.created_at: 23:26:54Z/updated_at: 23:42:30Zand the latest short SHA8027996cin the body.context: xcframework,style: info).s3://a8c-apps-public-artifacts/gutenbergkit/pr-builds/<n>/has the zip + checksum — confirmed:cdn.a8c-ci.services/gutenbergkit/pr-builds/495/...zipreturns HTTP 200 (18.6 MB) and the.checksum.txtreturns 200.pr-build/<n>branch into a consumer and confirm SPM resolves the XCFramework — confirmed in a scratch SPM consumer:swift package resolveresolvedpr-build/495 (51a9bb6), downloaded the binary artifact from CDN, checksum validated, andswift build --triple arm64-apple-ios17.0-simulatorlinked successfully. The framework contains bothios-arm64andios-arm64_x86_64-simulatorslices, each withGutenbergKitResources(Mach-O dylib), a Swift module (module.modulemap+.swiftinterface), and aGutenbergKit_GutenbergKitResources.bundlecarryingGutenberg/index.html+ 59 web assets.:s3:step and uploads undergutenbergkit/<commit-sha>/— verifiable on first trunk push after merge.BUILDKITE_PULL_REQUEST_REPOmismatch) skips cleanly rather than failing ongit push— not exercised; covered by the early-exit guard inpublish-pr-xcframework.sh.Cleanup
These all require trunk pushes; not verifiable pre-merge.
pr-build/*branches accumulated from publish-side testing.pr-build/<n>branch.pr-build/*branches are left alone.