diff --git a/.github/commit-scopes.txt b/.github/commit-scopes.txt new file mode 100644 index 0000000..85b6f68 --- /dev/null +++ b/.github/commit-scopes.txt @@ -0,0 +1,4 @@ +# Allowed commit scopes, one per line (blank lines and lines starting with `#` are ignored). +# Used by `.github/scripts/check-commit-messages`. +core +tests/core diff --git a/.github/scripts/add-commit-pr-id b/.github/scripts/add-commit-pr-id new file mode 100755 index 0000000..ea6a432 --- /dev/null +++ b/.github/scripts/add-commit-pr-id @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# ============================================================================== +# Make the title of every commit of the current branch end with ` (#)`. +# Commits whose title already ends with this ID only are left untouched, so the script can be run +# several times. +# +# Usage: add-commit-pr-id [--force] +# +# - ``: base of the pull request to check and modify. +# - ``: head of the pull request to check and modify. +# - ``: number of the pull request. +# - `--force`: replace the existing pull request IDs of the titles. +# +# Every trailing ` (#)` of a title is considered to be a pull request ID. Without `--force`, +# the commits that already end with another ID are ignored (and listed), the others are rewritten: +# an ignored commit is probably in the range because the base is wrong, or the range contains +# commits of other pull requests. +# ============================================================================== + +set -euo pipefail + +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +readonly SCRIPT_DIR + +readonly AMEND_SCRIPT_PATH="${SCRIPT_DIR}/amend-commit-pr-id" + +source "${SCRIPT_DIR}/lib/git.sh" +source "${SCRIPT_DIR}/lib/pr-id.sh" + +# ============================================================================== + +readonly FORCE_OPTION="--force" + +FORCE=false +ARGS=() +for arg in "$@"; do + if [ "$arg" = "$FORCE_OPTION" ]; then + FORCE=true + else + ARGS+=("$arg") + fi +done +readonly FORCE +readonly ARGS + +if [ "${#ARGS[@]}" -ne 3 ]; then + echo "Usage: $0 [${FORCE_OPTION}] " >&2 + exit 2 +fi + +BASE_SHA=$(resolve_commit "${ARGS[0]}") +readonly BASE_SHA + +HEAD_SHA=$(resolve_commit "${ARGS[1]}") +readonly HEAD_SHA + +readonly PR_NUMBER=${ARGS[2]} +if ! is_pr_number "$PR_NUMBER"; then + echo "Invalid pull request number: \`${PR_NUMBER}\`." >&2 + exit 2 +fi + +# ------------------------------------------------------------------------------ + +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "The working tree has uncommitted changes: commit or stash them first." >&2 + exit 1 +fi + +if [ "$(git rev-parse HEAD)" != "$HEAD_SHA" ]; then + echo "\`HEAD\` is not \`${HEAD_SHA}\`: checkout the head of the pull request first." >&2 + exit 1 +fi + +# ------------------------------------------------------------------------------ + +if ! MERGE_BASE_SHA=$(git merge-base "$BASE_SHA" "$HEAD_SHA"); then + echo "No common ancestor between \`${BASE_SHA}\` and \`${HEAD_SHA}\`." >&2 + exit 2 +fi +readonly MERGE_BASE_SHA + +readonly COMMIT_RANGE="${MERGE_BASE_SHA}..${HEAD_SHA}" +if [ -n "$(git rev-list --merges "${MERGE_BASE_SHA}..${HEAD_SHA}")" ]; then + echo "Merge commits found in \`${COMMIT_RANGE}\`: this script only supports linear history." >&2 + exit 1 +fi + +mapfile -t hashes < <(git log --format=%H "${MERGE_BASE_SHA}..${HEAD_SHA}") + +# ------------------------------------------------------------------------------ + +PR_ID_SUFFIX=$(get_pr_id_suffix "$PR_NUMBER") +readonly PR_ID_SUFFIX + +to_rewrite=0 +to_ignore=0 +for hash in "${hashes[@]}"; do + subject=$(git log -1 --format=%s "$hash") + if [ "$(with_pr_id "$subject" "$PR_ID_SUFFIX")" = "$subject" ]; then + continue + fi + + # The title already ends with a pull request ID other than the requested one. + if has_pr_id "$subject" && ! $FORCE; then + to_ignore=$((to_ignore + 1)) + echo "Commit ${hash} ignored, it already has another pull request ID: \"${subject}\"." >&2 + continue + fi + + to_rewrite=$((to_rewrite + 1)) +done + +if [ "$to_rewrite" -eq 0 ]; then + if [ "$to_ignore" -eq 0 ]; then + echo "Nothing to do: every commit of \`${COMMIT_RANGE}\` already ends with \"${PR_ID_SUFFIX}\"." + else + echo "Nothing to do: ${to_ignore} commit(s) of \`${COMMIT_RANGE}\` ignored with another pull request ID, the others already end with \"${PR_ID_SUFFIX}\"." + fi + exit 0 +fi + +# ------------------------------------------------------------------------------ + +exec_options=() +if $FORCE; then + exec_options+=("$FORCE_OPTION") +fi + +readonly PREVIOUS_HEAD=$HEAD_SHA +printf -v EXEC_COMMAND '%q ' "$AMEND_SCRIPT_PATH" "${exec_options[@]}" "$PR_ID_SUFFIX" +readonly EXEC_COMMAND + +if ! rebase_output=$(GIT_SEQUENCE_EDITOR=: git rebase --interactive --no-autosquash --exec "$EXEC_COMMAND" "$MERGE_BASE_SHA" 2>&1); then + echo "$rebase_output" >&2 + git rebase --abort || true + echo "Rewrite failed, branch restored (previous HEAD: ${PREVIOUS_HEAD})." >&2 + exit 1 +fi + +echo "${to_rewrite} commit title(s) updated with \"${PR_ID_SUFFIX}\" (previous HEAD: ${PREVIOUS_HEAD})." +if [ "$to_ignore" -ne 0 ]; then + echo "${to_ignore} commit(s) ignored because they already have another pull request ID." +fi diff --git a/.github/scripts/amend-commit-pr-id b/.github/scripts/amend-commit-pr-id new file mode 100755 index 0000000..b8fcf44 --- /dev/null +++ b/.github/scripts/amend-commit-pr-id @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# ============================================================================== +# Amend the title of `HEAD` so that it ends with the given pull request ID suffix, and only with it. +# The title is left untouched if it already ends like that. +# +# Usage: amend-commit-pr-id [--force] +# +# - `--force`: replace the pull request IDs already at the end of the title. Without it, a title +# that already ends with a pull request ID is left untouched. +# +# Run by `git rebase --exec` from `add-commit-pr-id`, once for each commit it replays. It does +# none of the checks of `add-commit-pr-id`: do not use it directly. +# ============================================================================== + +set -euo pipefail + +source "$(dirname "$0")/lib/pr-id.sh" + +# ============================================================================== + +readonly FORCE_OPTION="--force" + +FORCE=false +if [ "${1:-}" = "$FORCE_OPTION" ]; then + FORCE=true + shift +fi +readonly FORCE + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 [${FORCE_OPTION}] " >&2 + exit 2 +fi + +readonly PR_ID_SUFFIX=$1 + +# ------------------------------------------------------------------------------ + +subject=$(git log -1 --format=%s) +if has_pr_id "$subject" && ! $FORCE; then + exit 0 +fi + +new_subject=$(with_pr_id "$subject" "$PR_ID_SUFFIX") +if [ "$new_subject" != "$subject" ]; then + # Only the first line of the message (the title) changes, the body is kept as is. + { echo "$new_subject"; git log -1 --format=%B | tail -n +2; } | git commit --amend --allow-empty --no-verify -q -F - +fi diff --git a/.github/scripts/check-commit-messages b/.github/scripts/check-commit-messages new file mode 100755 index 0000000..2a7383c --- /dev/null +++ b/.github/scripts/check-commit-messages @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# ============================================================================== +# Check the commit message conventions of a pull request. +# +# Usage: check-commit-messages +# +# - ``: base of the range to check. +# - ``: head of the range to check. +# - ``: number of the pull request. +# +# Rules: +# - Format: `(): `, with type in `feat`, `fix`, `docs` or `chore`. +# - `feat`, `fix` and `docs` require a scope. `chore` accepts an optional one. +# - A scope must be in the allowed list of `.github/commit-scopes.txt`. +# - The subject is at most 70 characters long, counting the ` (#)` suffix (already there +# or added at merge). It is separated from the type/scope by exactly one space, starts with a +# lowercase letter or a non-letter character, and does not end with a period. +# - If the commit has a body, the second line of the message must be blank. +# - A commit that only touches files in `tests/` must have a scope starting with `tests/` +# (even for `chore`), and a scope starting with `tests/` is only allowed for such commits. +# - A `chore` commit touching files in `src/` or `tests/` only triggers a warning (never a failure). +# - A subject ends with at most one pull request ID. +# - If the pull request has several commits, each one should end with ` (#)`. +# With a single commit nothing is checked: the ID is added when the pull request is merged. +# ============================================================================== + +set -euo pipefail + +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +readonly SCRIPT_DIR + +readonly SCOPES_FILE_PATH="${SCRIPT_DIR}/../commit-scopes.txt" + +readonly MAX_SUBJECT_LENGTH=70 + +source "${SCRIPT_DIR}/lib/git.sh" +source "${SCRIPT_DIR}/lib/pr-id.sh" + +# ============================================================================== + +errors=0 + +report_error() { + echo "::error::$1" + errors=$((errors + 1)) +} + +report_warning() { + echo "::warning::$1" +} + +# ------------------------------------------------------------------------------ + +if [ "$#" -ne 3 ]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +BASE_SHA=$(resolve_commit "$1") +readonly BASE_SHA + +HEAD_SHA=$(resolve_commit "$2") +readonly HEAD_SHA + +readonly PR_NUMBER=$3 + +if ! is_pr_number "$PR_NUMBER"; then + report_error "Invalid pull request number: \`${PR_NUMBER}\`." + exit 2 +fi + +# Read the allowed scopes, one per line, ignoring comments and blank lines. +mapfile -t scopes < <(grep -vE '^[[:space:]]*(#|$)' "$SCOPES_FILE_PATH" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g') +if [ "${#scopes[@]}" -eq 0 ]; then + report_error "No allowed scope found in \`${SCOPES_FILE_PATH}\`." + exit 2 +fi + +# ------------------------------------------------------------------------------ + +# Print the reason why a message (without PR ID) does not match `COMMIT_REGEX`. +get_format_error() { + local message=$1 + if ! [[ $message =~ ^(feat|fix|docs|chore)($|[^[:alnum:]_-]) ]]; then + echo "the type must be \`feat\`, \`fix\`, \`docs\` or \`chore\` " + elif [[ $message =~ ^(feat|fix|docs)($|[^\(]) ]]; then + echo "this type requires a scope, one of [${DISPLAYED_SCOPES%, }]" + elif [[ $message =~ ^[a-z]+\( ]] && ! [[ $message =~ ^[a-z]+${SCOPE_REGEX} ]]; then + echo "invalid scope, expected one of [${DISPLAYED_SCOPES%, }]" + else + echo "expected \`: \` (colon and one space) after the type or the scope, then a summary" + fi +} + +# ------------------------------------------------------------------------------ + +# Escape the regex special characters of each scope, then join them with `|`. +ALLOWED_SCOPES=$(printf '%s\n' "${scopes[@]}" | sed -E 's/[][\\.^$*+?(){}|]/\\&/g' | paste -sd'|') +readonly ALLOWED_SCOPES + +DISPLAYED_SCOPES=$(printf "\`%s\`, " "${scopes[@]}") +readonly DISPLAYED_SCOPES + +readonly SUBJECT_REGEX='.*[^ ]' +readonly SCOPE_REGEX="\\((${ALLOWED_SCOPES})\\)" +readonly COMMIT_REGEX="^((feat|fix|docs)${SCOPE_REGEX}|chore(${SCOPE_REGEX})?): ${SUBJECT_REGEX}$" + +# ------------------------------------------------------------------------------ + +mapfile -t hashes < <(git log --no-merges --format=%H "${BASE_SHA}..${HEAD_SHA}") + +# ------------------------------------------------------------------------------ + +get_changed_files() { + git diff-tree --root --no-commit-id --name-only -r "$1" +} + +is_touching_only_tests() { + local files + files=$(get_changed_files "$1") + [ -n "$files" ] && ! grep -qv '^tests/' <<< "$files" +} + +is_touching_src_or_tests() { + local files + files=$(get_changed_files "$1") + grep -qE '^(src|tests)/' <<< "$files" +} + +# ------------------------------------------------------------------------------ + +PR_ID_SUFFIX=$(get_pr_id_suffix "$PR_NUMBER") +readonly PR_ID_SUFFIX + +for hash in "${hashes[@]}"; do + subject=$(git log -1 --format=%s "$hash") + # The ID is optional here: the format is checked without it. + message=$(strip_pr_id "$subject") + + if has_pr_id "$message"; then + report_error "Commit subject must not end with several pull request IDs: \"${subject}\"." + continue + fi + + if ! [[ $message =~ $COMMIT_REGEX ]]; then + reason=$(get_format_error "$message") + report_error "Invalid commit format, ${reason}: \"${subject}\"." + continue + fi + + subject_length=$((${#message} + ${#PR_ID_SUFFIX})) + if [ "$subject_length" -gt "$MAX_SUBJECT_LENGTH" ]; then + report_error "Commit subject must be at most ${MAX_SUBJECT_LENGTH} characters long including \`${PR_ID_SUFFIX}\`, got ${subject_length}: \"${subject}\"." + fi + + summary=${message#*: } + if [[ $summary == " "* ]]; then + report_error "Commit subject must be separated from the type by exactly one space: \"${subject}\"." + fi + if [[ $summary =~ ^[[:upper:]] ]]; then + report_error "Commit subject must not start with an uppercase letter: \"${subject}\"." + fi + if [[ $summary == *. ]]; then + report_error "Commit subject must not end with a period: \"${subject}\"." + fi + + scope="" + if [[ $message =~ ^[a-z]+\(([^\)]+)\) ]]; then + scope=${BASH_REMATCH[1]} + fi + if is_touching_only_tests "$hash"; then + if [[ $scope != tests/* ]]; then + report_error "Commit only touches \`tests/\`, its scope must start with \`tests/\`: \"${subject}\"." + fi + elif [[ $scope == tests/* ]]; then + report_error "Scope \`${scope}\` is only allowed for commits that only touch \`tests/\`: \"${subject}\"." + fi + + if [[ $message == chore* ]] && is_touching_src_or_tests "$hash"; then + report_warning "Commit \`chore\` touches files in \`src/\` or \`tests/\`, consider \`feat\`, \`fix\` or \`docs\`: \"${subject}\"." + fi + + if [ "${#hashes[@]}" -gt 1 ] && [[ $subject != *"$PR_ID_SUFFIX" ]]; then + report_warning "Commit message must end with \"${PR_ID_SUFFIX}\": \"${subject}\"." + fi + + second_line=$(git log -1 --format=%B "$hash" | sed -n 2p) + if [ -n "$second_line" ]; then + report_error "Commit message must have a blank line between the subject and the body: \"${subject}\"." + fi +done + +if [ "$errors" -ne 0 ]; then + exit 1 +fi +echo "${#hashes[@]} commit message(s) checked: OK." diff --git a/.github/scripts/lib/git.sh b/.github/scripts/lib/git.sh new file mode 100644 index 0000000..0a13b01 --- /dev/null +++ b/.github/scripts/lib/git.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# ============================================================================== +# Git helpers. +# ============================================================================== + +# Print the full SHA of the commit a revision points to, or fail with the exit code 2 (invalid +# argument) if there is none. +resolve_commit() { + if ! git rev-parse --verify --quiet "$1^{commit}"; then + echo "Unknown commit: \`$1\`." >&2 + exit 2 + fi +} diff --git a/.github/scripts/lib/pr-id.sh b/.github/scripts/lib/pr-id.sh new file mode 100644 index 0000000..d4205a2 --- /dev/null +++ b/.github/scripts/lib/pr-id.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2026 Vates SAS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# ============================================================================== +# Helpers to handle the pull request ID (` (#)`) at the end of a commit title. +# ============================================================================== + +# Matches one pull request ID, with its leading space, at the end of a title. +readonly PR_ID_REGEX=' \(#[0-9]+\)$' + +# Succeed if the argument is a valid pull request number. +is_pr_number() { + [[ $1 =~ ^[0-9]+$ ]] +} + +# Print the suffix to add to a commit title for the given pull request number. +get_pr_id_suffix() { + echo " (#$1)" +} + +# True if a commit title ends with a pull request ID. +has_pr_id() { + [[ $1 =~ $PR_ID_REGEX ]] +} + +# Print a commit title without its last pull request ID, if any. +strip_pr_id() { + local subject=$1 + if [[ $subject =~ $PR_ID_REGEX ]]; then + subject=${subject%"${BASH_REMATCH[0]}"} + fi + echo "$subject" +} + +# Print a commit title without its trailing pull request IDs. +strip_pr_ids() { + local subject=$1 + while has_pr_id "$subject"; do + subject=$(strip_pr_id "$subject") + done + echo "$subject" +} + +# Print a commit title with its trailing pull request IDs replaced by the given suffix. +with_pr_id() { + local subject=$1 + local pr_id_suffix=$2 + echo "$(strip_pr_ids "$subject")${pr_id_suffix}" +} diff --git a/.github/workflows/add-pr-id.yml b/.github/workflows/add-pr-id.yml new file mode 100644 index 0000000..11359b3 --- /dev/null +++ b/.github/workflows/add-pr-id.yml @@ -0,0 +1,136 @@ +# Comment `/add-pr-id` on a pull request to add its ` (#)` suffix to the title of every commit. +# `issue_comment` workflows always run the version of this file (and of the script) of the default +# branch, never the one of the pull request. + +name: Add pull request ID + +on: + issue_comment: + types: [created] + +permissions: + contents: write + pull-requests: write + +# Ensure we only run one job for one update on a PR. +concurrency: + group: add-pr-id-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + add-pr-id: + name: Add pull request ID + # Check command name and permissions to run the job. + if: >- + github.event.issue.pull_request && + github.event.comment.body == '/add-pr-id' && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.issue.number }} + steps: + - name: Check commenter write permission + env: + COMMENTER: ${{ github.event.comment.user.login }} + run: | + role=$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${COMMENTER}/permission" --jq .role_name) + case "$role" in + admin|maintain|write) ;; + *) + gh pr comment "$PR_NUMBER" --body "\`/add-pr-id\` requires write access to this repository." + exit 1 + ;; + esac + + - name: Get pull request + id: pr + run: | + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq ' + "base_sha=\(.base.sha)", + "head_sha=\(.head.sha)", + "head_ref=\(.head.ref)", + "is_same_repo=\(.head.repo.full_name == env.GITHUB_REPOSITORY)", + "state=\(.state)" + ' >> "$GITHUB_OUTPUT" + + - name: Refuse unsupported pull request + if: ${{ steps.pr.outputs.is_same_repo != 'true' || steps.pr.outputs.state != 'open' }} + run: | + gh pr comment "$PR_NUMBER" --body "\`/add-pr-id\` only works on open pull requests whose branch is in this repository." + exit 1 + + # /!\ The job code is run from the default branch, not from the pull request. + - name: Checkout tools + uses: actions/checkout@v6 + with: + path: tools + + - name: Checkout pull request + uses: actions/checkout@v6 + with: + path: pr + ref: ${{ steps.pr.outputs.head_sha }} + fetch-depth: 0 + + - name: Add the ID to the commits + id: add + working-directory: pr + env: + BASE_SHA: ${{ steps.pr.outputs.base_sha }} + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # With a single commit the ID is added by GitHub at merge: nothing to do (see + # `check-commit-messages`). + if [ "$(git rev-list --count "${BASE_SHA}..${HEAD_SHA}")" -le 1 ]; then + echo "The pull request has a single commit: its ID is added when it is merged, nothing to do." | tee ../output.txt + echo "pushed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if ! ../tools/.github/scripts/add-commit-pr-id "$BASE_SHA" "$HEAD_SHA" "$PR_NUMBER" 2>&1 | tee ../output.txt; then + echo "failed=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$(git rev-parse HEAD)" = "$HEAD_SHA" ]; then + echo "pushed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "pushed=true" >> "$GITHUB_OUTPUT" + + - name: Push + id: push + if: ${{ steps.add.outputs.pushed == 'true' }} + working-directory: pr + env: + HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + HEAD_REF: ${{ steps.pr.outputs.head_ref }} + # Fails if somebody pushed to the branch in the meantime. + run: git push "--force-with-lease=refs/heads/${HEAD_REF}:${HEAD_SHA}" origin "HEAD:refs/heads/${HEAD_REF}" + + - name: Report + if: ${{ always() && steps.add.conclusion != 'skipped' }} + env: + FAILED: ${{ steps.add.outputs.failed }} + PUSH_OUTCOME: ${{ steps.push.outcome }} + run: | + if [ "$FAILED" = "true" ]; then + status="Nothing was changed." + elif [ "$PUSH_OUTCOME" = "failure" ]; then + status="The branch was updated in the meantime, nothing was pushed: run \`/add-pr-id\` again." + else + status="Done." + fi + { + echo "\`/add-pr-id\`: ${status}" + echo + echo '```' + cat output.txt + echo '```' + } > comment.md + gh pr comment "$PR_NUMBER" --body-file comment.md diff --git a/.github/workflows/commit-conventions.yml b/.github/workflows/commit-conventions.yml new file mode 100644 index 0000000..2f43d9a --- /dev/null +++ b/.github/workflows/commit-conventions.yml @@ -0,0 +1,25 @@ +name: Commit conventions + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +jobs: + commit-messages: + name: Commit messages + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check commit messages + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: .github/scripts/check-commit-messages "$BASE_SHA" "$HEAD_SHA" "$PR_NUMBER"