name: '🏷️ Enforce Restricted Label Permissions' on: issues: types: - 'labeled' - 'unlabeled' jobs: enforce-label: # Run this job only when restricted labels are changed if: |- ${{ (github.event.label.name == 'help wanted' || github.event.label.name == 'status/need-triage' || github.event.label.name == '🔒 maintainer only') && (github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli') }} runs-on: 'ubuntu-latest' permissions: issues: 'write' steps: - name: 'Generate GitHub App Token' id: 'generate_token' env: APP_ID: '${{ secrets.APP_ID }}' if: |- ${{ env.APP_ID != '' }} uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2 with: app-id: '${{ secrets.APP_ID }}' private-key: '${{ secrets.PRIVATE_KEY }}' - name: 'Check if user is in the maintainers team' uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' with: github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' script: |- const org = context.repo.owner; const username = context.payload.sender.login; const team_slug = 'gemini-cli-maintainers'; const action = context.payload.action; // 'labeled' or 'unlabeled' const labelName = context.payload.label.name; // Skip if the change was made by a bot to avoid infinite loops if (username === 'github-actions[bot]' || username === 'gemini-cli[bot]' || username.endsWith('[bot]')) { core.info('Change made by a bot. Skipping.'); return; } try { // Check repository permission level directly. // This is more robust than team membership as it doesn't require Org-level read permissions // and correctly handles Repo Admins/Writers who might not be in the specific team. const { data: { permission } } = await github.rest.repos.getCollaboratorPermissionLevel({ owner: org, repo: context.repo.repo, username, }); if (permission === 'admin' || permission === 'write') { core.info(`${username} has '${permission}' permission. Allowed.`); return; } core.info(`${username} has '${permission}' permission (needs 'write' or 'admin'). Reverting '${action}' action for '${labelName}' label.`); } catch (error) { core.error(`Failed to check permissions for ${username}: ${error.message}`); // Fall through to revert logic if we can't verify permissions (fail safe) } // If we are here, the user is NOT authorized. if (true) { // wrapping block to preserve variable scope if needed if (action === 'labeled') { // 1. Remove the label if added by a non-maintainer await github.rest.issues.removeLabel ({ owner: org, repo: context.repo.repo, issue_number: context.issue.number, name: labelName }); // 2. Post a polite comment const comment = ` Hi @${username}, thank you for your interest in helping triage issues! The \`${labelName}\` label is reserved for project maintainers to apply. This helps us ensure that an issue is ready and properly vetted for community contribution. A maintainer will review this issue soon. Please see our [CONTRIBUTING.md](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md) for more details on our labeling process. `.trim().replace(/^[ ]+/gm, ''); await github.rest.issues.createComment ({ owner: org, repo: context.repo.repo, issue_number: context.issue.number, body: comment }); } else if (action === 'unlabeled') { // 1. Add the label back if removed by a non-maintainer await github.rest.issues.addLabels ({ owner: org, repo: context.repo.repo, issue_number: context.issue.number, labels: [labelName] }); // 2. Post a polite comment const comment = ` Hi @${username}, it looks like the \`${labelName}\` label was removed. This label is managed by project maintainers. We've added it back to ensure the issue remains visible to potential contributors until a maintainer decides otherwise. Thank you for your understanding! `.trim().replace(/^[ ]+/gm, ''); await github.rest.issues.createComment ({ owner: org, repo: context.repo.repo, issue_number: context.issue.number, body: comment }); } }