feat(semantic-release-config): add shared semantic-release configuration package (#22527)

This commit is contained in:
Mykola Mokhnach
2026-07-22 15:52:47 +02:00
committed by GitHub
parent bf30246a65
commit 32f0bdf870
7 changed files with 5124 additions and 247 deletions
+5
View File
@@ -123,6 +123,11 @@ labels:
matcher:
files: ['packages/schema/**']
- label: '@appium/semantic-release-config'
sync: true
matcher:
files: ['packages/semantic-release-config/**']
- label: '@appium/support'
sync: true
matcher:
+4800 -246
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,97 @@
# @appium/semantic-release-config
> Shared [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/) configuration factory for [Appium](https://github.com/appium/appium) and Appium-adjacent projects.
[![NPM version](https://img.shields.io/npm/v/@appium/semantic-release-config.svg)](https://npmjs.org/package/@appium/semantic-release-config)
[![Downloads](https://img.shields.io/npm/dm/@appium/semantic-release-config.svg)](https://npmjs.org/package/@appium/semantic-release-config)
## Usage
Install the package:
```bash
npm install @appium/semantic-release-config --save-dev
```
Then, in `release.config.mjs` (or `.releaserc.mjs`):
```js
// release.config.mjs
import releaseConfig from '@appium/semantic-release-config';
export default releaseConfig();
```
This reproduces the "standard" config used by most appium-org repos: an `angular`-preset
commit-analyzer, a `conventionalcommits`-preset release-notes-generator, a changelog committed
back to the repo, npm publish, and a GitHub release.
For a repo that deviates from the standard, pass options:
```js
export default releaseConfig({
branches: ['main'],
extraGitAssets: ['npm-shrinkwrap.json'],
removeGitAssets: ['docs'],
releaseNotesTypeOverrides: {chore: {hidden: true}},
});
```
For a repo that isn't published to npm (a sample/demo app rather than a library), use the `app`
flavor:
```js
export default releaseConfig({
flavor: 'app',
branches: ['main'],
extraGitAssets: ['app/build.gradle.kts'],
});
```
### Options
| Option | Type | Default | Effect |
| --- | --- | --- | --- |
| `flavor` | `'library' \| 'app'` | `'library'` | `'app'` switches to the `conventionalcommits` commit-analyzer preset, sets `npmPublish: false`, disables GitHub success/fail PR & issue comments, and defaults `@semantic-release/github`'s `assets` to `[]`. |
| `branches` | `string[]` | _(omitted)_ | Sets the top-level `branches` option. Omit to fall back to semantic-release's own default branches. |
| `extraGitAssets` | `string[]` | `[]` | Appended to `@semantic-release/git`'s `assets`. |
| `removeGitAssets` | `string[]` | `[]` | Removed from `@semantic-release/git`'s `assets` (applied after `extraGitAssets`). |
| `githubAssets` | `Array` | _(flavor default)_ | `@semantic-release/github`'s `assets` option — plain path strings or `{path, label}` objects. |
| `commitAnalyzerReleaseRules` | `Array` | _(flavor default)_ | Full override of commit-analyzer's `releaseRules`. |
| `releaseNotesTypeOverrides` | `Record<string, {hidden?: boolean, section?: string}>` | _(none)_ | Shallow-patched onto the default `presetConfig.types`, keyed by commit type, e.g. `{chore: {hidden: true}}`. |
## Gotchas
1. **`extends` in a JSON `.releaserc` does a shallow merge.** `plugins` and other array options
are replaced wholesale, not deep-merged, when redefined locally. This means a JSON config with
`extends` can't losslessly express "same plugin list, but tweak one nested option of one
plugin" — exactly what several appium-org repos need. That's why this package ships a **JS
factory function** instead of static JSON: the merging happens once, in JS, before
semantic-release ever sees the resulting config object.
2. **This package is ESM-only.** Consumers must `import` the factory from a `release.config.mjs`
(or `.releaserc.mjs`) — repos still on CommonJS need to migrate to ESM (or at least add
`"type": "module"`) first. Also: `cosmiconfig` (which semantic-release uses to locate its
config) searches `.releaserc*` files *before* `release.config.*`. If a migration adds
`release.config.mjs` without deleting the old `.releaserc`/`.releaserc.json` in the same
commit, semantic-release will silently keep using the old file.
## Migrating an existing repo
This package only defines the shared config; migrating the ~26 existing appium-org repos off
their hand-copied `.releaserc` files is a separate, repo-by-repo effort. For each repo:
1. Add `@appium/semantic-release-config` as a devDependency.
2. Remove that repo's own `semantic-release`, `@semantic-release/changelog`,
`@semantic-release/git`, and `conventional-changelog-conventionalcommits` devDependencies —
they're pulled in transitively via this package's own pinned `dependencies`, so restating
their versions per-repo is no longer necessary (and would risk drifting from the versions this
package actually ships).
3. Add a `release.config.mjs` that imports and calls the factory with whatever options reproduce
that repo's current deviations (see the table above).
4. **Delete the old `.releaserc`/`.releaserc.json` in the same commit** — see gotcha #2 above.
5. If the repo is still on CommonJS, migrate it to `"type": "module"` first (or as part of the
same change).
## License
Copyright © 2026 OpenJS Foundation. Licensed Apache-2.0
+152
View File
@@ -0,0 +1,152 @@
const GIT_COMMIT_MESSAGE = 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}';
const LIBRARY_RELEASE_NOTES_TYPES = [
{type: 'feat', section: 'Features'},
{type: 'fix', section: 'Bug Fixes'},
{type: 'perf', section: 'Performance Improvements'},
{type: 'revert', section: 'Reverts'},
{type: 'chore', section: 'Miscellaneous Chores'},
{type: 'refactor', section: 'Code Refactoring'},
{type: 'docs', section: 'Documentation', hidden: true},
{type: 'style', section: 'Styles', hidden: true},
{type: 'test', section: 'Tests', hidden: true},
{type: 'build', section: 'Build System', hidden: true},
{type: 'ci', section: 'Continuous Integration', hidden: true},
];
const LIBRARY_COMMIT_ANALYZER_RELEASE_RULES = [{type: 'chore', release: 'patch'}];
const LIBRARY_GIT_ASSETS = ['docs', 'package.json', 'CHANGELOG.md'];
const APP_RELEASE_NOTES_TYPES = [
{type: 'feat', section: 'Features'},
{type: 'fix', section: 'Bug Fixes'},
{type: 'perf', section: 'Performance Improvements'},
{type: 'revert', section: 'Reverts'},
{type: 'refactor', section: 'Code Refactoring'},
{type: 'docs', section: 'Documentation', hidden: false},
{type: 'style', section: 'Styles', hidden: true},
{type: 'chore', section: 'Miscellaneous Chores'},
{type: 'test', section: 'Tests', hidden: true},
{type: 'build', section: 'Build System', hidden: true},
{type: 'ci', section: 'Continuous Integration', hidden: true},
];
const APP_COMMIT_ANALYZER_RELEASE_RULES = [
{type: 'feat', release: 'minor'},
{type: 'fix', release: 'patch'},
{type: 'perf', release: 'patch'},
{type: 'revert', release: 'patch'},
{type: 'docs', release: false},
{type: 'style', release: false},
{type: 'refactor', release: 'patch'},
{type: 'test', release: false},
{type: 'build', release: false},
{type: 'ci', release: false},
{type: 'chore', release: 'patch'},
{breaking: true, release: 'major'},
];
const APP_CHANGELOG_TITLE = '# Changelog\n\nAll notable changes to this project will be documented in this file.';
const APP_GIT_ASSETS = ['package.json', 'package-lock.json', 'CHANGELOG.md'];
/**
* Builds a `semantic-release` configuration object for an Appium-org project.
*
* Reproduces the standard config shared by most appium-org repos, plus every known deviation
* (extra branches, extra/removed git-committed assets, GitHub release assets, custom
* commit-analyzer release rules, and changelog section visibility) via the options below.
* @param {object} [opts]
* @param {'library'|'app'} [opts.flavor='library'] - `'app'` selects the "not published to npm"
* flavor (`conventionalcommits` commit-analyzer preset, `npmPublish: false`, no GitHub release
* assets by default, PR/issue comments disabled).
* @param {string[]} [opts.branches] - Passed through as the top-level `branches` option; omit to
* use semantic-release's own default branches.
* @param {string[]} [opts.extraGitAssets] - Appended to `@semantic-release/git`'s `assets`.
* @param {string[]} [opts.removeGitAssets] - Removed from `@semantic-release/git`'s `assets`.
* @param {Array} [opts.githubAssets] - `@semantic-release/github`'s `assets` option.
* @param {Array} [opts.commitAnalyzerReleaseRules] - Full override of commit-analyzer's
* `releaseRules`.
* @param {Record<string, {hidden?: boolean, section?: string}>} [opts.releaseNotesTypeOverrides] -
* Shallow-patched onto the default `presetConfig.types`, keyed by commit type (e.g.
* `{chore: {hidden: true}}`).
* @returns {object} A plain config object suitable for `export default` from
* `release.config.mjs`.
*/
export default function semanticReleaseConfig(opts = {}) {
const {
flavor = 'library',
branches,
extraGitAssets = [],
removeGitAssets = [],
githubAssets,
commitAnalyzerReleaseRules,
releaseNotesTypeOverrides,
} = opts;
const isApp = flavor === 'app';
const commitAnalyzerPlugin = [
'@semantic-release/commit-analyzer',
{
preset: isApp ? 'conventionalcommits' : 'angular',
releaseRules:
commitAnalyzerReleaseRules ??
(isApp ? APP_COMMIT_ANALYZER_RELEASE_RULES : LIBRARY_COMMIT_ANALYZER_RELEASE_RULES),
},
];
const releaseNotesGeneratorPlugin = [
'@semantic-release/release-notes-generator',
{
preset: 'conventionalcommits',
presetConfig: {
types: applyReleaseNotesTypeOverrides(
isApp ? APP_RELEASE_NOTES_TYPES : LIBRARY_RELEASE_NOTES_TYPES,
releaseNotesTypeOverrides,
),
},
},
];
const changelogPlugin = [
'@semantic-release/changelog',
isApp ? {changelogFile: 'CHANGELOG.md', changelogTitle: APP_CHANGELOG_TITLE} : {changelogFile: 'CHANGELOG.md'},
];
const npmPlugin = isApp ? ['@semantic-release/npm', {npmPublish: false}] : '@semantic-release/npm';
const baseGitAssets = isApp ? APP_GIT_ASSETS : LIBRARY_GIT_ASSETS;
const gitAssets = [...baseGitAssets, ...extraGitAssets].filter((asset) => !removeGitAssets.includes(asset));
const gitPlugin = ['@semantic-release/git', {assets: gitAssets, message: GIT_COMMIT_MESSAGE}];
const githubPlugin = isApp
? ['@semantic-release/github', {assets: githubAssets ?? [], successComment: false, failComment: false}]
: githubAssets
? ['@semantic-release/github', {assets: githubAssets}]
: '@semantic-release/github';
const config = {
plugins: [commitAnalyzerPlugin, releaseNotesGeneratorPlugin, changelogPlugin, npmPlugin, gitPlugin, githubPlugin],
};
if (branches) {
config.branches = branches;
}
return config;
}
/**
* Shallow-patch a `release-notes-generator` `presetConfig.types` list, keyed by commit type.
* @param {Array<{type: string, section: string, hidden?: boolean}>} baseTypes
* @param {Record<string, {hidden?: boolean, section?: string}>} [overrides]
* @returns {Array<{type: string, section: string, hidden?: boolean}>}
*/
function applyReleaseNotesTypeOverrides(baseTypes, overrides) {
if (!overrides) {
return baseTypes;
}
return baseTypes.map((entry) => (overrides[entry.type] ? {...entry, ...overrides[entry.type]} : entry));
}
@@ -0,0 +1,42 @@
{
"name": "@appium/semantic-release-config",
"version": "1.0.0",
"description": "Shared semantic-release configuration factory for Appium projects",
"keywords": [
"appium",
"release",
"semantic-release"
],
"homepage": "https://appium.io",
"bugs": {
"url": "https://github.com/appium/appium/issues"
},
"license": "Apache-2.0",
"author": "https://github.com/appium",
"repository": {
"type": "git",
"url": "https://github.com/appium/appium.git",
"directory": "packages/semantic-release-config"
},
"files": [
"index.mjs"
],
"type": "module",
"exports": "./index.mjs",
"publishConfig": {
"access": "public"
},
"scripts": {
"test:smoke": "node scripts/test-smoke.mjs"
},
"dependencies": {
"@semantic-release/changelog": "7.0.0",
"@semantic-release/git": "11.0.0",
"conventional-changelog-conventionalcommits": "9.3.1",
"semantic-release": "25.0.8"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
"npm": ">=10"
}
}
@@ -0,0 +1,22 @@
import releaseConfig from '../index.mjs';
if (typeof releaseConfig !== 'function') {
throw new Error('expected default export to be a function');
}
const libraryConfig = releaseConfig();
if (!Array.isArray(libraryConfig.plugins) || libraryConfig.plugins.length !== 6) {
throw new Error('expected library config to define 6 plugins');
}
if (libraryConfig.branches) {
throw new Error('expected library config to omit branches by default');
}
const appConfig = releaseConfig({flavor: 'app', branches: ['main']});
const npmPlugin = appConfig.plugins.find((plugin) => plugin[0] === '@semantic-release/npm');
if (!npmPlugin || npmPlugin[1]?.npmPublish !== false) {
throw new Error('expected app flavor to disable npm publish');
}
if (appConfig.branches?.[0] !== 'main') {
throw new Error('expected branches option to be passed through');
}
+6 -1
View File
@@ -15,7 +15,12 @@ const APPIUM_PACKAGE_README = path.join(ROOT_PACKAGES_DIR, 'appium', 'README.md'
const COMMON_FIELDS_TO_COPY = ['author', 'license', 'bugs', 'homepage'];
const LOGGER_COMMON_FIELDS_TO_COPY = ['author', 'bugs', 'homepage'];
const KEYWORD_EXCLUDED_PACKAGES = new Set(['eslint-config-appium-ts', 'oxc-config', 'types']);
const KEYWORD_EXCLUDED_PACKAGES = new Set([
'eslint-config-appium-ts',
'oxc-config',
'semantic-release-config',
'types',
]);
// Package names in this set will not receive the LICENSE file from the root,
// as they have their own license terms (e.g., ISC for logger).
const LICENSE_EXCLUDED_PACKAGES = new Set(['logger']);