Summary
gh aw env update --scope org fails for any default_* key whose organization variable does not exist yet; --scope ent has the same defect. Updates and deletes work, so only creation is broken. Bootstrapping an organization therefore requires creating every variable out-of-band with gh variable set --org ... --visibility all first.
Reproduction
Against an organization with no GH_AW_DEFAULT_MAX_TURNS variable:
printf "default_max_turns: '5'\n" > defaults.yml
gh aw env update defaults.yml --scope org --org MY_ORG --yes
Result:
gh api -X POST orgs/MY_ORG/actions/variables -f name=GH_AW_DEFAULT_MAX_TURNS -f value=5 failed (exit 1):
{"message":"Invalid request.\n\nInvalid input: object is missing required key: visibility.",
"documentation_url":"https://docs.github.com/rest/actions/variables#create-an-organization-variable",
"status":"422"}
✗ failed to set GH_AW_DEFAULT_MAX_TURNS
Confirmed deterministic (5/5 attempts). Version: gh aw v0.86.2 (latest stable), gh 2.98.0.
Verified working on the same organization, so the defect is isolated to the create path:
| Operation |
Result |
Update existing variable (7 → 9) |
✅ succeeds via PATCH |
| Delete by omitting the key from the file |
✅ succeeds via DELETE |
| Create a variable that does not exist |
❌ HTTP 422 |
Root Cause
pkg/cli/env_command.go, upsertDefaultsVariable (around line 542):
func upsertDefaultsVariable(target defaultsTarget, name, value string) error {
patchOut, patchErr := runDefaultsGH("api", "-X", "PATCH", target.variableEndpoint(name), "-f", "name="+name, "-f", "value="+value)
if patchErr == nil {
return nil
}
if !isDefaultsNotFoundError(patchErr, patchOut) {
return fmt.Errorf("failed to update %s: %w", name, errWithOutput(patchErr, patchOut))
}
envCmdLog.Printf("Variable %s not found via PATCH, creating via POST", name)
out, err := runDefaultsGH("api", "-X", "POST", target.variablesEndpoint(), "-f", "name="+name, "-f", "value="+value)
if err != nil {
return fmt.Errorf("failed to set %s: %w", name, errWithOutput(err, out))
}
return nil
}
The POST sends only name and value. That is valid for repos/{owner}/{repo}/actions/variables, but POST /orgs/{org}/actions/variables requires visibility (all | private | selected), and the enterprise endpoint has the same requirement. Because repo scope is unaffected, the bug is invisible in repo-scoped testing.
PATCH does not need visibility — GitHub preserves the existing value — which is why only first-time creation breaks.
Proposed Fix
Send visibility on POST for org and enterprise scope, and let the caller choose it.
1. Add visibility to the target and the create call (pkg/cli/env_command.go)
- Add a
visibility string field to defaultsTarget, populated by resolveDefaultsTarget.
- In
upsertDefaultsVariable, build the POST arguments per scope:
args := []string{"api", "-X", "POST", target.variablesEndpoint(), "-f", "name=" + name, "-f", "value=" + value}
if target.scope == defaultsScopeOrg || target.scope == defaultsScopeEnt {
args = append(args, "-f", "visibility="+target.visibility)
}
out, err := runDefaultsGH(args...)
- Leave the PATCH path untouched so an existing variable keeps its current visibility.
2. Expose a --visibility flag (newDefaultsUpdateCommand)
- Add
--visibility {all|private|selected}, defaulting to all. Governance defaults are meant to apply org-wide, and a private variable does not resolve in public repositories.
- Validate the value, and reject the flag under
--scope repo, using the project error style: "--visibility is not valid for repo scope. Expected --scope org or --scope ent. Example: gh aw env update defaults.yml --scope org --org my-org --visibility all".
- With
selected, the caller must attach repositories separately via PUT /orgs/{org}/actions/variables/{name}/repositories; this command does not manage that list.
3. Improve the failure message
Wrap the 422 so the cause is actionable instead of a raw API dump, matching the [what's wrong]. [what's expected]. [example] guideline:
failed to create GH_AW_DEFAULT_MAX_TURNS at org scope: organization variables require a visibility.
Expected one of all, private, selected. Example: gh aw env update defaults.yml --scope org --org my-org --visibility all
4. Tests (pkg/cli/env_command_test.go)
upsertDefaultsVariable, variable absent: POST contains visibility=all at org and ent scope, and omits it at repo scope.
- Variable present at org scope: PATCH path taken, no
visibility sent, existing visibility preserved.
--visibility private|selected propagates to the POST.
--visibility bogus, and --visibility with --scope repo, both fail validation with the documented messages.
5. Documentation and changelog
docs/src/content/docs/reference/governance.md: document --visibility, note it applies only on first creation, and state that existing variables keep their current visibility.
docs/src/content/docs/reference/compiler-enterprise-environment-controls.md: add the flag to the gh aw env update examples.
CHANGELOG.md: record the CLI change. Adding a flag with an all default is non-breaking under Breaking CLI Rules — no command, flag, or output structure is removed or renamed.
6. Quality checks
- Emit the new error through
pkg/console (console.FormatErrorMessage) to match CLI output conventions.
- Run
make agent-finish (build, test, recompile, format, lint) and make lint-errors to validate error-message style.
Acceptance Criteria
gh aw env update defaults.yml --scope org --org MY_ORG --yes creates a previously nonexistent variable and exits 0.
- The POST issued at org and enterprise scope includes
visibility; the repo-scope POST does not.
- Updating an existing org variable still uses PATCH and leaves its visibility unchanged.
--visibility accepts all (default), private, and selected; any other value, or use with --scope repo, fails validation with the documented message.
- New tests in
pkg/cli/env_command_test.go cover each case above and make agent-finish passes.
Workaround
Create each variable once with the CLI, then let gh aw env manage it:
gh variable set GH_AW_DEFAULT_MAX_TURNS --org MY_ORG --visibility all --body '20'
Summary
gh aw env update --scope orgfails for anydefault_*key whose organization variable does not exist yet;--scope enthas the same defect. Updates and deletes work, so only creation is broken. Bootstrapping an organization therefore requires creating every variable out-of-band withgh variable set --org ... --visibility allfirst.Reproduction
Against an organization with no
GH_AW_DEFAULT_MAX_TURNSvariable:Result:
Confirmed deterministic (5/5 attempts). Version:
gh awv0.86.2 (latest stable),gh2.98.0.Verified working on the same organization, so the defect is isolated to the create path:
7→9)Root Cause
pkg/cli/env_command.go,upsertDefaultsVariable(around line 542):The POST sends only
nameandvalue. That is valid forrepos/{owner}/{repo}/actions/variables, butPOST /orgs/{org}/actions/variablesrequiresvisibility(all|private|selected), and the enterprise endpoint has the same requirement. Because repo scope is unaffected, the bug is invisible in repo-scoped testing.PATCH does not need
visibility— GitHub preserves the existing value — which is why only first-time creation breaks.Proposed Fix
Send
visibilityon POST for org and enterprise scope, and let the caller choose it.1. Add visibility to the target and the create call (
pkg/cli/env_command.go)visibility stringfield todefaultsTarget, populated byresolveDefaultsTarget.upsertDefaultsVariable, build the POST arguments per scope:2. Expose a
--visibilityflag (newDefaultsUpdateCommand)--visibility {all|private|selected}, defaulting toall. Governance defaults are meant to apply org-wide, and aprivatevariable does not resolve in public repositories.--scope repo, using the project error style:"--visibility is not valid for repo scope. Expected --scope org or --scope ent. Example: gh aw env update defaults.yml --scope org --org my-org --visibility all".selected, the caller must attach repositories separately viaPUT /orgs/{org}/actions/variables/{name}/repositories; this command does not manage that list.3. Improve the failure message
Wrap the 422 so the cause is actionable instead of a raw API dump, matching the
[what's wrong]. [what's expected]. [example]guideline:4. Tests (
pkg/cli/env_command_test.go)upsertDefaultsVariable, variable absent: POST containsvisibility=allat org and ent scope, and omits it at repo scope.visibilitysent, existing visibility preserved.--visibility private|selectedpropagates to the POST.--visibility bogus, and--visibilitywith--scope repo, both fail validation with the documented messages.5. Documentation and changelog
docs/src/content/docs/reference/governance.md: document--visibility, note it applies only on first creation, and state that existing variables keep their current visibility.docs/src/content/docs/reference/compiler-enterprise-environment-controls.md: add the flag to thegh aw env updateexamples.CHANGELOG.md: record the CLI change. Adding a flag with analldefault is non-breaking under Breaking CLI Rules — no command, flag, or output structure is removed or renamed.6. Quality checks
pkg/console(console.FormatErrorMessage) to match CLI output conventions.make agent-finish(build, test, recompile, format, lint) andmake lint-errorsto validate error-message style.Acceptance Criteria
gh aw env update defaults.yml --scope org --org MY_ORG --yescreates a previously nonexistent variable and exits 0.visibility; the repo-scope POST does not.--visibilityacceptsall(default),private, andselected; any other value, or use with--scope repo, fails validation with the documented message.pkg/cli/env_command_test.gocover each case above andmake agent-finishpasses.Workaround
Create each variable once with the CLI, then let
gh aw envmanage it: