Skip to content

Commit

Permalink
feat(github-actions): create an assistant to the branch manager (#888)
Browse files Browse the repository at this point in the history
Create a github action which creates workflow dispatches when appropriate for pull requests.  Triggers checks for merge ready prs when they are updated, or when the main branch is pushed to.

PR Close #888
  • Loading branch information
josephperrott committed Nov 1, 2022
1 parent 8f7738b commit e6c9fce
Show file tree
Hide file tree
Showing 9 changed files with 23,771 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/assistant-to-the-branch-manager.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Assistant to the Branch Manager

# Because the branch manager does not work for dev-infra, due to not using target branches, we don't
# actually use any automatic triggers. Repository dispatch is included for our own testing use and
# ensuring a valid config.
on:
repository_dispatch:
# push:
# pull_request_target:
# types: [opened, synchronize, reopened, ready_for_review, labeled]

# Declare default permissions as read only.
permissions: read-all

jobs:
assistant_to_the_branch_manager:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2
with:
# Setting persist-credentials instructs actions/checkout not to persist the credentials
# in configuration or environment. Since we don't rely on the credentials used for
# checkout this is an improved security measure.
persist-credentials: false
- uses: ./github-actions/branch-manager
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ github-actions/slash-commands/main.js
github-actions/post-approval-changes/main.js
github-actions/org-file-sync/main.js
github-actions/labels-sync/main.js
github-actions/branch-manager/main.js
.github/local-actions/branch-manager/main.js
.github/local-actions/changelog/main.js

Expand Down
10 changes: 10 additions & 0 deletions github-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-actions/branch-manager/lib:main.ts",
target = "node16",
deps = [
"//github-actions/branch-manager/lib",
],
)
10 changes: 10 additions & 0 deletions github-actions/branch-manager/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: 'Assistant to the Branch Manager'
description: 'Automatically determines if active pull requests are mergable to all of their target branches.'
author: 'Angular'
inputs:
angular-robot-key:
description: 'The private key for the Angular Robot Github app.'
required: true
runs:
using: 'node16'
main: 'main.js'
25 changes: 25 additions & 0 deletions github-actions/branch-manager/lib/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
load("//tools:defaults.bzl", "ts_library")

package(default_visibility = ["//github-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:labels",
"@npm//@actions/core",
"@npm//@actions/github",
"@npm//@octokit/rest",
"@npm//@octokit/webhooks-definitions",
"@npm//@types/node",
"@npm//typed-graphqlify",
],
)
108 changes: 108 additions & 0 deletions github-actions/branch-manager/lib/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as core from '@actions/core';
import {context} from '@actions/github';
import {Octokit} from '@octokit/rest';
import {actionLabels} from '../../../ng-dev/pr/common/labels.js';
import {revokeActiveInstallationToken, getAuthTokenFor, ANGULAR_ROBOT} from '../../utils.js';
import {
PullRequestLabeledEvent,
PullRequestEvent,
PushEvent,
} from '@octokit/webhooks-definitions/schema.js';

async function run() {
if (context.eventName === 'push') {
const {ref} = context.payload as PushEvent;
if (ref.startsWith('refs/tags/')) {
core.info('No evaluation needed as tags do not cause branches to need to be rechecked');
return;
}

if (ref !== 'refs/heads/main') {
// TODO: support pushes to all releasable branches rather than just main.
core.info('Skipping evaluation as the push does not affect the main branch');
return;
}

core.info(`Evaluating pull requests as a result of a push to '${ref}'`);
const prs = await github().then((api) =>
api.paginate(
api.pulls.list,
{...context.repo, state: 'open', labels: actionLabels.ACTION_MERGE.name},
(pulls) => pulls.data.map((pull) => `${pull.number}`),
),
);
core.info(`Triggering ${prs.length} prs to be evaluated`);
await Promise.all([...prs.map((pr) => createWorkflowForPullRequest({pr}))]);
}

if (context.eventName === 'pull_request_target') {
if (
['opened', 'synchronize', 'reopened', 'ready_for_review'].includes(context.payload.action!)
) {
const payload = context.payload as PullRequestEvent;
const hasMergeLabel = payload.pull_request.labels.some(
({name}) => name === actionLabels.ACTION_MERGE.name,
);
if (hasMergeLabel) {
await createWorkflowForPullRequest();
}
}

if (context.payload.action === 'labeled') {
const event = context.payload as PullRequestLabeledEvent;
if (event.label.name === actionLabels.ACTION_MERGE.name) {
await createWorkflowForPullRequest();
}
}
}
}

/** The pull request from the context of the action being run, used as the default pull request. */
const pullRequestFromContext = {
repo: context.issue.repo,
owner: context.issue.owner,
pr: `${context.issue.number}`,
};
type WorkflowInputs = typeof pullRequestFromContext;

/** Create a workflow dispatch event to trigger the pr to be evaluated for mergeability. */
function createWorkflowForPullRequest(prInfo?: Partial<WorkflowInputs>) {
const inputs = {...pullRequestFromContext, ...prInfo};
console.info(`Requesting workflow run for: ${JSON.stringify(inputs)}`);
return github().then((api) =>
api.actions.createWorkflowDispatch({
headers: {
// Github requires the authorization to be `token <token>` for this endpoint instead of the
// standard `Bearer <token>`.
'authorization': `token ${token}`,
},
owner: 'angular',
repo: 'dev-infra',
ref: 'main',
workflow_id: 'branch-manager.yml',
inputs,
}),
);
}

/** The authorization token for the Github app. */
let token: string;
/** The Octokit instance, if defined to allow token revokation after the action executes. */
let _github: Octokit | null = null;
/** Get the shared instance of Octokit, first creating the instance if necessary. */
async function github() {
if (_github === null) {
token = await getAuthTokenFor(ANGULAR_ROBOT);
_github = new Octokit({token});
}
return _github;
}

try {
await run().catch((e: Error) => {
console.error(e);
core.setFailed(e.message);
});
} finally {
_github && (await revokeActiveInstallationToken(_github));
}
Loading

0 comments on commit e6c9fce

Please sign in to comment.