Plugin-generator: Resolves #15595: Added local validations for new publish workflow (#15596)

This commit is contained in:
Akshaj Rawat
2026-06-18 10:02:20 +01:00
committed by GitHub
parent d37b6f3751
commit 364b22c5b6
7 changed files with 177 additions and 0 deletions
+3
View File
@@ -1269,6 +1269,9 @@ packages/generator-joplin/generators/app/templates/api/index.js
packages/generator-joplin/generators/app/templates/api/noteListType.js
packages/generator-joplin/generators/app/templates/api/types.js
packages/generator-joplin/generators/app/templates/api_index.js
packages/generator-joplin/generators/app/templates/script/publish/steps/verifyBuild.js
packages/generator-joplin/generators/app/templates/script/publish/steps/verifyGitState.js
packages/generator-joplin/generators/app/templates/script/publish/utils/logger.js
packages/generator-joplin/generators/app/templates/src/index.js
packages/generator-joplin/tools/updateCategories.js
packages/htmlpack/index.test.js
+3
View File
@@ -1295,6 +1295,9 @@ packages/generator-joplin/generators/app/templates/api/index.js
packages/generator-joplin/generators/app/templates/api/noteListType.js
packages/generator-joplin/generators/app/templates/api/types.js
packages/generator-joplin/generators/app/templates/api_index.js
packages/generator-joplin/generators/app/templates/script/publish/steps/verifyBuild.js
packages/generator-joplin/generators/app/templates/script/publish/steps/verifyGitState.js
packages/generator-joplin/generators/app/templates/script/publish/utils/logger.js
packages/generator-joplin/generators/app/templates/src/index.js
packages/generator-joplin/tools/updateCategories.js
packages/htmlpack/index.test.js
@@ -176,6 +176,11 @@ module.exports = class extends Generator {
this.templatePath('api'),
this.destinationPath('api'),
);
this.fs.copy(
this.templatePath('script'),
this.destinationPath('script'),
);
}
};
@@ -20,6 +20,8 @@
"copy-webpack-plugin": "^11.0.0",
"fs-extra": "^10.1.0",
"glob": "^8.0.3",
"semver": "^7.5.4",
"@types/semver": "^7.5.4",
"tar": "^6.1.11",
"ts-loader": "^9.3.1",
"typescript": "^4.8.2",
@@ -0,0 +1,83 @@
import { readFile } from 'fs/promises';
import { join } from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { valid } from 'semver';
import logger from '../utils/logger';
const distCommand = 'npm run dist';
const execAsync = promisify(exec);
interface Manifest {
version?: string;
repository_url?: string;
}
interface PackageJson {
name?: string;
}
const verifyBuild = async () => {
const metadata = await validateMetadata();
await build();
return metadata;
};
const validateMetadata = async () => {
logger.info('Validating metadata...');
// process.cwd() is the plugin root dir when run via `npm run publish`
const manifestPath = join(process.cwd(), 'src/manifest.json');
const packageJsonPath = join(process.cwd(), 'package.json');
let manifest: Manifest;
let packageJson: PackageJson;
try {
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
} catch (error: unknown) {
if (error instanceof Error) {
error.message = `manifest.json or package.json contains invalid JSON: ${error.message}`;
}
throw error;
}
const { version } = manifest;
const { name } = packageJson;
const repositoryUrl = manifest.repository_url;
if (!name || !name.startsWith('joplin-plugin-')) {
throw new Error('Plugin name must start with \'joplin-plugin-\' in package.json');
}
if (!version || !valid(version)) {
throw new Error(`Invalid plugin version: '${version}'. Must follow semver format.`);
}
const cleanUrl = typeof repositoryUrl === 'string'
? repositoryUrl.trim().replace(/\.git$/, '').replace(/\/$/, '')
: '';
const githubPattern = /^https:\/\/github\.com\/[^/]+\/[^/]+$/;
if (!cleanUrl || !githubPattern.test(cleanUrl)) {
throw new Error('Repository URL is missing or malformed in manifest.json. Valid format: https://github.com/username/repo');
}
logger.success(`Metadata validated: ${name}@${version}`);
return { name, version, repositoryUrl: cleanUrl };
};
const build = async () => {
try {
logger.info(`Running '${distCommand}'...`);
await execAsync(distCommand, { cwd: process.cwd() });
logger.success('Build verified!');
} catch (error: unknown) {
if (error instanceof Error) {
error.message = `Build failed. Fix the errors above before publishing: ${error.message}`;
}
throw error;
}
};
export default verifyBuild;
@@ -0,0 +1,70 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import logger from '../utils/logger';
const execAsync = promisify(exec);
// Verifies if the user has pushed the local code which he is going to publish on github or not
const verifyGitState = async () => {
const runGit = async (command: string, errorMessage: string) => {
try {
const { stdout } = await execAsync(command, { encoding: 'utf8', cwd: process.cwd() });
return stdout.trim();
} catch (error: unknown) {
if (error instanceof Error) {
error.message = `${errorMessage}: ${error.message}`;
}
throw error;
}
};
// Checks if the current folder is a git repository or not
await runGit('git rev-parse --is-inside-work-tree', 'The current directory is not a git repository or git is not installed.');
// Checks if there is any uncommitted changes
const status = await runGit('git status --porcelain', 'Failed to check git status. Ensure git is installed and configured.');
if (status !== '') {
throw new Error('You have uncommitted changes. Please commit or stash them before publishing.');
}
logger.success('Working tree is clean.');
// Gets the current latest local commit hash
const commitHash = await runGit('git rev-parse HEAD', 'Could not get commit hash. Ensure you are in a valid Git repository with at least one commit.');
if (commitHash.length !== 40) {
throw new Error('Failed to extract a valid commit hash. Ensure that git is properly initialized and you have made at least one local commit (git commit) before publishing.');
}
logger.success(`Commit hash extracted: ${commitHash}`);
// check if the local project is linked to github
await runGit('git remote get-url origin', 'No remote named \'origin\' found. Make sure your plugin repository is hosted on GitHub.');
const currentBranch = await runGit('git rev-parse --abbrev-ref HEAD', 'Failed to retrieve current branch name. Ensure git is configured correctly.');
if (currentBranch === 'HEAD') {
throw new Error('You are in a detached HEAD state. Checkout a branch (e.g. git checkout main) and push before publishing.');
}
const remoteHeadLine = await runGit(`git ls-remote origin ${currentBranch}`, 'Could not retrieve remote HEAD. Make sure you have pushed your changes and have an internet connection.');
if (!remoteHeadLine) {
throw new Error('Remote HEAD is empty. Make sure you have pushed your changes.');
}
const parts = remoteHeadLine.split('\n')[0].split(/\s+/);
if (parts.length < 2) {
throw new Error(`Unexpected git ls-remote output: "${remoteHeadLine}". Make sure your git remote and branch are configured correctly.`);
}
const remoteHash = parts[0];
if (remoteHash.length !== 40) {
throw new Error('Failed to extract a valid remote commit hash.');
}
if (remoteHash !== commitHash) {
throw new Error('Your local commit has not been pushed to GitHub. Run git push then try publishing again.');
}
logger.success('Local commit is synced with remote.');
return commitHash;
};
export default verifyGitState;
@@ -0,0 +1,11 @@
/* eslint-disable no-console */
import { green, yellow, red } from 'chalk';
const logger = {
info: (msg: string) => console.log(msg),
success: (msg: string) => console.log(green(msg)),
warn: (msg: string) => console.log(yellow(msg)),
error: (msg: string) => console.log(red(msg)),
};
export default logger;