Skip to content

Commit

Permalink
Add Sync KDS Roadmap Project Statuses worfrkflow
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexVelezLl committed Jan 8, 2025
1 parent 3dda510 commit 00584ac
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 0 deletions.
190 changes: 190 additions & 0 deletions .github/GithubAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
const axios = require("axios");

module.exports = class GithubAPI {
constructor(owner) {
this.url = 'https://api.github.com/graphql';
this.token = process.env.GITHUB_TOKEN;
console.log("Is there a github token?", !!this.token);
this.owner = owner;
}

async query(query, variables) {
try {
const response = await axios.post(this.url, {
query,
variables
}, {
headers: {
Authorization: `Bearer ${this.token}`
}
});

if (response.data.errors) {
throw new Error(JSON.stringify(response.data.errors, null, 2));
}

return response.data;
} catch (error) {
console.error(error);
throw error;
}
}

async getSourceAndTargetProjects({ sourceNumber, targetNumber }) {
const projectSubquery = `
id
title
fields(first: 30) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
`;
const query = `
query getSourceAndTargetProjectsIds($owner: String!, $source: Int!, $target: Int!) {
organization (login: $owner) {
source: projectV2(number: $source) {
${projectSubquery}
}
target: projectV2(number: $target) {
${projectSubquery}
}
}
}
`;

const response = await this.query(query, {
owner: this.owner,
source: sourceNumber,
target: targetNumber,
});

const { source, target } = response.data.organization;

if (!source) {
throw new Error(`Source project not found: ${sourceNumber}`);
}

if (!target) {
throw new Error(`Target project not found: ${targetNumber}`);
}

return {
sourceProject: source,
targetProject: target
};
}

async getProjectItems(projectId) {
const statusSubquery = `
status: fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
vaueId: id
value: name
valueOptionId: optionId
}
}
`;
// Subquery to get info about the status of the issue/PR on each project it belongs to
const projectItemsSubquery = `
projectItems(first: 10) {
nodes {
id
${ statusSubquery }
project {
id
title
}
}
}
`;
const query = `
query GetProjectItems($projectId: ID!, $cursor: String) {
node(id: $projectId) {
... on ProjectV2 {
items(first: 50, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
${ statusSubquery }
content {
__typename
... on Issue {
id
title
${ projectItemsSubquery }
}
... on PullRequest {
id
title
${ projectItemsSubquery }
}
}
}
}
}
}
}
`;

const _getProjectItems = async (cursor = null, items = []) => {
const response = await this.query(query, {
projectId,
cursor
});

const { nodes, pageInfo } = response.data.node.items;

items.push(...nodes);

if (pageInfo.hasNextPage) {
return _getProjectItems(pageInfo.endCursor, items);
}

return items;
};

return _getProjectItems();
}

async updateProjectItemsFields(items) {
const query = `
mutation UpdateProjectItemField($projectId: ID!, $itemId: ID!, $fieldId: ID!, $newValue: ProjectV2FieldValue!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId,
itemId: $itemId,
fieldId: $fieldId,
value: $newValue
}) {
projectV2Item {
id
}
}
}
`;

const BATCH_SIZE = 10;
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);

await Promise.all(batch.map(async item => {
await this.query(query, {
projectId: item.projectId,
itemId: item.projectItemId,
fieldId: item.fieldId,
newValue: item.newValue
});
}));
}
}
}
78 changes: 78 additions & 0 deletions .github/githubUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const GithubAPI = require('./GithubAPI');

const synchronizeProjectsStatuses = async (github) => {
const sourceNumber = 15; //"Iteration backlog";
const targetNumber = 29; // KDS Roadmap
const getTargetStatus = (sourceStatus) => {
const statusMap = {
"IN REVIEW": "IN REVIEW",
"NEEDS QA": "IN REVIEW",
"DONE": "DONE",
// All other statuses are mapped to "BACKLOG"
};

const targetStatus = Object.keys(statusMap).find((key) =>
sourceStatus.toUpperCase().includes(key)
);

return targetStatus ? statusMap[targetStatus] : "BACKLOG";
}

const githubAPI = new GithubAPI("LearningEquality", github);
const { sourceProject, targetProject } = await githubAPI.getSourceAndTargetProjects({ sourceNumber, targetNumber });

console.log("sourceName", sourceNumber);
console.log("sourceProject", sourceProject);
const targetStatusField = targetProject.fields.nodes.find((field) => field.name === "Status");

const targetProjectItems = await githubAPI.getProjectItems(targetProject.id);
const itemsToUpdate = targetProjectItems.filter((item) => {
const sourceProjectItem = item.content.projectItems?.nodes.find((sourceItem) => (
sourceItem.project.id === sourceProject.id
));
if (!sourceProjectItem) {
return false;
}

const sourceStatus = sourceProjectItem.status?.value;
if (!sourceStatus) {
return false;
}

const currentTargetStatusId = item.status?.valueOptionId;
const newTargetStatus = getTargetStatus(sourceStatus);
const newTargetStatusId = targetStatusField.options.find((option) => option.name.toUpperCase().includes(newTargetStatus))?.id;

if (!newTargetStatusId) {
console.log(`Status "${newTargetStatus}" not found in target project`);
return false;
}

item.newStatusId = newTargetStatusId;

return newTargetStatusId !== currentTargetStatusId;
});

if (itemsToUpdate.length === 0) {
console.log("No items to update");
return;
}

const itemsPayload = itemsToUpdate.map(item => ({
projectId: targetProject.id,
projectItemId: item.id,
fieldId: targetStatusField.id,
newValue: {
singleSelectOptionId: item.newStatusId
}
}))

console.log(`Updating ${itemsToUpdate.length} items...`);
console.log("Items payload", itemsPayload);
// await githubAPI.updateProjectItemsFields(itemsPayload);
console.log("Items updated successfully");
}

module.exports = {
synchronizeProjectsStatuses
};
41 changes: 41 additions & 0 deletions .github/workflows/sync_kds_roadmap_statuses.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Sync KDS Roadmap Project Statuses

on:
schedule:
- cron: "0 * * * *" # Run every hour
workflow_dispatch:

jobs:
sync-projects:
runs-on: ubuntu-latest
permissions: write-all

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

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

- name: Install axios
run: npm install axios

- 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: Check and Sync Project Statuses
uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}

with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
console.log("github", github);
const { synchronizeProjectsStatuses } = require('./.github/githubUtils.js');
synchronizeProjectsStatuses(github);

0 comments on commit 00584ac

Please sign in to comment.