diff --git a/.github/workflows/commit-lint-shell.yaml b/.github/workflows/commit-lint-shell.yaml deleted file mode 100644 index f93d985..0000000 --- a/.github/workflows/commit-lint-shell.yaml +++ /dev/null @@ -1,93 +0,0 @@ ---- -name: Lint Commit Messages - -on: - pull_request: - merge_group: - -jobs: - commit-lint: - runs-on: - group: infra1-runners-arc - labels: runners-small - permissions: - contents: read - pull-requests: read - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 100 - - id: set_commit_range - name: Set commit range based on event type - shell: bash - run: | - if [[ ${{ github.event_name }} == 'pull_request' ]]; then - echo "commit_range=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT - elif [[ ${{ github.event_name }} == 'merge_group' ]]; then - echo "commit_range=${{ github.event.merge_group.base_sha }}..${{ github.event.merge_group.head_sha }}" >> $GITHUB_OUTPUT - else - echo "Unsupported event type: ${{ github.event_name }}" - exit 1 - fi - - name: Validate commits - if: success() - shell: bash - run: | - # Validate commits against Conventional Commits guidelines and internal rules - - # Define conventional commit regex with optional breaking change mark - CONVENTIONAL_REGEX="^(build|chore|ci|docs|feat|fix|perf|refactor|revert|reapply|style|test)(\(.+\))?!?: .{1,70}" - - # Get commit subjects and hashes - COMMITS=$(git log --reverse --no-merges --pretty=format:"%h %s" ${{ steps.set_commit_range.outputs.commit_range }}) - - # Initialize violation flag and message - VIOLATIONS="" - - # Check each commit subject - if [[ -n "$COMMITS" ]]; then - while IFS= read -r COMMIT; do - COMMIT_HASH=$(echo "$COMMIT" | awk '{print $1}') - COMMIT_SUBJECT=$(echo "$COMMIT" | cut -d' ' -f2-) - COMMIT_VIOLATIONS="" - - if [[ "$COMMIT_SUBJECT" =~ ^([Rr]evert|[Rr]eapply) ]]; then - continue - fi - - if ! [[ "$COMMIT_SUBJECT" =~ $CONVENTIONAL_REGEX ]]; then - COMMIT_VIOLATIONS+=":x: does not follow Conventional Commits guidelines\n" - fi - - if [[ ! "$COMMIT_SUBJECT" =~ ^chore(\(deps\):|:[[:space:]]bump) ]] && [[ ${#COMMIT_SUBJECT} -gt 70 ]]; then - COMMIT_VIOLATIONS+=":x: exceeds 70 characters\n" - fi - - REPO_NAME="${{ github.repository }}" - if [[ "$REPO_NAME" =~ ^(.*/)?(gdc-nas|gdc-ui|gooddata-ui-sdk|gdc-panther)$ ]]; then - if [[ ! "$COMMIT_SUBJECT" =~ ^chore.*:[[:space:]][Uu]pdate ]]; then - TRAILERS=$(git show -s --format=%B "$COMMIT_HASH" | git interpret-trailers --parse) - if ! echo "$TRAILERS" | grep -iqE '^risk:\s*(nonprod|low|high)'; then - COMMIT_VIOLATIONS+=":x: does not contain a valid risk trailer\n" - fi - fi - fi - - if [[ -n "$COMMIT_VIOLATIONS" ]]; then - VIOLATIONS+="\`$COMMIT_HASH $COMMIT_SUBJECT\`\n$COMMIT_VIOLATIONS\n" - fi - done <<< "$COMMITS" - fi - - # Output violations if any - if [[ -n "$VIOLATIONS" ]]; then - echo -e "$VIOLATIONS" - echo "## Commit Lint Results" >> $GITHUB_STEP_SUMMARY - echo "### Violations" >> $GITHUB_STEP_SUMMARY - echo -e "$VIOLATIONS" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "All commit messages follow Conventional Commits guidelines." - echo "## Commit Lint Results" >> $GITHUB_STEP_SUMMARY - echo ":white_check_mark: All commit messages follow Conventional Commits guidelines." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/commit-lint.yaml b/.github/workflows/commit-lint.yaml index a8dcba0..29fa956 100644 --- a/.github/workflows/commit-lint.yaml +++ b/.github/workflows/commit-lint.yaml @@ -12,87 +12,85 @@ jobs: labels: runners-small permissions: contents: read - pull-requests: read + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }} steps: - - uses: actions/checkout@v4 - - run: | - set -eu - cat << EOF > commitlint.config.mjs - /* eslint-disable import/no-extraneous-dependencies */ - import { maxLength } from '@commitlint/ensure'; - import { default as conventionalConfig } from '@commitlint/config-conventional'; - import { execSync } from 'child_process'; - import toLines from '@commitlint/to-lines'; + - name: Validate commits + shell: bash + run: | + set -euo pipefail - const headerMaxLength = 70; + : "${BASE_SHA:?unsupported event: no base SHA in event payload}" + : "${HEAD_SHA:?unsupported event: no head SHA in event payload}" - const validateHeaderMaxLengthIgnoringDeps = (parsedCommit) => { - const { type, scope, header } = parsedCommit; - const isDepsCommit = type === 'chore' && scope === 'deps'; + HEADER_MAX_LENGTH=100 + CONVENTIONAL_TYPES='build|chore|ci|docs|feat|fix|perf|refactor|revert|reapply|style|test' + CONVENTIONAL_REGEX="^(${CONVENTIONAL_TYPES})(\(.+\))?!?: .+" + # Bot-generated dep/version bumps (renovate, extimage, ...) - skip length and risk checks + BOT_BUMP_REGEX='^chore(\(deps\))?:[[:space:]]([Uu]pdate|bump)' + # Repos that require a `risk:` trailer + RISK_REPOS_REGEX='^(.*/)?(gdc-nas|gdc-ui|gooddata-ui-sdk|gdc-panther)$' - return [ - isDepsCommit || maxLength(header, headerMaxLength), - \`header must not be longer than \${headerMaxLength}\`, - ]; - } + # Fetch non-merge commits in the range via the compare API (no checkout required). + # Output: one JSON object per line - {sha, message}. + COMMITS=$(gh api --paginate "repos/$REPO/compare/$BASE_SHA...$HEAD_SHA" \ + --jq '.commits[] | select((.parents | length) <= 1) | {sha: .sha[0:7], message: .commit.message} | @json') - const validateRisk = process.env.GITHUB_REPOSITORY?.match(/gooddata\/(gdc-nas|gdc-ui|gooddata-ui-sdk|gdc-panther)/) + VIOLATIONS="" + if [[ -n "$COMMITS" ]]; then + while IFS= read -r COMMIT_JSON; do + COMMIT_HASH=$(jq -r '.sha' <<< "$COMMIT_JSON") + COMMIT_MESSAGE=$(jq -r '.message' <<< "$COMMIT_JSON") + COMMIT_SUBJECT="${COMMIT_MESSAGE%%$'\n'*}" + COMMIT_VIOLATIONS="" - export default { - extends: ['@commitlint/config-conventional'], - plugins: [ - 'commitlint-plugin-function-rules', - { - rules: { - 'risk-rule': (parsedCommit) => { - const { type, subject } = parsedCommit; - if (type === 'chore' && subject?.startsWith('update')) return [true]; // skip renovate and extimage bumps + # Skip auto-generated revert/reapply commits ("Revert ...", "Reapply ...") + if [[ "$COMMIT_SUBJECT" =~ ^([Rr]evert|[Rr]eapply) ]]; then + continue + fi - try { - const trailers = execSync('git interpret-trailers --parse', { - input: parsedCommit.raw || '', - }).toString(); - const matches = toLines(trailers)?.filter((ln) => ln.match(/^risk:\s*(nonprod|low|high)/i))?.length; - return [ - matches === 1, - \`Should have exactly one risk label of value nonprod|low|high, but \${matches} found.\`, - ]; - } catch (err) { - console.error(err.toString()); - return [false, 'Error while trying to find risk label']; - } - }, - }, - }, - ], - rules: { - 'header-max-length': [0], - 'body-max-line-length': [0], - 'function-rules/header-max-length': [ - 2, - 'always', - validateHeaderMaxLengthIgnoringDeps, - ], - 'subject-case': [ - 2, - 'never', - ['upper-case', 'camel-case', 'kebab-case', 'pascal-case', 'snake-case'], - ], - 'type-enum': [ - 2, - 'always', - [ - ...conventionalConfig.rules['type-enum'][2], // Include default types - 'config', // Configuration change i.e. hiera or gitops config change - ], - ], - 'risk-rule': [ - validateRisk ? 2 : 0, - 'always', - ], - }, - } - EOF - cat commitlint.config.mjs - shell: bash - - uses: wagoid/commitlint-github-action@v6 + IS_BOT_BUMP=false + if [[ "$COMMIT_SUBJECT" =~ $BOT_BUMP_REGEX ]]; then + IS_BOT_BUMP=true + fi + + if ! [[ "$COMMIT_SUBJECT" =~ $CONVENTIONAL_REGEX ]]; then + COMMIT_VIOLATIONS+=":x: does not follow Conventional Commits guidelines\n" + fi + + if [[ "$IS_BOT_BUMP" == "false" ]] && (( ${#COMMIT_SUBJECT} > HEADER_MAX_LENGTH )); then + COMMIT_VIOLATIONS+=":x: exceeds $HEADER_MAX_LENGTH characters\n" + fi + + if [[ "$REPO" =~ $RISK_REPOS_REGEX ]] && [[ "$IS_BOT_BUMP" == "false" ]]; then + TRAILERS=$(git interpret-trailers --parse <<< "$COMMIT_MESSAGE") + if ! grep -iqE '^risk:[[:space:]]*(nonprod|low|high)' <<< "$TRAILERS"; then + COMMIT_VIOLATIONS+=":x: does not contain a valid risk trailer\n" + fi + fi + + if [[ -n "$COMMIT_VIOLATIONS" ]]; then + VIOLATIONS+="\`$COMMIT_HASH $COMMIT_SUBJECT\`\n$COMMIT_VIOLATIONS\n" + fi + done <<< "$COMMITS" + fi + + { + echo "## Commit Lint Results" + if [[ -n "$VIOLATIONS" ]]; then + echo "### Violations" + echo -e "$VIOLATIONS" + else + echo ":white_check_mark: All commit messages follow Conventional Commits guidelines." + fi + } >> "$GITHUB_STEP_SUMMARY" + + if [[ -n "$VIOLATIONS" ]]; then + echo -e "$VIOLATIONS" + exit 1 + fi + + echo "All commit messages follow Conventional Commits guidelines."