Skip to content

Sync Project with Milestones #3

Sync Project with Milestones

Sync Project with Milestones #3

name: Sync Project with Milestones
on:
issues:
types: [opened, edited, milestoned, demilestoned]
pull_request:
types: [opened, edited, milestoned, demilestoned]
workflow_dispatch:
# Manual trigger to process historical issues/PRs
schedule:
# Run daily at 2 AM UTC to sync any missed items
- cron: '0 2 * * *'
permissions:
issues: read
pull-requests: read
contents: read
# Organization-level permissions are required to access organization projects
# Note: This requires enabling "Read and write permissions" in repository settings or using a Personal Access Token
# The GITHUB_TOKEN needs project write permissions for organization projects
jobs:
get-project-id:
runs-on: ubuntu-latest
outputs:
project_id: ${{ steps.get_project.outputs.project_id }}
steps:
- name: Get Project ID
id: get_project
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const org = 'oceanbase';
const projectNumber = 27;
const owner = context.repo.owner;
const repo = context.repo.repo;
console.log('Looking for project #' + projectNumber + ' in organization "' + org + '" or repository "' + owner + '/' + repo + '"');
// First, try to find organization-level project
console.log(`Attempting to find organization project ${projectNumber} in ${org}...`);
const orgQuery = `
query($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) {
id
title
number
}
}
}
`;
try {
const orgResult = await github.graphql(orgQuery, {
org: org,
number: projectNumber
});
if (orgResult.organization && orgResult.organization.projectV2) {
console.log('✓ Found organization project:', orgResult.organization.projectV2.title);
console.log(' Project Number:', orgResult.organization.projectV2.number);
console.log(' Project ID:', orgResult.organization.projectV2.id);
core.setOutput('project_id', orgResult.organization.projectV2.id);
return;
}
} catch (orgError) {
console.log('Organization project not found, trying repository-level project...');
console.log('Error:', orgError.message);
// Check if it's a permission error
const isPermissionError = orgError.message && (
orgError.message.includes('A819') ||
orgError.message.includes('Something went wrong') ||
orgError.message.includes('permission') ||
orgError.message.includes('access') ||
orgError.message.includes('Could not resolve')
);
if (isPermissionError) {
console.log('\n⚠️ Permission issue detected when accessing organization project.');
console.log(' This usually means the token lacks organization project permissions.');
console.log(' Solution: Add a Personal Access Token (PAT) as secret PROJECT_ACCESS_TOKEN');
console.log(' Required PAT permissions: repo, read:org, write:org');
}
}
// If organization project not found, try repository-level project
console.log(`Attempting to find repository project ${projectNumber} in ${owner}/${repo}...`);
const repoQuery = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
projectV2(number: $number) {
id
title
number
}
}
}
`;
try {
const repoResult = await github.graphql(repoQuery, {
owner: owner,
repo: repo,
number: projectNumber
});
if (repoResult.repository && repoResult.repository.projectV2) {
console.log('✓ Found repository project:', repoResult.repository.projectV2.title);
console.log(' Project Number:', repoResult.repository.projectV2.number);
console.log(' Project ID:', repoResult.repository.projectV2.id);
core.setOutput('project_id', repoResult.repository.projectV2.id);
return;
}
} catch (repoError) {
console.log('Repository project not found.');
console.log('Error:', repoError.message);
}
// If both failed, list available projects for debugging
console.log('\n=== Listing available organization projects ===');
try {
const listOrgQuery = `
query($org: String!) {
organization(login: $org) {
projectsV2(first: 20) {
nodes {
id
title
number
}
}
}
}
`;
const listOrgResult = await github.graphql(listOrgQuery, { org: org });
if (listOrgResult.organization && listOrgResult.organization.projectsV2) {
console.log('Available organization projects:');
listOrgResult.organization.projectsV2.nodes.forEach(project => {
console.log(` - #${project.number}: ${project.title} (ID: ${project.id})`);
});
}
} catch (e) {
console.log('Could not list organization projects:', e.message);
if (e.message && (
e.message.includes('A819') ||
e.message.includes('Something went wrong')
)) {
console.log('⚠️ Permission error when listing organization projects.');
console.log(' This confirms the token lacks organization permissions.');
console.log(' Please add PROJECT_ACCESS_TOKEN secret with proper permissions.');
}
}
console.log(`\n=== Listing available repository projects for ${owner}/${repo} ===`);
try {
const listRepoQuery = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
projectsV2(first: 20) {
nodes {
id
title
number
}
}
}
}
`;
const listRepoResult = await github.graphql(listRepoQuery, {
owner: owner,
repo: repo
});
if (listRepoResult.repository && listRepoResult.repository.projectsV2) {
console.log('Available repository projects:');
listRepoResult.repository.projectsV2.nodes.forEach(project => {
console.log(` - #${project.number}: ${project.title} (ID: ${project.id})`);
});
}
} catch (e) {
console.log('Could not list repository projects:', e.message);
}
// Build comprehensive error message with solutions
let errorMsg = `\n❌ Project #${projectNumber} not found in organization '${org}' or repository '${owner}/${repo}'.\n\n`;
errorMsg += 'This is likely a permissions issue. Solutions:\n\n';
errorMsg += '🔑 Option 1: Add Personal Access Token (Recommended)\n';
errorMsg += ' 1. Create a PAT with permissions: repo, read:org, write:org\n';
errorMsg += ' 2. Add it as repository secret named: PROJECT_ACCESS_TOKEN\n';
errorMsg += ' 3. The workflow will automatically use it\n\n';
errorMsg += '⚙️ Option 2: Enable enhanced permissions\n';
errorMsg += ' 1. Repository Settings > Actions > General\n';
errorMsg += ' 2. Select "Read and write permissions"\n';
errorMsg += ' Note: May still not work for organization projects\n\n';
errorMsg += '✅ Option 3: Verify project exists\n';
errorMsg += ' - Confirm project #27 exists in organization or repository\n';
errorMsg += ' - Check if it\'s a Projects V2 (not legacy Projects)\n';
throw new Error(errorMsg);
add-to-project:
runs-on: ubuntu-latest
needs: get-project-id
if: |
(github.event_name == 'issues' && github.event.issue.milestone != null) ||
(github.event_name == 'pull_request' && github.event.pull_request.milestone != null)
steps:
- name: Add Issue/PR to Project
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const projectId = '${{ needs.get-project-id.outputs.project_id }}';
let itemId;
if (context.payload.issue) {
itemId = context.payload.issue.node_id;
console.log('Adding issue #' + context.payload.issue.number + ' to project');
} else if (context.payload.pull_request) {
itemId = context.payload.pull_request.node_id;
console.log('Adding PR #' + context.payload.pull_request.number + ' to project');
}
const mutation = `
mutation($projectId: ID!, $itemId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $itemId}) {
item {
id
}
}
}
`;
try {
const result = await github.graphql(mutation, {
projectId: projectId,
itemId: itemId
});
console.log('Successfully added to project:', result);
} catch (error) {
// If item already exists in project, ignore error
if (error.message && (
error.message.includes('already exists') ||
error.message.includes('already added')
)) {
console.log('Item already in project, skipping...');
} else {
console.error('Error adding to project:', error);
// Don't throw error to avoid workflow failure
}
}
sync-historical-items:
runs-on: ubuntu-latest
needs: get-project-id
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
steps:
- name: Sync Historical Issues and PRs with Milestones
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PROJECT_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const projectId = '${{ needs.get-project-id.outputs.project_id }}';
const owner = context.repo.owner;
const repo = context.repo.repo;
console.log(`Syncing historical items for ${owner}/${repo}...`);
// Function to add item to project
async function addItemToProject(itemId, itemNumber, itemType) {
const mutation = `
mutation($projectId: ID!, $itemId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $itemId}) {
item {
id
}
}
}
`;
try {
await github.graphql(mutation, {
projectId: projectId,
itemId: itemId
});
console.log(`✓ Added ${itemType} #${itemNumber} to project`);
return true;
} catch (error) {
if (error.message && (
error.message.includes('already exists') ||
error.message.includes('already added')
)) {
console.log(`- ${itemType} #${itemNumber} already in project`);
return false;
} else {
console.error(`✗ Error adding ${itemType} #${itemNumber}:`, error.message);
return false;
}
}
}
// Query all issues with milestones
let hasNextPage = true;
let cursor = null;
let totalIssues = 0;
let addedIssues = 0;
while (hasNextPage) {
const issuesQuery = `
query($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
issues(first: 100, after: $cursor, states: [OPEN, CLOSED], orderBy: {field: UPDATED_AT, direction: DESC}) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
number
milestone {
id
title
}
}
}
}
}
`;
const issuesResult = await github.graphql(issuesQuery, {
owner: owner,
repo: repo,
cursor: cursor
});
const issues = issuesResult.repository.issues.nodes.filter(issue => issue.milestone !== null);
totalIssues += issues.length;
for (const issue of issues) {
const added = await addItemToProject(issue.id, issue.number, 'Issue');
if (added) addedIssues++;
// Small delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 100));
}
hasNextPage = issuesResult.repository.issues.pageInfo.hasNextPage;
cursor = issuesResult.repository.issues.pageInfo.endCursor;
}
// Query all pull requests with milestones
hasNextPage = true;
cursor = null;
let totalPRs = 0;
let addedPRs = 0;
while (hasNextPage) {
const prsQuery = `
query($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
pullRequests(first: 100, after: $cursor, states: [OPEN, CLOSED, MERGED], orderBy: {field: UPDATED_AT, direction: DESC}) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
number
milestone {
id
title
}
}
}
}
}
`;
const prsResult = await github.graphql(prsQuery, {
owner: owner,
repo: repo,
cursor: cursor
});
const prs = prsResult.repository.pullRequests.nodes.filter(pr => pr.milestone !== null);
totalPRs += prs.length;
for (const pr of prs) {
const added = await addItemToProject(pr.id, pr.number, 'PR');
if (added) addedPRs++;
// Small delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 100));
}
hasNextPage = prsResult.repository.pullRequests.pageInfo.hasNextPage;
cursor = prsResult.repository.pullRequests.pageInfo.endCursor;
}
console.log('\n=== Sync Summary ===');
console.log(`Total issues with milestones: ${totalIssues}`);
console.log(`New issues added to project: ${addedIssues}`);
console.log(`Total PRs with milestones: ${totalPRs}`);
console.log(`New PRs added to project: ${addedPRs}`);
console.log(`Total items processed: ${totalIssues + totalPRs}`);
console.log(`Total items added: ${addedIssues + addedPRs}`);