diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml index 0bd2daf..77669e8 100644 --- a/.github/workflows/project-automation.yml +++ b/.github/workflows/project-automation.yml @@ -16,13 +16,16 @@ jobs: - name: Auto-move items uses: actions/github-script@v7 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + # 🔑 KEY FIX: Use personal access token instead of GITHUB_TOKEN + github-token: ${{ secrets.PROJECT_TOKEN }} script: | const PROJECT_ID = process.env.PROJECT_ID; - // Helper function to get project info + // Helper function to get project info with better error handling async function getProjectInfo() { - // Direct project lookup by ID (works for user, org, and repo projects) + console.log(`🔍 Looking up project by ID: ${PROJECT_ID}`); + + // Direct project lookup by ID const directQuery = ` query($projectId: ID!) { node(id: $projectId) { @@ -51,7 +54,6 @@ jobs: `; try { - console.log(`Looking up project by ID: ${PROJECT_ID}`); const result = await github.graphql(directQuery, { projectId: PROJECT_ID }); @@ -60,80 +62,153 @@ jobs: console.log(`✅ Found project: ${result.node.title} (${result.node.id})`); return result.node; } else { - console.log('❌ Project ID resolved but returned null'); - return null; + throw new Error('Project ID resolved but returned null'); } } catch (directError) { console.log('❌ Direct project lookup failed:', directError.message); - // Fallback: Try to find project via viewer query - console.log('Trying to find project via viewer query...'); - const viewerQuery = ` - query { - viewer { - projectsV2(first: 20) { - nodes { - id - title - fields(first: 20) { - nodes { - ... on ProjectV2Field { - id - name - } - ... on ProjectV2SingleSelectField { - id - name - options { + // Enhanced fallback: Try multiple project scopes + console.log('🔄 Trying fallback approaches...'); + + // Try viewer projects + try { + const viewerQuery = ` + query { + viewer { + login + projectsV2(first: 50) { + nodes { + id + title + fields(first: 20) { + nodes { + ... on ProjectV2Field { id name } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } } } } } } } - } - `; - - try { + `; + const viewerResult = await github.graphql(viewerQuery); + console.log(`👤 Authenticated as: ${viewerResult.viewer.login}`); + console.log(`📋 Found ${viewerResult.viewer.projectsV2.nodes.length} user projects`); + const project = viewerResult.viewer.projectsV2.nodes.find(p => p.id === PROJECT_ID); if (project) { - console.log(`✅ Found project via viewer: ${project.title} (${project.id})`); + console.log(`✅ Found project via viewer: ${project.title}`); return project; - } else { - console.log('❌ Project not found in viewer projects'); - console.log('Available projects:', viewerResult.viewer.projectsV2.nodes.map(p => `${p.title} (${p.id})`)); - return null; } + + // Show available projects for debugging + if (viewerResult.viewer.projectsV2.nodes.length > 0) { + console.log('📋 Available user projects:'); + viewerResult.viewer.projectsV2.nodes.forEach(p => { + console.log(` - ${p.title} (${p.id})`); + }); + } + } catch (viewerError) { console.log('❌ Viewer query failed:', viewerError.message); - return null; } + + // Try organization projects + try { + const orgQuery = ` + query($owner: String!) { + organization(login: $owner) { + projectsV2(first: 50) { + nodes { + id + title + fields(first: 20) { + nodes { + ... on ProjectV2Field { + id + name + } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + } + `; + + const orgResult = await github.graphql(orgQuery, { + owner: context.repo.owner + }); + + console.log(`🏢 Found ${orgResult.organization.projectsV2.nodes.length} org projects`); + + const project = orgResult.organization.projectsV2.nodes.find(p => p.id === PROJECT_ID); + + if (project) { + console.log(`✅ Found project via organization: ${project.title}`); + return project; + } + + // Show available org projects + if (orgResult.organization.projectsV2.nodes.length > 0) { + console.log('📋 Available org projects:'); + orgResult.organization.projectsV2.nodes.forEach(p => { + console.log(` - ${p.title} (${p.id})`); + }); + } + + } catch (orgError) { + console.log('❌ Organization query failed:', orgError.message); + } + + console.log('❌ Project not found in any scope'); + return null; } } // Helper function to get item ID for issue/PR async function getProjectItemId(contentId) { const projectInfo = await getProjectInfo(); - if (!projectInfo) return null; + if (!projectInfo) { + console.log('❌ Cannot get project item - project info unavailable'); + return null; + } const query = ` query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { - items(first: 100) { + items(first: 200) { nodes { id content { ... on Issue { id + number } ... on PullRequest { id + number } } } @@ -151,9 +226,16 @@ jobs: const item = result.node.items.nodes.find( item => item.content && item.content.id === contentId ); - return item ? item.id : null; + + if (item) { + console.log(`✅ Found project item for content ${item.content.number}`); + return item.id; + } else { + console.log(`❌ Content not found in project items`); + return null; + } } catch (error) { - console.log('Error finding project item:', error); + console.log('❌ Error finding project item:', error.message); return null; } } @@ -161,7 +243,10 @@ jobs: // Helper function to add item to project async function addToProject(contentId) { const projectInfo = await getProjectInfo(); - if (!projectInfo) return null; + if (!projectInfo) { + console.log('❌ Cannot add to project - project info unavailable'); + return null; + } const mutation = ` mutation($projectId: ID!, $contentId: ID!) { @@ -174,13 +259,17 @@ jobs: `; try { + console.log(`🔄 Adding content to project...`); const result = await github.graphql(mutation, { projectId: projectInfo.id, contentId }); - return result.addProjectV2ItemById.item.id; + + const itemId = result.addProjectV2ItemById.item.id; + console.log(`✅ Successfully added to project with item ID: ${itemId}`); + return itemId; } catch (error) { - console.log('Error adding to project:', error); + console.log('❌ Error adding to project:', error.message); return null; } } @@ -188,14 +277,18 @@ jobs: // Helper function to update item status async function updateItemStatus(itemId, statusName) { const projectInfo = await getProjectInfo(); - if (!projectInfo) return; + if (!projectInfo) { + console.log('❌ Cannot update status - project info unavailable'); + return; + } const statusField = projectInfo.fields.nodes.find( field => field.name.toLowerCase() === 'status' ); if (!statusField || !statusField.options) { - console.log('Status field not found'); + console.log('❌ Status field not found'); + console.log('Available fields:', projectInfo.fields.nodes.map(f => f.name)); return; } @@ -204,7 +297,8 @@ jobs: ); if (!statusOption) { - console.log(`Status option "${statusName}" not found`); + console.log(`❌ Status option "${statusName}" not found`); + console.log('Available status options:', statusField.options.map(o => o.name)); return; } @@ -234,15 +328,12 @@ jobs: }); console.log(`✅ Updated item status to: ${statusName}`); } catch (error) { - console.log(`❌ Failed to update status to ${statusName}:`, error); + console.log(`❌ Failed to update status to ${statusName}:`, error.message); } } // Helper function to get current item status async function getCurrentItemStatus(itemId) { - const projectInfo = await getProjectInfo(); - if (!projectInfo) return null; - const query = ` query($itemId: ID!) { node(id: $itemId) { @@ -271,42 +362,53 @@ jobs: ); return statusField ? statusField.name : null; } catch (error) { - console.log('Error getting current status:', error); + console.log('❌ Error getting current status:', error.message); return null; } } // Main automation logic + console.log(`🚀 Starting automation for ${context.eventName} event`); + if (context.eventName === 'issues') { const action = context.payload.action; const issue = context.payload.issue; const issueId = issue.node_id; - console.log(`📝 Processing issue #${issue.number} - Action: ${action}`); + console.log(`📝 Processing issue #${issue.number}: "${issue.title}"`); + console.log(` Action: ${action}`); + console.log(` Issue ID: ${issueId}`); // Ensure issue is added to project let itemId = await getProjectItemId(issueId); if (!itemId) { + console.log(`🔄 Issue not in project, adding...`); itemId = await addToProject(issueId); if (itemId) { console.log(`✅ Added issue #${issue.number} to project`); + } else { + console.log(`❌ Failed to add issue #${issue.number} to project`); + return; } + } else { + console.log(`✅ Issue #${issue.number} already in project`); } if (itemId) { // Smart status transitions based on action if (action === 'opened') { - // New issues go to backlog for triage + console.log(`🆕 New issue opened, moving to Backlog`); await updateItemStatus(itemId, 'Backlog'); } else if (action === 'assigned') { - // Assignment moves to Ready (not automatically In Progress) - // Team member decides when to start work + console.log(`👤 Issue assigned, moving to Ready`); await updateItemStatus(itemId, 'Ready'); } else if (action === 'closed') { + console.log(`✅ Issue closed, moving to Done`); await updateItemStatus(itemId, 'Done'); } else if (action === 'labeled') { - // Label-based transitions for more granular control const label = context.payload.label; + console.log(`🏷️ Label added: ${label.name}`); + if (label.name === 'ready') { await updateItemStatus(itemId, 'Ready'); } else if (label.name === 'in-progress') { @@ -316,9 +418,9 @@ jobs: } else if (label.name === 'needs-review') { await updateItemStatus(itemId, 'In Review'); } else if (label.name === 'priority-high') { - // High priority items can jump to Ready if not already started const currentStatus = await getCurrentItemStatus(itemId); if (currentStatus === 'Backlog') { + console.log(`🚀 High priority item, promoting from Backlog to Ready`); await updateItemStatus(itemId, 'Ready'); } } @@ -331,35 +433,48 @@ jobs: const pr = context.payload.pull_request; const prId = pr.node_id; - console.log(`🔍 Processing PR #${pr.number} - Action: ${action}`); + console.log(`🔍 Processing PR #${pr.number}: "${pr.title}"`); + console.log(` Action: ${action}`); + console.log(` PR ID: ${prId}`); // Ensure PR is added to project let itemId = await getProjectItemId(prId); if (!itemId) { + console.log(`🔄 PR not in project, adding...`); itemId = await addToProject(prId); if (itemId) { console.log(`✅ Added PR #${pr.number} to project`); + } else { + console.log(`❌ Failed to add PR #${pr.number} to project`); + return; } + } else { + console.log(`✅ PR #${pr.number} already in project`); } if (itemId) { - // Move based on action if (action === 'opened' || action === 'ready_for_review') { + console.log(`📝 PR ready for review, moving to In Review`); await updateItemStatus(itemId, 'In Review'); } else if (action === 'converted_to_draft') { + console.log(`📝 PR converted to draft, moving to In Progress`); await updateItemStatus(itemId, 'In Progress'); } else if (action === 'closed') { if (pr.merged) { + console.log(`🎉 PR merged, moving to Done`); await updateItemStatus(itemId, 'Done'); - console.log(`🎉 PR #${pr.number} merged and moved to Done`); // Auto-close and move linked issues const body = pr.body || ''; const issueNumbers = [...body.matchAll(/(?:closes|fixes|resolves)\s+#(\d+)/gi)] .map(match => parseInt(match[1])); + console.log(`🔗 Found ${issueNumbers.length} linked issues: ${issueNumbers}`); + for (const issueNumber of issueNumbers) { try { + console.log(`🔄 Processing linked issue #${issueNumber}...`); + // Close the issue await github.rest.issues.update({ owner: context.repo.owner, @@ -388,13 +503,15 @@ jobs: console.log(`✅ Auto-closed and moved issue #${issueNumber} to Done`); } catch (error) { - console.log(`❌ Failed to process linked issue #${issueNumber}:`, error); + console.log(`❌ Failed to process linked issue #${issueNumber}:`, error.message); } } } else { + console.log(`📝 PR closed without merge, moving back to Todo`); await updateItemStatus(itemId, 'Todo'); - console.log(`📝 PR #${pr.number} closed without merge, moved back to Todo`); } } } - } \ No newline at end of file + } + + console.log(`🎯 Automation completed for ${context.eventName} event`); \ No newline at end of file