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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +82 -52
M workflow/PORTING.md +26 -17
| @@ -43,16 +43,19 @@ code. | |||
| 43 | 43 | ## Recorded (github.com, ermite-conformance corpus) | |
| 44 | 44 | ||
| 45 | 45 | - 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 | |
| 50 | 54 | no dependencies.` (no root job: needs on an unknown job or a cycle with no | |
| 51 | 55 | other root), `Job 'x' depends on unknown job 'y'.`, `Job 'x' depends on job | |
| 52 | 56 | 'y' which creates a cycle in the dependency graph.`, `Required property is | |
| 53 | 57 | 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). | |
| 56 | 59 | ||
| 57 | 60 | ## To verify (recordings) | |
| 58 | 61 | ||
| @@ -75,10 +78,15 @@ code. | |||
| 75 | 78 | skip-checks-trailer, skip-directive-tag): markers are case-sensitive, | |
| 76 | 79 | anywhere; `skip-checks: true` needs two blank lines before it and | |
| 77 | 80 | 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 | |
| 82 | 90 | semantics (added to every matching combination, never overwriting | |
| 83 | 91 | original values, later includes overwrite earlier added values, a | |
| 84 | 92 | non-matching include is a new combination, match by vector keys only), | |
| @@ -86,15 +94,16 @@ code. | |||
| 86 | 94 | combinations after include, key case-insensitivity, value comparison | |
| 87 | 95 | (same kind; objects by containment), the error when all combinations are | |
| 88 | 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 name | |
| 97 | + | - [x] A **null or empty (`''`) matrix value** is not shown in the job name | |
| 90 | 98 | (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` | |
| 95 | 105 | 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. | |
| 98 | 107 | - [ ] **`strategy` context** (`Strategy.Context`): `fail-fast`, `job-index`, | |
| 99 | 108 | `job-total`, `max-parallel` (the job count when unlimited). | |
| 100 | 109 | - [ ] `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()`) in | 45 | - Status functions (`success()`, `failure()`, `always()`, `cancelled()`) in | ||
| 46 | - | `env` or `run` expressions make the workflow invalid with | 46 | + | `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 with | 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 | |||
| 50 | no dependencies.` (no root job: needs on an unknown job or a cycle with no | 54 | 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 job | 55 | 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 is | 56 | '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; ours | 58 | + | 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 and | 79 | 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: the | 80 | 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 per | 82 | + | the head commit counts (the scenarios push one commit at a time); a |
| 80 | - | offending entry, and the 256 limit message, see | 83 | + | 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), include | 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 | |||
| 82 | semantics (added to every matching combination, never overwriting | 90 | semantics (added to every matching combination, never overwriting | ||
| 83 | original values, later includes overwrite earlier added values, a | 91 | 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 comparison | 94 | combinations after include, key case-insensitivity, value comparison | ||
| 87 | (same kind; objects by containment), the error when all combinations are | 95 | (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 name | 97 | + | - [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 key | 101 | + | 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 the | 105 | 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 | 106 | + | 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"`) after | 109 | - [ ] `fail-fast` and `max-parallel` accept strings (`"false"`, `"2"`) after | ||
M workflow/matrix.go +29 -22
| @@ -34,8 +34,9 @@ type MatrixCombo struct { | |||
| 34 | 34 | // they were first added. | |
| 35 | 35 | Values *expr.Dictionary | |
| 36 | 36 | // 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. | |
| 39 | 40 | Segments []string | |
| 40 | 41 | } | |
| 41 | 42 | ||
| @@ -62,23 +63,26 @@ func (c MatrixCombo) DisplayName(base string) string { | |||
| 62 | 63 | // mentions no vector key matches all), without overwriting the original | |
| 63 | 64 | // values; an entry that matches no combination becomes a combination of | |
| 64 | 65 | // 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. | |
| 67 | 68 | // | |
| 68 | 69 | // The work is bounded: the product of the vectors may not exceed | |
| 69 | 70 | // [MaxMatrixProduct], include and exclude may have [MaxMatrixEntries] entries | |
| 70 | 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 | - | // 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. | |
| 82 | 86 | func ExpandMatrix(ctx context.Context, strategy expr.Value) ([]MatrixCombo, error) { | |
| 83 | 87 | s, err := ParseStrategy(ctx, strategy) | |
| 84 | 88 | if err != nil { | |
| @@ -159,10 +163,12 @@ func (m message) Error() string { return string(m) } | |||
| 159 | 163 | var errProductTooBig = fmt.Errorf("matrix too large: the cross product of the vectors exceeds %d combinations", MaxMatrixProduct) | |
| 160 | 164 | ||
| 161 | 165 | // 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]. | |
| 166 | 172 | type configurationLimitError struct { | |
| 167 | 173 | // Job is the id of the job. | |
| 168 | 174 | Job string | |
| @@ -412,10 +418,11 @@ func valuesMatch(have, want expr.Value) bool { | |||
| 412 | 418 | return false | |
| 413 | 419 | } | |
| 414 | 420 | ||
| 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. | |
| 419 | 426 | func segment(v expr.Value) (string, bool) { | |
| 420 | 427 | switch v.(type) { | |
| 421 | 428 | 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.Dictionary | 35 | Values *expr.Dictionary | ||
| 36 | // Segments are the scalar values of Values, in order, shown in the name | 36 | // 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 | 37 | + | // 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 []string | 40 | 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 original | 63 | // mentions no vector key matches all), without overwriting the original | ||
| 63 | // values; an entry that matches no combination becomes a combination of | 64 | // 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 a | 66 | + | // - 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 exceed | 69 | // The work is bounded: the product of the vectors may not exceed | ||
| 69 | // [MaxMatrixProduct], include and exclude may have [MaxMatrixEntries] entries | 70 | // [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 256 | 74 | + | // 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 null | 75 | + | // 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 semantics | 76 | + | // 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 by | 78 | + | // run page, not in the recordings. Still assumed, from the documented |
| 78 | - | // include (see [MatrixCombo.DisplayName]), the comparison of values (same | 79 | + | // semantics of include and exclude: the order of the cross product (first key |
| 79 | - | // kind, strings compared exactly, mappings by containment), an include key | 80 | + | // slowest), the display name listing every scalar value including those |
| 80 | - | // matching a vector key case-insensitively, the error when every combination | 81 | + | // 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 than | 165 | // configurationLimitError is returned when a matrix produces more than | ||
| 162 | - | // [MaxMatrixJobs] configurations. Its text is GitHub's (recorded by | 166 | + | // [MaxMatrixJobs] configurations. Its text is the one observed on the |
| 163 | - | // matrix-over-256): "Strategy for job 'too-many' produced 272 configurations | 167 | + | // 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 the | 168 | + | // 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 string | 174 | Job string | ||
| @@ -412,10 +418,11 @@ func valuesMatch(have, want expr.Value) bool { | |||||
| 412 | return false | 418 | 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 an | 423 | + | // 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) { | |||
| 382 | 382 | if got := names(`{"os": ["a", "b"], "n": [1, 2]}`); !reflect.DeepEqual(got, want) { | |
| 383 | 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 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. | |
| 387 | 387 | if got := names(`{"n": [1], "obj": [{"k": "v"}]}`); !reflect.DeepEqual(got, []string{"default-name (1)"}) { | |
| 388 | 388 | t.Errorf("object value: got %q", got) | |
| 389 | 389 | } | |
| 390 | 390 | } | |
| 391 | 391 | ||
| 392 | 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 | 396 | func TestRecordedExcludeUnknownKey(t *testing.T) { | |
| 395 | 397 | _, err := ExpandMatrix(ctxbg, jsonValue(t, `{"matrix": {"os": ["one", "two"], "exclude": [{"nokey": "x"}, {"os": "one", "nokey": "y"}]}}`)) | |
| 396 | 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 | 409 | // 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 | 413 | func TestRecordedMatrixOver256(t *testing.T) { | |
| 410 | 414 | w, errs := Parse("ci.yml", []byte(`on: push | |
| 411 | 415 | 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 a | 385 | + | // 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 = 272 | 409 | // 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: push | 414 | 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 { | |||
| 47 | 47 | // - when both a branches and a tags filter exist, a ref is checked against | |
| 48 | 48 | // the filter for its kind; | |
| 49 | 49 | // - 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 | 52 | // - patterns are evaluated in order and the last one that matches wins. | |
| 52 | 53 | // | |
| 53 | 54 | // 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 against | 47 | // - 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 first | 49 | // - 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 ( | |||
| 9 | 9 | ||
| 10 | 10 | // TestRecordedInvalidWorkflows parses the workflow files of the recorded | |
| 11 | 11 | // 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. | |
| 14 | 17 | func TestRecordedInvalidWorkflows(t *testing.T) { | |
| 15 | 18 | const on = "on:\n push:\n branches: ['scn/**']\n" | |
| 16 | 19 | tests := []struct { | |
| @@ -9,8 +9,11 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | // TestRecordedInvalidWorkflows parses the workflow files of the recorded | 10 | // TestRecordedInvalidWorkflows parses the workflow files of the recorded | ||
| 11 | // scenarios expr-status-in-env, expr-status-in-run, invalid-needs and | 11 | // 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 | 12 | + | // 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]" | |||
| 23 | 23 | // trailer, with another value ("false", "yes"), or with any change of case | |
| 24 | 24 | // in the key or the value. | |
| 25 | 25 | // | |
| 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. | |
| 30 | 32 | func SkipCI(headCommitMessage string) bool { | |
| 31 | 33 | for _, m := range skipMarkers { | |
| 32 | 34 | 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 case | 23 | // 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 trailer | 26 | + | // 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, more | 27 | + | // 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 message | 28 | + | // 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) { | |||
| 60 | 60 | {"crlf", "x\r\n\r\n\r\nskip-checks: true\r\n", true}, | |
| 61 | 61 | {"three blank lines", "x\n\n\n\nskip-checks: true", true}, | |
| 62 | 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 | 67 | {"empty", "", false}, | |
| 64 | 68 | } | |
| 65 | 69 | 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) | |||
| 167 | 167 | // already include the matrix and strategy contexts, see [Strategy.MatrixEnv]. | |
| 168 | 168 | // | |
| 169 | 169 | // 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)"). | |
| 172 | 172 | func (j *Job) DisplayName(ctx context.Context, env template.Env, s *Strategy, index int) (string, error) { | |
| 173 | 173 | if index < 0 || index >= len(s.Combos) { | |
| 174 | 174 | 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 custom | 169 | // 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 | 170 | + | // 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)) | ||