forgejo-mcp/.tekton/tasks/commit-title-check.yaml
Christoph Görn 508c7c0c68
ci: ✨ add go-licenses check + Conventional Commits PR title gate
- go-licenses v1.6.0 added to release-tools image; license-check step
  in go-ci task fails on forbidden/restricted licenses (GPL/AGPL/LGPL/MPL)
- new commit-title-check task validates PR title AND each PR branch
  commit against Conventional Commits regex using PAC dynamic var
  {{ body.pull_request.title }}; blocks merge before semantic-release
  post-merge analysis would silently drop malformed titles
- on-pull-request pipeline wires commit-title-check as parallel task
  alongside build-and-test; PAC resolves target_branch → base_ref
2026-05-26 15:16:42 +02:00

125 lines
4.9 KiB
YAML

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."