Skip to content

Commit

Permalink
feat(github-actions): create a local branch manager github action
Browse files Browse the repository at this point in the history
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
josephperrott committed Oct 26, 2022
1 parent d87b1b5 commit d5a8ca9
Show file tree
Hide file tree
Showing 21 changed files with 69,116 additions and 39 deletions.
10 changes: 10 additions & 0 deletions .github/local-actions/branch-manager/BUILD.bazel
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",
],
)
19 changes: 19 additions & 0 deletions .github/local-actions/branch-manager/action.yml
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'
28 changes: 28 additions & 0 deletions .github/local-actions/branch-manager/lib/BUILD.bazel
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",
],
)
160 changes: 160 additions & 0 deletions .github/local-actions/branch-manager/lib/main.ts
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);
}
Loading

0 comments on commit d5a8ca9

Please sign in to comment.