Skip to content

Commit 3ddfac2

Browse files
committed
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
1 parent 4907d31 commit 3ddfac2

File tree

5 files changed

+55
-68
lines changed

5 files changed

+55
-68
lines changed

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_safe_update_integration_test.go

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,8 @@ 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
95-
// SECURITY REVIEW REQUIRED warning so agents review newly introduced secrets.
96-
// The compile itself succeeds (warnings do not fail the build) and the lock file
97-
// written with the manifest serves as the baseline for future compilations.
94+
// (with no prior manifest) creates the manifest baseline silently without any
95+
// safe update warnings. Enforcement only kicks in on subsequent compilations.
9896
func TestSafeUpdateFirstCompileCreatesBaseline(t *testing.T) {
9997
setup := setupIntegrationTest(t)
10098
defer setup.cleanup()
@@ -103,17 +101,17 @@ func TestSafeUpdateFirstCompileCreatesBaseline(t *testing.T) {
103101
require.NoError(t, os.WriteFile(workflowPath, []byte(safeUpdateWorkflowWithSecret), 0o644),
104102
"should write workflow file")
105103

106-
// First compile with no prior lock file: should succeed but emit safe update
107-
// warnings because the agent must review newly introduced secrets.
104+
// First compile with no prior lock file: should succeed without safe update warnings
105+
// because there is no prior manifest to compare against.
108106
cmd := exec.Command(setup.binaryPath, "compile", workflowPath)
109107
cmd.Env = append(os.Environ(), "GH_AW_ACTION_MODE=release")
110108
output, err := cmd.CombinedOutput()
111109
outputStr := string(output)
112110

113-
assert.NoError(t, err, "first compile should succeed (warnings don't fail the build)\nOutput:\n%s", outputStr)
114-
// Safe update warning must be emitted even on first compile so the agent reviews secrets.
115-
assert.Contains(t, outputStr, "SECURITY REVIEW REQUIRED",
116-
"first compile should emit safe update warnings so the agent reviews newly introduced secrets")
111+
assert.NoError(t, err, "first compile should succeed\nOutput:\n%s", outputStr)
112+
// No safe update warnings on first compile (no prior manifest to compare against)
113+
assert.NotContains(t, outputStr, "SECURITY REVIEW REQUIRED",
114+
"first compile should not emit safe update warnings when no prior manifest exists")
117115
// Lock file must be written with the manifest baseline
118116
lockFilePath := filepath.Join(setup.workflowsDir, "safe-update-secret.lock.yml")
119117
lockContent, readErr := os.ReadFile(lockFilePath)
@@ -126,9 +124,8 @@ func TestSafeUpdateFirstCompileCreatesBaseline(t *testing.T) {
126124
}
127125

128126
// TestSafeUpdateFirstCompileCreatesBaselineForActions verifies that the first
129-
// compilation with a custom action and no prior manifest still enforces safe
130-
// update mode, emitting a SECURITY REVIEW REQUIRED warning. The compile succeeds
131-
// (warnings do not fail the build) and the new lock file serves as the baseline.
127+
// compilation with a custom action and no prior manifest creates the baseline
128+
// silently without safe update warnings.
132129
func TestSafeUpdateFirstCompileCreatesBaselineForActions(t *testing.T) {
133130
setup := setupIntegrationTest(t)
134131
defer setup.cleanup()
@@ -137,21 +134,20 @@ func TestSafeUpdateFirstCompileCreatesBaselineForActions(t *testing.T) {
137134
require.NoError(t, os.WriteFile(workflowPath, []byte(safeUpdateWorkflowWithCustomAction), 0o644),
138135
"should write workflow file")
139136

140-
// First compile with no prior lock file: should succeed but emit safe update
141-
// warning so the agent reviews the newly introduced custom action.
137+
// First compile with no prior lock file: should succeed without safe update warnings.
142138
cmd := exec.Command(setup.binaryPath, "compile", workflowPath)
143139
cmd.Env = append(os.Environ(), "GH_AW_ACTION_MODE=release")
144140
output, err := cmd.CombinedOutput()
145141
outputStr := string(output)
146142

147-
assert.NoError(t, err, "first compile should succeed (warnings don't fail the build)\nOutput:\n%s", outputStr)
148-
assert.Contains(t, outputStr, "SECURITY REVIEW REQUIRED",
149-
"first compile should emit safe update warnings so the agent reviews newly introduced actions")
143+
assert.NoError(t, err, "first compile should succeed\nOutput:\n%s", outputStr)
144+
assert.NotContains(t, outputStr, "SECURITY REVIEW REQUIRED",
145+
"first compile should not emit safe update warnings when no prior manifest exists")
150146
// Lock file must be written
151147
lockFilePath := filepath.Join(setup.workflowsDir, "safe-update-action.lock.yml")
152148
_, statErr := os.Stat(lockFilePath)
153149
assert.NoError(t, statErr, "lock file should be written after first compile")
154-
t.Logf("First compile correctly emitted warnings for new action.\nOutput:\n%s", outputStr)
150+
t.Logf("First compile correctly created baseline without warnings.\nOutput:\n%s", outputStr)
155151
}
156152

157153
// TestSafeUpdateAllowsKnownSecretWithPriorManifest verifies that safe update
@@ -400,7 +396,7 @@ func TestSafeUpdateManifestIncludesImportedSecret(t *testing.T) {
400396
"should write workflow file")
401397

402398
// Compile with --approve so we can inspect the manifest freely without safe update warnings.
403-
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve-updates")
399+
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve")
404400
cmd.Env = append(os.Environ(), "GH_AW_ACTION_MODE=release")
405401
output, err := cmd.CombinedOutput()
406402
outputStr := string(output)
@@ -420,10 +416,8 @@ func TestSafeUpdateManifestIncludesImportedSecret(t *testing.T) {
420416
}
421417

422418
// TestSafeUpdateFirstCompileCreatesBaselineForImport verifies that the first compilation
423-
// of a workflow that imports a shared config containing a secret emits a
424-
// SECURITY REVIEW REQUIRED warning so the agent reviews newly introduced secrets.
425-
// The compile succeeds (warnings don't fail the build) and the lock file written
426-
// serves as the baseline for future compilations.
419+
// of a workflow that imports a shared config containing a secret creates the baseline
420+
// manifest silently without safe update warnings.
427421
func TestSafeUpdateFirstCompileCreatesBaselineForImport(t *testing.T) {
428422
setup := setupIntegrationTest(t)
429423
defer setup.cleanup()
@@ -434,17 +428,17 @@ func TestSafeUpdateFirstCompileCreatesBaselineForImport(t *testing.T) {
434428
require.NoError(t, os.WriteFile(workflowPath, []byte(safeUpdateWorkflowWithImport), 0o644),
435429
"should write workflow file")
436430

437-
// No prior lock file — first compile enforces safe update and emits a warning.
431+
// No prior lock file — first compile creates baseline silently.
438432
cmd := exec.Command(setup.binaryPath, "compile", workflowPath)
439433
cmd.Env = append(os.Environ(), "GH_AW_ACTION_MODE=release")
440434
output, err := cmd.CombinedOutput()
441435
outputStr := string(output)
442436

443437
assert.NoError(t, err,
444-
"first compile should succeed (warnings don't fail the build)\nOutput:\n%s", outputStr)
445-
assert.Contains(t, outputStr, "SECURITY REVIEW REQUIRED",
446-
"first compile should emit safe update warnings so the agent reviews newly introduced secrets")
447-
t.Logf("First compile correctly emitted warnings for imported secret.\nOutput:\n%s", outputStr)
438+
"first compile should succeed\nOutput:\n%s", outputStr)
439+
assert.NotContains(t, outputStr, "SECURITY REVIEW REQUIRED",
440+
"first compile should not emit safe update warnings when no prior manifest exists")
441+
t.Logf("First compile correctly created baseline without warnings.\nOutput:\n%s", outputStr)
448442
}
449443

450444
// TestSafeUpdateAllowsImportedSecretWithPriorManifest verifies that safe update
@@ -509,7 +503,7 @@ func TestSafeUpdateManifestIncludesTransitivelyImportedSecret(t *testing.T) {
509503
"should write workflow file")
510504

511505
// Compile with --approve so we can freely inspect the manifest without safe update warnings.
512-
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve-updates")
506+
cmd := exec.Command(setup.binaryPath, "compile", workflowPath, "--approve")
513507
cmd.Env = append(os.Environ(), "GH_AW_ACTION_MODE=release")
514508
output, err := cmd.CombinedOutput()
515509
outputStr := string(output)
@@ -528,11 +522,7 @@ func TestSafeUpdateManifestIncludesTransitivelyImportedSecret(t *testing.T) {
528522
}
529523
}
530524

531-
// TestSafeUpdateFirstCompileCreatesBaselineForTransitiveImport verifies that
532-
// the first compilation of a workflow with a transitive import chain enforces
533-
// safe update mode and emits a SECURITY REVIEW REQUIRED warning. The compile
534-
// succeeds (warnings don't fail the build) and the new lock file serves as
535-
// the baseline.
525+
// TestSafeUpdateFirstCompileCreatesBaselineForTransitiveImport verifies that\n// the first compilation of a workflow with a transitive import chain creates the\n// baseline manifest silently without safe update warnings.
536526
func TestSafeUpdateFirstCompileCreatesBaselineForTransitiveImport(t *testing.T) {
537527
setup := setupIntegrationTest(t)
538528
defer setup.cleanup()
@@ -544,15 +534,15 @@ func TestSafeUpdateFirstCompileCreatesBaselineForTransitiveImport(t *testing.T)
544534
os.WriteFile(workflowPath, []byte(safeUpdateWorkflowWithTransitiveImport), 0o644),
545535
"should write workflow file")
546536

547-
// No prior lock file — first compile enforces safe update and emits a warning.
537+
// No prior lock file — first compile creates baseline silently.
548538
cmd := exec.Command(setup.binaryPath, "compile", workflowPath)
549539
cmd.Env = append(os.Environ(), "GH_AW_ACTION_MODE=release")
550540
output, err := cmd.CombinedOutput()
551541
outputStr := string(output)
552542

553543
assert.NoError(t, err,
554-
"first compile should succeed (warnings don't fail the build)\nOutput:\n%s", outputStr)
555-
assert.Contains(t, outputStr, "SECURITY REVIEW REQUIRED",
556-
"first compile should emit safe update warnings so the agent reviews newly introduced secrets")
557-
t.Logf("First compile correctly emitted warnings for transitively imported secret.\nOutput:\n%s", outputStr)
544+
"first compile should succeed\nOutput:\n%s", outputStr)
545+
assert.NotContains(t, outputStr, "SECURITY REVIEW REQUIRED",
546+
"first compile should not emit safe update warnings when no prior manifest exists")
547+
t.Logf("First compile correctly created baseline without warnings.\nOutput:\n%s", outputStr)
558548
}

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/safe_update_enforcement.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,9 @@ 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. When nil (no lock file exists yet), it is treated as an empty
35+
// manifest so that all non-GITHUB_TOKEN secrets and all custom actions are rejected
36+
// on a first-time safe-update compilation.
4037
//
4138
// secretNames contains the raw names produced by CollectSecretReferences (i.e.
4239
// they may or may not carry the "secrets." prefix; both forms are normalized
@@ -48,10 +45,12 @@ var ghAwInternalSecrets = map[string]bool{
4845
// Returns a structured, actionable error when violations are found.
4946
func EnforceSafeUpdate(manifest *GHAWManifest, secretNames []string, actionRefs []string) error {
5047
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}
48+
// No prior manifest found — either the lock file was compiled before the safe
49+
// updates feature existed, or this is the very first compilation. In both cases
50+
// skip enforcement: the newly generated lock file will embed a manifest that
51+
// serves as the baseline for future compilations.
52+
safeUpdateLog.Print("No existing manifest found; skipping safe update enforcement (baseline will be created)")
53+
return nil
5554
}
5655

5756
secretViolations := collectSecretViolations(manifest, secretNames)
@@ -213,7 +212,7 @@ func buildSafeUpdateError(secretViolations, addedActions, removedActions []strin
213212
sb.WriteString(strings.Join(removedActions, "\n - "))
214213
}
215214

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.")
215+
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.")
217216
return fmt.Errorf("%s", sb.String())
218217
}
219218

0 commit comments

Comments
 (0)