Skip to content

ci(ios): publish + prune per-PR XCFramework snapshot branches#495

Open
jkmassel wants to merge 2 commits intotrunkfrom
jkmassel/xcframework-per-pr
Open

ci(ios): publish + prune per-PR XCFramework snapshot branches#495
jkmassel wants to merge 2 commits intotrunkfrom
jkmassel/xcframework-per-pr

Conversation

@jkmassel
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel commented May 5, 2026

Summary

  • For each PR commit, upload the signed XCFramework to s3://a8c-apps-public-artifacts/gutenbergkit/pr-builds/<n>/, force-push a pr-build/<n> branch with Package.swift rewritten to consume the binary target, and post a sticky PR comment + Buildkite annotation pointing consumers at the branch.
  • On every trunk push, sweep pr-build/* refs whose PR is closed (merged or rejected) so the branches don't accumulate.
  • Existing trunk/tag publish step (Fastfile:25–43) gated on build.pull_request.id == null so PRs don't double-upload to the commit-SHA prefix.
  • Mirrors Automattic/wordpress-rs#1291 (publish) and #1321 (cleanup) — same comment marker pattern (<!-- gutenbergkit-xcframework-build -->), same lane structure, same sweep approach.

How a consumer uses it

The PR comment will look like:

XCFramework Build

This PR's XCFramework is available for testing. Add to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/<n>")

Built from <short-sha>

The pr-build/<n> branch points at the PR's HEAD commit with Package.swift:9 rewritten from .local to .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 S3 step on non-PR builds; add a :swift: :package: Publish PR XCFramework step that runs only when build.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: Add publish_pr_xcframework lane that uploads to S3, rewrites Package.swift, force-pushes pr-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 on BUILDKITE_BRANCH == "trunk", sources use-bot-for-git, enumerates via git ls-remote --heads origin 'refs/heads/pr-build/*', queries GET /repos/wordpress-mobile/GutenbergKit/pulls/<n> with GITHUB_TOKEN, and deletes closed-PR refs with batched git 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

  • Generated-source equivalent. wordpress-rs commits its Swift FFI bindings onto the snapshot branch because they're gitignored Swift source consumed by a regular .target. GutenbergKit's equivalent is the .html/.js bundle, but on the snapshot branch we're switching GutenbergKitResources from a source target with resources: [.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 snapshot branch. wordpress-rs also publishes a trunk-build branch keyed by SHA. Deferred to a follow-up — the existing trunk gutenbergkit/<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.
  • Dropping the local resource files. .gitignore:200–202 has a comment saying the ios/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 XCFramework step runs and succeeds — confirmed: swift-package-publish-pr-xcframework SUCCESS in build #2235.
  • pr-build/<n> branch appears on the remote with Package.swift:9 rewritten to .release(version: "pr-builds/<n>", checksum: "...") — confirmed: pr-build/495 has .release(version: "pr-builds/495", checksum: "d2d8a91c...").
  • <!-- gutenbergkit-xcframework-build --> comment posted with correct branch URL and short SHA — confirmed: comment by wpmobilebot.
  • Push another commit to the same PR; verify the existing comment is updated (PATCHed), not duplicated — confirmed: the cherry-pick force-push triggered a second build, and the GH API shows a single comment with created_at: 23:26:54Z / updated_at: 23:42:30Z and the latest short SHA 8027996c in the body.
  • Buildkite annotation appears in the build summary with the same body — confirmed via the Buildkite API (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/...zip returns HTTP 200 (18.6 MB) and the .checksum.txt returns 200.
  • Pull the pr-build/<n> branch into a consumer and confirm SPM resolves the XCFramework — confirmed in a scratch SPM consumer: swift package resolve resolved pr-build/495 (51a9bb6), downloaded the binary artifact from CDN, checksum validated, and swift build --triple arm64-apple-ios17.0-simulator linked successfully. The framework contains both ios-arm64 and ios-arm64_x86_64-simulator slices, each with GutenbergKitResources (Mach-O dylib), a Swift module (module.modulemap + .swiftinterface), and a GutenbergKit_GutenbergKitResources.bundle carrying Gutenberg/index.html + 59 web assets.
  • Verify a non-PR push (trunk) still hits the original :s3: step and uploads under gutenbergkit/<commit-sha>/ — verifiable on first trunk push after merge.
  • Confirm a fork PR (or a simulated BUILDKITE_PULL_REQUEST_REPO mismatch) skips cleanly rather than failing on git push — not exercised; covered by the early-exit guard in publish-pr-xcframework.sh.

Cleanup

These all require trunk pushes; not verifiable pre-merge.

  • First trunk merge after this lands sweeps any orphan pr-build/* branches accumulated from publish-side testing.
  • Subsequent trunk merges delete the just-merged PR's pr-build/<n> branch.
  • Step is skipped on PR builds.
  • Open-PR pr-build/* branches are left alone.
  • If GitHub returns non-200 for a PR (deleted, rate-limited, transient), the corresponding branch is preserved and the step continues.

@github-actions github-actions Bot added the [Type] Build Tooling Issues or PRs related to build tooling label May 5, 2026
@wpmobilebot
Copy link
Copy Markdown

wpmobilebot commented May 5, 2026

XCFramework Build

This PR's XCFramework is available for testing. Add to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/495")

Built from d7830c9

@jkmassel jkmassel changed the title ci(ios): publish per-PR XCFramework snapshot branch ci(ios): publish + prune per-PR XCFramework snapshot branches May 5, 2026
@jkmassel jkmassel requested a review from mokagio May 6, 2026 03:37
@jkmassel jkmassel self-assigned this May 6, 2026
jkmassel added 2 commits May 5, 2026 21:37
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.
@jkmassel jkmassel force-pushed the jkmassel/xcframework-per-pr branch from 8027996 to d7830c9 Compare May 6, 2026 03:37
exit 0
fi

PR_NUMBER="$BUILDKITE_PULL_REQUEST"
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.

Nitpick. Might as well have read BUILDKITE_PULL_REQUEST directly.

Comment thread fastlane/Fastfile
#{XCFRAMEWORK_COMMENT_MARKER}
## XCFramework Build

This PR's XCFramework is available for testing. Add to your `Package.swift`:
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.

Nitpick.

Suggested change
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

Suggested change
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`:

Comment thread fastlane/Fastfile
)
else
github_api(
server_url: 'https://api.github.com',
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.

Nitpick. This URL is duplicated here and in the lane below. I could be extracted in a constant.

Comment thread fastlane/Fastfile
Comment on lines +232 to +234
IO.popen(['buildkite-agent', 'annotate', '--context', 'xcframework', '--style', 'info'], 'w') do |io|
io.write(body)
end
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.

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_ci

So, maybe here this would work

Suggested change
IO.popen(['buildkite-agent', 'annotate', '--context', 'xcframework', '--style', 'info'], 'w') do |io|
io.write(body)
end
buildkite_annotate(context: 'xcframework', style: 'info', message: body)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_xcframework fastlane lane to upload PR artifacts to S3, force-push a pr-build/<n> branch with Package.swift rewritten 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.

Comment thread fastlane/Fastfile
Comment on lines +45 to +53
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)
Comment thread fastlane/Fastfile
Comment on lines +166 to +179
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
Comment thread fastlane/Fastfile
Comment on lines +229 to +235
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
Comment on lines +57 to +63
if [[ "$http_code" != "200" ]]; then
echo "Skipping $branch (HTTP $http_code from GitHub)"
continue
fi

state=$(printf '%s' "$body" | jq -r '.state')

Copy link
Copy Markdown
Contributor

@mokagio mokagio left a comment

Choose a reason for hiding this comment

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

Works as advertised.

Image Image

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Build Tooling Issues or PRs related to build tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants