Files
reverse-skill/.github/workflows/ci.yml
T
zhaoxuya520 a9ed8d04ad Merge pull request #93 from jhuang-tw/fix/client-neutral-bootstrap-capability
fix: make MCP bootstrap client-neutral by default

Owner integration: preserve all prior CI contracts, document explicit host selection on Windows, and make the Bash manifest regression UTF-8 deterministic.
2026-08-19 11:58:54 +08:00

334 lines
13 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# reverse-skill CI:路由回归 + 结构一致性 + 供应链 pin gate + 冒烟
# 矩阵:windows-latest(原生 powershell 5.1) + ubuntu-latest(pwsh + powershell shim)
# 触发:所有分支(含 fork 的改进分支),PR 也触发
name: CI
on:
push:
pull_request:
jobs:
routing-tests:
name: routing tests (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# 脚本内部以 `powershell` 调用子进程;Linux runner 只有 pwsh,做个 shim
- name: powershell shim (linux)
if: runner.os == 'Linux'
shell: bash
run: sudo ln -sf "$(command -v pwsh)" /usr/local/bin/powershell
- name: Routing regression (benchmark)
shell: pwsh
run: ./skills/scripts/test-routing.ps1
- name: Routing coherence + supply-chain pin gate
shell: pwsh
run: ./skills/scripts/verify-routing-coherence.ps1
- name: Bootstrap supply-chain regression
shell: pwsh
run: ./skills/scripts/test-bootstrap-supply-chain.ps1
- name: Bootstrap supply-chain regression (Windows PowerShell 5.1)
if: runner.os == 'Windows'
shell: powershell
run: ./skills/scripts/test-bootstrap-supply-chain.ps1
- name: Parse contracts (route-scope + IDA lock)
shell: pwsh
run: ./skills/scripts/test-parse-contracts.ps1
- name: Journal PR title safety
shell: pwsh
run: ./skills/scripts/test-workflow-title-safety.ps1
- name: Client-neutral bootstrap/discovery (Windows PowerShell 5.1)
if: runner.os == 'Windows'
shell: powershell
run: ./skills/scripts/test-client-neutral-bootstrap.ps1
- name: Offline sample case contract (Windows PowerShell 5.1)
if: runner.os == 'Windows'
shell: powershell
run: |
$scratch = Join-Path $env:RUNNER_TEMP ("reverse-skill-offline-" + [guid]::NewGuid().ToString('n'))
New-Item -ItemType Directory -Force -Path $scratch | Out-Null
$sample = Join-Path $scratch 'sample.apk'
Set-Content -Path $sample -Value 'fixture' -Encoding ASCII
./skills/scripts/case-init.ps1 `
-Hint "offline apk" `
-CaseName "offline-sample" `
-ProjectRoot $scratch `
-Preset offline-sample `
-Sample $sample
$scope = Join-Path $scratch 'work/offline-sample/scope.md'
$raw = Get-Content $scope -Raw
if ($raw -notmatch '(?m)^- mode: offline\r?$') { throw 'offline sample did not keep offline network mode' }
if ($raw -notmatch '(?m)^- ready_for_act: true\r?$') { throw 'offline sample did not become ready_for_act' }
./skills/scripts/case-guard.ps1 -CaseRoot (Join-Path $scratch 'work/offline-sample')
try {
./skills/scripts/case-init.ps1 `
-Hint "missing offline apk" `
-CaseName "missing-sample" `
-ProjectRoot $scratch `
-Preset offline-sample `
-Sample (Join-Path $scratch 'missing.apk')
throw 'missing offline sample was accepted'
} catch {
if ($_.Exception.Message -eq 'missing offline sample was accepted') { throw }
}
./skills/scripts/case-init.ps1 `
-Hint "pending offline apk" `
-CaseName "force-auth" `
-ProjectRoot $scratch `
-Sample $sample
& powershell -NoProfile -ExecutionPolicy Bypass -File ./skills/scripts/case-guard.ps1 `
-CaseRoot (Join-Path $scratch 'work/force-auth') `
-Force
$guardExit = $LASTEXITCODE
if ($guardExit -eq 0) { throw '-Force bypassed auth.status hard gate' }
$global:LASTEXITCODE = 0
- name: Smoke (verify + parse + quick route)
shell: pwsh
run: ./skills/scripts/smoke.ps1
- name: INDEX.md up-to-date check
shell: pwsh
run: ./skills/scripts/extract-summaries.ps1 -Check
- name: All JSON manifests valid
shell: pwsh
run: |
Get-Content skills/scripts/bootstrap-manifest.json -Raw -Encoding UTF8 | ConvertFrom-Json | Out-Null
Get-Content kali/scripts/bootstrap-manifest.json -Raw -Encoding UTF8 | ConvertFrom-Json | Out-Null
Get-Content skills/config/routing.json -Raw -Encoding UTF8 | ConvertFrom-Json | Out-Null
Get-Content skills/tests/routing-benchmark.json -Raw -Encoding UTF8 | ConvertFrom-Json | Out-Null
Write-Host "All JSON valid"
- name: Doc fact tables vs manifest (verify-doc-facts)
shell: pwsh
run: ./skills/scripts/verify-doc-facts.ps1
sh-syntax:
name: shell script syntax check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: bash -n all .sh
shell: bash
run: |
set -e
while IFS= read -r f; do
bash -n "$f"
echo "syntax OK: $f"
done < <(git ls-files '*.sh')
- name: Client-neutral bootstrap/discovery (Bash)
shell: bash
run: bash skills/scripts/test-client-neutral-bootstrap.sh
- name: Structured routing parity (Bash)
shell: bash
run: |
set -euo pipefail
bash skills/scripts/test-routing.sh
bash skills/scripts/test-bootstrap-manifest.sh
scratch="$(mktemp -d)"
trap 'rm -rf "$scratch"' EXIT
# Fresh Linux journey: no pwsh required, artifacts stay in caller project.
caller="$scratch/caller-project"
mkdir -p "$caller"
printf 'fixture' > "$scratch/sample.apk"
(
cd "$caller"
bash "$GITHUB_WORKSPACE/skills/scripts/master-route.sh" --hint "offline apk"
bash "$GITHUB_WORKSPACE/skills/scripts/case-init.sh" \
--hint "offline apk" \
--case-name "caller-default" \
--preset offline-sample \
--sample "$scratch/sample.apk"
)
test -f "$caller/work/caller-default/scope.md"
grep -Eq '^- project_root: .*/caller-project$' "$caller/work/caller-default/scope.md"
grep -Eq '^- mode: offline$' "$caller/work/caller-default/scope.md"
grep -Eq '^- ready_for_act: true$' "$caller/work/caller-default/scope.md"
bash skills/scripts/case-guard.sh --case-root "$caller/work/caller-default"
test ! -e "$GITHUB_WORKSPACE/work/caller-default"
if bash skills/scripts/case-init.sh \
--hint "missing offline apk" \
--case-name "missing-sample" \
--project-root "$caller" \
--preset offline-sample \
--sample "$scratch/missing.apk"; then
echo "missing offline sample was accepted" >&2
exit 1
fi
# Compatibility: legacy --package-root still pins the work root.
bash skills/scripts/case-init.sh \
--hint "authorized web review" \
--case-name "network-default" \
--package-root "$scratch/project" \
--auth-granted \
--target-url "https://example.test/"
grep -Eq '^- mode: authorized_target_only$' "$scratch/project/work/network-default/scope.md"
grep -Eq '^- ready_for_act: true$' "$scratch/project/work/network-default/scope.md"
bash skills/scripts/case-guard.sh --case-root "$scratch/project/work/network-default"
if bash skills/scripts/case-init.sh \
--hint "offline apk" \
--case-name "../case-escape" \
--package-root "$scratch/project" \
--preset offline-sample \
--sample "$scratch/sample.apk"; then
echo "case-init accepted an unsafe case name" >&2
exit 1
fi
if bash skills/scripts/case-init.sh \
--hint "authorized web review" \
--case-name "invalid-network" \
--package-root "$scratch/project" \
--auth-granted \
--network-profile "internet" \
--target-url "https://example.test/"; then
echo "case-init accepted an unsupported network profile" >&2
exit 1
fi
bash skills/scripts/case-init.sh \
--hint "authorized web review" \
--case-name "uppercase-network" \
--package-root "$scratch/project" \
--auth-granted \
--network-profile "AUTHORIZED_TARGET_ONLY" \
--target-url "https://example.test/"
grep -Eq '^- mode: authorized_target_only$' "$scratch/project/work/uppercase-network/scope.md"
bash skills/scripts/case-init.sh \
--hint "pending review" \
--case-name "guard-section" \
--package-root "$scratch/project" \
--target-url "https://example.test/"
cat >> "$scratch/project/work/guard-section/scope.md" <<'FAKE_FIELDS'
## notes
- status: granted
- mode: authorized_target_only
- ready_for_act: true
FAKE_FIELDS
if bash skills/scripts/case-guard.sh --case-root "$scratch/project/work/guard-section"; then
echo "case-guard accepted fields outside their contract sections" >&2
exit 1
fi
# --force is compatibility-only and must not bypass the hard auth gate.
(
cd "$caller"
bash "$GITHUB_WORKSPACE/skills/scripts/case-init.sh" \
--hint "pending offline apk" \
--case-name "force-auth" \
--sample "$scratch/sample.apk"
)
if bash skills/scripts/case-guard.sh --case-root "$caller/work/force-auth" --force; then
echo "case-guard --force bypassed auth.status hard gate" >&2
exit 1
fi
- name: PowerShell syntax check (PSParser, all tracked .ps1)
shell: pwsh
run: |
$files = git ls-files '*.ps1'
if (-not $files) { Write-Host 'No .ps1 files tracked'; exit 0 }
$failed = 0
foreach ($f in $files) {
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $f), [ref]$tokens, [ref]$errors) | Out-Null
if ($errors -and $errors.Count -gt 0) {
$failed++
Write-Host "::error file=$f::$($errors.Count) parse error(s)"
foreach ($e in $errors) {
Write-Host (" L{0}:{1} {2}" -f $e.Extent.StartLineNumber, $e.Extent.StartColumnNumber, $e.Message)
}
} else {
Write-Host "syntax OK: $f"
}
}
if ($failed -gt 0) { exit 1 }
- name: Non-ASCII .ps1 must carry a UTF-8 BOM (Windows PowerShell 5.1 safety)
shell: pwsh
run: |
# PS 5.1 parses a BOM-less .ps1 as the system ANSI codepage, garbling
# Chinese/em-dash string literals. Every non-ASCII script MUST have a BOM.
$files = git ls-files '*.ps1'
if (-not $files) { Write-Host 'No .ps1 files tracked'; exit 0 }
$bad = 0
foreach ($f in $files) {
$bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $f))
$hasNonAscii = $false
foreach ($b in $bytes) { if ($b -ge 0x80) { $hasNonAscii = $true; break } }
if (-not $hasNonAscii) { continue }
$hasBom = $bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF
if ($hasBom) {
Write-Host "BOM OK: $f"
} else {
$bad++
Write-Host "::error file=$f::non-ASCII .ps1 lacks a UTF-8 BOM; add one so literals survive Windows PowerShell 5.1"
}
}
if ($bad -gt 0) { Write-Host "$bad file(s) need a UTF-8 BOM"; exit 1 }
Write-Host 'All non-ASCII .ps1 carry a UTF-8 BOM'
leak-scan:
name: field-journal leak scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Scan field-journal for un-anonymized secrets
shell: pwsh
run: ./skills/scripts/scan-leaks.ps1 -Path skills/field-journal
case-contract:
name: case contract test (ctf-demo)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Review examples/ctf-demo under strict contract
shell: bash
run: python3 skills/case-review/scripts/review_case.py examples/ctf-demo --verify-hashes --strict
version-check:
name: version consistency
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: VERSION matches latest CHANGELOG release
shell: pwsh
run: |
$v = (Get-Content VERSION -Raw).Trim()
$cl = Get-Content CHANGELOG.md -Raw
$m = [regex]::Match($cl, '(?m)^## \[(\d+\.\d+\.\d+)\]')
if (-not $m.Success) { Write-Error 'No version header found in CHANGELOG'; exit 1 }
$latest = $m.Groups[1].Value
if ($v -ne $latest) {
Write-Error "VERSION ($v) does not match latest CHANGELOG release ($latest)"
exit 1
}
Write-Host "Version OK: $v"