-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(github-actions): create a local branch manager github action
Create the local branch manager action which can recieve requests for a branch manager check for any of the managed repositories in the angular organization.
- Loading branch information
1 parent
d87b1b5
commit d5a8ca9
Showing
21 changed files
with
69,116 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
load("//tools:defaults.bzl", "esbuild_checked_in") | ||
|
||
esbuild_checked_in( | ||
name = "main", | ||
entry_point = "//.github/local-actions/branch-manager/lib:main.ts", | ||
target = "node16", | ||
deps = [ | ||
"//.github/local-actions/branch-manager/lib", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
name: 'Branch Manager' | ||
description: 'Determine if a provided pull request is able to merge into its target branches' | ||
author: 'Angular' | ||
inputs: | ||
angular-robot-key: | ||
description: 'The private key for the Angular Robot Github app.' | ||
required: true | ||
pr: | ||
description: 'The pull request number' | ||
required: true | ||
repo: | ||
description: 'The name of the repo for the pull request' | ||
required: true | ||
owner: | ||
description: 'The owner of the repo for the pull request' | ||
required: true | ||
runs: | ||
using: 'node16' | ||
main: 'main.js' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
load("//tools:defaults.bzl", "ts_library") | ||
|
||
package(default_visibility = ["//.github/local-actions/branch-manager:__subpackages__"]) | ||
|
||
exports_files([ | ||
"main.ts", | ||
]) | ||
|
||
ts_library( | ||
name = "lib", | ||
srcs = glob( | ||
["*.ts"], | ||
exclude = ["*.spec.ts"], | ||
), | ||
deps = [ | ||
"//github-actions:utils", | ||
"//ng-dev/pr/common", | ||
"//ng-dev/pr/common:labels", | ||
"//ng-dev/pr/config", | ||
"//ng-dev/pr/merge", | ||
"//ng-dev/utils", | ||
"@npm//@actions/core", | ||
"@npm//@actions/github", | ||
"@npm//@octokit/rest", | ||
"@npm//@types/node", | ||
"@npm//typed-graphqlify", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
import * as core from '@actions/core'; | ||
import {PullRequestValidationConfig} from '../../../../ng-dev/pr/common/validation/validation-config.js'; | ||
import { | ||
assertValidPullRequestConfig, | ||
PullRequestConfig, | ||
} from '../../../../ng-dev/pr/config/index.js'; | ||
import {loadAndValidatePullRequest} from '../../../../ng-dev/pr/merge/pull-request.js'; | ||
import {AutosquashMergeStrategy} from '../../../../ng-dev/pr/merge/strategies/autosquash-merge.js'; | ||
import { | ||
assertValidGithubConfig, | ||
getConfig, | ||
GithubConfig, | ||
setConfig, | ||
} from '../../../../ng-dev/utils/config.js'; | ||
import {AuthenticatedGitClient} from '../../../../ng-dev/utils/git/authenticated-git-client.js'; | ||
import { | ||
ANGULAR_ROBOT, | ||
getAuthTokenFor, | ||
revokeActiveInstallationToken, | ||
} from '../../../../github-actions/utils.js'; | ||
import {MergeConflictsFatalError} from '../../../../ng-dev/pr/merge/failures.js'; | ||
import {chdir} from 'process'; | ||
import {spawnSync} from 'child_process'; | ||
|
||
interface CommmitStatus { | ||
state: 'pending' | 'error' | 'failure' | 'success'; | ||
description: string; | ||
} | ||
|
||
async function main(repo: {owner: string; repo: string}, token: string, pr: number) { | ||
if (isNaN(pr)) { | ||
core.setFailed('The provided pr value was not a number'); | ||
return; | ||
} | ||
|
||
// Because we want to perform this check in the targetted repository, we first need to check out the repo | ||
// and then move to the directory it is cloned into. | ||
chdir('/tmp'); | ||
console.log( | ||
spawnSync('git', [ | ||
'clone', | ||
`https://github.com/${repo.owner}/${repo.repo}.git`, | ||
'./branch-manager-repo', | ||
]).output.toString(), | ||
); | ||
chdir('/tmp/branch-manager-repo'); | ||
|
||
// Manually define the configuration for the pull request and github to prevent having to | ||
// checkout the repository before defining the config. | ||
// TODO(josephperrott): Load this from the actual repository. | ||
setConfig(<{pullRequest: PullRequestConfig; github: GithubConfig}>{ | ||
github: { | ||
mainBranchName: 'main', | ||
owner: repo.owner, | ||
name: repo.repo, | ||
}, | ||
pullRequest: { | ||
githubApiMerge: false, | ||
}, | ||
}); | ||
/** The configuration used for the ng-dev tooling. */ | ||
const config = await getConfig([assertValidGithubConfig, assertValidPullRequestConfig]); | ||
|
||
AuthenticatedGitClient.configure(token); | ||
/** The git client used to perform actions. */ | ||
const git = await AuthenticatedGitClient.get(); | ||
|
||
/** The pull request after being retrieved and validated. */ | ||
const pullRequest = await loadAndValidatePullRequest( | ||
{git, config}, | ||
pr, | ||
new PullRequestValidationConfig(), | ||
); | ||
core.info('Validated PR information:'); | ||
core.info(JSON.stringify(pullRequest)); | ||
/** Whether any fatal validation failures were discovered. */ | ||
let hasFatalFailures = false; | ||
/** The status information to be pushed as a status to the pull request. */ | ||
let statusInfo: CommmitStatus = await (async () => { | ||
// Log validation failures and check for any fatal failures. | ||
if (pullRequest.validationFailures.length !== 0) { | ||
core.info(`Found ${pullRequest.validationFailures.length} failing validation(s)`); | ||
for (const failure of pullRequest.validationFailures) { | ||
hasFatalFailures = !failure.canBeForceIgnored || hasFatalFailures; | ||
await core.group('Validation failures', async () => { | ||
core.info(failure.message); | ||
}); | ||
} | ||
} | ||
|
||
// With any fatal failure the check is not necessary to do. | ||
if (hasFatalFailures) { | ||
core.info('One of the validations was fatal, setting the status as pending for the pr'); | ||
return { | ||
description: 'Waiting to check mergeability due to failing status(es)', | ||
state: 'pending', | ||
}; | ||
} | ||
|
||
try { | ||
git.run(['checkout', 'main']); | ||
/** | ||
* A merge strategy used to perform the merge check. | ||
* Any concrete class implementing MergeStrategy is sufficient as all of our usage is | ||
* defined in the abstract base class. | ||
* */ | ||
const strategy = new AutosquashMergeStrategy(git); | ||
await strategy.prepare(pullRequest); | ||
await strategy.check(pullRequest); | ||
core.info('Merge check passes, setting a passing status on the pr'); | ||
return { | ||
description: `Merges cleanly to ${pullRequest.targetBranches.join(', ')}`, | ||
state: 'success', | ||
}; | ||
} catch (e) { | ||
// As the merge strategy class will express the failures during checks, any thrown error is a | ||
// failure for our merge check. | ||
let description: string; | ||
if (e instanceof MergeConflictsFatalError) { | ||
core.info('Merge conflict found'); | ||
description = `Unable to merge into ${e.failedBranches.join( | ||
', ', | ||
)} please update changes or PR target`; | ||
} else { | ||
core.info('Unknown error found when checking merge'); | ||
description = | ||
'Cannot cleanly merge to all target branches, please update changes or PR target'; | ||
} | ||
return { | ||
description, | ||
state: 'failure', | ||
}; | ||
} | ||
})(); | ||
|
||
await git.github.repos.createCommitStatus({ | ||
...repo, | ||
...statusInfo, | ||
sha: pullRequest.headSha, | ||
context: 'Branch Manager', | ||
}); | ||
} | ||
|
||
/** The token for the angular robot to perform actions. */ | ||
const token = await getAuthTokenFor(ANGULAR_ROBOT, true); | ||
/** The repository name for the pull request. */ | ||
const repo = core.getInput('repo', {required: true, trimWhitespace: true}); | ||
/** The owner of the repository for the pull request. */ | ||
const owner = core.getInput('owner', {required: true, trimWhitespace: true}); | ||
/** The pull request number. */ | ||
const pr = Number(core.getInput('pr', {required: true, trimWhitespace: true})); | ||
|
||
try { | ||
await main({repo, owner}, token, pr).catch((e: Error) => { | ||
core.error(e); | ||
core.setFailed(e.message); | ||
}); | ||
} finally { | ||
await revokeActiveInstallationToken(token); | ||
} |
Oops, something went wrong.