detached dcaf616

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +82 -4
M workflow/push.go +5 -4
@@ -52,10 +52,11 @@ type PushInfo struct {
5252 //
5353 // Commit-message directives are separate: see [SkipCI].
5454 //
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.
5960 func (w *Workflow) MatchPush(p PushInfo) bool {
6061 if w == nil || w.On == nil {
6162 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 a55+// Recorded on github.com: push-tags-limit, filter-tags, filter-branches-ignore,
56-// paths filter combine is "both must pass" (documented); behaviour on refs56+// 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, is57+// 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 false62 return false
M workflow/push_test.go +77 -0
@@ -149,6 +149,83 @@ func TestMatchPushFileLimit(t *testing.T) {
149149 }
150150 }
151151
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+
152229 func TestMatchPushNilSafe(t *testing.T) {
153230 var w *Workflow
154231 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 *Workflow230 var w *Workflow
154 if w.MatchPush(PushInfo{Ref: "refs/heads/main"}) {231 if w.MatchPush(PushInfo{Ref: "refs/heads/main"}) {

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