Commit 491c8395 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #950 from blockscout/tom2drum/issue_945

devops: label released issues
parents a8fcd42f 63eceaf9
name: Label released issues
on:
workflow_dispatch:
inputs:
label_color:
description: 'A color of the added label'
default: 'FFFFFF'
required: false
type: string
workflow_call:
inputs:
label_color:
description: 'A color of the added label'
default: 'FFFFFF'
required: false
type: string
outputs:
issues:
description: "JSON encoded list of issues linked to commits in the release"
value: ${{ jobs.run.outputs.issues }}
concurrency:
group: Label released issues
cancel-in-progress: true
jobs:
run:
name: Run
runs-on: ubuntu-latest
outputs:
issues: ${{ steps.linked_issues.outputs.result }}
steps:
- name: Getting tags of the two latestest releases
id: tags
uses: actions/github-script@v6
with:
script: |
const { repository: { releases: { nodes: releases } } } = await github.graphql(`
query ($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
releases(first: 10, orderBy: { field: CREATED_AT, direction: DESC }) {
nodes {
name
tagName
tagCommit {
oid
}
isPrerelease
isDraft
publishedAt
}
}
}
}
`,
{
owner: context.repo.owner,
repo: context.repo.repo,
}
);
const [ { tagName: latestTag }, { tagName: previousTag } ] = releases
.filter(({ isPrerelease, isDraft }) => !isPrerelease && !isDraft);
core.info('Found following tags:');
core.info(` latest: ${ latestTag }`);
core.info(` second latest: ${ previousTag }`);
core.setOutput('latest', latestTag);
core.setOutput('previous', previousTag);
- name: Getting info about latest release label
id: label
uses: actions/github-script@v6
env:
LABEL_NAME: ${{ steps.tags.outputs.latest }}
with:
script: |
try {
const result = await github.request('GET /repos/{owner}/{repo}/labels/{name}', {
owner: context.repo.owner,
repo: context.repo.repo,
name: process.env.LABEL_NAME,
});
core.info(`Found label with id: ${ result.data.id }`);
core.setOutput('id', result.data.id);
} catch (error) {
if (error.status === 404) {
core.info('Nothing has found.');
core.setOutput('id', 'null');
}
}
- name: Fetching issues with release label
id: has_labeled_issues
uses: actions/github-script@v6
env:
LABEL_NAME: ${{ steps.tags.outputs.latest }}
LABEL_ID: ${{ steps.label.outputs.id }}
with:
script: |
if (process.env.LABEL_ID === 'null') {
core.info(`Label does not exist. No need to fetch issues.`);
return false;
}
const { data } = await github.request('GET /repos/{owner}/{repo}/issues', {
owner: context.repo.owner,
repo: context.repo.repo,
labels: process.env.LABEL_NAME,
state: 'closed',
});
if (data.length > 0) {
core.info(`Found ${ data.length } closed issues with label ${ process.env.LABEL_NAME }. No further action required.`);
core.notice('Issues already labeled.');
return data.length > 0;
}
- name: Looking for commits between two releases
id: commits
uses: actions/github-script@v6
if: ${{ steps.has_labeled_issues.outputs.result == 'false' }}
env:
PREVIOUS_TAG: ${{ steps.tags.outputs.previous }}
LATEST_TAG: ${{ steps.tags.outputs.latest }}
with:
script: |
const { data: { commits: commitsInRelease } } = await github.request('GET /repos/{owner}/{repo}/compare/{basehead}', {
owner: context.repo.owner,
repo: context.repo.repo,
basehead: `${ process.env.PREVIOUS_TAG }...${ process.env.LATEST_TAG }`,
});
if (commitsInRelease.length === 0) {
core.notice(`No commits found between ${ process.env.PREVIOUS_TAG } and ${ process.env.LATEST_TAG }`);
return [];
}
const commits = commitsInRelease.map(({ sha }) => sha);
core.startGroup(`Found ${ commits.length } commits`);
commits.forEach((sha) => {
core.info(sha);
})
core.endGroup();
return commits;
- name: Looking for issues linked to commits
id: linked_issues
uses: actions/github-script@v6
if: ${{ steps.has_labeled_issues.outputs.result == 'false' }}
env:
COMMITS: ${{ steps.commits.outputs.result }}
with:
script: |
const commits = JSON.parse(process.env.COMMITS);
if (commits.length === 0) {
return [];
}
const map = {};
core.startGroup(`Looking for linked issues`);
for (const sha of commits) {
const result = await getLinkedIssuesForCommitPR(sha);
result.forEach((issue) => {
map[issue] = issue;
});
}
core.endGroup();
const issues = Object.values(map);
if (issues.length > 0) {
core.startGroup(`Found ${ issues.length } unique issues`);
issues.sort().forEach((issue) => {
core.info(`#${ issue } - https://github.com/${ context.repo.owner }/${ context.repo.repo }/issues/${ issue }`);
})
core.endGroup();
} else {
core.notice('No linked issues found.');
}
return issues;
async function getLinkedIssuesForCommitPR(sha) {
core.info(`Fetching issues for commit: ${ sha }`);
const response = await github.graphql(`
query ($owner: String!, $repo: String!, $sha: GitObjectID!) {
repository(owner: $owner, name: $repo) {
object(oid: $sha) {
... on Commit {
associatedPullRequests(first: 10) {
nodes {
number
title
state
merged
closingIssuesReferences(first: 10) {
nodes {
number
title
closed
}
}
}
}
}
}
}
}
`, {
owner: context.repo.owner,
repo: context.repo.repo,
sha,
});
if (!response) {
core.info('Nothing has found.');
return [];
}
const { repository: { object: { associatedPullRequests } } } = response;
const issues = associatedPullRequests
.nodes
.map(({ closingIssuesReferences: { nodes: issues } }) => issues.map(({ number }) => number))
.flat();
core.info(`Found following issues: ${ issues.join(', ') || '-' }\n`);
return issues;
}
- name: Creating label with latest release tag
id: label_creating
uses: actions/github-script@v6
if: ${{ steps.label.outputs.id == 'null' && steps.has_labeled_issues.outputs.result == 'false' }}
env:
LABEL_NAME: ${{ steps.tags.outputs.latest }}
LABEL_COLOR: ${{ inputs.label_color }}
with:
script: |
const result = await github.request('POST /repos/{owner}/{repo}/labels', {
owner: context.repo.owner,
repo: context.repo.repo,
name: process.env.LABEL_NAME,
color: process.env.LABEL_COLOR,
description: `Release ${ process.env.LABEL_NAME }`,
});
core.info('Label was created.');
- name: Adding label to issues
id: labeling_issues
uses: actions/github-script@v6
if: ${{ steps.has_labeled_issues.outputs.result == 'false' }}
env:
LABEL_NAME: ${{ steps.tags.outputs.latest }}
ISSUES: ${{ steps.linked_issues.outputs.result }}
with:
script: |
const issues = JSON.parse(process.env.ISSUES);
if (issues.length === 0) {
core.notice('No issues has found. Nothing to label.');
return;
}
for (const issue of issues) {
core.info(`Adding release label to the issue #${ issue }...`);
await addLabelToIssue(issue, process.env.LABEL_NAME);
core.info('Done.\n');
}
async function addLabelToIssue(issue, label) {
return await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue,
labels: [ label ],
});
}
name: Release
on:
release:
types: [ released ]
concurrency:
group: Release
cancel-in-progress: true
jobs:
label_released_issues:
name: Label released issues
uses: './.github/workflows/label-released-issues.yml'
secrets: inherit
if: ${{ github.event.action == 'released' }}
update_project_cards:
name: Update project tasks statuses
needs: label_released_issues
uses: './.github/workflows/update-project-cards.yml'
with:
project_name: Front-end tasks
field_name: Status
field_value: Released
issues: ${{ needs.label_released_issues.outputs.issues }}
secrets: inherit
name: Update project cards for issues
on:
workflow_dispatch:
inputs:
project_name:
description: Name of the project
default: Front-end tasks
required: true
type: string
field_name:
description: Field name to be updated
default: Status
required: true
type: string
field_value:
description: New value of the field
default: Released
required: true
type: string
issues:
description: JSON encoded list of issue numbers to be updated
required: true
type: string
workflow_call:
inputs:
project_name:
description: Name of the project
required: true
type: string
field_name:
description: Field name to be updated
required: true
type: string
field_value:
description: New value of the field
required: true
type: string
issues:
description: JSON encoded list of issue numbers to be updated
required: true
type: string
concurrency:
group: ${{ github.workflow }}/${{ inputs.project_name }}/${{ inputs.field_name }}
cancel-in-progress: true
jobs:
run:
name: Run
runs-on: ubuntu-latest
steps:
- name: Getting project info
id: project_info
uses: actions/github-script@v6
env:
PROJECT_NAME: ${{ inputs.project_name }}
FIELD_NAME: ${{ inputs.field_name }}
FIELD_VALUE: ${{ inputs.field_value }}
with:
github-token: ${{ secrets.BOT_LABEL_ISSUE_TOKEN }}
script: |
const response = await github.graphql(`
query ($login: String!, $q: String!) {
organization(login: $login) {
projectsV2(query: $q, first: 1) {
nodes {
id,
title,
number,
fields(first: 20) {
nodes {
... on ProjectV2Field {
id
name
}
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}
}
`, {
login: context.repo.owner,
q: process.env.PROJECT_NAME,
});
const { organization: { projectsV2: { nodes: projects } } } = response;
if (projects.length === 0) {
core.setFailed('Project not found.');
return;
}
if (projects.length > 1) {
core.info(`Fould ${ projects.length } with the similar name:`);
projects.forEach((issue) => {
core.info(` #${ projects.number } - ${ projects.title }`);
})
core.setFailed('Fould multiple projects with the similar name. Cannot determine which one to use.');
return;
}
const { id: projectId, fields: { nodes: fields } } = projects[0];
const field = fields.find((field) => field.name === process.env.FIELD_NAME);
if (!field) {
core.setFailed(`Field with name "${ process.env.FIELD_NAME }" not found in the project.`);
return;
}
const option = field.options.find((option) => option.name === process.env.FIELD_VALUE);
if (!option) {
core.setFailed(`Option with name "${ process.env.FIELD_VALUE }" not found in the field possible values.`);
return;
}
core.info('Found following info:');
core.info(` project_id: ${ projectId }`);
core.info(` field_id: ${ field.id }`);
core.info(` field_value_id: ${ option.id }`);
core.setOutput('id', projectId);
core.setOutput('field_id', field.id);
core.setOutput('field_value_id', option.id);
- name: Getting project items that linked to the issues
id: items
uses: actions/github-script@v6
env:
ISSUES: ${{ inputs.issues }}
with:
github-token: ${{ secrets.BOT_LABEL_ISSUE_TOKEN }}
script: |
const result = [];
const issues = JSON.parse(process.env.ISSUES);
for (const issue of issues) {
const response = await getProjectItemId(issue);
response?.length > 0 && result.push(...response);
}
return result;
async function getProjectItemId(issueId) {
core.info(`Fetching project items for issue #${ issueId }...`);
try {
const response = await github.graphql(`
query ($owner: String!, $repo: String!, $id: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $id) {
title,
projectItems(first: 10) {
nodes {
id,
}
}
}
}
}
`,
{
owner: context.repo.owner,
repo: context.repo.repo,
id: issueId,
}
);
const { repository: { issue: { projectItems: { nodes: projectItems } } } } = response;
if (projectItems.length === 0) {
core.info('No project items found.\n');
return [];
}
const ids = projectItems.map((item) => item.id);
core.info(`Found [ ${ ids.join(', ') } ].\n`);
return ids;
} catch (error) {
if (error.status === 404) {
core.info('Nothing has found.\n');
return [];
}
}
}
- name: Updating field value of the project items
id: updating_items
uses: actions/github-script@v6
env:
ITEMS: ${{ steps.items.outputs.result }}
PROJECT_ID: ${{ steps.project_info.outputs.id }}
FIELD_ID: ${{ steps.project_info.outputs.field_id }}
FIELD_VALUE_ID: ${{ steps.project_info.outputs.field_value_id }}
with:
github-token: ${{ secrets.BOT_LABEL_ISSUE_TOKEN }}
script: |
const items = JSON.parse(process.env.ITEMS);
if (items.length === 0) {
core.info('Nothing to update.');
core.notice('No project items found for provided issues. Nothing to update.');
return;
}
for (const item of items) {
core.info(`Changing field value for item ${ item }...`);
await changeItemFieldValue(item);
core.info('Done.\n');
}
async function changeItemFieldValue(itemId) {
return await github.graphql(
`
mutation($input: UpdateProjectV2ItemFieldValueInput!) {
updateProjectV2ItemFieldValue(input: $input) {
clientMutationId
}
}
`,
{
input: {
projectId: process.env.PROJECT_ID,
fieldId: process.env.FIELD_ID,
itemId,
value: {
singleSelectOptionId: process.env.FIELD_VALUE_ID,
},
},
}
);
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment