Repository navigation
feat: implement scc through validate — scaffolding, artifacts, and eight validators #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Normalize line endings to LF in the repository and working tree. scc runs on | ||
| # Windows/WSL where autocrlf otherwise makes the entire tree show as Modified and | ||
| # breaks gofmt/line-ending-sensitive checks on Windows CI. LF everywhere keeps | ||
| # manifest hashes, Go source, and shell hooks consistent across platforms. | ||
| * text=auto eol=lf | ||
|
|
||
| # Go, docs, shell hooks, and config are always LF. | ||
| *.go text eol=lf | ||
| *.mod text eol=lf | ||
| *.sum text eol=lf | ||
| *.sh text eol=lf | ||
| *.md text eol=lf | ||
| *.json text eol=lf | ||
| *.yml text eol=lf | ||
| *.yaml text eol=lf | ||
| *.tmpl text eol=lf | ||
| *.mjs text eol=lf | ||
| *.js text eol=lf | ||
|
|
||
| # Binary assets must never be line-ending converted. | ||
| *.png binary | ||
| *.jpg binary | ||
| *.gif binary | ||
| *.ico binary | ||
| *.gz binary | ||
| *.zip binary |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| name: ci | ||
|
|
||
| # Test + validate gate. Runs on: | ||
| # - every pull request — validate changes before they can merge. | ||
| # - push to main — validate the merge result on the default branch, since the | ||
| # release workflow is manual (workflow_dispatch) and does not run on push, | ||
| # so a merge would otherwise go unverified. | ||
| # - workflow_call — reusable, so the manual release pipeline requires this gate | ||
| # before building or publishing any binaries. | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| workflow_call: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| name: test (${{ matrix.os }}) | ||
| # Windows is a first-class target and the tool is path/line-ending heavy, so | ||
| # exercise the suite on all three OSes. fail-fast is off so one platform's | ||
| # failure still lets the others report. | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, windows-latest, macos-latest] | ||
| runs-on: ${{ matrix.os }} | ||
| steps: | ||
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | ||
|
|
||
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | ||
| with: | ||
| # Build/test with a current, patched toolchain. Keep this pin AHEAD of | ||
| # go.mod's directive and do NOT switch to go-version-file: go.mod — | ||
| # install-compat with older toolchains rides on GOTOOLCHAIN=auto. | ||
| # Pinned to .12 for the GO-2026-5856 crypto/tls fix; the float '1.25' | ||
| # still resolves to .11 on hosted runners (manifest lag). Float back | ||
| # once runners catch up. | ||
| go-version: '1.25.12' | ||
| cache: true | ||
|
|
||
| - name: go vet | ||
| run: go vet ./... | ||
|
|
||
| - name: build | ||
| run: go build ./... | ||
|
|
||
| # The race detector needs a C toolchain + external linker, which the | ||
| # Windows runners don't provide out of the box, so run race+coverage on | ||
| # Linux/macOS and a plain (still cgo-free) test pass on Windows. | ||
| - name: test (race + coverage) | ||
| if: runner.os != 'Windows' | ||
| run: go test -race -coverprofile=coverage.out ./... | ||
|
|
||
| - name: test (coverage, no race) | ||
| if: runner.os == 'Windows' | ||
| # Force bash so the coverprofile arg parses like Linux/macOS; the Windows | ||
| # runner's default PowerShell mangles `-coverprofile=coverage.out ./...` | ||
| # into a bogus `.out` package pattern. | ||
| shell: bash | ||
| run: go test -coverprofile=coverage.out ./... | ||
|
|
||
| lint: | ||
| name: lint & vulncheck | ||
| # gofmt and the static analyzers only need to run once; keep them on Linux | ||
| # (the Windows runner checks code out with CRLF endings, which gofmt -l would | ||
| # flag on every file). | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | ||
|
|
||
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | ||
| with: | ||
| go-version: '1.25.12' # GO-2026-5856 crypto/tls fix; see the test job's note | ||
| cache: true | ||
|
|
||
| - name: gofmt | ||
| run: | | ||
| unformatted="$(gofmt -l .)" | ||
| if [ -n "$unformatted" ]; then | ||
| echo "::error::these files are not gofmt-clean:" | ||
| echo "$unformatted" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Scan the module (and its dependencies) for known-vulnerable, *reachable* | ||
| # code paths. This is meant to fail the build — shipping binaries built on a | ||
| # flagged stdlib/dep is exactly what we're guarding against. | ||
| - name: govulncheck | ||
| run: | | ||
| go install golang.org/x/vuln/cmd/govulncheck@latest | ||
| "$(go env GOPATH)/bin/govulncheck" ./... | ||
|
|
||
| # golangci-lint runs the conservative set configured in .golangci.yml. | ||
| - name: golangci-lint | ||
| uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 | ||
| with: | ||
| version: v2.12.2 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| name: release | ||
|
|
||
| # Manual, versioned release — the only way a new version ships. | ||
| # | ||
| # Trigger from the Actions tab ("Run workflow") or: | ||
| # gh workflow run release.yml --field version=v0.1.0 | ||
| # (or: make release VERSION=v0.1.0) | ||
| # | ||
| # It runs the full pipeline for the given version: CI gate → cross-platform | ||
| # binaries → publish every npm package (the launcher + 6 platform binaries, all | ||
| # at the same version) → tag the commit + GitHub Release. Nothing is released on | ||
| # push; releasing is always a deliberate manual action. | ||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| version: | ||
| description: 'Release version (vX.Y.Z)' | ||
| required: true | ||
| type: string | ||
|
|
||
| # Least privilege at the workflow level: only the final "release" job needs to | ||
| # write (create the tag + GitHub Release). validate/build/npm-publish keep the | ||
| # read-only default and override to what they actually need. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| validate: | ||
| name: validate version | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Check format | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| run: | | ||
| case "$VERSION" in | ||
| v[0-9]*.[0-9]*.[0-9]*) echo "releasing $VERSION" ;; | ||
| *) echo "::error::version must look like vX.Y.Z (got '$VERSION')"; exit 1 ;; | ||
| esac | ||
|
Comment on lines
+36
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use one strict release-version rule. Both checks accept malformed values such as
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | ||
| with: | ||
| fetch-depth: 0 # tags — needed to tell a retry from a re-release | ||
|
|
||
| # A version is immutable. Re-dispatching an already-released version from a | ||
| # NEW commit is the one failure this pipeline cannot report: npm-publish | ||
| # skips every package that is already on the registry, the tag stays put, | ||
| # and the whole run goes green having shipped nothing. Fail loudly instead. | ||
| # | ||
| # Re-dispatching the SAME commit is still allowed: that is the intended way | ||
| # to resume a run that died between npm-publish and the tag/Release step. | ||
| - name: Refuse to re-release an existing version from a different commit | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| SHA: ${{ github.sha }} | ||
| run: | | ||
| set -euo pipefail | ||
| if ! git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then | ||
| echo "$VERSION is unreleased — proceeding" | ||
| exit 0 | ||
| fi | ||
| tagged=$(git rev-list -n1 "refs/tags/$VERSION") | ||
| if [ "$tagged" != "$SHA" ]; then | ||
| echo "::error::$VERSION is already released and points at $tagged, but this run is on $SHA." | ||
| echo "::error::Publishing is idempotent, so re-running would ship nothing. Bump the version instead." | ||
| exit 1 | ||
| fi | ||
| echo "$VERSION already tagged at this exact commit — resuming a partial release" | ||
|
|
||
| # Reuse the full CI gate (gofmt, vet, build, race tests, vulncheck) — no | ||
| # binaries are built or published unless this passes. | ||
| ci: | ||
| needs: validate | ||
| uses: ./.github/workflows/ci.yml | ||
|
|
||
| build: | ||
| name: build ${{ matrix.goos }}/${{ matrix.goarch }} | ||
| needs: ci | ||
| runs-on: ubuntu-latest | ||
| # Cross-compile only; no write access needed. Keep the read-only default | ||
| # (inherited from the workflow-level permissions above) explicit here. | ||
| permissions: | ||
| contents: read | ||
| strategy: | ||
| fail-fast: true | ||
| matrix: | ||
| include: | ||
| - { goos: linux, goarch: amd64 } | ||
| - { goos: linux, goarch: arm64 } | ||
| - { goos: darwin, goarch: amd64 } | ||
| - { goos: darwin, goarch: arm64 } | ||
| - { goos: windows, goarch: amd64 } | ||
| - { goos: windows, goarch: arm64 } | ||
| steps: | ||
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | ||
|
|
||
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | ||
| with: | ||
| go-version: '1.25.12' # see ci.yml's note before changing | ||
| cache: true | ||
|
|
||
| - name: Build & package | ||
| env: | ||
| GOOS: ${{ matrix.goos }} | ||
| GOARCH: ${{ matrix.goarch }} | ||
| CGO_ENABLED: '0' | ||
| VERSION: ${{ inputs.version }} | ||
| run: | | ||
| set -euo pipefail | ||
| bin=scc | ||
| [ "$GOOS" = "windows" ] && bin=scc.exe | ||
| mkdir -p dist | ||
| go build -trimpath \ | ||
| -ldflags "-s -w -X github.com/protonspy/spec-claude-code/internal/cli.version=${VERSION}" \ | ||
| -o "dist/${bin}" ./cmd/scc | ||
| name="scc_${VERSION}_${GOOS}_${GOARCH}" | ||
| cd dist | ||
| if [ "$GOOS" = "windows" ]; then | ||
| zip -q "${name}.zip" "${bin}" | ||
| else | ||
| tar -czf "${name}.tar.gz" "${bin}" | ||
| fi | ||
|
Comment on lines
+112
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Make release archives and checksums reproducible. The tar/zip metadata captures build-time timestamps, so rebuilding the same commit/version changes release assets and As per coding guidelines, “Maintain deterministic generated artifacts by normalizing newlines to LF and sorting before serialization.” Also applies to: 212-213 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| rm -f "${bin}" | ||
|
|
||
| # Upload only what this run built, never `dist/*` — a stale tarball sitting | ||
| # in the checked-out dist/ would otherwise ride along into every Release. | ||
| - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||
| with: | ||
| name: scc-${{ matrix.goos }}-${{ matrix.goarch }} | ||
| path: dist/scc_${{ inputs.version }}_* | ||
| if-no-files-found: error | ||
| retention-days: 7 | ||
|
|
||
| # Publish every npm package at the requested version. Authenticates with the | ||
| # NPM_TOKEN secret (must be an Automation token, which bypasses 2FA); | ||
| # id-token: write lets --provenance attach a signed build attestation. | ||
| npm-publish: | ||
| name: publish npm packages | ||
| needs: build | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| id-token: write # required for npm --provenance attestation | ||
| steps: | ||
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | ||
|
|
||
| - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||
| with: | ||
| node-version: '22' | ||
| registry-url: 'https://registry.npmjs.org' | ||
|
|
||
| # Trusted publishing requires npm >= 11.5.1 (newer than what ships with | ||
| # Node). Pin the major line so the release toolchain is reproducible. | ||
| - name: Upgrade npm | ||
| run: npm install -g npm@11 | ||
|
Comment on lines
+152
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
npm view npm@11.5.1 version engines dist.integrity --jsonRepository: protonspy/spec-claude-code Length of output: 676 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
sed -n '140,170p' .github/workflows/release.ymlRepository: protonspy/spec-claude-code Length of output: 1347 Pin npm to 11.5.1. 🧰 Tools🪛 zizmor (1.28.0)[warning] 155-155: ad-hoc installation of packages (adhoc-packages): installs a package outside of a lockfile (adhoc-packages) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||
| with: | ||
| path: artifacts | ||
| merge-multiple: true | ||
|
|
||
| - name: Assemble npm packages | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| run: node npm/scripts/build-packages.mjs "$VERSION" artifacts | ||
|
|
||
| - name: Publish (platform packages first, then the launcher) | ||
| env: | ||
| # Automation token (bypasses 2FA). setup-node wrote an .npmrc that | ||
| # reads this. --provenance still attaches a signed attestation via the | ||
| # job's id-token: write permission. | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| run: | | ||
| set -euo pipefail | ||
| # Idempotent publish: skip any package@version already on the registry so | ||
| # re-running after a partial failure can finish instead of hard-failing on | ||
| # the first already-published package (mirrors the Makefile's npm-publish). | ||
| publish() { | ||
| local dir="$1" | ||
| local name ver | ||
| name=$(node -p "require('./$dir/package.json').name") | ||
| ver=$(node -p "require('./$dir/package.json').version") | ||
| if npm view "$name@$ver" version >/dev/null 2>&1; then | ||
| echo "skip $name@$ver (already published)" | ||
| return 0 | ||
| fi | ||
| echo ">> publishing $name@$ver" | ||
| npm publish "$dir" --access public --provenance | ||
| } | ||
| # The platform packages must land before the launcher that depends on them. | ||
| for d in npm/dist/scc-*/; do | ||
| publish "${d%/}" | ||
| done | ||
| publish npm/dist/scc | ||
|
|
||
| # Tag the released commit and create the GitHub Release — only after npm | ||
| # publishing succeeds, so a failed publish never leaves a dangling tag/release. | ||
| release: | ||
| name: tag + GitHub release | ||
| needs: npm-publish | ||
| runs-on: ubuntu-latest | ||
| # This is the only job that mutates the repo (creates the tag + Release), so | ||
| # scope write access here instead of granting it workflow-wide. | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||
| with: | ||
| path: dist | ||
| merge-multiple: true | ||
|
|
||
| - name: Checksums | ||
| run: cd dist && sha256sum * | tee checksums.txt | ||
|
|
||
| - name: Create tag + release | ||
| uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 | ||
| with: | ||
| tag_name: ${{ inputs.version }} | ||
| name: ${{ inputs.version }} | ||
| target_commitish: ${{ github.sha }} | ||
| make_latest: 'true' | ||
| files: dist/* | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # golangci-lint configuration (v2 schema — golangci-lint v2.x). | ||
| # | ||
| # Deliberately conservative: only the standard, high-signal analyzers are | ||
| # enabled so CI stays green-friendly instead of drowning in style nits. Add more | ||
| # linters here as the codebase grows. | ||
| version: "2" | ||
|
|
||
| linters: | ||
| default: none | ||
| enable: | ||
| - errcheck # unchecked errors | ||
| - govet # go vet's analyzers | ||
| - ineffassign # assignments never read | ||
| - staticcheck # staticcheck (SA/S/ST) suite | ||
| - unused # unused code | ||
| exclusions: | ||
| rules: | ||
| # Test code is intentionally lax about unchecked errors (Close/Chdir/…) and | ||
| # uses patterns staticcheck false-positives on (e.g. Hash("a") != Hash("a") | ||
| # to assert determinism). | ||
| - path: _test\.go | ||
| linters: | ||
| - errcheck | ||
| - staticcheck | ||
| # QF1001 (apply De Morgan's law) and S1016 (struct literal -> conversion) | ||
| # are style suggestions where the explicit form reads clearer; keep it. | ||
| - linters: | ||
| - staticcheck | ||
| text: "QF1001" | ||
| - linters: | ||
| - staticcheck | ||
| text: "S1016" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: protonspy/spec-claude-code
Length of output: 4730
🏁 Script executed:
Repository: protonspy/spec-claude-code
Length of output: 350
🌐 Web query:
actions/checkout v5 persist-credentials default token .git/config💡 Result:
In
actions/checkoutv5, thepersist-credentialsinput defaults totrue[1][2]. Whenpersist-credentialsis set totrue(the default),actions/checkoutconfigures the GitHub Actions runner to authenticate Git commands by writing credential information to a file [3][4]. Crucially, in v5.0.1 and later versions of the v5 release, this behavior was updated: rather than writing credentials directly into the local.git/configfile, the action stores them in a separate configuration file located within the$RUNNER_TEMPdirectory [4]. The main.git/configis then updated with aninclude.pathdirective to reference this temporary file [4]. This change was implemented to improve security by isolating the credentials from the repository's main configuration and preventing accidental leakage [4]. The credentials are removed during the post-job cleanup process [3]. If you wish to prevent this behavior, you can explicitly setpersist-credentials: falsein your workflow step [3].Citations:
Disable persisted checkout credentials on both checkout steps. The default checkout config leaves a repo-scoped credential available to later steps; set
persist-credentials: falsehere and at line 72 so PR code can’t reuse it.🧰 Tools
🪛 zizmor (1.28.0)
[warning] 31-31: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Source: Linters/SAST tools