Skip to content
Draft
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
39 changes: 34 additions & 5 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,26 @@ steps:
- $CI_TOOLKIT_PLUGIN
- $NVM_PLUGIN

- label: ':white_check_mark: Validate Swift release ${NEW_VERSION:-(no version)}'
key: validate-release
if: build.env("NEW_VERSION") != null && build.branch == "trunk" && build.pull_request.id == null
command: |
install_gems
bundle exec fastlane validate "version:$NEW_VERSION"
plugins: *plugins

- label: ':eslint: Lint React App'
key: lint-js
command: make lint-js
plugins: *plugins

- label: ':javascript: Test JavaScript'
key: test-js
command: make test-js
plugins: *plugins

- label: ':performing_arts: Test Web E2E'
key: test-web-e2e
depends_on: build-react
command: |
buildkite-agent artifact download dist.tar.gz .
Expand Down Expand Up @@ -97,15 +108,19 @@ steps:

- label: ':s3: Publish XCFramework to S3'
depends_on: build-xcframework
if: build.pull_request.id == null
# This step only covers per-commit trunk uploads keyed by the commit
# SHA. Releases are handled by `:rocket: Publish Swift release` (gated
# on `NEW_VERSION`), and tag pushes from that step trigger a separate
# tag build whose iOS upload would just duplicate the rocket step's —
# the `build.tag == null` clause skips it. Android publish on tag
# builds is still load-bearing (produces the `vX.Y.Z` Maven artifact)
# and intentionally not gated here.
if: build.pull_request.id == null && build.env("NEW_VERSION") == null && build.tag == null
command: |
buildkite-agent artifact download '*.xcframework.zip' .
buildkite-agent artifact download '*.xcframework.zip.checksum.txt' .
install_gems
# Version precedence: explicit `NEW_VERSION` override wins, then a
# tag build publishes under the tag, otherwise fall back to the
# commit SHA so every push gets a stable artifact URL.
bundle exec fastlane publish_to_s3 version:${NEW_VERSION:-${BUILDKITE_TAG:-$BUILDKITE_COMMIT}}
bundle exec fastlane publish_to_s3 version:$BUILDKITE_COMMIT
plugins: *plugins

- label: ':swift: :package: Publish PR XCFramework'
Expand All @@ -114,7 +129,21 @@ steps:
command: .buildkite/publish-pr-xcframework.sh
plugins: *plugins

- label: ':rocket: Publish Swift release ${NEW_VERSION:-(no version)}'
depends_on:
- validate-release
- build-xcframework
- swift-test-swift-package
- lint-js
- test-js
- test-web-e2e
- test-ios-e2e
if: build.env("NEW_VERSION") != null && build.branch == "trunk" && build.pull_request.id == null
command: .buildkite/release.sh
plugins: *plugins

- label: ':ios: Test iOS E2E'
key: test-ios-e2e
depends_on: build-react
command: |
buildkite-agent artifact download dist.tar.gz .
Expand Down
21 changes: 21 additions & 0 deletions .buildkite/release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail

if [[ -z "${NEW_VERSION:-}" ]]; then
echo "ERROR: NEW_VERSION is not set or empty." >&2
echo "Set NEW_VERSION=vX.Y.Z when triggering this build." >&2
exit 1
fi

echo '--- :robot_face: Use bot for Git operations'
source use-bot-for-git

echo '--- :arrow_down: Downloading XCFramework artifacts'
buildkite-agent artifact download '*.xcframework.zip' . --step "build-xcframework"
buildkite-agent artifact download '*.xcframework.zip.checksum.txt' . --step "build-xcframework"

echo '--- :rubygems: Setting up Gems'
install_gems

