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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/generated-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 18 additions & 1 deletion e2e/scenarios_integrity_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package e2e

import (
"io/fs"
"os/exec"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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")

Expand Down
30 changes: 28 additions & 2 deletions internal/schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
}
Expand All @@ -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)

Expand Down