Skip to content
Open
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
4 changes: 4 additions & 0 deletions .buildkite/commands/build-for-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ If ($LastExitCode -ne 0) { Exit $LastExitCode }
Write-Host "--- :package: Building AppX package"
node scripts/package-appx.mjs
If ($LastExitCode -ne 0) { Exit $LastExitCode }

Write-Host "--- :package: Building standalone CLI bundle"
npm run cli:bundle -- win32 $Architecture
If ($LastExitCode -ne 0) { Exit $LastExitCode }
13 changes: 13 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,15 @@ steps:

echo "--- 📃 Notarizing Binary"
bundle exec fastlane notarize_binary

echo "--- :package: Building standalone CLI bundle"
npm run cli:bundle -- darwin {{matrix}}
plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN]
artifact_paths:
- apps/studio/out/**/*.app.zip
- apps/studio/out/*.dmg
- standalone-bundles/studio-cli-*.tgz
- standalone-bundles/studio-cli-*.tgz.sha256
matrix:
- x64
- arm64
Expand All @@ -243,6 +248,8 @@ steps:
- apps\studio\out\**\studio-update.nupkg
- apps\studio\out\**\RELEASES
- apps\studio\out\**\*.appx
- standalone-bundles/studio-cli-*.tgz
- standalone-bundles/studio-cli-*.tgz.sha256
plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN]
matrix:
- x64
Expand Down Expand Up @@ -283,8 +290,13 @@ steps:

echo "--- :node: Building DEB"
npm run make:linux-{{matrix}}

echo "--- :package: Building standalone CLI bundle"
npm run cli:bundle -- linux {{matrix}}
artifact_paths:
- apps/studio/out/make/deb/**/*.deb
- standalone-bundles/studio-cli-*.tgz
- standalone-bundles/studio-cli-*.tgz.sha256
plugins:
# Amazon Linux on the `default` queue lacks `dpkg`/`fakeroot`
# required by electron-forge's MakerDeb, so we build inside a
Expand All @@ -309,6 +321,7 @@ steps:
buildkite-agent artifact download "*.nupkg" .
buildkite-agent artifact download "*\\RELEASES" .
buildkite-agent artifact download "*.deb" .
buildkite-agent artifact download "standalone-bundles/*" .

.buildkite/commands/install-node-dependencies.sh

Expand Down
13 changes: 13 additions & 0 deletions .buildkite/release-build-and-distribute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,15 @@ steps:

echo "--- 📃 Notarizing Binary"
bundle exec fastlane notarize_binary

echo "--- :package: Building standalone CLI bundle"
npm run cli:bundle -- darwin {{matrix}}
plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN]
artifact_paths:
- apps/studio/out/**/*.app.zip
- apps/studio/out/*.dmg
- standalone-bundles/studio-cli-*.tgz
- standalone-bundles/studio-cli-*.tgz.sha256
matrix:
- x64
- arm64
Expand All @@ -78,6 +83,8 @@ steps:
- apps\studio\out\**\studio-update.nupkg
- apps\studio\out\**\RELEASES
- apps\studio\out\**\*.appx
- standalone-bundles/studio-cli-*.tgz
- standalone-bundles/studio-cli-*.tgz.sha256
plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN]
matrix:
- x64
Expand Down Expand Up @@ -109,8 +116,13 @@ steps:

echo "--- :node: Building DEB"
npm run make:linux-{{matrix}}

echo "--- :package: Building standalone CLI bundle"
npm run cli:bundle -- linux {{matrix}}
artifact_paths:
- apps/studio/out/make/deb/**/*.deb
- standalone-bundles/studio-cli-*.tgz
- standalone-bundles/studio-cli-*.tgz.sha256
plugins:
# Same Docker-on-default-queue pattern as the dev Linux build group
# (see .buildkite/pipeline.yml): Amazon Linux on `default` lacks
Expand Down Expand Up @@ -141,6 +153,7 @@ steps:
buildkite-agent artifact download "*.nupkg" .
buildkite-agent artifact download "*.deb" .
buildkite-agent artifact download "*\\RELEASES" .
buildkite-agent artifact download "standalone-bundles/*" .

