Skip to content

Commit

Permalink
[CMSP-481] Remove upstream-require script (#105)
Browse files Browse the repository at this point in the history
* add suggest to root composer.json

* remove UpstreamRequire

* remove updatelocaldependencies & add magic to unset upstream-require

* fix linting issues

* remove scripts-descriptions if empty

* add our postUpdate hook & remove upstream-require if it doesn't exist

* Apply --dry-run fixes

copy pasta from https://github.com/pantheon-upstreams/wordpress-composer-managed/pull/4/files

* closing brace on new line because psr-4 is dumb
  • Loading branch information
jazzsequence committed Jul 31, 2024
1 parent bc34dcb commit e529d16
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 95 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,8 @@
},
"scripts-descriptions": {
"upstream-require": "Add a dependency to an upstream. See https://pantheon.io/docs/create-custom-upstream for information on creating custom upstreams."
},
"suggest": {
"pantheon-systems/upstream-management": "Composer plugin that provides commands for managing custom upstreams on Pantheon."
}
}
3 changes: 2 additions & 1 deletion upstream-configuration/composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "pantheon-upstreams/upstream-configuration",
"type": "project",
"version": "dev-main",
"repositories": [
{
"type": "composer",
Expand All @@ -12,4 +13,4 @@
"extra": {
"_README": "To make a custom upstream, clone this repository and add any dependencies to be provided by this upstream to this composer.json file. Leave the root-level composer.json file for the exclusive use of each individual site; do not modify it after your custom upstream has been published. See https://pantheon.io/docs/create-custom-upstream for more information."
}
}
}
250 changes: 156 additions & 94 deletions upstream-configuration/scripts/ComposerScripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/**
* @file
* Contains \WordPressComposerManaged\ComposerScripts.
*
* Custom Composer scripts and implementations of Composer hooks.
*/

namespace WordPressComposerManaged;
Expand All @@ -11,120 +13,180 @@

class ComposerScripts
{

/**
* Add a dependency to the upstream-configuration section of a custom upstream.
*
* The upstream-configuration/composer.json is a place to put modules, themes
* and other dependencies that will be inherited by all sites created from
* the upstream. Separating the upstream dependencies from the site dependencies
* has the advantage that changes can be made to the upstream without causing
* conflicts in the downstream sites.
*
* To add a dependency to an upstream:
*
* composer upstream-require wpackagist-plugin/plugin-name
*
* Important: Dependencies should only be removed from upstreams with caution.
* The module / theme must be uninstalled from all sites that are using it
* before it is removed from the code base; otherwise, the module cannot be
* cleanly uninstalled.
* Prepare for Composer to update dependencies.
*/
public static function upstreamRequire(Event $event)
public static function preUpdate(Event $event)
{
}

/**
* postUpdate
*
* After "composer update" runs, we have the opportunity to do additional
* fixups to the project files.
*
* @param Composer\Script\Event $event
* The Event object passed in from Composer
*/
public static function postUpdate(Event $event)
{
// for future use
}

/**
* Apply composer.json Updates
*
* During the Composer pre-update hook, check to see if there are any
* updates that need to be made to the composer.json file. We cannot simply
* change the composer.json file in the upstream, because doing so would
* result in many merge conflicts.
*/
public static function applyComposerJsonUpdates(Event $event)
{
$io = $event->getIO();
$composer = $event->getComposer();
$name = $composer->getPackage()->getName();
$gitRepoUrl = exec('git config --get remote.origin.url');

// Refuse to run if:
// - This is a clone of the standard Pantheon upstream, and it hasn't been renamed
// - This is an local working copy of a Pantheon site instread of the upstream
$isPantheonStandardUpstream = (strpos($name, 'pantheon-systems/wordpress-composer-managed') !== false);
$isPantheonSite = (strpos($gitRepoUrl, '@codeserver') !== false);

if ($isPantheonStandardUpstream || $isPantheonSite) {
$io->writeError("<info>The upstream-require command can only be used with a custom upstream</info>");
$io->writeError("<info>See https://pantheon.io/docs/create-custom-upstream for information on how to create a custom upstream.</info>" . PHP_EOL);
throw new \RuntimeException("Cannot use upstream-require command with this project.");

$composerJsonContents = file_get_contents("composer.json");
$composerJson = json_decode($composerJsonContents, true);
$originalComposerJson = $composerJson;

// Check to see if the platform PHP version (which should be major.minor.patch)
// is the same as the Pantheon PHP version (which is only major.minor).
// If they do not match, force an update to the platform PHP version. If they
// have the same major.minor version, then
$platformPhpVersion = static::getCurrentPlatformPhp($event);
$pantheonPhpVersion = static::getPantheonPhpVersion($event);
$updatedPlatformPhpVersion = static::bestPhpPatchVersion($pantheonPhpVersion);
if ((substr($platformPhpVersion, 0, strlen($pantheonPhpVersion)) != $pantheonPhpVersion) && !empty($updatedPlatformPhpVersion)) {
$io->write("<info>Setting platform.php from '$platformPhpVersion' to '$updatedPlatformPhpVersion' to conform to pantheon php version.</info>");
$composerJson['config']['platform']['php'] = $updatedPlatformPhpVersion;
}

// add our post-update-cmd hook if it's not already present
$our_hook = 'WordPressComposerManaged\\ComposerScripts::postUpdate';
// if does not exist, add as an empty arry
if (! isset($composerJson['scripts']['post-update-cmd'])) {
$composerJson['scripts']['post-update-cmd'] = [];
}

// if exists and is a string, convert to a single-item array
if (is_string($composerJson['scripts']['post-update-cmd'])) {
$composerJson['scripts']['post-update-cmd'] = [$composerJson['scripts']['post-update-cmd']];
}

// Find arguments that look like projects.
$packages = [];
foreach ($event->getArguments() as $arg) {
if (preg_match('#[a-zA-Z][a-zA-Z0-9_-]*/[a-zA-Z][a-zA-Z0-9]:*[~^]*[0-9a-z._-]*#', $arg)) {
$packages[] = $arg;
// if exists and is an array and does not contain our hook, add our hook (again, only the last check is needed)
if (! in_array($our_hook, $composerJson['scripts']['post-update-cmd'])) {
// We're making our other changes if and only if we're already adding our hook
// so that we don't overwrite customer's changes if they undo these changes.
// We don't want customers to remove our hook, so it will be re-added if they remove it.
$io->write("<info>Adding post-update-cmd hook to composer.json</info>");
$composerJson['scripts']['post-update-cmd'][] = $our_hook;

// Remove our upstream convenience scripts, if the user has not removed them.
if (isset($composerJson['scripts']['upstream-require'])) {
unset($composerJson['scripts']['upstream-require']);
}
// Also remove it from the scripts-descriptions section.
if (isset($composerJson['scripts-descriptions']['upstream-require'])) {
unset($composerJson['scripts-descriptions']['upstream-require']);
}
// Now, if scripts-descriptions is empty, remove it. This prevents an issue where it's re-encoded as an array instead of an (empty) object.
if (empty($composerJson['scripts-descriptions'])) {
unset($composerJson['scripts-descriptions']);
}
}

// Insert the new projects into the upstream-configuration composer.json
// without updating the lock file or downloading the projects
$packagesParam = implode(' ', $packages);
$cmd = "composer --working-dir=upstream-configuration require --no-update $packagesParam";
$io->writeError($cmd . PHP_EOL);
passthru($cmd);
if (serialize($composerJson) == serialize($originalComposerJson)) {
return;
}

// Update composer.lock & etc. if present
static::updateLocalDependencies($io, $packages);
// Write the updated composer.json file
$composerJsonContents = static::jsonEncodePretty($composerJson);
file_put_contents("composer.json", $composerJsonContents . PHP_EOL);
}

/**
* Prepare for Composer to update dependencies.
*
* Composer will attempt to guess the version to use when evaluating
* dependencies for path repositories. This has the undesirable effect
* of producing different results in the composer.lock file depending on
* which branch was active when the update was executed. This can lead to
* unnecessary changes, and potentially merge conflicts when working with
* path repositories on Pantheon multidevs.
*
* To work around this problem, it is possible to define an environment
* variable that contains the version to use whenever Composer would normally
* "guess" the version from the git repository branch. We set this invariantly
* to "dev-main" so that the composer.lock file will not change if the same
* update is later ran on a different branch.
*
* @see https://github.com/composer/composer/blob/main/doc/articles/troubleshooting.md#dependencies-on-the-root-package
*/
public static function preUpdate(Event $event)
/**
* jsonEncodePretty
*
* Convert a nested array into a pretty-printed json-encoded string.
*
* @param array $data
* The data array to encode
* @return string
* The pretty-printed encoded string version of the supplied data.
*/
public static function jsonEncodePretty(array $data)
{
$io = $event->getIO();
$prettyContents = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$prettyContents = preg_replace('#": \[\s*("[^"]*")\s*\]#m', '": [\1]', $prettyContents);
return $prettyContents;
}

// We will only set the root version if it has not already been overriden
if (!getenv('COMPOSER_ROOT_VERSION')) {
// This is not an error; rather, we are writing to stderr.
$io->writeError("<info>Using version 'dev-main' for path repositories.</info>");
/**
* Get current platform.php value.
*/
private static function getCurrentPlatformPhp(Event $event)
{
$composer = $event->getComposer();
$config = $composer->getConfig();
$platform = $config->get('platform') ?: [];
if (isset($platform['php'])) {
return $platform['php'];
}
return null;
}

putenv('COMPOSER_ROOT_VERSION=dev-main');
/**
* Get the PHP version from pantheon.yml or pantheon.upstream.yml file.
*/
private static function getPantheonConfigPhpVersion($path)
{
if (!file_exists($path)) {
return null;
}

if (preg_match('/^php_version:\s?(\d+\.\d+)$/m', file_get_contents($path), $matches)) {
return $matches[1];
}
}

/**
* Update the composer.lock file and so on.
*
* Upstreams should *not* commit the composer.lock file. If a local working
* copy
*/
private static function updateLocalDependencies($io, $packages)
/**
* Get the PHP version from pantheon.yml.
*/
private static function getPantheonPhpVersion(Event $event)
{
if (!file_exists('composer.lock')) {
return;
$composer = $event->getComposer();
$config = $composer->getConfig();
$pantheonYmlPath = dirname($config->get('vendor-dir')) . '/pantheon.yml';
$pantheonUpstreamYmlPath = dirname($config->get('vendor-dir')) . '/pantheon.upstream.yml';

if ($pantheonYmlVersion = static::getPantheonConfigPhpVersion($pantheonYmlPath)) {
return $pantheonYmlVersion;
} elseif ($pantheonUpstreamYmlVersion = static::getPantheonConfigPhpVersion($pantheonUpstreamYmlPath)) {
return $pantheonUpstreamYmlVersion;
}
return null;
}

$io->writeError("<warning>composer.lock file present; do not commit composer.lock to a custom upstream, but updating for the purpose of local testing.");

// Remove versions from the parameters, if any
$versionlessPackages = array_map(
function ($package) {
return preg_replace('/:.*/', '', $package);
},
$packages
);

// Update the project-level composer.lock file
$versionlessPackagesParam = implode(' ', $versionlessPackages);
$cmd = "composer update $versionlessPackagesParam";
$io->writeError($cmd . PHP_EOL);
passthru($cmd);
/**
* Determine which patch version to use when the user changes their platform php version.
*/
private static function bestPhpPatchVersion($pantheonPhpVersion)
{
// Integrated Composer requires PHP 7.1 at a minimum.
$patchVersions = [
'8.2' => '8.2.0',
'8.1' => '8.1.13',
'8.0' => '8.0.26',
'7.4' => '7.4.33',
'7.3' => '7.3.33',
'7.2' => '7.2.34',
'7.1' => '7.1.33',
];
if (isset($patchVersions[$pantheonPhpVersion])) {
return $patchVersions[$pantheonPhpVersion];
}
// This feature is disabled if the user selects an unsupported php version.
return '';
}
}

0 comments on commit e529d16

Please sign in to comment.