Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,12 @@ MCP Gateway v0.1.5 introduces stricter MCP server validation:

### Bug Fixes

#### Create organization and enterprise defaults with explicit variable visibility

`gh aw env update` now sends the required visibility when creating organization
or enterprise variables. Use `--visibility all|private|selected`; existing
variables keep their current visibility.

#### Bump the default gh-aw-firewall version to v0.27.7 and sync the embedded AWF config schema.

This updates `DefaultFirewallVersion`, refreshes the embedded AWF schema for the new terminal-cap HTTP 403 behavior and `maxCacheMisses` support, and regenerates pinned workflow artifacts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ variables in batch at repo, org, or enterprise scope. The defaults file uses
`default_`-prefixed keys such as `default_max_ai_credits`, `default_max_turn_cache_misses`, `default_detection_max_ai_credits`, `default_max_daily_ai_credits`, `default_timeout_minutes`, `default_agent_job_timeout_minutes`, `default_detection_job_timeout_minutes`,
`default_model_copilot`, and `default_utc`.

```bash
gh aw env update defaults.yml --scope org --org MY_ORG --visibility all
gh aw env update defaults.yml --scope ent --enterprise MY_ENT --visibility all
```

## Project Timezone

By default, the CLI renders timestamps (table output, expiration footers, and the closing messages on expired issues, pull requests, and discussions) using the runner's local clock. Set a project home UTC offset so these times render consistently regardless of where the CLI runs.
Expand Down
9 changes: 7 additions & 2 deletions docs/src/content/docs/reference/governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ gh aw env get repo-defaults.yml --scope repo --repo OWNER/REPO
After editing the YAML file, preview and apply the change.

```bash
gh aw env update org-defaults.yml --scope org --org MY_ORG --dry-run
gh aw env update org-defaults.yml --scope org --org MY_ORG
gh aw env update org-defaults.yml --scope org --org MY_ORG --visibility all --dry-run
gh aw env update org-defaults.yml --scope org --org MY_ORG --visibility all
```

For organization and enterprise scopes, `--visibility` accepts
`all` (the default), `private`, or `selected`. It applies only when
creating a variable; existing variables keep their current visibility.
When using `selected`, attach repositories separately with the GitHub API.

Use `--yes` in automation to skip the interactive
confirmation prompt.

