diff --git a/README.md b/README.md index 906e401..73211d8 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ This package export the following plugins: Verify the presence of the authentication parameters, which are set via environment variables (see [Chrome webstore authentication][chrome-authentication]). +#### `verifyConditions` parameters + +- `extensionId`: **REQUIRED** parameter. The `extension id` from the webstore. For example: If the url of your extension is [https://chrome.google.com/webstore/detail/webplayer-hotkeys-shortcu/ikmkicnmahfdilneilgibeppbnolgkaf](https://chrome.google.com/webstore/detail/webplayer-hotkeys-shortcu/ikmkicnmahfdilneilgibeppbnolgkaf), then the last portion, `ikmkicnmahfdilneilgibeppbnolgkaf`, will be the `extension id`. You can also take this ID on the [developers dashboard](https://chrome.google.com/webstore/developer/dashboard), under the name `Item ID` located inside the `More info` dialog. This is used so that we can confirm that the credentials are working for the extension you are trying to publish. + ### `prepare` Writes the correct version to the `manifest.json` and creates a `zip` file with everything inside the `dist` folder. @@ -58,14 +62,13 @@ This plugin requires some parameters to be set, so be sure to check below and fi Uploads the generated zip file to the webstore and publishes a new release. +Unfortunately, due to Google's restrictions, this plugin can only publish extensions that already exists on the store, so you will have to at least make a draft release for yourself, so the plugin can create a proper release for the first time. You can create a draft release with just a minimum `manifest.json` with version `0.0.1` compressed in a zip file. +If you decide to make the draft, make sure to fill all the required fields on the drafts page, otherwise the publish will fail with a `400` status code (Bad request). + #### `publish` parameters - `extensionId`: **REQUIRED** parameter. The `extension id` from the webstore. For example: If the url of your extension is [https://chrome.google.com/webstore/detail/webplayer-hotkeys-shortcu/ikmkicnmahfdilneilgibeppbnolgkaf](https://chrome.google.com/webstore/detail/webplayer-hotkeys-shortcu/ikmkicnmahfdilneilgibeppbnolgkaf), then the last portion, `ikmkicnmahfdilneilgibeppbnolgkaf`, will be the `extension id`. You can also take this ID on the [developers dashboard](https://chrome.google.com/webstore/developer/dashboard), under the name `Item ID` located inside the `More info` dialog. - Unfortunately, due to Google's restrictions, this plugin can only publish extensions that already exists on the store, so you will have to at least make a draft release for yourself, so the plugin can create a proper release for the first time. You can create a draft release with just a minimum `manifest.json` with version `0.0.1` compressed in a zip file. - - If you decide to make the draft, make sure to fill all the required fields on the drafts page, otherwise the publishing will fail with a `400` status code (Bad request). - - `asset`: **REQUIRED** parameter. The zip file that will be published to the chrome webstore. - `target`: Valid options are: @@ -85,7 +88,13 @@ A basic configuration file example is available below: ```json { - "verifyConditions": ["semantic-release-chrome", "@semantic-release/github"], + "verifyConditions": [ + { + "path": "semantic-release-chrome", + "extensionId": "mppjhhbajcciljocgbadbhbgphjfdmhj" + }, + "@semantic-release/github" + ], "prepare": [ { "path": "semantic-release-chrome", diff --git a/package.json b/package.json index 7bc1143..5c5d31f 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ }, "dependencies": { "@semantic-release/error": "3.0.0", + "aggregate-error": "4.0.1", "archiver": "5.3.1", "chrome-webstore-upload": "1.0.0", "fs-extra": "10.1.0" diff --git a/src/getEsModule.ts b/src/getEsModule.ts new file mode 100644 index 0000000..b600c30 --- /dev/null +++ b/src/getEsModule.ts @@ -0,0 +1,14 @@ +const modulesCache: { [keyof: string]: any } = {} + +async function getEsModule(moduleName: string) { + if (modulesCache[moduleName]) { + return modulesCache[moduleName] + } + + const module = (await import(moduleName)).default + modulesCache[moduleName] = module + + return module +} + +export default getEsModule diff --git a/src/publish.ts b/src/publish.ts index a32f08e..6828d45 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -3,22 +3,10 @@ import { createReadStream } from 'fs-extra' import { Context } from 'semantic-release' import type PluginConfig from './@types/pluginConfig' +import getEsModule from './getEsModule' const errorWhitelist = ['PUBLISHED_WITH_FRICTION_WARNING'] -const modulesCache: { [keyof: string]: any } = {} - -const getEsModule = async (module: string) => { - if (modulesCache[module]) { - return modulesCache[module] - } - - const esModule = await import(module) - modulesCache[module] = esModule.default || esModule - - return modulesCache[module] -} - const publish = async ( { extensionId, target, asset }: PluginConfig, { logger }: Context, @@ -43,49 +31,103 @@ const publish = async ( ) } - const webStore = await ( - await getEsModule('chrome-webstore-upload') - )({ + const chromeWebstoreUpload = (await getEsModule( + 'chrome-webstore-upload', + )) as typeof import('chrome-webstore-upload')['default'] + + const webStore = (await chromeWebstoreUpload({ clientId, clientSecret, extensionId, refreshToken, - }) + })) as typeof import('chrome-webstore-upload')['default'] + + logger.log('Creating zip file...') const zipFile = createReadStream(asset) - const uploadRes = await webStore.uploadExisting(zipFile) + const errorMessage = ` + + [ERROR] Semantic Release Chrome + + Unfortunately we can't tell for sure what's the reason for that, but usually this happens when there's something wrong or missing with the configs. + Make sure to check: + + * The extensionId is correctly set on the plugin config + * The environment variables GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REFRESH_TOKEN are correctly set (Double check the authentication guide: https://github.com/GabrielDuarteM/semantic-release-chrome/blob/master/Authentication.md) + * Go to https://chrome.google.com/webstore/devconsole, click on the extension you are publishing, and verify that there's no errors there (for example, on the "Why can't I submit" button modal, or on any of the other tabs there) + +` + let uploadRes + + logger.log('Uploading zip file to Google Web Store...') + try { + uploadRes = await webStore.uploadExisting(zipFile) + } catch (err) { + throw new SemanticReleaseError( + `Error uploading extension to Google Web Store. ${errorMessage} Error details:\n\n${err}`, + err as string, + ) + } + + const AggregateError = (await getEsModule( + 'aggregate-error', + )) as typeof import('aggregate-error')['default'] - if (uploadRes.uploadState === 'FAILURE') { + if (uploadRes?.uploadState === 'FAILURE') { const errors: SemanticReleaseError[] = [] + uploadRes.itemError.forEach((err: any) => { const semanticError = new SemanticReleaseError( err.error_detail, err.error_code, ) + errors.push(semanticError) }) - throw new AggregateError(errors).errors + + throw new AggregateError(errors) } + logger.log(`Successfully uploaded extension to Google Web Store`) + if (target !== 'draft') { - const publishRes = await webStore.publish(target || 'default') + logger.log('Publishing extension to Google Web Store...') - if (!publishRes.status.includes('OK')) { + let publishRes + + try { + publishRes = await webStore.publish(target || 'default') + } catch (err) { + throw new SemanticReleaseError( + err as string, + `Error publishing extension to Google Web Store. ${errorMessage} Error details:\n\n`, + ) + } + + if (!publishRes?.status.includes('OK')) { const errors: SemanticReleaseError[] = [] + for (let i = 0; i < publishRes.status.length; i += 1) { const code = publishRes.status[i] const message = publishRes.statusDetail[i] + if (errorWhitelist.includes(code)) { logger.log(`${code}: ${message}`) } else { const err = new SemanticReleaseError(message, code) + errors.push(err) } } + if (errors.length > 0) { - throw new AggregateError(errors).errors + throw new AggregateError(errors) } } + + logger.log(`Successfully published extension to Google Web Store`) + } else { + logger.log(`Target option is set to "draft", skipping publish step`) } return { diff --git a/src/verifyConditions.ts b/src/verifyConditions.ts index a654ad4..db66cac 100644 --- a/src/verifyConditions.ts +++ b/src/verifyConditions.ts @@ -1,35 +1,78 @@ import SemanticReleaseError from '@semantic-release/error' +import { Context } from 'semantic-release' +import PluginConfig from './@types/pluginConfig' +import getEsModule from './getEsModule' const configMessage = 'Check the README.md for config info.' -const createErrorPATH = (param: string, code: string) => +const createErrorEnvFile = (param: string, code: string) => new SemanticReleaseError( - `No ${param} specified inside PATH. ${configMessage}`, + `Environment variable not found: ${param}. ${configMessage}`, code, ) -const verifyConditions = () => { - const { - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - GOOGLE_REFRESH_TOKEN, - } = process.env +const verifyConditions = async ( + { extensionId }: PluginConfig, + { logger }: Context, +) => { + const { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN } = + process.env const errors: Error[] = [] if (!GOOGLE_CLIENT_ID) { - errors.push(createErrorPATH('GOOGLE_CLIENT_ID', 'EGOOGLECLIENTID')) + errors.push(createErrorEnvFile('GOOGLE_CLIENT_ID', 'EGOOGLECLIENTID')) } if (!GOOGLE_CLIENT_SECRET) { - errors.push(createErrorPATH('GOOGLE_CLIENT_SECRET', 'EGOOGLECLIENTSECRET')) + errors.push( + createErrorEnvFile('GOOGLE_CLIENT_SECRET', 'EGOOGLECLIENTSECRET'), + ) } if (!GOOGLE_REFRESH_TOKEN) { - errors.push(createErrorPATH('GOOGLE_REFRESH_TOKEN', 'EGOOGLEREFRESHTOKEN')) + errors.push( + createErrorEnvFile('GOOGLE_REFRESH_TOKEN', 'EGOOGLEREFRESHTOKEN'), + ) + } + + if (!extensionId) { + errors.push( + new SemanticReleaseError( + "Option 'extensionId' was not included in the verifyConditions config. Check the README.md for config info.", + 'ENOEXTENSIONID', + ), + ) } if (errors.length > 0) { - throw new AggregateError(errors).errors + const AggregateError = (await getEsModule( + 'aggregate-error', + )) as typeof import('aggregate-error')['default'] + + throw new AggregateError(errors) + } + + const chromeWebstoreUpload = (await getEsModule( + 'chrome-webstore-upload', + )) as typeof import('chrome-webstore-upload')['default'] + + const webStore = await chromeWebstoreUpload({ + extensionId, + clientId: GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + refreshToken: GOOGLE_REFRESH_TOKEN, + }) + + let mainErrorMsg: string | undefined + + try { + logger.log('Verifying chrome webstore credentials...') + await webStore.get() + logger.log('Chrome webstore credentials seem to be valid.') + } catch (e) { + mainErrorMsg = + '\n[semantic-release-chrome] Could not connect to Chrome Web Store with the provided credentials. Please check if they are correct.' + throw new Error(mainErrorMsg) } } diff --git a/yarn.lock b/yarn.lock index 541af16..d07cdd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1624,6 +1624,14 @@ agentkeepalive@^4.2.1: depd "^1.1.2" humanize-ms "^1.2.1" +aggregate-error@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-4.0.1.tgz#25091fe1573b9e0be892aeda15c7c66a545f758e" + integrity sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w== + dependencies: + clean-stack "^4.0.0" + indent-string "^5.0.0" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -2035,6 +2043,13 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +clean-stack@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-4.2.0.tgz#c464e4cde4ac789f4e0735c5d75beb49d7b30b31" + integrity sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg== + dependencies: + escape-string-regexp "5.0.0" + cli-columns@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-4.0.0.tgz#9fe4d65975238d55218c41bd2ed296a7fa555646" @@ -2456,6 +2471,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-string-regexp@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -3067,6 +3087,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"