feat(executor): match the github-hosted Set up job log and name the job like GitHub
- commit
- e1538ebe3bbc3055531548394ae592a04267ac90
- author
- brice · · 2026-10-12 22:50 UTC
- parent
- 40f1da3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +250 -23
M go.mod +1 -1
| @@ -3,7 +3,7 @@ module bnema.dev/ermite-runner | |||
| 3 | 3 | go 1.27.1 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | - | bnema.dev/ermite-actions v0.0.0-20261012165742-b6c4c3a2f7ff | |
| 6 | + | bnema.dev/ermite-actions v0.0.0-20261012192445-fcc31accb5a4 | |
| 7 | 7 | bnema.dev/ermite-api v0.0.0-20261012155218-f8f64413e63c | |
| 8 | 8 | connectrpc.com/connect v1.21.0 | |
| 9 | 9 | github.com/bnema/zerowrap v1.4.1 | |
| @@ -3,7 +3,7 @@ module bnema.dev/ermite-runner | |||||
| 3 | go 1.27.1 | 3 | go 1.27.1 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | - | bnema.dev/ermite-actions v0.0.0-20261012165742-b6c4c3a2f7ff | 6 | + | bnema.dev/ermite-actions v0.0.0-20261012192445-fcc31accb5a4 |
| 7 | bnema.dev/ermite-api v0.0.0-20261012155218-f8f64413e63c | 7 | bnema.dev/ermite-api v0.0.0-20261012155218-f8f64413e63c | ||
| 8 | connectrpc.com/connect v1.21.0 | 8 | connectrpc.com/connect v1.21.0 | ||
| 9 | github.com/bnema/zerowrap v1.4.1 | 9 | github.com/bnema/zerowrap v1.4.1 | ||
M go.sum +2 -2 generated file: collapsed
M internal/e2e/chain_test.go +9 -6
| @@ -78,9 +78,13 @@ var ( | |||
| 78 | 78 | // Machine-dependent parts of the recordings, like the normalisation rules of | |
| 79 | 79 | // ermite-conformance: random script names and the repository name. | |
| 80 | 80 | repoRe = regexp.MustCompile(`/work/hello/hello`) | |
| 81 | + | // The runner version moves with each release (rule runner-version of | |
| 82 | + | // ermite-conformance). | |
| 83 | + | versionRe = regexp.MustCompile(`(Current runner version: ')[^']*(')`) | |
| 81 | 84 | ) | |
| 82 | 85 | ||
| 83 | 86 | func normalize(l string) string { | |
| 87 | + | l = versionRe.ReplaceAllString(l, "${1}<runner-version>${2}") | |
| 84 | 88 | l = repoRe.ReplaceAllString(l, "/work/<repo>/<repo>") | |
| 85 | 89 | return uuidRe.ReplaceAllString(l, "<uuid>") | |
| 86 | 90 | } | |
| @@ -215,8 +219,10 @@ func contains(l []string, s string) bool { | |||
| 215 | 219 | } | |
| 216 | 220 | ||
| 217 | 221 | // compareJob checks the steps, conclusions, outputs and log lines reported for | |
| 218 | - | // a job against the recording. "Set up job" (runner and image details) and the | |
| 219 | - | // ermite-dump step that the harness appends to every job are not compared. | |
| 222 | + | // a job against the recording. The ermite-dump step that the harness appends to | |
| 223 | + | // every job is not compared, and neither are the three groups of "Set up job" | |
| 224 | + | // that describe GitHub's fleet and image (ermite-conformance drops them from | |
| 225 | + | // the recordings, rule infra-groups): the other lines of "Set up job" are. | |
| 220 | 226 | func compareJob(t *testing.T, got hosttest.Result, want recJob, scenario string) { | |
| 221 | 227 | if got.Complete == nil { | |
| 222 | 228 | t.Fatal("the job did not complete") | |
| @@ -258,7 +264,7 @@ func compareJob(t *testing.T, got hosttest.Result, want recJob, scenario string) | |||
| 258 | 264 | type es = recStep | |
| 259 | 265 | var wantSteps, gotSteps []es | |
| 260 | 266 | for _, s := range want.Steps { | |
| 261 | - | if s.Name == "Set up job" || s.Name == "ermite-dump" { | |
| 267 | + | if s.Name == "ermite-dump" { | |
| 262 | 268 | continue | |
| 263 | 269 | } | |
| 264 | 270 | wantSteps = append(wantSteps, s) | |
| @@ -269,9 +275,6 @@ func compareJob(t *testing.T, got hosttest.Result, want recJob, scenario string) | |||
| 269 | 275 | } | |
| 270 | 276 | for _, n := range steps { | |
| 271 | 277 | u := got.Steps[n] | |
| 272 | - | if u.Name == "Set up job" { | |
| 273 | - | continue | |
| 274 | - | } | |
| 275 | 278 | if u.State != ermite.StepState_STEP_STATE_COMPLETED || u.StartedAt == nil || u.CompletedAt == nil { | |
| 276 | 279 | t.Errorf("step %d %q is not a complete snapshot: %v", n, u.Name, u) | |
| 277 | 280 | } | |
| @@ -78,9 +78,13 @@ var ( | |||||
| 78 | // Machine-dependent parts of the recordings, like the normalisation rules of | 78 | // Machine-dependent parts of the recordings, like the normalisation rules of | ||
| 79 | // ermite-conformance: random script names and the repository name. | 79 | // ermite-conformance: random script names and the repository name. | ||
| 80 | repoRe = regexp.MustCompile(`/work/hello/hello`) | 80 | repoRe = regexp.MustCompile(`/work/hello/hello`) | ||
| 81 | + | // The runner version moves with each release (rule runner-version of | |||
| 82 | + | // ermite-conformance). | |||
| 83 | + | versionRe = regexp.MustCompile(`(Current runner version: ')[^']*(')`) | |||
| 81 | ) | 84 | ) | ||
| 82 | 85 | ||||
| 83 | func normalize(l string) string { | 86 | func normalize(l string) string { | ||
| 87 | + | l = versionRe.ReplaceAllString(l, "${1}<runner-version>${2}") | |||
| 84 | l = repoRe.ReplaceAllString(l, "/work/<repo>/<repo>") | 88 | l = repoRe.ReplaceAllString(l, "/work/<repo>/<repo>") | ||
| 85 | return uuidRe.ReplaceAllString(l, "<uuid>") | 89 | return uuidRe.ReplaceAllString(l, "<uuid>") | ||
| 86 | } | 90 | } | ||
| @@ -215,8 +219,10 @@ func contains(l []string, s string) bool { | |||||
| 215 | } | 219 | } | ||
| 216 | 220 | ||||
| 217 | // compareJob checks the steps, conclusions, outputs and log lines reported for | 221 | // compareJob checks the steps, conclusions, outputs and log lines reported for | ||
| 218 | - | // a job against the recording. "Set up job" (runner and image details) and the | 222 | + | // a job against the recording. The ermite-dump step that the harness appends to |
| 219 | - | // ermite-dump step that the harness appends to every job are not compared. | 223 | + | // every job is not compared, and neither are the three groups of "Set up job" |
| 224 | + | // that describe GitHub's fleet and image (ermite-conformance drops them from | |||
| 225 | + | // the recordings, rule infra-groups): the other lines of "Set up job" are. | |||
| 220 | func compareJob(t *testing.T, got hosttest.Result, want recJob, scenario string) { | 226 | func compareJob(t *testing.T, got hosttest.Result, want recJob, scenario string) { | ||
| 221 | if got.Complete == nil { | 227 | if got.Complete == nil { | ||
| 222 | t.Fatal("the job did not complete") | 228 | t.Fatal("the job did not complete") | ||
| @@ -258,7 +264,7 @@ func compareJob(t *testing.T, got hosttest.Result, want recJob, scenario string) | |||||
| 258 | type es = recStep | 264 | type es = recStep | ||
| 259 | var wantSteps, gotSteps []es | 265 | var wantSteps, gotSteps []es | ||
| 260 | for _, s := range want.Steps { | 266 | for _, s := range want.Steps { | ||
| 261 | - | if s.Name == "Set up job" || s.Name == "ermite-dump" { | 267 | + | if s.Name == "ermite-dump" { |
| 262 | continue | 268 | continue | ||
| 263 | } | 269 | } | ||
| 264 | wantSteps = append(wantSteps, s) | 270 | wantSteps = append(wantSteps, s) | ||
| @@ -269,9 +275,6 @@ func compareJob(t *testing.T, got hosttest.Result, want recJob, scenario string) | |||||
| 269 | } | 275 | } | ||
| 270 | for _, n := range steps { | 276 | for _, n := range steps { | ||
| 271 | u := got.Steps[n] | 277 | u := got.Steps[n] | ||
| 272 | - | if u.Name == "Set up job" { | |||
| 273 | - | continue | |||
| 274 | - | } | |||
| 275 | if u.State != ermite.StepState_STEP_STATE_COMPLETED || u.StartedAt == nil || u.CompletedAt == nil { | 278 | if u.State != ermite.StepState_STEP_STATE_COMPLETED || u.StartedAt == nil || u.CompletedAt == nil { | ||
| 276 | t.Errorf("step %d %q is not a complete snapshot: %v", n, u.Name, u) | 279 | t.Errorf("step %d %q is not a complete snapshot: %v", n, u.Name, u) | ||
| 277 | } | 280 | } | ||
M internal/executor/contexts.go +5 -0
| @@ -127,6 +127,11 @@ type RunnerInfo struct { | |||
| 127 | 127 | Arch string | |
| 128 | 128 | } | |
| 129 | 129 | ||
| 130 | + | // githubHosted reports whether the runner.environment is "github-hosted". An | |
| 131 | + | // empty Environment is "self-hosted", the default of the official runner's | |
| 132 | + | // runner context. | |
| 133 | + | func (r RunnerInfo) githubHosted() bool { return r.Environment == "github-hosted" } | |
| 134 | + | ||
| 130 | 135 | func (r RunnerInfo) arch() string { | |
| 131 | 136 | if r.Arch != "" { | |
| 132 | 137 | return r.Arch | |
| @@ -127,6 +127,11 @@ type RunnerInfo struct { | |||||
| 127 | Arch string | 127 | Arch string | ||
| 128 | } | 128 | } | ||
| 129 | 129 | ||||
| 130 | + | // githubHosted reports whether the runner.environment is "github-hosted". An | |||
| 131 | + | // empty Environment is "self-hosted", the default of the official runner's | |||
| 132 | + | // runner context. | |||
| 133 | + | func (r RunnerInfo) githubHosted() bool { return r.Environment == "github-hosted" } | |||
| 134 | + | ||||
| 130 | func (r RunnerInfo) arch() string { | 135 | func (r RunnerInfo) arch() string { | ||
| 131 | if r.Arch != "" { | 136 | if r.Arch != "" { | ||
| 132 | return r.Arch | 137 | return r.Arch | ||
M internal/executor/corpus_test.go +6 -7
| @@ -181,6 +181,7 @@ var ( | |||
| 181 | 181 | shellPathRe = regexp.MustCompile(`^shell: \S*/(\w+) `) | |
| 182 | 182 | startPathRe = regexp.MustCompile(`process '\S*/(\w+)'`) | |
| 183 | 183 | uuidRe = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) | |
| 184 | + | versionRe = regexp.MustCompile(`(Current runner version: ')[^']*(')`) | |
| 184 | 185 | ) | |
| 185 | 186 | ||
| 186 | 187 | // normalizeLine maps the machine-dependent parts of a line to the form of the | |
| @@ -190,6 +191,7 @@ var ( | |||
| 190 | 191 | func normalizeLine(l, home string) string { | |
| 191 | 192 | l = shellPathRe.ReplaceAllString(l, "shell: /usr/bin/$1 ") | |
| 192 | 193 | l = startPathRe.ReplaceAllString(l, "process '/usr/bin/$1'") | |
| 194 | + | l = versionRe.ReplaceAllString(l, "${1}<runner-version>${2}") | |
| 193 | 195 | l = strings.ReplaceAll(l, home, "/home/runner") | |
| 194 | 196 | l = strings.ReplaceAll(l, "/work/hello/hello", "/work/<repo>/<repo>") | |
| 195 | 197 | return uuidRe.ReplaceAllString(l, "<uuid>") | |
| @@ -204,9 +206,9 @@ func shIsBash() bool { | |||
| 204 | 206 | } | |
| 205 | 207 | ||
| 206 | 208 | // compareWithRecording runs every job without needs of a scenario and checks | |
| 207 | - | // that the steps, conclusions and logs equal the recording of github.com. Set | |
| 208 | - | // up job (runner and image details) and the dump step the harness appends are | |
| 209 | - | // not compared. | |
| 209 | + | // that the steps, conclusions and logs equal the recording of github.com. The | |
| 210 | + | // dump step the harness appends is not compared (the recordings have no fleet | |
| 211 | + | // and image groups in Set up job, rule infra-groups of ermite-conformance). | |
| 210 | 212 | func compareWithRecording(t *testing.T, scenario string, skipJobs ...string) { | |
| 211 | 213 | t.Helper() | |
| 212 | 214 | needTools(t, "bash", "sh") | |
| @@ -243,16 +245,13 @@ func compareWithRecording(t *testing.T, scenario string, skipJobs ...string) { | |||
| 243 | 245 | } | |
| 244 | 246 | var want []es | |
| 245 | 247 | for _, s := range jr.Steps { | |
| 246 | - | if s.Name == SetUpJobName || s.Name == "ermite-dump" { | |
| 248 | + | if s.Name == "ermite-dump" { | |
| 247 | 249 | continue | |
| 248 | 250 | } | |
| 249 | 251 | want = append(want, es{s.Name, s.Conclusion, s.Log}) | |
| 250 | 252 | } | |
| 251 | 253 | var have []es | |
| 252 | 254 | for _, s := range got.steps { | |
| 253 | - | if s.Name == SetUpJobName { | |
| 254 | - | continue | |
| 255 | - | } | |
| 256 | 255 | var log []string | |
| 257 | 256 | for _, l := range s.Log { | |
| 258 | 257 | log = append(log, normalizeLine(l, home)) | |
| @@ -181,6 +181,7 @@ var ( | |||||
| 181 | shellPathRe = regexp.MustCompile(`^shell: \S*/(\w+) `) | 181 | shellPathRe = regexp.MustCompile(`^shell: \S*/(\w+) `) | ||
| 182 | startPathRe = regexp.MustCompile(`process '\S*/(\w+)'`) | 182 | startPathRe = regexp.MustCompile(`process '\S*/(\w+)'`) | ||
| 183 | uuidRe = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) | 183 | uuidRe = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) | ||
| 184 | + | versionRe = regexp.MustCompile(`(Current runner version: ')[^']*(')`) | |||
| 184 | ) | 185 | ) | ||
| 185 | 186 | ||||
| 186 | // normalizeLine maps the machine-dependent parts of a line to the form of the | 187 | // normalizeLine maps the machine-dependent parts of a line to the form of the | ||
| @@ -190,6 +191,7 @@ var ( | |||||
| 190 | func normalizeLine(l, home string) string { | 191 | func normalizeLine(l, home string) string { | ||
| 191 | l = shellPathRe.ReplaceAllString(l, "shell: /usr/bin/$1 ") | 192 | l = shellPathRe.ReplaceAllString(l, "shell: /usr/bin/$1 ") | ||
| 192 | l = startPathRe.ReplaceAllString(l, "process '/usr/bin/$1'") | 193 | l = startPathRe.ReplaceAllString(l, "process '/usr/bin/$1'") | ||
| 194 | + | l = versionRe.ReplaceAllString(l, "${1}<runner-version>${2}") | |||
| 193 | l = strings.ReplaceAll(l, home, "/home/runner") | 195 | l = strings.ReplaceAll(l, home, "/home/runner") | ||
| 194 | l = strings.ReplaceAll(l, "/work/hello/hello", "/work/<repo>/<repo>") | 196 | l = strings.ReplaceAll(l, "/work/hello/hello", "/work/<repo>/<repo>") | ||
| 195 | return uuidRe.ReplaceAllString(l, "<uuid>") | 197 | return uuidRe.ReplaceAllString(l, "<uuid>") | ||
| @@ -204,9 +206,9 @@ func shIsBash() bool { | |||||
| 204 | } | 206 | } | ||
| 205 | 207 | ||||
| 206 | // compareWithRecording runs every job without needs of a scenario and checks | 208 | // compareWithRecording runs every job without needs of a scenario and checks | ||
| 207 | - | // that the steps, conclusions and logs equal the recording of github.com. Set | 209 | + | // that the steps, conclusions and logs equal the recording of github.com. The |
| 208 | - | // up job (runner and image details) and the dump step the harness appends are | 210 | + | // dump step the harness appends is not compared (the recordings have no fleet |
| 209 | - | // not compared. | 211 | + | // and image groups in Set up job, rule infra-groups of ermite-conformance). |
| 210 | func compareWithRecording(t *testing.T, scenario string, skipJobs ...string) { | 212 | func compareWithRecording(t *testing.T, scenario string, skipJobs ...string) { | ||
| 211 | t.Helper() | 213 | t.Helper() | ||
| 212 | needTools(t, "bash", "sh") | 214 | needTools(t, "bash", "sh") | ||
| @@ -243,16 +245,13 @@ func compareWithRecording(t *testing.T, scenario string, skipJobs ...string) { | |||||
| 243 | } | 245 | } | ||
| 244 | var want []es | 246 | var want []es | ||
| 245 | for _, s := range jr.Steps { | 247 | for _, s := range jr.Steps { | ||
| 246 | - | if s.Name == SetUpJobName || s.Name == "ermite-dump" { | 248 | + | if s.Name == "ermite-dump" { |
| 247 | continue | 249 | continue | ||
| 248 | } | 250 | } | ||
| 249 | want = append(want, es{s.Name, s.Conclusion, s.Log}) | 251 | want = append(want, es{s.Name, s.Conclusion, s.Log}) | ||
| 250 | } | 252 | } | ||
| 251 | var have []es | 253 | var have []es | ||
| 252 | for _, s := range got.steps { | 254 | for _, s := range got.steps { | ||
| 253 | - | if s.Name == SetUpJobName { | |||
| 254 | - | continue | |||
| 255 | - | } | |||
| 256 | var log []string | 255 | var log []string | ||
| 257 | for _, l := range s.Log { | 256 | for _, l := range s.Log { | ||
| 258 | log = append(log, normalizeLine(l, home)) | 257 | log = append(log, normalizeLine(l, home)) | ||
M internal/executor/job.go +120 -6
| @@ -324,11 +324,17 @@ func (j *job) evalMap(m *template.Mapping, env *ordered) ([]Pair, error) { | |||
| 324 | 324 | func (j *job) setUp() ([]*workflow.Step, error) { | |
| 325 | 325 | cfg := j.cfg | |
| 326 | 326 | j.out(1, fmt.Sprintf("Current runner version: '%s'", cfg.RunnerVersion)) | |
| 327 | - | if cfg.Runner.Name != "" { | |
| 328 | - | j.out(1, fmt.Sprintf("Runner name: '%s'", cfg.Runner.Name)) | |
| 329 | - | } | |
| 330 | - | if host, err := os.Hostname(); err == nil { | |
| 331 | - | j.out(1, fmt.Sprintf("Machine name: '%s'", host)) | |
| 327 | + | // Only a self-hosted runner prints its name and machine (JobExtension.cs: | |
| 328 | + | // 88-98: the runner is identified by its OAuth client id); the recordings | |
| 329 | + | // of github-hosted runners have neither line. The runner group name | |
| 330 | + | // (system.runnerGroupName) is not known to ermite. | |
| 331 | + | if !cfg.Runner.githubHosted() { | |
| 332 | + | if cfg.Runner.Name != "" { | |
| 333 | + | j.out(1, fmt.Sprintf("Runner name: '%s'", cfg.Runner.Name)) | |
| 334 | + | } | |
| 335 | + | if host, err := os.Hostname(); err == nil { | |
| 336 | + | j.out(1, fmt.Sprintf("Machine name: '%s'", host)) | |
| 337 | + | } | |
| 332 | 338 | } | |
| 333 | 339 | ||
| 334 | 340 | wf, errs := workflow.Parse(cfg.WorkflowPath, cfg.WorkflowYAML) | |
| @@ -373,6 +379,8 @@ func (j *job) setUp() ([]*workflow.Step, error) { | |||
| 373 | 379 | return nil, errors.New("not supported yet: jobs.<id>.services") | |
| 374 | 380 | } | |
| 375 | 381 | ||
| 382 | + | j.printTokenInfo() | |
| 383 | + | ||
| 376 | 384 | // Directories (TempDirectoryManager, PipelineDirectoryManager). | |
| 377 | 385 | j.out(1, "Prepare workflow directory") | |
| 378 | 386 | for _, d := range []string{layout.Temp, layout.Workspace, layout.ToolCache} { | |
| @@ -439,9 +447,12 @@ func (j *job) setUp() ([]*workflow.Step, error) { | |||
| 439 | 447 | } | |
| 440 | 448 | ||
| 441 | 449 | j.out(1, "Prepare all required actions") | |
| 450 | + | // JobExtension.cs:418-422: the display name of the job, which the server | |
| 451 | + | // computes there (message.JobDisplayName). ermite's Job message has no such | |
| 452 | + | // field, so the executor evaluates it (see jobDisplayName). | |
| 442 | 453 | name := cfg.JobName | |
| 443 | 454 | if name == "" { | |
| 444 | - | name = cfg.JobKey | |
| 455 | + | name = j.jobDisplayName() | |
| 445 | 456 | } | |
| 446 | 457 | if strings.TrimSpace(name) != "" { | |
| 447 | 458 | j.out(1, "Complete job name: "+name) | |
| @@ -552,3 +563,106 @@ func (j *job) completeJob(num int) map[string]string { | |||
| 552 | 563 | rep.StepCompleted(num, CompleteJobName, result.conclusion(), j.now()) | |
| 553 | 564 | return outputs | |
| 554 | 565 | } | |
| 566 | + | ||
| 567 | + | // Defaults of the lines "Set up job" prints between the runner version and | |
| 568 | + | // "Prepare workflow directory" (JobExtension.cs:150-182). The server gives | |
| 569 | + | // GitHub's runner these as the variables system.github.token.permissions and | |
| 570 | + | // actions_cache_mode and the github context key secret_source; ermite's Job | |
| 571 | + | // message carries none of them, so the values recorded on github.com for a | |
| 572 | + | // push to a public repository are used. Every one of the 221 recorded jobs | |
| 573 | + | // has exactly these lines. | |
| 574 | + | const ( | |
| 575 | + | defaultSecretSource = "Actions" | |
| 576 | + | defaultCacheMode = "write" | |
| 577 | + | ) | |
| 578 | + | ||
| 579 | + | // defaultTokenPermissions is the GITHUB_TOKEN of a workflow without a | |
| 580 | + | // "permissions" key, as recorded (JobExtension.cs:157-166 prints the | |
| 581 | + | // permissions in the order of the server's JSON). | |
| 582 | + | var defaultTokenPermissions = [][2]string{{"Contents", "read"}, {"Metadata", "read"}, {"Packages", "read"}} | |
| 583 | + | ||
| 584 | + | // printTokenInfo prints the GITHUB_TOKEN permissions group, the secret source | |
| 585 | + | // and the cache mode. A workflow or job that sets "permissions" gets no group: | |
| 586 | + | // the effective permissions depend on what the server grants, which the runner | |
| 587 | + | // cannot know (it would print the default, which is wrong for such a workflow; | |
| 588 | + | // GitHub's runner prints nothing when the server sends none, JobExtension.cs: | |
| 589 | + | // 153). To print the real ones, runner.proto's Job would need the effective | |
| 590 | + | // GITHUB_TOKEN permissions (scope and level, in display order). | |
| 591 | + | func (j *job) printTokenInfo() { | |
| 592 | + | if j.wf.Permissions == nil && j.jb.Permissions == nil { | |
| 593 | + | j.out(1, "##[group]GITHUB_TOKEN Permissions") | |
| 594 | + | for _, p := range defaultTokenPermissions { | |
| 595 | + | j.out(1, p[0]+": "+p[1]) | |
| 596 | + | } | |
| 597 | + | j.out(1, "##[endgroup]") | |
| 598 | + | } | |
| 599 | + | // secret_source is a github context key (JobExtension.cs:172); the context | |
| 600 | + | // of a job from ermite lacks it, so it is added with the recorded value. | |
| 601 | + | src := j.state.GitHub("secret_source") | |
| 602 | + | if src == "" { | |
| 603 | + | src = defaultSecretSource | |
| 604 | + | j.state.SetGitHub("secret_source", src) | |
| 605 | + | } | |
| 606 | + | j.out(1, "Secret source: "+src) | |
| 607 | + | j.out(1, "Cache mode: "+defaultCacheMode) | |
| 608 | + | } | |
| 609 | + | ||
| 610 | + | // jobDisplayName is the name of the job as GitHub shows it and the runner | |
| 611 | + | // prints it after "Complete job name:": the "name" of the job (the job key when | |
| 612 | + | // it has none) evaluated with github, needs, strategy, matrix, vars and inputs | |
| 613 | + | // (the contexts the schema allows there), followed by the matrix values in | |
| 614 | + | // parentheses unless the name reads the matrix (recorded: matrix-job-name, see | |
| 615 | + | // workflow.Job.DisplayName). The combination of this job is the one at | |
| 616 | + | // strategy.job-index, else the one whose values equal the matrix. On any | |
| 617 | + | // failure the job key is used: the name is not worth failing the job for. | |
| 618 | + | func (j *job) jobDisplayName() string { | |
| 619 | + | ctx := context.Background() | |
| 620 | + | all := j.evalEnv(newOrdered()) | |
| 621 | + | env := all | |
| 622 | + | env.Contexts = map[string]expr.Value{} | |
| 623 | + | for _, k := range []string{"github", "needs", "strategy", "matrix", "vars", "inputs"} { | |
| 624 | + | if v, ok := all.Contexts[k]; ok { | |
| 625 | + | env.Contexts[k] = v | |
| 626 | + | } | |
| 627 | + | } | |
| 628 | + | // The strategy itself sees neither the strategy nor the matrix context. | |
| 629 | + | senv := env | |
| 630 | + | senv.Contexts = map[string]expr.Value{} | |
| 631 | + | for _, k := range []string{"github", "needs", "vars", "inputs"} { | |
| 632 | + | if v, ok := env.Contexts[k]; ok { | |
| 633 | + | senv.Contexts[k] = v | |
| 634 | + | } | |
| 635 | + | } | |
| 636 | + | st, err := j.jb.EvaluateStrategy(ctx, senv) | |
| 637 | + | if err != nil { | |
| 638 | + | return j.cfg.JobKey | |
| 639 | + | } | |
| 640 | + | idx := 0 | |
| 641 | + | if st.HasMatrix { | |
| 642 | + | idx = -1 | |
| 643 | + | if d, ok := env.Contexts["strategy"].(*expr.Dictionary); ok { | |
| 644 | + | if n, ok := d.Get("job-index"); ok { | |
| 645 | + | if f, ok := n.(expr.Number); ok && int(f) >= 0 && int(f) < len(st.Combos) { | |
| 646 | + | idx = int(f) | |
| 647 | + | } | |
| 648 | + | } | |
| 649 | + | } | |
| 650 | + | if idx < 0 { | |
| 651 | + | want := expr.ToJSON(env.Contexts["matrix"]) | |
| 652 | + | for i, c := range st.Combos { | |
| 653 | + | if expr.ToJSON(c.Values) == want { | |
| 654 | + | idx = i | |
| 655 | + | break | |
| 656 | + | } | |
| 657 | + | } | |
| 658 | + | } | |
| 659 | + | if idx < 0 { | |
| 660 | + | return j.cfg.JobKey | |
| 661 | + | } | |
| 662 | + | } | |
| 663 | + | name, err := j.jb.DisplayName(ctx, env, st, idx) | |
| 664 | + | if err != nil { | |
| 665 | + | return j.cfg.JobKey | |
| 666 | + | } | |
| 667 | + | return name | |
| 668 | + | } | |
| @@ -324,11 +324,17 @@ func (j *job) evalMap(m *template.Mapping, env *ordered) ([]Pair, error) { | |||||
| 324 | func (j *job) setUp() ([]*workflow.Step, error) { | 324 | func (j *job) setUp() ([]*workflow.Step, error) { | ||
| 325 | cfg := j.cfg | 325 | cfg := j.cfg | ||
| 326 | j.out(1, fmt.Sprintf("Current runner version: '%s'", cfg.RunnerVersion)) | 326 | j.out(1, fmt.Sprintf("Current runner version: '%s'", cfg.RunnerVersion)) | ||
| 327 | - | if cfg.Runner.Name != "" { | 327 | + | // Only a self-hosted runner prints its name and machine (JobExtension.cs: |
| 328 | - | j.out(1, fmt.Sprintf("Runner name: '%s'", cfg.Runner.Name)) | 328 | + | // 88-98: the runner is identified by its OAuth client id); the recordings |
| 329 | - | } | 329 | + | // of github-hosted runners have neither line. The runner group name |
| 330 | - | if host, err := os.Hostname(); err == nil { | 330 | + | // (system.runnerGroupName) is not known to ermite. |
| 331 | - | j.out(1, fmt.Sprintf("Machine name: '%s'", host)) | 331 | + | if !cfg.Runner.githubHosted() { |
| 332 | + | if cfg.Runner.Name != "" { | |||
| 333 | + | j.out(1, fmt.Sprintf("Runner name: '%s'", cfg.Runner.Name)) | |||
| 334 | + | } | |||
| 335 | + | if host, err := os.Hostname(); err == nil { | |||
| 336 | + | j.out(1, fmt.Sprintf("Machine name: '%s'", host)) | |||
| 337 | + | } | |||
| 332 | } | 338 | } | ||
| 333 | 339 | ||||
| 334 | wf, errs := workflow.Parse(cfg.WorkflowPath, cfg.WorkflowYAML) | 340 | wf, errs := workflow.Parse(cfg.WorkflowPath, cfg.WorkflowYAML) | ||
| @@ -373,6 +379,8 @@ func (j *job) setUp() ([]*workflow.Step, error) { | |||||
| 373 | return nil, errors.New("not supported yet: jobs.<id>.services") | 379 | return nil, errors.New("not supported yet: jobs.<id>.services") | ||
| 374 | } | 380 | } | ||
| 375 | 381 | ||||
| 382 | + | j.printTokenInfo() | |||
| 383 | + | ||||
| 376 | // Directories (TempDirectoryManager, PipelineDirectoryManager). | 384 | // Directories (TempDirectoryManager, PipelineDirectoryManager). | ||
| 377 | j.out(1, "Prepare workflow directory") | 385 | j.out(1, "Prepare workflow directory") | ||
| 378 | for _, d := range []string{layout.Temp, layout.Workspace, layout.ToolCache} { | 386 | for _, d := range []string{layout.Temp, layout.Workspace, layout.ToolCache} { | ||
| @@ -439,9 +447,12 @@ func (j *job) setUp() ([]*workflow.Step, error) { | |||||
| 439 | } | 447 | } | ||
| 440 | 448 | ||||
| 441 | j.out(1, "Prepare all required actions") | 449 | j.out(1, "Prepare all required actions") | ||
| 450 | + | // JobExtension.cs:418-422: the display name of the job, which the server | |||
| 451 | + | // computes there (message.JobDisplayName). ermite's Job message has no such | |||
| 452 | + | // field, so the executor evaluates it (see jobDisplayName). | |||
| 442 | name := cfg.JobName | 453 | name := cfg.JobName | ||
| 443 | if name == "" { | 454 | if name == "" { | ||
| 444 | - | name = cfg.JobKey | 455 | + | name = j.jobDisplayName() |
| 445 | } | 456 | } | ||
| 446 | if strings.TrimSpace(name) != "" { | 457 | if strings.TrimSpace(name) != "" { | ||
| 447 | j.out(1, "Complete job name: "+name) | 458 | j.out(1, "Complete job name: "+name) | ||
| @@ -552,3 +563,106 @@ func (j *job) completeJob(num int) map[string]string { | |||||
| 552 | rep.StepCompleted(num, CompleteJobName, result.conclusion(), j.now()) | 563 | rep.StepCompleted(num, CompleteJobName, result.conclusion(), j.now()) | ||
| 553 | return outputs | 564 | return outputs | ||
| 554 | } | 565 | } | ||
| 566 | + | ||||
| 567 | + | // Defaults of the lines "Set up job" prints between the runner version and | |||
| 568 | + | // "Prepare workflow directory" (JobExtension.cs:150-182). The server gives | |||
| 569 | + | // GitHub's runner these as the variables system.github.token.permissions and | |||
| 570 | + | // actions_cache_mode and the github context key secret_source; ermite's Job | |||
| 571 | + | // message carries none of them, so the values recorded on github.com for a | |||
| 572 | + | // push to a public repository are used. Every one of the 221 recorded jobs | |||
| 573 | + | // has exactly these lines. | |||
| 574 | + | const ( | |||
| 575 | + | defaultSecretSource = "Actions" | |||
| 576 | + | defaultCacheMode = "write" | |||
| 577 | + | ) | |||
| 578 | + | ||||
| 579 | + | // defaultTokenPermissions is the GITHUB_TOKEN of a workflow without a | |||
| 580 | + | // "permissions" key, as recorded (JobExtension.cs:157-166 prints the | |||
| 581 | + | // permissions in the order of the server's JSON). | |||
| 582 | + | var defaultTokenPermissions = [][2]string{{"Contents", "read"}, {"Metadata", "read"}, {"Packages", "read"}} | |||
| 583 | + | ||||
| 584 | + | // printTokenInfo prints the GITHUB_TOKEN permissions group, the secret source | |||
| 585 | + | // and the cache mode. A workflow or job that sets "permissions" gets no group: | |||
| 586 | + | // the effective permissions depend on what the server grants, which the runner | |||
| 587 | + | // cannot know (it would print the default, which is wrong for such a workflow; | |||
| 588 | + | // GitHub's runner prints nothing when the server sends none, JobExtension.cs: | |||
| 589 | + | // 153). To print the real ones, runner.proto's Job would need the effective | |||
| 590 | + | // GITHUB_TOKEN permissions (scope and level, in display order). | |||
| 591 | + | func (j *job) printTokenInfo() { | |||
| 592 | + | if j.wf.Permissions == nil && j.jb.Permissions == nil { | |||
| 593 | + | j.out(1, "##[group]GITHUB_TOKEN Permissions") | |||
| 594 | + | for _, p := range defaultTokenPermissions { | |||
| 595 | + | j.out(1, p[0]+": "+p[1]) | |||
| 596 | + | } | |||
| 597 | + | j.out(1, "##[endgroup]") | |||
| 598 | + | } | |||
| 599 | + | // secret_source is a github context key (JobExtension.cs:172); the context | |||
| 600 | + | // of a job from ermite lacks it, so it is added with the recorded value. | |||
| 601 | + | src := j.state.GitHub("secret_source") | |||
| 602 | + | if src == "" { | |||
| 603 | + | src = defaultSecretSource | |||
| 604 | + | j.state.SetGitHub("secret_source", src) | |||
| 605 | + | } | |||
| 606 | + | j.out(1, "Secret source: "+src) | |||
| 607 | + | j.out(1, "Cache mode: "+defaultCacheMode) | |||
| 608 | + | } | |||
| 609 | + | ||||
| 610 | + | // jobDisplayName is the name of the job as GitHub shows it and the runner | |||
| 611 | + | // prints it after "Complete job name:": the "name" of the job (the job key when | |||
| 612 | + | // it has none) evaluated with github, needs, strategy, matrix, vars and inputs | |||
| 613 | + | // (the contexts the schema allows there), followed by the matrix values in | |||
| 614 | + | // parentheses unless the name reads the matrix (recorded: matrix-job-name, see | |||
| 615 | + | // workflow.Job.DisplayName). The combination of this job is the one at | |||
| 616 | + | // strategy.job-index, else the one whose values equal the matrix. On any | |||
| 617 | + | // failure the job key is used: the name is not worth failing the job for. | |||
| 618 | + | func (j *job) jobDisplayName() string { | |||
| 619 | + | ctx := context.Background() | |||
| 620 | + | all := j.evalEnv(newOrdered()) | |||
| 621 | + | env := all | |||
| 622 | + | env.Contexts = map[string]expr.Value{} | |||
| 623 | + | for _, k := range []string{"github", "needs", "strategy", "matrix", "vars", "inputs"} { | |||
| 624 | + | if v, ok := all.Contexts[k]; ok { | |||
| 625 | + | env.Contexts[k] = v | |||
| 626 | + | } | |||
| 627 | + | } | |||
| 628 | + | // The strategy itself sees neither the strategy nor the matrix context. | |||
| 629 | + | senv := env | |||
| 630 | + | senv.Contexts = map[string]expr.Value{} | |||
| 631 | + | for _, k := range []string{"github", "needs", "vars", "inputs"} { | |||
| 632 | + | if v, ok := env.Contexts[k]; ok { | |||
| 633 | + | senv.Contexts[k] = v | |||
| 634 | + | } | |||
| 635 | + | } | |||
| 636 | + | st, err := j.jb.EvaluateStrategy(ctx, senv) | |||
| 637 | + | if err != nil { | |||
| 638 | + | return j.cfg.JobKey | |||
| 639 | + | } | |||
| 640 | + | idx := 0 | |||
| 641 | + | if st.HasMatrix { | |||
| 642 | + | idx = -1 | |||
| 643 | + | if d, ok := env.Contexts["strategy"].(*expr.Dictionary); ok { | |||
| 644 | + | if n, ok := d.Get("job-index"); ok { | |||
| 645 | + | if f, ok := n.(expr.Number); ok && int(f) >= 0 && int(f) < len(st.Combos) { | |||
| 646 | + | idx = int(f) | |||
| 647 | + | } | |||
| 648 | + | } | |||
| 649 | + | } | |||
| 650 | + | if idx < 0 { | |||
| 651 | + | want := expr.ToJSON(env.Contexts["matrix"]) | |||
| 652 | + | for i, c := range st.Combos { | |||
| 653 | + | if expr.ToJSON(c.Values) == want { | |||
| 654 | + | idx = i | |||
| 655 | + | break | |||
| 656 | + | } | |||
| 657 | + | } | |||
| 658 | + | } | |||
| 659 | + | if idx < 0 { | |||
| 660 | + | return j.cfg.JobKey | |||
| 661 | + | } | |||
| 662 | + | } | |||
| 663 | + | name, err := j.jb.DisplayName(ctx, env, st, idx) | |||
| 664 | + | if err != nil { | |||
| 665 | + | return j.cfg.JobKey | |||
| 666 | + | } | |||
| 667 | + | return name | |||
| 668 | + | } | |||
M internal/executor/job_test.go +107 -1
| @@ -31,7 +31,7 @@ func TestImplicitStepsAndNumbering(t *testing.T) { | |||
| 31 | 31 | } | |
| 32 | 32 | } | |
| 33 | 33 | setup := logOf(rec.steps[0]) | |
| 34 | - | for _, w := range []string{"Current runner version: 'test'", "Runner name: 'test-runner'", "Prepare workflow directory", "Prepare all required actions", "Complete job name: j"} { | |
| 34 | + | for _, w := range []string{"Current runner version: 'test'", "Prepare workflow directory", "Prepare all required actions", "Complete job name: j"} { | |
| 35 | 35 | if !strings.Contains(setup, w) { | |
| 36 | 36 | t.Errorf("Set up job lacks %q:\n%s", w, setup) | |
| 37 | 37 | } | |
| @@ -664,3 +664,109 @@ func TestMatrixContextWithoutMatrixIsNull(t *testing.T) { | |||
| 664 | 664 | t.Errorf("%q", got) | |
| 665 | 665 | } | |
| 666 | 666 | } | |
| 667 | + | ||
| 668 | + | func setupLog(t *testing.T, yaml, job string, tweak func(*Config)) string { | |
| 669 | + | t.Helper() | |
| 670 | + | rec, res := runLocal(t, yaml, job, tweak) | |
| 671 | + | if res.Conclusion != ConclusionSuccess { | |
| 672 | + | t.Fatalf("%+v", res) | |
| 673 | + | } | |
| 674 | + | return logOf(rec.steps[0]) | |
| 675 | + | } | |
| 676 | + | ||
| 677 | + | func TestSetUpJobMatchesGitHubHosted(t *testing.T) { | |
| 678 | + | needTools(t, "bash") | |
| 679 | + | want := strings.Join([]string{"Current runner version: 'test'", "##[group]GITHUB_TOKEN Permissions", "Contents: read", "Metadata: read", | |
| 680 | + | "Packages: read", "##[endgroup]", "Secret source: Actions", "Cache mode: write", "Prepare workflow directory", | |
| 681 | + | "Prepare all required actions", "Complete job name: j"}, "\n") | |
| 682 | + | if got := setupLog(t, wfHead+" steps:\n - run: echo hi\n", "j", nil); got != want { | |
| 683 | + | t.Errorf("github-hosted:\n%s", got) | |
| 684 | + | } | |
| 685 | + | } | |
| 686 | + | ||
| 687 | + | func TestSetUpJobSelfHostedNamesTheRunner(t *testing.T) { | |
| 688 | + | needTools(t, "bash") | |
| 689 | + | got := setupLog(t, wfHead+" steps:\n - run: echo hi\n", "j", func(c *Config) { c.Runner.Environment = "self-hosted" }) | |
| 690 | + | host, _ := os.Hostname() | |
| 691 | + | if !strings.Contains(got, "Runner name: 'test-runner'\nMachine name: '"+host+"'\n##[group]GITHUB_TOKEN") { | |
| 692 | + | t.Errorf("self-hosted:\n%s", got) | |
| 693 | + | } | |
| 694 | + | } | |
| 695 | + | ||
| 696 | + | // Permissions the workflow sets are not the default ones: no group is printed | |
| 697 | + | // rather than a wrong one. | |
| 698 | + | func TestSetUpJobWithPermissionsPrintsNoDefaultGroup(t *testing.T) { | |
| 699 | + | needTools(t, "bash") | |
| 700 | + | for name, yaml := range map[string]string{ | |
| 701 | + | "workflow": "permissions:\n contents: write\n" + wfHead + " steps:\n - run: echo hi\n", | |
| 702 | + | "job": wfHead + " permissions: read-all\n steps:\n - run: echo hi\n", | |
| 703 | + | } { | |
| 704 | + | got := setupLog(t, yaml, "j", nil) | |
| 705 | + | if strings.Contains(got, "GITHUB_TOKEN Permissions") || !strings.Contains(got, "Secret source: Actions\nCache mode: write") { | |
| 706 | + | t.Errorf("%s:\n%s", name, got) | |
| 707 | + | } | |
| 708 | + | } | |
| 709 | + | } | |
| 710 | + | ||
| 711 | + | func TestSetUpJobNamesTheJobLikeGitHub(t *testing.T) { | |
| 712 | + | needTools(t, "bash") | |
| 713 | + | // recordings/matrix-job-name.json | |
| 714 | + | yaml := `on: push | |
| 715 | + | jobs: | |
| 716 | + | default-name: | |
| 717 | + | runs-on: x | |
| 718 | + | strategy: | |
| 719 | + | matrix: | |
| 720 | + | os: [a, b] | |
| 721 | + | n: [1, 2] | |
| 722 | + | steps: | |
| 723 | + | - run: echo hi | |
| 724 | + | custom-name: | |
| 725 | + | name: "custom ${{ matrix.os }} / ${{ matrix.n }} / ${{ github.event_name }}" | |
| 726 | + | runs-on: x | |
| 727 | + | strategy: | |
| 728 | + | matrix: | |
| 729 | + | os: [a, b] | |
| 730 | + | n: [1] | |
| 731 | + | steps: | |
| 732 | + | - run: echo hi | |
| 733 | + | static-name: | |
| 734 | + | name: same name for every leg | |
| 735 | + | runs-on: x | |
| 736 | + | strategy: | |
| 737 | + | matrix: | |
| 738 | + | n: [1, 2] | |
| 739 | + | steps: | |
| 740 | + | - run: echo hi | |
| 741 | + | plain: | |
| 742 | + | name: "plain ${{ github.event_name }}" | |
| 743 | + | runs-on: x | |
| 744 | + | steps: | |
| 745 | + | - run: echo hi | |
| 746 | + | ` | |
| 747 | + | for _, tc := range []struct{ job, matrix, strategy, want string }{ | |
| 748 | + | {"default-name", `{"os":"b","n":1}`, `{"job-index":2}`, "default-name (b, 1)"}, | |
| 749 | + | {"default-name", `{"os":"a","n":2}`, `{"job-index":1}`, "default-name (a, 2)"}, | |
| 750 | + | {"default-name", `{"os":"b","n":2}`, ``, "default-name (b, 2)"}, | |
| 751 | + | {"custom-name", `{"os":"b","n":1}`, `{"job-index":1}`, "custom b / 1 / push"}, | |
| 752 | + | {"static-name", `{"n":2}`, `{"job-index":1}`, "same name for every leg (2)"}, | |
| 753 | + | {"plain", `{}`, ``, "plain push"}, | |
| 754 | + | {"static-name", `{"n":9}`, ``, "static-name"}, // not one of the combinations: the job key | |
| 755 | + | } { | |
| 756 | + | t.Run(tc.want, func(t *testing.T) { | |
| 757 | + | got := setupLog(t, yaml, tc.job, func(c *Config) { | |
| 758 | + | c.MatrixJSON = tc.matrix | |
| 759 | + | ctx := testGitHubContext(tc.job) | |
| 760 | + | ctx = strings.TrimSuffix(ctx, "}") | |
| 761 | + | ctx += `,"matrix":` + tc.matrix | |
| 762 | + | if tc.strategy != "" { | |
| 763 | + | ctx += `,"strategy":` + tc.strategy | |
| 764 | + | } | |
| 765 | + | c.ContextsJSON = ctx + "}" | |
| 766 | + | }) | |
| 767 | + | if !strings.HasSuffix(got, "Complete job name: "+tc.want) { | |
| 768 | + | t.Errorf("got:\n%s", got) | |
| 769 | + | } | |
| 770 | + | }) | |
| 771 | + | } | |
| 772 | + | } | |
| @@ -31,7 +31,7 @@ func TestImplicitStepsAndNumbering(t *testing.T) { | |||||
| 31 | } | 31 | } | ||
| 32 | } | 32 | } | ||
| 33 | setup := logOf(rec.steps[0]) | 33 | setup := logOf(rec.steps[0]) | ||
| 34 | - | for _, w := range []string{"Current runner version: 'test'", "Runner name: 'test-runner'", "Prepare workflow directory", "Prepare all required actions", "Complete job name: j"} { | 34 | + | for _, w := range []string{"Current runner version: 'test'", "Prepare workflow directory", "Prepare all required actions", "Complete job name: j"} { |
| 35 | if !strings.Contains(setup, w) { | 35 | if !strings.Contains(setup, w) { | ||
| 36 | t.Errorf("Set up job lacks %q:\n%s", w, setup) | 36 | t.Errorf("Set up job lacks %q:\n%s", w, setup) | ||
| 37 | } | 37 | } | ||
| @@ -664,3 +664,109 @@ func TestMatrixContextWithoutMatrixIsNull(t *testing.T) { | |||||
| 664 | t.Errorf("%q", got) | 664 | t.Errorf("%q", got) | ||
| 665 | } | 665 | } | ||
| 666 | } | 666 | } | ||
| 667 | + | ||||
| 668 | + | func setupLog(t *testing.T, yaml, job string, tweak func(*Config)) string { | |||
| 669 | + | t.Helper() | |||
| 670 | + | rec, res := runLocal(t, yaml, job, tweak) | |||
| 671 | + | if res.Conclusion != ConclusionSuccess { | |||
| 672 | + | t.Fatalf("%+v", res) | |||
| 673 | + | } | |||
| 674 | + | return logOf(rec.steps[0]) | |||
| 675 | + | } | |||
| 676 | + | ||||
| 677 | + | func TestSetUpJobMatchesGitHubHosted(t *testing.T) { | |||
| 678 | + | needTools(t, "bash") | |||
| 679 | + | want := strings.Join([]string{"Current runner version: 'test'", "##[group]GITHUB_TOKEN Permissions", "Contents: read", "Metadata: read", | |||
| 680 | + | "Packages: read", "##[endgroup]", "Secret source: Actions", "Cache mode: write", "Prepare workflow directory", | |||
| 681 | + | "Prepare all required actions", "Complete job name: j"}, "\n") | |||
| 682 | + | if got := setupLog(t, wfHead+" steps:\n - run: echo hi\n", "j", nil); got != want { | |||
| 683 | + | t.Errorf("github-hosted:\n%s", got) | |||
| 684 | + | } | |||
| 685 | + | } | |||
| 686 | + | ||||
| 687 | + | func TestSetUpJobSelfHostedNamesTheRunner(t *testing.T) { | |||
| 688 | + | needTools(t, "bash") | |||
| 689 | + | got := setupLog(t, wfHead+" steps:\n - run: echo hi\n", "j", func(c *Config) { c.Runner.Environment = "self-hosted" }) | |||
| 690 | + | host, _ := os.Hostname() | |||
| 691 | + | if !strings.Contains(got, "Runner name: 'test-runner'\nMachine name: '"+host+"'\n##[group]GITHUB_TOKEN") { | |||
| 692 | + | t.Errorf("self-hosted:\n%s", got) | |||
| 693 | + | } | |||
| 694 | + | } | |||
| 695 | + | ||||
| 696 | + | // Permissions the workflow sets are not the default ones: no group is printed | |||
| 697 | + | // rather than a wrong one. | |||
| 698 | + | func TestSetUpJobWithPermissionsPrintsNoDefaultGroup(t *testing.T) { | |||
| 699 | + | needTools(t, "bash") | |||
| 700 | + | for name, yaml := range map[string]string{ | |||
| 701 | + | "workflow": "permissions:\n contents: write\n" + wfHead + " steps:\n - run: echo hi\n", | |||
| 702 | + | "job": wfHead + " permissions: read-all\n steps:\n - run: echo hi\n", | |||
| 703 | + | } { | |||
| 704 | + | got := setupLog(t, yaml, "j", nil) | |||
| 705 | + | if strings.Contains(got, "GITHUB_TOKEN Permissions") || !strings.Contains(got, "Secret source: Actions\nCache mode: write") { | |||
| 706 | + | t.Errorf("%s:\n%s", name, got) | |||
| 707 | + | } | |||
| 708 | + | } | |||
| 709 | + | } | |||
| 710 | + | ||||
| 711 | + | func TestSetUpJobNamesTheJobLikeGitHub(t *testing.T) { | |||
| 712 | + | needTools(t, "bash") | |||
| 713 | + | // recordings/matrix-job-name.json | |||
| 714 | + | yaml := `on: push | |||
| 715 | + | jobs: | |||
| 716 | + | default-name: | |||
| 717 | + | runs-on: x | |||
| 718 | + | strategy: | |||
| 719 | + | matrix: | |||
| 720 | + | os: [a, b] | |||
| 721 | + | n: [1, 2] | |||
| 722 | + | steps: | |||
| 723 | + | - run: echo hi | |||
| 724 | + | custom-name: | |||
| 725 | + | name: "custom ${{ matrix.os }} / ${{ matrix.n }} / ${{ github.event_name }}" | |||
| 726 | + | runs-on: x | |||
| 727 | + | strategy: | |||
| 728 | + | matrix: | |||
| 729 | + | os: [a, b] | |||
| 730 | + | n: [1] | |||
| 731 | + | steps: | |||
| 732 | + | - run: echo hi | |||
| 733 | + | static-name: | |||
| 734 | + | name: same name for every leg | |||
| 735 | + | runs-on: x | |||
| 736 | + | strategy: | |||
| 737 | + | matrix: | |||
| 738 | + | n: [1, 2] | |||
| 739 | + | steps: | |||
| 740 | + | - run: echo hi | |||
| 741 | + | plain: | |||
| 742 | + | name: "plain ${{ github.event_name }}" | |||
| 743 | + | runs-on: x | |||
| 744 | + | steps: | |||
| 745 | + | - run: echo hi | |||
| 746 | + | ` | |||
| 747 | + | for _, tc := range []struct{ job, matrix, strategy, want string }{ | |||
| 748 | + | {"default-name", `{"os":"b","n":1}`, `{"job-index":2}`, "default-name (b, 1)"}, | |||
| 749 | + | {"default-name", `{"os":"a","n":2}`, `{"job-index":1}`, "default-name (a, 2)"}, | |||
| 750 | + | {"default-name", `{"os":"b","n":2}`, ``, "default-name (b, 2)"}, | |||
| 751 | + | {"custom-name", `{"os":"b","n":1}`, `{"job-index":1}`, "custom b / 1 / push"}, | |||
| 752 | + | {"static-name", `{"n":2}`, `{"job-index":1}`, "same name for every leg (2)"}, | |||
| 753 | + | {"plain", `{}`, ``, "plain push"}, | |||
| 754 | + | {"static-name", `{"n":9}`, ``, "static-name"}, // not one of the combinations: the job key | |||
| 755 | + | } { | |||
| 756 | + | t.Run(tc.want, func(t *testing.T) { | |||
| 757 | + | got := setupLog(t, yaml, tc.job, func(c *Config) { | |||
| 758 | + | c.MatrixJSON = tc.matrix | |||
| 759 | + | ctx := testGitHubContext(tc.job) | |||
| 760 | + | ctx = strings.TrimSuffix(ctx, "}") | |||
| 761 | + | ctx += `,"matrix":` + tc.matrix | |||
| 762 | + | if tc.strategy != "" { | |||
| 763 | + | ctx += `,"strategy":` + tc.strategy | |||
| 764 | + | } | |||
| 765 | + | c.ContextsJSON = ctx + "}" | |||
| 766 | + | }) | |||
| 767 | + | if !strings.HasSuffix(got, "Complete job name: "+tc.want) { | |||
| 768 | + | t.Errorf("got:\n%s", got) | |||
| 769 | + | } | |||
| 770 | + | }) | |||
| 771 | + | } | |||
| 772 | + | } | |||