Skip to content
Merged
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
2 changes: 1 addition & 1 deletion cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
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")
compileCmd.Flags().Bool("no-models-dev-lookup", false, "Disable compile-time models.dev pricing lookup for models missing from the embedded catalog")
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")
compileCmd.Flags().Bool("ghes", false, "Enable GitHub Enterprise Server (GHES) compatibility mode: emit upload-artifact@v3 and download-artifact@v3 instead of the latest v7/v8 which are not supported on GHES. Overrides the aw.json ghes field")
compileCmd.Flags().Bool("ghes", false, "Enable GitHub Enterprise Server (GHES) compatibility mode. Artifact actions continue using latest non-v3 pins (v3 is deprecated). Overrides the aw.json ghes field")
if err := compileCmd.Flags().MarkHidden("prior-manifest-file"); err != nil {
// Non-fatal: flag is registered even if MarkHidden fails
_ = err
Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/reference/enterprise-configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Enterprise Configuration
description: Configure GitHub Agentic Workflows for GitHub Enterprise Server (GHES) and GitHub Enterprise Cloud (GHEC), including artifact compatibility and CLI setup.
description: Configure GitHub Agentic Workflows for GitHub Enterprise Server (GHES) and GitHub Enterprise Cloud (GHEC), including GHES compatibility mode and CLI setup.
sidebar:
order: 51
---
Expand All @@ -11,11 +11,11 @@ This page covers configuration options specific to GitHub Enterprise Server (GHE

## GitHub Enterprise Server (GHES) Compatibility

### Artifact Compatibility Mode
### GHES Compatibility Mode

GHES instances running versions that predate `@actions/artifact` v2.0.0 support cannot use `actions/upload-artifact@v4+` or `actions/download-artifact@v4+`. Attempting to run compiled workflows on these instances produces a `GHESNotSupportedError`.

gh-aw includes a GHES compatibility mode that instructs the compiler to emit `upload-artifact@v3.2.2` and `download-artifact@v3.1.0` instead of the latest v4+ versions.
gh-aw includes a GHES compatibility mode toggle (`aw.json` `ghes` or `gh aw compile --ghes`) so GHES-targeted repositories can compile with explicit GHES mode enabled. Artifact actions continue using the latest non-v3 pins because v3 artifact actions are deprecated.

#### Enable via `aw.json` (recommended)

Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/reference/self-hosted-runners.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,11 @@ Self-hosted runners need outbound HTTPS access to:

Agentic workflows can run on GHES with some additional configuration.

### Artifact compatibility
### GHES compatibility mode

GHES does not support the `@actions/artifact` v2.0.0+ backend used by `upload-artifact@v4+` and `download-artifact@v4+`. Compiled workflows use the latest artifact action versions by default, which fail on GHES with `GHESNotSupportedError`.

Enable GHES compatibility mode in `.github/workflows/aw.json` to use compatible v3.x artifact actions:
Enable GHES compatibility mode in `.github/workflows/aw.json` to compile with GHES mode explicitly enabled:

```json
{
Expand All @@ -215,7 +215,7 @@ Or compile with `--ghes` for one-off workflow generation:
gh aw compile --ghes my-workflow.md
```

This makes the compiler emit `upload-artifact@v3.2.2` and `download-artifact@v3.1.0` instead of the latest versions, which are compatible with all GHES versions.
Artifact actions continue using the latest non-v3 pins because v3 artifact actions are deprecated.

### API endpoint

Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/compile_compiler_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ func configureCompilerFlags(compiler *workflow.Compiler, config CompileConfig) {
}

// Set GHES compatibility mode when the --ghes flag is passed.
// When enabled, the compiler emits v3.x artifact action pins for GHES compatibility.
// v3 artifact pins are deprecated, so artifact actions continue to use latest pins.
compiler.SetGHESCompat(config.GHESCompat)
if config.GHESCompat {
compileCompilerSetupLog.Print("GHES compatibility mode enabled via --ghes flag: artifact actions will use v3.x pins")
compileCompilerSetupLog.Print("GHES compatibility mode enabled via --ghes flag: artifact actions will use latest non-v3 pins")
}

// Load pre-cached manifests from file (written by MCP server at startup).
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/compile_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type CompileConfig struct {
Approve bool // Approve all safe update changes, skipping safe update enforcement regardless of strict mode setting.
ValidateImages bool // Require Docker to be available for container image validation (fail instead of skipping when Docker is unavailable)
PriorManifestFile string // Path to a JSON file containing pre-cached manifests (map[lockFile]*GHAWManifest) collected at MCP server startup; takes precedence over git HEAD / filesystem reads for safe update enforcement
GHESCompat bool // Enable GHES compatibility mode: emit v3.x artifact action pins instead of v7/v8 (overrides aw.json ghes field)
GHESCompat bool // Enable GHES compatibility mode (overrides aw.json ghes field); artifact actions still use latest non-v3 pins
DisableModelsDevLookup bool // Disable compile-time models.dev pricing lookup for models missing from the embedded catalog
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/parser/schemas/repo_config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
}
},
"ghes": {
"description": "Enable GitHub Enterprise Server (GHES) compatibility mode. When true, the compiler emits GHES-compatible artifact action versions (upload-artifact@v3, download-artifact@v3) instead of the latest v7/v8 which are not supported on GHES.",
"description": "Enable GitHub Enterprise Server (GHES) compatibility mode. Artifact actions continue to use latest non-v3 pins because upload-artifact/download-artifact v3 are deprecated.",
"type": "boolean"
},
"help_command": {
Expand Down
30 changes: 3 additions & 27 deletions pkg/workflow/action_pins.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,6 @@ func extractActionVersion(uses string) string {
return actionpins.ExtractVersion(uses)
}

// ghesArtifactPin holds the version tag and commit SHA for a GHES-compatible artifact action pin.
type ghesArtifactPin struct {
version string
sha string
}

// ghesArtifactCompatPins contains hardcoded GHES-compatible artifact action pins.
// These are the highest v3.x releases that avoid the @actions/artifact v2.0.0+
// GHES incompatibility (validated against GHES 3.19.1 per gh-aw#29551).
// Hardcoded because action_pins.json is auto-generated by `make recompile` and
// only tracks the latest versions — v3.x entries would be stripped.
var ghesArtifactCompatPins = map[string]ghesArtifactPin{
"actions/upload-artifact": {version: "v3.2.2", sha: "c6a366c94c3e0affe28c06c8df20a878f24da3cf"},
"actions/download-artifact": {version: "v3.1.0", sha: "a9bc5e6ef2cb54c177f32aa5726adaa15e7e2d59"},
}

// getActionPin returns the pinned reference for the latest version of the repo
// using only the embedded pins (no WorkflowData required).
func getActionPin(repo string) string {
Expand All @@ -82,23 +66,15 @@ func getActionPin(repo string) string {
return actionpins.FormatPinnedActionReference(repo, pins[0].SHA, pins[0].Version)
}

// getActionPin returns the pinned reference for the given repo, using GHES-compatible
// v3.x pins for artifact actions when c.ghesArtifactCompat is true.
// getActionPin returns the pinned reference for the given repo.
//
// This is the preferred call site for code running inside a Compiler method, since it
// automatically honours the per-compilation GHES compat flag without any global state.
// This is the preferred call site for code running inside a Compiler method because it
// reuses the compiler's shared cache/resolver and marks cached pins as used for pruning.
//
// If the compiler has an action cache and resolver, this method will check the cache for
// any existing entry and mark it as "used" for orphan pruning. This ensures compiler-generated
// action references (e.g., actions/cache/save in notify steps) are tracked.
func (c *Compiler) getActionPin(repo string) string {
if c.ghesArtifactCompat {
if pin, ok := ghesArtifactCompatPins[repo]; ok {
actionPinsLog.Printf("GHES compat: using %s@%s instead of latest", repo, pin.version)
return actionpins.FormatPinnedActionReference(repo, pin.sha, pin.version)
}
}

// Check the cache for any existing entry for this repo (regardless of version).
// Compiler-generated actions don't specify versions, so prefer a cached entry only
// when it is at least as new as the latest embedded pin.
Expand Down
68 changes: 21 additions & 47 deletions pkg/workflow/action_pins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1576,7 +1576,7 @@ func TestSliceToStepsErrorHandling(t *testing.T) {
}
}

// TestGetActionPinGHESArtifactCompat tests that GHES artifact compat mode returns v3 pins
// TestGetActionPinGHESArtifactCompat verifies GHES compat mode does not emit deprecated v3 artifact pins.
func TestGetActionPinGHESArtifactCompat(t *testing.T) {
// Verify default (compat disabled) returns latest (v7/v8)
defaultCompiler := NewCompiler()
Expand Down Expand Up @@ -1607,19 +1607,19 @@ func TestGetActionPinGHESArtifactCompat(t *testing.T) {
compatCompiler.ghesArtifactCompat = true

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 test sets compatCompiler.ghesArtifactCompat = true directly (a private field). Since the field now has no behavioral effect on getActionPin, this test validates a tautology: it sets a no-op flag, then asserts the result is the same as without the flag. The test will always pass regardless of whether GHES compat is wired up correctly.

Consider driving GHES compat through the public API (compatCompiler.SetGHESCompat(true) followed by a compile step) or testing at integration level via configureCompilerFlags, to ensure the public contract is actually exercised.

@copilot please address this.


uploadPinGHES := compatCompiler.getActionPin("actions/upload-artifact")
if !strings.Contains(uploadPinGHES, "# v3.2.2") {
t.Errorf("With GHES compat, expected upload-artifact v3.2.2, got: %s", uploadPinGHES)
if strings.Contains(uploadPinGHES, "# v3") {
t.Errorf("With GHES compat, expected non-v3 upload-artifact pin, got: %s", uploadPinGHES)
}
if !strings.Contains(uploadPinGHES, "c6a366c94c3e0affe28c06c8df20a878f24da3cf") {
t.Errorf("With GHES compat, expected upload-artifact v3.2.2 SHA, got: %s", uploadPinGHES)
if uploadPinGHES != uploadPin {
t.Errorf("With GHES compat, expected upload-artifact pin to match default, default=%s compat=%s", uploadPin, uploadPinGHES)
}

downloadPinGHES := compatCompiler.getActionPin("actions/download-artifact")
if !strings.Contains(downloadPinGHES, "# v3.1.0") {
t.Errorf("With GHES compat, expected download-artifact v3.1.0, got: %s", downloadPinGHES)
if strings.Contains(downloadPinGHES, "# v3") {
t.Errorf("With GHES compat, expected non-v3 download-artifact pin, got: %s", downloadPinGHES)
}
if !strings.Contains(downloadPinGHES, "a9bc5e6ef2cb54c177f32aa5726adaa15e7e2d59") {
t.Errorf("With GHES compat, expected download-artifact v3.1.0 SHA, got: %s", downloadPinGHES)
if downloadPinGHES != downloadPin {
t.Errorf("With GHES compat, expected download-artifact pin to match default, default=%s compat=%s", downloadPin, downloadPinGHES)
}

// Non-artifact actions should be unaffected by GHES compat
Expand All @@ -1629,46 +1629,20 @@ func TestGetActionPinGHESArtifactCompat(t *testing.T) {
}
}

// TestGHESArtifactCompatReset tests that two Compiler instances with different compat
// settings return different pins, confirming state is per-instance, not global.
func TestGHESArtifactCompatReset(t *testing.T) {
// Default compiler returns latest pin
defaultCompiler := NewCompiler()
defaultPin := defaultCompiler.getActionPin("actions/upload-artifact")

// Compat compiler returns v3 pin
compatCompiler := NewCompiler()
compatCompiler.ghesArtifactCompat = true
compatPin := compatCompiler.getActionPin("actions/upload-artifact")

if defaultPin == compatPin {
t.Error("GHES compat pin should differ from default pin")
}

// Default compiler is unchanged after compat compiler was used
if defaultCompiler.getActionPin("actions/upload-artifact") != defaultPin {
t.Error("Default compiler pin changed after compat compiler was used — state leaked")
}
}

// TestGHESArtifactCompatPinsExist verifies that the hardcoded GHES compat pins are valid.
func TestGHESArtifactCompatPinsExist(t *testing.T) {
// TestGHESArtifactCompatDoesNotUseV3 verifies GHES compat mode never emits deprecated v3 artifact pins.
func TestGHESArtifactCompatDoesNotUseV3(t *testing.T) {
c := NewCompiler()
c.ghesArtifactCompat = true
for repo, pin := range ghesArtifactCompatPins {
if pin.sha == "" {
t.Errorf("ghesArtifactCompatPins[%s] has empty SHA", repo)
}
if pin.version == "" {
t.Errorf("ghesArtifactCompatPins[%s] has empty version", repo)
}
result := c.getActionPin(repo)
if result == "" {
t.Errorf("getActionPin(%s) returned empty with GHES compat enabled", repo)
}
if !strings.Contains(result, pin.sha) {
t.Errorf("getActionPin(%s) did not contain expected SHA %s, got: %s", repo, pin.sha, result)
}
for _, repo := range []string{"actions/upload-artifact", "actions/download-artifact"} {
t.Run(repo, func(t *testing.T) {
result := c.getActionPin(repo)
if result == "" {
t.Errorf("getActionPin(%s) returned empty with GHES compat enabled", repo)
}
if strings.Contains(result, "# v3") {
t.Errorf("getActionPin(%s) should not return a v3 pin, got: %s", repo, result)
}
})
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath
}
}
if c.ghesArtifactCompat {

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.

After removing the v3 pin override from getActionPin(), ghesArtifactCompat is now write-only: it is set (lines 437–441) and logged here, but no code path makes a behavioral decision based on it. This means --ghes / aw.json ghes: true silently have no effect on compilation output beyond this log message.

If GHES mode is intended to gate future non-artifact behavior, this is fine as scaffolding. But if the flag is now truly inert, consider removing the field, CLI flag, and schema entry to avoid misleading users — or add an explicit comment documenting that GHES compat is retained for future use.

@copilot please address this.

actionPinsLog.Print("GHES artifact compatibility mode enabled: artifact actions will use v3.x pins")
actionPinsLog.Print("GHES compatibility mode enabled: artifact actions continue using latest non-v3 pins")
}
Comment on lines 444 to 446

// Generate lock file name
Expand Down
9 changes: 4 additions & 5 deletions pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ type Compiler struct {
priorManifests map[string]*GHAWManifest // Pre-cached manifests keyed by lock file path; takes precedence over git HEAD / filesystem reads
requireDocker bool // If true, fail validation when Docker is not available instead of silently skipping
ghesCompatFromCLI bool // If true, GHES compat was requested via --ghes CLI flag (takes precedence over aw.json)
ghesArtifactCompat bool // If true, emit GHES-compatible v3.x pins for artifact actions instead of the latest v7/v8
ghesArtifactCompat bool // If true, GHES compatibility mode is enabled; artifact actions still use latest non-v3 pins
ownerTypeCache map[string]string // Cached GitHub owner type ("User"/"Organization"/"") keyed by owner login; not goroutine-safe (Compiler is used sequentially)
copilotRequestsTipShown map[string]bool // Tracks markdown paths that already emitted the copilot-requests enable tip in this compiler instance
// modelPricingResolver is an optional callback for resolving per-token pricing of models that
Expand Down Expand Up @@ -241,10 +241,9 @@ func (c *Compiler) SetAllowActionRefs(allow bool) {
c.allowActionRefs = allow
}

// SetGHESCompat enables GHES artifact compatibility mode via the --ghes CLI flag.
// When true, the compiler emits GHES-compatible v3.x artifact action pins
// (upload-artifact@v3, download-artifact@v3) instead of the latest v7/v8.
// This flag takes precedence over the aw.json ghes field.
// SetGHESCompat enables GHES compatibility mode via the --ghes CLI flag.
// It overrides the aw.json ghes field for the current compilation run.
// Artifact actions still use the latest non-v3 pins.
func (c *Compiler) SetGHESCompat(enabled bool) {
c.ghesCompatFromCLI = enabled
}
Expand Down
7 changes: 3 additions & 4 deletions pkg/workflow/repo_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// Configuration reference:
//
// {
// "ghes": true, // enables GHES compatibility mode (v3 artifact pins)
// "ghes": true, // enables GHES compatibility mode (artifact pins remain latest non-v3)
// "help_command": false, // disables builtin centralized /help comment handler
// "utc": "-08:00", // project home UTC offset for rendered local times
// "auto_upgrade": true, // set to true to generate agentic-auto-upgrade.yml with weekly schedule
Expand Down Expand Up @@ -115,9 +115,8 @@ func (m *MaintenanceConfig) IsLabelTriggerEnabled() bool {
// RepoConfig is the parsed representation of aw.json.
type RepoConfig struct {
// GHES enables GitHub Enterprise Server compatibility mode.
// When true, the compiler emits GHES-compatible artifact action versions
// (upload-artifact@v3, download-artifact@v3) instead of the latest v7/v8
// which are not supported on GHES.
// When true, the compiler enables GHES compatibility behavior. Artifact actions
// continue to use latest non-v3 pins because v3 artifact actions are deprecated.
GHES bool

// UTC is the project's home UTC offset used for rendering local times in CLI output.
Expand Down
Loading