Run Shape in CI
CI needs one deterministic gate: shp check --changed-files changed.txt. It runs conformance, coverage, and bindings in one command, as described in Keep the Model Current. Run it strict, without --allow-unknown-effects, so that an unresolved effects unknown fails the build; that flag is for local drafting only.
The job
Section titled “The job”name: Shape
on: pull_request: push: branches: - main
jobs: shape: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: timbrinded/shapelang@v0.10.0 - run: shp fmt --check - name: Write changed.txt env: BASE_REF: ${{ github.base_ref }} run: | if [ -n "$BASE_REF" ]; then git diff --name-only "origin/$BASE_REF...HEAD" > changed.txt else touch changed.txt fi - name: Shape check env: BASE_REF: ${{ github.base_ref }} run: | if [ -n "$BASE_REF" ]; then shp check --changed-files changed.txt --base-ref "origin/$BASE_REF" --check-loosening else shp check --changed-files changed.txt fifetch-depth: 0fetches the full history, including the base branch. The default shallow checkout has noorigin/<base>, so the diff fails.- On a pull request, the diff against the base branch lists every file the pull request changes, including the
.shapefiles that make its Shape updates and attestations current. shp fmt --checkfails on any.shapefile that is not in canonical format. The formatter drops//and/* */comments, so a file that contains comments never passes; see the CLI Reference.shp check --changed-files changed.txtis the only gate needed; it replaces separateshp checkandshp coveragesteps.--base-ref "origin/$BASE_REF"compares attestations against the model at the merge base, so only attestations written for this pull request count and carried-over ones are reported as stale. It reads that history from git, whichfetch-depth: 0provides. The flag ships in v0.10.0; upgrade an older pinnedshpbefore using it.--check-looseningfails when an edit to the rules is what lets the pull request pass, for example removingAppendOnlyfrom a resource so that a new purge job is allowed, or narrowing an implementation’spathsso that an edited file is no longer governed. It needs--base-ref, and both flags are available in the v0.10.0 pin above. See Approve a rule loosening for the case where someone asked for the change.- Design-memory freshness is off by default. To enforce it, add
--as-of YYYY-MM-DD, which gives the same result on every run, rather than--strict-freshness, which uses today’s date (UTC). Both flags are in the CLI Reference.
A push has no base ref, so this job writes an empty list on push. The push run then checks conformance only, because coverage and bindings check nothing with an empty list; pull requests carry the change-set gate. To check pushes as well, diff ${{ github.event.before }} against HEAD. That SHA is all zeros when the push creates the branch, and the diff then fails.
Approve a rule loosening
Section titled “Approve a rule loosening”--check-loosening fails whether or not anyone asked for the loosening, because the checker cannot tell. Sometimes a person did ask: legal requires old revisions to be deleted, so AppendOnly has to go. That is a design decision, and the approval belongs somewhere the author of the change cannot write, not in the Shape model. A rationale added in the same pull request does not count, since a coding agent can write one as easily as the rule edit.
One way is a label that a maintainer adds after reading the change. The check then runs without --check-loosening, and every other error still fails the job:
on: pull_request: types: [opened, synchronize, reopened, labeled, unlabeled]
# ...
- name: Shape check env: BASE_REF: ${{ github.base_ref }} LOOSENING_APPROVED: ${{ contains(github.event.pull_request.labels.*.name, 'shape-loosening-approved') }} run: | if [ -z "$BASE_REF" ]; then shp check --changed-files changed.txt elif [ "$LOOSENING_APPROVED" = "true" ]; then shp check --changed-files changed.txt --base-ref "origin/$BASE_REF" else shp check --changed-files changed.txt --base-ref "origin/$BASE_REF" --check-loosening fi- The
labeledandunlabeledtypes rerun the job when the label is added or removed. - Anyone with triage access can add a label, and so can a coding agent that acts with a maintainer’s token. Treat the label like an approving review, and tell agents in your instructions never to add it.
- The label approves every loosening in the pull request. Remove it and ask for a new approval if later pushes change the rules again.
Pin the version
Section titled “Pin the version”Pin the setup action to an explicit release tag such as v0.10.0, in every workflow. With a version tag as its ref, timbrinded/shapelang@v0.10.0 installs that release. The optional version input selects a different release; with a branch ref and no version, the action installs the latest release. Do not install with shp update in CI: that command replaces a local developer binary.
The action verifies the download against the release’s checksums.txt when sha256sum or shasum is available, as it is on GitHub-hosted runners; otherwise it warns and skips the check. It supports Linux x64 and ARM64, macOS ARM64, and Windows x64 runners.
Install without the action
Section titled “Install without the action”Run the installer from the pinned release instead:
- name: Install shp run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/timbrinded/shapelang/releases/download/v0.10.0/install.sh | shAn installer downloaded from a release URL installs that release unless SHAPE_VERSION names another. It verifies the archive’s SHA-256 against checksums.txt and installs into ~/.local/bin, or into the directory that SHAPE_INSTALL_DIR or the installer’s --install-dir argument names. Under GitHub Actions it also appends the install directory to GITHUB_PATH, so later steps find shp.
Export the model (optional)
Section titled “Export the model (optional)”After the gate passes, a workflow can export the Shape model for a visualizer or another reporting tool:
- run: shp check --changed-files changed.txt- run: shp inspect --json > shape-model.jsonshp inspect --json uses the same shape/**/*.shape discovery and canonical lowering as the model queries. Its output has a versioned schema (schemaVersion), records the shapeVersion that produced it, uses module-qualified IDs, and has no current-time field. Identical inputs and the same pinned shp version therefore produce identical bytes. The export is not a gate: it exits 0 whenever the input parses, even when shp check would fail, so keep the check before it. Downstream consumers should reject a schemaVersion they do not support.
Claude contract review (optional)
Section titled “Claude contract review (optional)”Coverage confirms that the model changed alongside the code, not that the new claims are true. A separate pull-request job can ask Claude Code whether the Shape changes faithfully describe the source changes. The job is policy-gated: its last step fails on drift. Its verdict comes from a model, so it does not replace shp check or change what the checker decides.
Add this job under jobs: in the workflow above; its if skips it on push.
shape-claude-review: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - name: Detect Claude credentials id: claude-token env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_AUTH_TOKEN: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} run: | if [ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${ANTHROPIC_AUTH_TOKEN:-}" ]; then echo "available=true" >> "$GITHUB_OUTPUT" else echo "available=false" >> "$GITHUB_OUTPUT" echo "Skipping Claude Shape contract review because no Anthropic API credential is available." fi - uses: actions/checkout@v4 if: steps.claude-token.outputs.available == 'true' with: fetch-depth: 0 - uses: timbrinded/shapelang@v0.10.0 if: steps.claude-token.outputs.available == 'true' - name: Write changed.txt if: steps.claude-token.outputs.available == 'true' env: BASE_REF: ${{ github.base_ref }} run: git diff --name-only "origin/$BASE_REF...HEAD" > changed.txt - name: Run Claude Shape contract review id: claude if: steps.claude-token.outputs.available == 'true' uses: anthropics/claude-code-action@v1 env: ANTHROPIC_AUTH_TOKEN: ${{ secrets.ANTHROPIC_AUTH_TOKEN }} ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} with: anthropic_api_key: ${{ secrets.ANTHROPIC_AUTH_TOKEN || secrets.ANTHROPIC_API_KEY }} github_token: ${{ github.token }} prompt: | Review changed.txt against the Shape model in shape/**/*.shape. For changed source behavior that affects the architecture contract, require a faithful current Shape update or a current attestation. Do not count an unchanged .shape file as a current Shape update. Run `shp check --changed-files changed.txt`, `shp obligations`, and `shp memory`; run `shp explain <symbol>` when a symbol needs context. Return status "pass" with no findings only when the model faithfully covers the change. Otherwise return "drift" or "error" with terse, evidence-backed findings. claude_args: | --model claude-sonnet-5-5 --max-turns 100 --allowedTools 'Read,Glob,Grep,Bash(shp check --changed-files changed.txt),Bash(shp obligations),Bash(shp memory),Bash(shp explain *)' --disallowedTools Write,Edit --json-schema '{"type":"object","additionalProperties":false,"required":["status","summary","findings"],"properties":{"status":{"type":"string","enum":["pass","drift","error"]},"summary":{"type":"string"},"findings":{"type":"array","items":{"type":"string"}}}}' - name: Gate on the review result if: steps.claude-token.outputs.available == 'true' env: REVIEW_RESULT: ${{ steps.claude.outputs.structured_output }} run: | node <<'NODE' const result = JSON.parse(process.env.REVIEW_RESULT || "{}"); if (result.status !== "pass" || (result.findings ?? []).length > 0) { console.error(JSON.stringify(result, null, 2)); process.exit(1); } console.log(result.summary); NODE- Action. The job runs the official
anthropics/claude-code-action, which installs Claude Code and runs the prompt headless. - Result. With
--json-schemainclaude_args, the action validates Claude’s final answer against the schema and exposes it as thestructured_outputstep output. The gate step fails the job on any status other thanpassor on any finding. - Tools. Claude Code runs no shell commands unless
--allowedToolslists them. This job allows only the read-onlyshpcommands that the prompt names, and installsshpso that they can run.--disallowedTools Write,Editblocks file edits. If your repository runs the CLI through a package script, such asbun shp, use that form in both the prompt and--allowedTools. - Credentials. The action’s credential check accepts an
anthropic_api_keyorclaude_code_oauth_tokeninput. A proxy-backed repository can authenticate withANTHROPIC_AUTH_TOKENandANTHROPIC_BASE_URLinstead: the action forwards its environment to Claude Code, so set both on the step and pass the token throughanthropic_api_keyto satisfy the credential check. Through a proxy gateway the structured-output channel can degrade to plain text; the action then reports the review step as failed even when the model run succeeded, which fails this job. The Shape repository’s own workflow setscontinue-on-erroron that step and recovers the result from the action’sexecution_filelog. - Forks. Pull requests from forks receive no secrets. The first step detects whether a credential is available, and every later step is skipped without one, so those pull requests skip the review instead of failing.
A longer review policy can live in a committed file, for example .github/prompts/shape-contract-review.md, with the inline prompt telling Claude to read it.