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
12 changes: 6 additions & 6 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ Examples:
var compileCmd = &cobra.Command{
Use: "compile [workflow]...",
Short: "Compile agentic workflow Markdown files into GitHub Actions YAML",
Long: `Compile one or more agentic workflow Markdown files into GitHub Actions YAML.
Long: `Compile one or more agentic workflows to YAML workflows.

If no workflows are specified, all Markdown files in .github/workflows will be compiled.

Expand Down Expand Up @@ -286,7 +286,7 @@ Examples:
failFast, _ := cmd.Flags().GetBool("fail-fast")
noCheckUpdate, _ := cmd.Flags().GetBool("no-check-update")
scheduleSeed, _ := cmd.Flags().GetString("schedule-seed")
approve, _ := cmd.Flags().GetBool("approve-updates")
approve, _ := cmd.Flags().GetBool("approve")
validateImages, _ := cmd.Flags().GetBool("validate-images")
priorManifestFile, _ := cmd.Flags().GetString("prior-manifest-file")
verbose, _ := cmd.Flags().GetBool("verbose")
Expand Down Expand Up @@ -399,7 +399,7 @@ Examples:
push, _ := cmd.Flags().GetBool("push")
dryRun, _ := cmd.Flags().GetBool("dry-run")
jsonOutput, _ := cmd.Flags().GetBool("json")
approveRun, _ := cmd.Flags().GetBool("approve-updates")
approveRun, _ := cmd.Flags().GetBool("approve")

if err := validateEngine(engineOverride); err != nil {
return err
Expand Down Expand Up @@ -694,7 +694,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
compileCmd.Flags().Bool("fail-fast", false, "Stop at the first validation error instead of collecting all errors")
compileCmd.Flags().Bool("no-check-update", false, "Skip checking for gh-aw updates")
compileCmd.Flags().String("schedule-seed", "", "Override the repository slug (owner/repo) used as seed for fuzzy schedule scattering (e.g. 'github/gh-aw'). Bypasses git remote detection entirely. Use this when your git remote is not named 'origin' and you have multiple remotes configured")
compileCmd.Flags().Bool("approve-updates", false, "Approve all safe update changes. When strict mode is active (the default), the compiler emits warnings for new restricted secrets or unapproved action additions/removals not present in the existing gh-aw-manifest. Use this flag to approve and skip safe update enforcement")
compileCmd.Flags().Bool("approve", false, "Approve all safe update changes. When strict mode is active (the default), the compiler emits warnings for new restricted secrets or unapproved action additions/removals not present in the existing gh-aw-manifest. Use this flag to approve and skip safe update enforcement")
Comment thread
dsyme marked this conversation as resolved.
compileCmd.Flags().Bool("validate-images", false, "Require Docker to be available for container image validation. Without this flag, container image validation is silently skipped when Docker is not installed or the daemon is not running")
compileCmd.Flags().String("prior-manifest-file", "", "Path to a JSON file containing pre-cached gh-aw-manifests (map[lockFile]*GHAWManifest); used by the MCP server to supply a tamper-proof manifest baseline captured at startup")
if err := compileCmd.Flags().MarkHidden("prior-manifest-file"); err != nil {
Expand Down Expand Up @@ -729,13 +729,13 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
runCmd.Flags().Bool("enable-if-needed", false, "Enable the workflow before running if needed, and restore state afterward")
runCmd.Flags().StringP("engine", "e", "", "Override AI engine (claude, codex, copilot, custom)")
runCmd.Flags().StringP("repo", "r", "", "Target repository ([HOST/]owner/repo format). Defaults to current repository")
runCmd.Flags().String("ref", "", "Branch or tag name to run the workflow on (e.g., main, v1.0.0)")
runCmd.Flags().String("ref", "", "Branch or tag name to run the workflow on (default: current branch)")
runCmd.Flags().Bool("auto-merge-prs", false, "Auto-merge any pull requests created during the workflow execution")
runCmd.Flags().StringArrayP("raw-field", "F", []string{}, "Add a string parameter in key=value format (can be used multiple times)")
runCmd.Flags().Bool("push", false, "Commit and push workflow files (including transitive imports) before running")
runCmd.Flags().Bool("dry-run", false, "Validate workflow without actually triggering execution on GitHub Actions")
runCmd.Flags().BoolP("json", "j", false, "Output results in JSON format")
runCmd.Flags().Bool("approve-updates", false, "Approve all safe update changes during compilation (skip safe update enforcement)")
runCmd.Flags().Bool("approve", false, "Approve all safe update changes during compilation (skip safe update enforcement)")
// Register completions for run command
runCmd.ValidArgsFunction = cli.CompleteWorkflowNames
cli.RegisterEngineFlagCompletion(runCmd)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/compile_compiler_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func configureCompilerFlags(compiler *workflow.Compiler, config CompileConfig) {
// regardless of the workflow's strict mode setting.
compiler.SetApprove(config.Approve)
if config.Approve {
compileCompilerSetupLog.Print("Safe update changes approved via --approve-updates flag: skipping safe update enforcement for new restricted secrets or unapproved action additions/removals")
compileCompilerSetupLog.Print("Safe update changes approved via --approve flag: skipping safe update enforcement for new restricted secrets or unapproved action additions/removals")
}

// Set require docker flag: when set, container image validation fails instead of
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/compile_safe_update_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func manifestLockFileWithSecret(secretName string) string {
}

// TestSafeUpdateFirstCompileCreatesBaseline verifies that the first compilation
// (with no prior manifest) still enforces safe update mode and emits a
// (with no prior lock file) enforces safe update mode and emits a
// SECURITY REVIEW REQUIRED warning so agents review newly introduced secrets.
// The compile itself succeeds (warnings do not fail the build) and the lock file
// written with the manifest serves as the baseline for future compilations.
Expand Down Expand Up @@ -122,7 +122,7 @@ func TestSafeUpdateFirstCompileCreatesBaseline(t *testing.T) {
"lock file should contain a gh-aw-manifest header after first compile")
assert.Contains(t, string(lockContent), "MY_API_SECRET",
"manifest should include the secret from the workflow")
t.Logf("First compile correctly created baseline without warnings.\nOutput:\n%s", outputStr)
t.Logf("First compile correctly emitted warnings.\nOutput:\n%s", outputStr)
}

// TestSafeUpdateFirstCompileCreatesBaselineForActions verifies that the first
Expand Down Expand Up @@ -400,7 +400,7 @@ func TestSafeUpdateManifestIncludesImportedSecret(t *testing.T) {
"should write workflow file")

// Compile with --approve so we can inspect the manifest freely without safe update warnings.
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve-updates")
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve")
cmd.Env = append(os.Environ(), "GH_AW_ACTION_MODE=release")
output, err := cmd.CombinedOutput()
outputStr := string(output)
Expand Down Expand Up @@ -509,7 +509,7 @@ func TestSafeUpdateManifestIncludesTransitivelyImportedSecret(t *testing.T) {
"should write workflow file")

// Compile with --approve so we can freely inspect the manifest without safe update warnings.
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve-updates")
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve")
cmd.Env = append(os.Environ(), "GH_AW_ACTION_MODE=release")
output, err := cmd.CombinedOutput()
outputStr := string(output)
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/upgrade_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Examples:
auditFlag, _ := cmd.Flags().GetBool("audit")
jsonOutput, _ := cmd.Flags().GetBool("json")
skipExtensionUpgrade, _ := cmd.Flags().GetBool("skip-extension-upgrade")
approveUpgrade, _ := cmd.Flags().GetBool("approve-updates")
approveUpgrade, _ := cmd.Flags().GetBool("approve")

// Handle audit mode
if auditFlag {
Expand Down Expand Up @@ -111,7 +111,7 @@ Examples:
cmd.Flags().Bool("pr", false, "Alias for --create-pull-request")
_ = cmd.Flags().MarkHidden("pr") // Hide the short alias from help output
cmd.Flags().Bool("audit", false, "Check dependency health without performing upgrades")
cmd.Flags().Bool("approve-updates", false, "Approve all safe update changes during compilation (skip safe update enforcement)")
cmd.Flags().Bool("approve", false, "Approve all safe update changes during compilation (skip safe update enforcement)")
cmd.Flags().Bool("skip-extension-upgrade", false, "Skip automatic extension upgrade (used internally to prevent recursion after upgrade)")
_ = cmd.Flags().MarkHidden("skip-extension-upgrade")
addJSONFlag(cmd)
Expand Down Expand Up @@ -143,8 +143,8 @@ func runDependencyAudit(verbose bool, jsonOutput bool) error {

// runUpgradeCommand executes the upgrade process
func runUpgradeCommand(ctx context.Context, verbose bool, workflowDir string, noFix bool, noCompile bool, noActions bool, skipExtensionUpgrade bool, approve bool) error {
upgradeLog.Printf("Running upgrade command: verbose=%v, workflowDir=%s, noFix=%v, noCompile=%v, noActions=%v, skipExtensionUpgrade=%v, approve=%v",
verbose, workflowDir, noFix, noCompile, noActions, skipExtensionUpgrade, approve)
upgradeLog.Printf("Running upgrade command: verbose=%v, workflowDir=%s, noFix=%v, noCompile=%v, noActions=%v, skipExtensionUpgrade=%v",
verbose, workflowDir, noFix, noCompile, noActions, skipExtensionUpgrade)

// Step 0b: Ensure gh-aw extension is on the latest version.
// If the extension was just upgraded, re-launch the freshly-installed binary
Expand Down
6 changes: 5 additions & 1 deletion pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,11 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath
log.Printf("Failed to parse filesystem gh-aw-manifest: %v. Safe update enforcement will treat as empty manifest.", parseErr)
}
} else {
log.Printf("Lock file %s not found on filesystem either (new workflow or not yet written). Safe update enforcement will treat as empty manifest.", lockFile)
// No lock file anywhere — this is a brand-new workflow. Use an empty
// (non-nil) manifest so EnforceSafeUpdate applies enforcement and flags
// any newly introduced secrets or actions for review.
log.Printf("Lock file %s not found (new workflow). Safe update enforcement will use an empty baseline.", lockFile)
oldManifest = &GHAWManifest{Version: currentGHAWManifestVersion}
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func (c *Compiler) SetNoEmit(noEmit bool) {
c.noEmit = noEmit
}

// SetApprove configures whether to skip safe update enforcement via the CLI --approve-updates flag.
// SetApprove configures whether to skip safe update enforcement via the CLI --approve flag.
// When true, safe update enforcement is disabled regardless of strict mode setting,
// approving all changes.
func (c *Compiler) SetApprove(approve bool) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (c *Compiler) effectiveStrictMode(frontmatter map[string]any) bool {
// effectiveSafeUpdate returns true when safe update mode should be enforced for
// the given workflow. Safe update mode is equivalent to strict mode: it is
// enabled whenever strict mode is active (CLI --strict flag, frontmatter
// strict: true, or the default). It can be disabled via the CLI --approve-updates flag
// strict: true, or the default). It can be disabled via the CLI --approve flag
// to approve all changes.
func (c *Compiler) effectiveSafeUpdate(data *WorkflowData) bool {
if c.approve {
Expand Down
24 changes: 13 additions & 11 deletions pkg/workflow/safe_update_enforcement.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ var ghAwInternalSecrets = map[string]bool{
// changes have been introduced compared to those recorded in the existing manifest.
//
// manifest is the gh-aw-manifest extracted from the current lock file before
// recompilation. When nil (no lock file exists yet, or the lock file predates
// the safe-updates feature), it is treated as an empty baseline so that all
// non-GITHUB_TOKEN secrets and all custom actions are flagged on the very first
// compilation. This ensures agents receive a SECURITY REVIEW REQUIRED prompt even
// on the initial code-generation run. The newly generated lock file then embeds
// the manifest as the baseline for future compilations.
// recompilation.
//
// - nil means a lock file was found but it predates the safe-updates feature
// (no gh-aw-manifest section). Enforcement is skipped so legacy lock files
// are not flagged on upgrade.
// - non-nil (including an empty &GHAWManifest{}) means the caller has a
// baseline to compare against. Pass &GHAWManifest{} when no lock file
// exists yet (first compilation); all new secrets/actions will be flagged.
//
// secretNames contains the raw names produced by CollectSecretReferences (i.e.
// they may or may not carry the "secrets." prefix; both forms are normalized
Expand All @@ -48,10 +50,10 @@ var ghAwInternalSecrets = map[string]bool{
// Returns a structured, actionable error when violations are found.
func EnforceSafeUpdate(manifest *GHAWManifest, secretNames []string, actionRefs []string) error {
if manifest == nil {
// Treat no prior manifest as an empty baseline so that newly introduced
// secrets and actions are flagged on first compilation as well.
safeUpdateLog.Print("No existing manifest found; enforcing safe update with empty baseline (new secrets/actions will be flagged)")
manifest = &GHAWManifest{Version: currentGHAWManifestVersion}
// Lock file exists but predates the safe-updates feature (no gh-aw-manifest
// section). Skip enforcement so legacy lock files are not flagged on upgrade.
safeUpdateLog.Print("Lock file has no gh-aw-manifest; skipping safe update enforcement (legacy lock file)")
return nil
}

secretViolations := collectSecretViolations(manifest, secretNames)
Expand Down Expand Up @@ -213,7 +215,7 @@ func buildSafeUpdateError(secretViolations, addedActions, removedActions []strin
sb.WriteString(strings.Join(removedActions, "\n - "))
}

sb.WriteString("\n\nRemediation options:\n 1. Use the --approve-updates flag to allow the changes.\n 2. Revert the unapproved changes.\n 3. Use an interactive coding agent to review and approve the changes.")
sb.WriteString("\n\nRemediation options:\n 1. Use the --approve flag to allow the changes.\n 2. Revert the unapproved changes.\n 3. Use an interactive coding agent to review and approve the changes.")
return fmt.Errorf("%s", sb.String())
}

Expand Down
39 changes: 30 additions & 9 deletions pkg/workflow/safe_update_enforcement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,56 @@ func TestEnforceSafeUpdate(t *testing.T) {
wantErrMsgs []string
}{
{
name: "nil manifest (no lock file) enforces on first compile — new secret flagged",
name: "nil manifest (lock file without manifest section) skips enforcement",
manifest: nil,
secretNames: []string{"MY_SECRET"},
actionRefs: []string{},
wantErr: true,
wantErrMsgs: []string{"MY_SECRET", "safe update mode"},
wantErr: false,
},
{
name: "nil manifest (no lock file) enforces on first compile — custom action flagged",
name: "nil manifest (lock file without manifest section) skips enforcement for actions",
manifest: nil,
secretNames: []string{},
actionRefs: []string{"my-org/my-action@abc1234 # v1"},
wantErr: true,
wantErrMsgs: []string{"my-org/my-action", "safe update mode"},
wantErr: false,
},
{
name: "nil manifest (no lock file) allows GITHUB_TOKEN on first compile",
name: "nil manifest (lock file without manifest section) skips with GITHUB_TOKEN",
manifest: nil,
secretNames: []string{"GITHUB_TOKEN"},
actionRefs: []string{},
wantErr: false,
},
{
name: "nil manifest (no lock file) with no secrets or actions passes",
name: "nil manifest (lock file without manifest section) skips with no secrets",
manifest: nil,
secretNames: []string{},
actionRefs: []string{},
wantErr: false,
},
{
name: "empty non-nil manifest (no lock file) enforces — new secret flagged",
manifest: &GHAWManifest{Version: currentGHAWManifestVersion},
secretNames: []string{"MY_SECRET"},
actionRefs: []string{},
wantErr: true,
wantErrMsgs: []string{"MY_SECRET", "safe update mode"},
},
{
name: "empty non-nil manifest (no lock file) enforces — custom action flagged",
manifest: &GHAWManifest{Version: currentGHAWManifestVersion},
secretNames: []string{},
actionRefs: []string{"my-org/my-action@abc1234 # v1"},
wantErr: true,
wantErrMsgs: []string{"my-org/my-action", "safe update mode"},
},
{
name: "empty non-nil manifest (no lock file) allows GITHUB_TOKEN",
manifest: &GHAWManifest{Version: currentGHAWManifestVersion},
secretNames: []string{"GITHUB_TOKEN"},
actionRefs: []string{},
wantErr: false,
},
{
name: "empty secrets and actions with existing manifest passes",
manifest: &GHAWManifest{Version: 1, Secrets: []string{}, Actions: []GHAWManifestAction{}},
Expand Down Expand Up @@ -291,7 +312,7 @@ func TestBuildSafeUpdateError(t *testing.T) {
assert.Contains(t, msg, "safe update mode", "error message")
assert.Contains(t, msg, "NEW_SECRET", "violation in message")
assert.Contains(t, msg, "ANOTHER_SECRET", "violation in message")
assert.Contains(t, msg, "--approve-updates", "remediation guidance")
assert.Contains(t, msg, "--approve", "remediation guidance")
})

t.Run("added actions only", func(t *testing.T) {
Expand Down
Loading