Skip to content

feat(vcs): 增加引用树工具栏与分支更新[win] #96

feat(vcs): 增加引用树工具栏与分支更新[win]

feat(vcs): 增加引用树工具栏与分支更新[win] #96

name: Lithe issue claim
on:
issue_comment:
types: [created]
schedule:
- cron: '17 2 * * *'
workflow_dispatch:
permissions:
issues: write
concurrency:
group: lithe-issue-claim-${{ github.repository }}
cancel-in-progress: false
jobs:
command:
if: github.event_name == 'issue_comment' && github.event.issue.pull_request == null
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- uses: actions/github-script@v7
env:
CLAIM_LOGIC: ${{ github.workspace }}/.github/lithe-issue-claim/logic.mjs
with:
github-token: ${{ github.token }}
script: |
const logic = await import(process.env.CLAIM_LOGIC);
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const command = logic.parseCommand(context.payload.comment.body);
if (!command) return;
const login = context.payload.comment.user.login;
const issue = (await github.rest.issues.get({ owner, repo, issue_number })).data;
let assignee = issue.assignees?.[0]?.login;
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 });
const trusted = (c) => c.user?.type === 'Bot' && c.user?.login === 'github-actions[bot]';
const recordedClaim = [...comments].reverse().filter(trusted).map((c) => logic.parseMarker(c.body, logic.CLAIM_MARKER)).find(Boolean);
const recordedRelease = [...comments].reverse().filter(trusted).map((c) => logic.parseMarker(c.body, logic.RELEASE_MARKER)).find((release) => logic.releaseMatchesClaim(release, recordedClaim));
const activeClaim = logic.isActiveClaim(recordedClaim, recordedRelease) ? recordedClaim : null;
const maintainer = logic.isMaintainer(context.payload.comment.author_association);
const comment = async (body) => github.rest.issues.createComment({ owner, repo, issue_number, body });
if (command === '/assign') {
if (assignee === login) return comment(`@${login} 已经认领了这个 Issue。`);
if (assignee && recordedClaim?.login === assignee && !activeClaim) {
try {
await github.rest.issues.removeAssignees({ owner, repo, issue_number, assignees: [assignee] });
assignee = null;
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: logic.STALE_LABEL }).catch(() => {});
} catch (error) {
core.warning(`Unable to clear released assignee ${assignee}: ${error.message}`);
return comment('认领没有完成:当前 Issue 的旧负责人状态尚未清理,请稍后重试或联系维护者。');
}
}
if (assignee) return comment(`这个 Issue 当前由 @${assignee} 负责,不能抢占。`);
if (activeClaim?.login === login) return comment(`@${login} 已经登记认领了这个 Issue。`);
if (activeClaim && activeClaim.login !== login) return comment(`这个 Issue 当前由 @${activeClaim.login} 负责,不能抢占。`);
let official = false;
try {
await github.rest.issues.addAssignees({ owner, repo, issue_number, assignees: [login] });
const current = (await github.rest.issues.get({ owner, repo, issue_number })).data.assignees?.[0]?.login;
official = current === login;
} catch (error) {
core.warning(`Unable to set assignee for ${login}; using recorded claim fallback: ${error.message}`);
}
const marker = logic.claimMarker(login, new Date().toISOString());
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: logic.STALE_LABEL }).catch(() => {});
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [logic.CLAIM_LABEL] });
await comment(`${marker}\n@${login} ${official ? '已认领这个 Issue。' : '已登记认领这个 Issue,等待维护者确认 GitHub 负责人字段。'}`);
} catch (error) {
if (official) await github.rest.issues.removeAssignees({ owner, repo, issue_number, assignees: [login] }).catch(() => {});
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: logic.CLAIM_LABEL }).catch(() => {});
core.warning(`Unable to complete claim state for ${login}: ${error.message}`);
return comment('认领没有完成:机器人无法写入完整状态,请稍后重试或联系维护者。');
}
return;
}
const effectiveOwner = assignee || activeClaim?.login;
if (!effectiveOwner) return comment('这个 Issue 当前没有负责人。');
if (effectiveOwner !== login && !maintainer) return comment(`只有当前负责人 @${effectiveOwner} 或维护者可以释放认领。`);
try {
await comment(`${logic.releaseMarker(effectiveOwner, new Date().toISOString())}\n@${effectiveOwner} 已释放这个 Issue,其他贡献者可以继续认领。`);
} catch (error) {
core.warning(`Unable to record release for ${effectiveOwner}: ${error.message}`);
return comment('释放没有完成:机器人无法记录释放状态,请稍后重试或联系维护者。');
}
if (assignee) await github.rest.issues.removeAssignees({ owner, repo, issue_number, assignees: [assignee] }).catch((error) => core.warning(`Unable to remove assignee: ${error.message}`));
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: logic.CLAIM_LABEL }).catch(() => {});
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: logic.STALE_LABEL }).catch(() => {});
return;
stale:
if: github.event_name != 'issue_comment'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- uses: actions/github-script@v7
env:
CLAIM_LOGIC: ${{ github.workspace }}/.github/lithe-issue-claim/logic.mjs
with:
github-token: ${{ github.token }}
script: |
const logic = await import(process.env.CLAIM_LOGIC);
const { owner, repo } = context.repo;
const issues = await github.paginate(github.rest.issues.listForRepo, { owner, repo, state: 'open', labels: logic.CLAIM_LABEL, per_page: 100 });
for (const issue of issues.filter((item) => !item.pull_request)) {
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: issue.number, per_page: 100 });
const trusted = (c) => c.user?.type === 'Bot' && c.user?.login === 'github-actions[bot]';
const claim = [...comments].reverse().filter(trusted).map((c) => logic.parseMarker(c.body, logic.CLAIM_MARKER)).find(Boolean);
if (!claim) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: issue.number, name: logic.CLAIM_LABEL }).catch(() => {});
continue;
}
const release = [...comments].reverse().filter(trusted).map((c) => logic.parseMarker(c.body, logic.RELEASE_MARKER)).find((candidate) => logic.releaseMatchesClaim(candidate, claim));
if (!logic.isActiveClaim(claim, release)) {
const current = (await github.rest.issues.get({ owner, repo, issue_number: issue.number })).data;
let stillOwned = current.assignees?.some((a) => a.login === claim.login);
if (logic.needsReleaseRecovery(claim, release, current.assignees?.map((a) => a.login))) {
try {
await github.rest.issues.removeAssignees({ owner, repo, issue_number: issue.number, assignees: [claim.login] });
stillOwned = false;
} catch (error) {
core.warning(`Unable to recover released assignee: ${error.message}`);
continue;
}
}
if (!stillOwned) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: issue.number, name: logic.CLAIM_LABEL }).catch(() => {});
await github.rest.issues.removeLabel({ owner, repo, issue_number: issue.number, name: logic.STALE_LABEL }).catch(() => {});
}
continue;
}
const official = issue.assignees?.some((a) => a.login === claim.login);
const progressDate = logic.latestProgressDate(comments, claim.login, claim.isoDate);
const activityDate = progressDate || claim.isoDate;
const now = new Date();
const warning = [...comments].reverse().filter(trusted).map((c) => logic.parseMarker(c.body, logic.WARNING_MARKER)).find((m) => m?.login === claim.login && new Date(m.isoDate) > new Date(activityDate) && new Date(m.isoDate) <= now);
if (progressDate && !warning) await github.rest.issues.removeLabel({ owner, repo, issue_number: issue.number, name: logic.STALE_LABEL }).catch(() => {});
const status = logic.claimStatus({ ...claim, isoDate: activityDate }, warning);
if (status === 'warn') {
await github.rest.issues.addLabels({ owner, repo, issue_number: issue.number, labels: [logic.STALE_LABEL] });
await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${logic.warningMarker(claim.login, new Date().toISOString())}\n@${claim.login} 已认领此 Issue 30 天,但暂未看到进展。请在 7 天内更新进度或使用 /unassign 释放。` });
} else if (status === 'release') {
const latestComments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: issue.number, per_page: 100 });
const latestClaim = [...latestComments].reverse().filter(trusted).map((c) => logic.parseMarker(c.body, logic.CLAIM_MARKER)).find(Boolean);
if (latestClaim?.login !== claim.login || latestClaim?.isoDate !== claim.isoDate) continue;
const releaseBody = `${logic.releaseMarker(claim.login, new Date().toISOString())}\n@${claim.login} 已连续 7 天未更新进度,认领已自动释放,其他贡献者可以继续申请。`;
const existingRelease = latestComments.filter(trusted).map((c) => logic.parseMarker(c.body, logic.RELEASE_MARKER)).some((release) => logic.releaseMatchesClaim(release, claim));
if (!existingRelease) await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: releaseBody });
const current = (await github.rest.issues.get({ owner, repo, issue_number: issue.number })).data;
const stillOwned = current.assignees?.some((a) => a.login === claim.login);
if (stillOwned) {
try {
await github.rest.issues.removeAssignees({ owner, repo, issue_number: issue.number, assignees: [claim.login] });
} catch (error) {
core.warning(`Unable to remove stale assignee: ${error.message}`);
continue;
}
}
await github.rest.issues.removeLabel({ owner, repo, issue_number: issue.number, name: logic.CLAIM_LABEL }).catch(() => {});
await github.rest.issues.removeLabel({ owner, repo, issue_number: issue.number, name: logic.STALE_LABEL }).catch(() => {});
}
}