Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 39 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,44 @@ jobs:
echo "$OUTPUT"
COVERAGE=$(echo "$OUTPUT" | grep -oP '[\d.]+% coverage' | grep -oP '[\d.]+')
echo "Coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 40" | bc -l) )); then
echo "::error::Coverage ${COVERAGE}% is below 40% threshold"
if (( $(echo "$COVERAGE < 60" | bc -l) )); then
echo "::error::Coverage ${COVERAGE}% is below 60% threshold"
exit 1
fi

site:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
working-directory: site
run: bun install --frozen-lockfile
- name: Run site tests
working-directory: site
run: bun test
- name: Lint site
working-directory: site
run: bun run lint
- name: Build site
working-directory: site
run: bun run build

vscode-extension:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
working-directory: vscode-extension
run: bun install --frozen-lockfile
- name: Compile extension
working-directory: vscode-extension
run: bun run compile
- name: Package extension
working-directory: vscode-extension
run: bun run package

validate-action:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -94,12 +127,12 @@ jobs:
echo 'EOF'
} >> "$GITHUB_OUTPUT"
# Fail the step if check fails
cargo run -- check --strict
cargo run -- check --strict --require-coverage 100 --force

corvid-pet:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && always()
needs: [test, fmt, validate-action, spec-check, audit, coverage]
needs: [test, fmt, validate-action, spec-check, audit, coverage, site, vscode-extension]
steps:
- uses: actions/checkout@v4

Expand All @@ -112,6 +145,8 @@ jobs:
CHECK_SPEC: "Spec Validation=${{ needs.spec-check.result }}"
CHECK_AUDIT: "Dependency Audit=${{ needs.audit.result }}"
CHECK_COVERAGE: "Code Coverage=${{ needs.coverage.result }}"
CHECK_SITE: "Docs Site=${{ needs.site.result }}"
CHECK_VSCODE: "VS Code Extension=${{ needs.vscode-extension.result }}"
REPORT_SPEC: ${{ needs.spec-check.outputs.body }}
run: |
OVERALL="success"
Expand Down
2 changes: 1 addition & 1 deletion .specsync/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
specs_dir = "specs"
source_dirs = ["src"]
exclude_dirs = ["__tests__"]
exclude_patterns = ["**/__tests__/**", "**/*.test.ts", "**/*.spec.ts"]
exclude_patterns = ["**/__tests__/**", "**/*.test.ts", "**/*.spec.ts", "**/tests.rs"]
required_sections = ["Purpose", "Public API", "Invariants", "Behavioral Examples", "Error Cases", "Dependencies", "Change Log"]
enforcement = "strict"
ai_command = "claude -p --output-format text"
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [4.3.4] - 2026-06-07

### Security

- **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`.
- **Release checksum verification fails closed** — downloaded release archives now require matching `.sha256` files before extraction.

### Fixed

- **Action input validation** — `require-coverage` is validated as an integer from 0 to 100 before command execution.
- **MCP generated-spec test assertion** — replaced a tautological unsigned comparison with a meaningful generated-spec count assertion.
- **VS Code extension license packaging** — extension package includes the MIT license file so VSIX builds are complete.

### CI

- **Repo-wide validation expanded** — CI now builds/tests/lints the Astro docs site and compiles/packages the VS Code extension.
- **Spec gate requires full coverage** — project spec CI now runs `check --strict --require-coverage 100 --force`.
- **Coverage threshold raised** — tarpaulin minimum coverage increased from 40% to 60%.
- **Fledge tasks expanded** — repository lanes now cover Rust, specs, docs, extension packaging, and audit checks.
- **Known transitive audit warning tracked** — `RUSTSEC-2024-0384` is ignored explicitly while it remains pulled in through `notify`.

### Specs

- **Utility helpers specced** — added a dedicated spec for `src/util.rs`.
- **Companion files completed** — backfilled `testing.md` companions and missing `tasks.md`/`context.md` files for legacy specs.

## [v4.3.3] - 2026-05-18

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "specsync"
version = "4.3.3"
version = "4.3.4"
edition = "2024"
rust-version = "1.88"
description = "Bidirectional spec-to-code validation with schema column checking — 11 languages, single binary"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ Available on the [GitHub Marketplace](https://github.com/marketplace/actions/spe
| `strict` | `false` | Treat warnings as errors |
| `require-coverage` | `0` | Minimum file coverage % |
| `root` | `.` | Project root directory |
| `args` | `''` | Extra CLI arguments |
| `args` | `''` | Extra whitespace-separated CLI arguments; shell quoting is not supported |
| `comment` | `false` | Post spec drift results as a PR comment (requires `pull_request` event) |
| `token` | `${{ github.token }}` | GitHub token for posting PR comments (needs write permissions) |

Expand Down
68 changes: 37 additions & 31 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ inputs:
required: false
default: '.'
args:
description: 'Additional arguments to pass to specsync check'
description: 'Additional whitespace-separated arguments to pass to specsync check (shell quoting is not supported)'
required: false
default: ''
lifecycle-enforce:
Expand Down Expand Up @@ -87,34 +87,30 @@ runs:
ARCHIVE="specsync-${OS}-${ARCH}.exe.zip"
curl -fsSL "${BASE_URL}/${ARCHIVE}" -o "${INSTALL_DIR}/specsync.zip"

# Verify checksum if available
if curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/specsync.sha256" 2>/dev/null; then
echo "Verifying checksum..."
cd "$INSTALL_DIR"
EXPECTED=$(awk '{print $1}' specsync.sha256)
ACTUAL=$(shasum -a 256 specsync.zip | awk '{print $1}')
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::Checksum verification failed! Expected: $EXPECTED, Got: $ACTUAL"
exit 1
fi
echo "::notice::Checksum verified"
cd -
curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/specsync.sha256"
echo "Verifying checksum..."
cd "$INSTALL_DIR"
EXPECTED=$(awk '{print $1}' specsync.sha256)
ACTUAL=$(shasum -a 256 specsync.zip | awk '{print $1}')
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::Checksum verification failed! Expected: $EXPECTED, Got: $ACTUAL"
exit 1
fi
echo "::notice::Checksum verified"
cd -

unzip -o "${INSTALL_DIR}/specsync.zip" -d "$INSTALL_DIR"
mv "${INSTALL_DIR}/specsync-${OS}-${ARCH}.exe" "${INSTALL_DIR}/specsync.exe"
else
ARCHIVE="specsync-${OS}-${ARCH}.tar.gz"
curl -fsSL "${BASE_URL}/${ARCHIVE}" -o "${INSTALL_DIR}/${ARCHIVE}"

# Verify checksum if available
if curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/${ARCHIVE}.sha256" 2>/dev/null; then
echo "Verifying checksum..."
cd "$INSTALL_DIR"
shasum -a 256 -c "${ARCHIVE}.sha256"
echo "::notice::Checksum verified"
cd -
fi
curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" -o "${INSTALL_DIR}/${ARCHIVE}.sha256"
echo "Verifying checksum..."
cd "$INSTALL_DIR"
shasum -a 256 -c "${ARCHIVE}.sha256"
echo "::notice::Checksum verified"
cd -

tar xz -C "$INSTALL_DIR" -f "${INSTALL_DIR}/${ARCHIVE}"
mv "${INSTALL_DIR}/specsync-${OS}-${ARCH}" "${INSTALL_DIR}/specsync"
Expand All @@ -138,26 +134,36 @@ runs:
run: |
set -euo pipefail

if ! [[ "$INPUT_REQUIRE_COVERAGE" =~ ^[0-9]+$ ]] || [ "$INPUT_REQUIRE_COVERAGE" -gt 100 ]; then
echo "::error::require-coverage must be an integer from 0 to 100"
exit 1
fi

# Always use --force in CI — hash cache is not committed, so
# every CI run validates all specs from scratch.
CMD="specsync check --force"
CMD=(specsync check --force)

if [ "$INPUT_STRICT" = "true" ]; then
CMD="$CMD --strict"
CMD+=(--strict)
fi

if [ "$INPUT_REQUIRE_COVERAGE" != "0" ]; then
CMD="$CMD --require-coverage $INPUT_REQUIRE_COVERAGE"
CMD+=(--require-coverage "$INPUT_REQUIRE_COVERAGE")
fi

if [ -n "$INPUT_ARGS" ]; then
CMD="$CMD $INPUT_ARGS"
# INPUT_ARGS is intentionally split on whitespace only. Shell quoting,
# substitutions, pipes, and other shell syntax are not evaluated.
read -r -a EXTRA_ARGS <<< "$INPUT_ARGS"
Comment thread
0xLeif marked this conversation as resolved.
Outdated
CMD+=("${EXTRA_ARGS[@]}")
fi

echo "::group::SpecSync Check"
echo "Running: $CMD"
printf 'Running:'
printf ' %q' "${CMD[@]}"
printf '\n'
EXIT_CODE=0
eval "$CMD" || EXIT_CODE=$?
"${CMD[@]}" || EXIT_CODE=$?
echo "::endgroup::"

# If lifecycle enforcement is enabled, run it (may override exit code)
Expand All @@ -171,14 +177,14 @@ runs:
# Uses the same `specsync comment` pipeline as our own CI workflow
# for identical output between the marketplace action and direct usage.
if [ "$INPUT_COMMENT" = "true" ]; then
COMMENT_CMD="specsync comment"
COMMENT_CMD=(specsync comment)
if [ "$INPUT_STRICT" = "true" ]; then
COMMENT_CMD="$COMMENT_CMD --strict"
COMMENT_CMD+=(--strict)
fi
if [ "$INPUT_REQUIRE_COVERAGE" != "0" ]; then
COMMENT_CMD="$COMMENT_CMD --require-coverage $INPUT_REQUIRE_COVERAGE"
COMMENT_CMD+=(--require-coverage "$INPUT_REQUIRE_COVERAGE")
fi
COMMENT_OUTPUT=$(eval "$COMMENT_CMD" 2>/dev/null) || true
COMMENT_OUTPUT=$("${COMMENT_CMD[@]}" 2>/dev/null) || true
{
echo "SPECSYNC_MARKDOWN<<SPECSYNC_EOF"
echo "$COMMENT_OUTPUT"
Expand Down
41 changes: 39 additions & 2 deletions fledge.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,56 @@ cmd = "cargo clippy -- -D warnings"
cmd = "cargo fmt --check"

[tasks.audit]
cmd = "cargo audit"
cmd = "cargo audit --ignore RUSTSEC-2024-0384"

[tasks.check-types]
cmd = "cargo check"

[tasks.spec-check]
cmd = "cargo run -- check --strict --require-coverage 100 --force"

[tasks.docs-test]
cmd = "cd site && bun test"

[tasks.docs-lint]
cmd = "cd site && bun run lint"

[tasks.docs-build]
cmd = "cd site && bun run build"

[tasks.vscode-compile]
cmd = "cd vscode-extension && bun run compile"

[tasks.vscode-package]
cmd = "cd vscode-extension && bun run package"

[lanes.check]
description = "Quick pre-commit check"
steps = [{ parallel = ["fmt", "lint"] }, "test"]

[lanes.ci]
description = "Full CI pipeline"
steps = ["fmt", "lint", "test", "build"]
steps = [
"fmt",
"lint",
"test",
"build",
"audit",
"spec-check",
{ parallel = ["docs-test", "docs-lint", "docs-build", "vscode-compile", "vscode-package"] },
]

[lanes.pre-commit]
description = "Fast gate before committing"
steps = [{ parallel = ["fmt", "lint"] }, "check-types"]

[lanes.repo]
description = "Full repository validation, including docs and editor extension"
steps = [
{ parallel = ["fmt", "lint", "check-types"] },
"test",
"build",
"audit",
"spec-check",
{ parallel = ["docs-test", "docs-lint", "docs-build", "vscode-compile", "vscode-package"] },
]
2 changes: 1 addition & 1 deletion site/src/content/docs/integrations/github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Run SpecSync in CI with zero setup. Auto-detects OS/arch, downloads the binary,
| `strict` | `false` | Treat warnings as errors |
| `require-coverage` | `0` | Minimum file coverage % (0–100) |
| `root` | `.` | Project root directory |
| `args` | `''` | Extra CLI arguments passed to `specsync check` |
| `args` | `''` | Extra whitespace-separated CLI arguments passed to `specsync check`; shell quoting is not supported |
| `comment` | `false` | Post spec drift results as a PR comment. Requires `pull_request` event and write permissions |
| `token` | `${{ github.token }}` | GitHub token for posting PR comments. Override if using a PAT for cross-repo access |

Expand Down
23 changes: 23 additions & 0 deletions specs/ai/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
spec: ai.spec.md
---

## Automated Testing

<!-- Expected test file locations, coverage targets, fixture descriptions -->

| Test File | Type | What It Covers |
|-----------|------|----------------|

## Manual Testing

<!-- Step-by-step QA checklists, device/browser matrices, user flow walkthroughs -->

- [ ] <!-- Add manual test steps -->

## Edge Cases & Boundary Conditions

<!-- Boundary values, race conditions, permission matrices, error paths -->

| Scenario | Expected Behavior |
|----------|-------------------|
23 changes: 23 additions & 0 deletions specs/archive/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
spec: archive.spec.md
---

## Automated Testing

<!-- Expected test file locations, coverage targets, fixture descriptions -->

| Test File | Type | What It Covers |
|-----------|------|----------------|

## Manual Testing

<!-- Step-by-step QA checklists, device/browser matrices, user flow walkthroughs -->

- [ ] <!-- Add manual test steps -->

## Edge Cases & Boundary Conditions

<!-- Boundary values, race conditions, permission matrices, error paths -->

| Scenario | Expected Behavior |
|----------|-------------------|
Loading
Loading