Repository navigation
bug(portfolio-import): 欧洲 Excel 分号分隔 CSV 被整行当成单列 #435
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: Issue Claim | |
| on: | |
| issue_comment: | |
| types: [created] | |
| schedule: | |
| - cron: '17 1 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: read | |
| jobs: | |
| claim: | |
| if: ${{ github.event_name == 'issue_comment' && !github.event.issue.pull_request }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Assign claimed issue | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const issue = context.payload.issue; | |
| const comment = context.payload.comment; | |
| if (!issue || !comment || issue.state !== 'open') return; | |
| const actor = comment.user.login; | |
| const body = String(comment.body || '').trim(); | |
| const firstLine = body.split(/\r?\n/, 1)[0].trim(); | |
| const commandClaim = /^(?:\/?claim|\/?take|\/?认领)$/i.test(firstLine); | |
| const chineseClaim = /^(?:你好[,,!!。]?\s*)?(?:我想|我来|我要|我可以)?\s*(?:认领|接手)(?:\s*#?\d+)?(?:[((].*)?[。!!]?$/i.test(firstLine) | |
| || /^我来(?:也)?[。!!]?$/i.test(firstLine); | |
| const englishClaim = /^(?:hi[,!]?\s*)?(?:i(?:'d| would)? like to|i want to|i can|i(?:'ll| will))\s+(?:take|work on|pick up)\s+(?:this|this one|it)(?:[.!])?$/i.test(firstLine) | |
| || /^(?:please\s+)?assign\s+(?:this\s+)?(?:issue\s+)?to\s+me[.!]?$/i.test(firstLine); | |
| const isClaim = commandClaim || chineseClaim || englishClaim; | |
| const isRelease = /^(?:\/?release|\/?unclaim|\/?释放)$/i.test(firstLine); | |
| if (!isClaim && !isRelease) return; | |
| async function ensureClaimedLabel() { | |
| try { | |
| await github.rest.issues.getLabel({ owner, repo, name: 'claimed' }); | |
| } catch (error) { | |
| if (error.status !== 404) throw error; | |
| await github.rest.issues.createLabel({ | |
| owner, | |
| repo, | |
| name: 'claimed', | |
| color: '1d76db', | |
| description: 'Claimed by a contributor and currently in progress' | |
| }); | |
| } | |
| } | |
| async function removeClaimedLabel() { | |
| try { | |
| await github.rest.issues.removeLabel({ owner, repo, issue_number: issue.number, name: 'claimed' }); | |
| } catch (error) { | |
| if (error.status !== 404) throw error; | |
| } | |
| } | |
| const { data: current } = await github.rest.issues.get({ | |
| owner, | |
| repo, | |
| issue_number: issue.number | |
| }); | |
| const assignees = (current.assignees || []).map(a => a.login); | |
| if (isClaim) { | |
| if (assignees.includes(actor)) { | |
| await ensureClaimedLabel(); | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| labels: ['claimed'] | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: `@${actor} 这个 Issue 已经分配给你。` | |
| }); | |
| return; | |
| } | |
| if (assignees.length > 0) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: `@${actor} 这个 Issue 已经由 ${assignees.map(a => `@${a}`).join(', ')} 认领。` | |
| }); | |
| return; | |
| } | |
| await github.rest.issues.addAssignees({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| assignees: [actor] | |
| }); | |
| const { data: assignedIssue } = await github.rest.issues.get({ | |
| owner, | |
| repo, | |
| issue_number: issue.number | |
| }); | |
| const assigned = (assignedIssue.assignees || []).some(a => a.login === actor); | |
| if (!assigned) { | |
| core.setFailed(`GitHub did not persist @${actor} as an assignee for issue #${issue.number}`); | |
| return; | |
| } | |
| await ensureClaimedLabel(); | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| labels: ['claimed'] | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: `@${actor} 已认领该 Issue,并已自动设为 Assignee。提交 PR 时请在描述中写 \`Closes #${issue.number}\`。` | |
| }); | |
| return; | |
| } | |
| const canRelease = assignees.includes(actor) || actor === owner; | |
| if (!canRelease) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: `@${actor} 只有当前 Assignee 或仓库维护者可以释放这个 Issue。` | |
| }); | |
| return; | |
| } | |
| if (assignees.length > 0) { | |
| const toRemove = actor === owner ? assignees : [actor]; | |
| await github.rest.issues.removeAssignees({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| assignees: toRemove | |
| }); | |
| } | |
| await removeClaimedLabel(); | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: '该 Issue 已释放,可以重新认领。' | |
| }); | |
| stale-claims: | |
| if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Remind and release stale claims | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const now = Date.now(); | |
| const DAY = 24 * 60 * 60 * 1000; | |
| const REMIND_AFTER = 7 * DAY; | |
| const RELEASE_AFTER_REMINDER = 3 * DAY; | |
| const claimFirstLine = firstLine => { | |
| const commandClaim = /^(?:\/?claim|\/?take|\/?认领)$/i.test(firstLine); | |
| const chineseClaim = /^(?:你好[,,!!。]?\s*)?(?:我想|我来|我要|我可以)?\s*(?:认领|接手)(?:\s*#?\d+)?(?:[((].*)?[。!!]?$/i.test(firstLine) | |
| || /^我来(?:也)?[。!!]?$/i.test(firstLine); | |
| const englishClaim = /^(?:hi[,!]?\s*)?(?:i(?:'d| would)? like to|i want to|i can|i(?:'ll| will))\s+(?:take|work on|pick up)\s+(?:this|this one|it)(?:[.!])?$/i.test(firstLine) | |
| || /^(?:please\s+)?assign\s+(?:this\s+)?(?:issue\s+)?to\s+me[.!]?$/i.test(firstLine); | |
| return commandClaim || chineseClaim || englishClaim; | |
| }; | |
| const releaseFirstLine = firstLine => /^(?:\/?release|\/?unclaim|\/?释放)$/i.test(firstLine); | |
| const firstLineOf = body => String(body || '').trim().split(/\r?\n/, 1)[0].trim(); | |
| const reminderMarker = login => `<!-- stale-claim-reminder:${login} -->`; | |
| const issues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner, | |
| repo, | |
| state: 'open', | |
| labels: 'claimed', | |
| per_page: 100 | |
| }); | |
| const openPrs = await github.paginate(github.rest.pulls.list, { | |
| owner, | |
| repo, | |
| state: 'open', | |
| per_page: 100 | |
| }); | |
| for (const issue of issues) { | |
| if (issue.pull_request) continue; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| per_page: 100 | |
| }); | |
| const assignees = (issue.assignees || []).map(a => a.login); | |
| const staleAssignees = []; | |
| for (const login of assignees) { | |
| const userComments = comments.filter(c => c.user?.login === login); | |
| const claims = userComments.filter(c => claimFirstLine(firstLineOf(c.body))); | |
| if (claims.length === 0) continue; | |
| const claim = claims.reduce((latest, c) => | |
| new Date(c.created_at) > new Date(latest.created_at) ? c : latest | |
| ); | |
| const claimAt = new Date(claim.created_at).getTime(); | |
| const linkedOpenPr = openPrs.some(pr => { | |
| if (pr.user?.login !== login) return false; | |
| const text = `${pr.title || ''}\n${pr.body || ''}`; | |
| return new RegExp(`(^|\\s|[([])#${issue.number}(?!\\d)`).test(text); | |
| }); | |
| if (linkedOpenPr) continue; | |
| const progressComments = userComments.filter(c => { | |
| const at = new Date(c.created_at).getTime(); | |
| if (at <= claimAt) return false; | |
| const line = firstLineOf(c.body); | |
| if (claimFirstLine(line) || releaseFirstLine(line)) return false; | |
| return String(c.body || '').trim().length >= 10; | |
| }); | |
| const lastProgressAt = progressComments.reduce( | |
| (latest, c) => Math.max(latest, new Date(c.created_at).getTime()), | |
| claimAt | |
| ); | |
| if (now - lastProgressAt < REMIND_AFTER) continue; | |
| const marker = reminderMarker(login); | |
| const reminders = comments.filter(c => | |
| c.user?.login === 'github-actions[bot]' && | |
| String(c.body || '').includes(marker) | |
| ); | |
| const latestReminder = reminders.length === 0 | |
| ? null | |
| : reminders.reduce((latest, c) => | |
| new Date(c.created_at) > new Date(latest.created_at) ? c : latest | |
| ); | |
| if (!latestReminder || new Date(latestReminder.created_at).getTime() < lastProgressAt) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: `${marker}\n@${login} 这个 Issue 已认领超过 7 天,目前没有发现关联的 open PR,也没有看到最近 7 天的进度更新。\n\n如果仍在推进,请在 **3 天内** 留一条简短进度说明(包括正在等待的前置依赖也可以);如果已经有 PR,请贴出链接。若 3 天内没有回应,认领会自动释放,方便其他贡献者接手。` | |
| }); | |
| continue; | |
| } | |
| const reminderAt = new Date(latestReminder.created_at).getTime(); | |
| const repliedAfterReminder = userComments.some(c => | |
| new Date(c.created_at).getTime() > reminderAt && | |
| !claimFirstLine(firstLineOf(c.body)) && | |
| !releaseFirstLine(firstLineOf(c.body)) | |
| ); | |
| if (!repliedAfterReminder && now - reminderAt >= RELEASE_AFTER_REMINDER) { | |
| staleAssignees.push(login); | |
| } | |
| } | |
| if (staleAssignees.length === 0) continue; | |
| await github.rest.issues.removeAssignees({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| assignees: staleAssignees | |
| }); | |
| const { data: refreshed } = await github.rest.issues.get({ | |
| owner, | |
| repo, | |
| issue_number: issue.number | |
| }); | |
| if ((refreshed.assignees || []).length === 0) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| name: 'claimed' | |
| }); | |
| } catch (error) { | |
| if (error.status !== 404) throw error; | |
| } | |
| } | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issue.number, | |
| body: `${staleAssignees.map(a => `@${a}`).join(', ')} 在提醒后 3 天内没有进度回应,且仍未发现关联的 open PR;本次认领已自动释放。Issue 保持开放,可以重新认领。` | |
| }); | |
| } |