Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion .github/workflows/build-php-cli-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ jobs:
name: Publish Apps CDN artifacts
needs: build
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
pull-requests: write
env:
PHP_VERSION: ${{ inputs.php_version }}
PHP_CLI_VISIBILITY: ${{ inputs.apps_cdn_visibility }}
Expand Down Expand Up @@ -474,7 +478,7 @@ jobs:
if [ "$DRY_RUN" = "true" ]; then
echo "Dry run completed for PHP ${PHP_VERSION} CLI artifacts with ${PHP_CLI_VISIBILITY} visibility." >> "$GITHUB_STEP_SUMMARY"
else
echo "Uploaded PHP ${PHP_VERSION} CLI artifacts with ${PHP_CLI_VISIBILITY} visibility." >> "$GITHUB_STEP_SUMMARY"
echo "Uploaded or updated PHP ${PHP_VERSION} CLI artifacts with ${PHP_CLI_VISIBILITY} visibility." >> "$GITHUB_STEP_SUMMARY"
fi
echo "" >> "$GITHUB_STEP_SUMMARY"

Expand All @@ -492,3 +496,40 @@ jobs:
end
end
'

- name: Update PHP CLI metadata
if: ${{ inputs.upload_to_apps_cdn }}
id: php-cli-metadata
run: |
set -euo pipefail

node scripts/update-php-binary-cdn-metadata.mjs \
--version "${PHP_VERSION}" \
--upload-results "${PWD}/out/php-binaries/apps-cdn-upload-results.json"

