mirror of
https://github.com/appium/appium.git
synced 2026-10-16 20:32:05 +08:00
feat(semantic-release-config): add shared semantic-release configuration package (#22527)
This commit is contained in:
@@ -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:
|
||||
|
||||
Generated
+4800
-246
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.
|
||||
|
||||
[](https://npmjs.org/package/@appium/semantic-release-config)
|
||||
[](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
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user