Skip to content

Commit 0072209

Browse files
authored
v0.8.50: community harvest release (40 PRs, 14 contributors)
Full changelog in CHANGELOG.md. macOS failure is pre-existing flaky MCP network test; Windows was a CI timeout during compilation. Both unrelated to harvested changes.
2 parents 31f34c5 + f2df1d5 commit 0072209

111 files changed

Lines changed: 9373 additions & 566 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/APPROVED_CONTRIBUTORS

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Scoped contribution-gate allowlist.
2+
#
3+
# Maintainers and collaborators bypass the gate automatically. Use this file
4+
# for external contributors who are allowed through the automated front door.
5+
# Seed active contributors here before switching the gate workflows to enforce mode.
6+
#
7+
# Supported entries:
8+
# pr:username
9+
# issue:username
10+
# all:username
11+
all:hmbown
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
name: Approve gated contributor
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
permissions:
8+
contents: write
9+
issues: write
10+
pull-requests: write
11+
12+
concurrency:
13+
group: contribution-gate-approval
14+
cancel-in-progress: false
15+
16+
jobs:
17+
approve:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Open allowlist update PR
21+
uses: actions/github-script@v7
22+
with:
23+
script: |
24+
const comment = context.payload.comment;
25+
const issue = context.payload.issue;
26+
const owner = context.repo.owner;
27+
const repo = context.repo.repo;
28+
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
29+
const command = (comment.body || '').trim().toLowerCase();
30+
const scopeByCommand = new Map([
31+
['/lgtm', 'pr'],
32+
['lgtm', 'pr'],
33+
['/lgtmi', 'issue'],
34+
['lgtmi', 'issue'],
35+
]);
36+
const scope = scopeByCommand.get(command);
37+
38+
if (!scope) return;
39+
if (!privileged.has(comment.author_association)) return;
40+
if (scope === 'pr' && !issue.pull_request) {
41+
await github.rest.issues.createComment({
42+
owner,
43+
repo,
44+
issue_number: issue.number,
45+
body: '`/lgtm` grants PR access and must be used on a pull request. Use `/lgtmi` to grant issue access.',
46+
});
47+
return;
48+
}
49+
if (scope === 'issue' && issue.pull_request) {
50+
await github.rest.issues.createComment({
51+
owner,
52+
repo,
53+
issue_number: issue.number,
54+
body: '`/lgtmi` grants issue access and must be used on an issue. Use `/lgtm` to grant PR access.',
55+
});
56+
return;
57+
}
58+
59+
const path = '.github/APPROVED_CONTRIBUTORS';
60+
const targetLogin = issue.user.login;
61+
const normalizedLogin = targetLogin.toLowerCase();
62+
const entry = `${scope}:${normalizedLogin}`;
63+
const branchSlug = normalizedLogin.replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'contributor';
64+
65+
const defaultContent = [
66+
'# Scoped contribution-gate allowlist.',
67+
'#',
68+
'# Maintainers and collaborators bypass the gate automatically. Use this file',
69+
'# for external contributors who are allowed through the automated front door.',
70+
'# Seed active contributors here before switching the gate workflows to enforce mode.',
71+
'#',
72+
'# Supported entries:',
73+
'# pr:username',
74+
'# issue:username',
75+
'# all:username',
76+
'',
77+
].join('\n');
78+
79+
function parseAllowlist(content) {
80+
return new Set(
81+
content
82+
.split(/\r?\n/)
83+
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
84+
.filter(Boolean)
85+
);
86+
}
87+
88+
const { data: repoData } = await github.rest.repos.get({ owner, repo });
89+
const defaultBranch = repoData.default_branch;
90+
const { data: baseRef } = await github.rest.git.getRef({
91+
owner,
92+
repo,
93+
ref: `heads/${defaultBranch}`,
94+
});
95+
const baseSha = baseRef.object.sha;
96+
const { data: baseCommit } = await github.rest.git.getCommit({
97+
owner,
98+
repo,
99+
commit_sha: baseSha,
100+
});
101+
102+
let content = defaultContent;
103+
try {
104+
const { data } = await github.rest.repos.getContent({
105+
owner,
106+
repo,
107+
path,
108+
ref: defaultBranch,
109+
});
110+
if (!Array.isArray(data) && data.type === 'file') {
111+
content = Buffer.from(data.content, data.encoding || 'base64').toString('utf8');
112+
}
113+
} catch (error) {
114+
if (error.status !== 404) throw error;
115+
}
116+
117+
const existing = parseAllowlist(content);
118+
if (existing.has(entry) || existing.has(`all:${normalizedLogin}`)) {
119+
await github.rest.issues.createComment({
120+
owner,
121+
repo,
122+
issue_number: issue.number,
123+
body: `@${targetLogin} is already approved for ${scope} contributions in \`${path}\`.`,
124+
});
125+
return;
126+
}
127+
128+
const openPrs = [];
129+
for (let page = 1; ; page++) {
130+
const { data: pagePrs } = await github.rest.pulls.list({
131+
owner,
132+
repo,
133+
state: 'open',
134+
per_page: 100,
135+
page,
136+
});
137+
openPrs.push(...pagePrs);
138+
if (pagePrs.length < 100) break;
139+
}
140+
const repoFullName = `${owner}/${repo}`.toLowerCase();
141+
const pendingPr = openPrs.find(openPr => {
142+
const sameRepo = (openPr.head?.repo?.full_name || '').toLowerCase() === repoFullName;
143+
const body = openPr.body || '';
144+
return sameRepo && body.includes(`Adds \`${entry}\` to \`${path}\`.`);
145+
});
146+
147+
if (pendingPr) {
148+
await github.rest.issues.createComment({
149+
owner,
150+
repo,
151+
issue_number: issue.number,
152+
body: `@${targetLogin} already has a pending allowlist update PR for ${scope} contributions: ${pendingPr.html_url}`,
153+
});
154+
return;
155+
}
156+
157+
const nextContent = `${content.trimEnd()}\n${entry}\n`;
158+
const { data: blob } = await github.rest.git.createBlob({
159+
owner,
160+
repo,
161+
content: nextContent,
162+
encoding: 'utf-8',
163+
});
164+
const { data: tree } = await github.rest.git.createTree({
165+
owner,
166+
repo,
167+
base_tree: baseCommit.tree.sha,
168+
tree: [
169+
{
170+
path,
171+
mode: '100644',
172+
type: 'blob',
173+
sha: blob.sha,
174+
},
175+
],
176+
});
177+
178+
const branchName = `contribution-gate/${scope}-${branchSlug}-${Date.now()}`;
179+
await github.rest.git.createRef({
180+
owner,
181+
repo,
182+
ref: `refs/heads/${branchName}`,
183+
sha: baseSha,
184+
});
185+
186+
const { data: commit } = await github.rest.git.createCommit({
187+
owner,
188+
repo,
189+
message: `chore: approve @${targetLogin} for ${scope} contributions`,
190+
tree: tree.sha,
191+
parents: [baseSha],
192+
});
193+
await github.rest.git.updateRef({
194+
owner,
195+
repo,
196+
ref: `heads/${branchName}`,
197+
sha: commit.sha,
198+
});
199+
200+
const { data: pr } = await github.rest.pulls.create({
201+
owner,
202+
repo,
203+
title: `chore: approve @${targetLogin} for ${scope} contributions`,
204+
head: branchName,
205+
base: defaultBranch,
206+
body: [
207+
`Adds \`${entry}\` to \`${path}\`.`,
208+
'',
209+
`Requested by @${comment.user.login} in #${issue.number}.`,
210+
].join('\n'),
211+
});
212+
213+
await github.rest.issues.createComment({
214+
owner,
215+
repo,
216+
issue_number: issue.number,
217+
body: `Created allowlist update PR: ${pr.html_url}`,
218+
});

