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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ jobs:
- name: Run tests
run: npm test

- name: Check latest published CLI release
env:
GH_TOKEN: ${{ github.token }}
run: npm run docs:release:check

- name: Verify pinned example snapshots
env:
STACK_SPECIFICATION_ROOT: ${{ github.workspace }}/.stack-specification
Expand All @@ -78,6 +83,18 @@ jobs:
STACK_CLI_BIN: ${{ github.workspace }}/.stack-cli/target/debug/stack
run: npm run docs:smoke

- name: Verify and smoke test the published CLI archive
env:
GH_TOKEN: ${{ github.token }}
run: |
version=$(node --input-type=module -e 'import { documentationContract as c } from "./scripts/docs-validation.config.mjs"; console.log(c.cli.version)')
archive="stack-v${version}-x86_64-unknown-linux-gnu.tar.gz"
mkdir .stack-release
gh release download "v${version}" --repo stack-sh/cli --pattern "$archive" --dir .stack-release
gh attestation verify ".stack-release/$archive" --repo stack-sh/cli
tar -xzf ".stack-release/$archive" -C .stack-release
STACK_CLI_BIN="$PWD/.stack-release/stack-v${version}-x86_64-unknown-linux-gnu/stack" npm run docs:smoke

- name: Build playground
run: npm run build

Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/release-freshness.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Release freshness

on:
schedule:
- cron: "23 2 * * *"
workflow_dispatch:

permissions:
contents: read

jobs:
freshness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 22.14.0
- name: Compare documentation with the latest published CLI
env:
GH_TOKEN: ${{ github.token }}
run: node scripts/cli-release.mjs --live
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ The Playground publishes canonical, Open Graph, Twitter Card, and `WebApplicatio

[`public/robots.txt`](./public/robots.txt) permits public search crawling and advertises both sitemaps. [`public/llms.txt`](./public/llms.txt) and [`docs/public/llms.txt`](./docs/public/llms.txt) provide curated agent entry points. The VitePress build also emits clean Markdown alternatives for every documentation page and generates `/llms-full.txt` from the complete English documentation, so the agent-facing content stays synchronized with its public source.

## Scope
## Keeping CLI documentation current

`scripts/docs-validation.config.mjs` pins the published CLI version and its exact release commit. Run `npm run docs:release:sync` after a stable CLI release to update that pin and all four Getting Started version declarations together. Review any changed command behavior, then run `STACK_CLI_BIN=/absolute/path/to/the/verified/release/stack npm run docs:smoke` and the normal build checks before opening a PR. The sync command does not install a CLI, accept provider terms, or publish changes.

The required CI baseline compares the pin against GitHub's latest stable release and resolved tag commit, executes documentation commands using both the pinned source and the attested published Linux archive, and verifies that an unsupported flag fails the documentation smoke. Offline documentation builds check locale version consistency without contacting GitHub. The read-only `Release freshness` workflow checks daily and on manual dispatch, so a CLI-only release cannot remain silently stale until someone edits this repository. A failed run requires a synchronization PR and a verified Web deployment; this workflow does not auto-merge or auto-publish. GitHub scheduled runs may be delayed, and GitHub Actions notification preferences govern failure notifications.

## Playground scope

The initial playground includes:

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The fastest way to use Stack is the browser [Playground](https://stack-diagram.c

## Install the native CLI

For terminal workflows and local automation, install the owner-maintained Homebrew formula with `brew install stack-sh/tap/stack`. It uses the canonical Stack CLI 0.3.0 release archive and supports Apple Silicon macOS plus glibc-based Linux on arm64 and x86_64 when the host meets Homebrew's current tier-1 requirements. Homebrew owns upgrades through `brew upgrade stack-sh/tap/stack`; uninstalling the formula leaves your Stack configuration and icon store in place. See the [CLI distribution contract](https://github.com/stack-sh/cli/blob/main/docs/distribution.md#homebrew-installation) for the exact platform matrix, direct-install alternative, and recovery policy.
For terminal workflows and local automation, install the owner-maintained Homebrew formula with `brew install stack-sh/tap/stack`. It uses the canonical Stack CLI 0.4.0 release archive and supports Apple Silicon macOS plus glibc-based Linux on arm64 and x86_64 when the host meets Homebrew's current tier-1 requirements. Homebrew owns upgrades through `brew upgrade stack-sh/tap/stack`; uninstalling the formula leaves your Stack configuration and icon store in place. See the [CLI distribution contract](https://github.com/stack-sh/cli/blob/main/docs/distribution.md#homebrew-installation) for the exact platform matrix, direct-install alternative, and recovery policy.

## Write your first document

Expand Down
2 changes: 1 addition & 1 deletion docs/ja/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Native CLIをinstallする

Terminal workflowとlocal automationでは、owner管理のHomebrew formulaを`brew install stack-sh/tap/stack`でinstallします。CanonicalなStack CLI 0.3.0 release archiveを使用し、Homebrewの現行Tier 1要件を満たすApple Silicon macOSと、arm64 / x86_64のglibc Linuxをsupportします。UpgradeはHomebrewが`brew upgrade stack-sh/tap/stack`で管理し、formulaをuninstallしてもStackのconfigとicon storeは保持されます。正確なplatform matrix、direct install、recovery policyは[CLI distribution contract](https://github.com/stack-sh/cli/blob/main/docs/distribution.md#homebrew-installation)を参照してください。
Terminal workflowとlocal automationでは、owner管理のHomebrew formulaを`brew install stack-sh/tap/stack`でinstallします。CanonicalなStack CLI 0.4.0 release archiveを使用し、Homebrewの現行Tier 1要件を満たすApple Silicon macOSと、arm64 / x86_64のglibc Linuxをsupportします。UpgradeはHomebrewが`brew upgrade stack-sh/tap/stack`で管理し、formulaをuninstallしてもStackのconfigとicon storeは保持されます。正確なplatform matrix、direct install、recovery policyは[CLI distribution contract](https://github.com/stack-sh/cli/blob/main/docs/distribution.md#homebrew-installation)を参照してください。

## 最初のdocumentを書く

Expand Down
2 changes: 1 addition & 1 deletion docs/ko/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Stack을 가장 빠르게 사용하는 방법은 브라우저 [Playground](https

## 네이티브 CLI 설치

터미널 워크플로와 로컬 자동화에서는 `brew install stack-sh/tap/stack`으로 Stack이 관리하는 Homebrew formula를 설치합니다. 이 formula는 표준 Stack CLI 0.3.0 릴리스 아카이브를 사용하며, Homebrew의 현재 Tier 1 요구 사항을 충족하는 Apple Silicon macOS와 arm64 / x86_64 glibc Linux를 지원합니다. 업그레이드는 Homebrew가 `brew upgrade stack-sh/tap/stack`으로 관리하며, formula를 제거해도 Stack 설정과 아이콘 저장소는 유지됩니다. 정확한 플랫폼 매트릭스, 직접 설치 방법 및 복구 정책은 [CLI 배포 계약](https://github.com/stack-sh/cli/blob/main/docs/distribution.md#homebrew-installation)을 참고하세요.
터미널 워크플로와 로컬 자동화에서는 `brew install stack-sh/tap/stack`으로 Stack이 관리하는 Homebrew formula를 설치합니다. 이 formula는 표준 Stack CLI 0.4.0 릴리스 아카이브를 사용하며, Homebrew의 현재 Tier 1 요구 사항을 충족하는 Apple Silicon macOS와 arm64 / x86_64 glibc Linux를 지원합니다. 업그레이드는 Homebrew가 `brew upgrade stack-sh/tap/stack`으로 관리하며, formula를 제거해도 Stack 설정과 아이콘 저장소는 유지됩니다. 정확한 플랫폼 매트릭스, 직접 설치 방법 및 복구 정책은 [CLI 배포 계약](https://github.com/stack-sh/cli/blob/main/docs/distribution.md#homebrew-installation)을 참고하세요.

## 첫 문서 작성

Expand Down
2 changes: 1 addition & 1 deletion docs/zh/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## 安装原生 CLI

对于终端工作流和本地自动化,请使用 `brew install stack-sh/tap/stack` 安装由 Stack 维护的 Homebrew formula。它使用规范的 Stack CLI 0.3.0 发布归档,并支持符合 Homebrew 当前 Tier 1 要求的 Apple Silicon macOS,以及 arm64 / x86_64 的 glibc Linux。Homebrew 通过 `brew upgrade stack-sh/tap/stack` 管理升级;卸载 formula 不会删除 Stack 配置和图标存储。有关准确的平台矩阵、直接安装方式和恢复策略,请参阅 [CLI 分发约定](https://github.com/stack-sh/cli/blob/main/docs/distribution.md#homebrew-installation)。
对于终端工作流和本地自动化,请使用 `brew install stack-sh/tap/stack` 安装由 Stack 维护的 Homebrew formula。它使用规范的 Stack CLI 0.4.0 发布归档,并支持符合 Homebrew 当前 Tier 1 要求的 Apple Silicon macOS,以及 arm64 / x86_64 的 glibc Linux。Homebrew 通过 `brew upgrade stack-sh/tap/stack` 管理升级;卸载 formula 不会删除 Stack 配置和图标存储。有关准确的平台矩阵、直接安装方式和恢复策略,请参阅 [CLI 分发约定](https://github.com/stack-sh/cli/blob/main/docs/distribution.md#homebrew-installation)。

## 编写第一份文档

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"deploy": "npm run build && wrangler deploy",
"dev": "vite",
"docs:build": "npm run docs:check && vitepress build docs && node scripts/validate-docs-output.mjs",
"docs:check": "node scripts/validate-docs.mjs && node scripts/validate-docs-examples.mjs && npm run examples:check",
"docs:smoke": "node scripts/validate-docs-cli.mjs",
"docs:check": "node scripts/cli-release.mjs && node scripts/validate-docs.mjs && node scripts/validate-docs-examples.mjs && npm run examples:check",
"docs:release:check": "node scripts/cli-release.mjs --live",
"docs:release:sync": "node scripts/cli-release.mjs --sync",
"docs:smoke": "node scripts/validate-docs-cli.mjs && node scripts/validate-docs-cli-negative.mjs",
"docs:test": "node --test scripts/*.test.mjs",
"docs:dev": "vitepress dev docs",
"docs:preview": "vitepress preview docs",
Expand Down
91 changes: 91 additions & 0 deletions scripts/cli-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { readFile, writeFile } from "node:fs/promises"
import { fileURLToPath } from "node:url"
import path from "node:path"

export function validateRelease(lock, release, revision) {
const errors = []
if (release.draft || release.prerelease || !/^v\d+\.\d+\.\d+$/.test(release.tag_name)) {
errors.push("Expected a published stable CLI release")
}
if (release.tag_name !== `v${lock.version}`) {
errors.push(
`CLI release drift: docs ${lock.version}, latest ${release.tag_name}; run npm run docs:release:sync`,
)
}
if (revision !== lock.revision)
errors.push("CLI release tag commit differs from the documentation pin")
return errors
}

export function synchronizeVersion(source, version) {
if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error("Invalid CLI version")
if (!/Stack CLI \d+\.\d+\.\d+/.test(source)) throw new Error("Missing CLI version declaration")
return source.replace(/Stack CLI \d+\.\d+\.\d+/g, `Stack CLI ${version}`)
}

async function github(endpoint) {
const headers = { Accept: "application/vnd.github+json" }
if (process.env.GH_TOKEN) headers.Authorization = `Bearer ${process.env.GH_TOKEN}`
const response = await fetch(`https://api.github.com/repos/stack-sh/cli/${endpoint}`, {
headers,
signal: AbortSignal.timeout(15000),
})
if (!response.ok) throw new Error(`GitHub ${endpoint}: HTTP ${response.status}`)
return response.json()
}

