Skip to content

Commit

Permalink
Add/remove labels (#2)
Browse files Browse the repository at this point in the history
* feat: add/remove labels

* feat: create missing labels if not found

* feat: take label colors from linear

* fix: typo

Co-authored-by: Sergey Sova <[email protected]>

* fix: some naming/params issues

* refactor: rename createJobs

* refactor: try label object form

Co-authored-by: Sergey Sova <[email protected]>
  • Loading branch information
Evgeny Zakharov and sergeysova authored Jun 26, 2021
1 parent 71e4819 commit 932096b
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 11 deletions.
13 changes: 13 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ inputs:
required: true
description: 'How to change issue state when pull request is opened or merged (case sensitive)'
default: 'drafted = In Progress; ready = In Review; merged = Done; closed = Canceled'
add-labels:
required: false
default: 'true'
description: 'When the pull request is opened/edited, add the labels (if found) from the Linear issue'
remove-labels:
required: false
default: 'true'
description: 'When the pull request is edited, remove the wrong labels'
create-missing-labels:
required: false
default: 'true'
description: 'When the label was not found in the repo - create it'

runs:
using: 'docker'
image: 'Dockerfile'
Expand Down
183 changes: 172 additions & 11 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { getInput, setFailed } from '@actions/core';
import { getInput, getBooleanInput, setFailed } from '@actions/core';
import { context, getOctokit } from '@actions/github';
import { Issue, LinearClient, LinearFetch, Team, User, WorkflowState } from '@linear/sdk';
import { Issue, LinearClient, LinearFetch, Team, User, WorkflowState, IssueLabel } from '@linear/sdk';
import { WebhookPayload } from '@actions/github/lib/interfaces';

const linearToken = getInput('linear-token', { required: true });
const issuesRequired = getInput('issues-required', { required: false }) === 'true';
const issuesRequired = getBooleanInput('issues-required', { required: false });
const shouldAddLabels = getBooleanInput('add-labels', { required: false });
const shouldRemoveLabels = getBooleanInput('remove-labels', { required: false });
const createMissingLabels = getBooleanInput('create-missing-labels', { required: false });
const issuePrefixes = getInput('issue-prefixes', { required: true })
.split(' ')
.map((prefix) => prefix.trim())
Expand Down Expand Up @@ -48,22 +51,164 @@ async function main() {
throw new Error('Please, set issues in PR title');
}

const foundIssues = await Promise.all(foundIssuesIds.map((id) => linearIssueFind(id)));

// just the declarative wrapper over promise array
// we collect the promises here, and await them at the end
const batcher = createTaskBatcher();

if (action === 'opened' || action === 'edited') {
batcher.add(
githubSyncLabels({
linearIssues: foundIssues,
pr: context.payload.pull_request as PR,
}),
);
}

const prStatus = prStatusDetect(context.payload.pull_request as PR);
const linearNextState = prStatusMapToLinear(prStatus);
const linearPrLink = `[#${prId} ${title}](${prHtmlUrl}).`;
const linearIssueText = linearCommentText[prStatus](context.payload.pull_request as PR);

const linearComment = `${linearPrLink} ${linearIssueText}`;

const linearIssueProcess = async (issue: Issue) => {
const currentState = await issue.state;
if (currentState.name !== linearNextState) {
await linearIssueMove(issue, linearNextState);
await linearIssueCommentSend(issue, linearComment);
}
};

for (const issue of foundIssues) {
batcher.add(linearIssueProcess(issue));
}

await batcher.execute();
}

function createTaskBatcher() {
const promises: Promise<unknown>[] = [];

return {
add: (job: Promise<unknown>) => {
promises.push(job);
},
execute: () => Promise.all(promises),
};
}

async function githubSyncLabels({ linearIssues, pr }: { linearIssues: Issue[]; pr: PR }) {
const repoAllLabels = await repoLabelsList();
const linearActualLabelsMap = new Map<string, IssueLabel>();

const linearLabels = await Promise.all(linearIssues.map((issue) => issue.labels()));

for (const linearLabel of linearLabels) {
for (const node of linearLabel.nodes) {
linearActualLabelsMap.set(node.name, node);
}
}

const linearActualLabels = Array.from(linearActualLabelsMap.values())

const toAdd: AbstractLabel[] = [];
const toAddMissing: AbstractLabel[] = [];
const toRemove: AbstractLabel[] = [];

const byName = (label: AbstractLabel) => (another: AbstractLabel) => {
return label.name === another.name
}

if (shouldAddLabels) {
for (const requiredLabel of linearActualLabels) {
const foundLabel = pr.labels.find(byName(requiredLabel));
if (foundLabel) continue;
const existInRepo = repoAllLabels.find(byName(requiredLabel));
if (existInRepo) toAdd.push(requiredLabel);
else toAddMissing.push(requiredLabel);
}
}

if (shouldRemoveLabels) {
for (const currentLabel of pr.labels) {
const isWrong = !linearActualLabels.find(byName(currentLabel));
if (!isWrong) continue;
toRemove.push(currentLabel);
}
}

const batcher = createTaskBatcher();

if (toAdd.length > 0) {
batcher.add(prLabelsAdd(pr, toAdd));
}

if (toAddMissing.length > 0 && createMissingLabels) {
const createAndAdd = async () => {
await repoLabelsCreate(toAddMissing);
await prLabelsAdd(pr, toAddMissing);
};

batcher.add(createAndAdd());
}

if (toRemove.length > 0) {
batcher.add(prLabelsRemove(pr, toRemove));
}

return batcher.execute().catch((error) => {
console.log('Failed to sync labels');
console.log('PR Labels:');
console.log(pr.labels);
console.log('Linear Labels:');
console.log(Array.from(linearActualLabels));
console.error(error.message);
});
}

function repoLabelsList() {
return octokit.rest.issues
.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
})
.then((response) => response.data);
}

async function repoLabelsCreate(labels: AbstractLabel[]) {
await Promise.all(
foundIssuesIds.map(async (id) => {
const issue = await linearIssueFind(id);
const currentState = await issue.state;
if (currentState.name !== linearNextState) {
await linearIssueMove(issue, linearNextState);
await linearIssueCommentSend(issue, linearComment);
}
}),
labels.map((label) =>
octokit.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
color: label.color,
}),
),
);
}

async function prLabelsAdd(pr: PR, labels: AbstractLabel[]) {
await octokit.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: labels.map(label => label.name)
});
}

async function prLabelsRemove(pr: PR, labels: AbstractLabel[]) {
await Promise.all(
labels.map((label) =>
octokit.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
name: label.name,
}),
),
);
}

Expand All @@ -75,13 +220,29 @@ function prStatusMapToLinear(prStatus: PullState): string {
return state.linearStateName;
}

interface AbstractLabel {
name?: string
color?: string
}

interface Label {
id: number;
node_id: string;
url: string;
name: string;
description: string;
color: string;
default: boolean;
}

interface PR {
number: number;
rebaseable: boolean;
merged: boolean;
draft: boolean;
html_url: string;
state: 'open' | 'closed';
labels: Label[];
}

function prStatusDetect(pr: PR): PullState {
Expand Down

0 comments on commit 932096b

Please sign in to comment.