fix(tui): first-class shells on the work strip #6874
This file contains hidden or 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
| name: Approve gated contributor | |
| on: | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| concurrency: | |
| group: contribution-gate-approval | |
| cancel-in-progress: false | |
| jobs: | |
| approve: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Open allowlist update PR | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const comment = context.payload.comment; | |
| const issue = context.payload.issue; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); | |
| const command = (comment.body || '').trim().toLowerCase(); | |
| const scopeByCommand = new Map([ | |
| ['/lgtm', 'pr'], | |
| ['lgtm', 'pr'], | |
| ['/lgtmi', 'issue'], | |
| ['lgtmi', 'issue'], | |
| ]); | |
| const scope = scopeByCommand.get(command); | |
| if (!scope) return; | |
| if (!privileged.has(comment.author_association)) return; | |
| if (scope === 'pr' && !issue.pull_request) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: '`/lgtm` grants PR access and must be used on a pull request. Use `/lgtmi` to grant issue access.', | |
| }); | |
| return; | |
| } | |
| if (scope === 'issue' && issue.pull_request) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: '`/lgtmi` grants issue access and must be used on an issue. Use `/lgtm` to grant PR access.', | |
| }); | |
| return; | |
| } | |
| const path = '.github/APPROVED_CONTRIBUTORS'; | |
| const targetLogin = issue.user.login; | |
| const normalizedLogin = targetLogin.toLowerCase(); | |
| const entry = `${scope}:${normalizedLogin}`; | |
| const branchSlug = normalizedLogin.replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'contributor'; | |
| const defaultContent = [ | |
| '# Scoped contribution-gate allowlist.', | |
| '#', | |
| '# Maintainers and collaborators bypass the gate automatically. Use this file', | |
| '# for external contributors who are allowed through the automated front door.', | |
| '# Seed active contributors here before switching the gate workflows to enforce mode.', | |
| '#', | |
| '# Supported entries:', | |
| '# pr:username', | |
| '# issue:username', | |
| '# all:username', | |
| '', | |
| ].join('\n'); | |
| function parseAllowlist(content) { | |
| return new Set( | |
| content | |
| .split(/\r?\n/) | |
| .map(line => line.replace(/#.*/, '').trim().toLowerCase()) | |
| .filter(Boolean) | |
| ); | |
| } | |
| const { data: repoData } = await github.rest.repos.get({ owner, repo }); | |
| const defaultBranch = repoData.default_branch; | |
| const { data: baseRef } = await github.rest.git.getRef({ | |
| owner, | |
| repo, | |
| ref: `heads/${defaultBranch}`, | |
| }); | |
| const baseSha = baseRef.object.sha; | |
| const { data: baseCommit } = await github.rest.git.getCommit({ | |
| owner, | |
| repo, | |
| commit_sha: baseSha, | |
| }); | |
| let content = defaultContent; | |
| try { | |
| const { data } = await github.rest.repos.getContent({ | |
| owner, | |
| repo, | |
| path, | |
| ref: defaultBranch, | |
| }); | |
| if (!Array.isArray(data) && data.type === 'file') { | |
| content = Buffer.from(data.content, data.encoding || 'base64').toString('utf8'); | |
| } | |
| } catch (error) { | |
| if (error.status !== 404) throw error; | |
| } | |
| const existing = parseAllowlist(content); | |
| if (existing.has(entry) || existing.has(`all:${normalizedLogin}`)) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: `@${targetLogin} is already approved for ${scope} contributions in \`${path}\`.`, | |
| }); | |
| return; | |
| } | |
| const openPrs = []; | |
| for (let page = 1; ; page++) { | |
| const { data: pagePrs } = await github.rest.pulls.list({ | |
| owner, | |
| repo, | |
| state: 'open', | |
| per_page: 100, | |
| page, | |
| }); | |
| openPrs.push(...pagePrs); | |
| if (pagePrs.length < 100) break; | |
| } | |
| const repoFullName = `${owner}/${repo}`.toLowerCase(); | |
| const pendingPr = openPrs.find(openPr => { | |
| const sameRepo = (openPr.head?.repo?.full_name || '').toLowerCase() === repoFullName; | |
| const body = openPr.body || ''; | |
| return sameRepo && body.includes(`Adds \`${entry}\` to \`${path}\`.`); | |
| }); | |
| if (pendingPr) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: `@${targetLogin} already has a pending allowlist update PR for ${scope} contributions: ${pendingPr.html_url}`, | |
| }); | |
| return; | |
| } | |
| const nextContent = `${content.trimEnd()}\n${entry}\n`; | |
| const { data: blob } = await github.rest.git.createBlob({ | |
| owner, | |
| repo, | |
| content: nextContent, | |
| encoding: 'utf-8', | |
| }); | |
| const { data: tree } = await github.rest.git.createTree({ | |
| owner, | |
| repo, | |
| base_tree: baseCommit.tree.sha, | |
| tree: [ | |
| { | |
| path, | |
| mode: '100644', | |
| type: 'blob', | |
| sha: blob.sha, | |
| }, | |
| ], | |
| }); | |
| const branchName = `contribution-gate/${scope}-${branchSlug}-${Date.now()}`; | |
| await github.rest.git.createRef({ | |
| owner, | |
| repo, | |
| ref: `refs/heads/${branchName}`, | |
| sha: baseSha, | |
| }); | |
| const { data: commit } = await github.rest.git.createCommit({ | |
| owner, | |
| repo, | |
| message: `chore: approve @${targetLogin} for ${scope} contributions`, | |
| tree: tree.sha, | |
| parents: [baseSha], | |
| }); | |
| await github.rest.git.updateRef({ | |
| owner, | |
| repo, | |
| ref: `heads/${branchName}`, | |
| sha: commit.sha, | |
| }); | |
| const { data: pr } = await github.rest.pulls.create({ | |
| owner, | |
| repo, | |
| title: `chore: approve @${targetLogin} for ${scope} contributions`, | |
| head: branchName, | |
| base: defaultBranch, | |
| body: [ | |
| `Adds \`${entry}\` to \`${path}\`.`, | |
| '', | |
| `Requested by @${comment.user.login} in #${issue.number}.`, | |
| ].join('\n'), | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: `Created allowlist update PR: ${pr.html_url}`, | |
| }); |