.buildkite/commands/install-node-dependencies.sh

Expand Down
31 changes: 31 additions & 0 deletions docs/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,37 @@ ruby fastlane/test/studio_release_version_test.rb
ruby fastlane/test/studio_release_git_test.rb
```

## Standalone Studio CLI Bundles (curl installer)

Alongside the desktop app, every Studio build also publishes the standalone Studio CLI bundles — the `.tgz` (gzipped tar) archives the `install.sh` / `install.ps1` curl installers download — to the Apps CDN under the **`WordPress.com Studio CLI`** product. This is part of the normal build/distribute flow, so it happens for nightly/dev, beta, and stable builds with no separate step to run:

- Each platform's build group builds its own bundle (`npm run cli:bundle <platform> <arch>`) on its own runner, producing `standalone-bundles/studio-cli-<platform>-<arch>.tgz` + a `.sha256` sidecar for all six platform/arch targets.
- `distribute_builds` uploads the bundles with the **same version, build type, and visibility as the app build**, so the CLI tracks the app exactly: External for nightly/beta, and Internal-then-flipped-public for stable (their CDN post IDs ride the draft GitHub release and are flipped to External by `publish_release`, just like the app builds).

### How the installers find a bundle

`install.sh` / `install.ps1` download straight from the Apps CDN's versionless **`latest`** alias (a `302` to the newest published bundle), so they need no redirect layer:

```
https://appscdn.wordpress.com/downloads/wordpress-com-studio-cli/<slug>/latest/full-install
```

`<slug>` is the CDN platform slug (`mac-silicon`, `mac-intel`, `windows-x64`, `windows-arm64`, `linux-x64`, `linux-arm64`). Two env vars adjust this:

- `STUDIO_CLI_VERSION` — install a specific version instead of `latest` (e.g. `v1.11.0`).
- `STUDIO_CLI_URL` — bypass the CDN entirely and fetch `studio-cli-<platform>-<arch>.tgz` (+ `.sha256` sidecar) from a base URL or local dir. Used for local testing and mirrors; this path verifies the checksum, whereas the CDN path relies on HTTPS plus a staged-extraction guard (the CDN exposes the SHA-256 only as build metadata, not a downloadable sidecar).

Only the `install.sh` / `install.ps1` scripts themselves still need public hosting — the `curl … | bash` / `irm … | iex` entry points (e.g. `wp.build/install.sh`); the bundles come directly from the CDN.

To (re)publish CLI bundles without a full app release — e.g. backfilling — build them and run the standalone lane:

```sh
npm run cli:bundle -- darwin arm64 # repeat per platform/arch (each must be built on its own OS)
DRY_RUN=true bundle exec fastlane publish_studio_cli_binaries version:"v1.11.0"
```

Drop `DRY_RUN=true` to upload for real. The `version:` must match the app release's `v`-prefixed version so the bundles share its CDN path (it defaults to the current `package.json` version, `v`-prefixed). `visibility:` (default `external`) and `build_type:` (default `Production`) can be overridden.

## Running Lanes Locally

Lanes can be run locally for testing. Common requirements are Ruby and Bundler. Additional credentials depend on the lane:
Expand Down
130 changes: 130 additions & 0 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ PROJECT_ROOT_FOLDER = File.dirname(File.expand_path(__dir__))
SECRETS_FOLDER = File.join(Dir.home, '.configure', 'studio', 'secrets')
BUILDS_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'apps', 'studio', 'out')
PHP_CLI_BUILDS_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'out', 'php-binaries')
# Standalone Studio CLI bundles (studio-cli-<platform>-<arch>.tgz) from `npm run cli:bundle`
STANDALONE_BUNDLES_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'standalone-bundles')

# Enable dry run mode through environment variable
DRY_RUN = ENV['DRY_RUN'] == 'true'
Expand All @@ -43,6 +45,7 @@ APPLE_API_KEY_PATH = File.join(SECRETS_FOLDER, 'app_store_connect_fastlane_api_k
# Site ID for WordPress.com Studio in the Apps CDN
WPCOM_STUDIO_SITE_ID = '239164481'
WPCOM_STUDIO_PHP_CLI_PRODUCT = 'WordPress.com Studio PHP CLI'
WPCOM_STUDIO_CLI_PRODUCT = 'WordPress.com Studio CLI'

GITHUB_REPO = 'Automattic/studio'
MAIN_BRANCH = 'trunk'
Expand Down Expand Up @@ -300,6 +303,19 @@ lane :publish_php_cli_binaries do |version:, artifacts_dir: PHP_CLI_BUILDS_FOLDE
)
end

desc 'Upload or update Studio CLI standalone bundles on Apps CDN'
# version defaults to the app's `v`-prefixed form so a manual publish lands at the same CDN
# path the release flow uses (distribute_builds passes the same v-prefixed version).
lane :publish_studio_cli_binaries do |version: "v#{read_package_json_version}", artifacts_dir: STANDALONE_BUNDLES_FOLDER, build_type: 'Production', visibility: 'external', error_on_duplicate: false|
upload_studio_cli_binaries_to_apps_cdn(
version:,
artifacts_dir:,
build_type:,
visibility:,
error_on_duplicate: error_on_duplicate.to_s != 'false'
)
end

########################################################################
# Release Management Lanes
########################################################################
Expand Down Expand Up @@ -751,6 +767,24 @@ def distribute_builds(
build[:post_id] = result[:post_id]
end

# Upload the standalone Studio CLI bundles alongside the app so the curl installers
# track every Studio build (nightly/dev, beta, stable). Merging them into
# builds_to_upload routes their post IDs through create_draft_github_release, so
# publish_release flips them to External exactly like the app builds. Skipped when no
# bundles are present (e.g. a local app-only distribute run that didn't build them).
if Dir.exist?(STANDALONE_BUNDLES_FOLDER)
cli_builds = upload_studio_cli_binaries_to_apps_cdn(
version: version,
artifacts_dir: STANDALONE_BUNDLES_FOLDER,
build_type: build_type,
visibility: visibility,
error_on_duplicate: false
)
builds_to_upload = builds_to_upload.merge(cli_builds)
else
UI.important("No standalone CLI bundles in #{STANDALONE_BUNDLES_FOLDER}; skipping Studio CLI CDN upload.")
end

unless DRY_RUN
buildkite_annotate(
context: 'cdn-link',
Expand Down Expand Up @@ -956,6 +990,102 @@ def upload_php_cli_binaries_to_apps_cdn(version:, artifacts_dir:, visibility:, e
builds
end

# The standalone Studio CLI bundles to publish, one per platform/arch. File names are
# versionless (the curl installers fetch them by this exact name); the CDN path's version
# comes from the upload's `version:`. `platform` maps to the Apps CDN platform label, the
# same labels the desktop app and PHP CLI uploads use.
def studio_cli_cdn_builds
[
{ key: :cli_mac_arm64, file_name: 'studio-cli-darwin-arm64.tgz', name: 'macOS arm64', platform: 'Mac - Silicon' },
{ key: :cli_mac_x64, file_name: 'studio-cli-darwin-x64.tgz', name: 'macOS x64', platform: 'Mac - Intel' },
{ key: :cli_windows_x64, file_name: 'studio-cli-win32-x64.tgz', name: 'Windows x64', platform: 'Windows - x64' },
{ key: :cli_windows_arm64, file_name: 'studio-cli-win32-arm64.tgz', name: 'Windows arm64', platform: 'Windows - ARM64' },
{ key: :cli_linux_x64, file_name: 'studio-cli-linux-x64.tgz', name: 'Linux x64', platform: 'Linux - x64' },
{ key: :cli_linux_arm64, file_name: 'studio-cli-linux-arm64.tgz', name: 'Linux arm64', platform: 'Linux - ARM64' }
]
end

# Upload (or update) the standalone Studio CLI bundles on the Apps CDN.
#
# Mirrors {upload_php_cli_binaries_to_apps_cdn} but targets the `WordPress.com Studio CLI`
# product. Each bundle is verified against its `.sha256` sidecar before upload. `build_type`
# and `visibility` are passed through from the caller so the CLI tracks the app exactly
# (External for nightly/beta, Internal-then-flipped-public for stable). `build_number` is nil
# so the CDN URL ends in `/full-install` (the path the curl installers expect). Returns a hash
# keyed per build including each upload's `post_id`, so the caller can fold it into the release
# builds and let publish_release flip visibility to External.
def upload_studio_cli_binaries_to_apps_cdn(version:, artifacts_dir:, build_type:, visibility:, error_on_duplicate:, dry_run: DRY_RUN)
normalized_visibility = visibility.to_s.downcase.to_sym
unless %i[internal external].include?(normalized_visibility)
UI.user_error!('visibility must be :internal or :external')
end

builds = {}
studio_cli_cdn_builds.each do |build|
file_path = File.join(artifacts_dir, build[:file_name])

# A real upload requires every bundle; a dry run still previews the rest, so you
# can sanity-check the lane after building only your current platform locally.
unless File.exist?(file_path)
UI.user_error!("File #{file_path} does not exist") unless dry_run
UI.important("[DRY RUN] #{build[:file_name]} not found in #{artifacts_dir} — skipping (would upload to '#{WPCOM_STUDIO_CLI_PRODUCT}' / #{build[:platform]} / #{version}).")
next
end

sha = read_sha256_sidecar(file_path:)
release_notes = "Studio CLI #{version} for #{build[:name]}"
UI.message("Uploading Studio CLI bundle: #{build[:file_name]}")
media_url = nil
post_id = nil

if dry_run
UI.message('[DRY RUN] Upload step skipped due to dry run mode.')
UI.message(" File exists at: #{file_path}")
UI.message(" file size: #{File.size(file_path) / 1024 / 1024} MB")
UI.message(" product: #{WPCOM_STUDIO_CLI_PRODUCT}")
UI.message(" platform: #{build[:platform]}")
UI.message(" build type: #{build_type}")
UI.message(' install type: Full Install')
UI.message(" visibility: #{normalized_visibility}")
UI.message(" version: #{version}")
UI.message(" release notes: #{release_notes}")
UI.message(" sha: #{sha}")
UI.message(" error on duplicate: #{error_on_duplicate}")
else
result = upload_build_to_apps_cdn(
site_id: WPCOM_STUDIO_SITE_ID,
product: WPCOM_STUDIO_CLI_PRODUCT,
platform: build[:platform],
build_type: build_type,
install_type: 'Full Install',
visibility: normalized_visibility,
version: version,
build_number: nil,
release_notes: release_notes,
sha: sha,
file_path: file_path,
error_on_duplicate: error_on_duplicate
)
media_url = result[:media_url] || result['media_url']
post_id = result[:post_id] || result['post_id']
UI.user_error!('Apps CDN upload response did not include media_url') if media_url.to_s.empty?

UI.message('--------------------------------')
UI.message("✅ Uploaded Studio CLI bundle: #{file_path} to #{media_url}")
end

builds[build[:key]] = {
name: build[:name],
platform: build[:platform],
cdn_url: media_url,
post_id: post_id,
sha: sha
}
end

builds
end

########################################################################
# Release Management Helper Methods
########################################################################
Expand Down
6 changes: 3 additions & 3 deletions scripts/create-standalone-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
* cli/ CLI bundle (main.mjs, node_modules, wp-files, …)
*
* Output:
* standalone-bundles/studio-cli-{platform}-{arch}.tar.gz
* standalone-bundles/studio-cli-{platform}-{arch}.tar.gz.sha256
* standalone-bundles/studio-cli-{platform}-{arch}.tgz
* standalone-bundles/studio-cli-{platform}-{arch}.tgz.sha256
*
* Prerequisites: Node.js >= 22, npm dependencies installed
*
Expand Down Expand Up @@ -58,7 +58,7 @@ if ( ! supportedArchs.includes( archArg ) ) {
}

const isWindows = platformArg === 'win32';
const bundleName = `studio-cli-${ platformArg }-${ archArg }.tar.gz`;
const bundleName = `studio-cli-${ platformArg }-${ archArg }.tgz`;
const outputDir = path.join( repoRoot, 'standalone-bundles' );
const stagingDir = path.join( outputDir, `staging-${ platformArg }-${ archArg }` );
const cliDistDir = path.join( repoRoot, 'apps', 'cli', 'dist', 'cli' );
Expand Down
Loading