docs(workflow): record what the github.com scenarios settled, replay the glob scenarios
- commit
- 98ea59f9bdc5e6421d8f8ad4f2b2f0ccf20de628
- author
- brice · · 2026-10-12 19:18 UTC
- committer
- brice · · 2026-10-18 14:59 UTC
- parent
- 21505b0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +122 -18
M workflow/PORTING.md +38 -14
| @@ -40,23 +40,45 @@ code. | |||
| 40 | 40 | (`success() && (cond)` when the condition calls none of `success`, | |
| 41 | 41 | `failure`, `cancelled`, `always`, looking through nested calls). | |
| 42 | 42 | ||
| 43 | + | ## Recorded (github.com, ermite-conformance corpus) | |
| 44 | + | ||
| 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 | |
| 50 | + | 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 | |
| 52 | + | 'y' which creates a cycle in the dependency graph.`, `Required property is | |
| 53 | + | 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. | |
| 56 | + | ||
| 43 | 57 | ## To verify (recordings) | |
| 44 | 58 | ||
| 45 | 59 | - [ ] **Glob syntax** (`glob.go`): `*`, `**`, `?`, `+`, `[]`, `\`, `!`. The | |
| 46 | 60 | cheat sheet's examples pass; `**/` matching nothing, `?` and `+` after a | |
| 47 | 61 | `]`, `*` or an escaped character, `[!x]`, an unclosed `[` (literal here) | |
| 48 | 62 | and `/`-prefixed patterns are assumed. | |
| 49 | - | - [ ] **`MatchPush`**: deletes never run; more than 3 tags run nothing for | |
| 50 | - | tags (not for the branch); branches-only filter and a tag push (no run) | |
| 51 | - | and the reverse; with both filters each ref uses its own; `paths` only | |
| 52 | - | for branches; first 300 files (also for `paths-ignore`); truncated list | |
| 53 | - | passes; a push whose diff is empty never matches a `paths` filter and | |
| 54 | - | always matches a `paths-ignore` one; unknown ref kinds never match. | |
| 63 | + | - [x] **`MatchPush`** (recorded: push-tags-limit, filter-tags, | |
| 64 | + | filter-branches-ignore, filter-tag-branches-only, filter-paths-tag, | |
| 65 | + | filter-paths*, push-delete-*): deletes never run; more than 3 tags run | |
| 66 | + | nothing for tags (not for the branch); branches-only filter and a tag | |
| 67 | + | push (no run) and the reverse; with both filters each ref uses its own; | |
| 68 | + | `paths` only for branches; the first **3000** files, also for | |
| 69 | + | `paths-ignore` (not 300). Still assumed: a push whose diff is empty | |
| 70 | + | never matches a `paths` filter and always matches a `paths-ignore` one; | |
| 71 | + | unknown ref kinds never match; truncated list passes. | |
| 55 | 72 | - [ ] **`PushInfo.Created`** is not used: a new branch's file list is the | |
| 56 | 73 | caller's job (diff against the default branch merge-base). | |
| 57 | - | - [ ] **`SkipCI`**: marker case, trailer rules (see its doc comment); whether | |
| 58 | - | only the head commit counts; `[skip ci]` in the subject vs the body. | |
| 59 | - | - [ ] **`ExpandMatrix`**: order of the product (first key slowest), include | |
| 74 | + | - [x] **`SkipCI`** (recorded: skip-directives, skip-directives-placement, | |
| 75 | + | skip-checks-trailer, skip-directive-tag): markers are case-sensitive, | |
| 76 | + | 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 | |
| 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 | |
| 60 | 82 | semantics (added to every matching combination, never overwriting | |
| 61 | 83 | original values, later includes overwrite earlier added values, a | |
| 62 | 84 | non-matching include is a new combination, match by vector keys only), | |
| @@ -64,18 +86,20 @@ code. | |||
| 64 | 86 | combinations after include, key case-insensitivity, value comparison | |
| 65 | 87 | (same kind; objects by containment), the error when all combinations are | |
| 66 | 88 | excluded (GitHub may fail the run differently), empty vector error. | |
| 67 | - | - [ ] A **null matrix value** is shown as an empty segment (`job (, 1)`); | |
| 68 | - | GitHub may skip it. | |
| 69 | - | - [ ] **Job display names** (`MatrixCombo.DisplayName`, `Job.DisplayName`): | |
| 89 | + | - [x] A **null or empty matrix value** is not shown in the job name | |
| 90 | + | (recorded: matrix-edge-values). | |
| 91 | + | - [x] **Job display names** (`MatrixCombo.DisplayName`, `Job.DisplayName`; | |
| 92 | + | recorded: matrix-job-name, matrix-edge-values): | |
| 70 | 93 | `name (v1, v2)` listing every scalar value of the combination in key | |
| 71 | 94 | order, then keys added by include; a name that reads the `matrix` | |
| 72 | 95 | context (found in the expression's syntax tree, not by searching the | |
| 73 | - | text) is used as is. Booleans/numbers/null are shown with `CoerceString`. | |
| 96 | + | text) is used as is. Booleans and numbers are shown with `CoerceString`; null, `''`, arrays | |
| 97 | + | and objects are left out. | |
| 74 | 98 | - [ ] **`strategy` context** (`Strategy.Context`): `fail-fast`, `job-index`, | |
| 75 | 99 | `job-total`, `max-parallel` (the job count when unlimited). | |
| 76 | 100 | - [ ] `fail-fast` and `max-parallel` accept strings (`"false"`, `"2"`) after | |
| 77 | 101 | evaluation; `max-parallel` must be a whole number ≥ 1. | |
| 78 | - | - [ ] The 1 000-commit and 300-file limits themselves are the caller's. | |
| 102 | + | - [ ] The 1 000-commit and 3 000-file limits themselves are the caller's. | |
| 79 | 103 | ||
| 80 | 104 | ## New in the second pass | |
| 81 | 105 | ||
| @@ -40,23 +40,45 @@ code. | |||||
| 40 | (`success() && (cond)` when the condition calls none of `success`, | 40 | (`success() && (cond)` when the condition calls none of `success`, | ||
| 41 | `failure`, `cancelled`, `always`, looking through nested calls). | 41 | `failure`, `cancelled`, `always`, looking through nested calls). | ||
| 42 | 42 | ||||
| 43 | + | ## Recorded (github.com, ermite-conformance corpus) | |||
| 44 | + | ||||
| 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 | |||
| 50 | + | 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 | |||
| 52 | + | 'y' which creates a cycle in the dependency graph.`, `Required property is | |||
| 53 | + | 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. | |||
| 56 | + | ||||
| 43 | ## To verify (recordings) | 57 | ## To verify (recordings) | ||
| 44 | 58 | ||||
| 45 | - [ ] **Glob syntax** (`glob.go`): `*`, `**`, `?`, `+`, `[]`, `\`, `!`. The | 59 | - [ ] **Glob syntax** (`glob.go`): `*`, `**`, `?`, `+`, `[]`, `\`, `!`. The | ||
| 46 | cheat sheet's examples pass; `**/` matching nothing, `?` and `+` after a | 60 | cheat sheet's examples pass; `**/` matching nothing, `?` and `+` after a | ||
| 47 | `]`, `*` or an escaped character, `[!x]`, an unclosed `[` (literal here) | 61 | `]`, `*` or an escaped character, `[!x]`, an unclosed `[` (literal here) | ||
| 48 | and `/`-prefixed patterns are assumed. | 62 | and `/`-prefixed patterns are assumed. | ||
| 49 | - | - [ ] **`MatchPush`**: deletes never run; more than 3 tags run nothing for | 63 | + | - [x] **`MatchPush`** (recorded: push-tags-limit, filter-tags, |
| 50 | - | tags (not for the branch); branches-only filter and a tag push (no run) | 64 | + | filter-branches-ignore, filter-tag-branches-only, filter-paths-tag, |
| 51 | - | and the reverse; with both filters each ref uses its own; `paths` only | 65 | + | filter-paths*, push-delete-*): deletes never run; more than 3 tags run |
| 52 | - | for branches; first 300 files (also for `paths-ignore`); truncated list | 66 | + | nothing for tags (not for the branch); branches-only filter and a tag |
| 53 | - | passes; a push whose diff is empty never matches a `paths` filter and | 67 | + | push (no run) and the reverse; with both filters each ref uses its own; |
| 54 | - | always matches a `paths-ignore` one; unknown ref kinds never match. | 68 | + | `paths` only for branches; the first **3000** files, also for |
| 69 | + | `paths-ignore` (not 300). Still assumed: a push whose diff is empty | |||
| 70 | + | never matches a `paths` filter and always matches a `paths-ignore` one; | |||
| 71 | + | unknown ref kinds never match; truncated list passes. | |||
| 55 | - [ ] **`PushInfo.Created`** is not used: a new branch's file list is the | 72 | - [ ] **`PushInfo.Created`** is not used: a new branch's file list is the | ||
| 56 | caller's job (diff against the default branch merge-base). | 73 | caller's job (diff against the default branch merge-base). | ||
| 57 | - | - [ ] **`SkipCI`**: marker case, trailer rules (see its doc comment); whether | 74 | + | - [x] **`SkipCI`** (recorded: skip-directives, skip-directives-placement, |
| 58 | - | only the head commit counts; `[skip ci]` in the subject vs the body. | 75 | + | skip-checks-trailer, skip-directive-tag): markers are case-sensitive, |
| 59 | - | - [ ] **`ExpandMatrix`**: order of the product (first key slowest), include | 76 | + | 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 | |||
| 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 | |||
| 60 | semantics (added to every matching combination, never overwriting | 82 | semantics (added to every matching combination, never overwriting | ||
| 61 | original values, later includes overwrite earlier added values, a | 83 | original values, later includes overwrite earlier added values, a | ||
| 62 | non-matching include is a new combination, match by vector keys only), | 84 | non-matching include is a new combination, match by vector keys only), | ||
| @@ -64,18 +86,20 @@ code. | |||||
| 64 | combinations after include, key case-insensitivity, value comparison | 86 | combinations after include, key case-insensitivity, value comparison | ||
| 65 | (same kind; objects by containment), the error when all combinations are | 87 | (same kind; objects by containment), the error when all combinations are | ||
| 66 | excluded (GitHub may fail the run differently), empty vector error. | 88 | excluded (GitHub may fail the run differently), empty vector error. | ||
| 67 | - | - [ ] A **null matrix value** is shown as an empty segment (`job (, 1)`); | 89 | + | - [x] A **null or empty matrix value** is not shown in the job name |
| 68 | - | GitHub may skip it. | 90 | + | (recorded: matrix-edge-values). |
| 69 | - | - [ ] **Job display names** (`MatrixCombo.DisplayName`, `Job.DisplayName`): | 91 | + | - [x] **Job display names** (`MatrixCombo.DisplayName`, `Job.DisplayName`; |
| 92 | + | recorded: matrix-job-name, matrix-edge-values): | |||
| 70 | `name (v1, v2)` listing every scalar value of the combination in key | 93 | `name (v1, v2)` listing every scalar value of the combination in key | ||
| 71 | order, then keys added by include; a name that reads the `matrix` | 94 | order, then keys added by include; a name that reads the `matrix` | ||
| 72 | context (found in the expression's syntax tree, not by searching the | 95 | context (found in the expression's syntax tree, not by searching the | ||
| 73 | - | text) is used as is. Booleans/numbers/null are shown with `CoerceString`. | 96 | + | text) is used as is. Booleans and numbers are shown with `CoerceString`; null, `''`, arrays |
| 97 | + | and objects are left out. | |||
| 74 | - [ ] **`strategy` context** (`Strategy.Context`): `fail-fast`, `job-index`, | 98 | - [ ] **`strategy` context** (`Strategy.Context`): `fail-fast`, `job-index`, | ||
| 75 | `job-total`, `max-parallel` (the job count when unlimited). | 99 | `job-total`, `max-parallel` (the job count when unlimited). | ||
| 76 | - [ ] `fail-fast` and `max-parallel` accept strings (`"false"`, `"2"`) after | 100 | - [ ] `fail-fast` and `max-parallel` accept strings (`"false"`, `"2"`) after | ||
| 77 | evaluation; `max-parallel` must be a whole number ≥ 1. | 101 | evaluation; `max-parallel` must be a whole number ≥ 1. | ||
| 78 | - | - [ ] The 1 000-commit and 300-file limits themselves are the caller's. | 102 | + | - [ ] The 1 000-commit and 3 000-file limits themselves are the caller's. |
| 79 | 103 | ||||
| 80 | ## New in the second pass | 104 | ## New in the second pass | ||
| 81 | 105 | ||||
M workflow/glob.go +5 -4
| @@ -24,10 +24,11 @@ import ( | |||
| 24 | 24 | // Every other character, "." included, stands for itself. Matching is | |
| 25 | 25 | // case-sensitive and covers the whole name. | |
| 26 | 26 | // | |
| 27 | - | // TODO(verify with recording): the exact GitHub implementation is not | |
| 28 | - | // public. The cheat sheet fixes the common cases; "**/" matching nothing, | |
| 29 | - | // "\" escapes and how ? and + behave after a wildcard or a bracket are | |
| 30 | - | // reconstructed. | |
| 27 | + | // Recorded on github.com (filter-branches, filter-paths, filter-paths-ignore, | |
| 28 | + | // see TestMatchPushRecordedGlobs): "*", "**", "scn/filter-*/**", "**.md", | |
| 29 | + | // "*.md" and negation order. TODO(verify with recording): "**/" matching | |
| 30 | + | // nothing, "\" escapes and how ? and + behave after a wildcard or a bracket | |
| 31 | + | // are reconstructed. | |
| 31 | 32 | type Pattern struct { | |
| 32 | 33 | // Negated is set for a pattern that started with "!". | |
| 33 | 34 | Negated bool | |
| @@ -24,10 +24,11 @@ import ( | |||||
| 24 | // Every other character, "." included, stands for itself. Matching is | 24 | // Every other character, "." included, stands for itself. Matching is | ||
| 25 | // case-sensitive and covers the whole name. | 25 | // case-sensitive and covers the whole name. | ||
| 26 | // | 26 | // | ||
| 27 | - | // TODO(verify with recording): the exact GitHub implementation is not | 27 | + | // Recorded on github.com (filter-branches, filter-paths, filter-paths-ignore, |
| 28 | - | // public. The cheat sheet fixes the common cases; "**/" matching nothing, | 28 | + | // see TestMatchPushRecordedGlobs): "*", "**", "scn/filter-*/**", "**.md", |
| 29 | - | // "\" escapes and how ? and + behave after a wildcard or a bracket are | 29 | + | // "*.md" and negation order. TODO(verify with recording): "**/" matching |
| 30 | - | // reconstructed. | 30 | + | // nothing, "\" escapes and how ? and + behave after a wildcard or a bracket |
| 31 | + | // are reconstructed. | |||
| 31 | type Pattern struct { | 32 | type Pattern struct { | ||
| 32 | // Negated is set for a pattern that started with "!". | 33 | // Negated is set for a pattern that started with "!". | ||
| 33 | Negated bool | 34 | Negated bool | ||
M workflow/push_test.go +79 -0
| @@ -226,6 +226,85 @@ func TestMatchPushRecordedTags(t *testing.T) { | |||
| 226 | 226 | } | |
| 227 | 227 | } | |
| 228 | 228 | ||
| 229 | + | // TestMatchPushRecordedGlobs replays filter-branches (branch | |
| 230 | + | // scn/filter-branches/<run>: ran for b-match, b-negated-then-readded, | |
| 231 | + | // b-prefix-only, b-star-star and b-two-levels; not for b-negated, b-one-level | |
| 232 | + | // and b-other), filter-paths and filter-paths-ignore. | |
| 233 | + | func TestMatchPushRecordedGlobs(t *testing.T) { | |
| 234 | + | br := PushInfo{Ref: "refs/heads/scn/filter-branches/1"} | |
| 235 | + | for _, tc := range []struct { | |
| 236 | + | name, filter string | |
| 237 | + | want bool | |
| 238 | + | }{ | |
| 239 | + | {"b-match", "branches: ['scn/filter-branches/**']", true}, | |
| 240 | + | {"b-negated-then-readded", "branches: ['**', '!scn/**', 'scn/filter-branches/**']", true}, | |
| 241 | + | {"b-prefix-only", "branches: ['scn/filter-*/**']", true}, | |
| 242 | + | {"b-star-star", "branches: ['**']", true}, | |
| 243 | + | {"b-two-levels", "branches: ['scn/*/*']", true}, | |
| 244 | + | {"b-negated", "branches: ['scn/**', '!scn/filter-branches/**']", false}, | |
| 245 | + | {"b-one-level", "branches: ['scn/*']", false}, | |
| 246 | + | {"b-other", "branches: [main, 'release/**']", false}, | |
| 247 | + | } { | |
| 248 | + | if got := pushWorkflow(t, "on:\n push:\n "+tc.filter).MatchPush(br); got != tc.want { | |
| 249 | + | t.Errorf("filter-branches %s: %v, want %v", tc.name, got, tc.want) | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | // filter-paths: the files of each push. | |
| 254 | + | pushes := [][]string{ | |
| 255 | + | {".github/workflows/p-docs.yml", "docs/readme.md", "other/x.txt", "src/a.txt", "top.md"}, // 1: the workflow files are new too | |
| 256 | + | {"docs/readme.md"}, // 2 | |
| 257 | + | {"src/a.txt"}, // 3 | |
| 258 | + | {"top.md"}, // 4 | |
| 259 | + | {"docs/new.md", "other/x.txt"}, // 5 | |
| 260 | + | } | |
| 261 | + | for _, tc := range []struct { | |
| 262 | + | name, filter string | |
| 263 | + | ran [5]bool | |
| 264 | + | }{ | |
| 265 | + | {"p-docs", "paths: ['docs/**']", [5]bool{true, true, false, false, true}}, | |
| 266 | + | {"p-double-star-md", "paths: ['**.md']", [5]bool{true, true, false, true, true}}, | |
| 267 | + | {"p-exact-file", "paths: ['src/a.txt']", [5]bool{true, false, true, false, false}}, | |
| 268 | + | {"p-negated", "paths: ['**', '!docs/**']", [5]bool{true, false, true, true, true}}, | |
| 269 | + | {"p-no-match", "paths: ['nothing/**']", [5]bool{}}, | |
| 270 | + | {"p-single-star-md", "paths: ['*.md']", [5]bool{true, false, false, true, false}}, | |
| 271 | + | {"p-src", "paths: ['src/**']", [5]bool{true, false, true, false, false}}, | |
| 272 | + | } { | |
| 273 | + | w := pushWorkflow(t, "on:\n push:\n branches: ['scn/**']\n "+tc.filter) | |
| 274 | + | for n, files := range pushes { | |
| 275 | + | got := w.MatchPush(PushInfo{Ref: "refs/heads/scn/filter-paths/1", ChangedFiles: files}) | |
| 276 | + | if got != tc.ran[n] { | |
| 277 | + | t.Errorf("filter-paths %s push %d: %v, want %v", tc.name, n+1, got, tc.ran[n]) | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | // filter-paths-ignore: pushes 1 to 4 changed these files. | |
| 283 | + | ignorePushes := [][]string{ | |
| 284 | + | {".github/workflows/pi-docs.yml", "docs/readme.md", "src/a.txt"}, // 1: the workflow files are new too | |
| 285 | + | {"docs/readme.md"}, // 2 | |
| 286 | + | {"docs/readme.md", "src/a.txt"}, // 3 | |
| 287 | + | {"src/a.txt"}, // 4 | |
| 288 | + | } | |
| 289 | + | for _, tc := range []struct { | |
| 290 | + | name, filter string | |
| 291 | + | ran [4]bool | |
| 292 | + | }{ | |
| 293 | + | {"pi-docs", "paths-ignore: ['docs/**']", [4]bool{true, false, true, true}}, | |
| 294 | + | {"pi-md", "paths-ignore: ['**.md']", [4]bool{true, false, true, true}}, | |
| 295 | + | {"pi-src-docs", "paths-ignore: ['docs/**', 'src/**']", [4]bool{true, false, false, false}}, | |
| 296 | + | {"pi-everything", "paths-ignore: ['**']", [4]bool{}}, | |
| 297 | + | } { | |
| 298 | + | w := pushWorkflow(t, "on:\n push:\n branches: ['scn/**']\n "+tc.filter) | |
| 299 | + | for n, files := range ignorePushes { | |
| 300 | + | got := w.MatchPush(PushInfo{Ref: "refs/heads/scn/filter-paths-ignore/1", ChangedFiles: files}) | |
| 301 | + | if got != tc.ran[n] { | |
| 302 | + | t.Errorf("filter-paths-ignore %s push %d: %v, want %v", tc.name, n+1, got, tc.ran[n]) | |
| 303 | + | } | |
| 304 | + | } | |
| 305 | + | } | |
| 306 | + | } | |
| 307 | + | ||
| 229 | 308 | func TestMatchPushNilSafe(t *testing.T) { | |
| 230 | 309 | var w *Workflow | |
| 231 | 310 | if w.MatchPush(PushInfo{Ref: "refs/heads/main"}) { | |
| @@ -226,6 +226,85 @@ func TestMatchPushRecordedTags(t *testing.T) { | |||||
| 226 | } | 226 | } | ||
| 227 | } | 227 | } | ||
| 228 | 228 | ||||
| 229 | + | // TestMatchPushRecordedGlobs replays filter-branches (branch | |||
| 230 | + | // scn/filter-branches/<run>: ran for b-match, b-negated-then-readded, | |||
| 231 | + | // b-prefix-only, b-star-star and b-two-levels; not for b-negated, b-one-level | |||
| 232 | + | // and b-other), filter-paths and filter-paths-ignore. | |||
| 233 | + | func TestMatchPushRecordedGlobs(t *testing.T) { | |||
| 234 | + | br := PushInfo{Ref: "refs/heads/scn/filter-branches/1"} | |||
| 235 | + | for _, tc := range []struct { | |||
| 236 | + | name, filter string | |||
| 237 | + | want bool | |||
| 238 | + | }{ | |||
| 239 | + | {"b-match", "branches: ['scn/filter-branches/**']", true}, | |||
| 240 | + | {"b-negated-then-readded", "branches: ['**', '!scn/**', 'scn/filter-branches/**']", true}, | |||
| 241 | + | {"b-prefix-only", "branches: ['scn/filter-*/**']", true}, | |||
| 242 | + | {"b-star-star", "branches: ['**']", true}, | |||
| 243 | + | {"b-two-levels", "branches: ['scn/*/*']", true}, | |||
| 244 | + | {"b-negated", "branches: ['scn/**', '!scn/filter-branches/**']", false}, | |||
| 245 | + | {"b-one-level", "branches: ['scn/*']", false}, | |||
| 246 | + | {"b-other", "branches: [main, 'release/**']", false}, | |||
| 247 | + | } { | |||
| 248 | + | if got := pushWorkflow(t, "on:\n push:\n "+tc.filter).MatchPush(br); got != tc.want { | |||
| 249 | + | t.Errorf("filter-branches %s: %v, want %v", tc.name, got, tc.want) | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | // filter-paths: the files of each push. | |||
| 254 | + | pushes := [][]string{ | |||
| 255 | + | {".github/workflows/p-docs.yml", "docs/readme.md", "other/x.txt", "src/a.txt", "top.md"}, // 1: the workflow files are new too | |||
| 256 | + | {"docs/readme.md"}, // 2 | |||
| 257 | + | {"src/a.txt"}, // 3 | |||
| 258 | + | {"top.md"}, // 4 | |||
| 259 | + | {"docs/new.md", "other/x.txt"}, // 5 | |||
| 260 | + | } | |||
| 261 | + | for _, tc := range []struct { | |||
| 262 | + | name, filter string | |||
| 263 | + | ran [5]bool | |||
| 264 | + | }{ | |||
| 265 | + | {"p-docs", "paths: ['docs/**']", [5]bool{true, true, false, false, true}}, | |||
| 266 | + | {"p-double-star-md", "paths: ['**.md']", [5]bool{true, true, false, true, true}}, | |||
| 267 | + | {"p-exact-file", "paths: ['src/a.txt']", [5]bool{true, false, true, false, false}}, | |||
| 268 | + | {"p-negated", "paths: ['**', '!docs/**']", [5]bool{true, false, true, true, true}}, | |||
| 269 | + | {"p-no-match", "paths: ['nothing/**']", [5]bool{}}, | |||
| 270 | + | {"p-single-star-md", "paths: ['*.md']", [5]bool{true, false, false, true, false}}, | |||
| 271 | + | {"p-src", "paths: ['src/**']", [5]bool{true, false, true, false, false}}, | |||
| 272 | + | } { | |||
| 273 | + | w := pushWorkflow(t, "on:\n push:\n branches: ['scn/**']\n "+tc.filter) | |||
| 274 | + | for n, files := range pushes { | |||
| 275 | + | got := w.MatchPush(PushInfo{Ref: "refs/heads/scn/filter-paths/1", ChangedFiles: files}) | |||
| 276 | + | if got != tc.ran[n] { | |||
| 277 | + | t.Errorf("filter-paths %s push %d: %v, want %v", tc.name, n+1, got, tc.ran[n]) | |||
| 278 | + | } | |||
| 279 | + | } | |||
| 280 | + | } | |||
| 281 | + | ||||
| 282 | + | // filter-paths-ignore: pushes 1 to 4 changed these files. | |||
| 283 | + | ignorePushes := [][]string{ | |||
| 284 | + | {".github/workflows/pi-docs.yml", "docs/readme.md", "src/a.txt"}, // 1: the workflow files are new too | |||
| 285 | + | {"docs/readme.md"}, // 2 | |||
| 286 | + | {"docs/readme.md", "src/a.txt"}, // 3 | |||
| 287 | + | {"src/a.txt"}, // 4 | |||
| 288 | + | } | |||
| 289 | + | for _, tc := range []struct { | |||
| 290 | + | name, filter string | |||
| 291 | + | ran [4]bool | |||
| 292 | + | }{ | |||
| 293 | + | {"pi-docs", "paths-ignore: ['docs/**']", [4]bool{true, false, true, true}}, | |||
| 294 | + | {"pi-md", "paths-ignore: ['**.md']", [4]bool{true, false, true, true}}, | |||
| 295 | + | {"pi-src-docs", "paths-ignore: ['docs/**', 'src/**']", [4]bool{true, false, false, false}}, | |||
| 296 | + | {"pi-everything", "paths-ignore: ['**']", [4]bool{}}, | |||
| 297 | + | } { | |||
| 298 | + | w := pushWorkflow(t, "on:\n push:\n branches: ['scn/**']\n "+tc.filter) | |||
| 299 | + | for n, files := range ignorePushes { | |||
| 300 | + | got := w.MatchPush(PushInfo{Ref: "refs/heads/scn/filter-paths-ignore/1", ChangedFiles: files}) | |||
| 301 | + | if got != tc.ran[n] { | |||
| 302 | + | t.Errorf("filter-paths-ignore %s push %d: %v, want %v", tc.name, n+1, got, tc.ran[n]) | |||
| 303 | + | } | |||
| 304 | + | } | |||
| 305 | + | } | |||
| 306 | + | } | |||
| 307 | + | ||||
| 229 | func TestMatchPushNilSafe(t *testing.T) { | 308 | func TestMatchPushNilSafe(t *testing.T) { | ||
| 230 | var w *Workflow | 309 | var w *Workflow | ||
| 231 | if w.MatchPush(PushInfo{Ref: "refs/heads/main"}) { | 310 | if w.MatchPush(PushInfo{Ref: "refs/heads/main"}) { | ||