|
| 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