From d3c51da19853a45ad1d6a30000f157cb29c39c64 Mon Sep 17 00:00:00 2001 From: "Calmcacil B." <716671+calmcacil@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:13:35 +0200 Subject: [PATCH 1/2] fix(release): harden package publication --- .github/dependabot.yml | 16 ++ .github/pull_request_template.md | 2 +- .github/workflows/auto-tag-release.yml | 73 -------- .github/workflows/ci.yml | 207 +++++++++------------ .github/workflows/conventional-commits.yml | 69 ------- .github/workflows/publish-release.yml | 70 +++++++ .github/workflows/release-please.yml | 76 -------- .github/workflows/release.yml | 85 +++------ .gitignore | 4 +- .pkgmeta | 7 + AGENTS.md | 4 +- CONTRIBUTING.md | 22 +++ README.md | 3 +- SECURITY.md | 14 ++ docs/CI_RELEASES.md | 83 +++++++++ scripts/check-package.sh | 65 +++++++ scripts/check.sh | 16 ++ scripts/publish-assets.sh | 94 ++++++++++ 18 files changed, 505 insertions(+), 405 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/auto-tag-release.yml delete mode 100644 .github/workflows/conventional-commits.yml create mode 100644 .github/workflows/publish-release.yml delete mode 100644 .github/workflows/release-please.yml create mode 100644 .pkgmeta create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md create mode 100644 docs/CI_RELEASES.md create mode 100755 scripts/check-package.sh create mode 100755 scripts/check.sh create mode 100755 scripts/publish-assets.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9b754f8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "05:30" + timezone: Europe/Oslo + commit-message: + prefix: ci + include: scope + groups: + actions: + patterns: ["*"] + open-pull-requests-limit: 5 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1add34e..5fa8bbf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ Describe what changed and why. ## Validation -- [ ] I ran `bash scripts/check-static.sh` locally. +- [ ] I ran `bash scripts/check.sh` locally. - [ ] I verified CI checks relevant to this PR are green. ## Workflow Checklist diff --git a/.github/workflows/auto-tag-release.yml b/.github/workflows/auto-tag-release.yml deleted file mode 100644 index 0686490..0000000 --- a/.github/workflows/auto-tag-release.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Auto tag release - -on: - push: - branches: - - main - workflow_dispatch: - -permissions: {} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - tag: - permissions: - contents: write - runs-on: ubuntu-latest - timeout-minutes: 5 - env: - 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: 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/ci.yml b/.github/workflows/ci.yml index 4e8a10a..7714036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,134 +1,93 @@ name: CI on: - push: - branches: - - main - paths-ignore: - - '*.md' - - 'docs/**' - - 'LICENSE' - - 'NOTICE' - pull_request: - branches: - - main - paths-ignore: - - '*.md' - - 'docs/**' - - 'LICENSE' - - 'NOTICE' - workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: permissions: - contents: read + contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true -jobs: - changes: - name: changes - permissions: - contents: read - pull-requests: read - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - 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' - - '.github/workflows/*.yml' - - '.github/workflows/**/*.yml' - - static: - name: static - needs: changes - if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.should_run == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - 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: Run static checks - run: bash scripts/check-static.sh +env: + GO_VERSION: "1.25.12" - package: - name: package - needs: - - changes - - static - if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.should_run == 'true' - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 10 +jobs: + quality: + name: Quality + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Install Lua 5.1 + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends lua5.1 + - name: Set up Go for actionlint + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: ${{ env.GO_VERSION }} + cache: false + - name: Run repository checks + run: bash scripts/check.sh + - name: Validate conventional PR title + if: github.event_name == 'pull_request' env: - 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 - - 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 + PR_TITLE: ${{ github.event.pull_request.title }} + shell: bash + run: | + pattern='^(feat|fix|refactor|perf|docs|chore|ci|test|build)(\([a-z0-9._/-]+\))?(!)?: .+' + if [[ ! "$PR_TITLE" =~ $pattern ]]; then + echo "::error::PR title must use Conventional Commits, for example 'ci(workflows): replace release flow'." + exit 1 + fi + + package: + name: Package snapshot + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + GITHUB_OAUTH: ${{ github.token }} + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Build dry-run package + uses: BigWigsMods/packager@6d50adb6e8517eefef63f4afb16a6518166a6b28 # v2.5.1 + with: + args: -d + - name: Verify package compatibility + run: bash scripts/check-package.sh + - name: Upload package snapshot + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: calmchat-package-snapshot + path: .release/*.zip + if-no-files-found: error + include-hidden-files: false + retention-days: 7 + + required: + name: Required + if: always() + needs: [quality, package] + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Ensure required jobs passed + env: + QUALITY_RESULT: ${{ needs.quality.result }} + PACKAGE_RESULT: ${{ needs.package.result }} + shell: bash + run: | + if [[ "$QUALITY_RESULT" != success || "$PACKAGE_RESULT" != success ]]; then + echo "::error::One or more required jobs did not pass." + exit 1 + fi diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml deleted file mode 100644 index f01b820..0000000 --- a/.github/workflows/conventional-commits.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Conventional commits - -on: - pull_request: - types: - - opened - - edited - - synchronize - - reopened - -permissions: - pull-requests: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - 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 and commits - shell: bash - run: | - set -euo pipefail - - regex='^(feat|fix|refactor|perf|docs|chore|ci|test|build)(\([a-z0-9._/-]+\))?(!)?: [a-z].+' - - if [[ ! "$PR_TITLE" =~ $regex ]]; then - echo "error: PR title '$PR_TITLE' is not a conventional commit" >&2 - exit 1 - fi - - 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 - - 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" =~ $regex ]]; then - echo "error: commit subject '$subject' is not a conventional commit" >&2 - exit 1 - fi - done <<< "$subjects" - - page=$((page + 1)) - done diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..e0b5d8d --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,70 @@ +name: Publish release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: Existing immutable release tag to republish, for example v3.1.1 + required: true + type: string + +permissions: + contents: read + +concurrency: + group: publish-${{ github.event.release.tag_name || inputs.tag }} + cancel-in-progress: false + +jobs: + publish: + name: BigWigs package + if: >- + github.repository == 'calmcacil/CalmChat' && + (github.event_name == 'workflow_dispatch' || github.event.action == 'published') + permissions: + contents: write + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + GITHUB_OAUTH: ${{ github.token }} + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} + steps: + - name: Checkout immutable release tag + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 + persist-credentials: false + - name: Validate release source and version + shell: bash + run: | + set -euo pipefail + + semver='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' + [[ "$RELEASE_TAG" =~ $semver ]] || { echo "::error::Invalid release tag '$RELEASE_TAG'."; exit 1; } + + tag_commit="$(git rev-list -n 1 "$RELEASE_TAG")" + head_commit="$(git rev-parse HEAD)" + [[ "$tag_commit" == "$head_commit" ]] || { echo "::error::Checkout does not match $RELEASE_TAG."; exit 1; } + git merge-base --is-ancestor "$head_commit" origin/main || { echo "::error::$RELEASE_TAG is not contained in main."; exit 1; } + + version="$(tr -d '\r\n' < VERSION)" + [[ "v$version" == "$RELEASE_TAG" ]] || { echo "::error::VERSION '$version' does not match $RELEASE_TAG."; exit 1; } + gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null + - 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: Build release package + uses: BigWigsMods/packager@6d50adb6e8517eefef63f4afb16a6518166a6b28 # v2.5.1 + with: + args: -d + - name: Verify release package + run: bash scripts/check-package.sh ".release/CalmChat-${RELEASE_TAG}.zip" "${RELEASE_TAG#v}" + - name: Publish missing assets and verify reruns + run: bash scripts/publish-assets.sh diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml deleted file mode 100644 index 763691c..0000000 --- a/.github/workflows/release-please.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Release Please - -on: - push: - branches: - - main - paths-ignore: - - '*.md' - - 'docs/**' - - 'LICENSE' - - 'NOTICE' - workflow_dispatch: - -permissions: {} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - release-please: - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - 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 - - - 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 - - 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 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d6c11b..36a4645 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,66 +1,33 @@ -name: Package and release +name: Release on: - push: - tags: - - "v*" - workflow_dispatch: - inputs: - tag: - description: Release tag to package, for example v2.1.2 - required: true - type: string + push: + branches: [main] + workflow_dispatch: -permissions: {} +permissions: + contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }} - cancel-in-progress: false + group: release-main + cancel-in-progress: false jobs: - release: - name: release - permissions: - contents: write - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - GITHUB_OAUTH: ${{ github.token }} - RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} - - steps: - - name: Clone project - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true - ref: ${{ github.event.inputs.tag || github.ref }} - - - name: Validate semantic tag and version sync - shell: bash - run: | - set -euo pipefail - - tag="${RELEASE_TAG}" - semver_regex='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' - if [[ ! "$tag" =~ $semver_regex ]]; then - echo "error: tag '$tag' is not valid semantic versioning" >&2 - exit 1 - fi - - [[ -f VERSION ]] || { echo "error: VERSION is missing" >&2; exit 1; } - version_file=$(tr -d '\r\n' < VERSION) - tag_version="${tag#v}" - if [[ "$version_file" != "$tag_version" ]]; then - echo "error: VERSION '$version_file' does not match tag '$tag_version'" >&2 - exit 1 - fi - - - 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: Package and release - uses: BigWigsMods/packager@v2 + release: + name: Release Please + if: github.ref == 'refs/heads/main' && github.repository == 'calmcacil/CalmChat' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Create release GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v2.2.1 + with: + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + - name: Create or update the release pull request + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v4.4.0 + with: + token: ${{ steps.app-token.outputs.token }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.gitignore b/.gitignore index 6fd0a37..e22fcd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Compiled Lua sources luac.out +# BigWigs Packager output +.release/ + # luarocks build files *.src.rock *.zip @@ -38,4 +41,3 @@ luac.out *.i*86 *.x86_64 *.hex - diff --git a/.pkgmeta b/.pkgmeta new file mode 100644 index 0000000..736e3ff --- /dev/null +++ b/.pkgmeta @@ -0,0 +1,7 @@ +ignore: + - CONTRIBUTING.md + - SECURITY.md + - docs + - scripts/check.sh + - scripts/check-package.sh + - scripts/publish-assets.sh diff --git a/AGENTS.md b/AGENTS.md index 4411b04..734ef60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,9 +44,11 @@ This file defines how coding agents should work in this repository. ## Required Validation - Run local checks before finalizing a change when possible: - - `bash scripts/check-static.sh` + - `bash scripts/check.sh` - Ensure `.toc` files still include required release markers and correct `## Version`. - Ensure README compatibility version text remains aligned with `VERSION`. +- CI must preserve the BigWigs multi-client archive contract documented in `docs/CI_RELEASES.md`. +- CodeQL and dependency review are not applicable while this remains a dependency-free Lua addon; reassess if supported code or package manifests are added. ## Safety and Change Scope diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a6f06cd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing + +Create a short-lived `/` branch from `main` and open a +pull request. Use a Conventional Commit PR title such as +`fix(chat-routing): preserve trade tab filters`; the squash title determines the +next semantic release. + +Run the local CI equivalent before submitting changes: + +```bash +bash scripts/check.sh +``` + +This checks version and compatibility metadata, project invariants, shell scripts, +and GitHub Actions syntax. CI also builds a BigWigs dry-run archive and verifies all +six supported TOCs and required Lua modules. See [CI and releases](docs/CI_RELEASES.md) +for the exact release contract and recovery procedure. + +Do not hand-edit `CHANGELOG.md` for normal changes. Release Please owns version and +changelog updates. When intentionally changing compatibility metadata, keep +`VERSION`, `.release-please-manifest.json`, every `CalmChat*.toc`, and the README +version marker aligned. diff --git a/README.md b/README.md index 02e2c28..f1213ce 100644 --- a/README.md +++ b/README.md @@ -99,9 +99,10 @@ Use **Apply Chat Layout** in settings (or run `/calmchat`) to apply changes imme - Create a branch from `main` using `/`. - Use Conventional Commits for every commit and PR title, for example `fix(chat-routing): preserve trade tab filters`. - Keep each branch focused on one logical change to keep history and changelog clean. -- Run `bash scripts/check-static.sh` before opening or updating a PR. +- Run `bash scripts/check.sh` before opening or updating a PR. - Do not hand-edit `CHANGELOG.md`; release automation manages changelog entries and version bumps. - For release metadata updates, keep `VERSION`, `.release-please-manifest.json`, `CalmChat*.toc`, and the README version marker block in sync. +- See [CI and releases](docs/CI_RELEASES.md) for the package contract, release setup, recovery, and rollback. ## Troubleshooting diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..048102a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security + +Report suspected vulnerabilities privately through GitHub's security advisory +interface. Do not include credentials, private account data, or exploit details in +a public issue. + +If a token or private key may have been exposed, revoke or rotate it immediately and +then assess affected workflow runs and releases. Removing a secret from a later +commit does not invalidate a leaked credential. + +CalmChat is a dependency-free Lua addon. GitHub CodeQL does not support Lua, and +dependency review has no package manifest to analyze, so those optional workflows +are not enabled. CI instead validates the addon metadata and package structure, +checks shell scripts and workflow syntax, and builds the release format on every PR. diff --git a/docs/CI_RELEASES.md b/docs/CI_RELEASES.md new file mode 100644 index 0000000..f465ad8 --- /dev/null +++ b/docs/CI_RELEASES.md @@ -0,0 +1,83 @@ +# CI and releases + +## Local and pull-request checks + +Run `bash scripts/check.sh` locally. Pull requests and pushes to `main` run the same +static, shell, workflow, and version checks, then build a BigWigs snapshot archive. +The package check verifies the required Lua modules and these compatibility targets: + +| TOC | Client | Interface | +| --- | --- | --- | +| `CalmChat.toc` | Retail | `120005` | +| `CalmChat_Mists.toc` | Mists Classic | `50503` | +| `CalmChat_Cata.toc` | Cataclysm Classic | `40402` | +| `CalmChat_Wrath.toc` | Titan Reforged | `38001` | +| `CalmChat_TBC.toc` | Burning Crusade Classic | `20505` | +| `CalmChat_Vanilla.toc` | Classic Era and Season of Discovery | `11508` | + +The snapshot checkout intentionally has shallow history and no tags. This prevents a +release tag created concurrently on `main` from making BigWigs skip the CI package as +a future-tag safety measure; release publication uses the full immutable tag history. + +CI runs for every change; there are no path filters. The stable aggregate check to +require on `main` is `Required` from the `CI` workflow. Do not change the ruleset +until that exact check has reported successfully on a real pull request. + +CodeQL is unavailable for Lua, and the addon has no package dependencies, +containers, or infrastructure definitions. Dependency review, CodeQL, container +scanning, and IaC scanning are therefore not applicable. Secret scanning and push +protection should remain enabled in GitHub. + +## Release flow + +1. Normal Conventional Commit pull requests are squash-merged to `main`. +2. `Release` uses a short-lived GitHub App installation token to create or update a + reviewable Release Please PR. +3. A maintainer reviews and squash-merges that PR. Release Please updates `VERSION`, + the manifest, changelog, README marker, and all TOC versions, then creates an + immutable `vX.Y.Z` tag and GitHub Release. +4. The App-authored `release.published` event starts `Publish release`. The publisher + checks out the exact tag, verifies it is contained in `main`, reruns checks, and + invokes BigWigs Packager v2.5.1. +5. The release contains `CalmChat-vX.Y.Z.zip`, a BigWigs-compatible `release.json`, and + `checksums.txt`. The ZIP has one `CalmChat/` root and all six client TOCs. + +`.pkgmeta` keeps migration-only documentation and validation helpers out of the +installable archive, preserving the existing BigWigs package contents. + +The publisher is separate from the `main` coordinator deliberately. BigWigs refuses +to package a tag found during a push-to-branch event, even when that tag is checked +out, to protect against a future-tag race. The GitHub App token ensures the published +release event is not suppressed like an event created with the default workflow +token can be. + +### Required GitHub App and secrets + +Install the shared release App only on this repository with: + +- Contents: read and write +- Pull requests: read and write +- Metadata: read (implicit) + +Configure repository Actions secrets named `RELEASE_APP_ID` and +`RELEASE_APP_PRIVATE_KEY`. The default Actions token must remain read-only; only the +publisher job receives `contents: write` for release assets. + +## First release and recovery + +Before the first migrated release, confirm CI produces one snapshot ZIP and inspect +its root, TOCs, and version fields. Merge a normal releasable change, verify the App +opens a release PR with the expected SemVer, then merge it and confirm the tag points +to that reviewed `main` commit. Download all three release assets, run +`sha256sum --check checksums.txt` beside the ZIP and `release.json`, and inspect the +ZIP with `bash scripts/check-package.sh CalmChat-vX.Y.Z.zip X.Y.Z`. + +If publication fails after the tag and GitHub Release exist, fix the workflow on a +normal PR and manually dispatch `Publish release` with the existing tag. Never move +or recreate the tag. A recovery rerun downloads and verifies any existing assets and +uploads only missing ones; it never replaces a successful asset. If published source +is wrong, issue a new patch release instead of silently replacing it. + +Rollback is installation-based: download the prior known-good +`CalmChat-vX.Y.Z.zip`, verify it against that release's recorded SHA-256 digest, and +replace the installed `CalmChat` addon directory. No release branch is used. diff --git a/scripts/check-package.sh b/scripts/check-package.sh new file mode 100755 index 0000000..1459903 --- /dev/null +++ b/scripts/check-package.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +expected_archive=${1:-} +expected_version=${2:-$(tr -d '\r\n' < VERSION)} + +if [[ -n "$expected_archive" ]]; then + archives=("$expected_archive") +else + shopt -s nullglob + archives=(.release/CalmChat-*.zip) + shopt -u nullglob +fi + +if [[ ${#archives[@]} -ne 1 || ! -f "${archives[0]}" ]]; then + printf 'error: expected exactly one CalmChat package, found %d\n' "${#archives[@]}" >&2 + exit 1 +fi + +archive=${archives[0]} +mapfile -t entries < <(unzip -Z1 "$archive") + +if [[ ${#entries[@]} -eq 0 ]]; then + printf 'error: %s is empty\n' "$archive" >&2 + exit 1 +fi + +for entry in "${entries[@]}"; do + if [[ "$entry" != CalmChat && "$entry" != CalmChat/* ]]; then + printf 'error: package entry is outside the CalmChat directory: %s\n' "$entry" >&2 + exit 1 + fi +done + +declare -A interfaces=( + [CalmChat.toc]=120005 + [CalmChat_Mists.toc]=50503 + [CalmChat_Cata.toc]=40402 + [CalmChat_Wrath.toc]=38001 + [CalmChat_TBC.toc]=20505 + [CalmChat_Vanilla.toc]=11508 +) + +for toc in "${!interfaces[@]}"; do + contents=$(unzip -p "$archive" "CalmChat/$toc") + grep -Eq "^## Interface:[[:space:]]*${interfaces[$toc]}[[:space:]]*$" <<< "$contents" || { + printf 'error: %s has the wrong interface in %s\n' "$toc" "$archive" >&2 + exit 1 + } + grep -Eq "^## Version:[[:space:]]*${expected_version}[[:space:]]*$" <<< "$contents" || { + printf 'error: %s does not contain version %s in %s\n' "$toc" "$expected_version" "$archive" >&2 + exit 1 + } +done + +for file in CalmChat.lua Defaults.lua Client.lua DB.lua PresetBuilder.lua ChatApply.lua Core.lua SettingsUI.lua Commands.lua Events.lua; do + if [[ ! " ${entries[*]} " =~ [[:space:]]CalmChat/${file}[[:space:]] ]]; then + printf 'error: package is missing CalmChat/%s\n' "$file" >&2 + exit 1 + fi +done + +printf 'verified %s for CalmChat %s across all six client TOCs\n' "$archive" "$expected_version" diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 0000000..17d1cd7 --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +bash scripts/check-static.sh +shellcheck scripts/*.sh + +if command -v actionlint >/dev/null 2>&1; then + actionlint +elif command -v go >/dev/null 2>&1; then + go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 +else + printf 'error: actionlint or Go is required to validate workflows\n' >&2 + exit 1 +fi diff --git a/scripts/publish-assets.sh b/scripts/publish-assets.sh new file mode 100755 index 0000000..1671ec3 --- /dev/null +++ b/scripts/publish-assets.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +: "${RELEASE_TAG:?RELEASE_TAG is required}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${GH_TOKEN:?GH_TOKEN is required}" + +archive_name="CalmChat-${RELEASE_TAG}.zip" +archive=".release/$archive_name" +metadata=.release/release.json +expected_metadata=.release/release.expected.json +checksums=.release/checksums.txt + +[[ -f "$archive" ]] || { printf 'error: %s is missing\n' "$archive" >&2; exit 1; } + +jq -n \ + --arg version "$RELEASE_TAG" \ + --arg filename "$archive_name" \ + '{ + releases: [{ + name: "Calm\u0027s Custom Chat Preset", + version: $version, + filename: $filename, + nolib: false, + metadata: [ + {flavor: "mists", interface: 50503}, + {flavor: "titan", interface: 38001}, + {flavor: "mainline", interface: 120005}, + {flavor: "cata", interface: 40402}, + {flavor: "classic", interface: 11508}, + {flavor: "bcc", interface: 20505} + ] + }] + }' > "$expected_metadata" +cp "$expected_metadata" "$metadata" + +mapfile -t remote_assets < <( + gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' +) + +has_remote_asset() { + local expected=$1 + local asset + for asset in "${remote_assets[@]}"; do + [[ "$asset" == "$expected" ]] && return 0 + done + return 1 +} + +reuse_or_upload() { + local name=$1 + local path=$2 + if has_remote_asset "$name"; then + printf 'reusing existing release asset %s\n' "$name" + gh release download "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern "$name" \ + --dir .release \ + --clobber + else + printf 'uploading missing release asset %s\n' "$name" + gh release upload "$RELEASE_TAG" "$path" --repo "$GITHUB_REPOSITORY" + fi +} + +reuse_or_upload "$archive_name" "$archive" +reuse_or_upload release.json "$metadata" + +bash scripts/check-package.sh "$archive" "${RELEASE_TAG#v}" +diff -u <(jq -S . "$expected_metadata") <(jq -S . "$metadata") + +( + cd .release + sha256sum "$archive_name" release.json > checksums.txt +) + +if has_remote_asset checksums.txt; then + printf 'verifying existing release asset checksums.txt\n' + gh release download "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern checksums.txt \ + --dir .release \ + --clobber + [[ $(wc -l < "$checksums") -eq 2 ]] || { printf 'error: checksums.txt must contain two entries\n' >&2; exit 1; } + ( + cd .release + sha256sum --check checksums.txt + ) +else + printf 'uploading missing release asset checksums.txt\n' + gh release upload "$RELEASE_TAG" "$checksums" --repo "$GITHUB_REPOSITORY" +fi From 80cdd14ed75a55516120fcaaa8d00f800bbb61e9 Mon Sep 17 00:00:00 2001 From: "Calmcacil B." <716671+calmcacil@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:16:11 +0200 Subject: [PATCH 2/2] fix(ci): upload package from hidden directory --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7714036..c4cde85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,12 +66,12 @@ jobs: - name: Verify package compatibility run: bash scripts/check-package.sh - name: Upload package snapshot - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: calmchat-package-snapshot path: .release/*.zip if-no-files-found: error - include-hidden-files: false + include-hidden-files: true retention-days: 7 required: