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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Read [How Cascade works](https://stablekernel.github.io/cascade/start/how-it-wor
go install github.com/stablekernel/cascade/cmd/cascade@latest
```

See [Getting started](https://stablekernel.github.io/cascade/start/getting-started/) for the pinned-version install and the `setup-cli` action, both of which most teams should use instead of a bare `@latest` install.
See [Getting started](https://stablekernel.github.io/cascade/start/getting-started/) for the pinned-version install and the `setup-cli` action, both of which most teams should use instead of a bare `@latest` install. If your organization restricts Actions to an allowlist, generate with `--cli-install=binary` so the generated workflows install the CLI inline with no third-party action (same signed-release verification); see the [`--cli-install` flag](https://stablekernel.github.io/cascade/reference/cli/#installing-the-cli-in-generated-workflows).

Write a manifest, write your build and deploy callbacks, then generate. Callbacks must exist first: the generator reads their `workflow_call` outputs to wire the rest.

Expand Down
22 changes: 22 additions & 0 deletions docs/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,28 @@ cascade generate-workflow
| `--push`, `-p` | bool | false | Push (implies `--commit`) |
| `--orchestrate-only` | bool | false | Only generate the orchestrate workflow |
| `--promote-only` | bool | false | Only generate the promote workflow |
| `--cli-install` | string | `action` | How generated workflows install the cascade CLI: `action` or `binary` |

#### Installing the CLI in generated workflows

Every generated workflow installs the cascade CLI before it runs. `--cli-install`
selects how:

- `action` (default): the workflow uses the `setup-cli` composite action
(`stablekernel/cascade/.github/actions/setup-cli`). This is unchanged from prior
releases.
- `binary`: the workflow installs the CLI with a self-contained step that uses no
third-party action at all. It resolves the release tag, detects the runner
OS/arch, installs a checksum-verified [cosign](https://github.com/sigstore/cosign)
by direct download, and runs the same mandatory sha256 gate and keyless cosign
verification the action performs. Both modes share one install script, so the
authenticity guarantees are identical - see
[Verifying cascade releases](/cascade/security/#verifying-cascade-releases).

Use `binary` when your organization restricts GitHub Actions to an allowlist:
binary mode needs no allowlist entry for a third-party action, because it uses
none. Both modes install the same signed release binary with the same
verification; the choice is purely about how that install step is expressed.

#### Generated workflow features

Expand Down
6 changes: 6 additions & 0 deletions docs/src/content/docs/start/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ You rarely need to invoke Cascade manually in CI: generated workflows install it

The action downloads the release archive, extracts it, and puts `cascade` on `PATH`.

If your organization restricts GitHub Actions to an allowlist, you can instead
have `generate-workflow` emit a self-contained install that uses no third-party
action: run it with `--cli-install=binary`. The generated workflows then install
the CLI inline, with the same signed-release verification, and need no allowlist
entry. See [`--cli-install`](/cascade/reference/cli/#installing-the-cli-in-generated-workflows).

## Scaffold with `cascade init`

The fastest path to a working manifest is `cascade init`. It renders the manifest and callback stubs, verifies them through the real generator, and writes them into your repository:
Expand Down
22 changes: 21 additions & 1 deletion internal/generate/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func NewCommand() *cobra.Command {
var orchestrateOnly bool
var promoteOnly bool
var ownRepo bool
var cliInstall string

cmd := &cobra.Command{
Use: "generate-workflow",
Expand All @@ -45,6 +46,10 @@ the "ci" key by default. Use --manifest-key to change the key name.`,
if push {
commit = true
}
installMode, err := parseCLIInstallMode(cliInstall)
if err != nil {
return err
}
opts := generateOptions{
configPath: configPath,
manifestKey: manifestKey,
Expand All @@ -59,6 +64,7 @@ the "ci" key by default. Use --manifest-key to change the key name.`,
orchestrateOnly: orchestrateOnly,
promoteOnly: promoteOnly,
ownRepo: ownRepo,
cliInstallMode: installMode,
}
return runGenerateWorkflow(opts)
},
Expand All @@ -77,6 +83,7 @@ the "ci" key by default. Use --manifest-key to change the key name.`,
cmd.Flags().BoolVar(&orchestrateOnly, "orchestrate-only", false, "Only generate orchestrate.yaml")
cmd.Flags().BoolVar(&promoteOnly, "promote-only", false, "Only generate promote.yaml")
cmd.Flags().BoolVar(&ownRepo, "own-repo", false, "Generate cascade's own-repo release-plumbing variant (tag-only manage-release, non-triggering tag-create). Maintainer-only; never used by a downstream manifest.")
cmd.Flags().StringVar(&cliInstall, "cli-install", "action", "How generated workflows install the cascade CLI: \"action\" (setup-cli composite action, default) or \"binary\" (self-contained install with no third-party action, so no org Actions allowlist entry is needed)")

return cmd
}
Expand All @@ -96,6 +103,7 @@ type generateOptions struct {
orchestrateOnly bool
promoteOnly bool
ownRepo bool
cliInstallMode cliInstallMode
}

func runGenerateWorkflow(opts generateOptions) error {
Expand Down Expand Up @@ -161,6 +169,7 @@ func runGenerateWorkflow(opts generateOptions) error {
return fmt.Errorf("planning orchestrate workflow: %w", err)
}
for _, t := range orchTargets {
t.Gen.setInstallMode(opts.cliInstallMode)
for _, w := range t.Gen.Validate() {
fmt.Fprintf(os.Stderr, "%s\n", w)
}
Expand Down Expand Up @@ -203,7 +212,9 @@ func runGenerateWorkflow(opts generateOptions) error {
if generatePromote {
if cfg.IsSingleEnvironment() {
// Single-environment projects get a simpler Release workflow.
content, err := NewReleaseGenerator(cfg, baseDir).Generate()
relGen := NewReleaseGenerator(cfg, baseDir)
relGen.setInstallMode(opts.cliInstallMode)
content, err := relGen.Generate()
if err != nil {
return fmt.Errorf("generating release workflow: %w", err)
}
Expand All @@ -225,6 +236,7 @@ func runGenerateWorkflow(opts generateOptions) error {
return fmt.Errorf("planning promote workflow: %w", perr)
}
for _, t := range promoteTargets {
t.Gen.setInstallMode(opts.cliInstallMode)
content, err := t.Gen.Generate()
if err != nil {
return fmt.Errorf("generating promote workflow %s: %w", t.Path, err)
Expand All @@ -248,6 +260,7 @@ func runGenerateWorkflow(opts generateOptions) error {
// Generate external-update workflow for primary repos
if cfg.IsPrimary() {
externalGen := NewExternalUpdateGenerator(cfg, baseDir)
externalGen.setInstallMode(opts.cliInstallMode)
content, err := externalGen.Generate()
if err != nil {
return fmt.Errorf("generating external-update workflow: %w", err)
Expand All @@ -272,6 +285,7 @@ func runGenerateWorkflow(opts generateOptions) error {

// Generate the opt-in manifest-validation PR check (validate_check.enabled).
validateCheckGen := NewValidateCheckGenerator(cfg, baseDir)
validateCheckGen.setInstallMode(opts.cliInstallMode)
if validateCheckGen.Enabled() {
content, err := validateCheckGen.Generate()
if err != nil {
Expand All @@ -292,6 +306,7 @@ func runGenerateWorkflow(opts generateOptions) error {

// Generate the opt-in merge-queue validation lane (merge_queue.enabled).
mergeQueueGen := NewMergeQueueGenerator(cfg, baseDir)
mergeQueueGen.setInstallMode(opts.cliInstallMode)
if mergeQueueGen.Enabled() {
content, err := mergeQueueGen.Generate()
if err != nil {
Expand All @@ -318,6 +333,7 @@ func runGenerateWorkflow(opts generateOptions) error {
return fmt.Errorf("planning hotfix workflow: %w", err)
}
for _, t := range hfTargets {
t.Gen.setInstallMode(opts.cliInstallMode)
content, err := t.Gen.Generate()
if err != nil {
return fmt.Errorf("generating hotfix workflow %s: %w", t.Path, err)
Expand All @@ -343,6 +359,7 @@ func runGenerateWorkflow(opts generateOptions) error {
return fmt.Errorf("planning rollback workflow: %w", err)
}
for _, t := range rbTargets {
t.Gen.setInstallMode(opts.cliInstallMode)
content, err := t.Gen.Generate()
if err != nil {
return fmt.Errorf("generating rollback workflow %s: %w", t.Path, err)
Expand All @@ -363,6 +380,7 @@ func runGenerateWorkflow(opts generateOptions) error {
// disabled pr_preview emits nothing, so existing manifests are unaffected.
if cfg.PRPreview.IsEnabled() {
previewGen := NewPRPreviewGenerator(cfg, baseDir)
previewGen.setInstallMode(opts.cliInstallMode)
content, err := previewGen.Generate()
if err != nil {
return fmt.Errorf("generating pr-preview workflow: %w", err)
Expand All @@ -386,6 +404,7 @@ func runGenerateWorkflow(opts generateOptions) error {
// drift_check emits nothing, so existing manifests are unaffected. The
// comment companion is emitted only when drift_check.comment is also set.
driftGen := NewDriftCheckGenerator(cfg, baseDir)
driftGen.setInstallMode(opts.cliInstallMode)
if driftGen.Enabled() {
content, err := driftGen.Generate()
if err != nil {
Expand Down Expand Up @@ -426,6 +445,7 @@ func runGenerateWorkflow(opts generateOptions) error {
// the pull_request detector plus its workflow_run companion. Absent or
// disabled reconcile emits nothing, so existing manifests are unaffected.
reconcileGen := NewReconcileGenerator(cfg, baseDir)
reconcileGen.setInstallMode(opts.cliInstallMode)
if reconcileGen.Enabled() {
content, err := reconcileGen.Generate()
if err != nil {
Expand Down
8 changes: 5 additions & 3 deletions internal/generate/drift_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const driftCommentWorkflowName = "Cascade Drift Comment"
// pull_requests array, or a head-SHA lookup for fork PRs), never from the
// fork-controlled artifact, so a fork cannot redirect the comment at another PR.
type DriftCheckGenerator struct {
installModeHolder
config *config.TrunkConfig
baseDir string
}
Expand Down Expand Up @@ -140,9 +141,10 @@ func (g *DriftCheckGenerator) writeCheckJob(sb *strings.Builder) {
// github.token is the built-in Actions token, sufficient to authenticate
// gh release download against the public stablekernel/cascade repository.
writeSetupCLIStep(sb, setupCLIStep{
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
})
sb.WriteString("\n")

Expand Down
2 changes: 2 additions & 0 deletions internal/generate/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

// ExternalUpdateGenerator handles external-update workflow generation
type ExternalUpdateGenerator struct {
installModeHolder
config *config.TrunkConfig
baseDir string
}
Expand Down Expand Up @@ -231,6 +232,7 @@ func (g *ExternalUpdateGenerator) writeJob(sb *strings.Builder) {

// Setup CLI
writeSetupCLIStep(sb, setupCLIStep{
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: g.getReleaseTokenRef(),
Expand Down
3 changes: 3 additions & 0 deletions internal/generate/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ func writeGitConfigSteps(sb *strings.Builder, cfg *config.TrunkConfig, indent st

// Generator handles workflow file generation
type Generator struct {
installModeHolder
config *config.TrunkConfig
baseDir string
outputs map[string][]string // callback name -> output names
Expand Down Expand Up @@ -1070,6 +1071,7 @@ func (g *Generator) writeSetupJob(sb *strings.Builder) {

// Setup CLI
writeSetupCLIStep(sb, setupCLIStep{
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: g.getReleaseTokenRef(),
Expand Down Expand Up @@ -2302,6 +2304,7 @@ func (g *Generator) writeFinalizeCLIBootstrap(sb *strings.Builder) {
// it is emitted as the step's if: guard.
func (g *Generator) writeFinalizePinnedCLI(sb *strings.Builder, cond string) {
writeSetupCLIStep(sb, setupCLIStep{
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: g.getReleaseTokenRef(),
Expand Down
8 changes: 5 additions & 3 deletions internal/generate/hotfix.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const hotfixConflictLabel = "cascade-hotfix-conflict"
// when two or more environments are declared, because a single-environment
// pipeline has no intermediate target to hotfix onto.
type HotfixGenerator struct {
installModeHolder
config *config.TrunkConfig
baseDir string

Expand Down Expand Up @@ -920,9 +921,10 @@ func (g *HotfixGenerator) writeSetupCLI(sb *strings.Builder) {
// github.token is the built-in Actions token, sufficient to authenticate
// gh release download against the public stablekernel/cascade repository.
writeSetupCLIStep(sb, setupCLIStep{
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
})
}

Expand Down
8 changes: 5 additions & 3 deletions internal/generate/merge_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
// side-effecting orchestrate workflow on a speculative merge-queue build with no
// gh-readonly-queue guard; merge_queue.enabled emits this read-only lane instead.
type MergeQueueGenerator struct {
installModeHolder
config *config.TrunkConfig
baseDir string
}
Expand Down Expand Up @@ -105,9 +106,10 @@ func (g *MergeQueueGenerator) writeJob(sb *strings.Builder) {
// github.token is the built-in Actions token, sufficient to authenticate
// gh release download against the public stablekernel/cascade repository.
writeSetupCLIStep(sb, setupCLIStep{
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
})

// Validity gate: lint --json reports validity in its JSON output, so gate
Expand Down
8 changes: 5 additions & 3 deletions internal/generate/pr_preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const prPreviewMarker = "<!-- cascade-pr-preview -->"
// dry_run is enforced (--dry-run is always passed, never sourced from an input)
// so the preview can never create a release, write state, or trigger a deploy.
type PRPreviewGenerator struct {
installModeHolder
config *config.TrunkConfig
baseDir string
}
Expand Down Expand Up @@ -118,9 +119,10 @@ func (g *PRPreviewGenerator) writeJob(sb *strings.Builder) {
// authenticate gh release download against the public stablekernel/cascade
// repository and requires no adopter configuration.
writeSetupCLIStep(sb, setupCLIStep{
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
})
sb.WriteString("\n")

Expand Down
4 changes: 4 additions & 0 deletions internal/generate/promote.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

// PromoteGenerator handles promote workflow generation
type PromoteGenerator struct {
installModeHolder
config *config.TrunkConfig
baseDir string
inputs map[string][]string // deploy name -> input names
Expand Down Expand Up @@ -666,6 +667,7 @@ func (g *PromoteGenerator) writePreflightJob(sb *strings.Builder) {

// Setup CLI
writeSetupCLIStep(sb, setupCLIStep{
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: g.getReleaseTokenRef(),
Expand Down Expand Up @@ -719,6 +721,7 @@ func (g *PromoteGenerator) writePromoteJob(sb *strings.Builder) {
writeMintSteps(sb, g.config, " ", seamRelease)
writeActionStep(sb, g.config, " ", actionCheckout)
writeSetupCLIStep(sb, setupCLIStep{
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: g.getReleaseTokenRef(),
Expand Down Expand Up @@ -1112,6 +1115,7 @@ func (g *PromoteGenerator) writeFinalizeJob(sb *strings.Builder) {
sb.WriteString(" with:\n")
sb.WriteString(" fetch-depth: 0\n")
writeSetupCLIStep(sb, setupCLIStep{
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: g.getReleaseTokenRef(),
Expand Down
17 changes: 10 additions & 7 deletions internal/generate/reconcile_companion.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const reconcileFollowupBranchExpr = "cascade-reconcile/pr-${{ steps.resolve.outp
// as data, runs the pinned cascade binary to adopt the bump into the manifest,
// and pushes (or opens a followup PR) with the trigger-capable state token.
type ReconcileGenerator struct {
installModeHolder
config *config.TrunkConfig
baseDir string
ownRepo bool
Expand Down Expand Up @@ -147,9 +148,10 @@ func (g *ReconcileGenerator) writeCheckJob(sb *strings.Builder) {
// github.token is the built-in Actions token, sufficient to authenticate
// gh release download against the public stablekernel/cascade repository.
writeSetupCLIStep(sb, setupCLIStep{
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
})
sb.WriteString("\n")

Expand Down Expand Up @@ -413,10 +415,11 @@ func (g *ReconcileGenerator) writeCompanionCheckoutStep(sb *strings.Builder) {
// run` off the repository's own (possibly malicious) source tree.
func (g *ReconcileGenerator) writeCompanionSetupCLIStep(sb *strings.Builder) {
writeSetupCLIStep(sb, setupCLIStep{
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
ifExpr: "steps.resolve.outputs.relevant == 'true'",
installMode: g.installMode,
ref: g.getCLIRef(),
version: g.config.GetCLIVersion(),
token: "${{ github.token }}",
ifExpr: "steps.resolve.outputs.relevant == 'true'",
})
sb.WriteString("\n")
}
Expand Down
Loading