Skip to content

gh aw env update --scope org cannot create variables that do not already exist (HTTP 422: missing visibility) #56088

Description

@brendanlapuma

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 (79) ✅ 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'

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions