Skip to content
Merged
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
33 changes: 33 additions & 0 deletions .maintenance/autoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
/**
* Loads the maintenance commands.
*
* Within a full development environment, Composer does that. Without one, any
* `wp` executable can still run them, so the maintenance tooling only needs
* WP-CLI, git and the GitHub CLI, not a complete install of every package.
*/

$composer_autoloader = dirname( __DIR__ ) . '/vendor/autoload.php';

if ( is_file( $composer_autoloader ) ) {
require_once $composer_autoloader;
return;
}

spl_autoload_register(
static function ( $class_name ) {
$prefix = 'WP_CLI\\Maintenance\\';

if ( 0 !== strpos( $class_name, $prefix ) ) {
return;
}

$file = __DIR__ . '/src/' . str_replace( '\\', '/', substr( $class_name, strlen( $prefix ) ) ) . '.php';

if ( is_file( $file ) ) {
require $file;
}
}
);

require_once __DIR__ . '/bootstrap.php';
1 change: 1 addition & 0 deletions .maintenance/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
WP_CLI::add_command( 'maintenance', 'WP_CLI\Maintenance\Maintenance_Namespace' );

WP_CLI::add_command( 'maintenance contrib-list', 'WP_CLI\Maintenance\Contrib_List_Command' );
WP_CLI::add_command( 'maintenance doctor', 'WP_CLI\Maintenance\Doctor_Command' );
WP_CLI::add_command( 'maintenance milestones-after', 'WP_CLI\Maintenance\Milestones_After_Command' );
WP_CLI::add_command( 'maintenance milestones-since', 'WP_CLI\Maintenance\Milestones_Since_Command' );
WP_CLI::add_command( 'maintenance release', 'WP_CLI\Maintenance\Release_Command' );
Expand Down
86 changes: 62 additions & 24 deletions .maintenance/clone-all-repositories.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,64 @@ elif [[ -z "${CLONE_JOBS:-}" && -z "${WPCLI_DEV_JOBS:-}" && "${CORES}" -gt "${MA
CORES=${MAX_CORES}
fi

# Fetch repository list from the GitHub API.
CURL_OPTS=(-fsS)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_OPTS+=(--header "Authorization: Bearer ${GITHUB_TOKEN}")
fi

if ! RESPONSE=$(curl "${CURL_OPTS[@]}" 'https://api.github.com/orgs/wp-cli/repos?per_page=100'); then
echo "Failed to fetch repository list from the GitHub API." >&2
exit 1
fi
# Lists the repositories of the organization as "name<TAB>clone_url<TAB>ssh_url"
# lines. Uses the GitHub CLI when it is logged in, and the GitHub API with
# GH_TOKEN or GITHUB_TOKEN, or anonymously, otherwise.
fetch_repositories() {
if command -v gh &>/dev/null && gh auth status --hostname github.com &>/dev/null; then
gh api --paginate 'orgs/wp-cli/repos?per_page=100' --jq '.[] | [.name, .clone_url, .ssh_url] | @tsv'
return
fi

# Validate the response shape and detect API errors such as rate limiting.
if ! jq -e 'type == "array"' >/dev/null <<< "${RESPONSE}"; then
if jq -e '.message' >/dev/null <<< "${RESPONSE}"; then
MESSAGE=$(jq -r '.message' <<< "${RESPONSE}")
echo "GitHub responded with: ${MESSAGE}" >&2
echo "If you are running into a rate limiting issue during large events please set GITHUB_TOKEN environment variable." >&2
echo "See https://github.com/settings/tokens" >&2
else
echo "GitHub API returned an unexpected response; expected a JSON array of repositories." >&2
local curl_opts=(-sS)
local token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
if [[ -n "${token}" ]]; then
curl_opts+=(--header "Authorization: Bearer ${token}")
fi
exit 1

local page=1 response message
while true; do
if ! response=$(curl "${curl_opts[@]}" "https://api.github.com/orgs/wp-cli/repos?per_page=100&page=${page}"); then
echo "Failed to fetch repository list from the GitHub API." >&2
return 1
fi

# Validate the response shape and detect API errors such as rate limiting.
if ! jq -e 'type == "array"' >/dev/null <<< "${response}"; then
if jq -e '.message' >/dev/null 2>&1 <<< "${response}"; then
message=$(jq -r '.message' <<< "${response}")
echo "GitHub responded with: ${message}" >&2
echo "If you are running into a rate limiting issue, log in with 'gh auth login' or set the GITHUB_TOKEN environment variable." >&2
else
echo "GitHub API returned an unexpected response; expected a JSON array of repositories." >&2
fi
return 1
fi

if [[ $(jq 'length' <<< "${response}") -eq 0 ]]; then
return
fi

if ! jq -r '.[] | [.name, .clone_url, .ssh_url] | @tsv' <<< "${response}"; then
echo "GitHub API returned repositories in an unexpected format." >&2
return 1
fi
page=$((page + 1))
done
}

# Clone over the protocol the GitHub CLI is set up for, as its credential helper
# then covers pushing. Without it, use HTTPS on CI and SSH everywhere else.
if [[ -n "${GITHUB_ACTION:-}" ]]; then
PROTOCOL="https"
elif command -v gh &>/dev/null && gh auth status --hostname github.com &>/dev/null; then
PROTOCOL=$(gh config get git_protocol --host github.com 2>/dev/null || echo "https")
else
PROTOCOL="ssh"
fi

REPOSITORIES=$(fetch_repositories)

is_skipped() {
local name="$1"
for skip in "${SKIP_LIST[@]}"; do
Expand All @@ -84,22 +118,26 @@ CLONE_LIST=()
UPDATE_FOLDERS=()

while IFS=$'\t' read -r name clone_url ssh_url; do
if [[ -z "${name}" ]]; then
continue
fi

if is_skipped "${name}"; then
continue
fi

destination=$(get_destination "${name}")

if [[ ! -d "${destination}" ]]; then
if [[ -n "${GITHUB_ACTION:-}" ]]; then
CLONE_LIST+=("${destination}"$'\t'"${clone_url}")
else
if [[ "${PROTOCOL}" == "ssh" ]]; then
CLONE_LIST+=("${destination}"$'\t'"${ssh_url}")
else
CLONE_LIST+=("${destination}"$'\t'"${clone_url}")
fi
fi

UPDATE_FOLDERS+=("${destination}")
done < <(echo "${RESPONSE}" | jq -r '.[] | [.name, .clone_url, .ssh_url] | @tsv')
done <<< "${REPOSITORIES}"

if [[ ${#CLONE_LIST[@]} -gt 0 ]]; then
printf '%s\n' "${CLONE_LIST[@]}" | xargs -n2 -P"${CORES}" bash "${SCRIPT_DIR}/clone-repository.sh"
Expand Down
79 changes: 73 additions & 6 deletions .maintenance/refresh-repository.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,83 @@
<?php
/**
* Brings a clone up to date with its remote, without ever touching work in
* progress: a clone with uncommitted changes, or one on another branch than
* the default one, is only fetched. Exits with an error code when git fails.
*/

if ( ! isset( $argv[1] ) || ! is_dir( $argv[1] ) ) {
exit( "Usage: php refresh-repository.php <repository-folder>\n" );
}

$repository = $argv[1];
$path = realpath( getcwd() . "/{$repository}" );
$path = (string) realpath( $repository );
$git = 'git -C ' . escapeshellarg( $path );

printf( "--- Refreshing repository \033[32m{$repository}\033[0m ---\n" );
/**
* Runs a git command and returns its output, or null when it fails.
*
* @param string $git Git executable with its working directory.
* @param string $command Git command and arguments.
*
* @return string|null
*/
function git_output( $git, $command ) {
exec( "{$git} {$command} 2>/dev/null", $output, $exit_code );

printf( "Switching to latest \033[33mdefault\033[0m branch...\n" );
system( "git --git-dir={$path}/.git --work-tree={$path} checkout $(git --git-dir={$path}/.git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)" );
return 0 === $exit_code ? trim( implode( "\n", $output ) ) : null;
}

/**
* Prints an error and exits with an error code.
*
* @param string $repository Repository folder.
* @param string $message What went wrong.
*/
function fail( $repository, $message ) {
fwrite( STDERR, sprintf( "Could not refresh \033[31m%s\033[0m: %s\n", $repository, $message ) );
exit( 1 );
}

printf( "Pulling latest changes...\n" );
system( "git --git-dir={$path}/.git --work-tree={$path} pull" );
printf( "--- Refreshing repository \033[32m%s\033[0m ---\n", $repository );

// Git looks for a repository in parent folders too, so a folder that is not a
// clone would otherwise refresh the repository around it.
$toplevel = git_output( $git, 'rev-parse --show-toplevel' );
if ( null === $toplevel || realpath( $toplevel ) !== $path ) {
fail( $repository, 'it is not a Git clone.' );
}

passthru( "{$git} fetch --quiet origin", $exit_code );
if ( 0 !== $exit_code ) {
fail( $repository, 'fetching from origin failed.' );
}

$default_branch = git_output( $git, 'symbolic-ref --short refs/remotes/origin/HEAD' );
if ( null === $default_branch ) {
// Clones made before the remote HEAD was recorded need it looked up once.
git_output( $git, 'remote set-head origin --auto' );
$default_branch = git_output( $git, 'symbolic-ref --short refs/remotes/origin/HEAD' );
}
$default_branch = null === $default_branch ? null : preg_replace( '#^origin/#', '', $default_branch );
$current_branch = git_output( $git, 'rev-parse --abbrev-ref HEAD' );
$changes = git_output( $git, 'status --porcelain' );

if ( null === $changes ) {
fail( $repository, 'its status could not be read.' );
}

if ( '' !== $changes ) {
printf( "Leaving \033[33m%s\033[0m alone, it has uncommitted changes.\n", $repository );
exit;
}

if ( null === $default_branch || $current_branch !== $default_branch ) {
printf( "Leaving \033[33m%s\033[0m alone, it is on branch %s instead of %s.\n", $repository, $current_branch, $default_branch ?: 'the default branch' );
exit;
}

printf( "Pulling latest changes into %s...\n", $default_branch );
passthru( "{$git} pull --quiet --ff-only", $exit_code );
if ( 0 !== $exit_code ) {
fail( $repository, 'pulling the latest changes failed.' );
}
20 changes: 19 additions & 1 deletion .maintenance/src/Bundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ final class Bundle {
'wp-cli/mustangostang-spyc',
];

/**
* Bundled packages whose GitHub repository is named differently.
*/
const PACKAGE_REPOS = [
'wp-cli/mustangostang-spyc' => 'wp-cli/spyc',
];

/**
* Gets the GitHub repository a bundled package is developed in.
*
* @param string $package Package name.
*
* @return string
*/
public static function get_repo( $package ) {
return isset( self::PACKAGE_REPOS[ $package ] ) ? self::PACKAGE_REPOS[ $package ] : $package;
}

/**
* Gets the milestone of a release repository that belongs to a bundle
* release: the one titled after the release, or the lowest open one when
Expand Down Expand Up @@ -231,7 +249,7 @@ public static function get_packages( array $lock ) {
*/
public static function get_shipped_milestones( $package, $previous_version, $current_version ) {
return self::filter_shipped_milestones(
GitHub::get_project_milestones( $package, [ 'state' => 'closed' ] ),
GitHub::get_project_milestones( self::get_repo( $package ), [ 'state' => 'closed' ] ),
$previous_version,
$current_version
);
Expand Down
Loading
Loading