if git diff --quiet -- tools/common/lib/php-binary-cdn-metadata.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Create PHP CLI metadata PR
if: ${{ steps.php-cli-metadata.outputs.changed == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

branch="update-php-cli-metadata-${PHP_VERSION}-${GITHUB_RUN_ID}"
# Standard GitHub Actions bot noreply identity for workflow-created commits.
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -c "$branch"
git add tools/common/lib/php-binary-cdn-metadata.json
git commit -m "Update PHP ${PHP_VERSION} binary metadata"
git push --set-upstream origin "$branch"
gh pr create \
--base "${GITHUB_REF_NAME}" \
--head "$branch" \
--title "Update PHP ${PHP_VERSION} binary metadata" \
--body "Updates PHP CLI binary CDN metadata after a successful Apps CDN upload."
9 changes: 9 additions & 0 deletions docs/design-docs/native-php-binaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,12 @@ them as:
- build type: `Production`
- install type: `Full Install`
- platform: `Mac - Silicon`, `Mac - Intel`, or `Windows - x64`

The upload is update-friendly by default. If a matching PHP CLI build already
exists on Apps CDN, the lane lets the CDN replace the existing build artifact
instead of failing on the duplicate version.

After a successful Apps CDN upload, the workflow updates
`tools/common/lib/php-binary-cdn-metadata.json` and opens a PR with the new CDN
URLs and SHA-256 hashes. The metadata keeps one patch version per PHP minor
version; uploading a newer patch replaces the tracked patch for that minor.
8 changes: 4 additions & 4 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ lane :distribute_release_build do |version: read_package_json_version|
)
end

desc 'Upload Studio PHP CLI binaries to Apps CDN'
lane :publish_php_cli_binaries do |version:, artifacts_dir: PHP_CLI_BUILDS_FOLDER, visibility: 'internal', error_on_duplicate: true, output_file: nil|
desc 'Upload or update Studio PHP CLI binaries on Apps CDN'
lane :publish_php_cli_binaries do |version:, artifacts_dir: PHP_CLI_BUILDS_FOLDER, visibility: 'internal', error_on_duplicate: false, output_file: nil|
upload_php_cli_binaries_to_apps_cdn(
version:,
artifacts_dir:,
Expand Down Expand Up @@ -752,7 +752,7 @@ def upload_php_cli_binaries_to_apps_cdn(version:, artifacts_dir:, visibility:, e

sha = read_sha256_sidecar(file_path:)
release_notes = "PHP #{version} CLI binary for #{build[:name]}"
UI.message("Uploading PHP CLI binary: #{build[:file_name]}")
UI.message("Uploading or updating PHP CLI binary: #{build[:file_name]}")
media_url = nil

if dry_run
Expand Down Expand Up @@ -787,7 +787,7 @@ def upload_php_cli_binaries_to_apps_cdn(version:, artifacts_dir:, visibility:, e
UI.user_error!('Apps CDN upload response did not include media_url') if media_url.to_s.empty?

UI.message('--------------------------------')
UI.message("✅ Uploaded PHP CLI binary: #{file_path} to #{media_url}")
UI.message("✅ Uploaded or updated PHP CLI binary: #{file_path} to #{media_url}")
end

builds[build[:file_name]] = {
Expand Down
190 changes: 190 additions & 0 deletions scripts/update-php-binary-cdn-metadata.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/usr/bin/env node

import fs from 'fs';
import path from 'path';

const DEFAULT_METADATA_PATH = path.join(
process.cwd(),
'tools/common/lib/php-binary-cdn-metadata.json'
);

const ARTIFACT_PLATFORM_MAP = {
linux: 'linux',
macos: 'darwin',
windows: 'win32',
};

const ARTIFACT_ARCH_MAP = {
aarch64: 'arm64',
x86_64: 'x64',
};

const ARTIFACT_ORDER = [ 'darwin-arm64', 'darwin-x64', 'win32-x64', 'linux-arm64', 'linux-x64' ];

function parseArgs() {
const args = process.argv.slice( 2 );
const options = {};

for ( let i = 0; i < args.length; i += 2 ) {
const key = args[ i ];
const value = args[ i + 1 ];
if ( ! key?.startsWith( '--' ) || ! value ) {
throw new Error(
`Invalid arguments. Usage: ${ path.basename(
process.argv[ 1 ]
) } --version 8.4.20 --upload-results out/php-binaries/apps-cdn-upload-results.json`
);
}
options[ key.slice( 2 ) ] = value;
}

if ( ! options.version || ! options[ 'upload-results' ] ) {
throw new Error( 'Missing required --version or --upload-results argument.' );
}

return options;
}

function minorVersionFor( version ) {
const match = version.match( /^(\d+\.\d+)\.\d+$/ );
if ( ! match ) {
throw new Error( `Expected a PHP patch version like 8.4.20. Received: ${ version }` );
}
return match[ 1 ];
}

function comparePatchVersions( a, b ) {
const aParts = a.split( '.' ).map( Number );
const bParts = b.split( '.' ).map( Number );

for ( let i = 0; i < Math.max( aParts.length, bParts.length ); i += 1 ) {
const diff = ( aParts[ i ] || 0 ) - ( bParts[ i ] || 0 );
if ( diff !== 0 ) {
return diff;
}
}

return 0;
}

function artifactKeyFor( fileName, version ) {
const match = fileName.match(
/^php-(\d+\.\d+\.\d+)-cli-(linux|macos|windows)-(aarch64|x86_64)\.zip$/
);
if ( ! match ) {
throw new Error( `Unexpected PHP binary artifact filename: ${ fileName }` );
}

const [ , artifactVersion, platform, arch ] = match;
if ( artifactVersion !== version ) {
throw new Error( `Artifact ${ fileName } does not match requested PHP version ${ version }.` );
}

return `${ ARTIFACT_PLATFORM_MAP[ platform ] }-${ ARTIFACT_ARCH_MAP[ arch ] }`;
}

function normalizeMetadata( metadata ) {
if ( ! metadata || typeof metadata !== 'object' || Array.isArray( metadata ) ) {
throw new Error( 'PHP binary CDN metadata must be a JSON object.' );
}

metadata.versions = metadata.versions || {};
return metadata;
}

function orderedObject( object, preferredOrder = [] ) {
const ordered = {};
const keys = Object.keys( object ).sort( ( a, b ) => {
const aIndex = preferredOrder.indexOf( a );
const bIndex = preferredOrder.indexOf( b );

if ( aIndex !== -1 || bIndex !== -1 ) {
return (
( aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex ) -
( bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex )
);
}

return a.localeCompare( b );
} );

for ( const key of keys ) {
ordered[ key ] = object[ key ];
}

return ordered;
}

function validateUploadResult( fileName, result ) {
const url = result?.cdn_url;
const sha = result?.sha?.toLowerCase();

if ( ! url ) {
throw new Error( `Upload result for ${ fileName } does not include cdn_url.` );
}

if ( ! /^https:\/\/.+/.test( url ) ) {
throw new Error( `Upload result for ${ fileName } has an invalid cdn_url: ${ url }` );
}

if ( ! /^[a-f0-9]{64}$/.test( sha ) ) {
throw new Error( `Upload result for ${ fileName } does not include a valid SHA-256 hash.` );
}

return { url, sha };
}

function main() {
const options = parseArgs();
const version = options.version;
const minorVersion = minorVersionFor( version );
const metadataPath = path.resolve( options.metadata || DEFAULT_METADATA_PATH );
const metadata = normalizeMetadata( JSON.parse( fs.readFileSync( metadataPath, 'utf8' ) ) );
const uploadResults = JSON.parse( fs.readFileSync( options[ 'upload-results' ], 'utf8' ) );
const uploadEntries = Object.entries( uploadResults );
const currentVersion = metadata.versions[ minorVersion ]?.version;

if ( uploadEntries.length === 0 ) {
throw new Error( 'Upload results do not include any PHP binary artifacts.' );
}

if ( currentVersion && comparePatchVersions( version, currentVersion ) < 0 ) {
console.log(
`Skipping PHP ${ version } metadata because PHP ${ currentVersion } is already tracked for ${ minorVersion }.`
);
return;
}

const isNewPatch = currentVersion !== version;
const currentArtifacts = isNewPatch ? {} : metadata.versions[ minorVersion ]?.artifacts || {};
const artifacts = { ...currentArtifacts };

for ( const [ fileName, result ] of uploadEntries ) {
const artifactKey = artifactKeyFor( fileName, version );
const { url, sha } = validateUploadResult( fileName, result );

artifacts[ artifactKey ] = {
url,
sha,
};
}

metadata.versions[ minorVersion ] = {
version,
artifacts: orderedObject( artifacts, ARTIFACT_ORDER ),
};
metadata.versions = orderedObject(
metadata.versions,
Object.keys( metadata.versions ).sort( ( a, b ) => comparePatchVersions( b, a ) )
);

fs.writeFileSync( metadataPath, `${ JSON.stringify( metadata, null, '\t' ) }\n` );
console.log( `Updated PHP ${ minorVersion } CDN metadata to ${ version }.` );
}

try {
main();
} catch ( error ) {
console.error( error.message );
process.exit( 1 );
}
21 changes: 21 additions & 0 deletions tools/common/lib/php-binary-cdn-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"versions": {
"8.4": {
"version": "8.4.20",
"artifacts": {
"darwin-arm64": {
"url": "https://appscdn.wordpress.com/downloads/wordpress-com-studio-php-cli/mac-silicon/8.4.20/full-install",
"sha": "55eba456b7240376bd3c3dcc2a99af3fc32aae8b5347f63129332306b9830fbf"
},
"darwin-x64": {
"url": "https://appscdn.wordpress.com/downloads/wordpress-com-studio-php-cli/mac-intel/8.4.20/full-install",
"sha": "4c622f432685ff1839069d40fb34702418784dff01a6803b41867d472051e605"
},
"win32-x64": {
"url": "https://appscdn.wordpress.com/downloads/wordpress-com-studio-php-cli/windows-x64/8.4.20/full-install",
"sha": "948e9804462ab3e2906541ff81937954ce5eab7307c43ba8ac0bef236d42ec87"
}
}
}
}
}