Compare commits

...
Author SHA1 Message Date
Laurent Cozic cca6d46ad3 Doc: Remove obsolete CLI dependency version step from deploy doc
setupNewRelease already sets the @joplin dependency ranges of the CLI app.
2026-11-12 09:11:15 +00:00
Laurent Cozic db12dd7f2b Tools: Publish the CLI app from CI using npm trusted publishing
`yarn releaseCli` now bumps the app and libraries and generates the changelog. Pushing the cli-v* tag triggers the publish workflow, which publishes the libraries, then the app, so they no longer need to be released separately.
2026-11-12 08:48:52 +00:00
renovate[bot] b545a8ad42 chore(deps): update dependency ubuntu to v24 (#17092)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-12 08:39:57 +00:00
Laurent Cozic 953e3f7cd4 Tools: Cache default plugin builds (#17094) 2026-11-12 08:38:48 +00:00
Laurent Cozic 16749c0587 Tools: Remove stale gitHead field left by lerna from sub-packages
npm only sets gitHead when it is missing, so the value from the last lerna release was being published.

Ref #16659
2026-11-12 08:32:34 +00:00
23 changed files with 115 additions and 76 deletions

No files matched your search

+6 -4
View File
@@ -1,12 +1,14 @@
name: Publish sub-packages
name: Publish npm packages
# The tag is created and pushed by `yarn publishAll`. Publishing uses npm trusted publishing, so no
# token is needed, but each package must have this workflow registered as a trusted publisher on
# The sub-packages tag is pushed by `yarn publishAll`, and the cli tag by `yarn releaseCli`. Either way,
# any library or CLI app version not yet on npm is published. Publishing uses npm trusted publishing, so
# no token is needed, but each package must have this workflow registered as a trusted publisher on
# npmjs.com.
on:
push:
tags:
- 'sub-packages-v*'
- 'cli-v*'
permissions:
contents: read
@@ -21,7 +23,7 @@ concurrency:
jobs:
Publish:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
@@ -76,3 +76,10 @@ runs:
- uses: actions/setup-python@v6
with:
python-version: '3.14'
# Building the default plugins from source takes several minutes on Windows. Must be restored
# before "yarn install", which builds them.
- uses: actions/cache@v6
with:
path: packages/default-plugins/build-cache
key: default-plugins-${{ runner.os }}-${{ hashFiles('packages/default-plugins/pluginRepositories.json', 'packages/default-plugins/plugin-patches/**', 'packages/default-plugins/buildDefaultPlugins.ts') }}
+2 -1
View File
@@ -19,7 +19,8 @@
},
"repository": {
"type": "git",
"url": "https://github.com/laurent22/joplin"
"url": "git+https://github.com/laurent22/joplin.git",
"directory": "packages/app-cli"
},
"copyright": {
"title": "Joplin CLI",
+2 -1
View File
@@ -1,2 +1,3 @@
built-plugins/
plugin-sources/*
plugin-sources/*
build-cache/
@@ -1,9 +1,10 @@
/* eslint-disable no-console */
import { copy, exists, remove, readdir, mkdtemp } from 'fs-extra';
import { copy, exists, remove, readdir, mkdtemp, readFile } from 'fs-extra';
import { join, resolve, basename } from 'path';
import { tmpdir } from 'os';
import { createHash } from 'crypto';
import { chdir, cwd } from 'process';
import { execCommand } from '@joplin/utils';
import { glob } from 'glob';
@@ -16,8 +17,22 @@ interface Options {
outputParentDir: string|null;
beforeInstall: (buildDir: string, pluginName: string)=> Promise<void>;
beforePatch: ()=> Promise<void>;
useBuildCache: boolean;
}
// Increment to invalidate cached builds, for example after changing how plugins are built
const buildCacheVersion = 1;
const buildCacheDir = process.env.JOPLIN_DEFAULT_PLUGINS_CACHE_DIR || join(__dirname, 'build-cache');
const getBuildCachePath = async (pluginId: string, repositoryData: RepositoryData) => {
const patchFile = getPathToPatchFileFor(pluginId);
const patch = await exists(patchFile) ? await readFile(patchFile, 'utf8') : '';
const hash = createHash('sha256')
.update(JSON.stringify([buildCacheVersion, pluginId, repositoryData.cloneUrl, repositoryData.commit, patch]))
.digest('hex');
return join(buildCacheDir, `${pluginId}-${hash.substring(0, 16)}.jpl`);
};
const buildDefaultPlugins = async (options: Options) => {
const pluginRepositoryData = await readRepositoryJson(join(__dirname, 'pluginRepositories.json'));
@@ -26,7 +41,23 @@ const buildDefaultPlugins = async (options: Options) => {
const outputPath = options.outputParentDir && join(options.outputParentDir, `${pluginId}.jpl`);
if (repositoryData.type === BuiltInPluginType.Built) {
await buildPlugin(pluginId, repositoryData, outputPath, options);
const cachePath = options.useBuildCache && outputPath ? await getBuildCachePath(pluginId, repositoryData) : null;
if (cachePath && await exists(cachePath)) {
logStatus('Using cached build of', pluginId, 'from', cachePath);
await copy(cachePath, outputPath);
continue;
}
const sourceHasLocalChanges = await buildPlugin(pluginId, repositoryData, outputPath, options);
if (cachePath) {
if (sourceHasLocalChanges) {
logStatus('Not caching build of', pluginId, ': its source checkout has local changes');
} else {
await copy(outputPath, cachePath);
logStatus('Saved build of', pluginId, 'to', cachePath);
}
}
} else {
if (!outputPath) {
console.warn('Skipping NPM plugin,', pluginId, ': missing output path.');
@@ -80,6 +111,9 @@ const buildPlugin = async (pluginId: string, repositoryData: RepositoryData, out
throw new Error(`Unable to checkout commit ${expectedCommitHash}`);
}
// Local changes are included in the build but not in the build cache key
const sourceHasLocalChanges = (await execCommand(['git', 'status', '--porcelain'], { showStdout: false })).trim() !== '';
logStatus('Copying repository files...');
await copy(pluginDir, buildDir, {
filter: fileName => {
@@ -122,6 +156,8 @@ const buildPlugin = async (pluginId: string, repositoryData: RepositoryData, out
} else {
console.warn('No output directory specified. Not copying built .jpl files.');
}
return sourceHasLocalChanges;
} catch (error) {
console.error(error);
console.log('Build directory', buildDir);
@@ -5,6 +5,7 @@ const buildAll = (outputDirectory: string) => {
outputParentDir: outputDirectory,
beforeInstall: async () => { },
beforePatch: async () => { },
useBuildCache: true,
});
};
@@ -10,6 +10,7 @@ const editPatch = async (targetPluginId: string, outputParentDir: string|null) =
await buildDefaultPlugins({
outputParentDir: outputParentDir,
useBuildCache: false,
beforePatch: async () => {
// To make updating just the patch possible, a commit is created just before applying
// the patch.
+1 -2
View File
@@ -64,6 +64,5 @@
"license": "MIT",
"prettier": {
"tabWidth": 4
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -21,6 +21,5 @@
"devDependencies": {
"standard": "17.1.2",
"tap": "16.3.10"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -46,6 +46,5 @@
"type": "MIT",
"url": "http://github.com/jeremys/uslug/raw/master/LICENSE"
}
],
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
]
}
+1 -2
View File
@@ -45,6 +45,5 @@
"@types/jest": "29.5.14",
"jest": "29.7.0",
"typescript": "5.9.3"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -113,6 +113,5 @@
"uuid": "11.1.1",
"word-wrap": "1.2.5",
"xml2js": "0.4.23"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -20,6 +20,5 @@
"devDependencies": {
"wasm-pack": "0.13.1",
"yargs": "17.7.2"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -41,6 +41,5 @@
"typescript": "5.9.3",
"webpack": "5.97.1",
"webpack-cli": "4.10.0"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -56,6 +56,5 @@
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -60,6 +60,5 @@
"markdown-it-sup": "2.0.0",
"markdown-it-toc-done-right": "4.2.0",
"md5": "2.3.0"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -67,6 +67,5 @@
"style-to-js": "1.1.21",
"ts-node": "10.9.2",
"typescript": "5.9.3"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+10 -9
View File
@@ -1,28 +1,29 @@
import { execCommand } from '@joplin/utils';
import { rootDir, completeReleaseWithChangelog } from './tool-utils';
import { versionPatch } from '@joplin/utils/version';
import { versionAll } from './releaseSubPackages';
const appDir = `${rootDir}/packages/app-cli`;
const changelogPath = `${rootDir}/readme/about/changelog/cli.md`;
// Start with node Tools/release-cli.js --changelog-from cli-v1.0.126
// to specify from where the changelog should be created
//
// Pushing the tag triggers the publish-sub-packages workflow, which publishes the libraries, then the app.
async function main() {
process.chdir(appDir);
process.chdir(rootDir);
await execCommand('git pull');
await versionAll();
process.chdir(appDir);
const newVersion = await versionPatch();
console.info(`Building ${newVersion}...`);
console.info(`New version: ${newVersion}`);
const newTag = `cli-${newVersion}`;
await execCommand('touch app/main.js');
await execCommand('yarn build');
await execCommand('cp ../../README.md build/');
process.chdir(`${appDir}/build`);
await execCommand('npm publish');
process.chdir(rootDir);
await execCommand('yarn install --mode=update-lockfile');
await completeReleaseWithChangelog(changelogPath, newVersion, newTag, 'CLI', false);
}
+27 -11
View File
@@ -118,7 +118,25 @@ const publishPackage = async (dir: string, rootLicensePath: string, dryRun: bool
}
};
const versionAll = async () => {
// The CLI app is a private package because it is published from its build directory
const publishCliApp = async (rootDir: string, dryRun: boolean) => {
const buildDir = join(rootDir, 'packages', 'app-cli', 'build');
await execCommand(['yarn', 'workspace', 'joplin', 'build']);
await copy(join(rootDir, 'README.md'), join(buildDir, 'README.md'));
await publishPackage(buildDir, join(rootDir, 'LICENSE'), dryRun);
};
const publishIfNeeded = async (manifest: PackageJson, publish: ()=> Promise<void>) => {
const id = `${manifest.name}@${manifest.version}`;
if (await isPublished(manifest)) {
console.info(`Skipping ${id}: already published`);
return;
}
console.info(`Publishing ${id}...`);
await publish();
};
export const versionAll = async () => {
const packages = await loadPublicPackages(await getRootDir());
const bumped = bumpVersions(packages.map(p => p.manifest));
for (const [index, { dir, manifest }] of packages.entries()) {
@@ -127,19 +145,17 @@ const versionAll = async () => {
}
};
// Packages are published one at a time, dependencies first, so that a failure leaves the registry in a
// consistent state. Already published versions are skipped, so the command can be re-run after a failure.
// Packages are published one at a time, dependencies first and the CLI app last, so that a failure leaves
// the registry in a consistent state. Already published versions are skipped, so the command can be
// re-run after a failure, and the CLI app is only published when its version has been bumped.
const publishAll = async (dryRun: boolean) => {
const rootDir = await getRootDir();
for (const { dir, manifest } of sortByDependencies(await loadPublicPackages(rootDir))) {
const id = `${manifest.name}@${manifest.version}`;
if (await isPublished(manifest)) {
console.info(`Skipping ${id}: already published`);
continue;
}
console.info(`Publishing ${id}...`);
await publishPackage(dir, join(rootDir, 'LICENSE'), dryRun);
await publishIfNeeded(manifest, () => publishPackage(dir, join(rootDir, 'LICENSE'), dryRun));
}
const cliManifest: PackageJson = JSON.parse(await readFile(join(rootDir, 'packages', 'app-cli', 'package.json'), 'utf8'));
await publishIfNeeded(cliManifest, () => publishCliApp(rootDir, dryRun));
};
if (require.main === module) {
@@ -147,7 +163,7 @@ if (require.main === module) {
try {
await yargs(hideBin(process.argv))
.command('version', 'Bumps the patch version of all public packages', {}, () => versionAll())
.command('publish', 'Publishes all public packages whose version is not on the registry yet', yargs => {
.command('publish', 'Publishes all public packages and the CLI app whose version is not on the registry yet', yargs => {
return yargs.option('dry-run', {
type: 'boolean',
default: false,
+1 -2
View File
@@ -41,6 +41,5 @@
"build-iife": "rollup -c config/rollup.config.iife.js",
"build-test": "browserify test/turndown-plugin-gfm-test.js --outfile test/turndown-plugin-gfm-test.browser.js",
"prepare": "npm run build"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -46,6 +46,5 @@
"build-test": "browserify test/turndown-test.js --outfile test/turndown-test.browser.js",
"build": "npm run build-cjs",
"prepare": "npm run build"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+1 -2
View File
@@ -113,6 +113,5 @@
"jest": "29.7.0",
"ts-jest": "29.4.12",
"typescript": "5.9.3"
},
"gitHead": "898d2999f93fbedf0ce5f6626feb63e1a732ce0a"
}
}
+8 -22
View File
@@ -30,32 +30,18 @@ It must be built and released manually using XCode.
## CLI application
Unlike the mobile or desktop application, the CLI app doesn't bundle its dependencies and is always installed from source. For that reason, all its `@joplin` dependencies must be deployed publicly first. This is done using:
Unlike the mobile or desktop application, the CLI app doesn't bundle its dependencies and is always installed from source. For that reason, all its `@joplin` dependencies, such as `@joplin/lib`, `@joplin/tools`, etc., must be published to npm with it.
yarn publishAll
This is going to bump the version of all the Joplin libraries, such as `@joplin/lib`, `@joplin/tools`, etc., then commit and push a `sub-packages-v*` tag. The tag triggers the `publish-sub-packages.yml` workflow, which builds and publishes the libraries to npm using [trusted publishing](https://docs.npmjs.com/trusted-publishers). Wait for that workflow to complete before releasing the CLI app.
Each library must have `laurent22/joplin` and `publish-sub-packages.yml` registered as a trusted publisher on npmjs.com, with "Allow npm publish" enabled. When adding a new library, it must be published once manually before this can be configured.
Then in `app-cli/package.json`, all `@joplin` dependencies and devdependencies must be set to the last major/minor version. For example:
```json
"dependencies": {
"@joplin/lib": "1.8",
"@joplin/renderer": "1.8",
"...": "..."
},
"devDependencies": {
"@joplin/tools": "1.8",
"...": "..."
}
```
Finally, to release the actual app, run:
To release the app, run:
yarn releaseCli
This is going to bump the version of the app and of all the Joplin libraries, then generate the changelog. Once the changelog has been checked, run the printed commands to commit and push a `cli-v*` tag. The tag triggers the `publish-sub-packages.yml` workflow, which builds and publishes the libraries, then the app, to npm using [trusted publishing](https://docs.npmjs.com/trusted-publishers).
To publish only the libraries, run `yarn publishAll` instead. It bumps their versions, then commits and pushes a `sub-packages-v*` tag, which triggers the same workflow.
The `joplin` package and each library must have `laurent22/joplin` and `publish-sub-packages.yml` registered as a trusted publisher on npmjs.com, with "Allow npm publish" enabled. When adding a new library, it must be published once manually before this can be configured.
## Joplin Server
Run: