Skip to content
Closed
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
66 changes: 66 additions & 0 deletions .github/workflows/publication-lockfile-blob.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Prepare Vitest security-patch objects

on:
push:
branches: [codex/publication-review-20260914]
paths: [.github/workflows/publication-lockfile-blob.yml]

permissions: {}

jobs:
prepare:
if: github.repository == 'azumag/opencode-rate-limit-fallback' && github.ref == 'refs/heads/codex/publication-review-20260914'
runs-on: ubuntu-latest
timeout-minutes: 3
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: aaad1fa88f102dffb40846c5024bb07a7a7ee85b
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
package-manager-cache: false
- name: Resolve only the fixed Vitest patch without running package scripts
run: |
cp package.json "$RUNNER_TEMP/original-package.json"
cp package-lock.json "$RUNNER_TEMP/original-lock.json"
npm install --package-lock-only --ignore-scripts --save-exact --save-dev vitest@4.1.11 @vitest/coverage-v8@4.1.11
npm audit --json > "$RUNNER_TEMP/fixed-audit.json"
- name: Verify change boundaries and create blobs only; no commits or refs
env:
GH_TOKEN: ${{ github.token }}
run: |
python3 - <<'PY'
import copy, json, os, pathlib, urllib.request
temp = pathlib.Path(os.environ['RUNNER_TEMP'])
original = json.loads((temp / 'original-package.json').read_text())
current = json.loads(pathlib.Path('package.json').read_text())
expected = copy.deepcopy(original)
for name in ('vitest', '@vitest/coverage-v8'):
assert original['devDependencies'][name] == '^4.1.10'
expected['devDependencies'][name] = '4.1.11'
assert current == expected, 'Only the two Vitest dependency pins may change'
old_lock = json.loads((temp / 'original-lock.json').read_text())
new_lock = json.loads(pathlib.Path('package-lock.json').read_text())
assert {k: v for k, v in old_lock.items() if k != 'packages'} == {k: v for k, v in new_lock.items() if k != 'packages'}
before = old_lock['packages']
after = new_lock['packages']
changed = [p for p in sorted(set(before) | set(after)) if before.get(p) != after.get(p)]
assert all(p == '' or p == 'node_modules/vitest' or p.startswith('node_modules/@vitest/') for p in changed), 'Unrelated dependency updates are forbidden'
for name in ('vitest', '@vitest/coverage-v8', '@vitest/mocker'):
assert after['node_modules/' + name]['version'] == '4.1.11'
audit = json.loads((temp / 'fixed-audit.json').read_text())
assert audit['metadata']['vulnerabilities']['total'] == 0 and 'error' not in audit
print('Changed lockfile entries: ' + json.dumps(changed))
print('Full npm audit: zero vulnerabilities')
root = 'https://api.github.com/repos/azumag/opencode-rate-limit-fallback/git/blobs'
for name in ('package.json', 'package-lock.json'):
payload = json.dumps({'content': pathlib.Path(name).read_text(), 'encoding': 'utf-8'}).encode()
request = urllib.request.Request(root, data=payload, headers={'Authorization': 'Bearer ' + os.environ['GH_TOKEN'], 'Accept': 'application/vnd.github+json', 'Content-Type': 'application/json', 'User-Agent': 'publication-patch-review'})
with urllib.request.urlopen(request, timeout=30) as response:
result = json.load(response)
print(name + ' verified blob: ' + result['sha'])
PY
232 changes: 232 additions & 0 deletions .github/workflows/publication-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
name: Publication review

on:
workflow_dispatch:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false

- name: Fetch public pull-request heads for history review
run: git fetch --no-tags origin '+refs/pull/*/head:refs/audit/pull/*'

- name: Install checksum-pinned Gitleaks
shell: bash
run: |
set -euo pipefail
curl --fail --silent --show-error --location --retry 2 \
https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz \
-o "$RUNNER_TEMP/gitleaks.tar.gz"
echo "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb $RUNNER_TEMP/gitleaks.tar.gz" | sha256sum --check --status
mkdir -p "$RUNNER_TEMP/gitleaks-bin" "$RUNNER_TEMP/publication-snapshot"
tar -xzf "$RUNNER_TEMP/gitleaks.tar.gz" -C "$RUNNER_TEMP/gitleaks-bin" gitleaks
echo "$RUNNER_TEMP/gitleaks-bin" >> "$GITHUB_PATH"
git archive HEAD | tar -x -C "$RUNNER_TEMP/publication-snapshot"

- name: Scan reachable history and current tracked files without printing secrets
shell: bash
run: |
set -euo pipefail
printf '[extend]\nuseDefault = true\n' > "$RUNNER_TEMP/gitleaks-default.toml"
touch "$RUNNER_TEMP/empty-gitleaksignore"
set +e
gitleaks git . --log-opts=--all --redact=100 --no-banner --no-color \
--config "$RUNNER_TEMP/gitleaks-default.toml" \
--gitleaks-ignore-path "$RUNNER_TEMP/empty-gitleaksignore" --ignore-gitleaks-allow \
--max-decode-depth=3 --max-archive-depth=2 --exit-code=10 \
--report-format=json --report-path="$RUNNER_TEMP/history-secrets.json" \
> "$RUNNER_TEMP/history-scanner.log" 2>&1
echo "$?" > "$RUNNER_TEMP/history-exit"
gitleaks dir "$RUNNER_TEMP/publication-snapshot" --redact=100 --no-banner --no-color \
--config "$RUNNER_TEMP/gitleaks-default.toml" \
--gitleaks-ignore-path "$RUNNER_TEMP/empty-gitleaksignore" --ignore-gitleaks-allow \
--max-decode-depth=3 --max-archive-depth=2 --exit-code=10 \
--report-format=json --report-path="$RUNNER_TEMP/current-secrets.json" \
> "$RUNNER_TEMP/current-scanner.log" 2>&1
echo "$?" > "$RUNNER_TEMP/current-exit"
exit 0

- name: Produce value-free secret and personal-data review summary
shell: bash
run: |
python3 - <<'PY'
import hashlib, json, os, pathlib, re, subprocess
temp = pathlib.Path(os.environ['RUNNER_TEMP'])
def git(*args):
return subprocess.check_output(['git', *args])
summary = {
'head': git('rev-parse', 'HEAD').decode().strip(),
'reachable_commits': int(git('rev-list', '--all', '--count')),
'refs': git('for-each-ref', '--format=%(refname)').decode().splitlines(),
'tracked_files': git('ls-files', '-z').decode().rstrip('\0').split('\0'),
'secret_scanner': 'gitleaks 8.30.1; built-in rules; no baseline or local allowlist',
'scope_exclusions': ['unreachable/deleted Git objects', 'issue and PR discussion bodies', 'Actions logs and unrelated artifacts', 'already-published npm tarballs', 'local untracked files', 'GitHub secret settings'],
}
scan_ok = True
for scope in ('history', 'current'):
status = int((temp / f'{scope}-exit').read_text())
report = temp / f'{scope}-secrets.json'
findings = json.loads(report.read_text()) if report.exists() else None
summary[scope + '_secrets'] = {
'exit_code': status,
'count': len(findings) if isinstance(findings, list) else None,
'findings': [{k: f.get(k) for k in ('RuleID', 'File', 'StartLine', 'Commit')} for f in (findings or [])],
}
scan_ok = scan_ok and status == 0 and isinstance(findings, list) and not findings
patterns = {
'email': re.compile(r'[A-Za-z0-9_.+%-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'),
'personal_home_path': re.compile(r'/(?:Users|home)/[A-Za-z0-9_.-]+/'),
'private_ipv4': re.compile(r'(?<![\d.])(?:10\.(?:\d{1,3}\.){2}\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3})(?![\d.])'),
'japanese_mobile_number': re.compile(r'(?<!\d)0[789]0[- ]\d{4}[- ]\d{4}(?!\d)'),
}
def real_email(value):
domain = value.rsplit('@', 1)[-1].lower()
return not (domain == 'example.com' or domain.endswith('.example.com') or domain in ('example.org', 'example.net', 'test.com', 'test.org', 'localhost.localdomain', 'users.noreply.github.com', 'noreply.github.com') or value.lower() in ('noreply@github.com', 'noreply@anthropic.com'))
objects = {}
for line in git('rev-list', '--objects', '--all').decode().splitlines():
oid, _, path = line.partition(' ')
objects[oid] = path
types = git('cat-file', '--batch-check=%(objectname) %(objecttype)', '--batch-all-objects').decode().splitlines()
blobs = [(line.split()[0], objects[line.split()[0]]) for line in types if line.split()[1] == 'blob' and line.split()[0] in objects]
current_blobs = set()
for line in git('ls-tree', '-r', 'HEAD').decode().splitlines():
current_blobs.add(line.split()[2])
findings = []
for oid, path in blobs:
data = git('cat-file', 'blob', oid).decode('utf-8', errors='replace')
for rule, regex in patterns.items():
matches = [m for m in regex.finditer(data) if rule != 'email' or real_email(m.group())]
if matches:
item = {'rule': rule, 'path': path, 'blob': oid, 'current': oid in current_blobs, 'lines': sorted(set(data.count('\n', 0, m.start()) + 1 for m in matches))}
if rule == 'personal_home_path':
item['path_categories'] = sorted(set('runner' if m.group().startswith('/home/' + 'runner/') else 'generic-test' if m.group() in ('/home/' + 'user/', '/Users/' + 'user/') else 'other-local-user' for m in matches))
findings.append(item)
summary['reachable_blobs_scanned_for_personal_data'] = len(blobs)
summary['personal_data_candidates'] = findings
author_emails = {}
for row in git('log', '--all', '--format=%H%x09%ae%x09%ce').decode().splitlines():
commit, author, committer = row.split('\t')
for email in (author, committer):
if real_email(email):
author_emails.setdefault(hashlib.sha256(email.encode()).hexdigest()[:12], commit)
summary['non_noreply_commit_email_candidates'] = [{'email_fingerprint': key, 'example_commit': value} for key, value in author_emails.items()]
session_urls = []
for commit in git('rev-list', '--all').decode().splitlines():
message = git('show', '-s', '--format=%B', commit).decode()
if re.search(r'https://(?:claude\.ai/code/session_|chatgpt\.com/(?:c/|codex/)|chat\.openai\.com/c/)', message):
session_urls.append(commit)
summary['commits_with_coding_session_url_candidates'] = session_urls
summary['automated_secret_checks_passed'] = scan_ok
(temp / 'publication-review-summary.json').write_text(json.dumps(summary, indent=2, ensure_ascii=False) + '\n')
print(json.dumps(summary, indent=2, ensure_ascii=False))
if not scan_ok:
raise SystemExit('Secret checks did not pass. No source snapshot will be uploaded; inspect the listed locations privately.')
PY

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
cache: 'npm'

- name: Install locked development dependencies without lifecycle scripts
run: npm ci --ignore-scripts

- name: Typecheck, test, and build
run: |
npm run typecheck
npm test
npm run build

- name: Verify package contents, documentation links, and dependency audit
shell: bash
run: |
set -euo pipefail
npm pack --dry-run --ignore-scripts --json > "$RUNNER_TEMP/package-preview.json"
set +e
npm audit --json > "$RUNNER_TEMP/npm-audit.json" 2> "$RUNNER_TEMP/npm-audit.log"
echo "$?" > "$RUNNER_TEMP/npm-audit-exit"
npm audit --omit=dev --json > "$RUNNER_TEMP/npm-audit-production.json" 2> "$RUNNER_TEMP/npm-audit-production.log"
echo "$?" > "$RUNNER_TEMP/npm-audit-production-exit"
npm view @azumag/opencode-rate-limit-fallback dist-tags --json > "$RUNNER_TEMP/npm-dist-tags.json" 2> "$RUNNER_TEMP/npm-dist-tags.log"
echo "$?" > "$RUNNER_TEMP/npm-dist-tags-exit"
set -e
python3 - <<'PYCODE'
import json, os, pathlib, re
temp = pathlib.Path(os.environ['RUNNER_TEMP'])
preview = json.loads((temp / 'package-preview.json').read_text())[0]
paths = [f['path'] for f in preview['files']]
required = {'README.md', 'README.ja.md', 'docs/v1-reference.md', 'docs/quota-wait-mode.md', 'LICENSE', 'package.json', 'dist/v2.js', 'dist/v2.d.ts'}
assert required.issubset(paths), 'Missing distribution files'
assert all(p.startswith('dist/') or p in required for p in paths), 'Unexpected distribution files'
assert not any(p.endswith(('.map', '.json', '.log', '.pem', '.key')) and p != 'package.json' for p in paths), 'Unexpected sensitive/generated package file'
doc_files = ['README.md', 'README.ja.md', 'docs/v1-reference.md', 'docs/quota-wait-mode.md']
for file in doc_files:
text = pathlib.Path(file).read_text()
for target in re.findall(r'\[[^\]]+\]\(([^)]+)\)', text):
if '://' not in target and not target.startswith('#'):
assert (pathlib.Path(file).parent / target.split('#')[0]).is_file(), f'Broken local documentation link in {file}'
# Both top-level languages must use exactly the same runnable snippets.
code_blocks = lambda file: re.findall(r'```(?:json|sh)\n(.*?)```', pathlib.Path(file).read_text(), flags=re.S)
assert code_blocks('README.md') == code_blocks('README.ja.md'), 'Language examples diverged'
audit = {}
audit_ok = True
for name in ('npm-audit', 'npm-audit-production'):
status = int((temp / (name + '-exit')).read_text())
raw = json.loads((temp / (name + '.json')).read_text())
counts = raw.get('metadata', {}).get('vulnerabilities')
details = []
for package, item in raw.get('vulnerabilities', {}).items():
details.append({
'package': package, 'severity': item.get('severity'),
'is_direct': item.get('isDirect'), 'range': item.get('range'),
'fix_available': item.get('fixAvailable'),
'via': [value if isinstance(value, str) else {key: value.get(key) for key in ('name', 'title', 'url', 'severity', 'range')} for value in item.get('via', [])],
})
audit[name] = {'exit_code': status, 'vulnerabilities': counts, 'has_error': 'error' in raw, 'details': details}
audit_ok = audit_ok and status == 0 and isinstance(counts, dict) and counts.get('total') == 0 and 'error' not in raw
tags = None
if int((temp / 'npm-dist-tags-exit').read_text()) == 0:
tags = json.loads((temp / 'npm-dist-tags.json').read_text())
result = {'package': preview['name'], 'version': preview['version'], 'file_count': len(paths), 'files': paths, 'documentation_links': 'passed', 'bilingual_examples': 'identical', 'dependency_audit': audit, 'published_dist_tags_at_review': tags}
(temp / 'publication-package-summary.json').write_text(json.dumps(result, indent=2) + '\n')
print(json.dumps(result, indent=2))
if not audit_ok:
raise SystemExit('Dependency audit did not pass; inspect the value-free summary. No publish has occurred.')
PYCODE

- name: Archive only the scanned current source, never Git history or credentials
run: tar -czf "$RUNNER_TEMP/publication-source.tar.gz" -C "$RUNNER_TEMP/publication-snapshot" .

- name: Upload sanitized summary and scanned source for review
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: publication-review
path: |
${{ runner.temp }}/publication-review-summary.json
${{ runner.temp }}/publication-source.tar.gz
${{ runner.temp }}/publication-package-summary.json
retention-days: 3
if-no-files-found: error

- name: Preserve value-free findings even when a validation gate fails
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: publication-review-summary
path: |
${{ runner.temp }}/publication-review-summary.json
${{ runner.temp }}/publication-package-summary.json
retention-days: 3
if-no-files-found: warn
22 changes: 21 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,24 @@ bun.lock
.DS_Store
dist/
tmp/*
coverage/
coverage/
*.tsbuildinfo

# Local credentials and machine-specific configuration.
.env
.env.*
!.env.example
!.env.sample
.npmrc
auth.json
*.pem
*.key
.opencode/auth.json
rate-limit-fallback.json
rate-limit-fallback-health.json
.opencode/rate-limit-fallback.json
.opencode/rate-limit-fallback-health.json

# Generated diagnostic data can contain local paths or session metadata.
test-results*.json
publication-review-summary.json
Loading
Loading