Skip to content

Commit

Permalink
Add Update Issues statuses on Release workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexVelezLl committed Jan 9, 2025
1 parent 7ce6461 commit ac4be0b
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 3 deletions.
98 changes: 98 additions & 0 deletions .github/GithubAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,38 @@ module.exports = class GithubAPI {
};
}

async getProject (projectNumber) {
const query = `
query GetProject($owner: String!, $projectNumber: Int!) {
organization(login: $owner) {
projectV2(number: $projectNumber) {
id
status: field(name: "Status") {
... on ProjectV2SingleSelectField {
id
options {
id
name
}
}
}
releasedIn: field(name: "Released in") {
... on ProjectV2Field {
id
}
}
}
}
}
`;
const response = await this.github.graphql(query, {
owner: this.owner,
projectNumber
});

return response.organization.projectV2;
}

async getProjectItems(projectId) {
const statusSubquery = `
status: fieldValueByName(name: "Status") {
Expand Down Expand Up @@ -133,6 +165,72 @@ module.exports = class GithubAPI {
return _getProjectItems();
}


async getPRsWithLinkedIssues(prNumbers, repo) {
const getPRKey = (prNumber) => `PR_${prNumber}`;
const getPRQuery = (prNumber) => {
const projectItemsSubquery = `
projectItems(first: 20) {
nodes{
id
releasedIn: fieldValueByName(name: "Released in"){
... on ProjectV2ItemFieldTextValue {
text
}
}
project {
id
number
}
}
}
`;
return `
${getPRKey(prNumber)}: pullRequest(number: ${prNumber}) {
id
number
${ projectItemsSubquery }
closingIssuesReferences(first: 20) {
nodes {
id
${ projectItemsSubquery }
}
}
}
`;
};
const query = `
query GetLinkedIssuesFromPRs($owner: String!, $repo: String!) {
repository(name: $repo, owner: $owner) {
${ prNumbers.map(getPRQuery).join('\n') }
}
}
`;
const response = await this.github.graphql(query, {
owner: this.owner,
repo,
});

return prNumbers.map(prNumber => {
return response.repository[getPRKey(prNumber)];
});
}

async addContentToProject(projectId, contentId) {
const query = `
mutation AddContentToProject($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item {
id
}
}
}
`;

const response = await this.github.graphql(query, { projectId, contentId });
return response.addProjectV2ItemById.item.id;
}

async updateProjectItemsFields(items) {
const query = `
mutation UpdateProjectItemField($projectId: ID!, $itemId: ID!, $fieldId: ID!, $newValue: ProjectV2FieldValue!) {
Expand Down
115 changes: 112 additions & 3 deletions .github/githubUtils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
const GithubAPI = require('./GithubAPI');

const ITERATION_BACKLOG_PROJECT_NUMBER = 15;
const KDS_ROADMAP_PROJECT_NUMBER = 29;

const synchronizeProjectsStatuses = async (context, github) => {
const sourceNumber = 15; //"Iteration backlog";
const targetNumber = 29; // KDS Roadmap
const sourceNumber = ITERATION_BACKLOG_PROJECT_NUMBER;
const targetNumber = KDS_ROADMAP_PROJECT_NUMBER;
const getTargetStatus = (sourceStatus) => {
const statusMap = {
"IN REVIEW": "IN REVIEW",
Expand Down Expand Up @@ -71,6 +74,112 @@ const synchronizeProjectsStatuses = async (context, github) => {
console.log(`${itemsToUpdate.length} items updated: `, itemsPayload.map(item => item.url));
}

const extractPullRequestNumbers = (releaseBody, owner) => {
const prRegex = new RegExp(`github\\.com/${owner}/[a-zA-Z0-9-_]+/pull/(\\d+)`, "g");
const prNumbers = [];
let match;
while ((match = prRegex.exec(releaseBody)) !== null) {
prNumbers.push(parseInt(match[1]));
}

const uniquePrNumbers = [...new Set(prNumbers)];
return uniquePrNumbers;
};

const updateReleasedItemsStatuses = async (context, github) => {
const body = context.payload.release.body;
const owner = context.payload.repository.owner.login;
const repo = context.payload.repository.name;
const release = context.payload.release.name;
const prNumbers = extractPullRequestNumbers(body, owner);

if (prNumbers.length === 0) {
console.log("No PRs found in release body");
return;
}

const githubAPI = new GithubAPI(owner, github);
const prs = await githubAPI.getPRsWithLinkedIssues(prNumbers, repo);
const project = await githubAPI.getProject(KDS_ROADMAP_PROJECT_NUMBER);

const contentItemsToAddToProject = [];
const projectItemsToUpdate = [];

prs.forEach((pr) => {
const closingIssues = pr.closingIssuesReferences.nodes;
if (closingIssues.length === 0) {
const prProjectItems = pr.projectItems.nodes;
const projectItem = prProjectItems.find((item) => item.project.id === project.id);
if (!projectItem) {
contentItemsToAddToProject.push({
contentId: pr.id,
});
return;
}
projectItemsToUpdate.push(projectItem);
return;
}

closingIssues.forEach((issue) => {
const issueProjectItems = issue.projectItems.nodes;
const projectItem = issueProjectItems.find((item) => item.project.id === project.id);
if (!projectItem) {
contentItemsToAddToProject.push({
contentId: issue.id,
});
return;
}
projectItemsToUpdate.push(projectItem);
});
});

if (contentItemsToAddToProject.length > 0) {
await Promise.all(contentItemsToAddToProject.map(async ({ contentId }) => {
const projectItemId = await githubAPI.addContentToProject(project.id, contentId);
projectItemsToUpdate.push({ id: projectItemId });
}));
}

const projectItemsChanges = [];

if (projectItemsToUpdate.length > 0) {
const releasedInFieldId = project.releasedIn.id;
const statusFieldId = project.status.id;
const statusReleasedOption = project.status.options.find((option) => option.name === "Released");
const statusReleasedId = statusReleasedOption.id;

projectItemsToUpdate.map(async (item) => {
// Update "Released in" field with the release version
const releasedIn = item.releasedIn?.text;
const releasedInValue = releasedIn ? `${releasedIn},${release}` : release;
projectItemsChanges.push({
projectId: project.id,
projectItemId: item.id,
fieldId: releasedInFieldId,
newValue: {
text: releasedInValue,
},
});

// Update status field to "Released"
projectItemsChanges.push({
projectId: project.id,
projectItemId: item.id,
fieldId: statusFieldId,
newValue: {
singleSelectOptionId: statusReleasedId,
},
});
});
}

if (projectItemsChanges.length > 0) {
await githubAPI.updateProjectItemsFields(projectItemsChanges);
}
}


module.exports = {
synchronizeProjectsStatuses
synchronizeProjectsStatuses,
updateReleasedItemsStatuses,
};
45 changes: 45 additions & 0 deletions .github/workflows/update_project_items_on_release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Update Issues statuses on Release

on:
pull_request:
release:
types: [published]
workflow_dispatch:
inputs:
context:
description: 'The simulated context of the release'
required: true

jobs:
update-issues-statuses:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'

- uses: tibdex/github-app-token@v1
id: generate-token
with:
app_id: ${{ secrets.LE_BOT_APP_ID }}
private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }}

- name: Update Issues statuses
uses: actions/github-script@v7
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const { updateReleasedItemsStatuses } = require('./.github/githubUtils.js');
let ghContext; // get context from input if its a workflow_dispatch event
if (context.payload.release) {
ghContext = context;
} else {
ghContext = context.payload.inputs.context;
ghContext = JSON.parse(ghContext);
}
updateReleasedItemsStatuses(ghContext, github);

0 comments on commit ac4be0b

Please sign in to comment.