-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Sync KDS Roadmap Project Statuses worfrkflow
- Loading branch information
1 parent
3dda510
commit 00584ac
Showing
3 changed files
with
309 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}); | ||
})); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |