diff --git a/.github/workflows/auto-tag-release.yml b/.github/workflows/auto-tag-release.yml index bcbce08..0686490 100644 --- a/.github/workflows/auto-tag-release.yml +++ b/.github/workflows/auto-tag-release.yml @@ -5,12 +5,6 @@ on: branches: - main workflow_dispatch: - inputs: - allow_current_head: - description: Allow tagging current HEAD even if it is not a release-please commit - required: false - type: boolean - default: false permissions: {} @@ -21,105 +15,59 @@ concurrency: jobs: tag: permissions: - contents: read + contents: write runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 5 env: - ALLOW_CURRENT_HEAD: ${{ github.event.inputs.allow_current_head || 'false' }} RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} steps: - - name: Clone project - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true - token: ${{ secrets.RELEASE_PLEASE_TOKEN }} - - - name: Validate VERSION and create tag - shell: bash - run: | - set -euo pipefail - - if [[ -z "${RELEASE_PLEASE_TOKEN:-}" ]]; then - echo "error: RELEASE_PLEASE_TOKEN is required so the tag push can trigger the package release workflow." >&2 - exit 1 - fi - - head_subject="$(git log -1 --pretty=%s)" - head_message="$(git log -1 --pretty=%B)" - - if [[ "${GITHUB_EVENT_NAME}" == "push" || "${ALLOW_CURRENT_HEAD}" != "true" ]]; then - is_release_commit=false - while IFS= read -r line; do - if [[ "$line" =~ ^chore\(main\):[[:space:]]release[[:space:]] ]]; then - is_release_commit=true - break - fi - done <<< "$head_message" - - if [[ "$is_release_commit" != true ]]; then - echo "Head commit is not a release-please commit. Skipping tag creation." - exit 0 - fi - fi - - [[ -f VERSION ]] || { echo "error: VERSION is missing" >&2; exit 1; } - version=$(tr -d '\r\n' < VERSION) - semver_regex='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' - if [[ ! "$version" =~ $semver_regex ]]; then - echo "error: VERSION '$version' is not valid semantic versioning" >&2 - exit 1 - fi - - tag="v${version}" - if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then - echo "Tag $tag already exists. Nothing to do." - exit 0 - fi - - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - git tag -a "$tag" -m "$tag" - git push origin "$tag" - - - name: Mark release PR as tagged - shell: bash - env: - GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} - run: | - set -euo pipefail - - if [[ -z "${GH_TOKEN:-}" ]]; then - echo "error: RELEASE_PLEASE_TOKEN is required to update release PR labels." >&2 - exit 1 - fi - - if [[ "${GITHUB_EVENT_NAME}" != "push" ]]; then - echo "Manual run detected. Skipping release PR label update." - exit 0 - fi - - head_subject="$(git log -1 --pretty=%s)" - head_message="$(git log -1 --pretty=%B)" - - pr_number="" - if [[ "$head_subject" =~ ^Merge\ pull\ request\ \#([0-9]+)\ from\ ]]; then - pr_number="${BASH_REMATCH[1]}" - else - while IFS= read -r line; do - if [[ "$line" =~ ^chore\(main\):\ release\ .*\ \(#([0-9]+)\)$ ]]; then - pr_number="${BASH_REMATCH[1]}" - break - fi - done <<< "$head_message" - fi - - if [[ -z "$pr_number" ]]; then - echo "Head commit is not a release-please merge commit. Skipping label update." - exit 0 - fi - - gh api --method DELETE "repos/${GITHUB_REPOSITORY}/issues/${pr_number}/labels/autorelease%3A%20pending" >/dev/null || true - - echo "Removed autorelease: pending label from release PR #${pr_number}." + - name: Clone project + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} + + - name: Create release tag + shell: bash + run: | + set -euo pipefail + + if [[ -z "${RELEASE_PLEASE_TOKEN:-}" ]]; then + echo "error: RELEASE_PLEASE_TOKEN is required so the tag push can trigger release packaging." >&2 + exit 1 + fi + + head_message="$(git log -1 --pretty=%B)" + is_release_commit=false + while IFS= read -r line; do + if [[ "$line" =~ ^chore\(main\):[[:space:]]release[[:space:]] ]]; then + is_release_commit=true + break + fi + done <<< "$head_message" + + if [[ "$is_release_commit" != true ]]; then + echo "Head commit is not a release-please commit. Skipping tag creation." + exit 0 + fi + + [[ -f VERSION ]] || { echo "error: VERSION is missing" >&2; exit 1; } + version=$(tr -d '\r\n' < VERSION) + semver_regex='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' + if [[ ! "$version" =~ $semver_regex ]]; then + echo "error: VERSION '$version' is not valid semantic versioning" >&2 + exit 1 + fi + + tag="v${version}" + if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then + echo "Tag $tag already exists. Nothing to do." + exit 0 + fi + + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git tag -a "$tag" -m "$tag" + git push origin "$tag" diff --git a/.github/workflows/branch-protection.yml b/.github/workflows/branch-protection.yml deleted file mode 100644 index b2ac673..0000000 --- a/.github/workflows/branch-protection.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Enforce branch protection - -on: - workflow_dispatch: - schedule: - - cron: '37 4 * * 1' - -permissions: {} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - protect-main: - permissions: {} - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Require BRANCH_PROTECTION_TOKEN - shell: bash - env: - BRANCH_PROTECTION_TOKEN: ${{ secrets.BRANCH_PROTECTION_TOKEN }} - run: | - set -euo pipefail - if [[ -z "${BRANCH_PROTECTION_TOKEN:-}" ]]; then - echo "error: BRANCH_PROTECTION_TOKEN is required for branch protection sync." >&2 - exit 1 - fi - - - name: Require checks on main - shell: bash - env: - GH_TOKEN: ${{ secrets.BRANCH_PROTECTION_TOKEN }} - run: | - set -euo pipefail - - gh api \ - --method PUT \ - -H "Accept: application/vnd.github+json" \ - "repos/${GITHUB_REPOSITORY}/branches/main/protection" \ - --input - <<'JSON' - { - "required_status_checks": { - "strict": true, - "checks": [ - {"context": "static"}, - {"context": "package"}, - {"context": "lint"}, - {"context": "commit_messages"} - ] - }, - "enforce_admins": false, - "required_pull_request_reviews": null, - "restrictions": null, - "required_linear_history": true, - "allow_force_pushes": false, - "allow_deletions": false, - "block_creations": false, - "required_conversation_resolution": true, - "lock_branch": false, - "allow_fork_syncing": false - } - JSON diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e15ed3..4e8a10a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,23 @@ on: push: branches: - main + paths-ignore: + - '*.md' + - 'docs/**' + - 'LICENSE' + - 'NOTICE' pull_request: + branches: + - main + paths-ignore: + - '*.md' + - 'docs/**' + - 'LICENSE' + - 'NOTICE' workflow_dispatch: -permissions: {} +permissions: + contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -25,23 +38,25 @@ jobs: should_run: ${{ steps.filter.outputs.code_or_release }} steps: - - name: Clone project - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Detect code and release file changes - id: filter - uses: dorny/paths-filter@v3 - with: - filters: | - code_or_release: - - '**/*.lua' - - '**/*.toc' - - 'scripts/**' - - 'VERSION' - - '.release-please-manifest.json' - - 'release-please-config.json' + - name: Clone project + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect code and release file changes + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + code_or_release: + - '**/*.lua' + - '**/*.toc' + - 'scripts/**' + - 'VERSION' + - '.release-please-manifest.json' + - 'release-please-config.json' + - '.github/workflows/*.yml' + - '.github/workflows/**/*.yml' static: name: static @@ -53,16 +68,16 @@ jobs: timeout-minutes: 10 steps: - - name: Clone project - uses: actions/checkout@v4 + - name: Clone project + uses: actions/checkout@v4 - - name: Install Lua 5.1 - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends lua5.1 + - name: Install Lua 5.1 + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends lua5.1 - - name: Run static checks - run: bash scripts/check-static.sh + - name: Run static checks + run: bash scripts/check-static.sh package: name: package @@ -78,22 +93,42 @@ jobs: GITHUB_OAUTH: ${{ github.token }} steps: - - name: Clone project - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build dry-run package - uses: BigWigsMods/packager@v2 - with: - args: -d - - - name: Upload packaged artifact - uses: actions/upload-artifact@v4 - with: - name: calmchat-package-dry-run - path: | - .release/*.zip - .release/**/*.zip - if-no-files-found: warn - include-hidden-files: true + - name: Clone project + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build dry-run package + uses: BigWigsMods/packager@v2 + with: + args: -d + + - name: Upload packaged artifact + uses: actions/upload-artifact@v4 + with: + name: calmchat-package-dry-run + path: | + .release/*.zip + .release/**/*.zip + if-no-files-found: warn + include-hidden-files: true + + required: + name: required + if: always() + needs: + - changes + - static + - package + runs-on: ubuntu-latest + timeout-minutes: 2 + + steps: + - name: Ensure required checks passed + run: | + if [ "${{ needs.changes.result }}" = "failure" ] || + [ "${{ needs.static.result }}" = "failure" ] || + [ "${{ needs.package.result }}" = "failure" ]; then + echo "One or more required checks failed" + exit 1 + fi diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index a123aaa..f01b820 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -8,82 +8,62 @@ on: - synchronize - reopened -permissions: {} +permissions: + pull-requests: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: - lint: - permissions: - pull-requests: read + conventional: + name: conventional runs-on: ubuntu-latest timeout-minutes: 10 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + REPO: ${{ github.repository }} steps: - - name: Validate PR title as Conventional Commit - uses: amannn/action-semantic-pull-request@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - types: | - feat - fix - refactor - perf - docs - chore - ci - test - build - requireScope: false - subjectPattern: ^[a-z].+$ - wip: false + - name: Validate PR title and commits + shell: bash + run: | + set -euo pipefail - commit_messages: - permissions: - pull-requests: read - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Validate PR commits follow Conventional Commits - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - set -euo pipefail + regex='^(feat|fix|refactor|perf|docs|chore|ci|test|build)(\([a-z0-9._/-]+\))?(!)?: [a-z].+' - regex='^(feat|fix|refactor|perf|docs|chore|ci|test|build)(\([a-z0-9._/-]+\))?(!)?: .+' - page=1 + if [[ ! "$PR_TITLE" =~ $regex ]]; then + echo "error: PR title '$PR_TITLE' is not a conventional commit" >&2 + exit 1 + fi - while :; do - response=$(curl -fsS --retry 6 --retry-all-errors --retry-delay 2 --retry-max-time 120 \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${GH_TOKEN}" \ - "https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/commits?per_page=100&page=${page}") + page=1 + while :; do + response=$(curl -fsS --retry 6 --retry-all-errors --retry-delay 2 --retry-max-time 120 \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + "https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/commits?per_page=100&page=${page}") - count=$(printf '%s' "$response" | jq 'length') - if [[ "$count" -eq 0 ]]; then - break - fi + count=$(printf '%s' "$response" | jq 'length') + if [[ "$count" -eq 0 ]]; then + break + fi - subjects=$(printf '%s' "$response" | jq -r '.[].commit.message | split("\n")[0]') - while IFS= read -r subject; do - [[ -n "$subject" ]] || continue + subjects=$(printf '%s' "$response" | jq -r '.[].commit.message | split("\n")[0]') + while IFS= read -r subject; do + [[ -n "$subject" ]] || continue - if [[ "$subject" =~ ^Merge[[:space:]] ]] || [[ "$subject" =~ ^Revert[[:space:]] ]]; then - continue - fi + if [[ "$subject" =~ ^Merge[[:space:]] ]] || [[ "$subject" =~ ^Revert[[:space:]] ]]; then + continue + fi - if [[ ! "$subject" =~ $regex ]]; then - echo "error: commit subject '$subject' is not a conventional commit" >&2 - exit 1 - fi - done <<< "$subjects" + if [[ ! "$subject" =~ $regex ]]; then + echo "error: commit subject '$subject' is not a conventional commit" >&2 + exit 1 + fi + done <<< "$subjects" - page=$((page + 1)) - done + page=$((page + 1)) + done diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 346c325..763691c 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -4,6 +4,11 @@ on: push: branches: - main + paths-ignore: + - '*.md' + - 'docs/**' + - 'LICENSE' + - 'NOTICE' workflow_dispatch: permissions: {} @@ -20,52 +25,52 @@ jobs: timeout-minutes: 10 steps: - - name: Require RELEASE_PLEASE_TOKEN - shell: bash - env: - RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} - run: | - set -euo pipefail + - name: Require RELEASE_PLEASE_TOKEN + shell: bash + env: + RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} + run: | + set -euo pipefail - if [[ -z "${RELEASE_PLEASE_TOKEN:-}" ]]; then - echo "error: RELEASE_PLEASE_TOKEN is required for release-please automation." >&2 - exit 1 - fi + if [[ -z "${RELEASE_PLEASE_TOKEN:-}" ]]; then + echo "error: RELEASE_PLEASE_TOKEN is required for release-please automation." >&2 + exit 1 + fi - - name: Clone project - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true + - name: Clone project + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - - name: Detect releasable commits - id: releasable - shell: bash - run: | - set -euo pipefail + - name: Detect releasable commits + id: releasable + shell: bash + run: | + set -euo pipefail - releasable_regex='^(feat|fix|perf|refactor)(\([a-z0-9._/-]+\))?(!)?: .+' - last_tag="$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || true)" + releasable_regex='^(feat|fix|perf|refactor)(\([a-z0-9._/-]+\))?(!)?: .+' + last_tag="$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || true)" - if [[ -n "$last_tag" ]]; then - range="${last_tag}..HEAD" - else - range="HEAD" - fi + if [[ -n "$last_tag" ]]; then + range="${last_tag}..HEAD" + else + range="HEAD" + fi - if git log --format=%s "$range" | grep -Eq "$releasable_regex"; then - echo "enabled=true" >> "$GITHUB_OUTPUT" - echo "Found releasable commits in $range." - else - echo "enabled=false" >> "$GITHUB_OUTPUT" - echo "No releasable commits (feat|fix|perf|refactor) found in $range." - fi + if git log --format=%s "$range" | grep -Eq "$releasable_regex"; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + echo "Found releasable commits in $range." + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "No releasable commits (feat|fix|perf|refactor) found in $range." + fi - - name: Run release-please - if: steps.releasable.outputs.enabled == 'true' - uses: googleapis/release-please-action@v4 - with: - token: ${{ secrets.RELEASE_PLEASE_TOKEN }} - config-file: release-please-config.json - manifest-file: .release-please-manifest.json - skip-github-release: true + - name: Run release-please + if: steps.releasable.outputs.enabled == 'true' + uses: googleapis/release-please-action@v4 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + skip-github-release: true