name: 'Label Workstream Rollup' on: issues: types: ['opened', 'edited', 'reopened'] schedule: - cron: '0 * * * *' workflow_dispatch: jobs: labeler: runs-on: 'ubuntu-latest' permissions: issues: 'write' steps: - name: 'Check for Parent Workstream and Apply Label' uses: 'actions/github-script@v7' with: script: | const labelToAdd = 'workstream-rollup'; // Allow-list of parent issue URLs const allowedParentUrls = [ 'https://github.com/google-gemini/gemini-cli/issues/15374', 'https://github.com/google-gemini/gemini-cli/issues/15456', 'https://github.com/google-gemini/gemini-cli/issues/15324', 'https://github.com/google-gemini/gemini-cli/issues/17202', 'https://github.com/google-gemini/gemini-cli/issues/17203' ]; async function getIssueParent(owner, repo, number) { const query = ` query($owner:String!, $repo:String!, $number:Int!) { repository(owner:$owner, name:$repo) { issue(number:$number) { parent { url number } } } } `; try { const result = await github.graphql(query, { owner, repo, number }); return result.repository.issue.parent || null; } catch (error) { console.error(`Failed to fetch parent for #${number}:`, error); return null; } } // Determine which issues to process let issuesToProcess = []; if (context.eventName === 'issues') { // Context payload for 'issues' event already has the issue object issuesToProcess.push({ number: context.payload.issue.number, owner: context.repo.owner, repo: context.repo.repo }); } else { // For schedule/dispatch, fetch open issues (lightweight list) console.log(`Running for event: ${context.eventName}. Fetching open issues...`); const openIssues = await github.paginate(github.rest.issues.listForRepo, { owner: context.repo.owner, repo: context.repo.repo, state: 'open' }); issuesToProcess = openIssues.map(i => ({ number: i.number, owner: context.repo.owner, repo: context.repo.repo })); } console.log(`Processing ${issuesToProcess.length} issue(s)...`); for (const issue of issuesToProcess) { let currentNumber = issue.number; let depth = 0; const MAX_DEPTH = 5; // Safety limit for recursion let matched = false; let tracedParents = []; while (depth < MAX_DEPTH) { const parent = await getIssueParent(issue.owner, issue.repo, currentNumber); if (!parent) { break; } tracedParents.push(parent.url); if (allowedParentUrls.includes(parent.url)) { console.log(`SUCCESS: Issue #${issue.number} is a descendant of ${parent.url}. Trace: ${tracedParents.join(' -> ')}. Adding label.`); await github.rest.issues.addLabels({ owner: issue.owner, repo: issue.repo, issue_number: issue.number, labels: [labelToAdd] }); matched = true; break; } currentNumber = parent.number; depth++; } if (!matched && context.eventName === 'issues') { console.log(`Issue #${issue.number} did not match any allowed workstreams after checking ${depth} levels. Trace: ${tracedParents.join(' -> ') || 'None'}.`); } }