Skip to content

Commit 9f71e5a

Browse files
committed
Fix: harden action and complete repo validation
1 parent 9a31e0a commit 9f71e5a

83 files changed

Lines changed: 1870 additions & 45 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/workflows/ci.yml

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,44 @@ jobs:
6060
echo "$OUTPUT"
6161
COVERAGE=$(echo "$OUTPUT" | grep -oP '[\d.]+% coverage' | grep -oP '[\d.]+')
6262
echo "Coverage: ${COVERAGE}%"
63-
if (( $(echo "$COVERAGE < 40" | bc -l) )); then
64-
echo "::error::Coverage ${COVERAGE}% is below 40% threshold"
63+
if (( $(echo "$COVERAGE < 60" | bc -l) )); then
64+
echo "::error::Coverage ${COVERAGE}% is below 60% threshold"
6565
exit 1
6666
fi
6767
68+
site:
69+
runs-on: ubuntu-latest
70+
steps:
71+
- uses: actions/checkout@v4
72+
- uses: oven-sh/setup-bun@v2
73+
- name: Install dependencies
74+
working-directory: site
75+
run: bun install --frozen-lockfile
76+
- name: Run site tests
77+
working-directory: site
78+
run: bun test
79+
- name: Lint site
80+
working-directory: site
81+
run: bun run lint
82+
- name: Build site
83+
working-directory: site
84+
run: bun run build
85+
86+
vscode-extension:
87+
runs-on: ubuntu-latest
88+
steps:
89+
- uses: actions/checkout@v4
90+
- uses: oven-sh/setup-bun@v2
91+
- name: Install dependencies
92+
working-directory: vscode-extension
93+
run: bun install --frozen-lockfile
94+
- name: Compile extension
95+
working-directory: vscode-extension
96+
run: bun run compile
97+
- name: Package extension
98+
working-directory: vscode-extension
99+
run: bun run package
100+
68101
validate-action:
69102
runs-on: ubuntu-latest
70103
steps:
@@ -94,12 +127,12 @@ jobs:
94127
echo 'EOF'
95128
} >> "$GITHUB_OUTPUT"
96129
# Fail the step if check fails
97-
cargo run -- check --strict
130+
cargo run -- check --strict --require-coverage 100 --force
98131
99132
corvid-pet:
100133
runs-on: ubuntu-latest
101134
if: github.event_name == 'pull_request' && always()
102-
needs: [test, fmt, validate-action, spec-check, audit, coverage]
135+
needs: [test, fmt, validate-action, spec-check, audit, coverage, site, vscode-extension]
103136
steps:
104137
- uses: actions/checkout@v4
105138

@@ -112,6 +145,8 @@ jobs:
112145
CHECK_SPEC: "Spec Validation=${{ needs.spec-check.result }}"
113146
CHECK_AUDIT: "Dependency Audit=${{ needs.audit.result }}"
114147
CHECK_COVERAGE: "Code Coverage=${{ needs.coverage.result }}"
148+
CHECK_SITE: "Docs Site=${{ needs.site.result }}"
149+
CHECK_VSCODE: "VS Code Extension=${{ needs.vscode-extension.result }}"
115150
REPORT_SPEC: ${{ needs.spec-check.outputs.body }}
116151
run: |
117152
OVERALL="success"

.specsync/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
specs_dir = "specs"
55
source_dirs = ["src"]
66
exclude_dirs = ["__tests__"]
7-
exclude_patterns = ["**/__tests__/**", "**/*.test.ts", "**/*.spec.ts"]
7+
exclude_patterns = ["**/__tests__/**", "**/*.test.ts", "**/*.spec.ts", "**/tests.rs"]
88
required_sections = ["Purpose", "Public API", "Invariants", "Behavioral Examples", "Error Cases", "Dependencies", "Change Log"]
99
enforcement = "strict"
1010
ai_command = "claude -p --output-format text"

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [4.3.4] - 2026-06-07
11+
12+
### Security
13+
14+
- **GitHub Action command execution hardened** — marketplace action now builds `specsync` invocations as bash argv arrays instead of shell strings, eliminating `eval` around user-provided `args`.
15+
- **Release checksum verification fails closed** — downloaded release archives now require matching `.sha256` files before extraction.
16+
17+
### Fixed
18+
19+
- **Action input validation**`require-coverage` is validated as an integer from 0 to 100 before command execution.
20+
- **MCP generated-spec test assertion** — replaced a tautological unsigned comparison with a meaningful generated-spec count assertion.
21+
- **VS Code extension license packaging** — extension package includes the MIT license file so VSIX builds are complete.
22+
23+
### CI
24+
25+
- **Repo-wide validation expanded** — CI now builds/tests/lints the Astro docs site and compiles/packages the VS Code extension.
26+
- **Spec gate requires full coverage** — project spec CI now runs `check --strict --require-coverage 100 --force`.
27+
- **Coverage threshold raised** — tarpaulin minimum coverage increased from 40% to 60%.
28+
- **Fledge tasks expanded** — repository lanes now cover Rust, specs, docs, extension packaging, and audit checks.
29+
- **Known transitive audit warning tracked**`RUSTSEC-2024-0384` is ignored explicitly while it remains pulled in through `notify`.
30+
31+
### Specs
32+
33+
- **Utility helpers specced** — added a dedicated spec for `src/util.rs`.
34+
- **Companion files completed** — backfilled `testing.md` companions and missing `tasks.md`/`context.md` files for legacy specs.
35+
1036
## [v4.3.3] - 2026-05-18
1137

1238
### Fixed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "specsync"
3-
version = "4.3.3"
3+
version = "4.3.4"
44
edition = "2024"
55
rust-version = "1.88"
66
description = "Bidirectional spec-to-code validation with schema column checking — 11 languages, single binary"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ Available on the [GitHub Marketplace](https://github.com/marketplace/actions/spe
610610
| `strict` | `false` | Treat warnings as errors |
611611
| `require-coverage` | `0` | Minimum file coverage % |
612612
| `root` | `.` | Project root directory |
613-
| `args` | `''` | Extra CLI arguments |
613+
| `args` | `''` | Extra whitespace-separated CLI arguments; shell quoting is not supported |
614614
| `comment` | `false` | Post spec drift results as a PR comment (requires `pull_request` event) |
615615
| `token` | `${{ github.token }}` | GitHub token for posting PR comments (needs write permissions) |
616616

action.yml

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ inputs:
2222
required: false
2323
default: '.'
2424
args:
25-
description: 'Additional arguments to pass to specsync check'
25+
description: 'Additional whitespace-separated arguments to pass to specsync check (shell quoting is not supported)'
2626
required: false
2727
default: ''
2828
lifecycle-enforce:
@@ -87,34 +87,30 @@ runs:
8787
ARCHIVE="specsync-${OS}-${ARCH}.exe.zip"
8888
curl -fsSL "${BASE_URL}/${ARCHIVE}" -o "${INSTALL_DIR}/specsync.zip"
8989
90-
# Verify checksum if available
91-
if curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/specsync.sha256" 2>/dev/null; then
92-
echo "Verifying checksum..."
93-
cd "$INSTALL_DIR"
94-
EXPECTED=$(awk '{print $1}' specsync.sha256)
95-
ACTUAL=$(shasum -a 256 specsync.zip | awk '{print $1}')
96-
if [ "$EXPECTED" != "$ACTUAL" ]; then
97-
echo "::error::Checksum verification failed! Expected: $EXPECTED, Got: $ACTUAL"
98-
exit 1
99-
fi
100-
echo "::notice::Checksum verified"
101-
cd -
90+
curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/specsync.sha256"
91+
echo "Verifying checksum..."
92+
cd "$INSTALL_DIR"
93+
EXPECTED=$(awk '{print $1}' specsync.sha256)
94+
ACTUAL=$(shasum -a 256 specsync.zip | awk '{print $1}')
95+
if [ "$EXPECTED" != "$ACTUAL" ]; then
96+
echo "::error::Checksum verification failed! Expected: $EXPECTED, Got: $ACTUAL"
97+
exit 1
10298
fi
99+
echo "::notice::Checksum verified"
100+
cd -
103101
104102
unzip -o "${INSTALL_DIR}/specsync.zip" -d "$INSTALL_DIR"
105103
mv "${INSTALL_DIR}/specsync-${OS}-${ARCH}.exe" "${INSTALL_DIR}/specsync.exe"
106104
else
107105
ARCHIVE="specsync-${OS}-${ARCH}.tar.gz"
108106
curl -fsSL "${BASE_URL}/${ARCHIVE}" -o "${INSTALL_DIR}/${ARCHIVE}"
109107
110-
# Verify checksum if available
111-
if curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/${ARCHIVE}.sha256" 2>/dev/null; then
112-
echo "Verifying checksum..."
113-
cd "$INSTALL_DIR"
114-
shasum -a 256 -c "${ARCHIVE}.sha256"
115-
echo "::notice::Checksum verified"
116-
cd -
117-
fi
108+
curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/${ARCHIVE}.sha256"
109+
echo "Verifying checksum..."
110+
cd "$INSTALL_DIR"
111+
shasum -a 256 -c "${ARCHIVE}.sha256"
112+
echo "::notice::Checksum verified"
113+
cd -
118114
119115
tar xz -C "$INSTALL_DIR" -f "${INSTALL_DIR}/${ARCHIVE}"
120116
mv "${INSTALL_DIR}/specsync-${OS}-${ARCH}" "${INSTALL_DIR}/specsync"
@@ -138,26 +134,36 @@ runs:
138134
run: |
139135
set -euo pipefail
140136
137+
if ! [[ "$INPUT_REQUIRE_COVERAGE" =~ ^[0-9]+$ ]] || [ "$INPUT_REQUIRE_COVERAGE" -gt 100 ]; then
138+
echo "::error::require-coverage must be an integer from 0 to 100"
139+
exit 1
140+
fi
141+
141142
# Always use --force in CI — hash cache is not committed, so
142143
# every CI run validates all specs from scratch.
143-
CMD="specsync check --force"
144+
CMD=(specsync check --force)
144145
145146
if [ "$INPUT_STRICT" = "true" ]; then
146-
CMD="$CMD --strict"
147+
CMD+=(--strict)
147148
fi
148149
149150
if [ "$INPUT_REQUIRE_COVERAGE" != "0" ]; then
150-
CMD="$CMD --require-coverage $INPUT_REQUIRE_COVERAGE"
151+
CMD+=(--require-coverage "$INPUT_REQUIRE_COVERAGE")
151152
fi
152153
153154
if [ -n "$INPUT_ARGS" ]; then
154-
CMD="$CMD $INPUT_ARGS"
155+
# INPUT_ARGS is intentionally split on whitespace only. Shell quoting,
156+
# substitutions, pipes, and other shell syntax are not evaluated.
157+
read -r -a EXTRA_ARGS <<< "$INPUT_ARGS"
158+
CMD+=("${EXTRA_ARGS[@]}")
155159
fi
156160
157161
echo "::group::SpecSync Check"
158-
echo "Running: $CMD"
162+
printf 'Running:'
163+
printf ' %q' "${CMD[@]}"
164+
printf '\n'
159165
EXIT_CODE=0
160-
eval "$CMD" || EXIT_CODE=$?
166+
"${CMD[@]}" || EXIT_CODE=$?
161167
echo "::endgroup::"
162168
163169
# If lifecycle enforcement is enabled, run it (may override exit code)
@@ -171,14 +177,14 @@ runs:
171177
# Uses the same `specsync comment` pipeline as our own CI workflow
172178
# for identical output between the marketplace action and direct usage.
173179
if [ "$INPUT_COMMENT" = "true" ]; then
174-
COMMENT_CMD="specsync comment"
180+
COMMENT_CMD=(specsync comment)
175181
if [ "$INPUT_STRICT" = "true" ]; then
176-
COMMENT_CMD="$COMMENT_CMD --strict"
182+
COMMENT_CMD+=(--strict)
177183
fi
178184
if [ "$INPUT_REQUIRE_COVERAGE" != "0" ]; then
179-
COMMENT_CMD="$COMMENT_CMD --require-coverage $INPUT_REQUIRE_COVERAGE"
185+
COMMENT_CMD+=(--require-coverage "$INPUT_REQUIRE_COVERAGE")
180186
fi
181-
COMMENT_OUTPUT=$(eval "$COMMENT_CMD" 2>/dev/null) || true
187+
COMMENT_OUTPUT=$("${COMMENT_CMD[@]}" 2>/dev/null) || true
182188
{
183189
echo "SPECSYNC_MARKDOWN<<SPECSYNC_EOF"
184190
echo "$COMMENT_OUTPUT"

fledge.toml

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,56 @@ cmd = "cargo clippy -- -D warnings"
1111
cmd = "cargo fmt --check"
1212

1313
[tasks.audit]
14-
cmd = "cargo audit"
14+
cmd = "cargo audit --ignore RUSTSEC-2024-0384"
1515

1616
[tasks.check-types]
1717
cmd = "cargo check"
1818

19+
[tasks.spec-check]
20+
cmd = "cargo run -- check --strict --require-coverage 100 --force"
21+
22+
[tasks.docs-test]
23+
cmd = "cd site && bun test"
24+
25+
[tasks.docs-lint]
26+
cmd = "cd site && bun run lint"
27+
28+
[tasks.docs-build]
29+
cmd = "cd site && bun run build"
30+
31+
[tasks.vscode-compile]
32+
cmd = "cd vscode-extension && bun run compile"
33+
34+
[tasks.vscode-package]
35+
cmd = "cd vscode-extension && bun run package"
36+
1937
[lanes.check]
2038
description = "Quick pre-commit check"
2139
steps = [{ parallel = ["fmt", "lint"] }, "test"]
2240

2341
[lanes.ci]
2442
description = "Full CI pipeline"
25-
steps = ["fmt", "lint", "test", "build"]
43+
steps = [
44+
"fmt",
45+
"lint",
46+
"test",
47+
"build",
48+
"audit",
49+
"spec-check",
50+
{ parallel = ["docs-test", "docs-lint", "docs-build", "vscode-compile", "vscode-package"] },
51+
]
2652

2753
[lanes.pre-commit]
2854
description = "Fast gate before committing"
2955
steps = [{ parallel = ["fmt", "lint"] }, "check-types"]
56+
57+
[lanes.repo]
58+
description = "Full repository validation, including docs and editor extension"
59+
steps = [
60+
{ parallel = ["fmt", "lint", "check-types"] },
61+
"test",
62+
"build",
63+
"audit",
64+
"spec-check",
65+
{ parallel = ["docs-test", "docs-lint", "docs-build", "vscode-compile", "vscode-package"] },
66+
]

site/src/content/docs/integrations/github-action.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Run SpecSync in CI with zero setup. Auto-detects OS/arch, downloads the binary,
2727
| `strict` | `false` | Treat warnings as errors |
2828
| `require-coverage` | `0` | Minimum file coverage % (0–100) |
2929
| `root` | `.` | Project root directory |
30-
| `args` | `''` | Extra CLI arguments passed to `specsync check` |
30+
| `args` | `''` | Extra whitespace-separated CLI arguments passed to `specsync check`; shell quoting is not supported |
3131
| `comment` | `false` | Post spec drift results as a PR comment. Requires `pull_request` event and write permissions |
3232
| `token` | `${{ github.token }}` | GitHub token for posting PR comments. Override if using a PAT for cross-repo access |
3333

specs/ai/testing.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
spec: ai.spec.md
3+
---
4+
5+
## Automated Testing
6+
7+
<!-- Expected test file locations, coverage targets, fixture descriptions -->
8+
9+
| Test File | Type | What It Covers |
10+
|-----------|------|----------------|
11+
12+
## Manual Testing
13+
14+
<!-- Step-by-step QA checklists, device/browser matrices, user flow walkthroughs -->
15+
16+
- [ ] <!-- Add manual test steps -->
17+
18+
## Edge Cases & Boundary Conditions
19+
20+
<!-- Boundary values, race conditions, permission matrices, error paths -->
21+
22+
| Scenario | Expected Behavior |
23+
|----------|-------------------|

0 commit comments

Comments
 (0)