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
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ permissions:
contents: read

jobs:
dist-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install dependencies
run: npm install
working-directory: multi-review

- name: Rebuild dist
run: npm run build
working-directory: multi-review

- name: Verify dist is up to date
run: |
if ! git diff --quiet -- multi-review/dist/; then
echo "::error::multi-review/dist/ is out of date. Run 'npm run build' in multi-review/ and commit the result."
git diff -- multi-review/dist/
exit 1
fi

smoke-actions:
runs-on: ubuntu-latest
env:
Expand Down
16 changes: 10 additions & 6 deletions github-run-opencode/run-github-opencode.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,8 @@ def _main() -> int:
extra_env_raw = get_env("GITHUB_RUN_OPENCODE_EXTRA_ENV")
allow_sensitive = get_env("GITHUB_RUN_OPENCODE_EXTRA_ENV_ALLOW_SENSITIVE", "false").strip().lower() in ("true", "1", "yes")
if extra_env_raw:
blocked_keys: set[str] = set()
prefix_blocked: list[str] = []
sensitive_blocked: list[str] = []
for line in extra_env_raw.splitlines():
line = line.strip()
if not line or line.startswith("#"):
Expand All @@ -506,19 +507,22 @@ def _main() -> int:
if key:
if key.startswith("GITHUB_RUN_OPENCODE_"):
print(f"::error::extra-env key '{key}' starts with reserved prefix 'GITHUB_RUN_OPENCODE_' and is not allowed")
blocked_keys.add(key)
prefix_blocked.append(key)
continue
if key in SENSITIVE_ENV_KEYS:
if allow_sensitive:
print(f"::warning::extra-env key '{key}' overrides a sensitive runtime variable (allowed by extra-env-allow-sensitive)")
else:
print(f"::error::extra-env key '{key}' overrides a sensitive runtime variable; set extra-env-allow-sensitive to 'true' to allow")
blocked_keys.add(key)
sensitive_blocked.append(key)
continue
os.environ[key] = value
if blocked_keys:
sorted_keys = sorted(blocked_keys)
print(f"extra-env: blocked {len(sorted_keys)} disallowed key override(s): {', '.join(sorted_keys)}", file=sys.stderr)
all_blocked = prefix_blocked + sensitive_blocked
if all_blocked:
if prefix_blocked:
print(f"extra-env: blocked {len(prefix_blocked)} reserved-prefix key(s): {', '.join(prefix_blocked)}", file=sys.stderr)
if sensitive_blocked:
print(f"extra-env: blocked {len(sensitive_blocked)} sensitive key override(s): {', '.join(sensitive_blocked)}", file=sys.stderr)
sys.exit(1)

reasoning_effort = get_env("GITHUB_RUN_OPENCODE_REASONING_EFFORT", "")
Expand Down
24 changes: 16 additions & 8 deletions multi-review/dist/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5262,8 +5262,10 @@ function fetchAllGiteaComments(baseUrl, token) {
}
var HASH_NUM_RE = /(?:^|(?<=[\s(\[{<("'`>:,、:]))(#)(\d{1,6})(?=[\s)\]}>)"'`,.!?;,。!?、:]|$)/gm;
var FENCED_CODE_RE = /```[\s\S]*?```/g;
var INLINE_CODE_RE = /`[^`]+`/g;
var INLINE_CODE_RE = /`[^`\n]+`/g;
function escapeHashReferences(text) {
if (!text || !HASH_NUM_RE.test(text)) return text;
HASH_NUM_RE.lastIndex = 0;
const segments = [];
let lastEnd = 0;
for (const m of text.matchAll(FENCED_CODE_RE)) {
Expand Down Expand Up @@ -5652,7 +5654,8 @@ function parseExtraEnv() {
const allowSensitive = ["true", "1", "yes"].includes(
(process.env.MULTI_REVIEW_EXTRA_ENV_ALLOW_SENSITIVE || "false").trim().toLowerCase()
);
const blockedKeys = /* @__PURE__ */ new Set();
const prefixBlocked = [];
const sensitiveBlocked = [];
for (const line of raw.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
Expand All @@ -5663,24 +5666,29 @@ function parseExtraEnv() {
if (!key) continue;
if (key.startsWith("MULTI_REVIEW_")) {
console.log(`::error::extra-env key '${key}' starts with reserved prefix 'MULTI_REVIEW_' and is not allowed`);
blockedKeys.add(key);
prefixBlocked.push(key);
continue;
}
if (SENSITIVE_ENV_KEYS.has(key)) {
if (allowSensitive) {
console.log(`::warning::extra-env key '${key}' overrides a sensitive runtime variable (allowed by extra-env-allow-sensitive)`);
} else {
console.log(`::error::extra-env key '${key}' overrides a sensitive runtime variable; set extra-env-allow-sensitive to 'true' to allow`);
blockedKeys.add(key);
sensitiveBlocked.push(key);
continue;
}
}
process.env[key] = value;
}
const sorted = [...blockedKeys].sort();
if (sorted.length === 0) return { blockedKeys: [] };
console.error(`extra-env: blocked ${sorted.length} disallowed key override(s): ${sorted.join(", ")}`);
return { blockedKeys: sorted };
const allBlocked = [...prefixBlocked, ...sensitiveBlocked];
if (allBlocked.length === 0) return { blockedKeys: [] };
if (prefixBlocked.length > 0) {
console.error(`extra-env: blocked ${prefixBlocked.length} reserved-prefix key(s): ${prefixBlocked.join(", ")}`);
}
if (sensitiveBlocked.length > 0) {
console.error(`extra-env: blocked ${sensitiveBlocked.length} sensitive key override(s): ${sensitiveBlocked.join(", ")}`);
}
return { blockedKeys: allBlocked };
}

// src/index.ts
Expand Down
22 changes: 15 additions & 7 deletions multi-review/src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ const INLINE_CODE_RE = /`[^`\n]+`/g;
*/
/** @internal Exported for testing only — not a public API. */
export function escapeHashReferences(text: string): string {
if (!text || !HASH_NUM_RE.test(text)) return text;
HASH_NUM_RE.lastIndex = 0;
const segments: string[] = [];
let lastEnd = 0;
for (const m of text.matchAll(FENCED_CODE_RE)) {
Expand Down Expand Up @@ -613,7 +615,8 @@ export function parseExtraEnv(): ExtraEnvResult {
const allowSensitive = ["true", "1", "yes"].includes(
(process.env.MULTI_REVIEW_EXTRA_ENV_ALLOW_SENSITIVE || "false").trim().toLowerCase(),
);
const blockedKeys = new Set<string>();
const prefixBlocked: string[] = [];
const sensitiveBlocked: string[] = [];
for (const line of raw.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
Expand All @@ -624,22 +627,27 @@ export function parseExtraEnv(): ExtraEnvResult {
if (!key) continue;
if (key.startsWith("MULTI_REVIEW_")) {
console.log(`::error::extra-env key '${key}' starts with reserved prefix 'MULTI_REVIEW_' and is not allowed`);
blockedKeys.add(key);
prefixBlocked.push(key);
continue;
}
if (SENSITIVE_ENV_KEYS.has(key)) {
if (allowSensitive) {
console.log(`::warning::extra-env key '${key}' overrides a sensitive runtime variable (allowed by extra-env-allow-sensitive)`);
} else {
console.log(`::error::extra-env key '${key}' overrides a sensitive runtime variable; set extra-env-allow-sensitive to 'true' to allow`);
blockedKeys.add(key);
sensitiveBlocked.push(key);
continue;
}
}
process.env[key] = value;
}
const sorted = [...blockedKeys].sort();
if (sorted.length === 0) return { blockedKeys: [] };
console.error(`extra-env: blocked ${sorted.length} disallowed key override(s): ${sorted.join(", ")}`);
return { blockedKeys: sorted };
const allBlocked = [...prefixBlocked, ...sensitiveBlocked];
if (allBlocked.length === 0) return { blockedKeys: [] };
if (prefixBlocked.length > 0) {
console.error(`extra-env: blocked ${prefixBlocked.length} reserved-prefix key(s): ${prefixBlocked.join(", ")}`);
}
if (sensitiveBlocked.length > 0) {
console.error(`extra-env: blocked ${sensitiveBlocked.length} sensitive key override(s): ${sensitiveBlocked.join(", ")}`);
}
return { blockedKeys: allBlocked };
}
24 changes: 22 additions & 2 deletions tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,13 +758,13 @@ def test_extra_env_blocks_even_with_allow_sensitive_for_prefix(self):
self.assertIn("reserved prefix", result.stdout)

def test_extra_env_deduplicates_blocked_keys(self):
"""Duplicate sensitive keys should be deduplicated in summary."""
"""Duplicate sensitive keys are listed individually per occurrence."""
self.reset_env()
result = self.run_wrapper(
GITHUB_RUN_OPENCODE_EXTRA_ENV="MODEL=a\nMODEL=b",
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("blocked 1 disallowed key override(s): MODEL", result.stderr)
self.assertIn("sensitive key override(s)", result.stderr)

def test_extra_env_allow_sensitive_normalizes(self):
"""extra-env-allow-sensitive should accept '1' and 'yes'."""
Expand Down Expand Up @@ -1071,6 +1071,26 @@ def test_comma_separated(self):
self.assertIn("#\u200B1", result)
self.assertIn("#\u200B2", result)

def test_no_match_adjacent_letter(self):
result = self._run_escape("see #1abc")
self.assertNotIn("\u200B", result)

def test_no_match_html_attribute(self):
result = self._run_escape('<a href="#1">link</a>')
self.assertIn("#\u200B1", result)

def test_no_match_markdown_link(self):
result = self._run_escape("[text](#1)")
self.assertIn("#\u200B1", result)

def test_empty_string(self):
result = self._run_escape("")
self.assertEqual(result, "")

def test_no_hash_numbers(self):
result = self._run_escape("no references here")
self.assertEqual(result, "no references here")


class TestCrossLanguageHashInstructionConsistency(unittest.TestCase):
"""Verify that hash-avoidance instructions in TS and Python stay in sync.
Expand Down
Loading