Skip to content

Commit 9b7a992

Browse files
authored
test(act): add tests for tooling versions (go, node) and js package manager detection logic (#517)
1 parent 0cf46e1 commit 9b7a992

11 files changed

Lines changed: 739 additions & 11 deletions

File tree

actions/internal/plugins/setup/action.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@ inputs:
4949
description: mage version to use.
5050
required: true
5151

52+
outputs:
53+
node-version:
54+
description: The Node.js version that was installed.
55+
value: ${{ steps.node.outputs.node-version }}
56+
go-version:
57+
description: The Go version that was installed.
58+
value: ${{ steps.go.outputs.go-version }}
59+
node-package-manager-name:
60+
description: The name of the detected Node.js package manager (npm, yarn, or pnpm).
61+
value: ${{ steps.package-manager.outputs.name }}
62+
node-package-manager-version:
63+
description: The version of the detected Node.js package manager.
64+
value: ${{ steps.package-manager.outputs.version }}
65+
5266
runs:
5367
using: composite
5468
steps:
@@ -66,6 +80,7 @@ runs:
6680
version: ${{ steps.package-manager.outputs.version }}
6781

6882
- name: Node
83+
id: node
6984
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
7085
with:
7186
node-version: "${{ inputs.node-version }}"

tests/act/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/goccy/go-yaml v1.18.0
77
github.com/google/uuid v1.6.0
88
github.com/spf13/afero v1.15.0
9+
golang.org/x/mod v0.32.0
910
)
1011

1112
require golang.org/x/text v0.28.0 // indirect

tests/act/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
1010
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
1111
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
1212
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
13+
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
14+
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
1315
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
1416
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
1517
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Package versions provides functions to fetch the latest versions of Node.js and Go
2+
// from their official release APIs.
3+
package versions
4+
5+
import (
6+
"encoding/json"
7+
"fmt"
8+
"net/http"
9+
"strings"
10+
"time"
11+
12+
"golang.org/x/mod/semver"
13+
)
14+
15+
const (
16+
nodeReleasesURL = "https://nodejs.org/download/release/index.json"
17+
goReleasesURL = "https://golang.org/dl/?mode=json&include=all"
18+
httpTimeout = 30 * time.Second
19+
)
20+
21+
type nodeRelease struct {
22+
Version string `json:"version"` // e.g., "v24.12.0"
23+
}
24+
25+
type goRelease struct {
26+
Version string `json:"version"` // e.g., "go1.25.6"
27+
Stable bool `json:"stable"`
28+
}
29+
30+
// LatestNodeVersion returns the latest Node.js version for a given major version.
31+
// For example, LatestNodeVersion("24") might return "v24.12.0".
32+
func LatestNodeVersion(major string) (string, error) {
33+
releases, err := fetchJSON[[]nodeRelease](nodeReleasesURL)
34+
if err != nil {
35+
return "", fmt.Errorf("fetch node releases: %w", err)
36+
}
37+
38+
// Match versions like "v24.x.y" for major "24"
39+
prefix := "v" + major + "."
40+
for _, r := range releases {
41+
if strings.HasPrefix(r.Version, prefix) {
42+
// The releases are sorted newest first, so the first match is the latest
43+
return r.Version, nil
44+
}
45+
}
46+
47+
return "", fmt.Errorf("no releases found for Node.js major version %q", major)
48+
}
49+
50+
// LatestGoVersion returns the latest Go version for a given major.minor version.
51+
// For example, LatestGoVersion("1.25") might return "1.25.6".
52+
func LatestGoVersion(majorMinor string) (string, error) {
53+
releases, err := fetchJSON[[]goRelease](goReleasesURL)
54+
if err != nil {
55+
return "", fmt.Errorf("fetch go releases: %w", err)
56+
}
57+
58+
prefix := "go" + majorMinor
59+
var candidates []string
60+
61+
for _, r := range releases {
62+
if !r.Stable {
63+
continue
64+
}
65+
// Match "go1.25" or "go1.25.x"
66+
if r.Version == prefix || strings.HasPrefix(r.Version, prefix+".") {
67+
// Convert to semver format: "go1.25.6" -> "v1.25.6"
68+
v := "v" + strings.TrimPrefix(r.Version, "go")
69+
candidates = append(candidates, v)
70+
}
71+
}
72+
73+
if len(candidates) == 0 {
74+
return "", fmt.Errorf("no stable releases found for Go version %q", majorMinor)
75+
}
76+
77+
semver.Sort(candidates)
78+
latest := candidates[len(candidates)-1]
79+
80+
// Convert back: "v1.25.6" -> "1.25.6"
81+
return strings.TrimPrefix(latest, "v"), nil
82+
}
83+
84+
func fetchJSON[T any](url string) (T, error) {
85+
var result T
86+
87+
client := &http.Client{Timeout: httpTimeout}
88+
resp, err := client.Get(url)
89+
if err != nil {
90+
return result, fmt.Errorf("fetch %s: %w", url, err)
91+
}
92+
defer func() { _ = resp.Body.Close() }()
93+
94+
if resp.StatusCode != http.StatusOK {
95+
return result, fmt.Errorf("fetch %s: status %d", url, resp.StatusCode)
96+
}
97+
98+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
99+
return result, fmt.Errorf("decode JSON from %s: %w", url, err)
100+
}
101+
102+
return result, nil
103+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package versions
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestLatestNodeVersion(t *testing.T) {
10+
version, err := LatestNodeVersion("24")
11+
require.NoError(t, err)
12+
require.Regexp(t, `^v24\.\d+\.\d+$`, version, "should match v24.x.y pattern")
13+
t.Logf("Latest Node.js 24.x: %s", version)
14+
}
15+
16+
func TestLatestGoVersion(t *testing.T) {
17+
version, err := LatestGoVersion("1.25")
18+
require.NoError(t, err)
19+
require.Regexp(t, `^1\.25(\.\d+)?$`, version, "should match 1.25 or 1.25.x pattern")
20+
t.Logf("Latest Go 1.25.x: %s", version)
21+
}

tests/act/internal/workflow/ci/ci.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ type WorkflowInputs struct {
9595
PluginVersionSuffix *string
9696
DistArtifactsPrefix *string
9797

98+
GoVersion *string
99+
NodeVersion *string
100+
GolangciLintVersion *string
101+
MageVersion *string
102+
TrufflehogVersion *string
103+
98104
RunPlaywright *bool
99105

100106
RunPluginValidator *bool
@@ -112,10 +118,20 @@ func SetCIInputs(dst *workflow.Job, inputs WorkflowInputs) {
112118
workflow.SetJobInput(dst, "plugin-directory", inputs.PluginDirectory)
113119
workflow.SetJobInput(dst, "plugin-version-suffix", inputs.PluginVersionSuffix)
114120
workflow.SetJobInput(dst, "dist-artifacts-prefix", inputs.DistArtifactsPrefix)
121+
122+
workflow.SetJobInput(dst, "go-version", inputs.GoVersion)
123+
workflow.SetJobInput(dst, "node-version", inputs.NodeVersion)
124+
workflow.SetJobInput(dst, "golangci-lint-version", inputs.GolangciLintVersion)
125+
workflow.SetJobInput(dst, "mage-version", inputs.MageVersion)
126+
workflow.SetJobInput(dst, "trufflehog-version", inputs.TrufflehogVersion)
127+
115128
workflow.SetJobInput(dst, "run-playwright", inputs.RunPlaywright)
129+
116130
workflow.SetJobInput(dst, "run-plugin-validator", inputs.RunPluginValidator)
117131
workflow.SetJobInput(dst, "plugin-validator-config", inputs.PluginValidatorConfig)
132+
118133
workflow.SetJobInput(dst, "run-trufflehog", inputs.RunTruffleHog)
134+
119135
workflow.SetJobInput(dst, "allow-unsigned", inputs.AllowUnsigned)
120136
workflow.SetJobInput(dst, "testing", inputs.Testing)
121137
}

tests/act/internal/workflow/testing.go

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,21 +161,27 @@ func NewTestingWorkflow(baseName string, workflow BaseWorkflow, opts ...TestingW
161161
// TestingWorkflowOption defines a function type for configuring TestingWorkflow instances.
162162
type TestingWorkflowOption func(*TestingWorkflow)
163163

164-
// WithOnlyOneJob keeps only the given job ID and its dependencies
165-
// in the workflow, removing all other jobs.
166-
// This can be used to run only a specific job for testing purposes.
167-
func WithOnlyOneJob(t *testing.T, jobID string) TestingWorkflowOption {
164+
// WithOnlyOneJob keeps only the given job ID, removing all other jobs.
165+
// If removeDependencies is true, it will also remove all dependencies of the given job.
166+
// You normally don't want to remove dependencies, otherwise the workflow might fail if it consumes the output of a dependency.
167+
func WithOnlyOneJob(t *testing.T, jobID string, removeDependencies bool) TestingWorkflowOption {
168168
return func(twf *TestingWorkflow) {
169169
onlyJob, ok := twf.BaseWorkflow.Jobs[jobID]
170170
require.True(t, ok, fmt.Errorf("job %q not found", jobID))
171171

172-
// Remove all jobs except the given one and its dependencies
172+
// Remove all jobs
173173
for k := range twf.BaseWorkflow.Jobs {
174-
if k == jobID || slices.Contains(onlyJob.Needs, k) {
174+
// Do not remove the given job if it's a dependency and we don't want to remove dependencies
175+
if k == jobID || (slices.Contains(onlyJob.Needs, k) && !removeDependencies) {
175176
continue
176177
}
177178
delete(twf.BaseWorkflow.Jobs, k)
178179
}
180+
// Remove all dependencies from the only job left in the workflow, otherwise it won't run
181+
// because it depends on a job that has been removed.
182+
if removeDependencies {
183+
onlyJob.Needs = nil
184+
}
179185
}
180186
}
181187

@@ -209,6 +215,67 @@ func WithNoOpStep(t *testing.T, jobID, stepID string) TestingWorkflowOption {
209215
}
210216
}
211217

218+
// InjectedStepsOptions defines options for injecting steps into a job via WithInjectedSteps.
219+
type InjectedStepsOptions struct {
220+
// Position indicates whether to inject the new steps before or after the injection step.
221+
Position InjectedStepsOptionsPosition
222+
223+
// InjectionStepID is the ID of the step where the new steps will be injected.
224+
// Either InjectionStepID or InjectionStepIndex must be set, but not both.
225+
InjectionStepID string
226+
227+
// InjectionStepIndex is the index of the step where the new steps will be injected.
228+
// You can use 0 to inject before the first step.
229+
// You can use -1 to inject after the last step.
230+
// Otherwise, provide a valid step index to inject before/after that step, depending on Position.
231+
// Either InjectionStepID or InjectionStepIndex must be set, but not both.
232+
InjectionStepIndex int
233+
234+
// Steps are the steps to be injected.
235+
Steps Steps
236+
}
237+
238+
// InjectedStepsOptionsPosition indicates the position where the new steps will be injected.
239+
type InjectedStepsOptionsPosition int
240+
241+
const (
242+
// InjectedStepsOptionsPositionBefore indicates that the new steps will be injected before the injection step.
243+
InjectedStepsOptionsPositionBefore InjectedStepsOptionsPosition = iota
244+
245+
// InjectedStepsOptionsPositionAfter indicates that the new steps will be injected after the injection step.
246+
InjectedStepsOptionsPositionAfter
247+
)
248+
249+
// WithInjectedSteps injects the given steps into the given job at the specified position
250+
// relative to the step identified by InjectionStepID or InjectionStepIndex (see InjectedStepsOptions).
251+
// This can be used to add custom steps for testing purposes.
252+
func WithInjectedSteps(t *testing.T, jobID string, opts InjectedStepsOptions) TestingWorkflowOption {
253+
return func(twf *TestingWorkflow) {
254+
job, ok := twf.BaseWorkflow.Jobs[jobID]
255+
require.True(t, ok, fmt.Errorf("job %q not found", jobID))
256+
257+
var injectionStepIndex int
258+
if opts.InjectionStepID != "" {
259+
injectionStepIndex = job.getStepIndex(opts.InjectionStepID)
260+
require.GreaterOrEqual(t, injectionStepIndex, 0, "injection step with id %q not found", opts.InjectionStepID)
261+
} else {
262+
injectionStepIndex = opts.InjectionStepIndex
263+
require.GreaterOrEqual(t, injectionStepIndex, -1, "injection step index is < -1. it should be -1 (for injecting at the end) or a valid index.")
264+
if injectionStepIndex == -1 {
265+
injectionStepIndex = len(job.Steps) - 1
266+
}
267+
require.Less(t, injectionStepIndex, len(job.Steps), "injection step index %d out of bounds (steps length: %d)", injectionStepIndex, len(job.Steps))
268+
}
269+
270+
switch opts.Position {
271+
case InjectedStepsOptionsPositionBefore:
272+
job.Steps = append(job.Steps[:injectionStepIndex], append(opts.Steps, job.Steps[injectionStepIndex:]...)...)
273+
case InjectedStepsOptionsPositionAfter:
274+
job.Steps = append(job.Steps[:injectionStepIndex+1], append(opts.Steps, job.Steps[injectionStepIndex+1:]...)...)
275+
}
276+
}
277+
}
278+
212279
// WithMatrix sets the matrix for the given job.
213280
// This can be used to test workflows that use a dynamic matrix.
214281
// act doesn't support dynamic matrix values, so this is a workaround to set the matrix for the given job.

0 commit comments

Comments
 (0)