Skip to content

Commit bc44126

Browse files
authored
feat: add GHES-compatible artifact action versions via feature flag
Add a ghes-artifact-compat feature flag that makes the compiler emit actions/upload-artifact@v3.2.2 and actions/download-artifact@v3.0.2 instead of the latest v7/v8. GHES does not support @actions/artifact v2.0.0+ (upload-artifact@v4+ / download-artifact@v4+), so compiled workflows fail with GHESNotSupportedError on enterprise instances. The fix uses a compile-scoped flag in getActionPin() that automatically covers all 19 artifact action call sites across the compiler without modifying each caller. Users opt in via frontmatter: features: ghes-artifact-compat: true Or the GH_AW_FEATURES=ghes-artifact-compat environment variable. Closes #29551
1 parent 5e766fc commit bc44126

5 files changed

Lines changed: 138 additions & 0 deletions

File tree

pkg/actionpins/data/action_pins.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@
198198
"repo": "super-linter/super-linter",
199199
"version": "v8.6.0",
200200
"sha": "9e863354e3ff62e0727d37183162c4a88873df41"
201+
},
202+
"actions/upload-artifact@v3.2.2": {
203+
"repo": "actions/upload-artifact",
204+
"version": "v3.2.2",
205+
"sha": "c6a366c94c3e0affe28c06c8df20a878f24da3cf"
206+
},
207+
"actions/download-artifact@v3.0.2": {
208+
"repo": "actions/download-artifact",
209+
"version": "v3.0.2",
210+
"sha": "9bc31d5ccc31df68ecc42ccf4149144866c47d8a"
201211
}
202212
},
203213
"containers": {

pkg/constants/feature_constants.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,14 @@ const (
7272
// features:
7373
// integrity-reactions: true
7474
IntegrityReactionsFeatureFlag FeatureFlag = "integrity-reactions"
75+
// GHESArtifactCompatFeatureFlag enables GHES-compatible artifact action versions.
76+
// When enabled, the compiler emits actions/upload-artifact@v3 and
77+
// actions/download-artifact@v3 instead of the latest v7/v8, because GHES does not
78+
// support @actions/artifact v2.0.0+ (upload-artifact@v4+ / download-artifact@v4+).
79+
//
80+
// Workflow frontmatter usage:
81+
//
82+
// features:
83+
// ghes-artifact-compat: true
84+
GHESArtifactCompatFeatureFlag FeatureFlag = "ghes-artifact-compat"
7585
)

pkg/workflow/action_pins.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,55 @@ func extractActionVersion(uses string) string {
4949
return actionpins.ExtractVersion(uses)
5050
}
5151

52+
// ghesArtifactCompatEnabled controls whether artifact actions use GHES-compatible
53+
// versions (v3.x) instead of the latest (v7/v8). GHES does not support
54+
// @actions/artifact v2.0.0+ (upload-artifact@v4+ / download-artifact@v4+).
55+
//
56+
// Set by the compiler at the start of CompileWorkflowData when the
57+
// ghes-artifact-compat feature flag is enabled, and reset via defer.
58+
var ghesArtifactCompatEnabled bool
59+
60+
// ghesArtifactCompatVersions maps artifact action repos to the exact GHES-compatible
61+
// version to use. These are the highest v3.x releases before the breaking v4 change.
62+
var ghesArtifactCompatVersions = map[string]string{
63+
"actions/upload-artifact": "v3.2.2",
64+
"actions/download-artifact": "v3.0.2",
65+
}
66+
67+
// SetGHESArtifactCompat enables or disables GHES-compatible artifact action versions
68+
// for the current compilation scope. Callers must defer SetGHESArtifactCompat(false).
69+
func SetGHESArtifactCompat(enabled bool) {
70+
ghesArtifactCompatEnabled = enabled
71+
if enabled {
72+
actionPinsLog.Print("GHES artifact compatibility mode enabled: artifact actions will use v3.x pins")
73+
}
74+
}
75+
5276
// getActionPin returns the pinned reference for the latest version of the repo
5377
// using only the embedded pins (no WorkflowData required).
78+
//
79+
// When GHES artifact compatibility mode is active (via SetGHESArtifactCompat),
80+
// artifact actions are pinned to v3.x instead of the latest version.
5481
func getActionPin(repo string) string {
5582
pins := actionpins.GetActionPinsByRepo(repo)
5683
if len(pins) == 0 {
5784
actionPinsLog.Printf("No embedded pins found for repo: %s", repo)
5885
return ""
5986
}
87+
88+
// When GHES compat is enabled, use the known-compatible v3.x version for artifact actions.
89+
if ghesArtifactCompatEnabled {
90+
if targetVersion, ok := ghesArtifactCompatVersions[repo]; ok {
91+
for _, pin := range pins {
92+
if pin.Version == targetVersion {
93+
actionPinsLog.Printf("GHES compat: using %s@%s instead of latest", repo, targetVersion)
94+
return actionpins.FormatPinnedActionReference(repo, pin.SHA, pin.Version)
95+
}
96+
}
97+
actionPinsLog.Printf("GHES compat: target version %s not found in pins for %s, falling back to latest", targetVersion, repo)
98+
}
99+
}
100+
60101
return actionpins.FormatPinnedActionReference(repo, pins[0].SHA, pins[0].Version)
61102
}
62103

pkg/workflow/action_pins_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,3 +1434,72 @@ func TestSliceToStepsErrorHandling(t *testing.T) {
14341434
})
14351435
}
14361436
}
1437+
1438+
// TestGetActionPinGHESArtifactCompat tests that GHES artifact compat mode returns v3 pins
1439+
func TestGetActionPinGHESArtifactCompat(t *testing.T) {
1440+
// Verify default (compat disabled) returns latest (v7/v8)
1441+
uploadPin := getActionPin("actions/upload-artifact")
1442+
if uploadPin == "" {
1443+
t.Fatal("getActionPin(actions/upload-artifact) returned empty")
1444+
}
1445+
if strings.Contains(uploadPin, "# v3") {
1446+
t.Errorf("Without GHES compat, expected latest upload-artifact pin, got v3: %s", uploadPin)
1447+
}
1448+
1449+
downloadPin := getActionPin("actions/download-artifact")
1450+
if downloadPin == "" {
1451+
t.Fatal("getActionPin(actions/download-artifact) returned empty")
1452+
}
1453+
if strings.Contains(downloadPin, "# v3") {
1454+
t.Errorf("Without GHES compat, expected latest download-artifact pin, got v3: %s", downloadPin)
1455+
}
1456+
1457+
// Enable GHES compat
1458+
SetGHESArtifactCompat(true)
1459+
defer SetGHESArtifactCompat(false)
1460+
1461+
uploadPinGHES := getActionPin("actions/upload-artifact")
1462+
if !strings.Contains(uploadPinGHES, "# v3.2.2") {
1463+
t.Errorf("With GHES compat, expected upload-artifact v3.2.2, got: %s", uploadPinGHES)
1464+
}
1465+
if !strings.Contains(uploadPinGHES, "c6a366c94c3e0affe28c06c8df20a878f24da3cf") {
1466+
t.Errorf("With GHES compat, expected upload-artifact v3.2.2 SHA, got: %s", uploadPinGHES)
1467+
}
1468+
1469+
downloadPinGHES := getActionPin("actions/download-artifact")
1470+
if !strings.Contains(downloadPinGHES, "# v3.0.2") {
1471+
t.Errorf("With GHES compat, expected download-artifact v3.0.2, got: %s", downloadPinGHES)
1472+
}
1473+
if !strings.Contains(downloadPinGHES, "9bc31d5ccc31df68ecc42ccf4149144866c47d8a") {
1474+
t.Errorf("With GHES compat, expected download-artifact v3.0.2 SHA, got: %s", downloadPinGHES)
1475+
}
1476+
1477+
// Non-artifact actions should be unaffected
1478+
checkoutPin := getActionPin("actions/checkout")
1479+
if checkoutPin == "" {
1480+
t.Fatal("getActionPin(actions/checkout) returned empty with GHES compat")
1481+
}
1482+
if strings.Contains(checkoutPin, "# v3") {
1483+
t.Errorf("GHES compat should not affect non-artifact actions, got: %s", checkoutPin)
1484+
}
1485+
}
1486+
1487+
// TestGHESArtifactCompatReset tests that disabling GHES compat restores default behavior
1488+
func TestGHESArtifactCompatReset(t *testing.T) {
1489+
// Get default pin
1490+
defaultPin := getActionPin("actions/upload-artifact")
1491+
1492+
// Enable compat
1493+
SetGHESArtifactCompat(true)
1494+
compatPin := getActionPin("actions/upload-artifact")
1495+
if defaultPin == compatPin {
1496+
t.Error("GHES compat pin should differ from default pin")
1497+
}
1498+
1499+
// Disable compat — should restore default
1500+
SetGHESArtifactCompat(false)
1501+
restoredPin := getActionPin("actions/upload-artifact")
1502+
if restoredPin != defaultPin {
1503+
t.Errorf("After disabling GHES compat, expected default pin %s, got %s", defaultPin, restoredPin)
1504+
}
1505+
}

pkg/workflow/compiler.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/github/gh-aw/pkg/console"
13+
"github.com/github/gh-aw/pkg/constants"
1314
"github.com/github/gh-aw/pkg/gitutil"
1415
"github.com/github/gh-aw/pkg/logger"
1516
"github.com/github/gh-aw/pkg/stringutil"
@@ -393,6 +394,13 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath
393394
c.artifactManager.Reset()
394395
}
395396

397+
// Enable GHES artifact compatibility when the feature flag is set.
398+
// This makes getActionPin return v3.x artifact actions instead of v7/v8.
399+
if isFeatureEnabled(constants.GHESArtifactCompatFeatureFlag, workflowData) {
400+
SetGHESArtifactCompat(true)
401+
defer SetGHESArtifactCompat(false)
402+
}
403+
396404
// Generate lock file name
397405
lockFile := stringutil.MarkdownToLockFile(markdownPath)
398406

0 commit comments

Comments
 (0)