Skip to content

Commit eaab2e6

Browse files
committed
merge: bring the v2.8.0 stable line into preview
preview stalled at 2026-07-28 while v2.7.43 and v2.8.0 both shipped from main, so the npm preview dist-tag (2.7.43-preview.20260728) sat below latest (2.8.0). Resolve to main's tree exactly: the two conflicting paths (package.json version, tests/repo-hygiene.test.ts) already carry the landed form on main.
2 parents b04b872 + 1adad35 commit eaab2e6

934 files changed

Lines changed: 97568 additions & 5280 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/AGENTS.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# GitHub automation instructions
2+
3+
This file applies to `.github/` and inherits the repository-wide rules in `/AGENTS.md`.
4+
5+
## Security boundary
6+
7+
Every workflow, ownership, branch-enforcement, release, or repository-automation
8+
change requires explicit security review under `MAINTAINERS.md`.
9+
10+
## Workflow rules
11+
12+
- Grant the minimum required `permissions`.
13+
- Pin third-party actions to immutable full commit SHAs.
14+
- Preserve the human-readable version comment beside each pinned action.
15+
- Do not run untrusted pull-request code with secrets or write permissions.
16+
- Treat `pull_request_target`, workflow dispatch, reusable workflows, artifacts, caches, and generated command input as trust boundaries.
17+
- Do not broaden triggers, write permissions, token exposure, release eligibility, or publish capability without an explicit task requirement.
18+
- Preserve cross-platform coverage where the workflow currently promises Linux, macOS, and Windows behavior.
19+
- Keep branch-enforcement text synchronized with `AGENTS.md`, `MAINTAINERS.md`, and the public contributing guide.
20+
21+
## Validation
22+
23+
- Inspect the complete workflow diff, including event triggers, permissions, conditions, interpolation, and shell behavior.
24+
- Run the local commands represented by changed workflow steps where possible.
25+
- Run `bun run prepush` for CI, release, dependency, packaging, or cross-platform workflow changes.
26+
- Do not claim the workflow itself passed until GitHub Actions reports success for the exact commit.

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@
1414
/src/server/management-api.ts @lidge-jun @Ingwannu
1515