echo "--- :rocket: Publishing Swift release $NEW_VERSION"
bundle exec fastlane release "version:$NEW_VERSION"
90 changes: 34 additions & 56 deletions bin/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,6 @@ check_working_directory() {
check_dependencies() {
local missing_deps=()

if ! command -v gh &> /dev/null; then
missing_deps+=("gh (GitHub CLI)")
fi

if ! command -v npm &> /dev/null; then
missing_deps+=("npm")
fi
Expand Down Expand Up @@ -132,20 +128,6 @@ calculate_new_version() {
esac
}

# Function to check if a version is a prerelease
is_prerelease() {
local version=$1

# Use semver to check if the version has prerelease identifiers
local result=$(node -p "require('semver').prerelease('$version') !== null")

if [ "$result" = "true" ]; then
return 0 # It is a prerelease
else
return 1 # It is not a prerelease
fi
}

# Function to validate version type
validate_version_type() {
local version_type=$1
Expand Down Expand Up @@ -262,20 +244,6 @@ commit_changes() {
print_success "Changes committed with message: chore(release): $version"
}

# Function to create git tag
create_tag() {
local version=$1

print_status "Creating git tag: v$version"

if [ "$DRY_RUN" = "true" ]; then
return
fi

git tag "v$version"
print_success "Tag created: v$version"
}

# Function to push changes
push_changes() {
local version=$1
Expand All @@ -286,22 +254,38 @@ push_changes() {
return
fi

git push origin trunk --tags
git push origin trunk
print_success "Changes pushed successfully"
}

# Function to create GitHub release
create_github_release() {
# Function to print the post-push instructions for kicking off the
# Buildkite publish build. CI creates the tag and the GitHub release —
# this script just bumps the version files on trunk.
print_publish_instructions() {
local version=$1

print_status "Creating GitHub release: v$version"
local sha=$2
local tag="v$version"
local prefix=""

if [ "$DRY_RUN" = "true" ]; then
return
prefix="[DRY RUN] "
fi

gh release create "v$version" --generate-notes --title "$version"
print_success "GitHub release created: v$version"
echo
print_status "${prefix}Next: trigger the Buildkite publish build."
echo
echo " 1. Open https://buildkite.com/automattic/gutenbergkit/builds/new"
echo " 2. Branch: trunk"
echo " 3. Commit: $sha"
echo " 4. Environment Variables: NEW_VERSION=$tag"
echo
echo "Pin the Commit field to the SHA above — otherwise Buildkite resolves"
echo "'trunk' to whatever HEAD is at trigger time, and a concurrent merge"
echo "would tag the wrong commit."
echo
echo "The :rocket: 'Publish Swift release' step will build + sign the"
echo "XCFramework, upload it to S3, and publish the GitHub Release —"
echo "which also creates the $tag tag."
}

# Main function
Expand Down Expand Up @@ -381,35 +365,29 @@ main() {
commit_changes "$new_version"
echo

create_tag "$new_version"
echo

push_changes "$new_version"
echo

# Only create GitHub release for non-prerelease versions
if is_prerelease "$new_version"; then
print_status "Skipping GitHub release creation for prerelease version"
# Capture the SHA of the just-pushed release commit so the operator can
# pin it when triggering the Buildkite publish build (avoids drift if
# trunk moves between this push and the build trigger).
local pushed_sha
if [ "$DRY_RUN" = "true" ]; then
pushed_sha="<sha-of-pushed-commit>"
else
create_github_release "$new_version"
pushed_sha=$(git rev-parse HEAD)
fi
echo

# Summary
print_success "Release process completed successfully!"
print_success "Version bump completed successfully!"
print_status "Version: $current_version -> $new_version"

if [ "$DRY_RUN" = "true" ]; then
print_warning "This was a dry run. No actual changes were made."
print_status "To perform the actual release, run: make release VERSION_TYPE=$version_type"
else
if is_prerelease "$new_version"; then
print_status "Prerelease tag v$new_version has been created and pushed."
print_status "No GitHub release was created for this prerelease version."
else
print_status "The release is ready for integration into the WordPress app."
fi
fi

print_publish_instructions "$new_version" "$pushed_sha"
}

# Run main function with all arguments
Expand Down
52 changes: 45 additions & 7 deletions docs/releases.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# GutenbergKit Release Process

Use the provided release script to automate the entire process:
## How publishing works

Every push to `trunk` publishes both platforms automatically:

- **Android**: the `:android: Publish Android Library` step pushes a Maven artifact keyed by the commit (consumable via Git revision pins).
- **iOS**: the `:s3: Publish XCFramework to S3` step uploads the signed XCFramework under `gutenbergkit/<commit-sha>/`, and `Publish PR XCFramework` does the same on PR builds under a `pr-build/<n>` snapshot branch.

A **tagged release** is a separate, manually-triggered publish flow on top of that: it produces a stable `vX.Y.Z` tag whose `Package.swift` points at the prebuilt XCFramework on CDN, plus a GitHub Release with the XCFramework attached. SPM consumers pin the tag; everything else can pin a commit/branch.

The tagged release happens in two steps: a local script bumps the version on `trunk`, then a CI build creates the tag and the GitHub release.

## Step 1 — Bump versions on trunk

Run the release script:

```bash
# Standard version increments
Expand Down Expand Up @@ -31,13 +44,38 @@ The script:
1. Ensures required dependencies are installed
1. Increments the version number[^1]
1. Builds the project[^2]
1. Commits changes
1. Creates a Git tag
1. Pushes to `origin/trunk` with tags
1. Creates a GitHub release
1. Creates a new release on GitHub: `gh release create vX.X.X --generate-notes --title "X.X.X"`
1. Commits the version bump as `chore(release): X.Y.Z`
1. Pushes to `origin/trunk`

It does **not** create the git tag or the GitHub release — that's Step 2.

## Step 2 — Publish via Buildkite

Step 1 prints the SHA of the version-bump commit it just pushed. Trigger a new Buildkite build with that SHA pinned:

1. Open <https://buildkite.com/automattic/gutenbergkit/builds/new>
2. **Branch**: `trunk`
3. **Commit**: the SHA printed by Step 1
4. **Environment Variables**: `NEW_VERSION=vX.Y.Z`

Pinning the commit matters — if you leave it blank, Buildkite resolves `trunk` to HEAD at trigger time, and a concurrent merge would tag the wrong commit.

The build runs a `:white_check_mark: Validate Swift release` step early on (gated on `NEW_VERSION`) that fast-fails if the tag name is malformed, or if the tag or GitHub Release already exists. After that, the `:rocket: Publish Swift release` step:

1. Rewrites `Package.swift` to consume the binary target via `.release(version:, checksum:)`
1. Uploads the XCFramework to `s3://a8c-apps-public-artifacts/gutenbergkit/vX.Y.Z/`
1. Commits the rewrite on a local `release/vX.Y.Z` branch (never pushed to origin), tags `vX.Y.Z`, and pushes **only the tag** — `git push <tag>` carries the commit along with the tag ref, so the commit becomes reachable on origin via the tag alone
1. Creates the GitHub Release against the now-existing tag, uploading the XCFramework + checksum as assets (adds `--prerelease` when the version contains `-`)

The tag is pushed before the GitHub Release is created. Once the tag is on origin, SPM consumers pinning `vX.Y.Z` can resolve a `Package.swift` that fetches the prebuilt XCFramework from CDN — the GH Release is metadata and an asset mirror on top of that.

The tag's commit lives off `trunk`'s history (parented on `trunk` but only reachable via the tag ref), matching the `pr-build/<n>` snapshot-branch shape but published under a tag instead of a branch.

### Recovering from a partial publish

If the build fails before the tag is pushed (validate, Package.swift rewrite, S3 upload, or local commit/tag), no tag exists and no consumer can resolve `vX.Y.Z`. Re-run Step 2 with the same `NEW_VERSION` once the underlying issue is fixed — `validate` will pass (no tag, no release), and S3 uploads are idempotent (`if_exists: :replace`).

After the release is created, it is ready for integration into the WordPress app.
If the build fails specifically on `gh release create` (tag pushed, but GH Release missing), the tag is the source of truth: SPM consumers resolving `vX.Y.Z` already work. To create the missing Release page, re-run `gh release create vX.Y.Z --title vX.Y.Z --generate-notes [--prerelease] <xcframework.zip> <checksum.txt>` manually against the existing tag — re-running the full Buildkite step would fail at `validate` because the tag now exists.

## Release Notes

Expand Down
24 changes: 12 additions & 12 deletions docs/wordpress-app-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ Make sure the path points to your local GutenbergKit clone relative to your Word

1. Copy `local-builds.gradle-example` to `local-builds.gradle`
2. Uncomment the `localGutenbergKitPath` line and set it to your local GutenbergKit path:
```groovy
localGutenbergKitPath = "../GutenbergKit"
```
```groovy
localGutenbergKitPath = "../GutenbergKit"
```
3. Run Gradle sync — this substitutes the Maven dependency with the local project

### Git Revision
Expand Down Expand Up @@ -72,7 +72,7 @@ CI (Buildkite) publishes builds for PRs to the Maven repository automatically.

**Use case**: Integrating GutenbergKit work into WordPress app trunk before a formal release.

Pre-releases create alpha version tags without creating a GitHub Release. They're useful for getting changes into the WordPress apps' main branches early.
Pre-releases create alpha version tags with a GitHub Release marked `--prerelease`. They're useful for getting changes into the WordPress apps' main branches early.

#### Creating a Pre-release

Expand All @@ -89,7 +89,7 @@ Available version types:
- `premajor` — increments major and adds alpha suffix (0.13.2 → 1.0.0-alpha.0)
- `prerelease` — increments the alpha number (0.13.3-alpha.0 → 0.13.3-alpha.1)

This pushes a git tag (e.g., `v0.13.3-alpha.0`) and CI publishes the Android build to the Maven repository.
Every trunk push already publishes per-commit artifacts (Android → Maven, iOS → S3 keyed by commit SHA). This bumps the version on `trunk` so the next per-commit publish carries that version, and the follow-up Buildkite build triggered with `NEW_VERSION=v0.13.3-alpha.0` adds the `vX.Y.Z` tag, the binary-target `Package.swift`, and the GitHub prerelease. See [Release Process](./releases.md) for the full flow.

#### iOS

Expand Down Expand Up @@ -127,7 +127,7 @@ Available version types:
- `minor` — new features, backwards compatible (0.13.2 → 0.14.0)
- `major` — breaking changes (0.13.2 → 1.0.0)

This creates a GitHub Release with auto-generated notes and CI publishes the Android build to the Maven repository.
Every trunk push already publishes per-commit artifacts (Android → Maven, iOS → S3 keyed by commit SHA). This bumps the version on `trunk` so the next per-commit publish carries that version, and the follow-up Buildkite build triggered with `NEW_VERSION=v0.13.3` adds the `vX.Y.Z` tag, the binary-target `Package.swift`, and the GitHub Release. See [Release Process](./releases.md) for the full flow.

#### iOS

Expand All @@ -147,12 +147,12 @@ gutenberg-kit = '0.13.3'

## Workflow Recommendations

| Scenario | Recommended Method |
| --------------------------------- | ------------------ |
| Active feature development | Local Development |
| PR review / testing | Git Revision |
| Merging to WordPress app trunk | Pre-release |
| WordPress app release | Formal Release |
| Scenario | Recommended Method |
| ------------------------------ | ------------------ |
| Active feature development | Local Development |
| PR review / testing | Git Revision |
| Merging to WordPress app trunk | Pre-release |
| WordPress app release | Formal Release |

## Platform-Specific Notes

Expand Down
Loading
Loading