From 942d0be0fa339378c9b97572a9707d52e722d382 Mon Sep 17 00:00:00 2001 From: Akshaj Rawat <159126002+akshajrawat@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:09:24 +0530 Subject: [PATCH] Tools: Resolves #16012: Add GitHub authentication and issue submission to publish script (#16022) --- .gitignore | 5 +- .ignore.eslint | 4 +- .../app/templates/package_TEMPLATE.json | 4 +- .../app/templates/script/publish/index.ts | 31 +++++ .../script/publish/steps/authenticate.ts | 123 ++++++++++++++++++ .../script/publish/steps/submitPayload.ts | 71 ++++++++++ .../script/publish/steps/verifyBuild.ts | 2 +- .../templates/script/publish/tsconfig.json | 9 ++ .../templates/script/publish/utils/logger.ts | 7 +- .../tasks/updateIgnoredTypeScriptBuild.js | 1 + 10 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 packages/generator-joplin/generators/app/templates/script/publish/index.ts create mode 100644 packages/generator-joplin/generators/app/templates/script/publish/steps/authenticate.ts create mode 100644 packages/generator-joplin/generators/app/templates/script/publish/steps/submitPayload.ts create mode 100644 packages/generator-joplin/generators/app/templates/script/publish/tsconfig.json diff --git a/.gitignore b/.gitignore index 625386c4f5..2a389dadaa 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,8 @@ docs/**/*.mustache !.yarn/sdks !.yarn/versions +packages/generator-joplin/generators/app/templates/script/publish/dist/ + # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD packages/app-cli/app/LinkSelector.js packages/app-cli/app/ResourceServer.js @@ -1300,9 +1302,6 @@ 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 diff --git a/.ignore.eslint b/.ignore.eslint index ac6ccff8bb..b97cb03be6 100644 --- a/.ignore.eslint +++ b/.ignore.eslint @@ -8,6 +8,7 @@ _releases/ **/node_modules/ **/abcjs-basic-min.js packages/generator-joplin/generators/app/templates/api/ +packages/generator-joplin/generators/app/templates/script/publish/dist/ Assets/ docs/ highlight.pack.js @@ -1326,9 +1327,6 @@ 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 diff --git a/packages/generator-joplin/generators/app/templates/package_TEMPLATE.json b/packages/generator-joplin/generators/app/templates/package_TEMPLATE.json index 92fad8d15e..d96d929185 100644 --- a/packages/generator-joplin/generators/app/templates/package_TEMPLATE.json +++ b/packages/generator-joplin/generators/app/templates/package_TEMPLATE.json @@ -5,7 +5,8 @@ "dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive", "prepare": "npm run dist", "updateVersion": "webpack --env joplin-plugin-config=updateVersion", - "update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force" + "update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force", + "submit": "tsc --project script/publish/tsconfig.json && node ./script/publish/dist/index.js" }, "license": "MIT", "keywords": [ @@ -15,6 +16,7 @@ "publish" ], "devDependencies": { + "@octokit/auth-oauth-device": "^6.1.0", "@types/node": "^18.7.13", "chalk": "^4.1.0", "copy-webpack-plugin": "^11.0.0", diff --git a/packages/generator-joplin/generators/app/templates/script/publish/index.ts b/packages/generator-joplin/generators/app/templates/script/publish/index.ts new file mode 100644 index 0000000000..574692e76a --- /dev/null +++ b/packages/generator-joplin/generators/app/templates/script/publish/index.ts @@ -0,0 +1,31 @@ +import { authenticate } from './steps/authenticate'; +import submitPayload from './steps/submitPayload'; +import verifyBuild from './steps/verifyBuild'; +import verifyGitState from './steps/verifyGitState'; +import logger from './utils/logger'; + +// Central command to run all the steps of the `npm run submit` flow +async function submit() { + logger.header('Starting Joplin Plugin Publication'); + + logger.step('Phase 1: Metadata & Build Verification'); + const metadata = await verifyBuild(); + logger.divider(); + + logger.step('Phase 2: Git State Validation'); + const commitHash = await verifyGitState(); + logger.divider(); + + logger.step('Phase 3: GitHub Authentication'); + const token = await authenticate(); + logger.divider(); + + logger.step('Phase 4: Submission'); + await submitPayload(metadata, commitHash, token); +} + +void submit().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + logger.error(`Submission failed: ${message}`); + process.exitCode = 1; +}); diff --git a/packages/generator-joplin/generators/app/templates/script/publish/steps/authenticate.ts b/packages/generator-joplin/generators/app/templates/script/publish/steps/authenticate.ts new file mode 100644 index 0000000000..e081d72c3b --- /dev/null +++ b/packages/generator-joplin/generators/app/templates/script/publish/steps/authenticate.ts @@ -0,0 +1,123 @@ +// cspell:ignore rundll wslview + +import { readFile, mkdir, unlink, writeFile } from 'fs/promises'; +import { join } from 'path'; +import { homedir } from 'os'; +import { promisify } from 'util'; +import { execFile } from 'child_process'; +import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device'; +import logger from '../utils/logger'; + +const githubClientId = 'Ov23liiKfv0K6bqN2BbP'; + +const execFileAsync = promisify(execFile); + +interface Credentials { + token: string; +} + +interface DeviceVerification { + verification_uri: string; + user_code: string; +} + +// ~/.config/joplin-plugin -> For saving the token received after authentication +const configDir = join(homedir(), '.config', 'joplin-plugin'); +const credentialPath = join(configDir, 'credentials.json'); + +export const authenticate = async () => { + + // check if the user is authenticated by getting cache token + const cachedToken = await getCachedToken(); + if (cachedToken) { + logger.success('Using cached GitHub credentials..'); + return cachedToken; + } + + const deviceAuth = createOAuthDeviceAuth({ + clientType: 'oauth-app', + clientId: githubClientId, + scopes: ['public_repo'], + onVerification: async ({ verification_uri, user_code }: DeviceVerification) => { + logger.info(` + ------ GitHub Authentication Required ------ + 1. Your browser will open: ${verification_uri} + 2. Enter this code when prompted: ${user_code} + + Waiting for authorization... + `); + + await openBrowser(verification_uri); + }, + }); + + const { token: accessToken } = await deviceAuth({ type: 'oauth' }); + + await saveToken(accessToken); + logger.success('Authenticated successfully!!'); + + return accessToken; +}; + +// Opens browser for the given URL based on the OS the user is using +const openBrowser = async (url: string) => { + const platform = process.platform; + let command: string; + let args: string[]; + + if (platform === 'win32') { + command = 'rundll32.exe'; + args = ['url.dll,FileProtocolHandler', url]; + } else if (platform === 'darwin') { + command = 'open'; + args = [url]; + } else if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) { + command = 'wslview'; + args = [url]; + } else { + command = 'xdg-open'; + args = [url]; + } + + try { + await execFileAsync(command, args); + } catch { + logger.warn(`Could not open browser automatically. Please visit: ${url}`); + } +}; + +const getCachedToken = async () => { + try { + const creds: Credentials = JSON.parse(await readFile(credentialPath, 'utf8')); + if (typeof creds.token === 'string' && creds.token) { + return creds.token; + } + } catch { + // Ignore missing or invalid credentials and proceed to authentication + } + + return null; +}; + +const saveToken = async (token: string) => { + await mkdir(configDir, { recursive: true }); + + const creds: Credentials = { + token, + }; + + await writeFile(credentialPath, JSON.stringify(creds, null, 2), { + mode: 0o600, + encoding: 'utf8', + }); +}; + +export const clearCachedToken = async () => { + try { + await unlink(credentialPath); + } catch (error: unknown) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } +}; diff --git a/packages/generator-joplin/generators/app/templates/script/publish/steps/submitPayload.ts b/packages/generator-joplin/generators/app/templates/script/publish/steps/submitPayload.ts new file mode 100644 index 0000000000..c4bc92e4d4 --- /dev/null +++ b/packages/generator-joplin/generators/app/templates/script/publish/steps/submitPayload.ts @@ -0,0 +1,71 @@ +import { authenticate, clearCachedToken } from './authenticate'; +import logger from '../utils/logger'; + +interface GitHubIssueResponse { + html_url: string; + number: number; +} + +export interface PluginMetadata { + name: string; + version: string; + repositoryUrl: string; +} + +const registryRepo = 'joplin/plugins-test'; +const submissionTimeoutMs = 30_000; + +const createSubmissionIssue = async (issueTitle: string, issueBody: string, token: string) => { + return await fetch(`https://api.github.com/repos/${registryRepo}/issues`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/vnd.github.v3+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title: issueTitle, + body: issueBody, + }), + signal: AbortSignal.timeout(submissionTimeoutMs), + }); +}; + +// Creates an issue on the joplin/plugins-test repository for the plugin submission +const submitPayload = async (metadata: PluginMetadata, commitHash: string, token: string) => { + const issueTitle = `[Plugin Submission] ${metadata.name} v${metadata.version}`; + const payload = { + plugin_name: metadata.name, + version: metadata.version, + repository_url: metadata.repositoryUrl, + commit_hash: commitHash, + }; + const issueBody = `\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\``; + + logger.info('Submitting to Joplin registry...'); + + let response = await createSubmissionIssue(issueTitle, issueBody, token); + + if (response.status === 401) { + logger.warn('Cached GitHub credentials are no longer valid. Re-authenticating...'); + await clearCachedToken(); + const refreshedToken = await authenticate(); + response = await createSubmissionIssue(issueTitle, issueBody, refreshedToken); + } + + // If the issue is not created show the response message and status on terminal + if (response.status !== 201) { + const errorBody = await response.text(); + logger.error(`Status: ${response.status}`); + logger.error(`Response: ${errorBody}`); + throw new Error('Submission failed. See the error above.'); + } + + const issue = await response.json() as GitHubIssueResponse; + + logger.success('------ Submission Successful! ------'); + logger.info(`Track your submission here:\n${issue.html_url}`); + logger.info('Your plugin will appear in the registry once a maintainer approves it.'); +}; + +export default submitPayload; diff --git a/packages/generator-joplin/generators/app/templates/script/publish/steps/verifyBuild.ts b/packages/generator-joplin/generators/app/templates/script/publish/steps/verifyBuild.ts index f4bbc6b702..51d2e43504 100644 --- a/packages/generator-joplin/generators/app/templates/script/publish/steps/verifyBuild.ts +++ b/packages/generator-joplin/generators/app/templates/script/publish/steps/verifyBuild.ts @@ -25,7 +25,7 @@ const verifyBuild = async () => { const validateMetadata = async () => { logger.info('Validating metadata...'); - // process.cwd() is the plugin root dir when run via `npm run publish` + // process.cwd() is the plugin root dir when run via `npm run submit` const manifestPath = join(process.cwd(), 'src/manifest.json'); const packageJsonPath = join(process.cwd(), 'package.json'); diff --git a/packages/generator-joplin/generators/app/templates/script/publish/tsconfig.json b/packages/generator-joplin/generators/app/templates/script/publish/tsconfig.json new file mode 100644 index 0000000000..24757c6433 --- /dev/null +++ b/packages/generator-joplin/generators/app/templates/script/publish/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "rootDir": "./", + "module": "commonjs", + "target": "es2015", + "skipLibCheck": true + } +} diff --git a/packages/generator-joplin/generators/app/templates/script/publish/utils/logger.ts b/packages/generator-joplin/generators/app/templates/script/publish/utils/logger.ts index 4edd9a824c..23ba9d624a 100644 --- a/packages/generator-joplin/generators/app/templates/script/publish/utils/logger.ts +++ b/packages/generator-joplin/generators/app/templates/script/publish/utils/logger.ts @@ -1,11 +1,16 @@ /* eslint-disable no-console */ -import { green, yellow, red } from 'chalk'; +import { green, yellow, red, cyan, blue, gray } 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)), + + // UI Elements for the CLI + header: (msg: string) => console.log(cyan.bold(`\n ${msg}\n`)), + step: (msg: string) => console.log(blue.bold(msg)), + divider: () => console.log(gray('\n--------------------------------------------------')), }; export default logger; diff --git a/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js b/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js index 0bc4b38fb1..1d1a1da1c9 100644 --- a/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js +++ b/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js @@ -32,6 +32,7 @@ module.exports = { 'packages/app-mobile/android/**', 'packages/app-mobile/ios/**', 'packages/default-plugins/plugin-sources/**', + 'packages/generator-joplin/generators/app/templates/script/publish/**', 'packages/fork-sax/**', 'packages/lib/plugin_types/**', 'packages/server/**',