test(workflow): replay the recorded tag and branch push filters
- commit
- dcaf61689d6e804e308077152442b2bb3b70a656
- author
- brice · · 2026-10-12 19:14 UTC
- committer
- brice · · 2026-10-18 14:59 UTC
- parent
- 334d109
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +82 -4
M workflow/push.go +5 -4
| @@ -52,10 +52,11 @@ type PushInfo struct { | |||
| 52 | 52 | // | |
| 53 | 53 | // Commit-message directives are separate: see [SkipCI]. | |
| 54 | 54 | // | |
| 55 | - | // TODO(verify with recording): the order in which a branch filter and a | |
| 56 | - | // paths filter combine is "both must pass" (documented); behaviour on refs | |
| 57 | - | // that are neither a branch nor a tag, and on a push that changes no file, is | |
| 58 | - | // assumed. | |
| 55 | + | // Recorded on github.com: push-tags-limit, filter-tags, filter-branches-ignore, | |
| 56 | + | // filter-tag-branches-only, filter-paths-tag, push-delete-branch and | |
| 57 | + | // push-delete-tag, see TestMatchPushRecordedTags. Still assumed: the | |
| 58 | + | // behaviour on refs that are neither a branch nor a tag, and on a push that | |
| 59 | + | // changes no file. | |
| 59 | 60 | func (w *Workflow) MatchPush(p PushInfo) bool { | |
| 60 | 61 | if w == nil || w.On == nil { | |
| 61 | 62 | return false | |
| @@ -52,10 +52,11 @@ type PushInfo struct { | |||||
| 52 | // | 52 | // | ||
| 53 | // Commit-message directives are separate: see [SkipCI]. | 53 | // Commit-message directives are separate: see [SkipCI]. | ||
| 54 | // | 54 | // | ||
| 55 | - | // TODO(verify with recording): the order in which a branch filter and a | 55 | + | // Recorded on github.com: push-tags-limit, filter-tags, filter-branches-ignore, |
| 56 | - | // paths filter combine is "both must pass" (documented); behaviour on refs | 56 | + | // filter-tag-branches-only, filter-paths-tag, push-delete-branch and |
| 57 | - | // that are neither a branch nor a tag, and on a push that changes no file, is | 57 | + | // push-delete-tag, see TestMatchPushRecordedTags. Still assumed: the |
| 58 | - | // assumed. | 58 | + | // behaviour on refs that are neither a branch nor a tag, and on a push that |
| 59 | + | // changes no file. | |||
| 59 | func (w *Workflow) MatchPush(p PushInfo) bool { | 60 | func (w *Workflow) MatchPush(p PushInfo) bool { | ||
| 60 | if w == nil || w.On == nil { | 61 | if w == nil || w.On == nil { | ||
| 61 | return false | 62 | return false | ||
M workflow/push_test.go +77 -0
| @@ -149,6 +149,83 @@ func TestMatchPushFileLimit(t *testing.T) { | |||
| 149 | 149 | } | |
| 150 | 150 | } | |
| 151 | 151 | ||
| 152 | + | // TestMatchPushRecordedTags replays what github.com did in the scenarios | |
| 153 | + | // push-tags-limit, filter-tags, filter-branches-ignore, | |
| 154 | + | // filter-tag-branches-only and filter-paths-tag. The branches are named | |
| 155 | + | // scn/<scenario>/<run> and the tags scn-<scenario>-<run>-<n>. | |
| 156 | + | func TestMatchPushRecordedTags(t *testing.T) { | |
| 157 | + | tag := func(n, count int) PushInfo { | |
| 158 | + | return PushInfo{Ref: fmt.Sprintf("refs/tags/scn-filter-tags-1-%d", n), Created: true, TagCount: count} | |
| 159 | + | } | |
| 160 | + | branch := PushInfo{Ref: "refs/heads/scn/filter-tags/1", Created: true, ChangedFiles: []string{"a.txt"}} | |
| 161 | + | tests := []struct { | |
| 162 | + | scenario string | |
| 163 | + | on string | |
| 164 | + | push PushInfo | |
| 165 | + | want bool | |
| 166 | + | }{ | |
| 167 | + | // push-tags-limit: three tags at once start three runs, four tags none. | |
| 168 | + | {"push-tags-limit 3 tags", "on: push", tag(1, 3), true}, | |
| 169 | + | {"push-tags-limit 3 tags (third)", "on: push", tag(3, 3), true}, | |
| 170 | + | {"push-tags-limit 4 tags (first)", "on: push", tag(1, 4), false}, | |
| 171 | + | {"push-tags-limit 4 tags (last)", "on: push", tag(4, 4), false}, | |
| 172 | + | {"push-tags-limit 4 tags, tags filter", "on:\n push:\n tags: ['scn-*']", tag(2, 4), false}, | |
| 173 | + | ||
| 174 | + | // filter-tags, push 1 (a branch): t-both and t-branches-only ran, the | |
| 175 | + | // tags-only workflows did not. | |
| 176 | + | {"filter-tags branch t-both", "on:\n push:\n branches: ['scn/**']\n tags: ['scn-*']", branch, true}, | |
| 177 | + | {"filter-tags branch t-branches-only", "on:\n push:\n branches: ['scn/**']", branch, true}, | |
| 178 | + | {"filter-tags branch t-match", "on:\n push:\n tags: ['scn-*']", branch, false}, | |
| 179 | + | {"filter-tags branch t-ignore-other", "on:\n push:\n tags-ignore: ['v*']", branch, false}, | |
| 180 | + | {"filter-tags branch t-negated", "on:\n push:\n tags:\n - 'scn-*'\n - '!scn-*-1'", branch, false}, | |
| 181 | + | // filter-tags, push 2 (tag scn-...-1): ran for t-both, t-match, | |
| 182 | + | // t-ignore-other and t-negated-other; not for t-branches-only, t-ignore, | |
| 183 | + | // t-negated and t-nomatch. | |
| 184 | + | {"filter-tags tag t-both", "on:\n push:\n branches: ['scn/**']\n tags: ['scn-*']", tag(1, 1), true}, | |
| 185 | + | {"filter-tags tag t-match", "on:\n push:\n tags: ['scn-*']", tag(1, 1), true}, | |
| 186 | + | {"filter-tags tag t-ignore-other", "on:\n push:\n tags-ignore: ['v*']", tag(1, 1), true}, | |
| 187 | + | {"filter-tags tag t-negated-other", "on:\n push:\n tags:\n - 'scn-*'\n - '!scn-*-2'", tag(1, 1), true}, | |
| 188 | + | {"filter-tags tag t-branches-only", "on:\n push:\n branches: ['scn/**']", tag(1, 1), false}, | |
| 189 | + | {"filter-tags tag t-ignore", "on:\n push:\n tags-ignore: ['scn-*']", tag(1, 1), false}, | |
| 190 | + | {"filter-tags tag t-negated", "on:\n push:\n tags:\n - 'scn-*'\n - '!scn-*-1'", tag(1, 1), false}, | |
| 191 | + | {"filter-tags tag t-nomatch", "on:\n push:\n tags: ['v*']", tag(1, 1), false}, | |
| 192 | + | ||
| 193 | + | // filter-branches-ignore: the branch push ran bi-other and | |
| 194 | + | // bi-single-star, not bi-ignored and not bi-tags-too; the tag push ran | |
| 195 | + | // only bi-tags-too (branches-ignore alone ignores tags). | |
| 196 | + | {"filter-branches-ignore branch bi-ignored", "on:\n push:\n branches-ignore: ['scn/**']", branch, false}, | |
| 197 | + | {"filter-branches-ignore branch bi-other", "on:\n push:\n branches-ignore: ['main', 'release/**']", branch, true}, | |
| 198 | + | {"filter-branches-ignore branch bi-single-star", "on:\n push:\n branches-ignore: ['scn/*']", branch, true}, | |
| 199 | + | {"filter-branches-ignore branch bi-tags-too", "on:\n push:\n branches-ignore: ['scn/**']\n tags: ['scn-*']", branch, false}, | |
| 200 | + | {"filter-branches-ignore tag bi-tags-too", "on:\n push:\n branches-ignore: ['scn/**']\n tags: ['scn-*']", tag(1, 1), true}, | |
| 201 | + | {"filter-branches-ignore tag bi-ignored", "on:\n push:\n branches-ignore: ['scn/**']", tag(1, 1), false}, | |
| 202 | + | {"filter-branches-ignore tag bi-other", "on:\n push:\n branches-ignore: ['main']", tag(1, 1), false}, | |
| 203 | + | ||
| 204 | + | // filter-tag-branches-only: a tag pushed to a repository whose only | |
| 205 | + | // workflow filters on branches starts nothing. | |
| 206 | + | {"filter-tag-branches-only", "on:\n push:\n branches: ['scn/**']", tag(1, 1), false}, | |
| 207 | + | ||
| 208 | + | // filter-paths-tag: paths are not evaluated for tags. | |
| 209 | + | {"filter-paths-tag paths-only", "on:\n push:\n paths: ['nothing/**']", tag(1, 1), true}, | |
| 210 | + | {"filter-paths-tag tag-and-paths-ignore", "on:\n push:\n tags: ['scn-*']\n paths-ignore: ['**']", tag(1, 1), true}, | |
| 211 | + | {"filter-paths-tag tag-and-paths-nomatch", "on:\n push:\n tags: ['scn-*']\n paths: ['nothing/**']", tag(1, 1), true}, | |
| 212 | + | {"filter-paths-tag branches-and-paths", "on:\n push:\n branches: ['scn/**']\n paths: ['nothing/**']", tag(1, 1), false}, | |
| 213 | + | {"filter-paths-tag branches-and-paths, branch", "on:\n push:\n branches: ['scn/**']\n paths: ['nothing/**']", branch, false}, | |
| 214 | + | ||
| 215 | + | // push-delete-branch, push-delete-tag: a deletion starts nothing, | |
| 216 | + | // whatever the filters. | |
| 217 | + | {"push-delete-branch", "on: push", PushInfo{Ref: "refs/heads/scn/push-delete-branch/1", Deleted: true}, false}, | |
| 218 | + | {"push-delete-tag", "on: push", PushInfo{Ref: "refs/tags/scn-push-delete-tag-1-1", Deleted: true, TagCount: 1}, false}, | |
| 219 | + | } | |
| 220 | + | for _, tc := range tests { | |
| 221 | + | t.Run(tc.scenario, func(t *testing.T) { | |
| 222 | + | if got := pushWorkflow(t, tc.on).MatchPush(tc.push); got != tc.want { | |
| 223 | + | t.Errorf("MatchPush = %v, want %v", got, tc.want) | |
| 224 | + | } | |
| 225 | + | }) | |
| 226 | + | } | |
| 227 | + | } | |
| 228 | + | ||
| 152 | 229 | func TestMatchPushNilSafe(t *testing.T) { | |
| 153 | 230 | var w *Workflow | |
| 154 | 231 | if w.MatchPush(PushInfo{Ref: "refs/heads/main"}) { | |
| @@ -149,6 +149,83 @@ func TestMatchPushFileLimit(t *testing.T) { | |||||
| 149 | } | 149 | } | ||
| 150 | } | 150 | } | ||
| 151 | 151 | ||||
| 152 | + | // TestMatchPushRecordedTags replays what github.com did in the scenarios | |||
| 153 | + | // push-tags-limit, filter-tags, filter-branches-ignore, | |||
| 154 | + | // filter-tag-branches-only and filter-paths-tag. The branches are named | |||
| 155 | + | // scn/<scenario>/<run> and the tags scn-<scenario>-<run>-<n>. | |||
| 156 | + | func TestMatchPushRecordedTags(t *testing.T) { | |||
| 157 | + | tag := func(n, count int) PushInfo { | |||
| 158 | + | return PushInfo{Ref: fmt.Sprintf("refs/tags/scn-filter-tags-1-%d", n), Created: true, TagCount: count} | |||
| 159 | + | } | |||
| 160 | + | branch := PushInfo{Ref: "refs/heads/scn/filter-tags/1", Created: true, ChangedFiles: []string{"a.txt"}} | |||
| 161 | + | tests := []struct { | |||
| 162 | + | scenario string | |||
| 163 | + | on string | |||
| 164 | + | push PushInfo | |||
| 165 | + | want bool | |||
| 166 | + | }{ | |||
| 167 | + | // push-tags-limit: three tags at once start three runs, four tags none. | |||
| 168 | + | {"push-tags-limit 3 tags", "on: push", tag(1, 3), true}, | |||
| 169 | + | {"push-tags-limit 3 tags (third)", "on: push", tag(3, 3), true}, | |||
| 170 | + | {"push-tags-limit 4 tags (first)", "on: push", tag(1, 4), false}, | |||
| 171 | + | {"push-tags-limit 4 tags (last)", "on: push", tag(4, 4), false}, | |||
| 172 | + | {"push-tags-limit 4 tags, tags filter", "on:\n push:\n tags: ['scn-*']", tag(2, 4), false}, | |||
| 173 | + | ||||
| 174 | + | // filter-tags, push 1 (a branch): t-both and t-branches-only ran, the | |||
| 175 | + | // tags-only workflows did not. | |||
| 176 | + | {"filter-tags branch t-both", "on:\n push:\n branches: ['scn/**']\n tags: ['scn-*']", branch, true}, | |||
| 177 | + | {"filter-tags branch t-branches-only", "on:\n push:\n branches: ['scn/**']", branch, true}, | |||
| 178 | + | {"filter-tags branch t-match", "on:\n push:\n tags: ['scn-*']", branch, false}, | |||
| 179 | + | {"filter-tags branch t-ignore-other", "on:\n push:\n tags-ignore: ['v*']", branch, false}, | |||
| 180 | + | {"filter-tags branch t-negated", "on:\n push:\n tags:\n - 'scn-*'\n - '!scn-*-1'", branch, false}, | |||
| 181 | + | // filter-tags, push 2 (tag scn-...-1): ran for t-both, t-match, | |||
| 182 | + | // t-ignore-other and t-negated-other; not for t-branches-only, t-ignore, | |||
| 183 | + | // t-negated and t-nomatch. | |||
| 184 | + | {"filter-tags tag t-both", "on:\n push:\n branches: ['scn/**']\n tags: ['scn-*']", tag(1, 1), true}, | |||
| 185 | + | {"filter-tags tag t-match", "on:\n push:\n tags: ['scn-*']", tag(1, 1), true}, | |||
| 186 | + | {"filter-tags tag t-ignore-other", "on:\n push:\n tags-ignore: ['v*']", tag(1, 1), true}, | |||
| 187 | + | {"filter-tags tag t-negated-other", "on:\n push:\n tags:\n - 'scn-*'\n - '!scn-*-2'", tag(1, 1), true}, | |||
| 188 | + | {"filter-tags tag t-branches-only", "on:\n push:\n branches: ['scn/**']", tag(1, 1), false}, | |||
| 189 | + | {"filter-tags tag t-ignore", "on:\n push:\n tags-ignore: ['scn-*']", tag(1, 1), false}, | |||
| 190 | + | {"filter-tags tag t-negated", "on:\n push:\n tags:\n - 'scn-*'\n - '!scn-*-1'", tag(1, 1), false}, | |||
| 191 | + | {"filter-tags tag t-nomatch", "on:\n push:\n tags: ['v*']", tag(1, 1), false}, | |||
| 192 | + | ||||
| 193 | + | // filter-branches-ignore: the branch push ran bi-other and | |||
| 194 | + | // bi-single-star, not bi-ignored and not bi-tags-too; the tag push ran | |||
| 195 | + | // only bi-tags-too (branches-ignore alone ignores tags). | |||
| 196 | + | {"filter-branches-ignore branch bi-ignored", "on:\n push:\n branches-ignore: ['scn/**']", branch, false}, | |||
| 197 | + | {"filter-branches-ignore branch bi-other", "on:\n push:\n branches-ignore: ['main', 'release/**']", branch, true}, | |||
| 198 | + | {"filter-branches-ignore branch bi-single-star", "on:\n push:\n branches-ignore: ['scn/*']", branch, true}, | |||
| 199 | + | {"filter-branches-ignore branch bi-tags-too", "on:\n push:\n branches-ignore: ['scn/**']\n tags: ['scn-*']", branch, false}, | |||
| 200 | + | {"filter-branches-ignore tag bi-tags-too", "on:\n push:\n branches-ignore: ['scn/**']\n tags: ['scn-*']", tag(1, 1), true}, | |||
| 201 | + | {"filter-branches-ignore tag bi-ignored", "on:\n push:\n branches-ignore: ['scn/**']", tag(1, 1), false}, | |||
| 202 | + | {"filter-branches-ignore tag bi-other", "on:\n push:\n branches-ignore: ['main']", tag(1, 1), false}, | |||
| 203 | + | ||||
| 204 | + | // filter-tag-branches-only: a tag pushed to a repository whose only | |||
| 205 | + | // workflow filters on branches starts nothing. | |||
| 206 | + | {"filter-tag-branches-only", "on:\n push:\n branches: ['scn/**']", tag(1, 1), false}, | |||
| 207 | + | ||||
| 208 | + | // filter-paths-tag: paths are not evaluated for tags. | |||
| 209 | + | {"filter-paths-tag paths-only", "on:\n push:\n paths: ['nothing/**']", tag(1, 1), true}, | |||
| 210 | + | {"filter-paths-tag tag-and-paths-ignore", "on:\n push:\n tags: ['scn-*']\n paths-ignore: ['**']", tag(1, 1), true}, | |||
| 211 | + | {"filter-paths-tag tag-and-paths-nomatch", "on:\n push:\n tags: ['scn-*']\n paths: ['nothing/**']", tag(1, 1), true}, | |||
| 212 | + | {"filter-paths-tag branches-and-paths", "on:\n push:\n branches: ['scn/**']\n paths: ['nothing/**']", tag(1, 1), false}, | |||
| 213 | + | {"filter-paths-tag branches-and-paths, branch", "on:\n push:\n branches: ['scn/**']\n paths: ['nothing/**']", branch, false}, | |||
| 214 | + | ||||
| 215 | + | // push-delete-branch, push-delete-tag: a deletion starts nothing, | |||
| 216 | + | // whatever the filters. | |||
| 217 | + | {"push-delete-branch", "on: push", PushInfo{Ref: "refs/heads/scn/push-delete-branch/1", Deleted: true}, false}, | |||
| 218 | + | {"push-delete-tag", "on: push", PushInfo{Ref: "refs/tags/scn-push-delete-tag-1-1", Deleted: true, TagCount: 1}, false}, | |||
| 219 | + | } | |||
| 220 | + | for _, tc := range tests { | |||
| 221 | + | t.Run(tc.scenario, func(t *testing.T) { | |||
| 222 | + | if got := pushWorkflow(t, tc.on).MatchPush(tc.push); got != tc.want { | |||
| 223 | + | t.Errorf("MatchPush = %v, want %v", got, tc.want) | |||
| 224 | + | } | |||
| 225 | + | }) | |||
| 226 | + | } | |||
| 227 | + | } | |||
| 228 | + | ||||
| 152 | func TestMatchPushNilSafe(t *testing.T) { | 229 | func TestMatchPushNilSafe(t *testing.T) { | ||
| 153 | var w *Workflow | 230 | var w *Workflow | ||
| 154 | if w.MatchPush(PushInfo{Ref: "refs/heads/main"}) { | 231 | if w.MatchPush(PushInfo{Ref: "refs/heads/main"}) { | ||