detached 723958f

docs(workflow): say what was recorded, what was observed on the run page and what is assumed

commit
723958f8efd281235597c38f0a5c7313c6e894e2
author
brice · · 2026-10-12 19:24 UTC
committer
brice · · 2026-10-18 14:59 UTC
parent
87e6700
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +82 -52
M workflow/PORTING.md +26 -17
@@ -43,16 +43,19 @@ code.
4343 ## Recorded (github.com, ermite-conformance corpus)
4444
4545 - Status functions (`success()`, `failure()`, `always()`, `cancelled()`) in
46- `env` or `run` expressions make the workflow invalid with
47- `Unrecognized function: 'success'. Located at position 1 within expression:
48- success()` (expr-status-in-env, expr-status-in-run); `TestRecordedInvalidWorkflows`.
49-- Invalid workflow messages: `The workflow must contain at least one job with
46+ `env` or `run` expressions make the workflow invalid (recorded:
47+ expr-status-in-env, expr-status-in-run, each a failed run with no job). The
48+ text `Unrecognized function: 'success'. Located at position 1 within
49+ expression: success()` is the one observed on the github.com run page, not in
50+ the recordings; `TestRecordedInvalidWorkflows`.
51+- Invalid workflow messages (the recordings hold the failed run only, not the
52+ text; the messages are those of the reference parser, which refuses the same
53+ files): `The workflow must contain at least one job with
5054 no dependencies.` (no root job: needs on an unknown job or a cycle with no
5155 other root), `Job 'x' depends on unknown job 'y'.`, `Job 'x' depends on job
5256 'y' which creates a cycle in the dependency graph.`, `Required property is
5357 missing: runs-on`, `Unexpected value 'unknown-key'` (invalid-needs,
54- invalid-schema). GitHub's recordings do not keep the text of the message; ours
55- are the reference parser's, which the corpus shows refuses the same files.
58+ invalid-schema).
5659
5760 ## To verify (recordings)
5861
@@ -75,10 +78,15 @@ code.
7578 skip-checks-trailer, skip-directive-tag): markers are case-sensitive,
7679 anywhere; `skip-checks: true` needs two blank lines before it and
7780 nothing after it, in exact case. The tag case is the caller's: the
78- message of the commit the tag points at. Only the head commit counts.
79-- [ ] **`ExpandMatrix`** (recorded: the unknown exclude key, once per
80- offending entry, and the 256 limit message, see
81- `configurationLimitError`; matrix-edge-values, matrix-over-256): order of the product (first key slowest), include
81+ message of the commit the tag points at. Assumed, not recorded: only
82+ the head commit counts (the scenarios push one commit at a time); a
83+ line of spaces or tabs is not a blank line; more than two blank lines
84+ count like two.
85+- [ ] **`ExpandMatrix`**: the unknown exclude key error (once per offending
86+ entry) and the over-256 message (`configurationLimitError`) are the
87+ texts observed on the github.com run page for matrix-edge-values and
88+ matrix-over-256, not in the recordings (a failed run with no job). To
89+ verify: order of the product (first key slowest), include
8290 semantics (added to every matching combination, never overwriting
8391 original values, later includes overwrite earlier added values, a
8492 non-matching include is a new combination, match by vector keys only),
@@ -86,15 +94,16 @@ code.
8694 combinations after include, key case-insensitivity, value comparison
8795 (same kind; objects by containment), the error when all combinations are
8896 excluded (GitHub may fail the run differently), empty vector error.
89-- [x] A **null or empty matrix value** is not shown in the job name
97+- [x] A **null or empty (`''`) matrix value** is not shown in the job name
9098 (recorded: matrix-edge-values).
91-- [x] **Job display names** (`MatrixCombo.DisplayName`, `Job.DisplayName`;
92- recorded: matrix-job-name, matrix-edge-values):
93- `name (v1, v2)` listing every scalar value of the combination in key
94- order, then keys added by include; a name that reads the `matrix`
99+- [ ] **Job display names** (`MatrixCombo.DisplayName`, `Job.DisplayName`):
100+ recorded in matrix-job-name and matrix-edge-values: `name (v1, v2)`
101+ listing the values in key order, numbers and booleans as
102+ `CoerceString` shows them, a name that does not read `matrix` followed
103+ by the values. Assumed: the keys added by include come after the vector
104+ keys; arrays and objects are left out; a name that reads the `matrix`
95105 context (found in the expression's syntax tree, not by searching the
96- text) is used as is. Booleans and numbers are shown with `CoerceString`; null, `''`, arrays
97- and objects are left out.
106+ text) is used as is.
98107 - [ ] **`strategy` context** (`Strategy.Context`): `fail-fast`, `job-index`,
99108 `job-total`, `max-parallel` (the job count when unlimited).
100109 - [ ] `fail-fast` and `max-parallel` accept strings (`"false"`, `"2"`) after
@@ -43,16 +43,19 @@ code.
43 ## Recorded (github.com, ermite-conformance corpus)43 ## Recorded (github.com, ermite-conformance corpus)
44 44
45 - Status functions (`success()`, `failure()`, `always()`, `cancelled()`) in45 - Status functions (`success()`, `failure()`, `always()`, `cancelled()`) in
46- `env` or `run` expressions make the workflow invalid with46+ `env` or `run` expressions make the workflow invalid (recorded:
47- `Unrecognized function: 'success'. Located at position 1 within expression:47+ expr-status-in-env, expr-status-in-run, each a failed run with no job). The
48- success()` (expr-status-in-env, expr-status-in-run); `TestRecordedInvalidWorkflows`.48+ text `Unrecognized function: 'success'. Located at position 1 within
49-- Invalid workflow messages: `The workflow must contain at least one job with49+ expression: success()` is the one observed on the github.com run page, not in
50+ the recordings; `TestRecordedInvalidWorkflows`.
51+- Invalid workflow messages (the recordings hold the failed run only, not the
52+ text; the messages are those of the reference parser, which refuses the same
53+ files): `The workflow must contain at least one job with
50 no dependencies.` (no root job: needs on an unknown job or a cycle with no54 no dependencies.` (no root job: needs on an unknown job or a cycle with no
51 other root), `Job 'x' depends on unknown job 'y'.`, `Job 'x' depends on job55 other root), `Job 'x' depends on unknown job 'y'.`, `Job 'x' depends on job
52 'y' which creates a cycle in the dependency graph.`, `Required property is56 'y' which creates a cycle in the dependency graph.`, `Required property is
53 missing: runs-on`, `Unexpected value 'unknown-key'` (invalid-needs,57 missing: runs-on`, `Unexpected value 'unknown-key'` (invalid-needs,
54- invalid-schema). GitHub's recordings do not keep the text of the message; ours58+ invalid-schema).
55- are the reference parser's, which the corpus shows refuses the same files.
56 59
57 ## To verify (recordings)60 ## To verify (recordings)
58 61
@@ -75,10 +78,15 @@ code.
75 skip-checks-trailer, skip-directive-tag): markers are case-sensitive,78 skip-checks-trailer, skip-directive-tag): markers are case-sensitive,
76 anywhere; `skip-checks: true` needs two blank lines before it and79 anywhere; `skip-checks: true` needs two blank lines before it and
77 nothing after it, in exact case. The tag case is the caller's: the80 nothing after it, in exact case. The tag case is the caller's: the
78- message of the commit the tag points at. Only the head commit counts.81+ message of the commit the tag points at. Assumed, not recorded: only
79-- [ ] **`ExpandMatrix`** (recorded: the unknown exclude key, once per82+ the head commit counts (the scenarios push one commit at a time); a
80- offending entry, and the 256 limit message, see83+ line of spaces or tabs is not a blank line; more than two blank lines
81- `configurationLimitError`; matrix-edge-values, matrix-over-256): order of the product (first key slowest), include84+ count like two.
85+- [ ] **`ExpandMatrix`**: the unknown exclude key error (once per offending
86+ entry) and the over-256 message (`configurationLimitError`) are the
87+ texts observed on the github.com run page for matrix-edge-values and
88+ matrix-over-256, not in the recordings (a failed run with no job). To
89+ verify: order of the product (first key slowest), include
82 semantics (added to every matching combination, never overwriting90 semantics (added to every matching combination, never overwriting
83 original values, later includes overwrite earlier added values, a91 original values, later includes overwrite earlier added values, a
84 non-matching include is a new combination, match by vector keys only),92 non-matching include is a new combination, match by vector keys only),
@@ -86,15 +94,16 @@ code.
86 combinations after include, key case-insensitivity, value comparison94 combinations after include, key case-insensitivity, value comparison
87 (same kind; objects by containment), the error when all combinations are95 (same kind; objects by containment), the error when all combinations are
88 excluded (GitHub may fail the run differently), empty vector error.96 excluded (GitHub may fail the run differently), empty vector error.
89-- [x] A **null or empty matrix value** is not shown in the job name97+- [x] A **null or empty (`''`) matrix value** is not shown in the job name
90 (recorded: matrix-edge-values).98 (recorded: matrix-edge-values).
91-- [x] **Job display names** (`MatrixCombo.DisplayName`, `Job.DisplayName`;99+- [ ] **Job display names** (`MatrixCombo.DisplayName`, `Job.DisplayName`):
92- recorded: matrix-job-name, matrix-edge-values):100+ recorded in matrix-job-name and matrix-edge-values: `name (v1, v2)`
93- `name (v1, v2)` listing every scalar value of the combination in key101+ listing the values in key order, numbers and booleans as
94- order, then keys added by include; a name that reads the `matrix`102+ `CoerceString` shows them, a name that does not read `matrix` followed
103+ by the values. Assumed: the keys added by include come after the vector
104+ keys; arrays and objects are left out; a name that reads the `matrix`
95 context (found in the expression's syntax tree, not by searching the105 context (found in the expression's syntax tree, not by searching the
96- text) is used as is. Booleans and numbers are shown with `CoerceString`; null, `''`, arrays106+ text) is used as is.
97- and objects are left out.
98 - [ ] **`strategy` context** (`Strategy.Context`): `fail-fast`, `job-index`,107 - [ ] **`strategy` context** (`Strategy.Context`): `fail-fast`, `job-index`,
99 `job-total`, `max-parallel` (the job count when unlimited).108 `job-total`, `max-parallel` (the job count when unlimited).
100 - [ ] `fail-fast` and `max-parallel` accept strings (`"false"`, `"2"`) after109 - [ ] `fail-fast` and `max-parallel` accept strings (`"false"`, `"2"`) after
M workflow/matrix.go +29 -22
@@ -34,8 +34,9 @@ type MatrixCombo struct {
3434 // they were first added.
3535 Values *expr.Dictionary
3636 // Segments are the scalar values of Values, in order, shown in the name
37- // of the job: "build (ubuntu-latest, 14)". Arrays, mappings, null and the
38- // empty string are left out, like GitHub does (matrix-edge-values).
37+ // of the job: "build (ubuntu-latest, 14)". Null and the empty string are
38+ // left out, as recorded (matrix-edge-values); arrays and mappings are
39+ // left out too, which is assumed.
3940 Segments []string
4041 }
4142
@@ -62,23 +63,26 @@ func (c MatrixCombo) DisplayName(base string) string {
6263 // mentions no vector key matches all), without overwriting the original
6364 // values; an entry that matches no combination becomes a combination of
6465 // its own;
65-// - more than 256 jobs is a a limit error. A strategy without a
66-// matrix gives a single combination.
66+// - more than 256 jobs is an error. A strategy without a matrix gives a
67+// single combination.
6768 //
6869 // The work is bounded: the product of the vectors may not exceed
6970 // [MaxMatrixProduct], include and exclude may have [MaxMatrixEntries] entries
7071 // each, and ctx is checked every few thousand combinations.
7172 //
72-// Recorded on github.com (scenarios matrix-edge-values and matrix-over-256):
73-// the unknown exclude key error, reported once per offending entry, the 256
74-// limit with the number of configurations produced, and the omission of null
75-// and ” from the default name. Still assumed, from the documented semantics
76-// of include and exclude: the order of the cross product (first key slowest),
77-// the display name listing every scalar value including those added by
78-// include (see [MatrixCombo.DisplayName]), the comparison of values (same
79-// kind, strings compared exactly, mappings by containment), an include key
80-// matching a vector key case-insensitively, the error when every combination
81-// is excluded, the other limits above, and the wording of the other errors.
73+// What github.com settled. Recorded (matrix-edge-values): null and the empty
74+// string are left out of the default job name. For matrix-edge-values (exclude key not
75+// in the matrix) and matrix-over-256 the recordings hold only a failed run
76+// with no job: the wording of the two errors below, and the unknown exclude
77+// key being reported once per offending entry, were observed on the github.com
78+// run page, not in the recordings. Still assumed, from the documented
79+// semantics of include and exclude: the order of the cross product (first key
80+// slowest), the display name listing every scalar value including those
81+// added by include (see [MatrixCombo.DisplayName]), the comparison of values
82+// (same kind, strings compared exactly, mappings by containment), an include
83+// key matching a vector key case-insensitively, the error when every
84+// combination is excluded, the other limits above, and the wording of the
85+// other errors.
8286 func ExpandMatrix(ctx context.Context, strategy expr.Value) ([]MatrixCombo, error) {
8387 s, err := ParseStrategy(ctx, strategy)
8488 if err != nil {
@@ -159,10 +163,12 @@ func (m message) Error() string { return string(m) }
159163 var errProductTooBig = fmt.Errorf("matrix too large: the cross product of the vectors exceeds %d combinations", MaxMatrixProduct)
160164
161165 // configurationLimitError is returned when a matrix produces more than
162-// [MaxMatrixJobs] configurations. Its text is GitHub's (recorded by
163-// matrix-over-256): "Strategy for job 'too-many' produced 272 configurations
164-// which exceeds the maximum of 256 configurations". Job is empty when the
165-// matrix was expanded without a job, see [Job.EvaluateStrategy].
166+// [MaxMatrixJobs] configurations. Its text is the one observed on the
167+// github.com run page for matrix-over-256 (not in the recordings, which only
168+// hold the failed run): "Strategy for job 'too-many' produced 272
169+// configurations which exceeds the maximum of 256 configurations". Job is
170+// empty when the matrix was expanded without a job, see
171+// [Job.EvaluateStrategy].
166172 type configurationLimitError struct {
167173 // Job is the id of the job.
168174 Job string
@@ -412,10 +418,11 @@ func valuesMatch(have, want expr.Value) bool {
412418 return false
413419 }
414420
415-// segment is the text a matrix value adds to the name of a job; arrays,
416-// mappings, null and the empty string add nothing (matrix-edge-values:
417-// "default-name", "two-axes (x)"); numbers and booleans are shown as in an
418-// expression ("1.5", "true").
421+// segment is the text a matrix value adds to the name of a job. Null and the
422+// empty string add nothing, numbers and booleans are shown as in an
423+// expression ("1.5", "true"); all recorded (matrix-edge-values:
424+// "default-name", "two-axes (x)"). Arrays and mappings add nothing: assumed,
425+// not recorded.
419426 func segment(v expr.Value) (string, bool) {
420427 switch v.(type) {
421428 case nil, expr.Null, *expr.Array, *expr.Dictionary:
@@ -34,8 +34,9 @@ type MatrixCombo struct {
34 // they were first added.34 // they were first added.
35 Values *expr.Dictionary35 Values *expr.Dictionary
36 // Segments are the scalar values of Values, in order, shown in the name36 // Segments are the scalar values of Values, in order, shown in the name
37- // of the job: "build (ubuntu-latest, 14)". Arrays, mappings, null and the37+ // of the job: "build (ubuntu-latest, 14)". Null and the empty string are
38- // empty string are left out, like GitHub does (matrix-edge-values).38+ // left out, as recorded (matrix-edge-values); arrays and mappings are
39+ // left out too, which is assumed.
39 Segments []string40 Segments []string
40 }41 }
41 42
@@ -62,23 +63,26 @@ func (c MatrixCombo) DisplayName(base string) string {
62 // mentions no vector key matches all), without overwriting the original63 // mentions no vector key matches all), without overwriting the original
63 // values; an entry that matches no combination becomes a combination of64 // values; an entry that matches no combination becomes a combination of
64 // its own;65 // its own;
65-// - more than 256 jobs is a a limit error. A strategy without a66+// - more than 256 jobs is an error. A strategy without a matrix gives a
66-// matrix gives a single combination.67+// single combination.
67 //68 //
68 // The work is bounded: the product of the vectors may not exceed69 // The work is bounded: the product of the vectors may not exceed
69 // [MaxMatrixProduct], include and exclude may have [MaxMatrixEntries] entries70 // [MaxMatrixProduct], include and exclude may have [MaxMatrixEntries] entries
70 // each, and ctx is checked every few thousand combinations.71 // each, and ctx is checked every few thousand combinations.
71 //72 //
72-// Recorded on github.com (scenarios matrix-edge-values and matrix-over-256):73+// What github.com settled. Recorded (matrix-edge-values): null and the empty
73-// the unknown exclude key error, reported once per offending entry, the 25674+// string are left out of the default job name. For matrix-edge-values (exclude key not
74-// limit with the number of configurations produced, and the omission of null75+// in the matrix) and matrix-over-256 the recordings hold only a failed run
75-// and ” from the default name. Still assumed, from the documented semantics76+// with no job: the wording of the two errors below, and the unknown exclude
76-// of include and exclude: the order of the cross product (first key slowest),77+// key being reported once per offending entry, were observed on the github.com
77-// the display name listing every scalar value including those added by78+// run page, not in the recordings. Still assumed, from the documented
78-// include (see [MatrixCombo.DisplayName]), the comparison of values (same79+// semantics of include and exclude: the order of the cross product (first key
79-// kind, strings compared exactly, mappings by containment), an include key80+// slowest), the display name listing every scalar value including those
80-// matching a vector key case-insensitively, the error when every combination81+// added by include (see [MatrixCombo.DisplayName]), the comparison of values
81-// is excluded, the other limits above, and the wording of the other errors.82+// (same kind, strings compared exactly, mappings by containment), an include
83+// key matching a vector key case-insensitively, the error when every
84+// combination is excluded, the other limits above, and the wording of the
85+// other errors.
82 func ExpandMatrix(ctx context.Context, strategy expr.Value) ([]MatrixCombo, error) {86 func ExpandMatrix(ctx context.Context, strategy expr.Value) ([]MatrixCombo, error) {
83 s, err := ParseStrategy(ctx, strategy)87 s, err := ParseStrategy(ctx, strategy)
84 if err != nil {88 if err != nil {
@@ -159,10 +163,12 @@ func (m message) Error() string { return string(m) }
159 var errProductTooBig = fmt.Errorf("matrix too large: the cross product of the vectors exceeds %d combinations", MaxMatrixProduct)163 var errProductTooBig = fmt.Errorf("matrix too large: the cross product of the vectors exceeds %d combinations", MaxMatrixProduct)
160 164
161 // configurationLimitError is returned when a matrix produces more than165 // configurationLimitError is returned when a matrix produces more than
162-// [MaxMatrixJobs] configurations. Its text is GitHub's (recorded by166+// [MaxMatrixJobs] configurations. Its text is the one observed on the
163-// matrix-over-256): "Strategy for job 'too-many' produced 272 configurations167+// github.com run page for matrix-over-256 (not in the recordings, which only
164-// which exceeds the maximum of 256 configurations". Job is empty when the168+// hold the failed run): "Strategy for job 'too-many' produced 272
165-// matrix was expanded without a job, see [Job.EvaluateStrategy].169+// configurations which exceeds the maximum of 256 configurations". Job is
170+// empty when the matrix was expanded without a job, see
171+// [Job.EvaluateStrategy].
166 type configurationLimitError struct {172 type configurationLimitError struct {
167 // Job is the id of the job.173 // Job is the id of the job.
168 Job string174 Job string
@@ -412,10 +418,11 @@ func valuesMatch(have, want expr.Value) bool {
412 return false418 return false
413 }419 }
414 420
415-// segment is the text a matrix value adds to the name of a job; arrays,421+// segment is the text a matrix value adds to the name of a job. Null and the
416-// mappings, null and the empty string add nothing (matrix-edge-values:422+// empty string add nothing, numbers and booleans are shown as in an
417-// "default-name", "two-axes (x)"); numbers and booleans are shown as in an423+// expression ("1.5", "true"); all recorded (matrix-edge-values:
418-// expression ("1.5", "true").424+// "default-name", "two-axes (x)"). Arrays and mappings add nothing: assumed,
425+// not recorded.
419 func segment(v expr.Value) (string, bool) {426 func segment(v expr.Value) (string, bool) {
420 switch v.(type) {427 switch v.(type) {
421 case nil, expr.Null, *expr.Array, *expr.Dictionary:428 case nil, expr.Null, *expr.Array, *expr.Dictionary:
M workflow/matrix_test.go +8 -4
@@ -382,15 +382,17 @@ func TestRecordedMatrixNames(t *testing.T) {
382382 if got := names(`{"os": ["a", "b"], "n": [1, 2]}`); !reflect.DeepEqual(got, want) {
383383 t.Errorf("matrix-job-name: got %q, want %q", got, want)
384384 }
385- // matrix-value-types: objects are not listed ("types 2.5 false" has a
386- // custom name, so only the default name is checked here).
385+ // Objects are not listed. Assumed, not recorded: matrix-value-types has
386+ // custom names only.
387387 if got := names(`{"n": [1], "obj": [{"k": "v"}]}`); !reflect.DeepEqual(got, []string{"default-name (1)"}) {
388388 t.Errorf("object value: got %q", got)
389389 }
390390 }
391391
392392 // TestRecordedExcludeUnknownKey is matrix-edge-values a-exclude-unknown-key.yml:
393-// exclude: [{nokey: x}, {os: one, nokey: y}] made the workflow invalid.
393+// exclude: [{nokey: x}, {os: one, nokey: y}] made the workflow invalid. The
394+// recording holds only the failed run; the message, once per offending entry,
395+// was observed on the github.com run page.
394396 func TestRecordedExcludeUnknownKey(t *testing.T) {
395397 _, err := ExpandMatrix(ctxbg, jsonValue(t, `{"matrix": {"os": ["one", "two"], "exclude": [{"nokey": "x"}, {"os": "one", "nokey": "y"}]}}`))
396398 const one = "Matrix exclude key 'nokey' does not match any key within the matrix"
@@ -405,7 +407,9 @@ func TestRecordedExcludeUnknownKey(t *testing.T) {
405407 }
406408
407409 // TestRecordedMatrixOver256 is the scenario matrix-over-256: 17 x 16 = 272
408-// configurations in job too-many made the workflow invalid.
410+// configurations in job too-many made the workflow invalid. The recording
411+// holds only the failed run; the message was observed on the github.com run
412+// page.
409413 func TestRecordedMatrixOver256(t *testing.T) {
410414 w, errs := Parse("ci.yml", []byte(`on: push
411415 jobs:
@@ -382,15 +382,17 @@ func TestRecordedMatrixNames(t *testing.T) {
382 if got := names(`{"os": ["a", "b"], "n": [1, 2]}`); !reflect.DeepEqual(got, want) {382 if got := names(`{"os": ["a", "b"], "n": [1, 2]}`); !reflect.DeepEqual(got, want) {
383 t.Errorf("matrix-job-name: got %q, want %q", got, want)383 t.Errorf("matrix-job-name: got %q, want %q", got, want)
384 }384 }
385- // matrix-value-types: objects are not listed ("types 2.5 false" has a385+ // Objects are not listed. Assumed, not recorded: matrix-value-types has
386- // custom name, so only the default name is checked here).386+ // custom names only.
387 if got := names(`{"n": [1], "obj": [{"k": "v"}]}`); !reflect.DeepEqual(got, []string{"default-name (1)"}) {387 if got := names(`{"n": [1], "obj": [{"k": "v"}]}`); !reflect.DeepEqual(got, []string{"default-name (1)"}) {
388 t.Errorf("object value: got %q", got)388 t.Errorf("object value: got %q", got)
389 }389 }
390 }390 }
391 391
392 // TestRecordedExcludeUnknownKey is matrix-edge-values a-exclude-unknown-key.yml:392 // TestRecordedExcludeUnknownKey is matrix-edge-values a-exclude-unknown-key.yml:
393-// exclude: [{nokey: x}, {os: one, nokey: y}] made the workflow invalid.393+// exclude: [{nokey: x}, {os: one, nokey: y}] made the workflow invalid. The
394+// recording holds only the failed run; the message, once per offending entry,
395+// was observed on the github.com run page.
394 func TestRecordedExcludeUnknownKey(t *testing.T) {396 func TestRecordedExcludeUnknownKey(t *testing.T) {
395 _, err := ExpandMatrix(ctxbg, jsonValue(t, `{"matrix": {"os": ["one", "two"], "exclude": [{"nokey": "x"}, {"os": "one", "nokey": "y"}]}}`))397 _, err := ExpandMatrix(ctxbg, jsonValue(t, `{"matrix": {"os": ["one", "two"], "exclude": [{"nokey": "x"}, {"os": "one", "nokey": "y"}]}}`))
396 const one = "Matrix exclude key 'nokey' does not match any key within the matrix"398 const one = "Matrix exclude key 'nokey' does not match any key within the matrix"
@@ -405,7 +407,9 @@ func TestRecordedExcludeUnknownKey(t *testing.T) {
405 }407 }
406 408
407 // TestRecordedMatrixOver256 is the scenario matrix-over-256: 17 x 16 = 272409 // TestRecordedMatrixOver256 is the scenario matrix-over-256: 17 x 16 = 272
408-// configurations in job too-many made the workflow invalid.410+// configurations in job too-many made the workflow invalid. The recording
411+// holds only the failed run; the message was observed on the github.com run
412+// page.
409 func TestRecordedMatrixOver256(t *testing.T) {413 func TestRecordedMatrixOver256(t *testing.T) {
410 w, errs := Parse("ci.yml", []byte(`on: push414 w, errs := Parse("ci.yml", []byte(`on: push
411 jobs:415 jobs:
M workflow/push.go +2 -1
@@ -47,7 +47,8 @@ type PushInfo struct {
4747 // - when both a branches and a tags filter exist, a ref is checked against
4848 // the filter for its kind;
4949 // - paths and paths-ignore apply to branch pushes only, against the first
50-// 3000 changed files; when the list is truncated they pass;
50+// 3000 changed files; when the list is unavailable (see
51+// [PushInfo.FilesTruncated]) they pass;
5152 // - patterns are evaluated in order and the last one that matches wins.
5253 //
5354 // Commit-message directives are separate: see [SkipCI].
@@ -47,7 +47,8 @@ type PushInfo struct {
47 // - when both a branches and a tags filter exist, a ref is checked against47 // - when both a branches and a tags filter exist, a ref is checked against
48 // the filter for its kind;48 // the filter for its kind;
49 // - paths and paths-ignore apply to branch pushes only, against the first49 // - paths and paths-ignore apply to branch pushes only, against the first
50-// 3000 changed files; when the list is truncated they pass;50+// 3000 changed files; when the list is unavailable (see
51+// [PushInfo.FilesTruncated]) they pass;
51 // - patterns are evaluated in order and the last one that matches wins.52 // - patterns are evaluated in order and the last one that matches wins.
52 //53 //
53 // Commit-message directives are separate: see [SkipCI].54 // Commit-message directives are separate: see [SkipCI].
M workflow/recorded_test.go +5 -2
@@ -9,8 +9,11 @@ import (
99
1010 // TestRecordedInvalidWorkflows parses the workflow files of the recorded
1111 // scenarios expr-status-in-env, expr-status-in-run, invalid-needs and
12-// invalid-schema, which GitHub refused (a failed run without jobs), and checks
13-// the messages. The valid sibling of each scenario is in TestRecordedValid.
12+// invalid-schema, which GitHub refused (a failed run without jobs; the
13+// recordings do not hold the messages), and checks that this package refuses
14+// them, with the reference parser's messages, except for the status functions
15+// whose message was observed on the github.com run page. The valid sibling of
16+// each scenario is in TestRecordedValid.
1417 func TestRecordedInvalidWorkflows(t *testing.T) {
1518 const on = "on:\n push:\n branches: ['scn/**']\n"
1619 tests := []struct {
@@ -9,8 +9,11 @@ import (
9 9
10 // TestRecordedInvalidWorkflows parses the workflow files of the recorded10 // TestRecordedInvalidWorkflows parses the workflow files of the recorded
11 // scenarios expr-status-in-env, expr-status-in-run, invalid-needs and11 // scenarios expr-status-in-env, expr-status-in-run, invalid-needs and
12-// invalid-schema, which GitHub refused (a failed run without jobs), and checks12+// invalid-schema, which GitHub refused (a failed run without jobs; the
13-// the messages. The valid sibling of each scenario is in TestRecordedValid.13+// recordings do not hold the messages), and checks that this package refuses
14+// them, with the reference parser's messages, except for the status functions
15+// whose message was observed on the github.com run page. The valid sibling of
16+// each scenario is in TestRecordedValid.
14 func TestRecordedInvalidWorkflows(t *testing.T) {17 func TestRecordedInvalidWorkflows(t *testing.T) {
15 const on = "on:\n push:\n branches: ['scn/**']\n"18 const on = "on:\n push:\n branches: ['scn/**']\n"
16 tests := []struct {19 tests := []struct {
M workflow/skip.go +6 -4
@@ -23,10 +23,12 @@ var skipMarkers = []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]"
2323 // trailer, with another value ("false", "yes"), or with any change of case
2424 // in the key or the value.
2525 //
26-// Not recorded, so assumed: more than two blank lines before the trailer
27-// count like two, trailing blank lines and CRLF line ends are ignored, more
28-// than one space after the colon is not accepted, and a message
29-// made of the trailer alone does not skip.
26+// Not recorded, so assumed: only the head commit of a push is looked at (the
27+// scenarios push one commit at a time); more than two blank lines before the
28+// trailer count like two; a line holding only spaces or tabs is not blank
29+// (the two lines before the trailer must be empty); trailing blank lines and
30+// CRLF line ends are ignored; more than one space after the colon is not
31+// accepted; and a message made of the trailer alone does not skip.
3032 func SkipCI(headCommitMessage string) bool {
3133 for _, m := range skipMarkers {
3234 if strings.Contains(headCommitMessage, m) {
@@ -23,10 +23,12 @@ var skipMarkers = []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]"
23 // trailer, with another value ("false", "yes"), or with any change of case23 // trailer, with another value ("false", "yes"), or with any change of case
24 // in the key or the value.24 // in the key or the value.
25 //25 //
26-// Not recorded, so assumed: more than two blank lines before the trailer26+// Not recorded, so assumed: only the head commit of a push is looked at (the
27-// count like two, trailing blank lines and CRLF line ends are ignored, more27+// scenarios push one commit at a time); more than two blank lines before the
28-// than one space after the colon is not accepted, and a message28+// trailer count like two; a line holding only spaces or tabs is not blank
29-// made of the trailer alone does not skip.29+// (the two lines before the trailer must be empty); trailing blank lines and
30+// CRLF line ends are ignored; more than one space after the colon is not
31+// accepted; and a message made of the trailer alone does not skip.
30 func SkipCI(headCommitMessage string) bool {32 func SkipCI(headCommitMessage string) bool {
31 for _, m := range skipMarkers {33 for _, m := range skipMarkers {
32 if strings.Contains(headCommitMessage, m) {34 if strings.Contains(headCommitMessage, m) {
M workflow/skip_test.go +4 -0
@@ -60,6 +60,10 @@ func TestSkipCI(t *testing.T) {
6060 {"crlf", "x\r\n\r\n\r\nskip-checks: true\r\n", true},
6161 {"three blank lines", "x\n\n\n\nskip-checks: true", true},
6262 {"trailer alone", "\n\n\nskip-checks: true", false},
63+ // A line with only white space is not blank (GO-006, assumed).
64+ {"space-only lines before trailer", "x\n \n \nskip-checks: true", false},
65+ {"one space-only line before trailer", "x\n\n \nskip-checks: true", false},
66+ {"tab-only lines before trailer", "x\n\t\n\t\nskip-checks: true", false},
6367 {"empty", "", false},
6468 }
6569 for _, tc := range tests {
@@ -60,6 +60,10 @@ func TestSkipCI(t *testing.T) {
60 {"crlf", "x\r\n\r\n\r\nskip-checks: true\r\n", true},60 {"crlf", "x\r\n\r\n\r\nskip-checks: true\r\n", true},
61 {"three blank lines", "x\n\n\n\nskip-checks: true", true},61 {"three blank lines", "x\n\n\n\nskip-checks: true", true},
62 {"trailer alone", "\n\n\nskip-checks: true", false},62 {"trailer alone", "\n\n\nskip-checks: true", false},
63+ // A line with only white space is not blank (GO-006, assumed).
64+ {"space-only lines before trailer", "x\n \n \nskip-checks: true", false},
65+ {"one space-only line before trailer", "x\n\n \nskip-checks: true", false},
66+ {"tab-only lines before trailer", "x\n\t\n\t\nskip-checks: true", false},
63 {"empty", "", false},67 {"empty", "", false},
64 }68 }
65 for _, tc := range tests {69 for _, tc := range tests {
M workflow/strategy.go +2 -2
@@ -167,8 +167,8 @@ func (s *Strategy) MatrixEnv(env template.Env, index int) (template.Env, error)
167167 // already include the matrix and strategy contexts, see [Strategy.MatrixEnv].
168168 //
169169 // Recorded on github.com (matrix-job-name): "default-name (a, 1)", a custom
170-// name using the matrix as is, and a custom name that does not read the matrix
171-// followed by the values ("same name for every leg (1)").
170+// name using the matrix as is, and a custom name that does not read the
171+// matrix followed by the values ("same name for every leg (1)").
172172 func (j *Job) DisplayName(ctx context.Context, env template.Env, s *Strategy, index int) (string, error) {
173173 if index < 0 || index >= len(s.Combos) {
174174 return "", fmt.Errorf("matrix combination %d out of range (%d combinations)", index, len(s.Combos))
@@ -167,8 +167,8 @@ func (s *Strategy) MatrixEnv(env template.Env, index int) (template.Env, error)
167 // already include the matrix and strategy contexts, see [Strategy.MatrixEnv].167 // already include the matrix and strategy contexts, see [Strategy.MatrixEnv].
168 //168 //
169 // Recorded on github.com (matrix-job-name): "default-name (a, 1)", a custom169 // Recorded on github.com (matrix-job-name): "default-name (a, 1)", a custom
170-// name using the matrix as is, and a custom name that does not read the matrix170+// name using the matrix as is, and a custom name that does not read the
171-// followed by the values ("same name for every leg (1)").171+// matrix followed by the values ("same name for every leg (1)").
172 func (j *Job) DisplayName(ctx context.Context, env template.Env, s *Strategy, index int) (string, error) {172 func (j *Job) DisplayName(ctx context.Context, env template.Env, s *Strategy, index int) (string, error) {
173 if index < 0 || index >= len(s.Combos) {173 if index < 0 || index >= len(s.Combos) {
174 return "", fmt.Errorf("matrix combination %d out of range (%d combinations)", index, len(s.Combos))174 return "", fmt.Errorf("matrix combination %d out of range (%d combinations)", index, len(s.Combos))

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close