diff --git a/.github/workflows/commitmsg-conform.yml b/.github/workflows/commitmsg-conform.yml new file mode 100644 index 0000000..c55bca8 --- /dev/null +++ b/.github/workflows/commitmsg-conform.yml @@ -0,0 +1,123 @@ +name: Commit Message Conformance + +on: + workflow_call: + inputs: + config: + type: string + default: | + policies: + - type: commit + spec: + dco: false + gpg: + required: false + gitHubOrganization: aws-user-group-nz + spellcheck: + locale: US + maximumOfOneCommit: false + header: + length: 89 + imperative: true + case: lower + invalidLastCharacters: . + body: + required: true + conventional: + types: + - chore + - ci + - docs + - feat + - fix + - refactor + - release + - revert + - style + - test + scopes: [".*"] + +jobs: + conform: + runs-on: ubuntu-latest + steps: + - name: checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.head_ref }} + - id: determine-existing-config + run: | + if [ -f .conform.yaml ]; then + echo "exists=true" >> $GITHUB_OUTPUT + fi + - name: write config + if: ${{ steps.determine-existing-config.outputs.exists != 'true' }} + run: | + cat << EOF > .conform.yaml + ${{ inputs.config }} + EOF + - name: conform + env: + INPUT_TOKEN: ${{ github.token }} + run: | + docker run \ + -e "ACTIONS_CACHE_URL" \ + -e "ACTIONS_RUNTIME_TOKEN" \ + -e "ACTIONS_RUNTIME_URL" \ + -e "GITHUB_ACTION" \ + -e "GITHUB_ACTION_REF" \ + -e "GITHUB_ACTION_REPOSITORY" \ + -e "GITHUB_ACTOR" \ + -e "GITHUB_ACTOR_ID" \ + -e "GITHUB_API_URL" \ + -e "GITHUB_BASE_REF" \ + -e "GITHUB_ENV" \ + -e "GITHUB_EVENT_NAME" \ + -e "GITHUB_EVENT_PATH" \ + -e "GITHUB_GRAPHQL_URL" \ + -e "GITHUB_HEAD_REF" \ + -e "GITHUB_JOB" \ + -e "GITHUB_OUTPUT" \ + -e "GITHUB_PATH" \ + -e "GITHUB_REF" \ + -e "GITHUB_REF_NAME" \ + -e "GITHUB_REF_PROTECTED" \ + -e "GITHUB_REF_TYPE" \ + -e "GITHUB_REPOSITORY" \ + -e "GITHUB_REPOSITORY_ID" \ + -e "GITHUB_REPOSITORY_OWNER" \ + -e "GITHUB_REPOSITORY_OWNER_ID" \ + -e "GITHUB_RETENTION_DAYS" \ + -e "GITHUB_RUN_ATTEMPT" \ + -e "GITHUB_RUN_ID" \ + -e "GITHUB_RUN_NUMBER" \ + -e "GITHUB_SERVER_URL" \ + -e "GITHUB_SHA" \ + -e "GITHUB_STATE" \ + -e "GITHUB_STEP_SUMMARY" \ + -e "GITHUB_TRIGGERING_ACTOR" \ + -e "GITHUB_WORKFLOW" \ + -e "GITHUB_WORKFLOW_REF" \ + -e "GITHUB_WORKFLOW_SHA" \ + -e "GITHUB_WORKSPACE" \ + -e "HOME" \ + -e "INPUT_ARGS" \ + -e "RUNNER_ARCH" \ + -e "RUNNER_ENVIRONMENT" \ + -e "RUNNER_NAME" \ + -e "RUNNER_OS" \ + -e "RUNNER_TEMP" \ + -e "RUNNER_TOOL_CACHE" \ + -e "RUNNER_WORKSPACE" \ + -e CI=true \ + -e GITHUB_ACTIONS=true \ + -e GITHUB_EVENT_PATH=/github/workflow/event.json \ + -e INPUT_TOKEN="$INPUT_TOKEN" \ + -v "$PWD":"/github/workspace" \ + -v "/home/runner/work/_temp/_github_home":"/github/home" \ + -v "/home/runner/work/_temp/_github_workflow":"/github/workflow" \ + -v "/home/runner/work/_temp/_runner_file_commands":"/github/file_commands" \ + --workdir /github/workspace \ + ghcr.io/siderolabs/conform:v0.1.0-alpha.30-1-ga6572d2 \ + enforce --commit-ref="refs/remotes/origin/main" --reporter=github diff --git a/.github/workflows/tag-and-release.yml b/.github/workflows/tag-and-release.yml new file mode 100644 index 0000000..f452f61 --- /dev/null +++ b/.github/workflows/tag-and-release.yml @@ -0,0 +1,185 @@ +name: Tag and Release + +on: + pull_request: + types: [labeled, closed] + push: + branches: + - main + +permissions: + contents: write + +jobs: + tag: + if: | + (github.event_name == 'pull_request' && + github.event.action == 'labeled' && + (github.event.pull_request.labels.*.name == 'version:major' || + github.event.pull_request.labels.*.name == 'version:minor' || + github.event.pull_request.labels.*.name == 'version:patch')) || + (github.event_name == 'pull_request' && + github.event.action == 'closed' && + github.event.pull_request.merged == true && + github.event.pull_request.labels.*.name != 'version:major' && + github.event.pull_request.labels.*.name != 'version:minor' && + github.event.pull_request.labels.*.name != 'version:patch') || + (github.event_name == 'push' && + github.ref == 'refs/heads/main' && + !contains(github.event.head_commit.message, 'Merge pull request')) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get current version + id: get_version + run: | + # Get the latest tag + git fetch --tags + LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || echo "v1.0.0") + CURRENT_VERSION=${LATEST_TAG#v} + + # Debug information + echo "Event name: ${{ github.event_name }}" + echo "Event action: ${{ github.event.action }}" + echo "Commit message: ${{ github.event.head_commit.message }}" + + # Parse current version + if [[ "$CURRENT_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + else + major=1 + minor=0 + patch=0 + fi + + # Ensure we start at v1.0.0 minimum + # If no tags exist or version is v0.x.x, start at v1.0.0 + if [[ "$major" == "0" ]]; then + major=1 + minor=0 + patch=0 + fi + + # Determine version bump type + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + if [[ "${{ github.event.action }}" == "labeled" ]]; then + if [[ "${{ github.event.pull_request.labels.*.name }}" == "version:major" ]]; then + BUMP_TYPE="major" + elif [[ "${{ github.event.pull_request.labels.*.name }}" == "version:minor" ]]; then + BUMP_TYPE="minor" + elif [[ "${{ github.event.pull_request.labels.*.name }}" == "version:patch" ]]; then + BUMP_TYPE="patch" + else + echo "No version label provided. Skipping version bump." + exit 0 + fi + else + # PR was closed/merged without version label + # Check PR title and body for conventional commit types + PR_TITLE="${{ github.event.pull_request.title }}" + PR_BODY="${{ github.event.pull_request.body }}" + + if [[ "$PR_TITLE" =~ ^(feat|feature)(\([a-z0-9-]+\))?: ]]; then + BUMP_TYPE="minor" + elif [[ "$PR_TITLE" =~ ^(fix|bugfix|hotfix)(\([a-z0-9-]+\))?: ]]; then + BUMP_TYPE="patch" + elif [[ "$PR_TITLE" =~ ^(breaking|break)(\([a-z0-9-]+\))?: ]]; then + BUMP_TYPE="major" + else + # Default to patch for merged PRs without clear indicators + BUMP_TYPE="patch" + fi + fi + else + # Only process direct pushes that aren't PR merges + if [[ "${{ contains(github.event.head_commit.message, 'Merge pull request') }}" == "true" ]]; then + echo "Skipping version bump for PR merge commit" + exit 0 + fi + BUMP_TYPE="patch" + fi + + # Bump version with limits + case "$BUMP_TYPE" in + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + minor) + if (( minor < 99 )); then + minor=$((minor + 1)) + patch=0 + else + major=$((major + 1)) + minor=0 + patch=0 + fi + ;; + patch) + if (( patch < 99 )); then + patch=$((patch + 1)) + else + if (( minor < 99 )); then + minor=$((minor + 1)) + patch=0 + else + major=$((major + 1)) + minor=0 + patch=0 + fi + fi + ;; + esac + + NEW_VERSION="${major}.${minor}.${patch}" + MAJOR_VERSION="v${major}" + + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT + echo "major_version=$MAJOR_VERSION" >> $GITHUB_OUTPUT + echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT + echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT + + - name: Generate changelog + id: changelog + run: | + if [ -n "${{ steps.get_version.outputs.latest_tag }}" ]; then + git log "${{ steps.get_version.outputs.latest_tag }}"..HEAD --pretty=format:"- %s" > changelog.txt + else + git log --pretty=format:"- %s" > changelog.txt + fi + + - name: Create and push tags + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + # Check if tag already exists + if git rev-parse "v${{ steps.get_version.outputs.new_version }}" >/dev/null 2>&1; then + echo "Tag v${{ steps.get_version.outputs.new_version }} already exists. Skipping tag creation." + exit 0 + fi + + # Create and push version tag + echo "Creating tag v${{ steps.get_version.outputs.new_version }}" + git tag -a "v${{ steps.get_version.outputs.new_version }}" -m "Release v${{ steps.get_version.outputs.new_version }} (bump: ${{ steps.get_version.outputs.bump_type }})" + git push origin "v${{ steps.get_version.outputs.new_version }}" + + # Update major version tag + echo "Updating major version tag ${{ steps.get_version.outputs.major_version }}" + git tag -fa "${{ steps.get_version.outputs.major_version }}" -m "Update major version tag" + git push origin "${{ steps.get_version.outputs.major_version }}" --force + + # Create GitHub release + gh release create "v${{ steps.get_version.outputs.new_version }}" \ + --title "v${{ steps.get_version.outputs.new_version }}" \ + --notes-file changelog.txt diff --git a/.github/workflows/validate-commits.yml b/.github/workflows/validate-commits.yml new file mode 100644 index 0000000..4ca33af --- /dev/null +++ b/.github/workflows/validate-commits.yml @@ -0,0 +1,14 @@ +name: Validate Commit Messages + +on: + pull_request: {} + +permissions: + statuses: write + checks: write + contents: read + pull-requests: read + +jobs: + commitmsg-conform: + uses: ./.github/workflows/commitmsg-conform.yml diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..3dfb2ed --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,15 @@ +{ + "recommendations": [ + "davidanson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "esbenp.prettier-vscode", + "foxundermoon.shell-format", + "Gruntfuggly.todo-tree", + "mhutchie.git-graph", + "redhat.vscode-yaml", + "streetsidesoftware.code-spell-checker", + "usernamehw.errorlens", + "vscode-icons-team.vscode-icons" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9724fd6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,62 @@ +{ + "files.eol": "\n", + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "__debug_bin": true, + "vendor/": true, + "go.sum": true, + "dist/": true, + "node_modules/": true + }, + "files.trimTrailingWhitespace": true, + "files.trimFinalNewlines": true, + "files.insertFinalNewline": true, + "remote.extensionKind": { + "ms-azuretools.vscode-docker": "ui", + "ms-vscode-remote.remote-containers": "ui" + }, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "prettier.requireConfig": true, + "workbench.iconTheme": "vscode-icons", + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features", + "editor.foldingStrategy": "indentation" + }, + "[json]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2, + "editor.insertSpaces": true + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.wordWrap": "on", + "editor.quickSuggestions": { + "other": true, + "comments": true, + "strings": true + } + }, + "[shellscript]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml", + "editor.formatOnSave": true + }, + "yaml.schemas": { + "https://json.schemastore.org/github-action.json": "action.yml" + }, + "yaml.validate": true, + "yaml.hover": true, + "yaml.completion": true +} diff --git a/README.md b/README.md index c7e6f52..8b759b5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,141 @@ # actions-commitmsg-conform -Reusable GitHub Action to enforce commit message best practices + +A reusable GitHub Action workflow to enforce commit message best practices using [Conform](https://github.com/talos-systems/conform). + +![Deploy Status](https://github.com/aws-user-group-nz/actions-commitmsg-conform/actions/workflows/tag-and-release.yml/badge.svg) + +![GitHub release](https://img.shields.io/github/v/release/aws-user-group-nz/actions-commitmsg-conform) + +## Usage + +### Basic Usage + +```yaml +name: Commit Message Conformance + +on: + pull_request: {} + +permissions: + statuses: write + checks: write + contents: read + pull-requests: read + +jobs: + commitmsg-conform: + uses: aws-user-group-nz/actions-commitmsg-conform/.github/workflows/commitmsg-conform.yml@v1 +``` + +### With Custom Configuration + +```yaml +name: Commit Message Conformance + +on: + pull_request: {} + +permissions: + statuses: write + checks: write + contents: read + pull-requests: read + +jobs: + validate-commit: + uses: aws-user-group-nz/actions-commitmsg-conform/.github/workflows/commitmsg-conform.yml@v1 + with: + config: | + policies: + - type: commit + spec: + dco: false + gpg: + required: false + gitHubOrganization: aws-user-group-nz + spellcheck: + locale: US + maximumOfOneCommit: false + header: + length: 89 + imperative: true + case: lower + invalidLastCharacters: . + body: + required: true + conventional: + types: + - chore + - ci + - docs + - feat + - fix + - refactor + - release + - revert + - style + - test + scopes: [".*"] +``` + +## Versioning + +This workflow follows semantic versioning. You can use it in two ways: + +1. **Major Version Tag** (Recommended): + + ```yaml + uses: aws-user-group-nz/actions-commitmsg-conform/.github/workflows/commitmsg-conform.yml@v1 + ``` + + This will automatically use the latest release within the v1.x.x series. + +2. **Specific Version**: + + ```yaml + uses: aws-user-group-nz/actions-commitmsg-conform/.github/workflows/commitmsg-conform.yml@v1.0.1 + ``` + + This pins to a specific version for maximum stability. + +### Version History + +See the [Releases](../../releases) page for a full list of versions and changes. + +## Configuration + +The workflow accepts a `config` input that allows you to provide a custom Conform configuration. If no configuration is provided, it will use the default configuration which enforces: + +- Conventional commit types (chore, ci, docs, feat, fix, refactor, release, revert, style, test) +- Commit message length limits (89 characters) +- Imperative mood +- Lower case +- No trailing periods +- Required commit body +- Spell checking (US locale) +- Optional DCO and GPG signing + +### Configuration Options + +The configuration supports the following policies: + +- `dco`: Enable/disable Developer Certificate of Origin +- `gpg`: GPG signing requirements +- `spellcheck`: Spell checking configuration +- `maximumOfOneCommit`: Limit to one commit +- `header`: Commit message header formatting +- `body`: Commit message body requirements +- `conventional`: Conventional commit type and scope rules + +## Features + +- Validates commit messages against configurable patterns +- Supports custom Conform configurations +- Easy to integrate into existing workflows +- Runs on pull request events by default +- Uses Docker for consistent execution +- GitHub reporter for detailed feedback + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.