diff --git a/.github/hooks/commit-msg b/.github/hooks/commit-msg deleted file mode 100755 index 366dc57e94..0000000000 --- a/.github/hooks/commit-msg +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -function enforce_commit_msg() -{ - local cmtmsg="$1" - if [[ $cmtmsg != \[KNI\]* ]]; then - echo "commit message [$cmtmsg] not formatted correctly" - echo "please refer to https://github.com/openshift-kni/scheduler-plugins/blob/master/RESYNC.md#patching-openshift-kni-specific-commits" - exit 1 - fi -} - -enforce_commit_msg "$1" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bcc15954c1..9d8c34032c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,32 +7,6 @@ on: branches: [ master, release-4.12, release-4.14, release-4.16, release-4.18 ] jobs: - commit-check: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Get branch names (pull request) - shell: bash - run: | - echo "SOURCE_BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV - echo "TARGET_BRANCH_NAME=$(echo ${GITHUB_BASE_REF} | tr / -)" >> $GITHUB_ENV - - - name: Debug - run: | - echo ${{ env.SOURCE_BRANCH_NAME }} - echo ${{ env.TARGET_BRANCH_NAME }} - - - name: Verify commits - run: | - TRIGGER_BRANCH=${{ env.SOURCE_BRANCH_NAME }} \ - UPSTREAM_BRANCH=${{ env.TARGET_BRANCH_NAME }} \ - COMMITS=${{ github.event.pull_request.commits }} \ - ./hack-kni/verify-commits.sh - integration-test: runs-on: ubuntu-latest env: @@ -40,14 +14,14 @@ jobs: GOPATH: "/home/runner/go" steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up golang - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: - go-version: 1.23 + go-version: 1.24 - name: Run integration test run: @@ -57,14 +31,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up golang - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: - go-version: 1.23 + go-version: 1.24 - name: Verify vendoring run: ./hack-kni/verify-vendoring.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 813d2a4857..a2a65ee484 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,15 +18,15 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Setup golang - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 id: go with: - go-version: 1.23 + go-version: 1.24 - name: Set release version env var run: | diff --git a/.github/workflows/verifycommit.yaml b/.github/workflows/verifycommit.yaml new file mode 100644 index 0000000000..fde46a1fbe --- /dev/null +++ b/.github/workflows/verifycommit.yaml @@ -0,0 +1,70 @@ +name: Verify Commits + +on: + push: + branches: [ "master", "release-4.*" ] + pull_request: + branches: [ "master", "release-4.*" ] + +jobs: + commit-check: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up golang + uses: actions/setup-go@v6 + with: + go-version: 1.24 + + - name: Get branch names (pull request) + shell: bash + run: | + echo "SOURCE_BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV + echo "TARGET_BRANCH_NAME=$(echo ${GITHUB_BASE_REF} | tr / -)" >> $GITHUB_ENV + + - name: Debug + run: | + echo ${{ env.SOURCE_BRANCH_NAME }} + echo ${{ env.TARGET_BRANCH_NAME }} + + - name: Setup git remote + run: + git remote add upstream https://github.com/kubernetes-sigs/scheduler-plugins.git + + - name: Build verification tool + run: | + make -f Makefile.kni build-tools + + - name: Verify commits + run: | + cd bin + ./verify-commit-message -n ${{ github.event.pull_request.commits }} + + unit-testing: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up golang + uses: actions/setup-go@v6 + with: + go-version: 1.24 + + - name: Add upstream remote + run: | + git remote add upstream https://github.com/kubernetes-sigs/scheduler-plugins.git + + - name: Build verification tool + run: | + make -f Makefile.kni build-tools + + - name: Run unit tests + run: + go test -v ./hack-kni/tools/verifycommit diff --git a/Makefile.kni b/Makefile.kni index 74c3962569..57b1cfec86 100644 --- a/Makefile.kni +++ b/Makefile.kni @@ -90,3 +90,8 @@ verify-crdgen: update-vendor .PHONY: clean clean: rm -rf ./bin + +.PHONY: build-tools +build-tools: + go build -o bin/verify-commit-message hack-kni/tools/verifycommit/verify-commit-message.go + cp hack-kni/tools/verifycommit/config.json bin/ diff --git a/hack-kni/tools/verifycommit/config.json b/hack-kni/tools/verifycommit/config.json new file mode 100644 index 0000000000..a55a8529a1 --- /dev/null +++ b/hack-kni/tools/verifycommit/config.json @@ -0,0 +1,4 @@ +{ + "originName": "origin", + "upstreamName": "upstream" +} \ No newline at end of file diff --git a/hack-kni/tools/verifycommit/test-config-missing.json b/hack-kni/tools/verifycommit/test-config-missing.json new file mode 100644 index 0000000000..6be3632383 --- /dev/null +++ b/hack-kni/tools/verifycommit/test-config-missing.json @@ -0,0 +1,3 @@ +{ + "upstreamName": "upstream" +} \ No newline at end of file diff --git a/hack-kni/tools/verifycommit/verify-commit-message.go b/hack-kni/tools/verifycommit/verify-commit-message.go new file mode 100644 index 0000000000..0c3ee3d261 --- /dev/null +++ b/hack-kni/tools/verifycommit/verify-commit-message.go @@ -0,0 +1,328 @@ +package main + +import ( + "bufio" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +const ( + exitCodeSuccess = 0 + exitCodeErrorWrongArguments = 1 + exitCodeErrorProcessingFile = 2 + exitCodeErrorVerificationFailed = 3 +) + +const ( + exitCodeGitMalformedObject = 129 +) + +const ( + tagKNI = "[KNI]" + tagUpstream = "[upstream]" + + cherryPickLinePrefix = "(cherry picked from commit " + cherryPickLineSuffix = ")" // yes that simple + + signedOffByPrefix = "Signed-off-by: " + + konfluxUsername = "red-hat-konflux" + + defaultOriginName = "origin" + defaultUpstreamName = "upstream" + referenceBranchName = "master" + + resyncBranchPrefix = "resync-" + + headBranchName = "HEAD" +) + +var ( + errEmptyCommitMessage = errors.New("empty commit message") + errMissingTagKNI = errors.New("missing tag: " + tagKNI) + errMissingCherryPickReference = errors.New("missing cherry pick reference") + errWrongCherryPickReference = errors.New("wrong cherry pick reference") +) + +type commitMessage struct { + lines []string +} + +type config struct { + // OriginName is the git remote that points to the clone of this repo + OriginName string `json:"originName"` + // UpstreamName is the git remote that points kubernetes-sigs/scheduler-plugins repo + UpstreamName string `json:"upstreamName"` + // TriggerBranch is the branch name that triggers the verification + TriggerBranch string `json:"triggerBranch"` +} + +var conf = config{ + OriginName: defaultOriginName, + UpstreamName: defaultUpstreamName, +} + +var sourceBranch *string + +func newCommitMessageFromString(text string) commitMessage { + var cm commitMessage + scanner := bufio.NewScanner(strings.NewReader(text)) + for scanner.Scan() { + cm.lines = append(cm.lines, scanner.Text()) + } + log.Printf("commit message has %d lines", cm.numLines()) + return cm +} + +func (cm commitMessage) numLines() int { + return len(cm.lines) +} + +func (cm commitMessage) isEmpty() bool { + return cm.numLines() == 0 +} + +func (cm commitMessage) summary() string { + return cm.lines[0] +} + +func (cm commitMessage) isKNISpecific() bool { + return strings.Contains(cm.summary(), tagKNI) +} + +func (cm commitMessage) isUpstream() bool { + return strings.Contains(cm.summary(), tagUpstream) +} + +func (cm commitMessage) isKonflux() bool { + for idx := cm.numLines() - 1; idx > 0; idx-- { + line := cm.lines[idx] // shortcut + line = strings.TrimSpace(line) + signedOff, ok := strings.CutPrefix(line, signedOffByPrefix) + if !ok { + continue + } + if strings.HasPrefix(signedOff, konfluxUsername) { + return true + } + } + return false // nothing found +} + +func isResyncBranch(branch string) bool { + return strings.HasPrefix(branch, resyncBranchPrefix) +} + +// cherryPickOrigin returns the commit hash this commit was cherry-picked +// from if this commit has cherry-pick reference; otherwise returns empty string. +func (cm commitMessage) cherryPickOrigin() string { + for idx := cm.numLines() - 1; idx > 0; idx-- { + line := cm.lines[idx] // shortcut + cmHash, ok := strings.CutPrefix(line, cherryPickLinePrefix) + if !ok { // we don't have the prefix, so we don't care + continue + } + cmHash, ok = strings.CutSuffix(cmHash, cherryPickLineSuffix) + if !ok { // we don't have the suffix, so we don't care + continue + } + return cmHash + } + return "" // nothing found +} + +func validateCommitMessage(commitMessage string) error { + cm := newCommitMessageFromString(commitMessage) + + if cm.isKonflux() { + return nil + } + return verifyHumanCommitMessage(cm) +} + +func getCommitMessageByHash(commitHash string) (string, error) { + cmd := exec.Command("git", "show", "--format=%B", commitHash) + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get commit message for %s: %v", commitHash, err) + } + return string(out), nil +} + +func verifyLastNCommits(numCommits int) error { + log.Printf("considering %d commits in PR whose head is %s:\n", numCommits, *sourceBranch) + + // Get the list of commits + cmd := exec.Command("git", "log", *sourceBranch, "--oneline", "--no-merges", "-n", fmt.Sprintf("%d", numCommits)) + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to get commit list: %v", err) + } + log.Println(string(out)) + + commitLines := strings.Split(strings.TrimSpace(string(out)), "\n") + for _, line := range commitLines { + log.Printf("examining %q", line) + + fields := strings.Fields(line) + if len(fields) == 0 { + continue + } + commitHash := fields[0] + + commit, err := getCommitMessageByHash(commitHash) + if err != nil { + return err + } + log.Printf("verifying message:\n%q\n", commit) + if err = validateCommitMessage(commit); err != nil { + return err + } + } + + return nil +} + +func verifyHumanCommitMessage(cm commitMessage) error { + if cm.isEmpty() { + return errEmptyCommitMessage + } + + if !cm.isKNISpecific() { + return errMissingTagKNI + } + + cpOrigin := cm.cherryPickOrigin() + upstream := cm.isUpstream() + + if cpOrigin == "" { + if upstream { + return errMissingCherryPickReference + } + } + + if cpOrigin != "" { + remoteName := conf.OriginName + if upstream { + remoteName = conf.UpstreamName + } + + err := isCommitInBranch(remoteName, cpOrigin) + if err != nil { + return err + } + } + + return nil +} + +func isCommitInBranch(remoteName, cpOrigin string) error { + cmd := exec.Command("git", "branch", "-r", "--contains", cpOrigin) + out, err := cmd.Output() + if err != nil { + if isMalformedObjectErr(err, cpOrigin) { + return errWrongCherryPickReference + } + return err + } + + outStr := string(out) + + if !strings.Contains(outStr, remoteName+"/"+referenceBranchName) { + return errWrongCherryPickReference + } + return nil +} + +func isMalformedObjectErr(err error, objHash string) bool { + if exitErr, ok := err.(*exec.ExitError); ok { + if string(exitErr.Stderr) == fmt.Sprintf("error: malformed object name %s\n", objHash) && exitErr.ExitCode() == exitCodeGitMalformedObject { + return true + } + } + return false +} + +func main() { + var configFileName = flag.String("f", "config.json", "config file path") + var numCommits = flag.Int("n", 0, "number of last commits to verify (defaults to 0)") + sourceBranch = flag.String("b", "", "source branch name") + flag.Parse() + + if *numCommits < 0 { + programName := os.Args[0] + log.Printf("usage: %s -b -n [-f config-file]", programName) + log.Printf(" -b: source branch name (if empty, the tool will use the current branch)") + log.Printf(" -n: number of last commits to verify (if not provided it defaults to 0)") + log.Printf(" -f: config file path (optional, remote defaults are origin and upstream)") + os.Exit(exitCodeErrorWrongArguments) + } + + if *numCommits == 0 { + log.Printf("number of commits to verify is 0, skipping verification") + os.Exit(exitCodeSuccess) + } + + if strings.TrimSpace(*sourceBranch) == "" { + *sourceBranch = headBranchName + log.Printf("using branch: %s", *sourceBranch) + } + + if isResyncBranch(*sourceBranch) { + log.Printf("WARN: resync branch no commit enforcement will be triggered\n") + os.Exit(exitCodeSuccess) + } + + err := processConfigFile(*configFileName) + if err != nil { + log.Printf("error processing config file: %v", err) + os.Exit(exitCodeErrorProcessingFile) + } + + err = verifyLastNCommits(*numCommits) + if err != nil { + log.Printf("verification failed: %v", err) + os.Exit(exitCodeErrorVerificationFailed) + } + + os.Exit(exitCodeSuccess) // all good! redundant but let's be explicit about our success +} + +func processConfigFile(filePath string) error { + // Use os.OpenRoot to prevent directory traversal attacks + // This ensures file access is scoped to the validated absolute path + root, err := os.OpenRoot(filepath.Dir(filePath)) + if err != nil { + return fmt.Errorf("error opening root directory for %s: %v", filePath, err) + } + defer func() { + _ = root.Close() + }() + + file, err := root.Open(filepath.Base(filePath)) + if err != nil { + return fmt.Errorf("error opening file %s: %v", filePath, err) + } + defer func() { + _ = file.Close() + }() + + fileContent, err := io.ReadAll(file) + if err != nil { + return fmt.Errorf("error reading content from %s: %v", filePath, err) + } + + err = json.Unmarshal(fileContent, &conf) // keep it flexible + if err != nil { + return fmt.Errorf("error parsing %s: %v", filePath, err) + } + return nil +} diff --git a/hack-kni/tools/verifycommit/verify-commit-message_test.go b/hack-kni/tools/verifycommit/verify-commit-message_test.go new file mode 100644 index 0000000000..b181874031 --- /dev/null +++ b/hack-kni/tools/verifycommit/verify-commit-message_test.go @@ -0,0 +1,255 @@ +package main + +import ( + "bytes" + "fmt" + "os/exec" + "path/filepath" + goruntime "runtime" + "testing" +) + +const ( + binariesDir = "bin" + programName = "verify-commit-message" +) + +func TestRunCommand(t *testing.T) { + testcases := []struct { + description string + args []string + expectedExitCode int + }{ + { + description: "valid call", + args: []string{"-n", "5", "-b", "release-4.19"}, + expectedExitCode: exitCodeSuccess, + }, + { + description: "valid call with config file", + args: []string{"-f", "config.json", "-n", "5", "-b", "release-4.19"}, + expectedExitCode: exitCodeSuccess, + }, + { + description: "valid:missing config data should default to default values", + args: []string{"-f", "test-config-missing.json", "-n", "5", "-b", "resync-docs"}, + expectedExitCode: exitCodeSuccess, + }, + { + description: "valid: too many non-flag arguments should be ignored", + args: []string{"-n", "5", "-b", "release-4.19", "extra-arg"}, + expectedExitCode: exitCodeSuccess, + }, + { + description: "valid: missing number of commits defaults to 0", + args: []string{"-b", "release-4.19"}, + expectedExitCode: exitCodeSuccess, + }, + { + description: "valid: missing source branch", + args: []string{"-n", "5"}, + expectedExitCode: exitCodeSuccess, + }, + { + description: "valid: no arguments at all", + args: []string{}, + expectedExitCode: exitCodeSuccess, + }, + { + description: "invalid: not-found config file", + args: []string{"-f", "not-found.json", "-n", "5", "-b", "release-4.17"}, + expectedExitCode: exitCodeErrorProcessingFile, + }, + { + description: "invalid: not-found branch name", + args: []string{"-n", "5", "-b", "branch-not-found"}, + expectedExitCode: exitCodeErrorVerificationFailed, + }, + } + + for _, tc := range testcases { + t.Run(tc.description, func(t *testing.T) { + out, errBuf, err := runCommand(t, tc.args...) + + if err == nil { + if tc.expectedExitCode == exitCodeSuccess { // everything is as expected + return + } + t.Fatalf("Received unexpected success %s", out) + } + + if tc.expectedExitCode == exitCodeSuccess && err != nil { + t.Fatalf("Received unexpected error %v (stderr=%s)", err, errBuf) + } + + exiterr, ok := err.(*exec.ExitError) + if !ok { + t.Fatalf("Received unexpected error type %T: %v", err, err) + } + + errCode := exiterr.ExitCode() + if errCode != tc.expectedExitCode { + t.Fatalf("Received unexpected exit code: expected %d got %d", tc.expectedExitCode, errCode) + } + }) + } +} + +func TestValidateCommitMessage(t *testing.T) { + testcases := []struct { + description string + commitMsg string + expectedErr error + }{ + { + description: "only KNI in local fork", + commitMsg: `[KNI] hack-kni: skip commit verification for konflux commits + + Skip validating konflux commits structure. Usually konflux bot commits + signed off by either "red-hat-konflux" or "red-hat-konflux[bot]". + + Signed-off-by: Shereen Haj `, + }, + { + description: "KNI & upstream tags", + commitMsg: `[KNI][upstream] nrt: test: ensure generation is updated correctly +Add a unit test mainly for GetCachedNRTCopy() to verify the returned +generation. To help with the verification, make FlushNodes not only +report about the new generation (which only updated there) but also +return it so we'd be able to compare the values. + +Signed-off-by: Shereen Haj +(cherry picked from commit ffe2ce2)`, + }, + { + // this test depends on the local setup. The expected setup should be that the forked project remote + // name is called "origin" and the main project from which this project is forked called "upstream" + description: "KNI and local cherrypick", + commitMsg: `[KNI][release-4.18] ci: ghactions: ensure golang version in vendor check +make sure we run the vendor check in a controlled environment, +and also make sure to emit the golang version we use. + +Signed-off-by: Francesco Romani +(cherry picked from commit 2f4974a)`, + }, + { + description: "Konflux signed - github email", + commitMsg: `Update Konflux references to 252e5c9 + Signed-off-by: red-hat-konflux <126015336+red-hat-konflux[bot]@users.noreply.github.com>`, + }, + { + description: "Konflux signed - ci email", + commitMsg: `Update Konflux references +Signed-off-by: red-hat-konflux `, + }, + { + description: "Konflux signed - github email", + commitMsg: `chore(deps): update konflux references to 2c32152 + Signed-off-by: red-hat-konflux <126015336+red-hat-konflux[bot]@users.noreply.github.com>`, + }, + { + description: "Negative - no KNI tag and not konflux signed", + commitMsg: `nrt: test: ensure generation is updated correctly +Add a unit test + + Signed-off-by: Shereen Haj `, + expectedErr: errMissingTagKNI, + }, + { + description: "Negative - no KNI tag but with upstream tag", + commitMsg: `[upstream] nrt: test: ensure generation is updated correctly +Add a unit test + +Signed-off-by: Shereen Haj +(cherry picked from commit ffe2ce2)`, + expectedErr: errMissingTagKNI, + }, + { + description: "Negative - KNI & upstream tags without cherrypick hash", + commitMsg: `[KNI][upstream] nrt: test: ensure generation is updated correctly +Add a unit test + +Signed-off-by: Shereen Haj `, + expectedErr: errMissingCherryPickReference, + }, + { + description: "Negative - Konflux generated without signature", + commitMsg: `Update Konflux references to 252e5c9 + + +`, + expectedErr: errMissingTagKNI, + }, + { + description: "Negative - invalid cherry-pick reference", + commitMsg: `[KNI][upstream] nrt: test: ensure generation is updated correctly +Add a unit test + +Signed-off-by: Shereen Haj +(cherry picked from commit 123a)`, + expectedErr: errWrongCherryPickReference, + }, + { + description: "Negative - empty cherry-pick reference", + commitMsg: `[KNI][upstream] nrt: test: ensure generation is updated correctly +Add a unit test + +Signed-off-by: Shereen Haj +(cherry picked from commit )`, + expectedErr: errMissingCherryPickReference, + }, + } + for _, tc := range testcases { + t.Run(tc.description, func(t *testing.T) { + got := validateCommitMessage(tc.commitMsg) + + if got == nil && tc.expectedErr == nil { + return + } + + if tc.expectedErr != nil && got == nil { + t.Fatalf("expected error %v but recieved nil", tc.expectedErr) + } + + if tc.expectedErr == nil && got != nil { + t.Fatalf("unexpected error %v", got) + } + + if got.Error() != tc.expectedErr.Error() { + t.Fatalf("mismatching error strings: expected %s, got %s", tc.expectedErr.Error(), got.Error()) + } + }) + } +} + +// runCommand returns stdout as string, stderr as string, error value +func runCommand(t *testing.T, args ...string) (string, string, error) { + bin, err := getBinPath() + if err != nil { + t.Fatalf("failed to find the binary path: %v", err) + } + fmt.Printf("going to use %q\n", bin) + var errBuf bytes.Buffer + cmd := exec.Command(bin, args...) + cmd.Stderr = &errBuf + out, err := cmd.Output() + fmt.Printf("tool returned <%s>\n", out) + return string(out), errBuf.String(), err +} + +func getBinPath() (string, error) { + rootDir, err := getRootPath() + if err != nil { + return "", err + } + return filepath.Join(rootDir, binariesDir, programName), nil +} + +func getRootPath() (string, error) { + _, file, _, ok := goruntime.Caller(0) + if !ok { + return "", fmt.Errorf("cannot retrieve tests directory") + } + basedir := filepath.Dir(file) + return filepath.Abs(filepath.Join(basedir, "..", "..", "..")) +} diff --git a/hack-kni/verify-commits.sh b/hack-kni/verify-commits.sh deleted file mode 100755 index 035db4484d..0000000000 --- a/hack-kni/verify-commits.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -set -e -o pipefail - -function finish { - if [ -f "$commit_msg_filename" ]; then - rm -f "$commit_msg_filename" - fi -} -trap finish EXIT - -echo "checking branch: [$TRIGGER_BRANCH]" - -shopt -s extglob -if [[ "$TRIGGER_BRANCH" == resync-* ]]; then - echo "WARN: resync branch no commit enforcement will be triggered" - exit 0 -fi - -if [ -z "$COMMITS" ] || (( $COMMITS <= 0 )); then - echo "WARN: no changes detected [COMMITS=${COMMITS}]" - exit 0 -fi - -echo "considering ${COMMITS} commits in PR whose head is $TRIGGER_BRANCH (into $UPSTREAM_BRANCH):" -echo "---" -git log --oneline --no-merges -n ${COMMITS} -echo "---" - -# list commits -for commitish in $( git log --oneline --no-merges -n ${COMMITS} | cut -d' ' -f 1); do - echo "CHECK: $commitish" - - author_name=$( git log --pretty=format:"%an" -n 1 "$commitish" ) - if [[ "$author_name" == red-hat-konflux* ]]; then - echo "Skip verifying commit from Konflux bot" - continue - fi - - .github/hooks/commit-msg $( git log --format=%s -n 1 "$commitish" ) - if [[ "$?" != "0" ]]; then - echo "-> FAIL: $commitish" - exit 20 - fi - echo "-> PASS: $commitish" -done -