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
4 changes: 4 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Release tags must be visible so the DefaultCLIVersion release-lag
# guard runs instead of skipping on a tag-less shallow checkout.
fetch-tags: true

- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Release tags must be visible so the DefaultCLIVersion release-lag
# guard runs instead of skipping on a tag-less shallow checkout.
fetch-tags: true

- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ A `Migration` section is added to any release that bumps `schema_version`.

## [Unreleased]

### Fixed

- **config:** Validate `run_policy`, `on_failure`, and `retries` on the
`validate` block the same way as builds and deploys: `run_policy` and
`on_failure` must be one of their documented values and `retries` must stay
between 0 and 3, instead of out-of-range settings being silently accepted
- **config:** Bump the default `cli_version` pin for generated workflows to
v0.16.1, and guard the default against lagging the latest stable release
- **hotfix:** Converge the missing tag and release when the hotfix finalize
job reruns after a partial failure, instead of skipping release creation

## [0.16.1] - 2026-07-15

### Security
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ ci:
config:
schema_version: 1
trunk_branch: main
cli_version: v0.16.0
cli_version: v0.16.1
environments: [dev, staging, prod]
builds:
- name: app
Expand Down Expand Up @@ -105,7 +105,7 @@ ci:
config:
schema_version: 1
trunk_branch: main
cli_version: v0.16.0
cli_version: v0.16.1
environments: [dev, staging, prod]
builds:
- name: api
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/guides/adopt.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ ci:
config:
schema_version: 1
trunk_branch: main
cli_version: v0.16.0
cli_version: v0.16.1

environments: [dev, staging, prod]

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/guides/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ ci:
config:
schema_version: 1
trunk_branch: main
cli_version: v0.16.0
cli_version: v0.16.1
environments: [dev, staging, prod]

components:
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ cascade version
Output:

```text
cascade v0.16.0
cascade v0.16.1
commit: abc123d
built: 2026-01-15T10:30:00Z
```
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/reference/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ ci:
- name: prod
gha_environment: production
environment_url: "https://app.example.com"
cli_version: v0.16.0
cli_version: v0.16.1
```

Each entry is one of two forms:
Expand Down Expand Up @@ -195,7 +195,7 @@ These fields pin the cascade CLI and third-party actions the generated workflows
|-------|----------|
| `latest` | Most recent stable release (default). |
| `beta` | Newest prerelease build. |
| `vX.Y.Z` | A specific version (for example `v0.16.0`). Pin for reproducibility. |
| `vX.Y.Z` | A specific version (for example `v0.16.1`). Pin for reproducibility. |

### cli_version_sha

Expand Down
8 changes: 4 additions & 4 deletions docs/src/content/docs/start/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ go install github.com/stablekernel/cascade/cmd/cascade@latest
for the newest tag):

```bash
go install github.com/stablekernel/cascade/cmd/cascade@v0.16.0
go install github.com/stablekernel/cascade/cmd/cascade@v0.16.1
```

Verify the install:
Expand All @@ -34,7 +34,7 @@ cascade version
You rarely need to invoke Cascade manually in CI: generated workflows install it for you via the `setup-cli` composite action, pinned to whatever `cli_version` you set in your manifest. If you need to call it directly in a workflow of your own:

```yaml
- uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.0
- uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
```
Expand Down Expand Up @@ -70,7 +70,7 @@ ci:
schema_version: 1
trunk_branch: main
environments: [dev, test, prod]
cli_version: v0.16.0
cli_version: v0.16.1

validate:
workflow: .github/workflows/validate.yaml
Expand Down Expand Up @@ -113,7 +113,7 @@ ci:
config:
schema_version: 1
trunk_branch: main
cli_version: v0.16.0
cli_version: v0.16.1
builds:
- name: cli
workflow: .github/workflows/build-cli.yaml
Expand Down
6 changes: 3 additions & 3 deletions internal/config/default_cli_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ func TestDefaultCLIVersion_IsStableReleaseTag(t *testing.T) {
//
// The expectation is derived from the repository's own stable tags, so there
// is no network dependency: each release must bump DefaultCLIVersion in the
// same body of work that cuts the tag (see CONTRIBUTING.md). Shallow CI
// checkouts without tags skip; any full clone (every local inner loop and
// fetch-depth: 0 workflow) enforces the match.
// same body of work that cuts the tag (see CONTRIBUTING.md). Checkouts
// without tags skip; local full clones and the CI test jobs (whose checkout
// steps set fetch-tags: true) enforce the match.
func TestDefaultCLIVersion_MatchesLatestReleaseTag(t *testing.T) {
out, err := exec.Command("git", "tag", "--list", "v*").Output()
if err != nil {
Expand Down
79 changes: 79 additions & 0 deletions internal/config/docs_version_sync_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package config

import (
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)

// docsVersionPinPattern matches the concrete version pins that docs and the
// README present as current: `cli_version:` manifest examples, `go install
// .../cmd/cascade@vX.Y.Z`, tag-pinned `setup-cli@vX.Y.Z` uses lines, and the
// sample `cascade vX.Y.Z` version output. Deliberately outside the guard:
// sha-pinned `setup-cli@<sha> # vX.Y.Z` examples (the sha/tag pairing is
// internally consistent whatever release it names) and generic prose such as
// "for example `vX.Y.Z`".
var docsVersionPinPattern = regexp.MustCompile(
`(?:cli_version:\s*|cascade/cmd/cascade@|setup-cli@|cascade )(v\d+\.\d+\.\d+)`)

// TestDefaultCLIVersion_MatchesDocsExamples keeps the README and docs-site
// examples in lockstep with DefaultCLIVersion, so a release bump cannot leave
// the quickstart installing or pinning the previous CLI. The examples went
// stale on the v0.16.0 -> v0.16.1 bump; this turns that staleness into a
// failing gate instead of a per-release manual refresh.
func TestDefaultCLIVersion_MatchesDocsExamples(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
root := filepath.Clean(filepath.Join(wd, "..", ".."))

files := []string{filepath.Join(root, "README.md")}
docsRoot := filepath.Join(root, "docs", "src", "content", "docs")
err = filepath.WalkDir(docsRoot, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if ext := filepath.Ext(path); ext == ".md" || ext == ".mdx" {
files = append(files, path)
}
return nil
})
if err != nil {
t.Fatalf("walk docs content: %v", err)
}

pins := 0
for _, path := range files {
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
rel, err := filepath.Rel(root, path)
if err != nil {
rel = path
}
for i, line := range strings.Split(string(data), "\n") {
for _, m := range docsVersionPinPattern.FindAllStringSubmatch(line, -1) {
pins++
if m[1] != DefaultCLIVersion {
t.Errorf("%s:%d pins %q but DefaultCLIVersion is %q; refresh the example (line: %s)",
rel, i+1, m[1], DefaultCLIVersion, strings.TrimSpace(line))
}
}
}
}

// The guard only works while the docs actually contain pinned examples.
// If a docs restructure drops below this floor, re-point the pattern at
// the new example surface rather than deleting the check.
if pins < 5 {
t.Fatalf("found only %d version-pinned examples across README.md and docs/; expected at least 5, so the pattern no longer matches the docs surface", pins)
}
}