From 11b05ead0586a3a9b235f56e969bbadcd8b7b41a Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 18 Jul 2026 03:51:42 -0400 Subject: [PATCH 1/3] test(e2e): walk both scenario tiers in gitignore guard The guard globbed scenarios/*/*.y*ml, covering only subdirectory scenarios and none of the root-tier files. A root-tier scenario swallowed by a .gitignore rule (the cascade-* class the guard exists to catch) stayed invisible. Walk the whole scenarios tree so both tiers are checked. Signed-off-by: Joshua Temple --- e2e/scenarios_integrity_test.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/e2e/scenarios_integrity_test.go b/e2e/scenarios_integrity_test.go index 243d4f6..879a41e 100644 --- a/e2e/scenarios_integrity_test.go +++ b/e2e/scenarios_integrity_test.go @@ -1,6 +1,7 @@ package e2e import ( + "io/fs" "os/exec" "path/filepath" "strings" @@ -34,7 +35,23 @@ func TestScenarios_AreNotGitIgnored(t *testing.T) { t.Skip("git not available") } - files, err := filepath.Glob("scenarios/*/*.y*ml") + // Walk the whole tree so both tiers are covered: root-level scenarios + // (scenarios/02-two-env-repo.yaml) and subdirectory scenarios + // (scenarios/hotfix/x.yaml). A single-depth glob missed the root tier, the + // very tier the `cascade-*` incident struck, leaving it invisible again. + var files []string + err := filepath.WalkDir("scenarios", func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if d.IsDir() { + return nil + } + if ext := filepath.Ext(path); ext == ".yaml" || ext == ".yml" { + files = append(files, path) + } + return nil + }) require.NoError(t, err) require.NotEmpty(t, files, "no scenario files found; the corpus or its path moved") From 3836bdadc565a973f737771cf8463288be4c802c Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 18 Jul 2026 03:51:42 -0400 Subject: [PATCH 2/3] test(schema): recognize annotated yaml fences in docs guard extractYAMLFences matched only the exact yaml/yml info string, so a Starlight fence like yaml title="cascade.yaml" escaped TestSchema_ValidatesDocsExamples. Match yaml/yml as a whole leading token followed by end-of-string or whitespace, without over-matching yamlfoo or yaml-lint. Signed-off-by: Joshua Temple --- internal/schema/schema_test.go | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/internal/schema/schema_test.go b/internal/schema/schema_test.go index 5274401..d2d0db6 100644 --- a/internal/schema/schema_test.go +++ b/internal/schema/schema_test.go @@ -229,7 +229,11 @@ func firstMeaningfulLineIsCI(block string) bool { return false } -// extractYAMLFences returns the contents of every ```yaml fenced code block. +// extractYAMLFences returns the contents of every ```yaml fenced code block, +// including fences that carry Starlight-style info-string attributes such as +// ```yaml title="cascade.yaml". An annotated fence is still a yaml fence and must +// not escape validation; only the exact ```yaml / ```yml used to match, so the +// first annotated example would silently dodge the schema check. func extractYAMLFences(md string) []string { var blocks []string lines := strings.Split(md, "\n") @@ -238,7 +242,7 @@ func extractYAMLFences(md string) []string { for _, line := range lines { trimmed := strings.TrimSpace(line) if !inBlock { - if trimmed == "```yaml" || trimmed == "```yml" { + if isYAMLFenceOpen(trimmed) { inBlock = true cur = nil } @@ -254,6 +258,28 @@ func extractYAMLFences(md string) []string { return blocks } +// isYAMLFenceOpen reports whether a trimmed line opens a yaml code fence. The +// info string must begin with the whole token "yaml" or "yml", optionally +// followed by whitespace and attributes (```yaml title="x"). It deliberately +// does not match neighbours like ```yamlfoo or ```yaml-lint, which are different +// languages, not annotated yaml. +func isYAMLFenceOpen(trimmed string) bool { + info, ok := strings.CutPrefix(trimmed, "```") + if !ok { + return false + } + for _, lang := range []string{"yaml", "yml"} { + rest, ok := strings.CutPrefix(info, lang) + if !ok { + continue + } + if rest == "" || rest[0] == ' ' || rest[0] == '\t' { + return true + } + } + return false +} + func TestSchema_RejectsKnownBadManifests(t *testing.T) { sch := compileSchema(t) From 1008d5b31251da8063de514dd6e6e1d5952765a5 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 18 Jul 2026 03:51:42 -0400 Subject: [PATCH 3/3] docs(reference): trunk_branch is required with no default The generated-workflows reference claimed the push trigger reads config.trunk_branch with a default of main. Since trunk_branch became required, it carries no default: a manifest omitting it fails lint. Correct the claim to match the manifest reference and lint behavior. Signed-off-by: Joshua Temple --- CHANGELOG.md | 6 ++++++ docs/src/content/docs/reference/generated-workflows.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a28180..054aae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -300,6 +300,12 @@ A `Migration` section is added to any release that bumps `schema_version`. aggregate result would make a retry redeploy every environment including the ones that already succeeded ([#626](https://github.com/stablekernel/cascade/issues/626)). +- **reference:** The generated-workflows reference no longer describes the + orchestrate push trigger as reading `config.trunk_branch` with a default of + `main`. The field carries no default and is required, so the page now matches + the manifest reference and what `lint` enforces: a manifest that omits + `trunk_branch` fails rather than falling back to `main`. + ### Added - **test:** A durable emitted-field guard: a reflection walk over the diff --git a/docs/src/content/docs/reference/generated-workflows.md b/docs/src/content/docs/reference/generated-workflows.md index aec1e4e..c35f11d 100644 --- a/docs/src/content/docs/reference/generated-workflows.md +++ b/docs/src/content/docs/reference/generated-workflows.md @@ -40,7 +40,7 @@ flowchart TD class sn,vn,bn,dn,fn note; ``` -The trigger is written directly from `config.trunk_branch` (default `main`): +The trigger is written directly from `config.trunk_branch`, a required field with no default (the example below uses `main`): ```yaml on: