Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
* @JerryBlackoo
/.github/workflows/ @JerryBlackoo
/.github/codex/ @JerryBlackoo
/docs/collaboration/ @JerryBlackoo
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
blank_issues_enabled: false
1 change: 0 additions & 1 deletion .github/ISSUE_TEMPLATE/task_issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ assignees: ''
- 依赖原因:写清楚依赖的接口、schema、数据结构、环境变量、服务能力或验收条件。
- 建议分支:`Team/type/short-title`
- GitHub Project:`Team Project`
- Project sync:`pending / synced / blocked`

## 发布前检查

Expand Down
3 changes: 1 addition & 2 deletions .github/ISSUE_TEMPLATE/test_task_issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: Test Task Issue
about: Create a tracked task for testing work and test execution follow-up
title: '[Q-001] 中文测试任务标题'
labels: 'testing'
labels: 'area:testing'
assignees: ''
---

Expand All @@ -29,7 +29,6 @@ assignees: ''
- 依赖原因:写清楚依赖的接口、schema、数据结构、环境变量、服务能力或验收条件。
- 建议分支:`QA/test/short-title`
- GitHub Project:`Team Project`
- Project sync:`pending / synced / blocked`

## 发布前检查

Expand Down
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
target-branch: develop
schedule:
interval: weekly
commit-message:
prefix: "chore(deps)"
labels:
- ci
8 changes: 7 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@

## 验证

请用中文说明已经运行的检查,例如 lint、type-check、test、build、人工验收。
已运行:

- `<command>`:<结果>

未运行:

- 无。或填写 `<command>`:原因:<原因>;残余风险:<风险>。

## 已知风险

Expand Down
111 changes: 111 additions & 0 deletions .github/tests/repository-hardening.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');

const root = path.resolve(__dirname, '..', '..');
const read = (relativePath) => fs.readFileSync(path.join(root, relativePath), 'utf8');

test('all GitHub Actions dependencies are pinned to full commit SHAs', () => {
const workflowDir = path.join(root, '.github', 'workflows');
for (const name of fs.readdirSync(workflowDir).filter((entry) => entry.endsWith('.yml'))) {
const source = fs.readFileSync(path.join(workflowDir, name), 'utf8');
for (const match of source.matchAll(/^\s*uses:\s*([^\s#]+)(?:\s+#.*)?$/gmu)) {
assert.match(match[1], /@[0-9a-f]{40}$/u, `${name}: ${match[1]} must be pinned`);
}
}
});

test('Codex review is opt-in, trusted-only, read-only, and publishes from a separate job', () => {
const source = read('.github/workflows/codex-pr-review.yml');
assert.match(source, /vars\.CODEX_REVIEW_ENABLED\s*==\s*'true'/u);
assert.doesNotMatch(source, /allow-users:\s*["']?\*["']?/u);
assert.match(source, /permission-profile:\s*["']?:read-only["']?/u);
assert.match(source, /\n\s{2}publish:\s*\n/u);
assert.match(source, /needs:\s*review/u);
});

test('Docs Check validates the event commit range instead of the clean worktree', () => {
const source = read('.github/workflows/docs-check.yml');
assert.match(source, /github\.event\.pull_request\.base\.sha/u);
assert.match(source, /github\.event\.before/u);
assert.doesNotMatch(source, /run:\s*git diff --check\s*$/mu);
});

test('PR Guard validates Conventional Commit PR titles', () => {
const source = read('.github/workflows/pr-guard.yml');
assert.match(source, /conventionalTitlePattern/u);
assert.match(source, /PR title must follow Conventional Commits/u);
});

test('Dependabot targets develop and is explicitly recognized as trusted automation', () => {
const dependabot = read('.github/dependabot.yml');
assert.match(dependabot, /^\s*target-branch:\s*develop$/mu);
assert.match(dependabot, /^\s*prefix:\s*["']chore\(deps\)["']$/mu);
for (const workflow of ['pr-guard.yml', 'commitlint.yml']) {
const source = read(`.github/workflows/${workflow}`);
assert.match(source, /TRUSTED_AUTOMATION_USERS/u);
assert.match(source, /dependabot\[bot\]/u);
}
});

test('required workflow jobs expose the documented check names', () => {
const checks = new Map([
['pr-guard.yml', 'PR Guard'],
['commitlint.yml', 'Commitlint'],
['docs-check.yml', 'Docs Check'],
]);
for (const [workflow, checkName] of checks) {
const source = read(`.github/workflows/${workflow}`);
assert.match(source, new RegExp(`^\\s{4}name:\\s*${checkName}$`, 'mu'));
}
});

test('required read-only checks bind to the pull request commit', () => {
for (const workflow of ['pr-guard.yml', 'commitlint.yml']) {
const source = read(`.github/workflows/${workflow}`);
assert.match(source, /^\s{2}pull_request:\s*$/mu);
assert.doesNotMatch(source, /^\s{2}pull_request_target:\s*$/mu);
}
assert.match(read('.github/workflows/auto-label.yml'), /^\s{2}pull_request_target:\s*$/mu);
});

test('security reporting instructions do not point copied repositories to the template source', () => {
const security = read('SECURITY.md');
const issueConfig = read('.github/ISSUE_TEMPLATE/config.yml');
assert.doesNotMatch(security, /JerryBlackoo\/github-teamwork/u);
assert.doesNotMatch(issueConfig, /JerryBlackoo\/github-teamwork/u);
assert.match(security, /Private vulnerability reporting/u);
});

test('issue templates use configured labels and do not expose obsolete sync state', () => {
const task = read('.github/ISSUE_TEMPLATE/task_issue.md');
const testTask = read('.github/ISSUE_TEMPLATE/test_task_issue.md');
assert.doesNotMatch(task, /Project sync/u);
assert.doesNotMatch(testTask, /Project sync/u);
assert.match(testTask, /^labels:\s*'area:testing'$/mu);
const config = read('.github/ISSUE_TEMPLATE/config.yml');
assert.match(config, /^blank_issues_enabled:\s*false$/mu);
});

test('local Markdown links resolve to repository files', () => {
const markdownFiles = [];
const visit = (directory) => {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
if (entry.name === '.git') continue;
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) visit(absolute);
else if (entry.name.endsWith('.md')) markdownFiles.push(absolute);
}
};
visit(root);
for (const file of markdownFiles) {
const source = fs.readFileSync(file, 'utf8');
for (const match of source.matchAll(/\[[^\]]+\]\(([^)]+)\)/gu)) {
const target = match[1].split('#', 1)[0];
if (!target || /^(?:https?:|mailto:)/u.test(target)) continue;
const resolved = path.resolve(path.dirname(file), decodeURIComponent(target));
assert.ok(fs.existsSync(resolved), `${path.relative(root, file)} links to missing ${target}`);
}
}
});
Loading
Loading