.github/workflows/auto-tag.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ on:
1919
paths:
2020
- 'Cargo.toml'
2121
- 'npm/codewhale/package.json'
22-
- 'npm/deepseek-tui/package.json'
2322
workflow_dispatch:
2423

2524
permissions:

.github/workflows/issue-gate.yml

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Contribution gate - issues
2+
3+
on:
4+
issues:
5+
types: [opened, reopened]
6+
7+
permissions:
8+
contents: read
9+
issues: write
10+
11+
env:
12+
# Keep new gates observable first. Switch to "enforce" only after maintainers
13+
# have seeded active contributors and reviewed the dry-run signal.
14+
CONTRIBUTION_GATE_MODE: dry-run
15+
16+
jobs:
17+
gate:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Gate unapproved external issues
21+
uses: actions/github-script@v7
22+
with:
23+
script: |
24+
const issue = context.payload.issue;
25+
const owner = context.repo.owner;
26+
const repo = context.repo.repo;
27+
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
28+
const gateMode = (process.env.CONTRIBUTION_GATE_MODE || 'dry-run').trim().toLowerCase();
29+
const enforceGate = gateMode === 'enforce';
30+
31+
if (!['dry-run', 'enforce'].includes(gateMode)) {
32+
core.warning(`Unknown CONTRIBUTION_GATE_MODE "${gateMode}"; defaulting to dry-run.`);
33+
}
34+
35+
if (privileged.has(issue.author_association)) return;
36+
if (issue.user.login === 'github-actions[bot]') return;
37+
38+
function parseAllowlist(content) {
39+
return new Set(
40+
content
41+
.split(/\r?\n/)
42+
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
43+
.filter(Boolean)
44+
);
45+
}
46+
47+
async function readAllowlist() {
48+
try {
49+
const { data } = await github.rest.repos.getContent({
50+
owner,
51+
repo,
52+
path: '.github/APPROVED_CONTRIBUTORS',
53+
ref: context.payload.repository.default_branch,
54+
});
55+
if (Array.isArray(data) || data.type !== 'file') return new Set();
56+
return parseAllowlist(
57+
Buffer.from(data.content, data.encoding || 'base64').toString('utf8')
58+
);
59+
} catch (error) {
60+
if (error.status === 404) return new Set();
61+
throw error;
62+
}
63+
}
64+
65+
const allowlist = await readAllowlist();
66+
const login = issue.user.login.toLowerCase();
67+
if (
68+
allowlist.has(`all:${login}`) ||
69+
allowlist.has(`issue:${login}`)
70+
) {
71+
return;
72+
}
73+
74+
const gateMessage = enforceGate
75+
? 'This repository currently uses a maintainer-managed contribution gate, so issues from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` are closed automatically.'
76+
: 'This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this issue is staying open. When enforcement is enabled, issues from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` will be closed automatically.';
77+
78+
await github.rest.issues.createComment({
79+
owner,
80+
repo,
81+
issue_number: issue.number,
82+
body: [
83+
`Thanks @${issue.user.login} for the report.`,
84+
'',
85+
gateMessage,
86+
'',
87+
'Please read `CONTRIBUTING.md` for the expected issue shape. A maintainer can grant issue access by commenting `/lgtmi` on an issue.',
88+
].join('\n'),
89+
});
90+
91+
if (!enforceGate) return;
92+
93+
await github.rest.issues.update({
94+
owner,
95+
repo,
96+
issue_number: issue.number,
97+
state: 'closed',
98+
state_reason: 'not_planned',
99+
});

0 commit comments

Comments
 (0)