1616
# Governance and security policy
17+
/AGENTS.md @lidge-jun @Ingwannu
18+
**/AGENTS.md @lidge-jun @Ingwannu
1719
/MAINTAINERS.md @lidge-jun @Ingwannu
1820
/SECURITY.md @lidge-jun @Ingwannu

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
blank_issues_enabled: false
22
contact_links:
3+
- name: Report a security vulnerability (private)
4+
url: https://github.com/lidge-jun/opencodex/security/advisories/new
5+
about: Report undisclosed vulnerabilities privately to the maintainers. Do not open a public issue.
36
- name: Security policy
47
url: https://github.com/lidge-jun/opencodex/blob/main/SECURITY.md
58
about: Read the supported-version and reporting guidance before sharing security-sensitive details.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"use strict";
2+
3+
const fs = require("node:fs");
4+
const path = require("node:path");
5+
const { describe, it } = require("node:test");
6+
const assert = require("node:assert/strict");
7+
8+
describe("enforce-pr-target workflow", () => {
9+
const workflowPath = path.join(__dirname, "../workflows/enforce-pr-target.yml");
10+
const workflow = fs.readFileSync(workflowPath, "utf8");
11+
12+
it("uses pull_request_target without checking out PR head code", () => {
13+
assert.match(workflow, /pull_request_target:/);
14+
assert.doesNotMatch(
15+
workflow,
16+
/ref:\s*\$\{\{\s*github\.event\.pull_request\.head/,
17+
"enforcer must not check out untrusted PR head code",
18+
);
19+
});
20+
21+
it("grants contents:write so draft GraphQL mutations work with GITHUB_TOKEN", () => {
22+
// convertPullRequestToDraft / markPullRequestReadyForReview fail with
23+
// "Resource not accessible by integration" when contents stays unset/read
24+
// (seen on #626). Assert the real permissions block, not comment text
25+
// that also mentions these scopes.
26+
const permissionsBlock = workflow.match(/^permissions:\n((?:[ \t]+.+\n)+)/m);
27+
assert.ok(permissionsBlock, "workflow must declare a top-level permissions block");
28+
const lines = permissionsBlock[1]
29+
.split("\n")
30+
.map((line) => line.trim())
31+
.filter(Boolean)
32+
.sort();
33+
assert.deepEqual(lines, ["contents: write", "pull-requests: write"]);
34+
});
35+
36+
it("fails the required check on a wrong base even if draft conversion fails", () => {
37+
assert.match(workflow, /core\.setFailed\(/);
38+
assert.match(workflow, /draftConversionFailed/);
39+
assert.match(workflow, /Could not convert pull request to draft/);
40+
});
41+
42+
it("soft-fails ready-for-review restoration the same way", () => {
43+
assert.match(workflow, /readyConversionFailed/);
44+
assert.match(workflow, /Could not mark pull request ready for review/);
45+
});
46+
47+
it("listens for synchronize so rebase can clear ancestry failures", () => {
48+
assert.match(workflow, /synchronize/);
49+
});
50+
51+
it("checks out trusted default-branch scripts only (never PR head)", () => {
52+
assert.match(workflow, /actions\/checkout@[0-9a-f]{40}/);
53+
assert.match(workflow, /ref:\s*\$\{\{\s*github\.event\.repository\.default_branch\s*\}\}/);
54+
assert.match(workflow, /sparse-checkout:\s*\.github\/scripts/);
55+
assert.match(workflow, /persist-credentials:\s*false/);
56+
assert.doesNotMatch(workflow, /ref:\s*\$\{\{\s*github\.event\.pull_request\.head/);
57+
});
58+
59+
it("loads pr-quality via require from the checked-out scripts", () => {
60+
assert.match(workflow, /pr-quality\.cjs/);
61+
assert.match(workflow, /collectPrQualityFailures/);
62+
});
63+
64+
it("checks stacked bases via open PR heads before wrong_base enforcement", () => {
65+
assert.match(workflow, /stackedBase/);
66+
assert.match(workflow, /github\.rest\.pulls\.list/);
67+
assert.match(workflow, /treating as stacked/);
68+
const qualityCall = workflow.match(
69+
/collectPrQualityFailures\(\{([\s\S]*?)\}\);/,
70+
);
71+
assert.ok(qualityCall, "must call collectPrQualityFailures");
72+
assert.match(qualityCall[1], /stackedBase/);
73+
});
74+
75+
it("strips stale WRONG BRANCH prefix on failure when base is corrected", () => {
76+
const failureBlock = workflow.match(
77+
/if \(failures\.length > 0\) \{([\s\S]*?)core\.setFailed\(/,
78+
);
79+
assert.ok(failureBlock, "workflow must have a failure path");
80+
const failurePath = failureBlock[1];
81+
assert.match(failurePath, /shouldStripTitlePrefix/);
82+
assert.match(failurePath, /!hasWrongBase/);
83+
assert.match(failurePath, /titlePrefixedByBot = false/);
84+
assert.match(failurePath, /pr\.title\.slice\(TITLE_PREFIX\.length\)/);
85+
});
86+
});

.github/scripts/issue-quality.cjs

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,29 @@ function unwrapSingleEnclosingFence(text) {
2525
return match[2];
2626
}
2727

28-
function isPlaceholderOnlyValue(raw) {
29-
if (typeof raw !== "string") return false;
28+
/**
29+
* Shared strip/trim/unwrap used by placeholder and unusable-stand-in matchers.
30+
* Returns null when the value is absent after normalisation.
31+
*/
32+
function normalizeRawSectionValue(raw) {
33+
if (typeof raw !== "string") return null;
3034
let value = raw.replace(/<!--[\s\S]*?-->/g, "").trim();
31-
if (!value) return false;
35+
if (!value) return null;
3236

33-
// A lone fenced block whose entire body is a placeholder is still placeholder
34-
// text (e.g. ```text\nN/A\n```), not a real example.
37+
// A lone fenced block whose entire body is a stand-in is still a stand-in
38+
// (e.g. ```text\nN/A\n```), not a real example.
3539
const unwrapped = unwrapSingleEnclosingFence(value);
3640
if (unwrapped !== null) {
3741
value = unwrapped.trim();
38-
if (!value) return false;
42+
if (!value) return null;
3943
}
4044

45+
return value;
46+
}
47+
48+
function isPlaceholderOnlyValue(raw) {
49+
const value = normalizeRawSectionValue(raw);
50+
if (value === null) return false;
4151
return PLACEHOLDER_ONLY_RE.test(value);
4252
}
4353

@@ -374,7 +384,10 @@ function detectIssueKind(issue) {
374384
// ---------------------------------------------------------------------------
375385

376386
function isEmpty(text) {
377-
return clean(text).length === 0;
387+
const c = clean(text);
388+
if (c.length === 0) return true;
389+
// Stand-ins like "...", "…", "---" are not actionable report content.
390+
return /^[\p{P}\p{S}\s]+$/u.test(c);
378391
}
379392

380393
function allSameCanonical(sections) {
@@ -395,6 +408,20 @@ function isPlaceholder(text) {
395408
return isPlaceholderOnlyValue(text);
396409
}
397410

411+
/**
412+
* True when Version is an "I don't know" stand-in rather than an install id.
413+
* Kept separate from PLACEHOLDER_ONLY_RE so legacy N/A / No response soft-pass
414+
* behaviour is unchanged.
415+
*/
416+
const UNUSABLE_VERSION_RE =
417+
/^[\s_*~`]*(?:unknown|unkown|uknown|don'?t\s+know|do\s+not\s+know|idk|dunno|not\s+sure|unsure|\?+||\s*|(?:|)?||||||keine\s+ahnung|wei[sß]{1,2}\s+nicht)[\s_*~`]*[.!?]*$/i;
418+
419+
function isUnusableVersion(raw) {
420+
const value = normalizeRawSectionValue(raw);
421+
if (value === null) return false;
422+
return UNUSABLE_VERSION_RE.test(value);
423+
}
424+
398425
const CJK_RE =
399426
/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu;
400427

@@ -430,6 +457,16 @@ function isTooTerseFeatureSection(text) {
430457
return true;
431458
}
432459

460+
/**
461+
* Bug Reproduction needs steps or concrete signals. A title-like phrase with
462+
* no commands, paths, digits, or product keywords is not actionable.
463+
*/
464+
function isTooTerseBugReproduction(text) {
465+
if (isEmpty(text) || isPlaceholder(text)) return false;
466+
if (hasConcreteDetail(text)) return false;
467+
return countWords(text) < 12;
468+
}
469+
433470
/**
434471
* Check if raw section text is a placeholder-only variant without relying on
435472
* clean() first. Used to distinguish intentionally blank optional fields
@@ -625,6 +662,8 @@ function validateIssue(issue) {
625662
const repro = extractSection(body, "Reproduction");
626663
const version = extractSection(body, "Version");
627664
const os = extractSection(body, "Operating system") ?? extractSection(body, "OS");
665+
// New Bug report template always includes Client or integration.
666+
const isNewBugForm = extractSection(body, "Client or integration") !== null;
628667

629668
if (isEmpty(summary) && isEmpty(repro)) {
630669
// Soft-pass substantial non-English / freeform structured reports once
@@ -642,16 +681,65 @@ function validateIssue(issue) {
642681
reasons.push("Both Summary and Reproduction are empty.");
643682
guidance.push("Describe what happened and how to reproduce it.");
644683
}
684+
} else {
685+
// Each mapped field is required on its own — a filled Summary with an
686+
// empty / ellipsis Reproduction (e.g. #598) must not pass.
687+
if (isEmpty(summary)) {
688+
reasons.push("Summary is empty.");
689+
guidance.push("Describe what happened (the symptom or error).");
690+
}
691+
if (isEmpty(repro)) {
692+
reasons.push("Reproduction is empty.");
693+
guidance.push("List the exact steps to reproduce the problem.");
694+
} else if (!softPass && isTooTerseBugReproduction(repro)) {
695+
reasons.push("Reproduction is too vague to act on.");
696+
guidance.push("List exact steps, commands, and the observed failure — not only a short phrase.");
697+
}
645698
}
646699

647-
// Required environment fields removed after submission.
648-
// Only fire when the headings exist in the body (new form). Legacy bug
649-
// reports never had Version or OS fields, so null means absent, not removed.
650-
// Skip when the raw value is a "No response" placeholder -- the old form had
651-
// both fields as optional, so legacy issues legitimately contain those headings
652-
// with the GitHub placeholder. Only close when the field was actively cleared.
653-
if (!softPass && version !== null && os !== null && isEmpty(version) && isEmpty(os) &&
654-
!isRawPlaceholder(version) && !isRawPlaceholder(os)) {
700+
// Version "Unknown" / "모름" / "idk" is never actionable, on any form.
701+
if (!softPass && version !== null && isUnusableVersion(version)) {
702+
reasons.push("Version is missing or unknown.");
703+
guidance.push("Report the installed `@bitkyc08/opencodex` version (for example `2.7.42`) or a commit SHA from `ocx --version`.");
704+
} else if (
705+
!softPass &&
706+
isNewBugForm &&
707+
(version === null || isEmpty(version) || isRawPlaceholder(version))
708+
) {
709+
// New form requires Version (including when the heading was removed).
710+
// Legacy N/A / No response soft-pass stays only for bodies without
711+
// Client or integration.
712+
reasons.push("Version is missing.");
713+
guidance.push("Add your OpenCodex version so we can reproduce the environment.");
714+
}
715+
716+
if (!softPass && isNewBugForm && os !== null && isUnusableVersion(os)) {
717+
reasons.push("Operating system is missing or unknown.");
718+
guidance.push("Add your OS name and version (for example Windows 11 24H2).");
719+
} else if (
720+
!softPass &&
721+
isNewBugForm &&
722+
(os === null || isEmpty(os) || isRawPlaceholder(os))
723+
) {
724+
reasons.push("Operating system is missing.");
725+
guidance.push("Add your OS name and version (for example Windows 11 24H2).");
726+
}
727+
728+
// Required environment fields removed after submission on bodies that are
729+
// not the new form (no Client or integration). Legacy reports never had
730+
// Version or OS fields, so null means absent, not removed. Skip when the
731+
// raw value is a "No response" placeholder — the old form had both fields
732+
// as optional. Only close when the field was actively cleared.
733+
if (
734+
!softPass &&
735+
!isNewBugForm &&
736+
version !== null &&
737+
os !== null &&
738+
isEmpty(version) &&
739+
isEmpty(os) &&
740+
!isRawPlaceholder(version) &&
741+
!isRawPlaceholder(os)
742+
) {
655743
reasons.push("Version and Operating system are both missing.");
656744
guidance.push("Add your OpenCodex version and OS so we can reproduce the environment.");
657745
}
@@ -859,6 +947,7 @@ module.exports = {
859947
isPlaceholderOnlyValue,
860948
isPlaceholder,
861949
isRawPlaceholder,
950+
isUnusableVersion,
862951
countWords,
863952
hasConcreteDetail,
864953
labelForKind,

0 commit comments

Comments
 (0)