Skip to content

feat(github-actions): add label checks to unified status check #927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions github-actions/unified-status-check/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ts_library(
),
deps = [
"//github-actions:utils",
"//ng-dev/pr/common:labels",
"@npm//@actions/core",
"@npm//@actions/github",
"@npm//@octokit/graphql-schema",
Expand Down
215 changes: 215 additions & 0 deletions github-actions/unified-status-check/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -23697,6 +23697,13 @@ var PR_SCHEMA = {
isDraft: import_typed_graphqlify.types.boolean,
state: import_typed_graphqlify.types.custom(),
number: import_typed_graphqlify.types.number,
labels: (0, import_typed_graphqlify.params)({ last: 100 }, {
nodes: [
{
name: import_typed_graphqlify.types.string
}
]
}),
commits: (0, import_typed_graphqlify.params)({ last: 1 }, {
nodes: [
{
Expand Down Expand Up @@ -23757,6 +23764,7 @@ function parseGithubPullRequest({ repository: { pullRequest } }) {
return {
sha: pullRequest.commits.nodes[0].commit.oid,
isDraft: pullRequest.isDraft,
labels: pullRequest.labels.nodes.map(({ name }) => name),
state: pullRequest.state,
statuses
};
Expand Down Expand Up @@ -23811,6 +23819,203 @@ var checkOnlyPassingStatuses = ({ statuses }) => {
};
};

//
var createTypedObject = () => (v) => v;

//
var managedLabels = createTypedObject()({
DETECTED_BREAKING_CHANGE: {
description: "PR contains a commit with a breaking change",
name: "detected: breaking change",
commitCheck: (c) => c.breakingChanges.length !== 0
},
DETECTED_DEPRECATION: {
description: "PR contains a commit with a deprecation",
name: "detected: deprecation",
commitCheck: (c) => c.deprecations.length !== 0
},
DETECTED_FEATURE: {
description: "PR contains a feature commit",
name: "detected: feature",
commitCheck: (c) => c.type === "feat"
},
DETECTED_DOCS_CHANGE: {
description: "Related to the documentation",
name: "area: docs",
commitCheck: (c) => c.type === "docs"
},
DETECTED_INFRA_CHANGE: {
description: "Related the build and CI infrastructure of the project",
name: "area: build & ci",
commitCheck: (c) => c.type === "build" || c.type === "ci"
}
});

//
var actionLabels = createTypedObject()({
ACTION_MERGE: {
description: "The PR is ready for merge by the caretaker",
name: "action: merge"
},
ACTION_CLEANUP: {
description: "The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews",
name: "action: cleanup"
},
ACTION_PRESUBMIT: {
description: "The PR is in need of a google3 presubmit",
name: "action: presubmit"
},
ACTION_REVIEW: {
description: "The PR is still awaiting reviews from at least one requested reviewer",
name: "action: review"
}
});

//
var mergeLabels = createTypedObject()({
MERGE_PRESERVE_COMMITS: {
description: "When the PR is merged, a rebase and merge should be performed",
name: "merge: preserve commits"
},
MERGE_SQUASH_COMMITS: {
description: "When the PR is merged, a squash and merge should be performed",
name: "merge: squash commits"
},
MERGE_FIX_COMMIT_MESSAGE: {
description: "When the PR is merged, rewrites/fixups of the commit messages are needed",
name: "merge: fix commit message"
},
MERGE_CARETAKER_NOTE: {
description: "Alert the caretaker performing the merge to check the PR for an out of normal action needed or note",
name: "merge: caretaker note"
}
});

//
var targetLabels = createTypedObject()({
TARGET_FEATURE: {
description: "This PR is targeted for a feature branch (outside of main and semver branches)",
name: "target: feature"
},
TARGET_LTS: {
description: "This PR is targeting a version currently in long-term support",
name: "target: lts"
},
TARGET_MAJOR: {
description: "This PR is targeted for the next major release",
name: "target: major"
},
TARGET_MINOR: {
description: "This PR is targeted for the next minor release",
name: "target: minor"
},
TARGET_PATCH: {
description: "This PR is targeted for the next patch release",
name: "target: patch"
},
TARGET_RC: {
description: "This PR is targeted for the next release-candidate",
name: "target: rc"
}
});

//
var priorityLabels = createTypedObject()({
P0: {
name: "P0",
description: "Issue that causes an outage, breakage, or major function to be unusable, with no known workarounds"
},
P1: {
name: "P1",
description: "Impacts a large percentage of users; if a workaround exists it is partial or overly painful"
},
P2: {
name: "P2",
description: "The issue is important to a large percentage of users, with a workaround"
},
P3: {
name: "P3",
description: "An issue that is relevant to core functions, but does not impede progress. Important, but not urgent"
},
P4: {
name: "P4",
description: "A relatively minor issue that is not relevant to core functions"
},
P5: {
name: "P5",
description: "The team acknowledges the request but does not plan to address it, it remains open for discussion"
}
});

//
var featureLabels = createTypedObject()({
FEATURE_IN_BACKLOG: {
name: "feature: in backlog",
description: "Feature request for which voting has completed and is now in the backlog"
},
FEATURE_VOTES_REQUIRED: {
name: "feature: votes required",
description: "Feature request which is currently still in the voting phase"
},
FEATURE_UNDER_CONSIDERATION: {
name: "feature: under consideration",
description: "Feature request for which voting has completed and the request is now under consideration"
},
FEATURE_INSUFFICIENT_VOTES: {
name: "feature: insufficient votes",
description: "Label to add when the not a sufficient number of votes or comments from unique authors"
}
});

//
var allLabels = {
...managedLabels,
...actionLabels,
...mergeLabels,
...targetLabels,
...priorityLabels,
...featureLabels
};

//
var checkForTargelLabel = (pullRequest) => {
const appliedLabel = Object.values(targetLabels).find((label) => pullRequest.labels.includes(label.name));
if (appliedLabel !== void 0) {
return {
state: "success",
description: `Pull request has target label: "${appliedLabel.name}"`
};
}
return {
state: "pending",
description: "Waiting for target label on the pull request"
};
};
var isMergeReady = (pullRequest) => {
if (!pullRequest.labels.includes(actionLabels.ACTION_MERGE.name)) {
return {
state: "pending",
description: `Waiting for "${actionLabels.ACTION_MERGE.name}" label`
};
}
if (pullRequest.labels.includes(actionLabels.ACTION_REVIEW.name)) {
return {
state: "failure",
description: `Marked for merge but still has the "${actionLabels.ACTION_REVIEW.name}" label`
};
}
if (pullRequest.labels.includes(actionLabels.ACTION_CLEANUP.name)) {
return {
state: "failure",
description: `Marked for merge but still has the "${actionLabels.ACTION_CLEANUP.name}" label`
};
}
return {
state: "success",
description: `Marked for merge by the "${actionLabels.ACTION_MERGE.name}" label`
};
};

//
async function main() {
const github = new import_rest2.Octokit({ auth: await getAuthTokenFor(ANGULAR_ROBOT) });
Expand All @@ -23836,6 +24041,11 @@ async function main() {
await setStatus(isDraftValidationResult.state, isDraftValidationResult.description);
return;
}
const isMergeReadyResult = isMergeReady(pullRequest);
if (isMergeReadyResult.state === "pending") {
await setStatus(isMergeReadyResult.state, isMergeReadyResult.description);
return;
}
const requiredStatusesResult = checkRequiredStatuses(pullRequest);
if (requiredStatusesResult.state === "pending") {
await setStatus(requiredStatusesResult.state, requiredStatusesResult.description);
Expand All @@ -23846,6 +24056,11 @@ async function main() {
await setStatus(onlyPassingStatusesResult.state, onlyPassingStatusesResult.description);
return;
}
const targetLabelResult = checkForTargelLabel(pullRequest);
if (targetLabelResult.state === "pending") {
await setStatus(targetLabelResult.state, targetLabelResult.description);
return;
}
} finally {
await revokeActiveInstallationToken(github);
}
Expand Down
47 changes: 47 additions & 0 deletions github-actions/unified-status-check/src/labels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {actionLabels, targetLabels} from '../../../ng-dev/pr/common/labels.js';
import {PullRequest} from './pull-request.js';
import {ValidationFunction} from './validation.js';

export const checkForTargelLabel: ValidationFunction = (pullRequest: PullRequest) => {
const appliedLabel = Object.values(targetLabels).find((label) =>
pullRequest.labels.includes(label.name),
);
if (appliedLabel !== undefined) {
return {
state: 'success',
description: `Pull request has target label: "${appliedLabel.name}"`,
};
}
return {
state: 'pending',
description: 'Waiting for target label on the pull request',
};
};

export const isMergeReady: ValidationFunction = (pullRequest: PullRequest) => {
if (!pullRequest.labels.includes(actionLabels.ACTION_MERGE.name)) {
return {
state: 'pending',
description: `Waiting for "${actionLabels.ACTION_MERGE.name}" label`,
};
}

if (pullRequest.labels.includes(actionLabels.ACTION_REVIEW.name)) {
return {
state: 'failure',
description: `Marked for merge but still has the "${actionLabels.ACTION_REVIEW.name}" label`,
};
}

if (pullRequest.labels.includes(actionLabels.ACTION_CLEANUP.name)) {
return {
state: 'failure',
description: `Marked for merge but still has the "${actionLabels.ACTION_CLEANUP.name}" label`,
};
}

return {
state: 'success',
description: `Marked for merge by the "${actionLabels.ACTION_MERGE.name}" label`,
};
};
13 changes: 13 additions & 0 deletions github-actions/unified-status-check/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {getAuthTokenFor, ANGULAR_ROBOT, revokeActiveInstallationToken} from '../
import {getPullRequest, NormalizedState, unifiedStatusCheckName} from './pull-request.js';
import {isDraft} from './draft-mode.js';
import {checkOnlyPassingStatuses, checkRequiredStatuses} from './statuses.js';
import {checkForTargelLabel, isMergeReady} from './labels.js';

async function main() {
/** A Github API instance. */
Expand Down Expand Up @@ -44,6 +45,12 @@ async function main() {
return;
}

const isMergeReadyResult = isMergeReady(pullRequest);
if (isMergeReadyResult.state === 'pending') {
await setStatus(isMergeReadyResult.state, isMergeReadyResult.description);
return;
}

const requiredStatusesResult = checkRequiredStatuses(pullRequest);
if (requiredStatusesResult.state === 'pending') {
await setStatus(requiredStatusesResult.state, requiredStatusesResult.description);
Expand All @@ -55,6 +62,12 @@ async function main() {
await setStatus(onlyPassingStatusesResult.state, onlyPassingStatusesResult.description);
return;
}

const targetLabelResult = checkForTargelLabel(pullRequest);
if (targetLabelResult.state === 'pending') {
await setStatus(targetLabelResult.state, targetLabelResult.description);
return;
}
} finally {
await revokeActiveInstallationToken(github);
}
Expand Down
12 changes: 12 additions & 0 deletions github-actions/unified-status-check/src/pull-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type PullRequest = {
sha: string;
isDraft: boolean;
state: PullRequestState;
labels: string[];
statuses: Statuses;
};

Expand Down Expand Up @@ -87,6 +88,16 @@ const PR_SCHEMA = {
isDraft: graphqlTypes.boolean,
state: graphqlTypes.custom<PullRequestState>(),
number: graphqlTypes.number,
labels: params(
{last: 100},
{
nodes: [
{
name: graphqlTypes.string,
},
],
},
),
commits: params(
{last: 1},
{
Expand Down Expand Up @@ -164,6 +175,7 @@ function parseGithubPullRequest({repository: {pullRequest}}: typeof PR_SCHEMA):
return {
sha: pullRequest.commits.nodes[0].commit.oid,
isDraft: pullRequest.isDraft,
labels: pullRequest.labels.nodes.map(({name}) => name),
state: pullRequest.state,
statuses,
};
Expand Down