Expand Down
70 changes: 58 additions & 12 deletions pkg/cli/env_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ import (
var envCmdLog = logger.New("cli:env_command")

const (
defaultsScopeRepo = "repo"
defaultsScopeOrg = "org"
defaultsScopeEnt = "ent"
defaultsScopeRepo = "repo"
defaultsScopeOrg = "org"
defaultsScopeEnt = "ent"
defaultsVisibilityAll = "all"
defaultsVisibilityPriv = "private"
defaultsVisibilitySel = "selected"
)

type defaultsFile struct {
Expand Down Expand Up @@ -55,6 +58,7 @@ type defaultsTarget struct {
repoName string
org string
enterprise string
visibility string
}

type defaultsUpdateChange struct {
Expand Down Expand Up @@ -155,7 +159,7 @@ Scope resolution:
if len(args) == 1 {
outputFile = args[0]
}
target, err := resolveDefaultsTarget(scope, repo, org, enterprise, false)
target, err := resolveDefaultsTarget(scope, repo, org, enterprise, "", false)
if err != nil {
return err
}
Expand All @@ -171,7 +175,7 @@ Scope resolution:
}

func newDefaultsUpdateCommand() *cobra.Command {
var scope, repo, org, enterprise string
var scope, repo, org, enterprise, visibility string
var yes, dryRun bool

cmd := &cobra.Command{
Expand All @@ -186,6 +190,7 @@ Scope and flag behavior:
- repo scope uses --repo owner/repo, or the current repository when --repo is omitted.
- org scope uses --org when provided; otherwise it infers the organization from --repo (or the current repository).
- ent scope requires --enterprise <slug>.
- --visibility controls access when creating org or ent variables (all|private|selected). Existing variables keep their visibility.
- --dry-run previews planned changes and exits without applying updates.
- --yes skips the confirmation prompt for real updates; it has no effect with --dry-run.`,
Args: cobra.MaximumNArgs(1),
Expand All @@ -194,7 +199,10 @@ Scope and flag behavior:
if len(args) == 1 {
inputFile = args[0]
}
target, err := resolveDefaultsTarget(scope, repo, org, enterprise, true)
if err := validateDefaultsVisibility(scope, visibility, cmd.Flags().Changed("visibility")); err != nil {
return errors.New(console.FormatErrorMessage(err.Error()))
}
target, err := resolveDefaultsTarget(scope, repo, org, enterprise, visibility, true)
if err != nil {
return err
}
Expand All @@ -206,6 +214,7 @@ Scope and flag behavior:
cmd.Flags().StringVarP(&repo, "repo", "r", "", "Target repository (owner/repo format only; GHES host prefixes are not supported). Defaults to current repository")
cmd.Flags().StringVar(&org, "org", "", "Target organization (required for --scope org unless inferable from --repo/current repo)")
cmd.Flags().StringVar(&enterprise, "enterprise", "", "Target enterprise slug (required for --scope ent)")
cmd.Flags().StringVar(&visibility, "visibility", defaultsVisibilityAll, "Visibility for newly created org or ent variables (all|private|selected)")
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Skip confirmation prompt")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview updates without applying any changes")
_ = cmd.MarkFlagRequired("scope")
Expand Down Expand Up @@ -429,7 +438,19 @@ func defaultsUpdateRows(changes []defaultsUpdateChange) []defaultsUpdateRow {
return rows
}

func resolveDefaultsTarget(scope, repo, org, enterprise string, scopeRequired bool) (defaultsTarget, error) {
func validateDefaultsVisibility(scope, visibility string, visibilitySet bool) error {
switch visibility {
case defaultsVisibilityAll, defaultsVisibilityPriv, defaultsVisibilitySel:
default:
return fmt.Errorf("invalid --visibility value %q. Expected one of all, private, selected. Example: gh aw env update defaults.yml --scope org --org my-org --visibility all", visibility)
}
if strings.TrimSpace(scope) == defaultsScopeRepo && visibilitySet {
return errors.New("--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")
}
return nil
}

func resolveDefaultsTarget(scope, repo, org, enterprise, visibility string, scopeRequired bool) (defaultsTarget, error) {
normalizedScope := strings.TrimSpace(scope)
if normalizedScope == "" {
if scopeRequired {
Expand Down Expand Up @@ -470,13 +491,13 @@ func resolveDefaultsTarget(scope, repo, org, enterprise string, scopeRequired bo
}
targetOrg = owner
}
return defaultsTarget{scope: defaultsScopeOrg, org: targetOrg}, nil
return defaultsTarget{scope: defaultsScopeOrg, org: targetOrg, visibility: visibility}, nil
case defaultsScopeEnt:
targetEnt := strings.TrimSpace(enterprise)
if targetEnt == "" {
return defaultsTarget{}, errors.New("enterprise scope requires --enterprise <slug>")
}
return defaultsTarget{scope: defaultsScopeEnt, enterprise: targetEnt}, nil
return defaultsTarget{scope: defaultsScopeEnt, enterprise: targetEnt, visibility: visibility}, nil
default:
return defaultsTarget{}, fmt.Errorf("invalid scope %q; expected repo, org, or ent", scope)
}
Expand All @@ -500,7 +521,7 @@ func (t defaultsTarget) variableEndpoint(name string) string {
func (t defaultsTarget) displayName() string {
switch t.scope {
case defaultsScopeRepo:
return t.repoOwner + "/" + t.repoName
return fmt.Sprintf("%s/%s", t.repoOwner, t.repoName)
case defaultsScopeOrg:
return t.org
default:
Expand Down Expand Up @@ -549,9 +570,23 @@ func upsertDefaultsVariable(target defaultsTarget, name, value string) error {
}

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)
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...)
if err != nil {
return fmt.Errorf("failed to set %s: %w", name, errWithOutput(err, out))
if (target.scope == defaultsScopeOrg || target.scope == defaultsScopeEnt) && isDefaultsMissingVisibilityError(err, out) {
scopeName := "organization"
example := "gh aw env update defaults.yml --scope org --org my-org --visibility all"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visibility-specific error branch relies on a string search for "visibility" in the raw API response body:

strings.Contains(strings.ToLower(string(out)), "visibility")

This is a brittle heuristic — any API error that incidentally mentions "visibility" (e.g. an unrelated 422 about a different field, or a future error message that changes wording) would trigger the misleading "variables require a visibility" hint even when the real failure is a permissions error, bad slug, or quota issue.

Consider always including the visibility hint for all create failures at org/ent scope, since it is always valid context:

if target.scope == defaultsScopeOrg || target.scope == defaultsScopeEnt {
    // always surface the visibility hint for create failures
}

This makes the message both more reliable and more helpful without risking false negatives if the API response wording changes. @copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the guidance limited to the actual HTTP 422 missing-visibility response so policy and permission failures retain their original error context. The classifier and tests now cover this distinction in pkg/cli/env_command.go.

if target.scope == defaultsScopeEnt {
scopeName = "enterprise"
example = "gh aw env update defaults.yml --scope ent --enterprise my-ent --visibility all"
}
message := fmt.Sprintf("failed to create %s at %s scope: %s variables require a visibility. Expected one of all, private, selected. Example: %s", name, target.scope, scopeName, example)
return fmt.Errorf("%s: %w", message, errWithOutput(err, out))
}
return fmt.Errorf("failed to create %s at %s scope: %w", name, target.scope, errWithOutput(err, out))
}
return nil
}
Expand All @@ -575,6 +610,17 @@ func isDefaultsNotFoundError(err error, out []byte) bool {
return strings.Contains(strings.ToLower(string(out)), "http 404")
}

func isDefaultsMissingVisibilityError(err error, out []byte) bool {
if err == nil {
return false
}
response := strings.ToLower(string(out))
return strings.Contains(response, "http 422") &&
(strings.Contains(response, "missing visibility") ||
strings.Contains(response, `"visibility" is missing`) ||
(strings.Contains(response, "missing required") && strings.Contains(response, "visibility")))
}

func errWithOutput(err error, out []byte) error {
trimmed := strings.TrimSpace(string(out))
if trimmed == "" {
Expand Down
Loading
Loading