Skip to content

Commit f07c59a

Browse files
authored
fix: rename --approve-updates to --approve and skip safe update enforcement on first compile (#26350)
* fix: rename --approve-updates to --approve and skip safe update enforcement on first compile - Rename --approve-updates flag to --approve in compile, run, and upgrade commands - Change first-compile behavior: skip enforcement when no prior manifest exists instead of flagging all new secrets/actions (baseline is created silently) - Update remediation message to reference --approve instead of --approve-updates - Update all tests to reflect new first-compile behavior * fix: enforce safe update on first compile (no lock file), skip only for legacy lock files without manifest Distinguish between two nil-manifest cases: - No lock file exists (new workflow): pass &GHAWManifest{} (empty non-nil) to EnforceSafeUpdate so all new secrets/actions are flagged for review - Lock file exists without a gh-aw-manifest section (legacy): pass nil to EnforceSafeUpdate which skips enforcement This restores SECURITY REVIEW REQUIRED warnings on first compilation while still allowing legacy lock files to upgrade without false positives.
1 parent fde68bb commit f07c59a

9 files changed

Lines changed: 65 additions & 38 deletions

cmd/gh-aw/main.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ Examples:
233233
var compileCmd = &cobra.Command{
234234
Use: "compile [workflow]...",
235235
Short: "Compile agentic workflow Markdown files into GitHub Actions YAML",
236-
Long: `Compile one or more agentic workflow Markdown files into GitHub Actions YAML.
236+
Long: `Compile one or more agentic workflows to YAML workflows.
237237
238238
If no workflows are specified, all Markdown files in .github/workflows will be compiled.
239239
@@ -286,7 +286,7 @@ Examples:
286286
failFast, _ := cmd.Flags().GetBool("fail-fast")
287287
noCheckUpdate, _ := cmd.Flags().GetBool("no-check-update")
288288
scheduleSeed, _ := cmd.Flags().GetString("schedule-seed")
289-
approve, _ := cmd.Flags().GetBool("approve-updates")
289+
approve, _ := cmd.Flags().GetBool("approve")
290290
validateImages, _ := cmd.Flags().GetBool("validate-images")
291291
priorManifestFile, _ := cmd.Flags().GetString("prior-manifest-file")
292292
verbose, _ := cmd.Flags().GetBool("verbose")
@@ -399,7 +399,7 @@ Examples:
399399
push, _ := cmd.Flags().GetBool("push")
400400
dryRun, _ := cmd.Flags().GetBool("dry-run")
401401
jsonOutput, _ := cmd.Flags().GetBool("json")
402-
approveRun, _ := cmd.Flags().GetBool("approve-updates")
402+
approveRun, _ := cmd.Flags().GetBool("approve")
403403

404404
if err := validateEngine(engineOverride); err != nil {
405405
return err
@@ -694,7 +694,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
694694
compileCmd.Flags().Bool("fail-fast", false, "Stop at the first validation error instead of collecting all errors")
695695
compileCmd.Flags().Bool("no-check-update", false, "Skip checking for gh-aw updates")
696696
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")
697-
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")
697+
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")
698698
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")
699699
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")
700700
if err := compileCmd.Flags().MarkHidden("prior-manifest-file"); err != nil {
@@ -729,13 +729,13 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
729729
runCmd.Flags().Bool("enable-if-needed", false, "Enable the workflow before running if needed, and restore state afterward")
730730
runCmd.Flags().StringP("engine", "e", "", "Override AI engine (claude, codex, copilot, custom)")
731731
runCmd.Flags().StringP("repo", "r", "", "Target repository ([HOST/]owner/repo format). Defaults to current repository")
732-
runCmd.Flags().String("ref", "", "Branch or tag name to run the workflow on (e.g., main, v1.0.0)")
732+
runCmd.Flags().String("ref", "", "Branch or tag name to run the workflow on (default: current branch)")
733733
runCmd.Flags().Bool("auto-merge-prs", false, "Auto-merge any pull requests created during the workflow execution")
734734
runCmd.Flags().StringArrayP("raw-field", "F", []string{}, "Add a string parameter in key=value format (can be used multiple times)")
735735
runCmd.Flags().Bool("push", false, "Commit and push workflow files (including transitive imports) before running")
736736
runCmd.Flags().Bool("dry-run", false, "Validate workflow without actually triggering execution on GitHub Actions")
737737
runCmd.Flags().BoolP("json", "j", false, "Output results in JSON format")
738-
runCmd.Flags().Bool("approve-updates", false, "Approve all safe update changes during compilation (skip safe update enforcement)")
738+
runCmd.Flags().Bool("approve", false, "Approve all safe update changes during compilation (skip safe update enforcement)")
739739
// Register completions for run command
740740
runCmd.ValidArgsFunction = cli.CompleteWorkflowNames
741741
cli.RegisterEngineFlagCompletion(runCmd)

pkg/cli/compile_compiler_setup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func configureCompilerFlags(compiler *workflow.Compiler, config CompileConfig) {
160160
// regardless of the workflow's strict mode setting.
161161
compiler.SetApprove(config.Approve)
162162
if config.Approve {
163-
compileCompilerSetupLog.Print("Safe update changes approved via --approve-updates flag: skipping safe update enforcement for new restricted secrets or unapproved action additions/removals")
163+
compileCompilerSetupLog.Print("Safe update changes approved via --approve flag: skipping safe update enforcement for new restricted secrets or unapproved action additions/removals")
164164
}
165165

166166
// Set require docker flag: when set, container image validation fails instead of

pkg/cli/compile_safe_update_integration_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func manifestLockFileWithSecret(secretName string) string {
9191
}
9292

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

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

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

511511
// Compile with --approve so we can freely inspect the manifest without safe update warnings.
512-
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve-updates")
512+
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve")
513513
cmd.Env = append(os.Environ(), "GH_AW_ACTION_MODE=release")
514514
output, err := cmd.CombinedOutput()
515515
outputStr := string(output)

pkg/cli/upgrade_command.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Examples:
7575
auditFlag, _ := cmd.Flags().GetBool("audit")
7676
jsonOutput, _ := cmd.Flags().GetBool("json")
7777
skipExtensionUpgrade, _ := cmd.Flags().GetBool("skip-extension-upgrade")
78-
approveUpgrade, _ := cmd.Flags().GetBool("approve-updates")
78+
approveUpgrade, _ := cmd.Flags().GetBool("approve")
7979

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

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

149149
// Step 0b: Ensure gh-aw extension is on the latest version.
150150
// If the extension was just upgraded, re-launch the freshly-installed binary

pkg/workflow/compiler.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,11 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath
718718
log.Printf("Failed to parse filesystem gh-aw-manifest: %v. Safe update enforcement will treat as empty manifest.", parseErr)
719719
}
720720
} else {
721-
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)
721+
// No lock file anywhere — this is a brand-new workflow. Use an empty
722+
// (non-nil) manifest so EnforceSafeUpdate applies enforcement and flags
723+
// any newly introduced secrets or actions for review.
724+
log.Printf("Lock file %s not found (new workflow). Safe update enforcement will use an empty baseline.", lockFile)
725+
oldManifest = &GHAWManifest{Version: currentGHAWManifestVersion}
722726
}
723727
}
724728

pkg/workflow/compiler_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func (c *Compiler) SetNoEmit(noEmit bool) {
163163
c.noEmit = noEmit
164164
}
165165

166-
// SetApprove configures whether to skip safe update enforcement via the CLI --approve-updates flag.
166+
// SetApprove configures whether to skip safe update enforcement via the CLI --approve flag.
167167
// When true, safe update enforcement is disabled regardless of strict mode setting,
168168
// approving all changes.
169169
func (c *Compiler) SetApprove(approve bool) {

pkg/workflow/compiler_yaml.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func (c *Compiler) effectiveStrictMode(frontmatter map[string]any) bool {
3737
// effectiveSafeUpdate returns true when safe update mode should be enforced for
3838
// the given workflow. Safe update mode is equivalent to strict mode: it is
3939
// enabled whenever strict mode is active (CLI --strict flag, frontmatter
40-
// strict: true, or the default). It can be disabled via the CLI --approve-updates flag
40+
// strict: true, or the default). It can be disabled via the CLI --approve flag
4141
// to approve all changes.
4242
func (c *Compiler) effectiveSafeUpdate(data *WorkflowData) bool {
4343
if c.approve {

pkg/workflow/safe_update_enforcement.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ var ghAwInternalSecrets = map[string]bool{
3131
// changes have been introduced compared to those recorded in the existing manifest.
3232
//
3333
// manifest is the gh-aw-manifest extracted from the current lock file before
34-
// recompilation. When nil (no lock file exists yet, or the lock file predates
35-
// the safe-updates feature), it is treated as an empty baseline so that all
36-
// non-GITHUB_TOKEN secrets and all custom actions are flagged on the very first
37-
// compilation. This ensures agents receive a SECURITY REVIEW REQUIRED prompt even
38-
// on the initial code-generation run. The newly generated lock file then embeds
39-
// the manifest as the baseline for future compilations.
34+
// recompilation.
35+
//
36+
// - nil means a lock file was found but it predates the safe-updates feature
37+
// (no gh-aw-manifest section). Enforcement is skipped so legacy lock files
38+
// are not flagged on upgrade.
39+
// - non-nil (including an empty &GHAWManifest{}) means the caller has a
40+
// baseline to compare against. Pass &GHAWManifest{} when no lock file
41+
// exists yet (first compilation); all new secrets/actions will be flagged.
4042
//
4143
// secretNames contains the raw names produced by CollectSecretReferences (i.e.
4244
// they may or may not carry the "secrets." prefix; both forms are normalized
@@ -48,10 +50,10 @@ var ghAwInternalSecrets = map[string]bool{
4850
// Returns a structured, actionable error when violations are found.
4951
func EnforceSafeUpdate(manifest *GHAWManifest, secretNames []string, actionRefs []string) error {
5052
if manifest == nil {
51-
// Treat no prior manifest as an empty baseline so that newly introduced
52-
// secrets and actions are flagged on first compilation as well.
53-
safeUpdateLog.Print("No existing manifest found; enforcing safe update with empty baseline (new secrets/actions will be flagged)")
54-
manifest = &GHAWManifest{Version: currentGHAWManifestVersion}
53+
// Lock file exists but predates the safe-updates feature (no gh-aw-manifest
54+
// section). Skip enforcement so legacy lock files are not flagged on upgrade.
55+
safeUpdateLog.Print("Lock file has no gh-aw-manifest; skipping safe update enforcement (legacy lock file)")
56+
return nil
5557
}
5658

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

216-
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.")
218+
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.")
217219
return fmt.Errorf("%s", sb.String())
218220
}
219221

pkg/workflow/safe_update_enforcement_test.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,35 +19,56 @@ func TestEnforceSafeUpdate(t *testing.T) {
1919
wantErrMsgs []string
2020
}{
2121
{
22-
name: "nil manifest (no lock file) enforces on first compile — new secret flagged",
22+
name: "nil manifest (lock file without manifest section) skips enforcement",
2323
manifest: nil,
2424
secretNames: []string{"MY_SECRET"},
2525
actionRefs: []string{},
26-
wantErr: true,
27-
wantErrMsgs: []string{"MY_SECRET", "safe update mode"},
26+
wantErr: false,
2827
},
2928
{
30-
name: "nil manifest (no lock file) enforces on first compile — custom action flagged",
29+
name: "nil manifest (lock file without manifest section) skips enforcement for actions",
3130
manifest: nil,
3231
secretNames: []string{},
3332
actionRefs: []string{"my-org/my-action@abc1234 # v1"},
34-
wantErr: true,
35-
wantErrMsgs: []string{"my-org/my-action", "safe update mode"},
33+
wantErr: false,
3634
},
3735
{
38-
name: "nil manifest (no lock file) allows GITHUB_TOKEN on first compile",
36+
name: "nil manifest (lock file without manifest section) skips with GITHUB_TOKEN",
3937
manifest: nil,
4038
secretNames: []string{"GITHUB_TOKEN"},
4139
actionRefs: []string{},
4240
wantErr: false,
4341
},
4442
{
45-
name: "nil manifest (no lock file) with no secrets or actions passes",
43+
name: "nil manifest (lock file without manifest section) skips with no secrets",
4644
manifest: nil,
4745
secretNames: []string{},
4846
actionRefs: []string{},
4947
wantErr: false,
5048
},
49+
{
50+
name: "empty non-nil manifest (no lock file) enforces — new secret flagged",
51+
manifest: &GHAWManifest{Version: currentGHAWManifestVersion},
52+
secretNames: []string{"MY_SECRET"},
53+
actionRefs: []string{},
54+
wantErr: true,
55+
wantErrMsgs: []string{"MY_SECRET", "safe update mode"},
56+
},
57+
{
58+
name: "empty non-nil manifest (no lock file) enforces — custom action flagged",
59+
manifest: &GHAWManifest{Version: currentGHAWManifestVersion},
60+
secretNames: []string{},
61+
actionRefs: []string{"my-org/my-action@abc1234 # v1"},
62+
wantErr: true,
63+
wantErrMsgs: []string{"my-org/my-action", "safe update mode"},
64+
},
65+
{
66+
name: "empty non-nil manifest (no lock file) allows GITHUB_TOKEN",
67+
manifest: &GHAWManifest{Version: currentGHAWManifestVersion},
68+
secretNames: []string{"GITHUB_TOKEN"},
69+
actionRefs: []string{},
70+
wantErr: false,
71+
},
5172
{
5273
name: "empty secrets and actions with existing manifest passes",
5374
manifest: &GHAWManifest{Version: 1, Secrets: []string{}, Actions: []GHAWManifestAction{}},
@@ -291,7 +312,7 @@ func TestBuildSafeUpdateError(t *testing.T) {
291312
assert.Contains(t, msg, "safe update mode", "error message")
292313
assert.Contains(t, msg, "NEW_SECRET", "violation in message")
293314
assert.Contains(t, msg, "ANOTHER_SECRET", "violation in message")
294-
assert.Contains(t, msg, "--approve-updates", "remediation guidance")
315+
assert.Contains(t, msg, "--approve", "remediation guidance")
295316
})
296317

297318
t.Run("added actions only", func(t *testing.T) {

0 commit comments

Comments
 (0)