-
Notifications
You must be signed in to change notification settings - Fork 434
Auto-pin unversioned action uses refs in compiler; fail compilation when no pin is available
#40475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a6c799e
51f6e8f
1bb2fb0
bb942f6
fdce4aa
2223db6
0a1d9be
af624cb
b4149bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import ( | |
| const setupNodeV6ExpectedUsesPlaceholder = "__setup_node_v6__" | ||
| const checkoutV6ExpectedUsesPlaceholder = "__checkout_v6__" | ||
| const checkoutSHAExpectedUsesPlaceholder = "__checkout_sha__" | ||
| const checkoutLatestExpectedUsesPlaceholder = "__checkout_latest__" | ||
|
|
||
| func expectedPinnedUses(t *testing.T, repo, version string) string { | ||
| t.Helper() | ||
|
|
@@ -29,6 +30,16 @@ func expectedPinnedUses(t *testing.T, repo, version string) string { | |
| return result | ||
| } | ||
|
|
||
| func latestPinnedUsesForRepo(t *testing.T, repo string) string { | ||
| t.Helper() | ||
|
|
||
| result := getCachedActionPin(repo, &WorkflowData{}) | ||
| if result == "" { | ||
| t.Fatalf("getCachedActionPin(%s) returned empty", repo) | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| // TestGetActionPinFallback tests that getActionPin returns empty string for unknown actions | ||
| func TestGetActionPinFallback(t *testing.T) { | ||
| result := getActionPin("unknown/action") | ||
|
|
@@ -173,6 +184,15 @@ func TestApplyActionPinToStep(t *testing.T) { | |
| expectPinned: false, | ||
| expectedUses: "my-org/my-action@v1", | ||
| }, | ||
| { | ||
| name: "step with unversioned action", | ||
| stepMap: map[string]any{ | ||
| "name": "Checkout", | ||
| "uses": "actions/checkout", | ||
| }, | ||
| expectPinned: true, | ||
| expectedUses: checkoutLatestExpectedUsesPlaceholder, | ||
| }, | ||
| { | ||
| name: "step without uses field", | ||
| stepMap: map[string]any{ | ||
|
|
@@ -205,7 +225,10 @@ func TestApplyActionPinToStep(t *testing.T) { | |
| } | ||
|
|
||
| // Apply action pinning using typed version | ||
| pinnedStep := applyActionPinToTypedStep(typedStep, data) | ||
| pinnedStep, pinErr := applyActionPinToTypedStep(typedStep, data) | ||
| if pinErr != nil { | ||
| t.Fatalf("applyActionPinToTypedStep() returned error: %v", pinErr) | ||
| } | ||
| if pinnedStep == nil { | ||
| t.Fatal("applyActionPinToTypedStep returned nil") | ||
| } | ||
|
|
@@ -231,6 +254,9 @@ func TestApplyActionPinToStep(t *testing.T) { | |
| if expectedUses == checkoutSHAExpectedUsesPlaceholder { | ||
| expectedUses = expectedPinnedUses(t, "actions/checkout", "de0fac2e4500dabe0009e67214ff5f5447ce83dd") | ||
| } | ||
| if expectedUses == checkoutLatestExpectedUsesPlaceholder { | ||
| expectedUses = latestPinnedUsesForRepo(t, "actions/checkout") | ||
| } | ||
| if usesStr != expectedUses { | ||
| t.Errorf("applyActionPinToTypedStep uses = %q, want %q", usesStr, expectedUses) | ||
| } | ||
|
|
@@ -319,6 +345,7 @@ func TestApplyActionPinToTypedStep(t *testing.T) { | |
| step *WorkflowStep | ||
| expectPinned bool | ||
| expectedUses string | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "step with pinned action (checkout)", | ||
|
|
@@ -350,6 +377,23 @@ func TestApplyActionPinToTypedStep(t *testing.T) { | |
| expectPinned: false, | ||
| expectedUses: "my-org/my-action@v1", | ||
| }, | ||
| { | ||
| name: "step with unversioned action in embedded pins", | ||
| step: &WorkflowStep{ | ||
| Name: "Checkout", | ||
| Uses: "actions/checkout", | ||
| }, | ||
| expectPinned: true, | ||
| expectedUses: checkoutLatestExpectedUsesPlaceholder, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No test for the 💡 Suggested fixAdd a case for an action that is NOT in the embedded pin set, in both {
name: "step with unversioned action not in embedded pins",
step: &WorkflowStep{
Name: "Unknown Action",
Uses: "unknown-org/not-in-pins-action",
},
expectPinned: false,
expectedUses: "unknown-org/not-in-pins-action",
},This exercises the fallback path and records the current accepted behavior: an unversioned
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error path is already covered: |
||
| }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The two new test cases only cover the happy path (a known embedded action). The skip path — where 💡 Suggested table entry{
name: "step with unversioned unknown action is unchanged",
step: &WorkflowStep{
Name: "Deploy",
Uses: "my-org/private-action",
},
expectPinned: false,
expectedUses: "my-org/private-action",
},This documents the current graceful-skip contract and prevents it from silently breaking.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The behavior now returns an error for unversioned actions not in the pin set (rather than passing through). The error path is covered by the |
||
| { | ||
| name: "step with unversioned action not in pins returns error", | ||
| step: &WorkflowStep{ | ||
| Name: "Custom action", | ||
| Uses: "my-org/my-unpinned-action", | ||
| }, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "step without uses field", | ||
| step: &WorkflowStep{ | ||
|
|
@@ -388,15 +432,29 @@ func TestApplyActionPinToTypedStep(t *testing.T) { | |
| // Create a test WorkflowData | ||
| data := &WorkflowData{} | ||
|
|
||
| result := applyActionPinToTypedStep(tt.step, data) | ||
| result, err := applyActionPinToTypedStep(tt.step, data) | ||
|
|
||
| if tt.step == nil { | ||
| if err != nil { | ||
| t.Errorf("applyActionPinToTypedStep(nil) returned error: %v", err) | ||
| } | ||
| if result != nil { | ||
| t.Errorf("applyActionPinToTypedStep(nil) = %v, want nil", result) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| if tt.wantErr { | ||
| if err == nil { | ||
| t.Errorf("applyActionPinToTypedStep() expected error, got nil") | ||
| } | ||
| return | ||
| } | ||
|
|
||
| if err != nil { | ||
| t.Fatalf("applyActionPinToTypedStep() returned error: %v", err) | ||
| } | ||
|
|
||
| if result == nil { | ||
| t.Fatalf("applyActionPinToTypedStep() returned nil") | ||
| } | ||
|
|
@@ -409,6 +467,9 @@ func TestApplyActionPinToTypedStep(t *testing.T) { | |
| if expectedUses == checkoutV6ExpectedUsesPlaceholder { | ||
| expectedUses = expectedPinnedUses(t, "actions/checkout", "v6") | ||
| } | ||
| if expectedUses == checkoutLatestExpectedUsesPlaceholder { | ||
| expectedUses = latestPinnedUsesForRepo(t, "actions/checkout") | ||
| } | ||
| if result.Uses != expectedUses { | ||
| t.Errorf("applyActionPinToTypedStep() uses = %q, want %q", result.Uses, expectedUses) | ||
| } | ||
|
|
@@ -453,7 +514,10 @@ func TestApplyActionPinToTypedStep_Immutability(t *testing.T) { | |
| originalUses := originalStep.Uses | ||
|
|
||
| data := &WorkflowData{} | ||
| result := applyActionPinToTypedStep(originalStep, data) | ||
| result, err := applyActionPinToTypedStep(originalStep, data) | ||
| if err != nil { | ||
| t.Fatalf("applyActionPinToTypedStep() returned error: %v", err) | ||
| } | ||
|
|
||
| // Verify the original step was not modified | ||
| if originalStep.Uses != originalUses { | ||
|
|
@@ -649,9 +713,10 @@ func TestApplyActionPinsToTypedSteps(t *testing.T) { | |
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| steps []*WorkflowStep | ||
| want []*WorkflowStep | ||
| name string | ||
| steps []*WorkflowStep | ||
| want []*WorkflowStep | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "nil steps", | ||
|
|
@@ -725,11 +790,32 @@ func TestApplyActionPinsToTypedSteps(t *testing.T) { | |
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "unversioned action not in pins returns error", | ||
| steps: []*WorkflowStep{ | ||
| { | ||
| Name: "Unknown action", | ||
| Uses: "my-org/my-unpinned-action", | ||
| }, | ||
| }, | ||
| wantErr: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := applyActionPinsToTypedSteps(tt.steps, data) | ||
| got, err := applyActionPinsToTypedSteps(tt.steps, data) | ||
|
|
||
| if tt.wantErr { | ||
| if err == nil { | ||
| t.Errorf("applyActionPinsToTypedSteps() expected error, got nil") | ||
| } | ||
| return | ||
| } | ||
|
|
||
| if err != nil { | ||
| t.Fatalf("applyActionPinsToTypedSteps() returned unexpected error: %v", err) | ||
| } | ||
|
|
||
| if len(got) != len(tt.want) { | ||
| t.Errorf("applyActionPinsToTypedSteps() returned %d steps, want %d", len(got), len(tt.want)) | ||
|
|
@@ -1224,7 +1310,10 @@ func TestMapToStepWithActionPinning(t *testing.T) { | |
| } | ||
|
|
||
| // Apply action pinning using typed version | ||
| pinnedStep := applyActionPinToTypedStep(typedStep, data) | ||
| pinnedStep, pinErr := applyActionPinToTypedStep(typedStep, data) | ||
| if pinErr != nil { | ||
| t.Fatalf("applyActionPinToTypedStep() returned error: %v", pinErr) | ||
| } | ||
| if pinnedStep == nil { | ||
| t.Fatal("applyActionPinToTypedStep returned nil") | ||
| } | ||
|
|
@@ -1328,7 +1417,10 @@ func TestSliceToStepsWithActionPinning(t *testing.T) { | |
| } | ||
|
|
||
| // Apply action pinning using typed version | ||
| pinnedSteps := applyActionPinsToTypedSteps(typedSteps, data) | ||
| pinnedSteps, pinErr := applyActionPinsToTypedSteps(typedSteps, data) | ||
| if pinErr != nil { | ||
| t.Fatalf("applyActionPinsToTypedSteps() returned unexpected error: %v", pinErr) | ||
| } | ||
| if len(pinnedSteps) != len(typedSteps) { | ||
| t.Errorf("applyActionPinsToTypedSteps() returned %d steps, want %d", len(pinnedSteps), len(typedSteps)) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd]
latestPinnedUsesForReporesolves viagetLatestActionPinByRepo→getActionPinWithData— the same call chain used by the implementation under test. This makes the assertions circular: they verify that the output matches itself, not a known-good SHA.💡 Suggestion
Either snapshot a fixed SHA in the test constant (updating it when pins rotate) or, at minimum, assert that the returned string matches the
{repo}@{40-char-sha} # {version}format:A format-only assertion is still far stronger than nothing — it catches the regression where unversioned refs pass through unchanged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in the latest commit:
latestPinnedUsesForReponow callsgetCachedActionPin(repo, &WorkflowData{})directly, matching the production code path.