export async function latestRelease() {
const release = await github("releases/latest")
if (release.draft || release.prerelease || !/^v\d+\.\d+\.\d+$/.test(release.tag_name)) {
throw new Error("Expected a published stable CLI release")
}
let { object } = await github(`git/ref/tags/${encodeURIComponent(release.tag_name)}`)
for (let depth = 0; object.type === "tag" && depth < 4; depth += 1) {
;({ object } = await github(`git/tags/${object.sha}`))
}
if (object.type !== "commit" || !/^[a-f0-9]{40}$/.test(object.sha)) {
throw new Error("CLI release tag does not resolve to a commit")
}
return { release, revision: object.sha }
}

async function main() {
const { documentationContract } = await import("./docs-validation.config.mjs")
const configPath = new URL("./docs-validation.config.mjs", import.meta.url)
const { cli } = documentationContract
const sync = process.argv.includes("--sync")
let version = cli.version
if (sync || process.argv.includes("--live")) {
const { release, revision } = await latestRelease()
if (sync) {
version = release.tag_name.slice(1)
const config = await readFile(configPath, "utf8")
await writeFile(
configPath,
config
.replace(cli.revision, revision)
.replace(`version: "${cli.version}"`, `version: "${version}"`),
)
} else {
const errors = validateRelease(cli, release, revision)
if (errors.length) throw new Error(errors.join("\n"))
}
}
for (const locale of ["", ...documentationContract.locales]) {
const page = new URL(
`../docs/${locale ? `${locale}/` : ""}guide/getting-started.md`,
import.meta.url,
)
const source = await readFile(page, "utf8")
const updated = synchronizeVersion(source, version)
if (sync) await writeFile(page, updated)
else if (source !== updated) throw new Error(`Stale CLI version in ${fileURLToPath(page)}`)
}
console.log(
`Validated CLI ${version} documentation${sync ? " and synchronized release pin" : ""}.`,
)
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main()
}
30 changes: 30 additions & 0 deletions scripts/cli-release.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import assert from "node:assert/strict"
import test from "node:test"
import { synchronizeVersion, validateRelease } from "./cli-release.mjs"

