fix(workflow): skip directives are case-sensitive and skip-checks needs two blank lines
- commit
- ccaa3bbc39b50b68a6db6451b570ccb8680d4319
- author
- brice · · 2026-10-12 19:14 UTC
- committer
- brice · · 2026-10-18 14:59 UTC
- parent
- dcaf616
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +86 -67
M workflow/skip.go +31 -35
| @@ -6,53 +6,49 @@ import "strings" | |||
| 6 | 6 | ||
| 7 | 7 | var skipMarkers = []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"} | |
| 8 | 8 | ||
| 9 | - | // SkipCI reports whether the message of the head commit of a push tells | |
| 10 | - | // GitHub not to run workflows for it: it contains [skip ci], [ci skip], | |
| 11 | - | // [no ci], [skip actions] or [actions skip], or it ends with a | |
| 12 | - | // "skip-checks: true" (or "skip-checks:true") trailer in its last paragraph. | |
| 9 | + | // SkipCI reports whether the message of the head commit of a push (or of the | |
| 10 | + | // commit a tag points at) tells GitHub not to run workflows for it. As | |
| 11 | + | // recorded on github.com (scenarios skip-directives, skip-directives-placement, | |
| 12 | + | // skip-checks-trailer and skip-directive-tag): | |
| 13 | 13 | // | |
| 14 | - | // TODO(verify with recording): the following is assumed. Markers are matched | |
| 15 | - | // without regard to case, anywhere in the message, subject or body. GitHub | |
| 16 | - | // documents the trailer as "two empty lines followed by skip-checks: true"; | |
| 17 | - | // here one blank line is enough, the trailer must be in the last paragraph | |
| 18 | - | // and not the first one, its key and value are case-insensitive, and other | |
| 19 | - | // trailers may share its paragraph. Whether GitHub looks at the head commit | |
| 20 | - | // only, or at every commit of the push, is not settled by its documentation. | |
| 14 | + | // - the markers [skip ci], [ci skip], [no ci], [skip actions] and | |
| 15 | + | // [actions skip] skip anywhere in the subject or the body, and are | |
| 16 | + | // case-sensitive: [SKIP CI] and [Skip Ci] do not skip. Near misses do | |
| 17 | + | // not skip either: [skip-ci], "skip ci" without brackets, [skip ci please] | |
| 18 | + | // and the markers ***NO_CI*** and ***SKIP_CI***; | |
| 19 | + | // - the trailer "skip-checks: true" skips only as the end of the message, | |
| 20 | + | // preceded by two blank lines ("x\n\n\nskip-checks: true"). The space | |
| 21 | + | // after the colon is optional and trailing white space is ignored. It does | |
| 22 | + | // not skip with one blank line or none, with text after it, after another | |
| 23 | + | // trailer, with another value ("false", "yes"), or with any change of case | |
| 24 | + | // in the key or the value. | |
| 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. | |
| 21 | 30 | func SkipCI(headCommitMessage string) bool { | |
| 22 | - | lower := strings.ToLower(headCommitMessage) | |
| 23 | 31 | for _, m := range skipMarkers { | |
| 24 | - | if strings.Contains(lower, m) { | |
| 32 | + | if strings.Contains(headCommitMessage, m) { | |
| 25 | 33 | return true | |
| 26 | 34 | } | |
| 27 | 35 | } | |
| 28 | 36 | return hasSkipChecksTrailer(headCommitMessage) | |
| 29 | 37 | } | |
| 30 | 38 | ||
| 39 | + | // hasSkipChecksTrailer reports whether the message ends with a | |
| 40 | + | // "skip-checks: true" line preceded by two empty lines and some text. | |
| 31 | 41 | func hasSkipChecksTrailer(msg string) bool { | |
| 32 | 42 | msg = strings.ReplaceAll(msg, "\r\n", "\n") | |
| 33 | - | var paras [][]string | |
| 34 | - | var cur []string | |
| 35 | - | for _, line := range strings.Split(msg, "\n") { | |
| 36 | - | if strings.TrimSpace(line) == "" { | |
| 37 | - | if len(cur) > 0 { | |
| 38 | - | paras = append(paras, cur) | |
| 39 | - | cur = nil | |
| 40 | - | } | |
| 41 | - | continue | |
| 42 | - | } | |
| 43 | - | cur = append(cur, line) | |
| 44 | - | } | |
| 45 | - | if len(cur) > 0 { | |
| 46 | - | paras = append(paras, cur) | |
| 47 | - | } | |
| 48 | - | if len(paras) < 2 { | |
| 43 | + | msg = strings.TrimRight(msg, " \t\n") | |
| 44 | + | lines := strings.Split(msg, "\n") | |
| 45 | + | n := len(lines) | |
| 46 | + | if n < 4 { | |
| 49 | 47 | return false | |
| 50 | 48 | } | |
| 51 | - | for _, line := range paras[len(paras)-1] { | |
| 52 | - | key, val, ok := strings.Cut(line, ":") | |
| 53 | - | if ok && strings.EqualFold(strings.TrimSpace(key), "skip-checks") && strings.EqualFold(strings.TrimSpace(val), "true") { | |
| 54 | - | return true | |
| 55 | - | } | |
| 49 | + | val, ok := strings.CutPrefix(lines[n-1], "skip-checks:") | |
| 50 | + | if !ok || strings.TrimPrefix(val, " ") != "true" { | |
| 51 | + | return false | |
| 56 | 52 | } | |
| 57 | - | return false | |
| 53 | + | return lines[n-2] == "" && lines[n-3] == "" && strings.TrimSpace(strings.Join(lines[:n-3], "")) != "" | |
| 58 | 54 | } | |
| @@ -6,53 +6,49 @@ import "strings" | |||||
| 6 | 6 | ||||
| 7 | var skipMarkers = []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"} | 7 | var skipMarkers = []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"} | ||
| 8 | 8 | ||||
| 9 | - | // SkipCI reports whether the message of the head commit of a push tells | 9 | + | // SkipCI reports whether the message of the head commit of a push (or of the |
| 10 | - | // GitHub not to run workflows for it: it contains [skip ci], [ci skip], | 10 | + | // commit a tag points at) tells GitHub not to run workflows for it. As |
| 11 | - | // [no ci], [skip actions] or [actions skip], or it ends with a | 11 | + | // recorded on github.com (scenarios skip-directives, skip-directives-placement, |
| 12 | - | // "skip-checks: true" (or "skip-checks:true") trailer in its last paragraph. | 12 | + | // skip-checks-trailer and skip-directive-tag): |
| 13 | // | 13 | // | ||
| 14 | - | // TODO(verify with recording): the following is assumed. Markers are matched | 14 | + | // - the markers [skip ci], [ci skip], [no ci], [skip actions] and |
| 15 | - | // without regard to case, anywhere in the message, subject or body. GitHub | 15 | + | // [actions skip] skip anywhere in the subject or the body, and are |
| 16 | - | // documents the trailer as "two empty lines followed by skip-checks: true"; | 16 | + | // case-sensitive: [SKIP CI] and [Skip Ci] do not skip. Near misses do |
| 17 | - | // here one blank line is enough, the trailer must be in the last paragraph | 17 | + | // not skip either: [skip-ci], "skip ci" without brackets, [skip ci please] |
| 18 | - | // and not the first one, its key and value are case-insensitive, and other | 18 | + | // and the markers ***NO_CI*** and ***SKIP_CI***; |
| 19 | - | // trailers may share its paragraph. Whether GitHub looks at the head commit | 19 | + | // - the trailer "skip-checks: true" skips only as the end of the message, |
| 20 | - | // only, or at every commit of the push, is not settled by its documentation. | 20 | + | // preceded by two blank lines ("x\n\n\nskip-checks: true"). The space |
| 21 | + | // after the colon is optional and trailing white space is ignored. It does | |||
| 22 | + | // not skip with one blank line or none, with text after it, after another | |||
| 23 | + | // trailer, with another value ("false", "yes"), or with any change of case | |||
| 24 | + | // in the key or the value. | |||
| 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. | |||
| 21 | func SkipCI(headCommitMessage string) bool { | 30 | func SkipCI(headCommitMessage string) bool { | ||
| 22 | - | lower := strings.ToLower(headCommitMessage) | |||
| 23 | for _, m := range skipMarkers { | 31 | for _, m := range skipMarkers { | ||
| 24 | - | if strings.Contains(lower, m) { | 32 | + | if strings.Contains(headCommitMessage, m) { |
| 25 | return true | 33 | return true | ||
| 26 | } | 34 | } | ||
| 27 | } | 35 | } | ||
| 28 | return hasSkipChecksTrailer(headCommitMessage) | 36 | return hasSkipChecksTrailer(headCommitMessage) | ||
| 29 | } | 37 | } | ||
| 30 | 38 | ||||
| 39 | + | // hasSkipChecksTrailer reports whether the message ends with a | |||
| 40 | + | // "skip-checks: true" line preceded by two empty lines and some text. | |||
| 31 | func hasSkipChecksTrailer(msg string) bool { | 41 | func hasSkipChecksTrailer(msg string) bool { | ||
| 32 | msg = strings.ReplaceAll(msg, "\r\n", "\n") | 42 | msg = strings.ReplaceAll(msg, "\r\n", "\n") | ||
| 33 | - | var paras [][]string | 43 | + | msg = strings.TrimRight(msg, " \t\n") |
| 34 | - | var cur []string | 44 | + | lines := strings.Split(msg, "\n") |
| 35 | - | for _, line := range strings.Split(msg, "\n") { | 45 | + | n := len(lines) |
| 36 | - | if strings.TrimSpace(line) == "" { | 46 | + | if n < 4 { |
| 37 | - | if len(cur) > 0 { | |||
| 38 | - | paras = append(paras, cur) | |||
| 39 | - | cur = nil | |||
| 40 | - | } | |||
| 41 | - | continue | |||
| 42 | - | } | |||
| 43 | - | cur = append(cur, line) | |||
| 44 | - | } | |||
| 45 | - | if len(cur) > 0 { | |||
| 46 | - | paras = append(paras, cur) | |||
| 47 | - | } | |||
| 48 | - | if len(paras) < 2 { | |||
| 49 | return false | 47 | return false | ||
| 50 | } | 48 | } | ||
| 51 | - | for _, line := range paras[len(paras)-1] { | 49 | + | val, ok := strings.CutPrefix(lines[n-1], "skip-checks:") |
| 52 | - | key, val, ok := strings.Cut(line, ":") | 50 | + | if !ok || strings.TrimPrefix(val, " ") != "true" { |
| 53 | - | if ok && strings.EqualFold(strings.TrimSpace(key), "skip-checks") && strings.EqualFold(strings.TrimSpace(val), "true") { | 51 | + | return false |
| 54 | - | return true | |||
| 55 | - | } | |||
| 56 | } | 52 | } | ||
| 57 | - | return false | 53 | + | return lines[n-2] == "" && lines[n-3] == "" && strings.TrimSpace(strings.Join(lines[:n-3], "")) != "" |
| 58 | } | 54 | } | ||
M workflow/skip_test.go +55 -32
| @@ -4,44 +4,67 @@ package workflow | |||
| 4 | 4 | ||
| 5 | 5 | import "testing" | |
| 6 | 6 | ||
| 7 | + | // TestSkipCI replays the commit messages pushed by the recorded scenarios | |
| 8 | + | // skip-directives, skip-directives-placement, skip-checks-trailer and | |
| 9 | + | // skip-directive-tag. skip is true when GitHub started no run. | |
| 7 | 10 | func TestSkipCI(t *testing.T) { | |
| 8 | 11 | tests := []struct { | |
| 9 | - | name, msg string | |
| 10 | - | want bool | |
| 12 | + | scenario, msg string | |
| 13 | + | skip bool | |
| 11 | 14 | }{ | |
| 12 | - | {"empty", "", false}, | |
| 13 | - | {"plain", "fix: a bug", false}, | |
| 14 | - | {"skip ci", "docs: typo [skip ci]", true}, | |
| 15 | - | {"ci skip", "docs: typo [ci skip]", true}, | |
| 16 | - | {"no ci", "docs: typo [no ci]", true}, | |
| 17 | - | {"skip actions", "docs: typo [skip actions]", true}, | |
| 18 | - | {"actions skip", "docs: typo [actions skip]", true}, | |
| 19 | - | {"in body", "docs: typo\n\nlong text [skip ci] more", true}, | |
| 20 | - | {"in subject start", "[skip ci] docs", true}, | |
| 21 | - | {"no brackets", "skip ci", false}, | |
| 22 | - | {"other brackets", "[skip tests]", false}, | |
| 23 | - | {"partial", "[skip ci]", false}, | |
| 24 | - | // TODO(verify with recording): case of the markers. | |
| 25 | - | {"upper case marker", "docs [SKIP CI]", true}, | |
| 26 | - | {"mixed case marker", "docs [Skip Ci]", true}, | |
| 15 | + | // skip-directives | |
| 16 | + | {"skip-directives 1", "change one [skip ci]", true}, | |
| 17 | + | {"skip-directives 2", "change two [ci skip]", true}, | |
| 18 | + | {"skip-directives 3", "change three [no ci]", true}, | |
| 19 | + | {"skip-directives 4", "change four [skip actions]", true}, | |
| 20 | + | {"skip-directives 5", "change five [actions skip]", true}, | |
| 21 | + | {"skip-directives 6", "change six, no directive", false}, | |
| 22 | + | ||
| 23 | + | // skip-directives-placement | |
| 24 | + | {"placement 1 body", "subject\n\nbody line [skip ci] more body", true}, | |
| 25 | + | {"placement 2 mid-subject", "prefix [skip ci] suffix", true}, | |
| 26 | + | {"placement 3 upper case", "upper case [SKIP CI]", false}, | |
| 27 | + | {"placement 4 mixed case", "mixed case [Skip Ci]", false}, | |
| 28 | + | {"placement 5 near miss dash", "near miss [skip-ci]", false}, | |
| 29 | + | {"placement 6 no brackets", "near miss skip ci without brackets", false}, | |
| 30 | + | {"placement 7 extra word", "near miss [skip ci please]", false}, | |
| 31 | + | {"placement 8 NO_CI", "marker ***NO_CI*** in subject", false}, | |
| 32 | + | {"placement 9 SKIP_CI", "marker ***SKIP_CI*** in subject", false}, | |
| 33 | + | ||
| 34 | + | // skip-checks-trailer | |
| 35 | + | {"trailer 1 one blank line", "trailer true\n\nskip-checks: true", false}, | |
| 36 | + | {"trailer 2 false, one blank line", "trailer false\n\nskip-checks: false", false}, | |
| 37 | + | {"trailer 3 upper, one blank line", "trailer upper\n\nSkip-Checks: true", false}, | |
| 38 | + | {"trailer 4 other trailer first", "trailer after other trailer\n\nSigned-off-by: A Person <a@example.invalid>\nskip-checks: true", false}, | |
| 39 | + | {"trailer 5 in the subject", "not a trailer, in the subject skip-checks: true", false}, | |
| 40 | + | {"trailer 6 no blank line", "trailer without blank line\nskip-checks: true", false}, | |
| 41 | + | {"trailer 7 two blank lines", "two blank lines true\n\n\nskip-checks: true", true}, | |
| 42 | + | {"trailer 8 no space", "two blank lines no space\n\n\nskip-checks:true", true}, | |
| 43 | + | {"trailer 9 false", "two blank lines false\n\n\nskip-checks: false", false}, | |
| 44 | + | {"trailer 10 value upper", "two blank lines upper\n\n\nSkip-Checks: TRUE", false}, | |
| 45 | + | {"trailer 11 text after", "two blank lines then more text\n\n\nskip-checks: true\nand more", false}, | |
| 46 | + | {"trailer 12 after other trailer", "two blank lines after other trailer\n\n\nSigned-off-by: A Person <a@example.invalid>\nskip-checks: true", false}, | |
| 47 | + | {"trailer 13 trailing spaces", "two blank lines trailing spaces\n\n\nskip-checks: true ", true}, | |
| 48 | + | {"trailer 14 yes", "two blank lines yes\n\n\nskip-checks: yes", false}, | |
| 49 | + | {"trailer 15 key upper", "two blank lines key upper value lower\n\n\nSkip-Checks: true", false}, | |
| 50 | + | {"trailer 16 value upper", "two blank lines key lower value upper\n\n\nskip-checks: TRUE", false}, | |
| 51 | + | {"trailer 17 key mixed", "two blank lines key mixed\n\n\nSkip-checks: true", false}, | |
| 52 | + | {"trailer 18 plain", "plain commit", false}, | |
| 27 | 53 | ||
| 28 | - | // skip-checks trailer. | |
| 29 | - | {"trailer", "feat: x\n\nskip-checks: true", true}, | |
| 30 | - | {"trailer no space", "feat: x\n\nskip-checks:true", true}, | |
| 31 | - | {"trailer with body", "feat: x\n\nbody text\n\nskip-checks: true", true}, | |
| 32 | - | {"trailer after two blank lines", "feat: x\n\n\nskip-checks: true\n", true}, | |
| 33 | - | {"trailer crlf", "feat: x\r\n\r\nskip-checks: true\r\n", true}, | |
| 34 | - | {"trailer with other trailers", "feat: x\n\nSigned-off-by: A <a@x>\nskip-checks: true", true}, | |
| 35 | - | {"trailer false", "feat: x\n\nskip-checks: false", false}, | |
| 36 | - | {"trailer not last paragraph", "feat: x\n\nskip-checks: true\n\nmore text", false}, | |
| 37 | - | {"trailer in first paragraph only", "skip-checks: true", false}, | |
| 38 | - | {"trailer in subject", "feat: x skip-checks: true", false}, | |
| 39 | - | // TODO(verify with recording): case of key and value. | |
| 40 | - | {"trailer upper", "feat: x\n\nSkip-Checks: TRUE", true}, | |
| 54 | + | // skip-directive-tag: the message of the commit a tag points at. | |
| 55 | + | {"tag 1", "tagged [skip ci]", true}, | |
| 56 | + | {"tag 2", "tagged normally", false}, | |
| 57 | + | ||
| 58 | + | // Not recorded: see the doc comment of SkipCI. | |
| 59 | + | {"trailing newline", "x\n\n\nskip-checks: true\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}, | |
| 62 | + | {"trailer alone", "\n\n\nskip-checks: true", false}, | |
| 63 | + | {"empty", "", false}, | |
| 41 | 64 | } | |
| 42 | 65 | for _, tc := range tests { | |
| 43 | - | if got := SkipCI(tc.msg); got != tc.want { | |
| 44 | - | t.Errorf("%s: SkipCI(%q) = %v, want %v", tc.name, tc.msg, got, tc.want) | |
| 66 | + | if got := SkipCI(tc.msg); got != tc.skip { | |
| 67 | + | t.Errorf("%s: SkipCI(%q) = %v, want %v", tc.scenario, tc.msg, got, tc.skip) | |
| 45 | 68 | } | |
| 46 | 69 | } | |
| 47 | 70 | } | |
| @@ -4,44 +4,67 @@ package workflow | |||||
| 4 | 4 | ||||
| 5 | import "testing" | 5 | import "testing" | ||
| 6 | 6 | ||||
| 7 | + | // TestSkipCI replays the commit messages pushed by the recorded scenarios | |||
| 8 | + | // skip-directives, skip-directives-placement, skip-checks-trailer and | |||
| 9 | + | // skip-directive-tag. skip is true when GitHub started no run. | |||
| 7 | func TestSkipCI(t *testing.T) { | 10 | func TestSkipCI(t *testing.T) { | ||
| 8 | tests := []struct { | 11 | tests := []struct { | ||
| 9 | - | name, msg string | 12 | + | scenario, msg string |
| 10 | - | want bool | 13 | + | skip bool |
| 11 | }{ | 14 | }{ | ||
| 12 | - | {"empty", "", false}, | 15 | + | // skip-directives |
| 13 | - | {"plain", "fix: a bug", false}, | 16 | + | {"skip-directives 1", "change one [skip ci]", true}, |
| 14 | - | {"skip ci", "docs: typo [skip ci]", true}, | 17 | + | {"skip-directives 2", "change two [ci skip]", true}, |
| 15 | - | {"ci skip", "docs: typo [ci skip]", true}, | 18 | + | {"skip-directives 3", "change three [no ci]", true}, |
| 16 | - | {"no ci", "docs: typo [no ci]", true}, | 19 | + | {"skip-directives 4", "change four [skip actions]", true}, |
| 17 | - | {"skip actions", "docs: typo [skip actions]", true}, | 20 | + | {"skip-directives 5", "change five [actions skip]", true}, |
| 18 | - | {"actions skip", "docs: typo [actions skip]", true}, | 21 | + | {"skip-directives 6", "change six, no directive", false}, |
| 19 | - | {"in body", "docs: typo\n\nlong text [skip ci] more", true}, | 22 | + | |
| 20 | - | {"in subject start", "[skip ci] docs", true}, | 23 | + | // skip-directives-placement |
| 21 | - | {"no brackets", "skip ci", false}, | 24 | + | {"placement 1 body", "subject\n\nbody line [skip ci] more body", true}, |
| 22 | - | {"other brackets", "[skip tests]", false}, | 25 | + | {"placement 2 mid-subject", "prefix [skip ci] suffix", true}, |
| 23 | - | {"partial", "[skip ci]", false}, | 26 | + | {"placement 3 upper case", "upper case [SKIP CI]", false}, |
| 24 | - | // TODO(verify with recording): case of the markers. | 27 | + | {"placement 4 mixed case", "mixed case [Skip Ci]", false}, |
| 25 | - | {"upper case marker", "docs [SKIP CI]", true}, | 28 | + | {"placement 5 near miss dash", "near miss [skip-ci]", false}, |
| 26 | - | {"mixed case marker", "docs [Skip Ci]", true}, | 29 | + | {"placement 6 no brackets", "near miss skip ci without brackets", false}, |
| 30 | + | {"placement 7 extra word", "near miss [skip ci please]", false}, | |||
| 31 | + | {"placement 8 NO_CI", "marker ***NO_CI*** in subject", false}, | |||
| 32 | + | {"placement 9 SKIP_CI", "marker ***SKIP_CI*** in subject", false}, | |||
| 33 | + | ||||
| 34 | + | // skip-checks-trailer | |||
| 35 | + | {"trailer 1 one blank line", "trailer true\n\nskip-checks: true", false}, | |||
| 36 | + | {"trailer 2 false, one blank line", "trailer false\n\nskip-checks: false", false}, | |||
| 37 | + | {"trailer 3 upper, one blank line", "trailer upper\n\nSkip-Checks: true", false}, | |||
| 38 | + | {"trailer 4 other trailer first", "trailer after other trailer\n\nSigned-off-by: A Person <a@example.invalid>\nskip-checks: true", false}, | |||
| 39 | + | {"trailer 5 in the subject", "not a trailer, in the subject skip-checks: true", false}, | |||
| 40 | + | {"trailer 6 no blank line", "trailer without blank line\nskip-checks: true", false}, | |||
| 41 | + | {"trailer 7 two blank lines", "two blank lines true\n\n\nskip-checks: true", true}, | |||
| 42 | + | {"trailer 8 no space", "two blank lines no space\n\n\nskip-checks:true", true}, | |||
| 43 | + | {"trailer 9 false", "two blank lines false\n\n\nskip-checks: false", false}, | |||
| 44 | + | {"trailer 10 value upper", "two blank lines upper\n\n\nSkip-Checks: TRUE", false}, | |||
| 45 | + | {"trailer 11 text after", "two blank lines then more text\n\n\nskip-checks: true\nand more", false}, | |||
| 46 | + | {"trailer 12 after other trailer", "two blank lines after other trailer\n\n\nSigned-off-by: A Person <a@example.invalid>\nskip-checks: true", false}, | |||
| 47 | + | {"trailer 13 trailing spaces", "two blank lines trailing spaces\n\n\nskip-checks: true ", true}, | |||
| 48 | + | {"trailer 14 yes", "two blank lines yes\n\n\nskip-checks: yes", false}, | |||
| 49 | + | {"trailer 15 key upper", "two blank lines key upper value lower\n\n\nSkip-Checks: true", false}, | |||
| 50 | + | {"trailer 16 value upper", "two blank lines key lower value upper\n\n\nskip-checks: TRUE", false}, | |||
| 51 | + | {"trailer 17 key mixed", "two blank lines key mixed\n\n\nSkip-checks: true", false}, | |||
| 52 | + | {"trailer 18 plain", "plain commit", false}, | |||
| 27 | 53 | ||||
| 28 | - | // skip-checks trailer. | 54 | + | // skip-directive-tag: the message of the commit a tag points at. |
| 29 | - | {"trailer", "feat: x\n\nskip-checks: true", true}, | 55 | + | {"tag 1", "tagged [skip ci]", true}, |
| 30 | - | {"trailer no space", "feat: x\n\nskip-checks:true", true}, | 56 | + | {"tag 2", "tagged normally", false}, |
| 31 | - | {"trailer with body", "feat: x\n\nbody text\n\nskip-checks: true", true}, | 57 | + | |
| 32 | - | {"trailer after two blank lines", "feat: x\n\n\nskip-checks: true\n", true}, | 58 | + | // Not recorded: see the doc comment of SkipCI. |
| 33 | - | {"trailer crlf", "feat: x\r\n\r\nskip-checks: true\r\n", true}, | 59 | + | {"trailing newline", "x\n\n\nskip-checks: true\n", true}, |
| 34 | - | {"trailer with other trailers", "feat: x\n\nSigned-off-by: A <a@x>\nskip-checks: true", true}, | 60 | + | {"crlf", "x\r\n\r\n\r\nskip-checks: true\r\n", true}, |
| 35 | - | {"trailer false", "feat: x\n\nskip-checks: false", false}, | 61 | + | {"three blank lines", "x\n\n\n\nskip-checks: true", true}, |
| 36 | - | {"trailer not last paragraph", "feat: x\n\nskip-checks: true\n\nmore text", false}, | 62 | + | {"trailer alone", "\n\n\nskip-checks: true", false}, |
| 37 | - | {"trailer in first paragraph only", "skip-checks: true", false}, | 63 | + | {"empty", "", false}, |
| 38 | - | {"trailer in subject", "feat: x skip-checks: true", false}, | |||
| 39 | - | // TODO(verify with recording): case of key and value. | |||
| 40 | - | {"trailer upper", "feat: x\n\nSkip-Checks: TRUE", true}, | |||
| 41 | } | 64 | } | ||
| 42 | for _, tc := range tests { | 65 | for _, tc := range tests { | ||
| 43 | - | if got := SkipCI(tc.msg); got != tc.want { | 66 | + | if got := SkipCI(tc.msg); got != tc.skip { |
| 44 | - | t.Errorf("%s: SkipCI(%q) = %v, want %v", tc.name, tc.msg, got, tc.want) | 67 | + | t.Errorf("%s: SkipCI(%q) = %v, want %v", tc.scenario, tc.msg, got, tc.skip) |
| 45 | } | 68 | } | ||
| 46 | } | 69 | } | ||
| 47 | } | 70 | } | ||