Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 13 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
run: git diff --check "$(git hash-object -t tree /dev/null)" HEAD
- name: Validate provider catalogs
run: node scripts/validate-provider-catalogs.mjs
- name: Validate pinned example templates
env:
STACK_SPECIFICATION_DIR: ${{ github.workspace }}/.stack-specification
run: node scripts/sync-example-templates.mjs --check
- name: Install latest stable Rust toolchain
run: rustup toolchain install stable --profile minimal --component clippy,rustfmt,llvm-tools-preview
- name: Check formatting
Expand All @@ -42,6 +46,10 @@ jobs:
env:
STACK_SPECIFICATION_DIR: ${{ github.workspace }}/.stack-specification
run: cargo +stable test --features conformance --test formatter-conformance --locked
- name: Run template conformance suite
env:
STACK_SPECIFICATION_DIR: ${{ github.workspace }}/.stack-specification
run: cargo +stable test --features conformance --test template-conformance --locked
- name: Run Clippy
run: cargo +stable clippy --all-targets --all-features --locked -- -D warnings
- name: Build documentation
Expand Down Expand Up @@ -72,6 +80,7 @@ jobs:
./target/release/stack -V
./target/release/stack --help
./target/release/stack --version
./target/release/stack init --help
./target/release/stack check --help
./target/release/stack fmt --help
./target/release/stack render --help
Expand All @@ -94,14 +103,18 @@ jobs:
test -s Cargo.lock
test -s src/config.rs
test -s src/main.rs
test -s src/templates.rs
test -s src/provider.rs
test -s src/provider_catalog.rs
test -s catalogs/aws.json
test -s catalogs/gcp.json
test -s catalogs/azure.json
test -s catalogs/simple-icons.json
test -s scripts/generate-provider-catalogs.mjs
test -s scripts/sync-example-templates.mjs
test -s scripts/validate-provider-catalogs.mjs
test -s templates/catalog.json
test -s templates/sources/01-minimal.stack
test -s tests/specification-revision
test -s tests/fixtures/render.stack

Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ name = "formatter-conformance"
path = "tests/formatter_conformance.rs"
required-features = ["conformance"]

[[test]]
name = "template-conformance"
path = "tests/template_conformance.rs"
required-features = ["conformance"]

[lints.clippy]
expect_used = "deny"
panic = "deny"
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ The repository contains native validation, formatting, and rendering commands. T
stack help
stack help render
stack version
stack init
stack init --template groups-and-layout
stack init --template aws-serverless-checkout -o checkout.stack
stack check arch.stack
stack fmt arch.stack
stack fmt --check arch.stack
Expand All @@ -25,6 +28,8 @@ stack render arch.stack -o arch.svg --notice arch.NOTICE.md

`stack help`, `stack -h`, and `stack --help` print top-level help. Use `stack help <COMMAND>` or `<COMMAND> -h` / `<COMMAND> --help` for command-specific usage and examples; nested icon help is available through `stack help icons <COMMAND>`. `stack version`, `stack -v`, `stack -V`, and `stack --version` print the same Cargo package version. Help and version output use standard output and exit with status `0`. Invalid arguments and unknown commands use standard error and status `2`; close command typos include a suggested command and the relevant help invocation.

`stack init` creates `diagram.stack` from the versioned `hello-stack` template without prompting. Use `--template <ID>` to select any of the nine curated examples shared with the public Stack specification and Web gallery, and `-o` / `--output` to choose another file. Existing paths are never replaced unless `--force` is explicit; forced writes use the same atomic output behavior as rendering. Provider templates print the exact `stack icons import` commands needed for branded rendering and remain valid with deterministic fallback icons when packs are absent. The embedded catalog and source bytes are pinned by `tests/specification-revision`, and CI rejects drift from that public specification commit.

`stack check` reads the file as bytes and runs the full compiler, theme, layout, and routing validation pipeline without changing the source. Diagnostics are written to standard error in source order. Standard output remains empty.

`stack fmt` uses the engine formatter and preserves comments. File mode replaces changed source atomically through a temporary file in the same directory; unchanged files are not replaced. Syntax, encoding, and host I/O failures leave the original file untouched. `stack fmt -` reads bytes from standard input and writes only canonical source to standard output. `--check` never writes source and exits with status `1` when formatting is required.
Expand Down Expand Up @@ -59,6 +64,17 @@ CI validates formatting, unit and process-level integration tests, at least 90%

Canonical formatter behavior is checked against the pinned `stack-sh/specification` fixture revision recorded in `tests/specification-revision`.

The same checkout validates and updates the embedded `stack init` templates:

```sh
STACK_SPECIFICATION_DIR=../specification \
node scripts/sync-example-templates.mjs --check
STACK_SPECIFICATION_DIR=../specification \
node scripts/sync-example-templates.mjs
STACK_SPECIFICATION_DIR=../specification \
cargo test --features conformance --test template-conformance --locked
```

See [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a change. Please report security vulnerabilities through the process in [SECURITY.md](./SECURITY.md), not a public issue.

## Licensing
Expand Down
61 changes: 61 additions & 0 deletions scripts/sync-example-templates.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import assert from "node:assert/strict"
import { execFile } from "node:child_process"
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"
import path from "node:path"
import { promisify } from "node:util"

const execute = promisify(execFile)
const specificationRootValue = process.env.STACK_SPECIFICATION_DIR
if (!specificationRootValue) {
throw new Error("STACK_SPECIFICATION_DIR must point to the pinned specification checkout")
}

const checkOnly = process.argv.includes("--check")
const specificationRoot = path.resolve(specificationRootValue)
const expectedRevision = (await readFile("tests/specification-revision", "utf8")).trim()
assert.match(expectedRevision, /^[0-9a-f]{40}$/)
const { stdout } = await execute("git", ["rev-parse", "HEAD"], {
cwd: specificationRoot,
encoding: "utf8",
})
assert.equal(stdout.trim(), expectedRevision, "Specification checkout does not match the pin")

const catalogSource = await readFile(path.join(specificationRoot, "examples/catalog.json"), "utf8")
const catalog = JSON.parse(catalogSource)
const sourceNames = catalog.examples.map((example) => example.source).sort()
const templateRoot = path.resolve("templates")
const sourceRoot = path.join(templateRoot, "sources")
const snapshots = new Map([[path.join(templateRoot, "catalog.json"), catalogSource]])
for (const sourceName of sourceNames) {
snapshots.set(
path.join(sourceRoot, sourceName),
await readFile(path.join(specificationRoot, "examples", sourceName), "utf8"),
)
}

if (checkOnly) {
const actualSources = (await readdir(sourceRoot))
.filter((entry) => entry.endsWith(".stack"))
.sort()
assert.deepEqual(actualSources, sourceNames, "Template source inventory has drifted")
for (const [destination, expected] of snapshots) {
assert.equal(
await readFile(destination, "utf8"),
expected,
`${path.relative(process.cwd(), destination)} has drifted from the pinned specification`,
)
}
console.log(`Verified ${sourceNames.length} templates against stack-sh/specification@${expectedRevision}.`)
} else {
await mkdir(sourceRoot, { recursive: true })
for (const entry of await readdir(sourceRoot)) {
if (entry.endsWith(".stack") && !sourceNames.includes(entry)) {
await rm(path.join(sourceRoot, entry))
}
}
for (const [destination, contents] of snapshots) {
await mkdir(path.dirname(destination), { recursive: true })
await writeFile(destination, contents)
}
console.log(`Synchronized ${sourceNames.length} templates from stack-sh/specification@${expectedRevision}.`)
}
Loading