Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 .github/workflows/aw.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"auto_upgrade": { "cron": "0 9 * * 1" },
"ghes": true,
"ghes": false,
Comment thread
github-actions[bot] marked this conversation as resolved.
"maintenance": {
"action_failure_issue_expires": 12,
"label_triggers": true
Expand Down
2 changes: 1 addition & 1 deletion cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ func configureCompileToolFlags() {
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")
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().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. Artifact actions continue using latest non-v3 pins (v3 is deprecated). Overrides the aw.json ghes field.")
compileCmd.Flags().Bool("ghes", false, "Enable GitHub Enterprise Server (GHES) compatibility mode: emit upload-artifact@v3.2.2 and download-artifact@v3.1.0. Overrides the aw.json ghes field.")
}

func finalizeCompileFlags() {
Expand Down
2 changes: 1 addition & 1 deletion docs/enterprise-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ When GHES compatibility mode is active, the compiler emits:
| Action | Default | GHES compatible |
|--------|---------|-----------------|
| `actions/upload-artifact` | `@v7` (latest) | `@v3.2.2` |
Comment thread
github-actions[bot] marked this conversation as resolved.
| `actions/download-artifact` | `@v4` (latest) | `@v3.1.0` |
| `actions/download-artifact` | `@v8` (latest) | `@v3.1.0` |

All other actions are unaffected.

Expand Down
4 changes: 3 additions & 1 deletion docs/src/content/docs/reference/enterprise-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ This page covers configuration options specific to GitHub Enterprise Server (GHE

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 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.
gh-aw includes a GHES compatibility mode toggle (`aw.json` `ghes` or `gh aw compile --ghes`) for GHES releases that require the v3 artifact backend. Compatibility mode emits `upload-artifact@v3.2.2` and `download-artifact@v3.1.0`; default GitHub.com compilation continues to use the latest artifact actions.

This compatibility path supports GHES 3.21.x and earlier. For later GHES releases, keep compatibility mode enabled until your instance supports the v4 artifact backend.
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated

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

Expand Down
4 changes: 3 additions & 1 deletion docs/src/content/docs/reference/self-hosted-runners.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,9 @@ Or compile with `--ghes` for one-off workflow generation:
gh aw compile --ghes my-workflow.md
```

Artifact actions continue using the latest non-v3 pins because v3 artifact actions are deprecated.
Compatibility mode emits `upload-artifact@v3.2.2` and `download-artifact@v3.1.0`, which use the artifact backend supported by GHES. Default GitHub.com compilation continues to use the latest artifact actions.

This path supports GHES 3.21.x and earlier. Keep compatibility mode enabled on later releases until the instance supports the v4 artifact backend.
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated

### API endpoint

Expand Down
49 changes: 49 additions & 0 deletions pkg/actionpins/actionpins_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,55 @@ func (r *countingResolver) ResolveSHA(_ context.Context, _, _ string) (string, e
return "", nil
}

func TestResolveActionPin_GHESArtifactCompatibility(t *testing.T) {
t.Parallel()

tests := []struct {
repo string
want string
}{
{
repo: "actions/upload-artifact",
want: "actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf # v3.2.2",
},
{
repo: "actions/download-artifact",
want: "actions/download-artifact@a9bc5e6ef2cb54c177f32aa5726adaa15e7e2d59 # v3.1.0",
},
}

for _, tt := range tests {
t.Run(tt.repo, func(t *testing.T) {
t.Parallel()
resolver := &countingResolver{}
got, err := ResolveActionPin(tt.repo, "latest", &PinContext{
GHES: true,
Resolver: resolver,
})
require.NoError(t, err)
assert.Equal(t, tt.want, got)
assert.Zero(t, resolver.called, "GHES compatibility pins should not require dynamic resolution")
})
}
}

func TestResolveActionPin_GHESMappingTakesPrecedence(t *testing.T) {
t.Parallel()

resolver := &countingResolver{}
got, err := ResolveActionPin("actions/upload-artifact", "v7", &PinContext{
GHES: true,
Resolver: resolver,
Comment thread
github-actions[bot] marked this conversation as resolved.
Mappings: map[string]string{
"actions/upload-artifact@v7": "enterprise/upload-artifact@v3",
},
})

require.NoError(t, err)
assert.Empty(t, got)
assert.Equal(t, 1, resolver.called, "mapped enterprise action should use normal resolution")
}

func TestBuildByRepoIndex_GroupsByRepoAndSortsDescending(t *testing.T) {
t.Parallel()
pins := []ActionPin{
Expand Down
29 changes: 29 additions & 0 deletions pkg/actionpins/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ import (
"github.com/github/gh-aw/pkg/semverutil"
)

var ghesArtifactPins = map[string]ActionPin{
"actions/upload-artifact": {
Repo: "actions/upload-artifact",
Version: "v3.2.2",
SHA: "c6a366c94c3e0affe28c06c8df20a878f24da3cf",
},
"actions/download-artifact": {
Repo: "actions/download-artifact",
Version: "v3.1.0",
SHA: "a9bc5e6ef2cb54c177f32aa5726adaa15e7e2d59",
},
}

// recordPinResolutionFailure silently records an unresolved action-ref pinning event
// to the audit callback (ctx.RecordResolutionFailure), if one is configured.
// If ctx is nil or ctx.RecordResolutionFailure is nil, the function returns early without recording.
Expand All @@ -36,6 +49,13 @@ func ResolveActionPin(actionRepo, version string, ctx *PinContext) (string, erro
// Apply repository/version mapping from aw.json action_pins before resolution.
actionRepo, version = applyActionPinMapping(actionRepo, version, ctx)

if ctx.GHES {
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
if pin, ok := ghesArtifactPins[actionRepo]; ok {
actionPinsLog.Printf("GHES mode: using %s@%s", actionRepo, pin.Version)
return FormatPinnedActionReference(pin.Repo, pin.SHA, pin.Version), nil
}
}

isAlreadySHA := gitutil.IsValidFullSHA(version)
if pinnedRef, ok := resolveActionPinDynamically(actionRepo, version, isAlreadySHA, ctx); ok {
return pinnedRef, nil
Expand Down Expand Up @@ -71,6 +91,15 @@ func ResolveActionPin(actionRepo, version string, ctx *PinContext) (string, erro
return "", nil
}

// ResolveGHESActionPin returns the GHES-compatible pin for repo, if one is required.
func ResolveGHESActionPin(repo string) (string, bool) {
pin, ok := ghesArtifactPins[repo]
if !ok {
return "", false
}
return FormatPinnedActionReference(pin.Repo, pin.SHA, pin.Version), true
}

func resolveActionPinDynamically(actionRepo, version string, isAlreadySHA bool, ctx *PinContext) (string, bool) {
if ctx.Resolver == nil || isAlreadySHA {
logDynamicResolutionSkipped(ctx.Resolver != nil, isAlreadySHA)
Expand Down
2 changes: 2 additions & 0 deletions pkg/actionpins/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ type PinContext struct {
// AllowActionRefs lowers unresolved pinning failures to warnings.
// When false, unresolved action refs return an error.
AllowActionRefs bool
// GHES selects action versions compatible with GitHub Enterprise Server.
GHES bool
// Warnings is a shared map for deduplicating warning messages.
// Keys are cache keys in the form "repo@version".
Warnings map[string]bool
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 @@ -180,10 +180,10 @@ func configureCompilerFlags(compiler *workflow.Compiler, config CompileConfig) {
}

// Set GHES compatibility mode when the --ghes flag is passed.
// v3 artifact pins are deprecated, so artifact actions continue to use latest pins.
// When enabled, artifact actions use versions supported by GHES.
compiler.SetGHESCompat(config.GHESCompat)
if config.GHESCompat {
compileCompilerSetupLog.Print("GHES compatibility mode enabled via --ghes flag: artifact actions will use latest non-v3 pins")
compileCompilerSetupLog.Print("GHES compatibility mode enabled via --ghes flag: artifact actions will use v3-compatible 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 @@ -41,7 +41,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 (overrides aw.json ghes field); artifact actions still use latest non-v3 pins
GHESCompat bool // Enable GHES-compatible v3 artifact actions (overrides aw.json ghes field)
}

func (c CompileConfig) shellcheckEnabled() bool {
Expand Down
44 changes: 44 additions & 0 deletions pkg/cli/compile_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,50 @@ Please check the repository for any open issues and create a summary.
t.Logf("Successfully compiled workflow to %s", lockFilePath)
}

func TestCompileGHESArtifactPinsIntegration(t *testing.T) {
setup := setupIntegrationTest(t)
defer setup.cleanup()

testWorkflowPath := filepath.Join(setup.workflowsDir, "ghes-artifacts.md")
err := os.WriteFile(testWorkflowPath, []byte(`---
on: workflow_dispatch
permissions:
contents: read
engine: copilot
strict: false
steps:
- uses: actions/upload-artifact@v7
with:
name: test
path: test.txt
---
# GHES artifact pins
`), 0o600)
if err != nil {
t.Fatalf("Failed to write test workflow: %v", err)
}

cmd := exec.Command(setup.binaryPath, "compile", "--ghes", testWorkflowPath)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output))
}

lockContent, err := os.ReadFile(filepath.Join(setup.workflowsDir, "ghes-artifacts.lock.yml"))
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}
contents := string(lockContent)
if !strings.Contains(contents, "actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf # v3.2.2") {
t.Error("Lock file should contain the GHES-compatible upload-artifact pin")
}
if !strings.Contains(contents, "actions/download-artifact@a9bc5e6ef2cb54c177f32aa5726adaa15e7e2d59 # v3.1.0") {
t.Error("Lock file should contain the GHES-compatible download-artifact pin")
}
if strings.Contains(contents, "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1") {
t.Error("Lock file should not contain the default upload-artifact pin")
}
}

func removeAllWithRetry(path string) error {
attempts := 1
if runtime.GOOS == "windows" {
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 @@ -45,7 +45,7 @@
}
},
"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.",
"description": "Enable GitHub Enterprise Server (GHES) compatibility mode. Artifact actions use upload-artifact@v3.2.2 and download-artifact@v3.1.0.",
"type": "boolean"
},
"help_command": {
Expand Down
6 changes: 6 additions & 0 deletions pkg/workflow/action_pins.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ func getActionPin(repo string) string {
// 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 := actionpins.ResolveGHESActionPin(repo); ok {
return pin
}
Comment thread
github-actions[bot] marked this conversation as resolved.
}

// 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
26 changes: 13 additions & 13 deletions pkg/workflow/action_pins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1622,7 +1622,7 @@ func TestSliceToStepsErrorHandling(t *testing.T) {
}
}

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

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

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

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

// TestGHESArtifactCompatDoesNotUseV3 verifies GHES compat mode never emits deprecated v3 artifact pins.
func TestGHESArtifactCompatDoesNotUseV3(t *testing.T) {
// TestGHESArtifactCompatPinsExist verifies GHES compatibility pins are complete.
func TestGHESArtifactCompatPinsExist(t *testing.T) {
c := NewCompiler()
c.ghesArtifactCompat = true
for _, repo := range []string{"actions/upload-artifact", "actions/download-artifact"} {
Expand All @@ -1685,8 +1685,8 @@ func TestGHESArtifactCompatDoesNotUseV3(t *testing.T) {
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)
if !strings.Contains(result, "# v3") {
t.Errorf("getActionPin(%s) should return a v3 pin, got: %s", repo, result)
}
})
}
Expand Down
22 changes: 13 additions & 9 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,23 @@ func (c *Compiler) CompileWorkflow(markdownPath string) error {
if isFormattedCompilerError(err) {
return err
}

// Fallback for any unformatted error that slipped through.
return formatCompilerError(markdownPath, "error", err.Error(), err)
}

return c.CompileWorkflowData(workflowData, markdownPath)
}

func (c *Compiler) configureGHESCompatibility() {
c.ghesArtifactCompat = c.ghesCompatFromCLI
if !c.ghesArtifactCompat {
if repoConfig, err := c.loadRepoConfig(); err == nil && repoConfig != nil {
c.ghesArtifactCompat = repoConfig.GHES
}
}
}

// validateWorkflowData orchestrates all validation of workflow configuration by
// delegating to four focused validators. Each validator is independently testable
// and covers a distinct concern:
Expand Down Expand Up @@ -441,17 +451,11 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath
}

// Enable GHES artifact compatibility from CLI flag or aw.json (CLI flag wins).
// c.ghesCompatFromCLI is set once per compiler instance via SetGHESCompat().
c.ghesArtifactCompat = c.ghesCompatFromCLI
if !c.ghesArtifactCompat {
// Fall back to aw.json ghes field when CLI flag was not passed.
if repoConfig, err := c.loadRepoConfig(); err == nil && repoConfig != nil {
c.ghesArtifactCompat = repoConfig.GHES
}
}
c.configureGHESCompatibility()
if c.ghesArtifactCompat {
actionPinsLog.Print("GHES compatibility mode enabled: artifact actions continue using latest non-v3 pins")
actionPinsLog.Print("GHES compatibility mode enabled: artifact actions will use v3-compatible pins")
}
workflowData.GHES = c.ghesArtifactCompat

// Generate lock file name
lockFile := stringutil.MarkdownToLockFile(markdownPath)
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_mutators.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func (c *Compiler) SetAllowActionRefs(allow bool) {

// 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.
// Artifact actions use versions supported by GHES.
func (c *Compiler) SetGHESCompat(enabled bool) {
c.ghesCompatFromCLI = enabled
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/workflow/compiler_orchestrator_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type workflowBuildContext struct {
// ParseWorkflowFile parses a workflow markdown file and returns a WorkflowData structure.
// This is the main orchestration function that coordinates all compilation phases.
func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error) {
c.configureGHESCompatibility()
Comment thread
github-actions[bot] marked this conversation as resolved.
Comment thread
github-actions[bot] marked this conversation as resolved.

// Behavior-defined engines are contributed by a workflow's imports, so their
// registry and catalog must not affect subsequent compilations.
c.engineRegistry = NewEngineRegistry()
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,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, GHES compatibility mode is enabled; artifact actions still use latest non-v3 pins
ghesArtifactCompat bool // If true, emit GHES-compatible v3 pins for artifact actions
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
copilotTipNeeded bool // Tracks whether batch output should include the copilot-requests enable tip
Expand Down
Loading
Loading