|
| 1 | +<?php |
| 2 | + |
| 3 | +function try_run(array $args): bool { |
| 4 | + $command = implode(' ', array_map('escapeshellarg', $args)); |
| 5 | + echo "> $command\n"; |
| 6 | + passthru($command, $status); |
| 7 | + return $status === 0; |
| 8 | +} |
| 9 | + |
| 10 | +function run(array $args, ?string $failure_message = null) { |
| 11 | + if (!try_run($args)) { |
| 12 | + throw new RuntimeException($failure_message ?? 'Command failed'); |
| 13 | + } |
| 14 | +} |
| 15 | + |
| 16 | +function origin_branch_exists(string $branch): bool { |
| 17 | + return try_run(['git', 'show-ref', '--verify', '--quiet', "refs/remotes/origin/$branch"]); |
| 18 | +} |
| 19 | + |
| 20 | +function find_next_release_branch(string $current): ?string { |
| 21 | + if ($current === 'master') { |
| 22 | + return null; |
| 23 | + } |
| 24 | + |
| 25 | + if (!preg_match('(^PHP-(?<major>\d+)\.(?<minor>\d+)$)', $current, $matches)) { |
| 26 | + return null; |
| 27 | + } |
| 28 | + |
| 29 | + $major = $matches['major']; |
| 30 | + $minor = $matches['minor']; |
| 31 | + |
| 32 | + $next = "PHP-$major." . ($minor + 1); |
| 33 | + if (origin_branch_exists($next)) { |
| 34 | + return $next; |
| 35 | + } |
| 36 | + |
| 37 | + $next = 'PHP-' . ($major + 1) . '.0'; |
| 38 | + if (origin_branch_exists($next)) { |
| 39 | + return $next; |
| 40 | + } |
| 41 | + |
| 42 | + return 'master'; |
| 43 | +} |
| 44 | + |
| 45 | +function find_release_branches(string $target): array { |
| 46 | + $branches = [$target]; |
| 47 | + while (null !== $next = find_next_release_branch(end($branches))) { |
| 48 | + $branches[] = $next; |
| 49 | + } |
| 50 | + return $branches; |
| 51 | +} |
| 52 | + |
| 53 | +function merge_pr_into_target(string $pr_sha, string $pr_first_sha, string $target, string $message, string $description): string { |
| 54 | + $author = trim((string) shell_exec('git log -1 --format=' . escapeshellarg('%an <%ae>') . ' ' . escapeshellarg($pr_first_sha))); |
| 55 | + |
| 56 | + run(['git', 'checkout', '-B', $target, "refs/remotes/origin/$target"]); |
| 57 | + run(['git', 'merge', '--squash', $pr_sha], |
| 58 | + failure_message: "Failed to squash PR into $target."); |
| 59 | + run(['git', 'commit', "--author=$author", '-m', $message, '-m', wrap_commit_message($description)]); |
| 60 | + $squashed_sha = trim((string) shell_exec('git rev-parse HEAD')); |
| 61 | + |
| 62 | + return $squashed_sha; |
| 63 | +} |
| 64 | + |
| 65 | +function merge_upwards(array $branches) { |
| 66 | + for ($i = 1; $i < count($branches); $i++) { |
| 67 | + $prev = $branches[$i - 1]; |
| 68 | + $current = $branches[$i]; |
| 69 | + run(['git', 'checkout', '-B', $current, "refs/remotes/origin/$current"]); |
| 70 | + run(['git', 'merge', '--no-ff', '--no-edit', $prev], |
| 71 | + failure_message: "Failed to merge $prev into $current."); |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +function push_pr_branch(string $url, string $branch, string $squashed_sha, string $original_sha) { |
| 76 | + run(['git', 'push', "--force-with-lease=$branch:$original_sha", $url, "$squashed_sha:refs/heads/$branch"], |
| 77 | + failure_message: 'Failed to push rebased PR branch.'); |
| 78 | +} |
| 79 | + |
| 80 | +function push_release_branches(array $branches) { |
| 81 | + run(['git', 'push', '--atomic', 'origin', ...$branches], |
| 82 | + failure_message: 'Failed to push release branches.'); |
| 83 | +} |
| 84 | + |
| 85 | +function wrap_commit_message(string $message, int $width = 80): string { |
| 86 | + $lines = explode("\n", $message); |
| 87 | + $result = []; |
| 88 | + $code_section = false; |
| 89 | + |
| 90 | + foreach ($lines as $line) { |
| 91 | + if (preg_match('(^\s*```)', $line)) { |
| 92 | + $code_section = !$code_section; |
| 93 | + $result[] = $line; |
| 94 | + continue; |
| 95 | + } |
| 96 | + |
| 97 | + if ($code_section) { |
| 98 | + $result[] = $line; |
| 99 | + continue; |
| 100 | + } |
| 101 | + |
| 102 | + if ($line === '' || preg_match('(^\s)', $line)) { |
| 103 | + $result[] = $line; |
| 104 | + continue; |
| 105 | + } |
| 106 | + |
| 107 | + $result[] = wordwrap($line, $width, "\n", false); |
| 108 | + } |
| 109 | + |
| 110 | + return implode("\n", $result); |
| 111 | +} |
| 112 | + |
| 113 | +function main(): int { |
| 114 | + $target = getenv('TARGET_BRANCH'); |
| 115 | + $pr_number = getenv('PR_NUMBER'); |
| 116 | + $pr_first_sha = getenv('PR_FIRST_SHA'); |
| 117 | + $pr_sha = getenv('PR_SHA'); |
| 118 | + $pr_ref = getenv('PR_REF'); |
| 119 | + $pr_repo_url = getenv('PR_REPO_URL'); |
| 120 | + $pr_title = getenv('PR_TITLE'); |
| 121 | + $pr_description = getenv('PR_DESCRIPTION'); |
| 122 | + |
| 123 | + $release_branches = find_release_branches($target); |
| 124 | + |
| 125 | + try { |
| 126 | + $squashed_sha = merge_pr_into_target($pr_sha, $pr_first_sha, $target, "$pr_title (GH-$pr_number)", $pr_description); |
| 127 | + merge_upwards($release_branches); |
| 128 | + push_pr_branch($pr_repo_url, $pr_ref, $squashed_sha, $pr_sha); |
| 129 | + push_release_branches($release_branches); |
| 130 | + } catch (Throwable $e) { |
| 131 | + if (false !== ($github_output = getenv('GITHUB_OUTPUT'))) { |
| 132 | + file_put_contents($github_output, "fail_reason<<EOF\n{$e->getMessage()}\nEOF\n", FILE_APPEND); |
| 133 | + } |
| 134 | + fwrite(STDERR, "::error::{$e->getMessage()}\n"); |
| 135 | + return 1; |
| 136 | + } |
| 137 | + |
| 138 | + return 0; |
| 139 | +} |
| 140 | + |
| 141 | +exit(main()); |
0 commit comments