diff --git a/lib/App.js b/lib/App.js index 86a482b9..026ab26c 100644 --- a/lib/App.js +++ b/lib/App.js @@ -918,6 +918,8 @@ $ sudo systemctl restart docker manifestFolder = this.path; } + const prevVersion = manifest.version; + switch (true) { case semver.valid(version): manifest.version = version; @@ -939,6 +941,21 @@ $ sudo systemctl restart docker } Log.success(`Updated app.json version to \`${manifest.version}\``); + + const undo = async () => { + manifest.version = prevVersion; + await writeFileAsync( + path.join(manifestFolder, 'app.json'), + JSON.stringify(manifest, false, 2), + ); + + // Build app.json from Homey Compose files + if (App.hasHomeyCompose({ appPath: this.path })) { + await HomeyCompose.build({ appPath: this.path }); + } + }; + + return undo; } async changelog(text) { @@ -961,298 +978,316 @@ $ sudo systemctl restart docker } async publish() { - await this.preprocess(); + const undos = { + version: null, + }; - const profile = await AthomApi.getProfile(); - const level = profile.roleIds.includes('app_developer_trusted') - ? 'verified' - : 'publish'; - const valid = await this._validate({ level }); - if (valid !== true) throw new Error('The app is not valid, please fix the validation issues first.'); + try { + const env = await this._getEnv(); + + const manifest = App.getManifest({ appPath: this.path }); + const { + id: appId, + name: appName, + } = manifest; + let { version: appVersion } = manifest; + + const versionBumpChoices = { + patch: { + value: 'patch', + targetVersion: `${semver.inc(appVersion, 'patch')}`, + get name() { + return `Patch (to v${this.targetVersion})`; + }, + }, + minor: { + value: 'minor', + targetVersion: `${semver.inc(appVersion, 'minor')}`, + get name() { + return `Minor (to v${this.targetVersion})`; + }, + }, + major: { + value: 'major', + targetVersion: `${semver.inc(appVersion, 'major')}`, + get name() { + return `Major (to v${this.targetVersion})`; + }, + }, + }; - if (await GitCommands.isGitInstalled() && await GitCommands.isGitRepo({ path: this.path })) { - if (await this._git.hasUncommittedChanges()) { - const { shouldContinue } = await inquirer.prompt([ + // First ask if version bump is desired + const shouldUpdateVersion = (process.env.HOMEY_HEADLESS === '1') + ? { value: false } + : await inquirer.prompt([ { type: 'confirm', - name: 'shouldContinue', - default: false, - message: 'There are uncommitted changes. Are you sure you want to continue?', + name: 'value', + message: `Do you want to update your app's version number? (current v${appVersion})`, + default: true, }, ]); - if (!shouldContinue) return; - } - } - - const env = await this._getEnv(); + let shouldUpdateVersionTo = null; - const manifest = App.getManifest({ appPath: this.path }); - const { - id: appId, - name: appName, - } = manifest; - let { version: appVersion } = manifest; - - const versionBumpChoices = { - patch: { - value: 'patch', - targetVersion: `${semver.inc(appVersion, 'patch')}`, - get name() { - return `Patch (to v${this.targetVersion})`; - }, - }, - minor: { - value: 'minor', - targetVersion: `${semver.inc(appVersion, 'minor')}`, - get name() { - return `Minor (to v${this.targetVersion})`; - }, - }, - major: { - value: 'major', - targetVersion: `${semver.inc(appVersion, 'major')}`, - get name() { - return `Major (to v${this.targetVersion})`; - }, - }, - }; + // If version bump is desired ask for patch/minor/major + if (shouldUpdateVersion.value) { + shouldUpdateVersionTo = await inquirer.prompt([ + { + type: 'list', + name: 'version', + message: 'Select the desired version number', + choices: Object.values(versionBumpChoices), + }, + ]); + } - // First ask if version bump is desired - const shouldUpdateVersion = (process.env.HOMEY_HEADLESS === '1') - ? { value: false } - : await inquirer.prompt([ - { - type: 'confirm', - name: 'value', - message: `Do you want to update your app's version number? (current v${appVersion})`, - }, - ]); - let shouldUpdateVersionTo = null; + let bumpedVersion = false; + const commitFiles = []; + if (shouldUpdateVersion.value) { + // Apply new version (this changes app.json and .homeycompose/app.json if needed) + undos.version = await this.version(shouldUpdateVersionTo.version); - // If version bump is desired ask for patch/minor/major - if (shouldUpdateVersion.value) { - shouldUpdateVersionTo = await inquirer.prompt([ - { - type: 'list', - name: 'version', - message: 'Select the desired version number', - choices: Object.values(versionBumpChoices), - }, - ]); - } + // Check if only app.json or also .homeycompose/app.json needs to be committed + commitFiles.push(path.join(this.path, 'app.json')); + if (await fse.exists(path.join(this._homeyComposePath, 'app.json'))) { + commitFiles.push(path.join(this._homeyComposePath, 'app.json')); + } - let bumpedVersion = false; - const commitFiles = []; - if (shouldUpdateVersion.value) { - // Apply new version (this changes app.json and .homeycompose/app.json if needed) - await this.version(shouldUpdateVersionTo.version); + // Update version number + appVersion = versionBumpChoices[shouldUpdateVersionTo.version].targetVersion; - // Check if only app.json or also .homeycompose/app.json needs to be committed - commitFiles.push(path.join(this.path, 'app.json')); - if (await fse.exists(path.join(this._homeyComposePath, 'app.json'))) { - commitFiles.push(path.join(this._homeyComposePath, 'app.json')); + // Set flag to know that we have changed the version number + bumpedVersion = true; } - // Update version number - appVersion = versionBumpChoices[shouldUpdateVersionTo.version].targetVersion; + await this.preprocess(); - // Set flag to know that we have changed the version number - bumpedVersion = true; - } + const profile = await AthomApi.getProfile(); + const level = profile.roleIds.includes('app_developer_trusted') + ? 'verified' + : 'publish'; + const valid = await this._validate({ level }); + if (valid !== true) throw new Error('The app is not valid, please fix the validation issues first.'); - // Get or create changelog - let updatedChangelog = false; - const changelog = await Promise.resolve().then(async () => { - const changelogJsonPath = path.join(this.path, '.homeychangelog.json'); - const changelogJson = (await fse.pathExists(changelogJsonPath)) - ? await fse.readJson(changelogJsonPath) - : {}; + delete undos.version; - if (!changelogJson[appVersion] || !changelogJson[appVersion]['en']) { - if (process.env.HOMEY_HEADLESS === '1') { - throw new Error(`Missing changelog for v${appVersion}, and running in headless mode.`); + if (await GitCommands.isGitInstalled() && await GitCommands.isGitRepo({ path: this.path })) { + if (await this._git.hasUncommittedChanges()) { + const { shouldContinue } = await inquirer.prompt([ + { + type: 'confirm', + name: 'shouldContinue', + message: 'There are uncommitted changes. Are you sure you want to continue?', + default: false, + }, + ]); + if (!shouldContinue) return; } + } - const { text } = await inquirer.prompt([ - { - type: 'input', - name: 'text', - message: `(Changelog) What's new in ${appName.en} v${appVersion}?`, - validate: input => { - return input.length > 3; - }, - }, - ]); + // Get or create changelog + let updatedChangelog = false; + const changelog = await Promise.resolve().then(async () => { + const changelogJsonPath = path.join(this.path, '.homeychangelog.json'); + const changelogJson = (await fse.pathExists(changelogJsonPath)) + ? await fse.readJson(changelogJsonPath) + : {}; + + if (!changelogJson[appVersion] || !changelogJson[appVersion]['en']) { + if (process.env.HOMEY_HEADLESS === '1') { + throw new Error(`Missing changelog for v${appVersion}, and running in headless mode.`); + } - changelogJson[appVersion] = changelogJson[appVersion] || {}; - changelogJson[appVersion]['en'] = text; - await fse.writeJson(changelogJsonPath, changelogJson, { - spaces: 2, - }); + const { text } = await inquirer.prompt([ + { + type: 'input', + name: 'text', + message: `(Changelog) What's new in ${appName.en} v${appVersion}?`, + validate: input => { + return input.length > 3; + }, + }, + ]); - Log.info(` — Changelog: ${text}`); + changelogJson[appVersion] = changelogJson[appVersion] || {}; + changelogJson[appVersion]['en'] = text; + await fse.writeJson(changelogJsonPath, changelogJson, { + spaces: 2, + }); - // Mark as changed - updatedChangelog = true; + Log.info(` — Changelog: ${text}`); - // Make sure to commit changelog changes - commitFiles.push(changelogJsonPath); - } + // Mark as changed + updatedChangelog = true; - return changelogJson[appVersion]; - }); + // Make sure to commit changelog changes + commitFiles.push(changelogJsonPath); + } - // Get readme - const en = await readFileAsync(path.join(this.path, 'README.txt')) - .then(buf => buf.toString()) - .catch(err => { - throw new Error('Missing file `/README.txt`. Please provide a README for your app. The contents of this file will be visible in the App Store.'); + return changelogJson[appVersion]; }); - const readme = { en }; + // Get readme + const en = await readFileAsync(path.join(this.path, 'README.txt')) + .then(buf => buf.toString()) + .catch(err => { + throw new Error('Missing file `/README.txt`. Please provide a README for your app. The contents of this file will be visible in the App Store.'); + }); + + const readme = { en }; - // Read files in app dir - const files = await readDirAsync(this.path, { withFileTypes: true }); + // Read files in app dir + const files = await readDirAsync(this.path, { withFileTypes: true }); - // Loop all paths to check for matching readme names - for (const file of files) { - if (Object.prototype.hasOwnProperty.call(file, 'name') && typeof file.name === 'string') { + // Loop all paths to check for matching readme names + for (const file of files) { + if (Object.prototype.hasOwnProperty.call(file, 'name') && typeof file.name === 'string') { // Check for README..txt file name - if (file.name.startsWith('README.') && file.name.endsWith('.txt')) { - const languageCode = file.name.replace('README.', '').replace('.txt', ''); + if (file.name.startsWith('README.') && file.name.endsWith('.txt')) { + const languageCode = file.name.replace('README.', '').replace('.txt', ''); - // Check language code against homey-lib supported language codes - if (getAppLocales().includes(languageCode)) { + // Check language code against homey-lib supported language codes + if (getAppLocales().includes(languageCode)) { // Read contents of file into readme object - readme[languageCode] = await readFileAsync(path.join(this.path, file.name)) - .then(buf => buf.toString()); + readme[languageCode] = await readFileAsync(path.join(this.path, file.name)) + .then(buf => buf.toString()); + } } } } - } - // Get delegation token - Log.success(`Submitting ${appId}@${appVersion}...`); - if (Object.keys(env).length) { - Log.info(' — Homey.env (env.json)'); - Object.keys(env).forEach(key => { - const value = env[key]; - Log.info(` — ${key}=${Util.ellipsis(value)}`); - }); - } + // Get delegation token + Log.success(`Submitting ${appId}@${appVersion}...`); + if (Object.keys(env).length) { + Log.info(' — Homey.env (env.json)'); + Object.keys(env).forEach(key => { + const value = env[key]; + Log.info(` — ${key}=${Util.ellipsis(value)}`); + }); + } - const athomAppsApi = new AthomAppsAPI(); - const { - url, - method, - headers, - buildId, - } = await athomAppsApi.createBuild({ - $token: await AthomApi.createDelegationToken({ - audience: 'apps', - }), - env, - appId, - changelog, - version: appVersion, - readme, - }); + const athomAppsApi = new AthomAppsAPI(); + const { + url, + method, + headers, + buildId, + } = await athomAppsApi.createBuild({ + $token: await AthomApi.createDelegationToken({ + audience: 'apps', + }), + env, + appId, + changelog, + version: appVersion, + readme, + }); - // Make sure archive stream is created after any additional changes to the app - // and right before publishing - const archiveStream = await this._getPackStream(); - const { size } = await fse.stat(archiveStream.path); + // Make sure archive stream is created after any additional changes to the app + // and right before publishing + const archiveStream = await this._getPackStream(); + const { size } = await fse.stat(archiveStream.path); - Log.success(`Created Build ID ${buildId}`); - Log.success(`Uploading ${appId}@${appVersion}...`); + Log.success(`Created Build ID ${buildId}`); + Log.success(`Uploading ${appId}@${appVersion}...`); - await fetch(url, { - method, - headers: { - 'Content-Length': size, - ...headers, - }, - body: archiveStream, - }).then(async res => { - if (!res.ok) { - throw new Error(res.statusText); - } - }); + await fetch(url, { + method, + headers: { + 'Content-Length': size, + ...headers, + }, + body: archiveStream, + }).then(async res => { + if (!res.ok) { + throw new Error(res.statusText); + } + }); - // Commit the version bump and/or changelog to Git if the current path is a repo - if (await GitCommands.isGitInstalled() && await GitCommands.isGitRepo({ path: this.path })) { - let createdGitTag = false; - // Only commit and tag if version is bumped - if (bumpedVersion) { + // Commit the version bump and/or changelog to Git if the current path is a repo + if (await GitCommands.isGitInstalled() && await GitCommands.isGitRepo({ path: this.path })) { + let createdGitTag = false; + // Only commit and tag if version is bumped + if (bumpedVersion) { // First ask if version bump is desired - const shouldCommit = await inquirer.prompt([ - { - type: 'confirm', - name: 'value', - message: `Do you want to commit ${bumpedVersion ? 'the version bump' : ''} ${updatedChangelog ? 'and updated changelog' : ''}?`, - }, - ]); + const shouldCommit = await inquirer.prompt([ + { + type: 'confirm', + name: 'value', + message: `Do you want to commit ${bumpedVersion ? 'the version bump' : ''} ${updatedChangelog ? 'and updated changelog' : ''}?`, + default: true, + }, + ]); - // Check if commit is desired - if (shouldCommit.value) { + // Check if commit is desired + if (shouldCommit.value) { // If version is bumped via wizard and changelog is changed via wizard // then commit all at once - if (updatedChangelog) { - await this._git.commitFiles({ - files: commitFiles, - message: `Bump version to v${appVersion}`, - description: `Changelog: ${changelog['en']}`, - }); - Log.success(`Committed ${commitFiles.map(i => i.replace(`${this.path}/`, '')).join(', and ')} with version bump`); - } else { - await this._git.commitFiles({ - files: commitFiles, - message: `Bump version to v${appVersion}`, - }); - Log.success(`Committed ${commitFiles.map(i => i.replace(`${this.path}/`, '')).join(', and ')} with version bump`); - } - - try { - if (await this._git.hasUncommittedChanges()) { - throw new Error('There are uncommitted or untracked files in this git repository'); + if (updatedChangelog) { + await this._git.commitFiles({ + files: commitFiles, + message: `Bump version to v${appVersion}`, + description: `Changelog: ${changelog['en']}`, + }); + Log.success(`Committed ${commitFiles.map(i => i.replace(`${this.path}/`, '')).join(', and ')} with version bump`); + } else { + await this._git.commitFiles({ + files: commitFiles, + message: `Bump version to v${appVersion}`, + }); + Log.success(`Committed ${commitFiles.map(i => i.replace(`${this.path}/`, '')).join(', and ')} with version bump`); } - await this._git.createTag({ - version: appVersion, - message: changelog['en'], - }); + try { + if (await this._git.hasUncommittedChanges()) { + throw new Error('There are uncommitted or untracked files in this git repository'); + } - Log.success(`Successfully created Git tag \`${appVersion}\``); - createdGitTag = true; - } catch (error) { - Log.warning(`Warning: could not create git tag (v${appVersion}), reason:`); - Log.info(error); + await this._git.createTag({ + version: appVersion, + message: changelog['en'], + }); + + Log.success(`Successfully created Git tag \`${appVersion}\``); + createdGitTag = true; + } catch (error) { + Log.warning(`Warning: could not create git tag (v${appVersion}), reason:`); + Log.info(error); + } } } - } - if (await this._git.hasRemoteOrigin() && bumpedVersion) { - const answers = await inquirer.prompt([ - { - type: 'confirm', - name: 'push', - message: 'Do you want to push the local changes to `remote "origin"`?', - default: false, - }, - ]); + if (await this._git.hasRemoteOrigin() && bumpedVersion) { + const answers = await inquirer.prompt([ + { + type: 'confirm', + name: 'push', + message: 'Do you want to push the local changes to `remote "origin"`?', + default: false, + }, + ]); - if (answers.push) { + if (answers.push) { // First push tag - if (createdGitTag) await this._git.pushTag({ version: appVersion }); + if (createdGitTag) await this._git.pushTag({ version: appVersion }); - // Push all staged changes - await this._git.push(); - Log.success('Successfully pushed changes to remote.'); + // Push all staged changes + await this._git.push(); + Log.success('Successfully pushed changes to remote.'); + } } } + Log.success(`App ${appId}@${appVersion} successfully uploaded.`); + Log(colors.white(`\nVisit https://tools.developer.homey.app/apps/app/${appId}/build/${buildId} to publish your app.`)); + } catch (error) { + for (const undo of Object.values(undos)) { + await undo().catch(err => { + Log.error(err); + }); + } + + throw error; } - Log.success(`App ${appId}@${appVersion} successfully uploaded.`); - Log(colors.white(`\nVisit https://tools.developer.homey.app/apps/app/${appId}/build/${buildId} to publish your app.`)); } _onStd(std) { @@ -1577,6 +1612,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'switch_compose', message: 'Do you want to use Homey compose? It will split the app.json file into separate files for Drivers, Flow Cards and Discovery Strategies.', + default: true, }, ); @@ -1684,6 +1720,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'confirm', message: 'Seems good?', + default: true, }, ]); @@ -1801,6 +1838,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'shouldInstallRFDriver', message: 'Do you want to install homey-rfdriver?', + default: true, }, ]); @@ -1819,6 +1857,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'shouldInstallRFDriver', message: 'Do you want to install homey-rfdriver?', + default: true, }, ]); @@ -1837,6 +1876,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'shouldInstallRFDriver', message: 'Do you want to install homey-rfdriver?', + default: true, }, ]); @@ -1856,6 +1896,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'shouldInstallZigbeeDriver', message: 'Do you want to install homey-zigbeedriver?', + default: true, }, ]); @@ -1874,6 +1915,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'shouldInstallZwaveDriver', message: 'Do you want to install homey-zwavedriver?', + default: true, }, ]); @@ -1896,6 +1938,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'shouldInstallOAuth2App', message: 'Do you want to install homey-oauth2app?', + default: true, }, ]); @@ -2237,6 +2280,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'confirm', message: 'Seems good?', + default: true, }, ]); @@ -2501,6 +2545,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'addAnotherToken', message: 'Add more tokens?', + default: false, }, ]); @@ -2613,6 +2658,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'more', message: 'Add more MAC addresses?', + default: false, }, ]); @@ -2723,6 +2769,7 @@ $ sudo systemctl restart docker type: 'confirm', name: 'confirm', message: 'Seems good?', + default: true, }, ]); @@ -2797,11 +2844,13 @@ $ sudo systemctl restart docker type: 'confirm', name: 'license', message: 'Use standard license for Homey Apps (GPL3)?', + default: true, }, { type: 'confirm', name: 'confirm', message: 'Seems good?', + default: true, }, ]); diff --git a/package-lock.json b/package-lock.json index da2edf50..ac54fdcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homey", - "version": "3.6.1", + "version": "3.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homey", - "version": "3.6.1", + "version": "3.6.2", "license": "ISC", "dependencies": { "cli-table": "^0.3.11", diff --git a/package.json b/package.json index 30ac9407..848d022b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homey", - "version": "3.6.1", + "version": "3.6.2", "description": "Command-line interface and type declarations for Homey Apps", "main": "bin/homey.js", "files": [