mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
ci: cut PR CI to the fast required set; welcome contributor PRs [skip ci]
Founder direction (2026-11-07): remove CI/CD steps that cost time without need, use CI conservatively, and make contributor rules standard and welcoming instead of closing or blocking PRs over CI. A read-only audit of two weeks of run history (Sonnet workflow with an adversarial release- engineer critique) drove the list; nothing release-critical is touched. Removed (enforced nothing, duplicated other checks, or gated contributors): - dco.yml (always exited 0), pr-issue-link.yml (red X on ~8% of contributor PRs), auto-close-harvested.yml, pr-gate.yml / issue-gate.yml / approve-contributor.yml and .github/APPROVED_CONTRIBUTORS (dry-run allowlist gate; drops a pull_request_target surface), agent-task-labels.yml (duplicates triage.yml). Moved to manual (workflow_dispatch) only: - codewhale-review.yml (review bot; AGENTS.md says review bots stay off), main-landing.yml (push trigger; run scripts/preflight.sh --check locally), ci.yml test (Linux suite, 54-94 min per PR), safety-gate (18-29 min), msrv, mobile-smoke, npm-wrapper-smoke (macOS leg dropped from the dispatch matrix; the release artifacts cover macOS). - The PR feature release-note receipt step is advisory (continue-on-error). - spam-lockdown keeps only unambiguous spam patterns. Unchanged: Lint and the compile-only Test (windows-latest) stay the required PR checks; release-candidate, release-artifacts, release-parity, release, the mirrors, OHOS and the Windows import guard are untouched. The branch ruleset drops "npm wrapper smoke (ubuntu-latest)" next (founder approved "Lint + 1 compile"). Rules: AGENTS.md, CONTRIBUTING.md, the PR template, AGENT_ETHOS.md and the cw-land skill now say contributor PRs are never closed or blocked because CI is slow, flaky or red for a reason they did not cause; nothing closes a PR automatically; harvesting is a rare, asked-first fallback with the contributor as author; the PR issue line is optional; maintainers reply generously. web/data/agent-docs.generated.json is regenerated from a clean export of main plus this change only. Verified: actionlint with CI's flags (-ignore SC2129/SC2221/SC2222) over all workflows passes; web/scripts/check-facts.mjs passes on the isolated export; only the contribution entry of agent-docs.generated.json changed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
92c268959e
commit
99cb5a0f4f
18 files changed
+96
-1054
No files matched your search
@@ -1,77 +0,0 @@
|
||||
# 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
|
||||
all:hmbown
|
||||
all:reidliu41
|
||||
all:ousamabenyounes
|
||||
all:ljm3790865
|
||||
all:HUQIANTAO
|
||||
all:xyuai
|
||||
all:merchloubna70-dot
|
||||
all:h3c-hexin
|
||||
all:axobase001
|
||||
all:donglovejava
|
||||
all:Oliver-ZPLiu
|
||||
all:idling11
|
||||
all:angziii
|
||||
all:aboimpinto
|
||||
all:encyc
|
||||
all:Duducoco
|
||||
all:cyq1017
|
||||
all:zlh124
|
||||
all:THINKER-ONLY
|
||||
all:nightt5879
|
||||
all:Liu-Vince
|
||||
all:JiarenWang
|
||||
all:wdw8276
|
||||
all:pengyou200902
|
||||
all:linzhiqin2003
|
||||
all:LING71671
|
||||
all:JasonOA888
|
||||
all:Inference1
|
||||
all:hongqitai
|
||||
all:gordonlu
|
||||
all:gaord
|
||||
all:zhuangbiaowei
|
||||
all:yuanchenglu
|
||||
all:Vishnu1837
|
||||
all:sximelon
|
||||
all:Sskift
|
||||
all:New2Niu
|
||||
all:shenjackyuanjie
|
||||
all:AdityaVG13
|
||||
all:mvanhorn
|
||||
all:MengZ-super
|
||||
all:membphis
|
||||
all:LeoAlex0
|
||||
all:Lee-take
|
||||
all:lbcheng888
|
||||
all:Implementist
|
||||
all:jrcjrcc
|
||||
all:yusufgurdogan
|
||||
all:kunpeng-ai-lab
|
||||
all:elowen53
|
||||
all:CrepuscularIRIS
|
||||
all:chnjames
|
||||
all:ChaceLyee2101
|
||||
all:AresNing
|
||||
pr:dmitri-0
|
||||
pr:sparkofspike
|
||||
pr:angel-hair
|
||||
pr:wuisabel-gif
|
||||
all:whp233
|
||||
all:musichen
|
||||
all:M-Maciej
|
||||
all:serephus
|
||||
all:Pinvou
|
||||
all:SparkofSpike
|
||||
all:goransh-walia
|
||||
all:xiechimon
|
||||
all:yrk111222
|
||||
@@ -4,22 +4,17 @@
|
||||
|
||||
<!-- What does this change, and why? A few sentences is enough. -->
|
||||
|
||||
## Issue
|
||||
## Related issue (optional)
|
||||
|
||||
<!-- CI needs one line here. Replace N with a number:
|
||||
- Closes #N this PR finishes the issue (Fixes / Resolves also work)
|
||||
- Refs #N related or partial work; the issue stays open
|
||||
- No-Issue: <one-line reason> for typos, chores, dependency bumps
|
||||
Only write a closing word when you mean it. GitHub closes the issue even in
|
||||
"this does not close ..." and CI rejects that wording.
|
||||
Put issue numbers here rather than in `feat:` commit messages: CI requires a
|
||||
`feat:` commit that mentions #N to also add #N to CHANGELOG.md. -->
|
||||
<!-- Closes #N if this finishes it, Refs #N if related. Skip it if there is none.
|
||||
Please do not write a closing word inside a negation ("this does not close #N"):
|
||||
GitHub closes the issue anyway. -->
|
||||
|
||||
## How I tested it
|
||||
|
||||
<!-- Commands you ran and what they showed. Test what you changed, for example
|
||||
`scripts/dev-test.sh tui <filter>`; the PR checks run fmt, clippy, the Linux
|
||||
suite and a Windows build for you (CONTRIBUTING.md lists them).
|
||||
`scripts/dev-test.sh tui <filter>`. A few quick checks run on your PR; if one
|
||||
fails for a reason unrelated to your change, a maintainer will deal with it.
|
||||
For visible UI changes, add a screenshot or recording. -->
|
||||
|
||||
## Checklist
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
name: Agent task labels
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
labels:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Add area hints to agent tasks
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
if (!issue) return;
|
||||
|
||||
const labels = new Set((issue.labels || []).map(label => label.name));
|
||||
if (!labels.has('agent-ready')) {
|
||||
core.info(`#${issue.number} is not an agent task; leaving it unchanged.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Milestones are the canonical release roadmap and are assigned
|
||||
// explicitly during triage. Never infer one from a title or label:
|
||||
// old version labels must remain harmless historical metadata.
|
||||
const title = (issue.title || '').toLowerCase();
|
||||
const body = (issue.body || '').toLowerCase();
|
||||
const text = `${title}\n${body}`;
|
||||
|
||||
if (/\bworkflow\b|whaleflow|workflow-runtime/.test(text)) labels.add('workflow-runtime');
|
||||
// `tui` means "touches the terminal UI layer", not "mentions the TUI
|
||||
// in passing". A bare \btui\b over-fired on every issue that merely
|
||||
// referenced the UI — the v0.9.2 localization issues all describe TUI
|
||||
// locale packs in prose and were labelled on that alone.
|
||||
//
|
||||
// Anchor on signals that survive the single-binary refactor (#4747):
|
||||
// the crate path and the rendering framework stay put when
|
||||
// codewhale-tui is lib-ized, whereas the `codewhale-tui` *binary*
|
||||
// name goes away. Do not reintroduce a bare word match here.
|
||||
if (/crates\/tui\b|ratatui|\bcodewhale-tui\b/.test(text)) labels.add('tui');
|
||||
if (/fleet|subagent|sub-agent/.test(text)) labels.add('subagents');
|
||||
if (/catalog|openrouter|model.?picker|provider.?lake/.test(text)) labels.add('reliability');
|
||||
|
||||
const existing = await github.paginate(github.rest.issues.listLabelsForRepo, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100,
|
||||
});
|
||||
const existingNames = new Set(existing.map(label => label.name));
|
||||
const currentNames = new Set((issue.labels || []).map(label => label.name));
|
||||
const toAdd = [...labels].filter(
|
||||
name => existingNames.has(name) && !currentNames.has(name)
|
||||
);
|
||||
if (toAdd.length === 0) return;
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: toAdd,
|
||||
});
|
||||
@@ -1,216 +0,0 @@
|
||||
name: Approve gated contributor
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
approve:
|
||||
# Cheap pre-filter before any token is minted: this workflow fires on
|
||||
# every issue comment, but only a maintainer's lgtm/lgtmi command does
|
||||
# anything. Expression string comparisons are case-insensitive; the
|
||||
# script below still performs the exact trimmed-command match.
|
||||
if: >-
|
||||
contains(github.event.comment.body, 'lgtm')
|
||||
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
# Job-level so skipped runs never enter the group: a workflow-level group
|
||||
# let any unrelated comment queue and cancel a pending approval run.
|
||||
concurrency:
|
||||
group: contribution-gate-approval
|
||||
cancel-in-progress: false
|
||||
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);
|
||||
|
||||
// Answer the maintainer's command with a reaction, never a comment
|
||||
// (founder, 2026-09-22). The run log carries the detail, and the
|
||||
// allowlist PR body links back here, so the thread still shows it.
|
||||
async function react(content, message) {
|
||||
core.notice(message);
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
if (!scope) return;
|
||||
if (!privileged.has(comment.author_association)) return;
|
||||
if (scope === 'pr' && !issue.pull_request) {
|
||||
await react('confused', '`/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 react('confused', '`/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 react('eyes', `@${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 react('eyes', `@${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 react('rocket', `Created allowlist update PR: ${pr.html_url}`);
|
||||
@@ -1,154 +0,0 @@
|
||||
name: Auto-close harvested PRs
|
||||
|
||||
# When a commit on main contains a "Harvested from PR #N" line in its
|
||||
# message, close PR #N with a templated thank-you that links back to
|
||||
# the merged commit. Solves the long-standing problem where contributor
|
||||
# PRs whose code lands via maintainer cherry-pick stay open and
|
||||
# `CONFLICTING` forever, even though their fix is credited in the
|
||||
# CHANGELOG.
|
||||
#
|
||||
# The expected commit-message convention is documented in
|
||||
# CONTRIBUTING.md. Two patterns are recognised:
|
||||
#
|
||||
# * `Harvested from PR #1234 by @username` (preferred)
|
||||
# * `harvested from #1234` (case-insensitive fallback)
|
||||
#
|
||||
# The first match's PR number is closed; multiple PRs can be closed
|
||||
# per commit by repeating the line. The match runs on the commit
|
||||
# body only, not on the subject line, so the subject can describe
|
||||
# the change naturally without baking a number into it.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
# Only one auto-close run at a time so two near-simultaneous main
|
||||
# pushes can't both try to close the same PR (the second would just
|
||||
# fail with "Pull request is already closed", harmless but noisy).
|
||||
concurrency:
|
||||
group: auto-close-harvested
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
close:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# We need at least the commits that this push introduced.
|
||||
# fetch-depth: 0 is the simplest correct option; the
|
||||
# alternative (fetching just `before..after`) is fragile
|
||||
# when force-pushes happen.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Close PRs referenced by harvested-from lines
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BEFORE_SHA: ${{ github.event.before }}
|
||||
AFTER_SHA: ${{ github.event.after }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# The first push to a fresh branch has BEFORE_SHA = 0000…0000.
|
||||
# In that case fall back to the latest commit only — we don't
|
||||
# want to scan the entire history.
|
||||
if [[ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" || -z "${BEFORE_SHA:-}" ]]; then
|
||||
RANGE="${AFTER_SHA}"
|
||||
RANGE_ARGS=("-1" "${AFTER_SHA}")
|
||||
else
|
||||
RANGE="${BEFORE_SHA}..${AFTER_SHA}"
|
||||
RANGE_ARGS=("${RANGE}")
|
||||
fi
|
||||
echo "Scanning commit range: ${RANGE}"
|
||||
|
||||
# `git log --format=%H%n%B%n--END--` separates commits with a
|
||||
# sentinel so multi-line bodies don't get mangled.
|
||||
mapfile -t commits < <(git log "${RANGE_ARGS[@]}" --format="%H")
|
||||
|
||||
if [[ ${#commits[@]} -eq 0 ]]; then
|
||||
echo "No commits in range; nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
declare -A processed_prs=()
|
||||
|
||||
for sha in "${commits[@]}"; do
|
||||
body="$(git log -1 --format=%B "${sha}")"
|
||||
# Two patterns, both case-insensitive on the keyword:
|
||||
# "Harvested from PR #1234 by @username" (preferred form)
|
||||
# "harvested from #1234" (short form)
|
||||
mapfile -t pr_numbers < <(
|
||||
printf '%s\n' "${body}" \
|
||||
| grep -oiE 'harvested from (pr )?#[0-9]+' \
|
||||
| grep -oE '#[0-9]+' \
|
||||
| tr -d '#' \
|
||||
| sort -u || true
|
||||
)
|
||||
|
||||
if [[ ${#pr_numbers[@]} -eq 0 ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
short_sha="${sha:0:12}"
|
||||
subject="$(git log -1 --format=%s "${sha}")"
|
||||
|
||||
for pr in "${pr_numbers[@]}"; do
|
||||
key="${pr}-${sha}"
|
||||
if [[ -n "${processed_prs[${key}]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
processed_prs[${key}]=1
|
||||
|
||||
# Idempotency: skip if the PR is already closed.
|
||||
state="$(gh pr view "${pr}" --json state --jq .state 2>/dev/null || echo "MISSING")"
|
||||
if [[ "${state}" == "CLOSED" || "${state}" == "MERGED" ]]; then
|
||||
echo "PR #${pr} is already ${state}; skipping."
|
||||
continue
|
||||
fi
|
||||
if [[ "${state}" == "MISSING" ]]; then
|
||||
echo "::warning::PR #${pr} not found or inaccessible; skipping."
|
||||
continue
|
||||
fi
|
||||
|
||||
author="$(gh pr view "${pr}" --json author --jq '.author.login' 2>/dev/null || echo "")"
|
||||
greeting="Hi"
|
||||
if [[ -n "${author}" ]]; then
|
||||
greeting="Thanks @${author}"
|
||||
fi
|
||||
|
||||
# NOTE: this block intentionally avoids `<<EOF` heredocs.
|
||||
# YAML's `|` block scalar requires consistent indentation,
|
||||
# but heredoc bodies have to start at column 0 — those two
|
||||
# constraints can't coexist in the same file. We assemble
|
||||
# the body with `printf` + `\n` so every line of the
|
||||
# message lives at the same indent as the surrounding
|
||||
# shell code.
|
||||
commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${sha}"
|
||||
contributing_url="https://github.com/${GITHUB_REPOSITORY}/blob/main/CONTRIBUTING.md"
|
||||
body_text="$(printf '%s\n' \
|
||||
"${greeting} — your contribution landed in [\`${short_sha}\`](${commit_url}) on \`main\`:" \
|
||||
"" \
|
||||
"> ${subject}" \
|
||||
"" \
|
||||
"Closing this PR now that the code is on \`main\`. Credit lives in the commit message and (where applicable) the \`CHANGELOG.md\` entry for the next release. Apologies for not closing this at the time of the merge — the auto-close workflow is new in v0.8.31." \
|
||||
"" \
|
||||
"If you want to land more work and would prefer your future PRs merge cleanly without a harvest step, the [\`CONTRIBUTING.md\`](${contributing_url}) doc has a short note on what makes a contribution mergeable as-is." \
|
||||
)"
|
||||
|
||||
echo "Closing PR #${pr} (harvested in ${short_sha})"
|
||||
if ! gh pr close "${pr}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--comment "${body_text}"; then
|
||||
echo "::warning::Failed to close PR #${pr}; continuing"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "Auto-close pass complete."
|
||||
+10
-15
@@ -215,12 +215,11 @@ jobs:
|
||||
# missing receipt actually matters.
|
||||
run: ./scripts/release/check-versions.sh --range-audit-advisory
|
||||
- name: Check this PR's feature release-note receipts
|
||||
# The range audit above is advisory because previous-tag..HEAD blames
|
||||
# every open PR for receipts other merges forgot. This is the same
|
||||
# check scoped to the PR's own commits, so it blocks: a `feat:` commit
|
||||
# that references #N must add #N to CHANGELOG.md in the same PR.
|
||||
# Advisory, like the range audit above: a missing CHANGELOG receipt
|
||||
# shows as a warning and maintainers add it while landing.
|
||||
# Locally: scripts/preflight.sh.
|
||||
if: github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
env:
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: ./scripts/release/check-feature-release-notes.sh "${PR_BASE_SHA}" HEAD
|
||||
@@ -422,7 +421,7 @@ jobs:
|
||||
safety-gate:
|
||||
name: Safety gate
|
||||
needs: changes
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -726,7 +725,7 @@ jobs:
|
||||
# fail the build there while stable stays green (#6543).
|
||||
name: MSRV check (1.89)
|
||||
needs: changes
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -775,6 +774,7 @@ jobs:
|
||||
test:
|
||||
name: Test
|
||||
needs: changes
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
# Contexts "Test (ubuntu-latest)" / "Test (macos-latest)" /
|
||||
# "Test (windows-latest)" derive from job name + matrix.os and are
|
||||
# independent of runs-on. For light changes the macOS/Windows legs only
|
||||
@@ -1282,7 +1282,7 @@ jobs:
|
||||
npm-wrapper-smoke:
|
||||
name: npm wrapper smoke
|
||||
needs: changes
|
||||
if: github.event_name != 'schedule'
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
# Same ternary rationale as the Test job: light legs only echo, so keep
|
||||
# them off macOS/Windows runners. On pull_request the matrix is
|
||||
# ubuntu-only, so the required "npm wrapper smoke (ubuntu-latest)"
|
||||
@@ -1298,7 +1298,7 @@ jobs:
|
||||
runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || github.event_name == 'workflow_dispatch' && '["ubuntu-latest","macos-latest","windows-latest"]' || '["ubuntu-latest","windows-latest"]') }}
|
||||
os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || github.event_name == 'workflow_dispatch' && '["ubuntu-latest","windows-latest"]' || '["ubuntu-latest","windows-latest"]') }}
|
||||
steps:
|
||||
- name: Skip npm wrapper smoke for light change
|
||||
if: needs.changes.outputs.heavy != 'true'
|
||||
@@ -1358,13 +1358,8 @@ jobs:
|
||||
mobile-smoke:
|
||||
name: Mobile runtime smoke
|
||||
needs: changes
|
||||
# Not a required PR context. Pull requests run it only when the mobile
|
||||
# runtime surface changed (see the `mobile` filter above); every push to
|
||||
# main runs it unconditionally as the pre-release safety net.
|
||||
if: >-
|
||||
github.event_name != 'schedule' &&
|
||||
needs.changes.outputs.heavy == 'true' &&
|
||||
(github.event_name != 'pull_request' || needs.changes.outputs.mobile == 'true')
|
||||
# Manual (workflow_dispatch) only; the release candidate builds android-arm64.
|
||||
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
name: Codewhale PR Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr-number:
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
name: DCO
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dco:
|
||||
name: Check Signed-off-by
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Report missing Signed-off-by (dry-run advisory)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
base_rev=$(git merge-base origin/${{ github.base_ref }} HEAD 2>/dev/null || echo "HEAD~1")
|
||||
echo "✅ merge-base: $base_rev"
|
||||
echo ""
|
||||
|
||||
bad=()
|
||||
while IFS= read -r commit; do
|
||||
msg=$(git log --format=%B -n1 "$commit")
|
||||
if ! echo "$msg" | grep -qi "^Signed-off-by:"; then
|
||||
bad+=("$commit")
|
||||
fi
|
||||
done < <(git rev-list "${base_rev}..HEAD")
|
||||
|
||||
if [ ${#bad[@]} -eq 0 ]; then
|
||||
echo "✅ All commits have Signed-off-by."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "⚠️ Advisory — the following commit(s) are missing Signed-off-by:"
|
||||
for c in "${bad[@]}"; do
|
||||
echo " • $c $(git log --format=%s -n1 "$c")"
|
||||
done
|
||||
echo ""
|
||||
echo "This check is advisory and does not block merging."
|
||||
echo "Please amend commits with:"
|
||||
echo " git commit --amend -s"
|
||||
echo "See CONTRIBUTING.md for details."
|
||||
echo ""
|
||||
echo "::warning title=DCO: missing Signed-off-by::Some commits are missing Signed-off-by — amend with git commit --amend -s"
|
||||
|
||||
# Dry-run: always pass, but surface the warning
|
||||
exit 0
|
||||
@@ -1,90 +0,0 @@
|
||||
name: Contribution intake - issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
# Labels only, never comments (founder, 2026-09-22): the intake note used
|
||||
# to thank other bots, and a comment is noise a label does not make.
|
||||
# Maintainers still see who needs triage; `/lgtmi` still skips it.
|
||||
- name: Label new external issues for triage
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||
|
||||
async function label(name, color, description) {
|
||||
try {
|
||||
await github.rest.issues.createLabel({ owner, repo, name, color, description });
|
||||
} catch (error) {
|
||||
if (error.status !== 422) throw error; // 422: label already exists
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
labels: [name],
|
||||
});
|
||||
}
|
||||
|
||||
if (privileged.has(issue.author_association)) return;
|
||||
// `user.type` is set by GitHub for app and bot accounts and cannot
|
||||
// be spoofed by a login that merely ends in "[bot]".
|
||||
if (issue.user.type === 'Bot') {
|
||||
await label('bot-authored', 'ededed', 'Opened by a bot or app account');
|
||||
return;
|
||||
}
|
||||
|
||||
function parseAllowlist(content) {
|
||||
return new Set(
|
||||
content
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
async function readAllowlist() {
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: '.github/APPROVED_CONTRIBUTORS',
|
||||
ref: context.payload.repository.default_branch,
|
||||
});
|
||||
if (Array.isArray(data) || data.type !== 'file') return new Set();
|
||||
return parseAllowlist(
|
||||
Buffer.from(data.content, data.encoding || 'base64').toString('utf8')
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.status === 404) return new Set();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const allowlist = await readAllowlist();
|
||||
const login = issue.user.login.toLowerCase();
|
||||
if (
|
||||
allowlist.has(`all:${login}`) ||
|
||||
allowlist.has(`issue:${login}`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await label(
|
||||
'needs-triage',
|
||||
'fbca04',
|
||||
'New external report awaiting maintainer triage; repro, logs and version output help'
|
||||
);
|
||||
@@ -22,11 +22,8 @@ name: Main landing gate
|
||||
# catch a red landing in minutes, not to re-run the release gate on every push.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# Also manually runnable: GitHub does not evaluate a workflow on the push that
|
||||
# introduces it, so the first run of this file would otherwise wait for the
|
||||
# next landing. A maintainer can also re-run the gate after an infra outage.
|
||||
# Manual only: run it after a landing you want checked or after an infra
|
||||
# outage. Before pushing, run scripts/preflight.sh --check locally.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
name: Contribution gate - pull requests
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
# Keep new gates observable first. Switch to "enforce" only after maintainers
|
||||
# have seeded active contributors and reviewed the dry-run signal.
|
||||
CONTRIBUTION_GATE_MODE: dry-run
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Gate unapproved external pull requests
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||
const gateMode = (process.env.CONTRIBUTION_GATE_MODE || 'dry-run').trim().toLowerCase();
|
||||
const enforceGate = gateMode === 'enforce';
|
||||
|
||||
if (!['dry-run', 'enforce'].includes(gateMode)) {
|
||||
core.warning(`Unknown CONTRIBUTION_GATE_MODE "${gateMode}"; defaulting to dry-run.`);
|
||||
}
|
||||
|
||||
async function label(name, color, description) {
|
||||
try {
|
||||
await github.rest.issues.createLabel({ owner, repo, name, color, description });
|
||||
} catch (error) {
|
||||
if (error.status !== 422) throw error; // 422: label already exists
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
labels: [name],
|
||||
});
|
||||
}
|
||||
|
||||
if (privileged.has(pr.author_association)) return;
|
||||
// `user.type` is set by GitHub for app and bot accounts and cannot
|
||||
// be spoofed by a login that merely ends in "[bot]".
|
||||
if (pr.user.type === 'Bot') {
|
||||
await label('bot-authored', 'ededed', 'Opened by a bot or app account');
|
||||
return;
|
||||
}
|
||||
|
||||
function parseAllowlist(content) {
|
||||
return new Set(
|
||||
content
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
async function readAllowlist() {
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: '.github/APPROVED_CONTRIBUTORS',
|
||||
ref: context.payload.repository.default_branch,
|
||||
});
|
||||
if (Array.isArray(data) || data.type !== 'file') return new Set();
|
||||
return parseAllowlist(
|
||||
Buffer.from(data.content, data.encoding || 'base64').toString('utf8')
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.status === 404) return new Set();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const allowlist = await readAllowlist();
|
||||
const login = pr.user.login.toLowerCase();
|
||||
if (
|
||||
allowlist.has(`all:${login}`) ||
|
||||
allowlist.has(`pr:${login}`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Labels only (founder, 2026-09-22). The label description carries
|
||||
// the contributor-facing explanation a comment used to.
|
||||
await label(
|
||||
'contribution-gate',
|
||||
'c5def5',
|
||||
'Author not yet in .github/APPROVED_CONTRIBUTORS; a maintainer grants access with /lgtm'
|
||||
);
|
||||
|
||||
if (!enforceGate) return;
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
state: 'closed',
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
name: PR closes an issue
|
||||
|
||||
# 342 open issues, 329 of them touched within the month: nothing here is rotting,
|
||||
# the drain is just clogged. Only 8 of 35 open PRs carried a closing keyword, so
|
||||
# work ships and its issue stays open, and nobody can tell which of the 342 are
|
||||
# already done. That is how 121 issues end up on one milestone.
|
||||
#
|
||||
# This check asks every PR to close an issue, reference one with `Refs #N`, or
|
||||
# say why it has none (`No-Issue:`). The opt-out is one line, so this is a
|
||||
# prompt, not a wall. It also fails a negated closing keyword ("does not close
|
||||
# #N"), which GitHub would otherwise act on and close the issue.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
link:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# Automated dependency bumps (dependabot and any other GitHub-verified
|
||||
# bot account) are machine-generated and can never carry a closing
|
||||
# keyword; failing them here would require hand-editing every bot body,
|
||||
# which defeats the automation. The gate stays strict for every human
|
||||
# PR. `user.type` is set by GitHub for verified bot accounts, so a PR
|
||||
# author cannot spoof it to dodge the check.
|
||||
- name: Require an issue link or an explicit opt-out
|
||||
if: github.event.pull_request.user.type != 'Bot'
|
||||
env:
|
||||
# Fetched live rather than read from the event payload. A rerun
|
||||
# replays the payload the run started with, so a body-only fix could
|
||||
# never turn this check green: the obvious operator move — add the
|
||||
# missing line, rerun the failed check — re-read the old body and
|
||||
# failed again with no hint why. Reading the current body makes a
|
||||
# rerun mean what everyone already assumes it means.
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Through a variable, never interpolated into the script body:
|
||||
# a PR body is attacker-controlled text.
|
||||
PR_BODY=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json body --jq '.body // ""')
|
||||
# Body only, deliberately. GitHub resolves closing keywords from the
|
||||
# PR description; a "Closes #123" in the title auto-closes nothing.
|
||||
# Accepting the title here would pass PRs that never close an issue,
|
||||
# which is the exact false-assurance this check exists to prevent.
|
||||
text="${PR_BODY:-}"
|
||||
|
||||
# GitHub resolves a closing keyword wherever it appears, negated or
|
||||
# not: PR #6371 said "does not close #6184", GitHub put #6184 in
|
||||
# closingIssuesReferences, and the P1 closed on merge. A keyword a
|
||||
# few words after a negation is always that mistake, so fail first.
|
||||
# The reference takes all three forms GitHub closes on: #N,
|
||||
# owner/repo#N and a full issue URL.
|
||||
keyword='(close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]*:?[[:space:]]*([[:alnum:]_.-]+/[[:alnum:]_.-]+#|#|https?://github\.com/[[:alnum:]_.-]+/[[:alnum:]_.-]+/issues/)[0-9]+'
|
||||
negation="(\\b(not|never|no longer|without)|n't)([[:space:]]+[[:alnum:]'-]+){0,3}[[:space:]]+"
|
||||
# `|| true`, not `| head`: under pipefail a SIGPIPE'd grep would turn a
|
||||
# hit into a miss.
|
||||
negated=$(grep -m1 -oiE "${negation}${keyword}" <<<"$text" || true)
|
||||
if [ -n "$negated" ]; then
|
||||
cat >&2 <<MSG
|
||||
Negated closing keyword found: "${negated}"
|
||||
|
||||
GitHub closes the issue anyway: it ignores the "not". Reword it as
|
||||
|
||||
Refs #1234 (the PR relates to the issue; nothing closes)
|
||||
|
||||
and keep Closes / Fixes / Resolves for the issue this PR really finishes.
|
||||
MSG
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# GitHub's own closing-keyword set, plus the #N it must attach to.
|
||||
if grep -qiE "\\b${keyword}" <<<"$text"; then
|
||||
echo "Closing keyword found — this PR will close its issue on merge."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Opt-outs. `Refs #N` links the issue without closing it; `No-Issue:`
|
||||
# takes a one-line reason for work that has no issue at all.
|
||||
if grep -qiE '^[[:space:]]*Refs?:?[[:space:]]+#[0-9]+' <<<"$text"; then
|
||||
echo "Linked without closing — $(grep -iE '^[[:space:]]*Refs?:?[[:space:]]+#[0-9]+' <<<"$text" | head -1)"
|
||||
exit 0
|
||||
fi
|
||||
if grep -qiE '^[[:space:]]*No-Issue:[[:space:]]*\S' <<<"$text"; then
|
||||
reason=$(grep -iE '^[[:space:]]*No-Issue:' <<<"$text" | head -1)
|
||||
echo "Opted out — ${reason}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat >&2 <<'MSG'
|
||||
This PR neither links an issue nor says why it doesn't.
|
||||
|
||||
Add one of these lines to the PR body:
|
||||
|
||||
Closes #1234 (only when this PR finishes the issue; Fixes / Resolves also close)
|
||||
Refs #1234 (related or partial work; the issue stays open)
|
||||
No-Issue: <one-line why> (chores, docs typos, revert, dependency bump)
|
||||
|
||||
Write a closing keyword only when you mean it: GitHub closes the issue
|
||||
on merge even inside "does not close #1234".
|
||||
MSG
|
||||
exit 1
|
||||
@@ -27,11 +27,6 @@ jobs:
|
||||
|
||||
const blob = `${issue.title || ''}\n${issue.body || ''}`;
|
||||
const patterns = [
|
||||
/\bcrypto\b/i,
|
||||
/\bairdrop\b/i,
|
||||
/\bnft\b/i,
|
||||
/\bpresale\b/i,
|
||||
/\busdt\b/i,
|
||||
/\btg\s*@/i,
|
||||
/\btelegram\s+@/i,
|
||||
/\bt\.me\//i,
|
||||
@@ -40,8 +35,6 @@ jobs:
|
||||
/\bguest\s+post/i,
|
||||
/\bbacklink/i,
|
||||
/\bbuy\s+followers/i,
|
||||
/\bjoin\s+our\s+(community|server|group)/i,
|
||||
/\bpromot[ei]\s+your\b/i,
|
||||
];
|
||||
const hit = patterns.find(p => p.test(blob));
|
||||
if (!hit) return;
|
||||
|
||||
@@ -77,11 +77,11 @@ base prompt". Two more corollaries earned here:
|
||||
- Commit by explicit path; never `git add -A`.
|
||||
- Work that fails its check and cannot be fixed now is not pushed to `main`.
|
||||
Record it and report it instead.
|
||||
- **Maintainers do not open PR branches** (founder, 2026-10-08: more PRs means
|
||||
more CI and slower work). Verified slices land directly on `main` and are
|
||||
pushed: run the focused gate, put the real counts in the commit message,
|
||||
fetch and rebase onto `origin/main` and retry on rejection. Never force-push
|
||||
or rewrite a shared ref. Contributor PRs still merge as PRs.
|
||||
- **Maintainers land small verified slices directly on `main`.** Run the
|
||||
focused local check (`scripts/preflight.sh --check`), put the real counts in
|
||||
the commit message, fetch and rebase onto `origin/main` and retry on
|
||||
rejection. Never force-push or rewrite a shared ref. Contributor PRs are
|
||||
always welcome and merge as themselves (founder, 2026-11-07).
|
||||
- When the task is local-only, stay fully offline: no browsing, GitHub or remote
|
||||
Git operations, downloads, dependency installation, provider calls, or
|
||||
source/diff transmission. Record the missing external receipt and keep working
|
||||
@@ -102,15 +102,14 @@ base prompt". Two more corollaries earned here:
|
||||
- **Write down what a design does not do**, beside the behaviour it owns — a
|
||||
short known-limitations note in the owning module. A stated limit stops the
|
||||
next reader from assuming a capability that was never built.
|
||||
- **Substantive contributor replies are authorized for the 0.10.2 release**
|
||||
(founder, 2026-10-13). Thank people specifically for their reports, diagnosis,
|
||||
fixes and testing; link the carrying commit or PR and distinguish candidate
|
||||
work from a shipped release. Status, answers and useful questions are welcome;
|
||||
repetitive progress posts and bot noise are not. Evidence stays in commits
|
||||
and PR bodies; ownership stays in Linear. Outside this release, the previous
|
||||
no-comment rule remains, except a one-sentence reason when closing or
|
||||
superseding a human contribution. Review workflows remain disabled unless
|
||||
the founder separately authorizes enabling them.
|
||||
- **Be generous in public** (founder, 2026-11-07). Thank people specifically
|
||||
for their reports, diagnosis, fixes and testing; link the carrying commit or
|
||||
PR and distinguish candidate work from a shipped release. Status, answers
|
||||
and useful questions are welcome; repetitive progress posts and bot noise
|
||||
are not. Evidence stays in commits and PR bodies; ownership stays in Linear.
|
||||
A contributor PR is never closed because CI is slow, flaky or red for a
|
||||
reason the PR did not cause; when a PR is closed, say why in one specific,
|
||||
thankful sentence. Review bots stay off unless the founder enables them.
|
||||
- **A user feature lands with its registry row.** A new command, `[features]`
|
||||
flag, provider or user-visible feature adds or updates its row in
|
||||
`docs/features.toml` in the same change;
|
||||
@@ -123,17 +122,18 @@ base prompt". Two more corollaries earned here:
|
||||
An external contributor's branch goes stale because *we* land things, not
|
||||
because they did anything wrong. Treat their time as more expensive than ours.
|
||||
|
||||
**The goal is the contributor's PR merging as itself.** Review it, help it
|
||||
rebase, or fix it on their branch — that is the default path. Closing their PR
|
||||
and re-landing the work as our own commit (`auto-close-harvested`) is the
|
||||
fallback for a branch that truly cannot merge in reasonable time; done
|
||||
casually it reads as taking the work even when credit is preserved.
|
||||
**The goal is the contributor's PR merging as itself.** Review it, run or
|
||||
rerun only the checks it needs, fix small problems on their branch, and resolve
|
||||
conflicts that `main` caused. Landing their commits as ours (harvesting) is the
|
||||
rare fallback for a branch we cannot push to and that cannot merge in
|
||||
reasonable time; ask on the PR first, because done casually it reads as taking
|
||||
the work even when credit is preserved.
|
||||
|
||||
- **Never make a contributor rebase around our churn.** If their PR conflicts
|
||||
only because main moved, a maintainer resolves it.
|
||||
- Landing mechanics — merge-base diffing, mid-function conflicts, fork-push
|
||||
refusal, the contribution gate — live in `cw-land`. Follow them instead of
|
||||
improvising.
|
||||
refusal, runs awaiting first-contributor approval — live in `cw-land`.
|
||||
Follow them instead of improvising.
|
||||
- **Preserve credit in the mechanical sense, not just the polite one.** Commit
|
||||
authorship and `Co-authored-by` trailers must use the contributor's own
|
||||
GitHub-linked address. `AUTHOR_MAP` and `.mailmap` are project conventions —
|
||||
@@ -258,8 +258,11 @@ lands as our commit, the contributor is that commit's **author** (`git commit
|
||||
list counts commit authors only: about 200 harvested commits authored by
|
||||
maintainer identities left their contributors off it, which is why
|
||||
`docs/CONTRIBUTORS.md` carries the full wall (`scripts/contributor-wall.py`).
|
||||
`auto-close-harvested.yml` closes their PR with credit. Canonical human
|
||||
identities come from `.github/AUTHOR_MAP`.
|
||||
A maintainer closes their PR by hand with a thank-you naming the commit;
|
||||
nothing closes a PR automatically. Canonical human identities come from
|
||||
`.github/AUTHOR_MAP`.
|
||||
|
||||
Leave unrelated work intact and keep new enforcement dry-run unless explicitly
|
||||
approved.
|
||||
Leave unrelated work intact. No automation closes, blocks or labels a
|
||||
contributor as unapproved, and no workflow checks out or runs untrusted PR code
|
||||
under `pull_request_target`. Keep required PR checks few and fast; exhaustive
|
||||
validation belongs to the release candidate.
|
||||
+33
-82
@@ -229,9 +229,9 @@ Co-author a *person* freely; the address must be their GitHub-linked one
|
||||
|
||||
When a commit harvests code from a community PR (see "How Your Contribution
|
||||
Lands" below), include a `Harvested from PR #N by @author` line in the commit
|
||||
body. An auto-close workflow watches for this pattern and closes the
|
||||
referenced PR with credit so the contributor gets a clear signal that
|
||||
their work shipped.
|
||||
body. A maintainer then closes the referenced PR by hand with a thank-you and
|
||||
a link to the commit, so the contributor gets a clear signal that their work
|
||||
shipped.
|
||||
|
||||
## What happens after you open a pull request
|
||||
|
||||
@@ -242,26 +242,23 @@ maintainer to approve the first run for a brand-new GitHub account.
|
||||
| Check | What it runs | When it is red |
|
||||
| --- | --- | --- |
|
||||
| `Lint` (required) | `cargo fmt --check`, clippy on every target, and the fast drift checks (locales, READMEs, provider registry, dead-code and blocking-call budgets) | Run `cargo fmt --all` and the clippy command in [Pre-push verification](#pre-push-verification); the log names the file and the check |
|
||||
| `Test (ubuntu-latest)` | The whole workspace suite on Linux with `cargo nextest`, plus doctests | Re-run the named test locally, for example `scripts/dev-test.sh tui <test name>` |
|
||||
| `Test (windows-latest)` (required) | Compiles every workspace target for Windows with warnings denied, and the installer PATH helper tests | Usually a `cfg(windows)` path or an import only Windows sees; the log has the compiler error |
|
||||
| `npm wrapper smoke (ubuntu-latest)` (required) | Builds `codewhale` and installs it through the npm wrapper | The log shows the failing install or entrypoint step |
|
||||
| `Safety gate` | The sandbox, authorization and auto-review tests | Treat it as a real regression in a trust boundary |
|
||||
| `Version drift` | Version files, the `feat:` release-note rule, release helper tests and contributor credit | The credit step only reports: a maintainer adds your credit when the PR merges |
|
||||
| `PR closes an issue` | Your description has `Closes #N`, `Refs #N` or `No-Issue: <reason>` | Edit the description, then re-run the check |
|
||||
| `DCO` | Lists commits without `Signed-off-by` | Advisory; it never blocks |
|
||||
| `Contribution gate` | Labels PRs from authors not yet in `.github/APPROVED_CONTRIBUTORS` | A label, not a judgement; see [Contribution Gate](#contribution-gate) |
|
||||
|
||||
Smaller checks cover MSRV, the VS Code extension, integrations, the extension
|
||||
host and plugin conversion, plus workflow lint when you change
|
||||
`.github/workflows`. A documentation-only change skips the Rust builds but
|
||||
still reports the required checks, so it can merge. The Linux test suite took
|
||||
45 minutes on a recent pull request; the other checks run alongside it.
|
||||
Smaller checks cover the VS Code extension, integrations, the extension host
|
||||
and plugin conversion, plus workflow lint when you change `.github/workflows`.
|
||||
A documentation-only change skips the Rust builds but still reports the
|
||||
required checks, so it can merge.
|
||||
|
||||
Your pull request does not run the macOS and Windows test suites, the slow
|
||||
runtime-contract and persistence-backlog measurements, the dependency audits
|
||||
or the OpenHarmony build. Maintainers run those once per release, on the exact
|
||||
commit being released. If that run finds a problem your change introduced, we
|
||||
fix it on `main`; we will not ask you to chase it.
|
||||
Your pull request does not run the test suites, the safety gate, MSRV, the npm
|
||||
wrapper smoke, the slow runtime-contract and persistence-backlog measurements,
|
||||
the dependency audits or the OpenHarmony build. Maintainers run those once per
|
||||
release, on the exact commit being released. If that run finds a problem your
|
||||
change introduced, we fix it on `main`; we will not ask you to chase it.
|
||||
|
||||
A red check that your change did not cause is not your job to fix. We rerun it
|
||||
or fix it on `main`, and we never close a PR because CI is slow or flaky. Your
|
||||
PR may be a draft, and the issue line in its description is optional.
|
||||
|
||||
## How Your Contribution Lands
|
||||
|
||||
@@ -280,9 +277,10 @@ Before merging, a maintainer may:
|
||||
### Fallback — harvest
|
||||
|
||||
Rarely, a branch cannot merge in reasonable time, for example when it is in a
|
||||
fork we cannot push to and the conflicts cannot be resolved there. Then a
|
||||
maintainer may **harvest** the useful commits into `main` instead. This is
|
||||
**not a rejection**: your code landed, and your credit is kept mechanically.
|
||||
fork we cannot push to and the conflicts cannot be resolved there. We will ask
|
||||
on your PR first. If it still cannot merge, a maintainer may **harvest** the
|
||||
useful commits into `main` instead. This is **not a rejection**: your code
|
||||
landed, and your credit is kept mechanically.
|
||||
|
||||
- You stay the author of the harvested commit, with your GitHub-linked address
|
||||
from `.github/AUTHOR_MAP` or
|
||||
@@ -291,8 +289,8 @@ maintainer may **harvest** the useful commits into `main` instead. This is
|
||||
- The commit body includes `Harvested from PR #N by @your-handle`. That line is
|
||||
the contract: it is your credit and the signal that your work shipped.
|
||||
- The `CHANGELOG.md` entry for the next release credits you by handle.
|
||||
- `auto-close-harvested.yml` closes your PR with a thank-you and a link to the
|
||||
commit on `main`.
|
||||
- A maintainer closes your PR by hand with a thank-you and a link to the commit
|
||||
on `main`.
|
||||
|
||||
When a maintainer closes a harvested PR by hand, the closing comment
|
||||
follows this template (the pattern set on PR #2634):
|
||||
@@ -402,50 +400,6 @@ them from the title and form answers.
|
||||
|
||||
Release targeting uses milestones, not version labels.
|
||||
|
||||
## Contribution Gate
|
||||
|
||||
Codewhale uses a maintainer-managed contribution gate for the community front
|
||||
door. Maintainers and collaborators bypass this gate automatically. The gate
|
||||
workflows run in dry-run mode and only add labels, so maintainers can observe
|
||||
the signal before changing contributor flow.
|
||||
|
||||
The maintainer posture is documented in
|
||||
[docs/AGENT_ETHOS.md](docs/AGENT_ETHOS.md): automation should reduce load while
|
||||
keeping good-faith contributors seen, credited, and able to keep helping.
|
||||
|
||||
Issues are never auto-closed by the contribution gate. Unapproved external
|
||||
issues receive a triage label and remain open for maintainers. Codewhale
|
||||
depends on real edge cases from real users, so issue intake should stay warm
|
||||
and open.
|
||||
|
||||
Pull requests are different because they can touch code, CI, release plumbing,
|
||||
auth, sandboxing, provider policy, and other trust-boundary surfaces. The PR
|
||||
gate can be switched from dry-run to enforcement when maintainers decide they
|
||||
need that safety control, but it should be treated as a review-load control,
|
||||
not a judgment on contributor quality. Before enabling PR enforcement, seed the
|
||||
allowlist broadly enough for active external contributors who should not be
|
||||
interrupted by the rollout.
|
||||
|
||||
The allowlist is scoped:
|
||||
|
||||
- `pr:username` allows pull requests.
|
||||
- `issue:username` allows issues.
|
||||
- `all:username` allows both.
|
||||
|
||||
A maintainer can approve someone by commenting `/lgtm` on a pull request for PR
|
||||
access, or `/lgtmi` on an issue for issue access. The exact bare commands
|
||||
`lgtm` and `lgtmi` are also accepted for compatibility, but the prefixed forms
|
||||
are preferred because they are harder to trigger accidentally in ordinary review
|
||||
discussion.
|
||||
|
||||
Approvals do not edit `main` directly. The approval workflow opens a small
|
||||
allowlist update PR so the new entry is reviewable before it takes effect.
|
||||
|
||||
If the PR gate fires on a good contributor incorrectly, use the same approval
|
||||
flow to restore them: comment `/lgtm`, merge the generated allowlist PR, then
|
||||
reopen the affected pull request. If GitHub will not allow the closed PR to be
|
||||
reopened, ask the contributor to resubmit after the allowlist PR is merged.
|
||||
|
||||
## Agent-Assisted Improvements
|
||||
|
||||
Codewhale is allowed to help improve Codewhale, but the contribution still has
|
||||
@@ -469,13 +423,12 @@ branding, or global prompts without prior maintainer sign-off.
|
||||
|
||||
## How maintainers and agents ship
|
||||
|
||||
Maintainers, and the agents working for them, do not open pull requests. They
|
||||
land verified work directly on `main` with the local checks they ran and their
|
||||
counts in the commit message. A push to `main` starts no GitHub build or test
|
||||
workflow: GitHub only mirrors `main` to CNB and GitCode and closes harvested
|
||||
PRs. CNB then runs its own Linux gates on the mirror and reports them on CNB,
|
||||
not on GitHub. Do not write `[skip ci]`; nothing needs skipping, and it would
|
||||
also skip the mirrors and the harvest auto-close.
|
||||
Maintainers, and the agents working for them, land small verified slices
|
||||
directly on `main` with the local checks they ran and their counts in the
|
||||
commit message. A push to `main` starts no GitHub build or test workflow:
|
||||
GitHub only mirrors `main` to CNB and GitCode. CNB then runs its own Linux
|
||||
gates on the mirror and reports them on CNB, not on GitHub. Do not write
|
||||
`[skip ci]`; nothing needs skipping, and it would also skip the mirrors.
|
||||
|
||||
The exhaustive gate runs once, at the final release step, from one dispatch on
|
||||
the frozen candidate commit:
|
||||
@@ -555,19 +508,17 @@ verification.
|
||||
|
||||
- Use the [pull request template](.github/PULL_REQUEST_TEMPLATE.md) when opening
|
||||
a PR — what and why, the issue line, and how you tested it
|
||||
- The PR description needs one issue line, checked by CI
|
||||
(`.github/workflows/pr-issue-link.yml`): `Closes #N` (or `Fixes` /
|
||||
- An issue line in the PR description is optional: `Closes #N` (or `Fixes` /
|
||||
`Resolves`) when the PR finishes the issue, `Refs #N` for related or partial
|
||||
work, or `No-Issue: <one-line reason>`. Never write a negated closing
|
||||
keyword such as "does not close #N": GitHub closes the issue anyway, so CI
|
||||
rejects it
|
||||
work. Never write a closing keyword inside a negation such as "does not
|
||||
close #N": GitHub closes the issue anyway
|
||||
- If you add a new layer, module, or abstraction, say which one it replaces
|
||||
or deletes
|
||||
- Keep PRs focused on a single change
|
||||
- Update documentation if needed
|
||||
- Add tests for new functionality
|
||||
- Get the pull-request checks green before requesting review, or say in the
|
||||
PR why one is red
|
||||
- If a check is red for a reason your change did not cause, a maintainer will
|
||||
deal with it
|
||||
|
||||
## Shape of a Typical PR
|
||||
|
||||
|
||||
+7
-11
@@ -14,17 +14,13 @@ could not cover by ourselves.
|
||||
- Issues are intake, not a privilege boundary. Do not auto-close good-faith
|
||||
issues because the reporter is not allowlisted. Ask for missing reproduction
|
||||
detail and leave room for maintainer triage.
|
||||
- PR gates exist for code review, CI load, and trust-boundary safety. They are
|
||||
not a quality judgment on the contributor. Keep dry-run mode unless a
|
||||
maintainer deliberately enables enforcement, and use warm copy when the gate
|
||||
comments.
|
||||
- Be generous with recurring contributors. When someone repeatedly brings
|
||||
useful reports or patches, use `/lgtmi` for issue access or `/lgtm` for PR
|
||||
access so the automation gets out of their way.
|
||||
- Preserve contributor credit. When harvesting work, inspect the PR and linked
|
||||
issues, keep author/co-author attribution where possible, add
|
||||
`Harvested from PR #N by @handle`, and credit the contributor in the
|
||||
changelog or release notes.
|
||||
- Never close or block a PR because CI is slow, flaky or red for a reason the
|
||||
PR did not cause; maintainers rerun or fix it. No automation closes, blocks
|
||||
or labels a contributor as unapproved.
|
||||
- Preserve contributor credit. If contributor work lands as our commit, keep
|
||||
them as author, add `Harvested from PR #N by @handle`, credit them in the
|
||||
changelog or release notes, and close the PR by hand with a specific
|
||||
thank-you.
|
||||
- Make credit machine-readable. If a harvested commit cannot preserve the
|
||||
contributor as the author, add a `Co-authored-by` trailer with the GitHub
|
||||
numeric noreply address from `.github/AUTHOR_MAP` or
|
||||
|
||||
@@ -97,9 +97,8 @@ Stage 5 of the loop: [cw-orient](../cw-orient/SKILL.md) →
|
||||
`gh api users/<handle> --jq '"\(.id)+\(.login)@users.noreply.github.com"'`.
|
||||
A clean cherry-pick keeps the author by itself. This step is the single
|
||||
owner of credit mechanics; other skills link here.
|
||||
The `Harvested from` line is what lets `auto-close-harvested.yml` close
|
||||
their PR with credit. Canonical human identities live in
|
||||
`.github/AUTHOR_MAP`.
|
||||
Close their PR by hand with a thank-you naming the commit; nothing closes a
|
||||
PR automatically. Canonical human identities live in `.github/AUTHOR_MAP`.
|
||||
|
||||
Whether a bot or agent also appears in a trailer no longer matters — the CI
|
||||
check that policed trailer identities was removed because it rejected
|
||||
@@ -128,11 +127,10 @@ Stage 5 of the loop: [cw-orient](../cw-orient/SKILL.md) →
|
||||
`${sha}`, not `$sha:refs`, because `:r` is a history modifier.
|
||||
`maintainerCanModify` does not guarantee that the push is accepted. When
|
||||
the fork refuses it, ask on the PR. Harvesting is the last resort.
|
||||
- **Check the contribution gate before assuming a PR is stalled.** An
|
||||
unlisted author's workflow runs sit at `action_required` and never start,
|
||||
so the PR looks abandoned when nobody has actually looked at it. Approve
|
||||
the runs, then fix the cause: add them to `.github/APPROVED_CONTRIBUTORS`
|
||||
(`all:username`), or comment `/lgtm` (PR scope) or `/lgtmi` (issue scope).
|
||||
- **Check for runs awaiting approval before assuming a PR is stalled.** A
|
||||
first-time contributor's workflow runs can sit at `action_required` until
|
||||
a maintainer approves them, so the PR looks abandoned when nobody has
|
||||
actually looked at it. Approve the runs.
|
||||
|
||||
5. **Merging under a gate.** `AGENTS.md` ("Merging under a gate") owns this:
|
||||
the artifact must literally say PASS, the review thread outranks the check
|
||||
|
||||
@@ -331,13 +331,13 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "CONTRIBUTING.md",
|
||||
"sha256": "c2747cbfb73a335fb77ef217497a5e8f07c70eab0922fcd23dfb1a46e28ced4b",
|
||||
"content": "# Contributing to codewhale\n\nThank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Rust 1.89 or later (edition 2024)\n- Cargo package manager\n- Git\n\n### Setting Up Development Environment\n\n1. Fork and clone the repository:\n ```bash\n git clone https://github.com/YOUR_USERNAME/CodeWhale.git\n cd CodeWhale\n ```\n\n2. Build the current engine and terminal:\n ```bash\n CODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --locked -p codewhale-cli\n ```\n\n3. Run the tests near your change (see [Fast local loop](#fast-local-loop)):\n ```bash\n scripts/dev-test.sh tui your_test_filter\n ```\n\n4. Run with development settings:\n ```bash\n ./target/debug/codewhale --version\n ./target/debug/codewhale\n ```\n\n### Testing the latest source\n\nThe canonical source is [`codewhale-hq/Codewhale`'s `main` branch](https://github.com/codewhale-hq/Codewhale/tree/main).\nRelease candidates land there after their CI gates pass, so contributors can\ntest and build on the same source. Tagged downloads remain the latest published\nrelease; their version can lag the development version on `main`.\n\nFor a fresh checkout of the current source:\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/Codewhale.git\ncd Codewhale\nCODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --release --locked -p codewhale-cli\n./target/release/codewhale --version\n./target/release/codewhale\n```\n\nInclude the commit shown by `--version` when reporting a problem. If you work\nfrom a fork, add the canonical repository as `upstream` and fetch `upstream/main`\nbefore starting a change; preserve any local work when updating your branch.\n\n## Development Workflow\n\n### Code Style\n\n- Run `cargo fmt` before committing to ensure consistent formatting\n- Run `cargo clippy` and address all warnings\n- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)\n- Add documentation comments for public APIs\n\n### Testing\n\n- Write tests for new functionality\n- Run the tests near your change (see [Fast local loop](#fast-local-loop));\n your pull request runs the Linux workspace suite and a Windows build for\n you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))\n- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`\n modules), and add integration tests under the owning crate's `tests/`\n directory (for example `crates/tui/tests/` or `crates/state/tests/`). The\n repository root `tests/` directory is not used\n\n### Pre-push verification\n\nThese are the Rust commands your pull request runs on Linux (the `Lint` and\n`Test (ubuntu-latest)` checks). You do not need all of them before every\npush: run `cargo fmt`, then check and test the crates you touched (see\n[Fast local loop](#fast-local-loop)). Run the full set locally when a change\nspans many crates, or let the pull request run it for you:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets --all-features --locked -- \\\n -D warnings \\\n -A clippy::uninlined_format_args \\\n -A clippy::too_many_arguments \\\n -A clippy::unnecessary_map_or\ncargo test --workspace --all-features --locked\n```\n\nClippy lints test, bench and example targets too (`--all-targets`), and the\nthree allowances above are the only ones CI and the release lane accept.\n\n#### Fast local loop\n\nThe full gate above is what CI enforces, but you do not need it for every\nedit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is\nthe one that avoids rebuilding it more than necessary (numbers and the\nreasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):\n\n```bash\n# 1. Type-check first (seconds after the first build; no codegen, no link).\nscripts/dev-cargo.sh check -p codewhale-tui\n\n# 2. Run only the tests near your change (one crate, one filter).\nscripts/dev-test.sh tui fleet_setup\n# or: scripts/dev-test.sh crates/command-contract/src/elapsed.rs\n\n# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when\n# it is installed (one process per test, all cores busy, slow tests\n# named; ~100 s here vs ~270 s with libtest).\ncargo install cargo-nextest --locked # once\nscripts/dev-test.sh tui\nscripts/dev-cargo.sh nextest run --workspace --all-features --locked\n\n# 4. The Linux suite your PR runs:\ncargo test --workspace --all-features --locked\n```\n\n`.config/nextest.toml` already serializes the PTY suite and bounds the\nintegration tests that spawn the real binary, so `cargo nextest run` is\nsafe to use on the whole workspace (nextest does not run doctests; the\nauthoritative `cargo test` gate does). Tests must not depend on running in\nthe same process as another test (nextest gives every test its own\nprocess); if a test Line truncated
|
||||
"sha256": "3336ce37f447b947a456e84db91a1abdd63aab2eb98a399bf427e8b81f13003f",
|
||||
"content": "# Contributing to codewhale\n\nThank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Rust 1.89 or later (edition 2024)\n- Cargo package manager\n- Git\n\n### Setting Up Development Environment\n\n1. Fork and clone the repository:\n ```bash\n git clone https://github.com/YOUR_USERNAME/CodeWhale.git\n cd CodeWhale\n ```\n\n2. Build the current engine and terminal:\n ```bash\n CODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --locked -p codewhale-cli\n ```\n\n3. Run the tests near your change (see [Fast local loop](#fast-local-loop)):\n ```bash\n scripts/dev-test.sh tui your_test_filter\n ```\n\n4. Run with development settings:\n ```bash\n ./target/debug/codewhale --version\n ./target/debug/codewhale\n ```\n\n### Testing the latest source\n\nThe canonical source is [`codewhale-hq/Codewhale`'s `main` branch](https://github.com/codewhale-hq/Codewhale/tree/main).\nRelease candidates land there after their CI gates pass, so contributors can\ntest and build on the same source. Tagged downloads remain the latest published\nrelease; their version can lag the development version on `main`.\n\nFor a fresh checkout of the current source:\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/Codewhale.git\ncd Codewhale\nCODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --release --locked -p codewhale-cli\n./target/release/codewhale --version\n./target/release/codewhale\n```\n\nInclude the commit shown by `--version` when reporting a problem. If you work\nfrom a fork, add the canonical repository as `upstream` and fetch `upstream/main`\nbefore starting a change; preserve any local work when updating your branch.\n\n## Development Workflow\n\n### Code Style\n\n- Run `cargo fmt` before committing to ensure consistent formatting\n- Run `cargo clippy` and address all warnings\n- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)\n- Add documentation comments for public APIs\n\n### Testing\n\n- Write tests for new functionality\n- Run the tests near your change (see [Fast local loop](#fast-local-loop));\n your pull request runs the Linux workspace suite and a Windows build for\n you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))\n- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`\n modules), and add integration tests under the owning crate's `tests/`\n directory (for example `crates/tui/tests/` or `crates/state/tests/`). The\n repository root `tests/` directory is not used\n\n### Pre-push verification\n\nThese are the Rust commands your pull request runs on Linux (the `Lint` and\n`Test (ubuntu-latest)` checks). You do not need all of them before every\npush: run `cargo fmt`, then check and test the crates you touched (see\n[Fast local loop](#fast-local-loop)). Run the full set locally when a change\nspans many crates, or let the pull request run it for you:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets --all-features --locked -- \\\n -D warnings \\\n -A clippy::uninlined_format_args \\\n -A clippy::too_many_arguments \\\n -A clippy::unnecessary_map_or\ncargo test --workspace --all-features --locked\n```\n\nClippy lints test, bench and example targets too (`--all-targets`), and the\nthree allowances above are the only ones CI and the release lane accept.\n\n#### Fast local loop\n\nThe full gate above is what CI enforces, but you do not need it for every\nedit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is\nthe one that avoids rebuilding it more than necessary (numbers and the\nreasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):\n\n```bash\n# 1. Type-check first (seconds after the first build; no codegen, no link).\nscripts/dev-cargo.sh check -p codewhale-tui\n\n# 2. Run only the tests near your change (one crate, one filter).\nscripts/dev-test.sh tui fleet_setup\n# or: scripts/dev-test.sh crates/command-contract/src/elapsed.rs\n\n# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when\n# it is installed (one process per test, all cores busy, slow tests\n# named; ~100 s here vs ~270 s with libtest).\ncargo install cargo-nextest --locked # once\nscripts/dev-test.sh tui\nscripts/dev-cargo.sh nextest run --workspace --all-features --locked\n\n# 4. The Linux suite your PR runs:\ncargo test --workspace --all-features --locked\n```\n\n`.config/nextest.toml` already serializes the PTY suite and bounds the\nintegration tests that spawn the real binary, so `cargo nextest run` is\nsafe to use on the whole workspace (nextest does not run doctests; the\nauthoritative `cargo test` gate does). Tests must not depend on running in\nthe same process as another test (nextest gives every test its own\nprocess); if a test Line truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/AGENT_ETHOS.md",
|
||||
"sha256": "d9cd36fc7b6d9d236c68e62ffc92f41d3f3fab6e2e5eb9bf2bb2ae1d53f82331",
|
||||
"content": "# Agent Ethos\n\n> 阅读简体中文版:[zh_hans/AGENT_ETHOS.md](zh_hans/AGENT_ETHOS.md)。\n\nCodewhale is maintained with agents, but it is not maintained by automation\nalone. Treat community reports and patches as real collaboration: people are\nbringing us machines, providers, regions, shells, packages, and edge cases we\ncould not cover by ourselves.\n\n## Stewardship\n\n- Verify live truth before acting. Check the current branch, release state,\n registry state, CI, and linked issues instead of trusting a handoff.\n- Issues are intake, not a privilege boundary. Do not auto-close good-faith\n issues because the reporter is not allowlisted. Ask for missing reproduction\n detail and leave room for maintainer triage.\n- PR gates exist for code review, CI load, and trust-boundary safety. They are\n not a quality judgment on the contributor. Keep dry-run mode unless a\n maintainer deliberately enables enforcement, and use warm copy when the gate\n comments.\n- Be generous with recurring contributors. When someone repeatedly brings\n useful reports or patches, use `/lgtmi` for issue access or `/lgtm` for PR\n access so the automation gets out of their way.\n- Preserve contributor credit. When harvesting work, inspect the PR and linked\n issues, keep author/co-author attribution where possible, add\n `Harvested from PR #N by @handle`, and credit the contributor in the\n changelog or release notes.\n- Make credit machine-readable. If a harvested commit cannot preserve the\n contributor as the author, add a `Co-authored-by` trailer with the GitHub\n numeric noreply address from `.github/AUTHOR_MAP` or\n `gh api users/<login> --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"'`.\n Do not use `.local`, placeholder, bot/tool, or raw third-party emails for\n human contributor credit.\n- Deferral is a maintainer action, not a dismissal. If a PR or issue is not\n ready, say what is blocked, what evidence would change the decision, and\n which part of the work remains valuable.\n\n## Agent Workflow\n\n- Use sub-agents for exploration, review, and verification, but keep a human\n maintainer posture in the parent session. Sub-agent output is evidence; the\n parent is responsible for the final decision.\n- Personally review community PRs before merging, harvesting, closing, or\n deferring them. Do not close work based only on title, labels, or an agent's\n summary.\n- Prefer narrow, reversible changes that match the existing codebase. Avoid\n drive-by refactors while harvesting community work.\n- Run the smallest meaningful validation first, then broaden tests when a\n change touches shared behavior, release plumbing, auth, sandboxing,\n providers, or UI workflows.\n- Do not tag, publish, push release artifacts, or create GitHub releases\n without explicit maintainer approval.\n\n## Product Tone\n\nCodewhale should feel like a capable coding harness with a public community,\nnot a closed queue. Automation should reduce maintainer load while making\ncontributors feel seen, credited, and able to keep helping.\n"
|
||||
"sha256": "f656bf5b39018c295d5b82f406c9b67c658c2646b766821e20bdbf8633cc6667",
|
||||
"content": "# Agent Ethos\n\n> 阅读简体中文版:[zh_hans/AGENT_ETHOS.md](zh_hans/AGENT_ETHOS.md)。\n\nCodewhale is maintained with agents, but it is not maintained by automation\nalone. Treat community reports and patches as real collaboration: people are\nbringing us machines, providers, regions, shells, packages, and edge cases we\ncould not cover by ourselves.\n\n## Stewardship\n\n- Verify live truth before acting. Check the current branch, release state,\n registry state, CI, and linked issues instead of trusting a handoff.\n- Issues are intake, not a privilege boundary. Do not auto-close good-faith\n issues because the reporter is not allowlisted. Ask for missing reproduction\n detail and leave room for maintainer triage.\n- Never close or block a PR because CI is slow, flaky or red for a reason the\n PR did not cause; maintainers rerun or fix it. No automation closes, blocks\n or labels a contributor as unapproved.\n- Preserve contributor credit. If contributor work lands as our commit, keep\n them as author, add `Harvested from PR #N by @handle`, credit them in the\n changelog or release notes, and close the PR by hand with a specific\n thank-you.\n- Make credit machine-readable. If a harvested commit cannot preserve the\n contributor as the author, add a `Co-authored-by` trailer with the GitHub\n numeric noreply address from `.github/AUTHOR_MAP` or\n `gh api users/<login> --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"'`.\n Do not use `.local`, placeholder, bot/tool, or raw third-party emails for\n human contributor credit.\n- Deferral is a maintainer action, not a dismissal. If a PR or issue is not\n ready, say what is blocked, what evidence would change the decision, and\n which part of the work remains valuable.\n\n## Agent Workflow\n\n- Use sub-agents for exploration, review, and verification, but keep a human\n maintainer posture in the parent session. Sub-agent output is evidence; the\n parent is responsible for the final decision.\n- Personally review community PRs before merging, harvesting, closing, or\n deferring them. Do not close work based only on title, labels, or an agent's\n summary.\n- Prefer narrow, reversible changes that match the existing codebase. Avoid\n drive-by refactors while harvesting community work.\n- Run the smallest meaningful validation first, then broaden tests when a\n change touches shared behavior, release plumbing, auth, sandboxing,\n providers, or UI workflows.\n- Do not tag, publish, push release artifacts, or create GitHub releases\n without explicit maintainer approval.\n\n## Product Tone\n\nCodewhale should feel like a capable coding harness with a public community,\nnot a closed queue. Automation should reduce maintainer load while making\ncontributors feel seen, credited, and able to keep helping.\n"
|
||||
},
|
||||
{
|
||||
"path": "docs/CONTRIBUTORS.md",
|
||||
|
||||
Reference in new issue
Block a user