-
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 an assistant to the branch manager (#888)
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
1 parent
8f7738b
commit e6c9fce
Showing
9 changed files
with
23,771 additions
and
0 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,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 }} |
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
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-actions/branch-manager/lib:main.ts", | ||
target = "node16", | ||
deps = [ | ||
"//github-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,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' |
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,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", | ||
], | ||
) |
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,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)); | ||
} |
Oops, something went wrong.