const lock = { version: "0.4.0", revision: "a".repeat(40) }
const release = { tag_name: "v0.4.0", draft: false, prerelease: false }

test("matches a published release and its exact commit", () => {
assert.deepEqual(validateRelease(lock, release, lock.revision), [])
})
test("rejects a new release omitted from docs", () => {
assert.match(
validateRelease(lock, { ...release, tag_name: "v0.5.0" }, lock.revision).join(),
/release drift/,
)
})
test("rejects draft, prerelease, and retargeted tags", () => {
for (const field of ["draft", "prerelease"]) {
assert.ok(validateRelease(lock, { ...release, [field]: true }, lock.revision).length)
}
assert.ok(validateRelease(lock, release, "b".repeat(40)).length)
})
test("updates all declarations without changing unrelated prose or versions", () => {
assert.equal(
synchronizeVersion("Stack CLI 0.3.0, Engine 0.7.0, Stack CLI 0.3.0", "0.4.0"),
"Stack CLI 0.4.0, Engine 0.7.0, Stack CLI 0.4.0",
)
assert.throws(() => synchronizeVersion("No declaration", "0.4.0"))
assert.throws(() => synchronizeVersion("Stack CLI 0.3.0", "main"))
})
4 changes: 2 additions & 2 deletions scripts/docs-validation.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export const documentationContract = {
],
cli: {
repository: "stack-sh/cli",
revision: "ca4a3c8f3eba3bac374f120e05151ac516de0faa",
version: "0.3.0",
revision: "7f4066884f7902d4c582d45184f3a9019fe59bf7",
version: "0.4.0",
executionExceptions: [
{
prefix: "stack icons import ",
Expand Down
34 changes: 34 additions & 0 deletions scripts/validate-docs-cli-negative.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import assert from "node:assert/strict"
import { execFileSync } from "node:child_process"
import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"

if (!process.env.STACK_CLI_BIN) throw new Error("STACK_CLI_BIN is required")
const temporary = await mkdtemp(path.join(os.tmpdir(), "stack-docs-negative-"))
try {
const docsRoot = path.join(temporary, "docs")
await cp("docs", docsRoot, { recursive: true, filter: (entry) => !entry.includes(".vitepress") })
for (const locale of ["", "ja", "zh", "ko"]) {
const page = path.join(docsRoot, locale, "guide/getting-started.md")
await writeFile(
page,
`${await readFile(page, "utf8")}\n\`\`\`sh\n$ stack check architecture.stack --not-a-real-option\n\`\`\`\n`,
)
}
assert.throws(
() =>
execFileSync(process.execPath, ["scripts/validate-docs-cli.mjs"], {
env: { ...process.env, STACK_DOCS_ROOT: docsRoot },
stdio: "pipe",
}),
(error) => {
assert.notEqual(error.status, 0)
assert.match(error.stderr.toString(), /unexpected argument.*--not-a-real-option/)
return true
},
)
console.log("Documentation smoke rejects unsupported CLI flags.")
} finally {
await rm(temporary, { recursive: true, force: true })
}
2 changes: 1 addition & 1 deletion scripts/validate-docs-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const execute = promisify(execFile)
const cli = process.env.STACK_CLI_BIN
if (!cli) throw new Error("STACK_CLI_BIN must point to the pinned Stack CLI binary")

const docsRoot = path.resolve("docs")
const docsRoot = path.resolve(process.env.STACK_DOCS_ROOT ?? "docs")
const { documents, cliExamples } = await validateDocumentationContract({
docsRoot,
locales: documentationContract.locales,
Expand Down