From 3b7b91bbb416e95e7016798883e39f4539e28157 Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:22:52 +0300 Subject: [PATCH] ci(deps): automate dependency updates with Renovate --- .commitlintrc.json | 22 +++++- .github/workflows/ci.yml | 50 +++++++++--- .github/workflows/release.yml | 10 ++- .github/workflows/renovate.yml | 63 +++++++++++++++ .release-it.cjs | 3 +- CONTRIBUTING.md | 42 ++++++++-- package-lock.json | 125 +++++++++++++----------------- package.json | 8 +- renovate.json | 54 +++++++++++++ tests/dependency-release.test.mjs | 114 +++++++++++++++++++++++++++ tests/release-it.test.cjs | 16 +++- 11 files changed, 409 insertions(+), 98 deletions(-) create mode 100644 .github/workflows/renovate.yml create mode 100644 renovate.json create mode 100644 tests/dependency-release.test.mjs diff --git a/.commitlintrc.json b/.commitlintrc.json index 0df1d25..15cf559 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -1,5 +1,25 @@ { "extends": [ "@commitlint/config-conventional" - ] + ], + "rules": { + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "deps", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test" + ] + ] + } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4707e81..afe7bb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: - 'feature/**' pull_request: branches: + - main - develop - 'feature/**' workflow_call: @@ -20,19 +21,21 @@ on: jobs: compute-matrix: name: Compute matrix - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.set.outputs.matrix }} name_suffix: ${{ steps.set.outputs.name_suffix }} steps: - id: set + env: + FULL_MATRIX: ${{ inputs.full || (github.event_name == 'pull_request' && github.base_ref == 'main') }} run: | - if [[ "${{ inputs.full }}" == "true" ]]; then - echo 'matrix={"os":["ubuntu-latest","windows-latest"],"node":[18,20,22]}' >> $GITHUB_OUTPUT - echo 'name_suffix=(full matrix)' >> $GITHUB_OUTPUT + if [[ "$FULL_MATRIX" == "true" ]]; then + echo 'matrix={"os":["ubuntu-24.04","windows-latest"],"node":[18,20,22]}' >> "$GITHUB_OUTPUT" + echo 'name_suffix=(full matrix)' >> "$GITHUB_OUTPUT" else - echo 'matrix={"os":["ubuntu-latest"],"node":[20]}' >> $GITHUB_OUTPUT - echo 'name_suffix=' >> $GITHUB_OUTPUT + echo 'matrix={"os":["ubuntu-24.04"],"node":[20]}' >> "$GITHUB_OUTPUT" + echo 'name_suffix=' >> "$GITHUB_OUTPUT" fi build-and-test: @@ -46,7 +49,7 @@ jobs: matrix: ${{ fromJSON(needs.compute-matrix.outputs.matrix) }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Configure git EOL (Windows) if: runner.os == 'Windows' @@ -55,7 +58,7 @@ jobs: git config core.eol lf - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node }} cache: npm @@ -77,7 +80,36 @@ jobs: - name: Upload coverage artifact if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: coverage-${{ matrix.os }}-node${{ matrix.node }} path: coverage + + renovate-config: + name: Validate Renovate configuration + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Use Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + + - name: Validate configuration + run: npx --yes --package renovate@44.155.8 -- renovate-config-validator --strict renovate.json + + ci-success: + name: CI passed + if: ${{ always() }} + needs: [compute-matrix, build-and-test, renovate-config] + runs-on: ubuntu-24.04 + permissions: {} + steps: + - name: Require all checks to pass + env: + CHECKS_PASSED: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'skipped') }} + run: test "$CHECKS_PASSED" = true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7584889..eb15b9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,23 +33,25 @@ jobs: release: name: Release & Publish - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: ci steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 fetch-tags: true - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 22 cache: 'npm' - name: Update npm - run: npm install -g npm@latest + run: | + npm install -g npm@11.7.0 + npm -v - name: Install dependencies run: npm ci diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml new file mode 100644 index 0000000..be18985 --- /dev/null +++ b/.github/workflows/renovate.yml @@ -0,0 +1,63 @@ +name: Update Dependencies + +on: + schedule: + - cron: '15 2 * * 0' + timezone: America/Los_Angeles + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: renovate-dependencies + cancel-in-progress: false + +jobs: + renovate: + name: Create or update dependency PR + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Check GitHub App configuration + env: + APP_CLIENT_ID: ${{ vars.RENOVATE_APP_CLIENT_ID }} + APP_PRIVATE_KEY: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }} + run: | + if [[ -z "$APP_CLIENT_ID" || -z "$APP_PRIVATE_KEY" ]]; then + echo '::error::Set RENOVATE_APP_CLIENT_ID (Actions variable) and RENOVATE_APP_PRIVATE_KEY (Actions secret). See CONTRIBUTING.md.' + exit 1 + fi + + - name: Create repository-scoped GitHub App token + id: app-token + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 + with: + # Omitting owner/repositories scopes the shared app's token to this repository. + client-id: ${{ vars.RENOVATE_APP_CLIENT_ID }} + private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }} + + - name: Resolve bot identity + id: bot + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + BOT_LOGIN="${APP_SLUG}[bot]" + BOT_ID=$(gh api "/users/${BOT_LOGIN}" --jq .id) + printf 'username=%s\n' "$BOT_LOGIN" >> "$GITHUB_OUTPUT" + printf 'git-author=%s <%s+%s@users.noreply.github.com>\n' "$BOT_LOGIN" "$BOT_ID" "$BOT_LOGIN" >> "$GITHUB_OUTPUT" + + - name: Run Renovate + uses: renovatebot/github-action@f0f41e5f2e706165f6814b1d0f0d0e319de7d1d9 # v46.3.8 + with: + token: ${{ steps.app-token.outputs.token }} + renovate-version: 44.155.8 + env: + RENOVATE_PLATFORM: github + RENOVATE_REPOSITORIES: ${{ github.repository }} + RENOVATE_AUTODISCOVER: 'false' + RENOVATE_ONBOARDING: 'false' + RENOVATE_REQUIRE_CONFIG: required + RENOVATE_USERNAME: ${{ steps.bot.outputs.username }} + RENOVATE_GIT_AUTHOR: ${{ steps.bot.outputs.git-author }} diff --git a/.release-it.cjs b/.release-it.cjs index f979ad8..c1aee55 100644 --- a/.release-it.cjs +++ b/.release-it.cjs @@ -94,6 +94,7 @@ function getContributors() { const types = new Map([ ["feat", "✨ Features"], ["fix", "🐛 Bug Fixed"], + ["deps", "📦 Dependencies"], ["perf", "⚡️ Performance Improvements"], ["refactor", "🛠️ Refactoring"], ["docs", "📝 Documentation"], @@ -154,7 +155,7 @@ function whatBump(commits, currentVersion = pkg.version) { isMinor = true; } - if (["fix", "perf", "refactor", "ci"].includes(type)) { + if (["fix", "deps", "perf", "refactor"].includes(type)) { isPatch = true; } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20ab225..ffe6051 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ git switch -c feature/ ## Local setup -Node.js 20 is the default CI environment. The full release gate also exercises Node.js 18, 20, and 22. +Node.js 20 is the default CI environment. Pull requests into `main` and the release gate also exercise Node.js 18, 20, and 22. ```bash git clone git@github.com:addon-stack/inject-script.git @@ -48,7 +48,8 @@ Useful commands: | `npm run typecheck` | Type-check package sources | | `npm run test:types` | Type-check public API contract fixtures | | `npm run test` | Build and run the Jest suite | -| `npm run test:ci` | Build and run Jest with coverage | +| `npm run test:ci` | Build, run Jest with coverage, and check dependency-release integration | +| `npm run test:tooling` | Verify Renovate, commitlint, and changelog integration | | `npm run release` | Perform a real maintainer release through release-it | `npm run release` is not a dry run. Do not execute it unless you are intentionally publishing a release and have the required maintainer access. @@ -71,11 +72,12 @@ When changing public behavior, update the implementation, types, tests, and READ ## Tests -The test suite covers three different contracts: +The test suite covers the following contracts: - `tests/types.test.ts` verifies compile-time API behavior. - `tests/inject-script.test.cjs` verifies MV2/MV3 runtime behavior against the built package. - `tests/release-it.test.cjs` verifies release versioning policy. +- `tests/dependency-release.test.mjs` verifies the real parser, commitlint CLI, and generated dependency release notes. For adapter changes, cover the affected combinations where relevant: @@ -107,7 +109,7 @@ Local hooks provide additional protection: - `commit-msg` validates Conventional Commits with commitlint. - `pre-push` runs `typecheck` and the full `test:ci` command. -GitHub Actions repeats lint, typecheck, tests, coverage, and production build checks. Release runs use the full operating-system and Node.js matrix. +GitHub Actions repeats lint, typecheck, tests, coverage, and production build checks. Pull requests into `main` and release runs use the full operating-system and Node.js matrix. The aggregate `CI passed` check also requires strict Renovate configuration validation; failed, cancelled, or skipped prerequisites fail this check. ## Commit messages @@ -125,6 +127,8 @@ Common examples: ```text feat(target): add document targeting fix(mv2): preserve partial timeout results +deps: update dependencies +ci: update workflow actions refactor(results): centralize native normalization test(types): cover interface arguments docs: simplify the quick start @@ -145,12 +149,38 @@ Release versions are derived from commit history by `release-it` and `@release-i - A breaking change increments major at `1.x` and newer. - A breaking change increments minor while the package is on `0.x`. - `feat` and `revert` increment minor. -- `fix`, `perf`, `refactor`, and `ci` increment patch. -- `docs`, `test`, `build`, `chore`, and `style` do not trigger a release by themselves. +- `fix`, `deps`, `perf`, and `refactor` increment patch. +- `docs`, `test`, `build`, `chore`, `ci`, and `style` do not trigger a release by themselves. - When several changes are present, the highest applicable increment wins. Do not edit released changelog entries manually. `CHANGELOG.md` is generated from Conventional Commits during release. +## Automated dependency updates + +The **Update Dependencies** workflow runs every Sunday at **02:15 America/Los_Angeles** and can also be started from **Actions → Update Dependencies → Run workflow**. The time follows Los Angeles daylight saving rules. + +Renovate reads `renovate.json` from `main` and updates only `@addon-core/browser` and `@types/chrome`, together with their required lockfile changes. Stable updates of any size are grouped in one `deps/update-dependencies` branch and one PR into `main`, including Chrome-types-only updates. Other direct dependencies and standalone lockfile maintenance are disabled. + +On each run, Renovate refreshes its branch from the current `main` when needed. An open bot-managed PR is updated in place. After merging, a future update can recreate the same branch name. No available update means no empty PR. Manually committing to an open bot branch pauses automatic updates to protect those commits. + +The bot creates `deps: update dependencies` commits. Ordinary dependency-only changes trigger a **patch** release and appear under **📦 Dependencies** in the changelog and GitHub release notes. Keep the `deps: ...` title when squashing. Breaking markers and other commits still follow the normal release policy. + +The App token triggers the normal PR checks: lint, source and public API type checks, build, tests, and strict Renovate configuration validation. **Merging remains manual.** Failed checks leave the PR open and do not publish a release. After merge, the existing Release workflow repeats the full checks, publishes the package, and syncs `main` back into `develop`. + +Keep `conventional-commits-parser` 6.x as an explicit development dependency. The release tooling needs parser 6.x through optional peers, while commitlint needs parser 5.x. Explicitly declaring parser 6.x prevents npm lockfile-only regeneration from dropping the release parser. After changing it, regenerate the lockfile in a clean directory and verify `npm ci` and the release tooling tests. + +### Connect the shared GitHub App + +Reuse the existing organization App **addon-stack-dependency-updates**; each package keeps its own workflow and dependency rules. + +1. In the `addon-stack` organization settings, open **GitHub Apps → Configure** for the existing installation and add **inject-script** under **Only select repositories**. Keep existing repositories selected. The existing App permissions are sufficient; no new App or key is needed. +2. Under **Secrets and variables → Actions**, add `inject-script` to the selected repositories for both the organization variable `RENOVATE_APP_CLIENT_ID` and the organization secret `RENOVATE_APP_PRIVATE_KEY`. The workflow creates a short-lived token scoped to this repository only. Renovate does not need an npm publishing token. +3. Merge the setup from `develop` into `main`. Both scheduled runs and the manual workflow entry need the workflow file on the default branch. +4. Run **Update Dependencies** manually and inspect the first PR. Configure the PR check **CI passed** as required for `main` and require an up-to-date branch. Do not give the dependency App a branch-protection bypass. +5. Account for release automation before enforcing branch rules: `release-it` pushes version/changelog commits to `main`, and the sync step writes to `develop`. Rules requiring PRs for all writes need a suitable exception for the release writer; the dependency App is not the release writer. + +For another package, repeat the installation and organization-variable/secret access steps, then add its workflow, dependency allowlist, release policy, tests, and documentation. GitHub supports [changing the repositories available to an installed App](https://docs.github.com/en/apps/using-github-apps/reviewing-and-modifying-installed-github-apps) and [sharing organization secrets with selected repositories](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets). + ## Pull requests Open regular pull requests against `develop` and include: diff --git a/package-lock.json b/package-lock.json index ccefefb..1009bf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@release-it/conventional-changelog": "^10.0.1", "@types/chrome": "^0.2.7", "@types/jest": "^30.0.0", + "conventional-commits-parser": "^6.4.0", "husky": "^9.1.7", "jest": "^30.1.3", "lint-staged": "^16.2.1", @@ -893,6 +894,38 @@ "node": ">=v18" } }, + "node_modules/@commitlint/parse/node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@commitlint/parse/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@commitlint/read": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.0.0.tgz", @@ -3160,6 +3193,19 @@ "win32" ] }, + "node_modules/@simple-libs/stream-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", + "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -4621,22 +4667,6 @@ } } }, - "node_modules/conventional-changelog-core/node_modules/conventional-commits-parser": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.2.0.tgz", - "integrity": "sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag==", - "dev": true, - "license": "MIT", - "dependencies": { - "meow": "^13.0.0" - }, - "bin": { - "conventional-commits-parser": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/conventional-changelog-core/node_modules/git-raw-commits": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.0.tgz", @@ -4773,35 +4803,20 @@ } }, "node_modules/conventional-commits-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", - "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", + "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", "dev": true, "license": "MIT", "dependencies": { - "is-text-path": "^2.0.0", - "JSONStream": "^1.3.5", - "meow": "^12.0.1", - "split2": "^4.0.0" + "@simple-libs/stream-utils": "^1.2.0", + "meow": "^13.0.0" }, "bin": { - "conventional-commits-parser": "cli.mjs" + "conventional-commits-parser": "dist/cli/index.js" }, "engines": { - "node": ">=16" - } - }, - "node_modules/conventional-commits-parser/node_modules/meow": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, "node_modules/conventional-recommended-bump": { @@ -4850,22 +4865,6 @@ } } }, - "node_modules/conventional-recommended-bump/node_modules/conventional-commits-parser": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.2.0.tgz", - "integrity": "sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag==", - "dev": true, - "license": "MIT", - "dependencies": { - "meow": "^13.0.0" - }, - "bin": { - "conventional-commits-parser": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5663,24 +5662,6 @@ } } }, - "node_modules/git-semver-tags/node_modules/conventional-commits-parser": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.2.0.tgz", - "integrity": "sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "meow": "^13.0.0" - }, - "bin": { - "conventional-commits-parser": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/git-up": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz", diff --git a/package.json b/package.json index 687a104..94ff885 100644 --- a/package.json +++ b/package.json @@ -54,11 +54,12 @@ "lint": "biome check .", "lint:fix": "biome check --write .", "lint:fix:unsafe": "biome check --write --unsafe .", - "test": "npm run test:types && npm run build && jest --bail --passWithNoTests", - "test:ci": "npm run test:types && npm run build && jest --ci --passWithNoTests --coverage", + "test": "npm run test:types && npm run build && jest --bail --passWithNoTests && npm run test:tooling", + "test:ci": "npm run test:types && npm run build && jest --ci --passWithNoTests --coverage && npm run test:tooling", "test:types": "tsc -p tests/tsconfig.json --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit", - "release": "release-it" + "release": "release-it", + "test:tooling": "node --test tests/dependency-release.test.mjs" }, "lint-staged": { "src/**/*.{js,jsx,ts,tsx,cjs,mjs}": [ @@ -84,6 +85,7 @@ "@release-it/conventional-changelog": "^10.0.1", "@types/chrome": "^0.2.7", "@types/jest": "^30.0.0", + "conventional-commits-parser": "^6.4.0", "husky": "^9.1.7", "jest": "^30.1.3", "lint-staged": "^16.2.1", diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..f7837ab --- /dev/null +++ b/renovate.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "baseBranchPatterns": [ + "main" + ], + "branchPrefix": "deps/", + "enabledManagers": [ + "npm" + ], + "includePaths": [ + "package.json", + "package-lock.json" + ], + "automerge": false, + "dependencyDashboard": false, + "ignoreUnstable": true, + "prCreation": "immediate", + "rebaseWhen": "behind-base-branch", + "semanticCommits": "enabled", + "semanticCommitType": "deps", + "semanticCommitScope": null, + "separateMajorMinor": false, + "separateMinorPatch": false, + "lockFileMaintenance": { + "enabled": false + }, + "vulnerabilityAlerts": { + "enabled": false + }, + "packageRules": [ + { + "description": "Keep dependency updates limited to the explicitly enabled packages.", + "matchPackageNames": [ + "*" + ], + "enabled": false + }, + { + "matchPackageNames": [ + "@addon-core/browser", + "@types/chrome" + ], + "matchDepTypes": [ + "dependencies", + "devDependencies" + ], + "enabled": true, + "rangeStrategy": "bump", + "groupName": "dependencies", + "groupSlug": "update-dependencies", + "groupSingleUpdates": true + } + ] +} diff --git a/tests/dependency-release.test.mjs b/tests/dependency-release.test.mjs new file mode 100644 index 0000000..65cbbf5 --- /dev/null +++ b/tests/dependency-release.test.mjs @@ -0,0 +1,114 @@ +import assert from "node:assert/strict"; +import {spawnSync} from "node:child_process"; +import {mkdtempSync, readFileSync, rmSync} from "node:fs"; +import {tmpdir} from "node:os"; +import {join} from "node:path"; +import {text} from "node:stream/consumers"; +import {test} from "node:test"; +import {fileURLToPath} from "node:url"; +import conventionalCommitConfig from "@commitlint/config-conventional"; +import conventionalChangelog from "conventional-changelog"; +import {CommitParser} from "conventional-commits-parser"; +import createReleaseConfig from "../.release-it.cjs"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +const renovate = JSON.parse(readFileSync(join(root, "renovate.json"), "utf8")); +const commitlint = JSON.parse(readFileSync(join(root, ".commitlintrc.json"), "utf8")); +const release = createReleaseConfig(); +const options = release.plugins["@release-it/conventional-changelog"]; +const parse = message => new CommitParser(options.parserOpts).parse(message); +const scope = renovate.semanticCommitScope ? `(${renovate.semanticCommitScope})` : ""; +const updateMessage = `${renovate.semanticCommitType}${scope}: update dependencies`; + +const lint = message => + spawnSync(process.execPath, [join(root, "node_modules/@commitlint/cli/cli.js"), "--strict"], { + cwd: root, + input: message, + encoding: "utf8", + timeout: 10000, + }); + +test("Renovate's commit passes commitlint and triggers a patch release", () => { + assert.equal(updateMessage, "deps: update dependencies"); + const result = lint(updateMessage); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.deepEqual(options.whatBump([parse(updateMessage)], "0.5.0"), {level: 2}); +}); + +test("commitlint preserves conventional types and rejects unknown types", () => { + for (const type of conventionalCommitConfig.rules["type-enum"][2]) { + assert.ok(commitlint.rules["type-enum"][2].includes(type)); + } + const result = lint("depz: update dependencies"); + assert.notEqual(result.status, 0); + assert.match(result.stdout, /type-enum/); +}); + +for (const [message, version, level] of [ + ["deps(chrome): update chrome types", "0.5.0", 2], + ["deps(chrome): update chrome types", "1.2.3", 2], + ["deps!: require Chrome 140", "0.5.0", 1], + ["deps!: require Chrome 140", "1.2.3", 0], + ["deps: update browser\n\nBREAKING CHANGE: require Chrome 140", "1.2.3", 0], +]) { + test(`applies the release policy to ${message} at ${version}`, () => { + assert.deepEqual(options.whatBump([parse(message)], version), {level}); + }); +} + +test("renders dependency updates in Dependencies in changelog and GitHub release notes", async () => { + const cwd = mkdtempSync(join(tmpdir(), "inject-script-dependency-release-")); + // Isolate fixture commits from hooks, signing settings, and parent Git environment. + const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_"))); + const git = (...args) => { + const result = spawnSync( + "git", + [ + "-c", + `core.hooksPath=${join(cwd, "no-hooks")}`, + "-c", + "commit.gpgSign=false", + "-c", + "tag.gpgSign=false", + "-c", + "user.name=Release Test", + "-c", + "user.email=release-test@example.invalid", + ...args, + ], + {cwd, env, encoding: "utf8", timeout: 10000} + ); + assert.equal(result.status, 0, result.error?.message || result.stderr); + }; + + try { + git("init", "--quiet"); + git("commit", "--allow-empty", "-m", "chore: initial release"); + git("tag", "v0.5.0"); + git("commit", "--allow-empty", "-m", updateMessage); + git("commit", "--allow-empty", "-m", "deps(chrome): update chrome types"); + git("commit", "--allow-empty", "-m", "fix(inject): fix injection results"); + + const changelog = await text( + conventionalChangelog( + {preset: options.preset, cwd}, + {...options.context, version: "0.5.1", contributors: []}, + {from: "v0.5.0"}, + options.parserOpts, + options.writerOpts + ) + ); + const sections = changelog.split(/^### /m); + const dependencies = sections.find(section => section.startsWith("📦 Dependencies\n")); + const fixes = sections.find(section => section.startsWith("🐛 Bug Fixed\n")); + assert.match(dependencies, /update dependencies/); + assert.match(dependencies, /\*\*chrome:\*\* update chrome types/); + assert.doesNotMatch(dependencies, /fix injection results/); + assert.match(fixes, /fix injection results/); + assert.doesNotMatch(fixes, /update dependencies/); + assert.doesNotMatch(changelog, /🧩 Other/); + assert.equal(release.github.releaseNotes({changelog}), changelog); + } finally { + rmSync(cwd, {recursive: true, force: true}); + } +}); diff --git a/tests/release-it.test.cjs b/tests/release-it.test.cjs index 64ea776..d77c2be 100644 --- a/tests/release-it.test.cjs +++ b/tests/release-it.test.cjs @@ -29,15 +29,27 @@ describe("release-it version policy", () => { expect(whatBump([{type}], "0.3.1")).toEqual({level: 1}); }); - test.each(["fix", "perf", "refactor", "ci"])("uses a patch bump for %s", type => { + test.each(["fix", "deps", "perf", "refactor"])("uses a patch bump for %s", type => { expect(whatBump([{type}], "0.3.1")).toEqual({level: 2}); }); + test("does not release for CI and maintenance changes together", () => { + expect(whatBump([{type: "ci"}, {type: "chore"}, {type: "docs"}], "0.5.0")).toBeNull(); + }); + + test("releases a patch for dependency changes alongside CI changes", () => { + expect(whatBump([{type: "ci"}, {type: "deps"}], "0.5.0")).toEqual({level: 2}); + }); + + test("keeps breaking CI changes subject to the breaking-change policy", () => { + expect(whatBump([{type: "ci", breaking: "!"}], "1.0.0")).toEqual({level: 0}); + }); + test("uses the highest non-breaking bump", () => { expect(whatBump([{type: "fix"}, {type: "feat"}], "0.3.1")).toEqual({level: 1}); }); - test.each(["docs", "test", "chore", "build"])("does not release for %s alone", type => { + test.each(["docs", "test", "chore", "build", "ci", "style"])("does not release for %s alone", type => { expect(whatBump([{type}], "0.3.1")).toBeNull(); }); });