Skip to content

Commit 3de0393

Browse files
authored
test: add tests for workflows consistency (#568)
1 parent f1ad141 commit 3de0393

2 files changed

Lines changed: 243 additions & 1 deletion

File tree

.github/workflows/release-please-pr-update-tagged-references.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ jobs:
4747

4848
update-tagged-references:
4949
name: Update tagged references
50-
needs: automated-commit-check
50+
needs:
51+
- automated-commit-check
5152
permissions:
5253
contents: write
5354
pull-requests: write
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
10+
"github.com/grafana/plugin-ci-workflows/tests/act/internal/workflow"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
type workflowEntry struct {
15+
path string
16+
internal bool
17+
}
18+
19+
// knownWorkflows is the list of all workflow files in .github/workflows/.
20+
// Each entry is classified as public (used by plugin repos) or internal (used by this repo only).
21+
// If a new workflow is added to the repo, it must be added here or the tests will fail.
22+
var knownWorkflows = []workflowEntry{
23+
// Public workflows (used by plugin repos)
24+
{path: "ci.yml", internal: false},
25+
{path: "cd.yml", internal: false},
26+
{path: "playwright.yml", internal: false},
27+
{path: "playwright-docker.yml", internal: false},
28+
{path: "check-release-channel.yml", internal: false},
29+
30+
// Internal workflows (used by this repo only)
31+
{path: "pr-checks-examples-readmes.yml", internal: true},
32+
{path: "pr-checks-lint.yml", internal: true},
33+
{path: "pr-checks-semantic-title.yml", internal: true},
34+
{path: "pr-checks-test-ci.yml", internal: true},
35+
{path: "pr-checks-workflow-references.yml", internal: true},
36+
{path: "release-please-pr-update-tagged-references.yml", internal: true},
37+
{path: "release-please-restore-rolling-release.yml", internal: true},
38+
{path: "release-please.yml", internal: true},
39+
}
40+
41+
// ciOnlyInputs lists ci.yml inputs that are CI-only and should NOT be in cd.yml.
42+
var ciOnlyInputs = map[string]bool{
43+
"testing": true,
44+
}
45+
46+
func TestCDWorkflowContainsAllCIInputs(t *testing.T) {
47+
// Ensure that all inputs defined in ci.yml are also defined in cd.yml, so that CD can pass them through to CI.
48+
t.Parallel()
49+
50+
ciWf, err := workflow.NewBaseWorkflowFromFile(filepath.Join(".github", "workflows", "ci.yml"))
51+
require.NoError(t, err)
52+
cdWf, err := workflow.NewBaseWorkflowFromFile(filepath.Join(".github", "workflows", "cd.yml"))
53+
require.NoError(t, err)
54+
55+
ciInputs := ciWf.On.WorkflowCall.Inputs
56+
cdInputs := cdWf.On.WorkflowCall.Inputs
57+
58+
for inputName := range ciInputs {
59+
if ciOnlyInputs[inputName] {
60+
continue
61+
}
62+
require.Contains(t, cdInputs, inputName, "cd.yml should contain ci.yml input %q", inputName)
63+
}
64+
}
65+
66+
func TestCDWorkflowCIJobPassesAllInputs(t *testing.T) {
67+
// Ensure that CD's ci job passes all relevant inputs to CI, so that CI can function correctly when called from CD.
68+
t.Parallel()
69+
70+
ciWf, err := workflow.NewBaseWorkflowFromFile(filepath.Join(".github", "workflows", "ci.yml"))
71+
require.NoError(t, err)
72+
cdWf, err := workflow.NewBaseWorkflowFromFile(filepath.Join(".github", "workflows", "cd.yml"))
73+
require.NoError(t, err)
74+
75+
ciInputs := ciWf.On.WorkflowCall.Inputs
76+
77+
cdCIJob := cdWf.Jobs["ci"]
78+
require.NotNil(t, cdCIJob, "cd.yml should have a 'ci' job")
79+
cdCIJobWith := cdCIJob.With
80+
81+
// Forward: every CI input should be passed by CD's ci job
82+
t.Run("ci inputs are passed", func(t *testing.T) {
83+
t.Parallel()
84+
for inputName := range ciInputs {
85+
if ciOnlyInputs[inputName] {
86+
continue
87+
}
88+
require.Contains(t, cdCIJobWith, inputName, "cd.yml ci job should pass ci.yml input %q", inputName)
89+
}
90+
})
91+
92+
// Reverse: every key in CD's ci job 'with' should be a valid CI input
93+
t.Run("cd ci with keys are valid", func(t *testing.T) {
94+
for withKey := range cdCIJobWith {
95+
require.Contains(t, ciInputs, withKey, "cd.yml ci job passes unknown input %q to ci.yml", withKey)
96+
}
97+
})
98+
}
99+
100+
func TestExportedWorkflowsInSyncWithReleasePlease(t *testing.T) {
101+
t.Parallel()
102+
103+
knownSet := make(map[string]struct{}, len(knownWorkflows))
104+
for _, w := range knownWorkflows {
105+
knownSet[w.path] = struct{}{}
106+
}
107+
108+
var publicPaths []string
109+
for _, w := range knownWorkflows {
110+
if !w.internal {
111+
publicPaths = append(publicPaths, filepath.Join(".github", "workflows", w.path))
112+
}
113+
}
114+
115+
t.Run("all workflows are classified", func(t *testing.T) {
116+
t.Parallel()
117+
118+
files, err := filepath.Glob(filepath.Join(".github", "workflows", "*.yml"))
119+
require.NoError(t, err)
120+
121+
for _, f := range files {
122+
name := filepath.Base(f)
123+
// Skip temporary act workflow files generated by the test framework
124+
if strings.HasPrefix(name, "act-") {
125+
continue
126+
}
127+
require.Contains(t, knownSet, name, "workflow file %q is not classified as public or internal; please add it to knownWorkflows", name)
128+
}
129+
})
130+
131+
type switchRefSource struct {
132+
name string
133+
file string
134+
job string
135+
stepID string
136+
}
137+
138+
sources := []switchRefSource{
139+
{
140+
name: "pr-checks-workflow-references",
141+
file: "pr-checks-workflow-references.yml",
142+
job: "check-references",
143+
stepID: "switch-references",
144+
},
145+
{
146+
name: "release-please-pr-update-tagged-references",
147+
file: "release-please-pr-update-tagged-references.yml",
148+
job: "update-tagged-references",
149+
stepID: "switch-references",
150+
},
151+
{
152+
name: "release-please-restore-rolling-release",
153+
file: "release-please-restore-rolling-release.yml",
154+
job: "update-tagged-references",
155+
stepID: "switch-references",
156+
},
157+
}
158+
159+
for _, src := range sources {
160+
t.Run(src.name, func(t *testing.T) {
161+
t.Parallel()
162+
163+
wf, err := workflow.NewBaseWorkflowFromFile(filepath.Join(".github", "workflows", src.file))
164+
require.NoError(t, err)
165+
166+
job := wf.Jobs[src.job]
167+
require.NotNilf(t, job, "job %q not found in %s", src.job, src.file)
168+
169+
step := job.GetStep(src.stepID)
170+
require.NotNilf(t, step, "step %q not found in job %q of %s", src.stepID, src.job, src.file)
171+
172+
pathsRaw, ok := step.With["paths"]
173+
require.True(t, ok, "step %q in job %q of %s should have 'paths' in with", src.stepID, src.job, src.file)
174+
175+
pathsStr, ok := pathsRaw.(string)
176+
require.True(t, ok, "'paths' should be a string, got %T", pathsRaw)
177+
178+
var paths []string
179+
for _, line := range strings.Split(pathsStr, "\n") {
180+
trimmed := strings.TrimSpace(line)
181+
if trimmed != "" {
182+
paths = append(paths, trimmed)
183+
}
184+
}
185+
186+
require.Contains(t, paths, "actions/plugins/**", "%s should contain 'actions/plugins/**' in switch-references paths", src.file)
187+
for _, publicPath := range publicPaths {
188+
require.Contains(t, paths, publicPath, "%s switch-references paths should contain public workflow %q", src.file, publicPath)
189+
}
190+
})
191+
}
192+
}
193+
194+
func TestPluginActionsInReleasePleaseConfig(t *testing.T) {
195+
// Ensure that all public plugin actions (those not under actions/internal/) are registered
196+
// in release-please-config.json's "packages" section.
197+
t.Parallel()
198+
199+
// Discover all action directories under actions/ that are NOT under actions/internal/.
200+
// An action directory is one that contains action.yml or action.yaml.
201+
actionDirs, err := findPluginActionDirs("actions")
202+
require.NoError(t, err)
203+
require.NotEmpty(t, actionDirs, "should find at least one plugin action")
204+
205+
// Parse release-please-config.json to get the packages map.
206+
configBytes, err := os.ReadFile("release-please-config.json")
207+
require.NoError(t, err)
208+
209+
var config struct {
210+
Packages map[string]json.RawMessage `json:"packages"`
211+
}
212+
require.NoError(t, json.Unmarshal(configBytes, &config))
213+
214+
for _, actionDir := range actionDirs {
215+
require.Contains(t, config.Packages, actionDir, "action %q should be listed in release-please-config.json packages", actionDir)
216+
}
217+
}
218+
219+
// findPluginActionDirs walks the given root and returns directories that contain
220+
// an action.yml or action.yaml file, excluding anything under actions/internal/.
221+
func findPluginActionDirs(root string) ([]string, error) {
222+
var dirs []string
223+
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
224+
if err != nil {
225+
return err
226+
}
227+
// Skip the actions/internal subtree entirely.
228+
if info.IsDir() && path == filepath.Join("actions", "internal") {
229+
return filepath.SkipDir
230+
}
231+
if info.IsDir() {
232+
return nil
233+
}
234+
base := filepath.Base(path)
235+
if base == "action.yml" || base == "action.yaml" {
236+
dirs = append(dirs, filepath.Dir(path))
237+
}
238+
return nil
239+
})
240+
return dirs, err
241+
}

0 commit comments

Comments
 (0)