apiVersion: tekton.dev/v1beta1 kind: Task metadata: name: commit-title-check annotations: tekton.dev/displayName: "Conventional Commits PR title + commit message check" spec: description: | Gate Conventional Commits format BEFORE merge. semantic-release reads individual commit titles post-merge (see .releaserc); a malformed title silently dropped a release in the past. This task fails the PR early instead of waiting for the post-merge release pipeline to no-op. Checks: 1. PR title (passed via PR_TITLE param from PAC dynamic var {{ body.pull_request.title }}) matches Conventional Commits. 2. Every commit on the PR branch (revision..origin/main..HEAD on the clone) matches the same regex. Merge commits are skipped. The regex accepts the type whitelist from .releaserc plus 'revert'. Optional scope (`(scope)`), optional breaking marker (`!`), required colon+space+subject. Allows gitmoji or any character in subject. params: - name: PR_TITLE description: | Pull request title from PAC dynamic variable {{ body.pull_request.title }}. type: string - name: BASE_REF description: Base branch to diff PR commits against. type: string default: main - name: GIT_IMAGE description: | Image carrying git + bash + grep. Reuses release-tools because it is already pulled by go-ci on every PR run. type: string default: codeberg.org/operate-first/release-tools:latest - name: CC_REGEX description: Conventional Commits regex (anchored, ERE). type: string default: '^(feat|fix|docs|style|refactor|perf|test|ci|chore|build|revert)(\([^)]+\))?!?: .+' workspaces: - name: source mountPath: /workspace/src stepTemplate: workingDir: $(workspaces.source.path) steps: - name: check-pr-title image: $(params.GIT_IMAGE) env: - name: PR_TITLE value: $(params.PR_TITLE) - name: CC_REGEX value: $(params.CC_REGEX) script: | #!/usr/bin/env bash set -euo pipefail if [ -z "${PR_TITLE}" ]; then echo "PR_TITLE param is empty — cannot validate Conventional Commits format." >&2 echo "Hint: PipelineRun must pass {{ body.pull_request.title }} as PR_TITLE." >&2 exit 1 fi printf 'PR title: %s\n' "${PR_TITLE}" if printf '%s' "${PR_TITLE}" | grep -Eq "${CC_REGEX}"; then echo "OK: PR title matches Conventional Commits." else echo "::error::PR title does not match Conventional Commits format." >&2 echo "Expected regex: ${CC_REGEX}" >&2 echo "Allowed types: feat, fix, docs, style, refactor, perf, test, ci, chore, build, revert" >&2 echo "Examples:" >&2 echo " feat(api): add /healthz endpoint" >&2 echo " fix: handle nil session token" >&2 echo " chore(release): 2.25.1" >&2 exit 1 fi - name: check-commits image: $(params.GIT_IMAGE) env: - name: BASE_REF value: $(params.BASE_REF) - name: CC_REGEX value: $(params.CC_REGEX) script: | #!/usr/bin/env bash # Validate each commit unique to the PR branch. Skips merge commits # (--no-merges) — their subject is generated by Forgejo and is not # what semantic-release reads. set -euo pipefail git config --global --add safe.directory "$(workspaces.source.path)" git fetch --no-tags --depth=200 origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}" || \ git fetch --no-tags origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}" range="origin/${BASE_REF}..HEAD" echo "Checking commits in range: ${range}" # Bail out clean if PR branch is empty relative to base (no commits to check). if ! git rev-list --no-merges "${range}" --count >/dev/null 2>&1; then echo "No commits found in range — nothing to validate." exit 0 fi count=$(git rev-list --no-merges "${range}" --count) if [ "${count}" -eq 0 ]; then echo "No non-merge commits in range — nothing to validate." exit 0 fi failed=0 while IFS=$'\t' read -r sha subject; do if printf '%s' "${subject}" | grep -Eq "${CC_REGEX}"; then printf ' OK %s %s\n' "${sha}" "${subject}" else printf ' FAIL %s %s\n' "${sha}" "${subject}" >&2 failed=$((failed + 1)) fi done < <(git log --no-merges --format='%h%x09%s' "${range}") if [ "${failed}" -gt 0 ]; then echo "::error::${failed} commit(s) do not match Conventional Commits format." >&2 echo "Rewrite history (git rebase -i origin/${BASE_REF}) so every commit matches:" >&2 echo " ${CC_REGEX}" >&2 exit 1 fi echo "OK: ${count} commit(s) match Conventional Commits."