Skip to content
Permalink
Browse files

feat(publisher): adds dryRun and resumeDryRun to the API to allow pos…

…t-make publishes

Fixes #230
  • Loading branch information
MarshallOfSound authored and malept committed Jun 28, 2017
1 parent 3ec0cfa commit 288edbc193d84d7c9cda78e335d2e3ca9c5f5af7
Showing with 312 additions and 13 deletions.
  1. +9 −2 src/api/make.js
  2. +80 −10 src/api/publish.js
  3. +4 −0 src/electron-forge-publish.js
  4. +1 −1 src/publishers/github.js
  5. +76 −0 src/util/publish-state.js
  6. +142 −0 test/fast/publish_spec.js
@@ -146,14 +146,21 @@ export default async (providedOptions = {}) => {
// eslint-disable-next-line no-loop-func
await asyncOra(`Making for target: ${target.cyan} - On platform: ${platform.cyan} - For arch: ${targetArch.cyan}`, async () => {
try {
outputs.push(await maker({
const output = await maker({
dir: packageDir,
appName,
targetPlatform: platform,
targetArch,
forgeConfig,
packageJSON,
}));
});

output.platform = platform;
output.arch = targetArch;
output.packageJSON = packageJSON;
output.forgeConfig = forgeConfig;

outputs.push(output);
} catch (err) {
if (err) {
throw {
@@ -1,14 +1,19 @@
import 'colors';
import debug from 'debug';
import fs from 'fs-extra';
import path from 'path';

import asyncOra from '../util/ora-handler';
import getForgeConfig from '../util/forge-config';
import readPackageJSON from '../util/read-package-json';
import requireSearch from '../util/require-search';
import resolveDir from '../util/resolve-dir';
import PublishState from '../util/publish-state';

import make from './make';

const d = debug('electron-forge:publish');

/**
* @typedef {Object} PublishOptions
* @property {string} [dir=process.cwd()] The path to the app to be published
@@ -17,6 +22,10 @@ import make from './make';
* @property {string} [tag=packageJSON.version] The string to tag this release with
* @property {Array<string>} [publishTargets=[github]] The publish targets
* @property {MakeOptions} [makeOptions] Options object to passed through to make()
* @property {string} [outDir=`${dir}/out`] The path to the directory containing generated distributables
* @property {boolean} [dryRun=false] Whether or not to generate dry run meta data and not actually publish
* @property {boolean} [dryRunResume=false] Whether or not to attempt to resume a previously saved dryRun and publish
* @property {Object} [makeResults=null] Provide results from make so that the publish step doesn't run make itself
*/

/**
@@ -25,21 +34,84 @@ import make from './make';
* @param {PublishOptions} providedOptions - Options for the Publish method
* @return {Promise} Will resolve when the publish process is complete
*/
export default async (providedOptions = {}) => {
const publish = async (providedOptions = {}) => {
// eslint-disable-next-line prefer-const, no-unused-vars
let { dir, interactive, authToken, tag, publishTargets, makeOptions } = Object.assign({
let { dir, interactive, authToken, tag, publishTargets, makeOptions, dryRun, dryRunResume, makeResults } = Object.assign({
dir: process.cwd(),
interactive: false,
tag: null,
makeOptions: {},
publishTargets: null,
dryRun: false,
dryRunResume: false,
makeResults: null,
}, providedOptions);
asyncOra.interactive = interactive;

const makeResults = await make(Object.assign({
dir,
interactive,
}, makeOptions));
const outDir = providedOptions.outDir || path.resolve(dir, 'out');
const dryRunDir = path.resolve(outDir, 'publish-dry-run');

if (dryRun && dryRunResume) {
throw 'Can\'t dry run and resume a dry run at the same time';
}
if (dryRunResume && makeResults) {
throw 'Can\'t resume a dry run and use the provided makeResults at the same time';
}

let packageJSON = await readPackageJSON(dir);

let forgeConfig = await getForgeConfig(dir);

if (dryRunResume) {
d('attempting to resume from dry run');
const publishes = await PublishState.loadFromDirectory(dryRunDir);
for (const states of publishes) {
d('publishing for given state set');
await publish({
dir,
interactive,
authToken,
tag,
target,
makeOptions,
dryRun,
dryRunResume: false,
makeResults: states.map(({ state }) => state),
});
}
return;
} else if (!makeResults) {
d('triggering make');
makeResults = await make(Object.assign({
dir,
interactive,
}, makeOptions));
} else {
// Restore values from dry run
d('restoring publish settings from dry run');

for (const makeResult of makeResults) {
packageJSON = makeResult.packageJSON;
forgeConfig = makeResult.forgeConfig;
makeOptions.platform = makeResult.platform;
makeOptions.arch = makeResult.arch;

for (const makePath of makeResult.paths) {
if (!await fs.exists(makePath)) {
throw `Attempted to resume a dry run but an artifact (${makePath}) could not be found`;
}
}
}

makeResults = makeResults.map(makeResult => makeResult.paths);
}

if (dryRun) {
d('saving results of make in dry run state');
await fs.remove(dryRunDir);
await PublishState.saveToDirectory(dryRunDir, makeResults);
return;
}

dir = await resolveDir(dir);
if (!dir) {
@@ -51,10 +123,6 @@ export default async (providedOptions = {}) => {
return accum;
}, []);

const packageJSON = await readPackageJSON(dir);

const forgeConfig = await getForgeConfig(dir);

if (publishTargets === null) {
publishTargets = forgeConfig.publish_targets[makeOptions.platform || process.platform];
}
@@ -77,3 +145,5 @@ export default async (providedOptions = {}) => {
await publisher(artifacts, packageJSON, forgeConfig, authToken, tag, makeOptions.platform || process.platform, makeOptions.arch || process.arch);
}
};

export default publish;
@@ -14,6 +14,8 @@ import { getMakeOptions } from './electron-forge-make';
.option('--auth-token', 'Authorization token for your publisher target (if required)')
.option('--tag', 'The tag to publish to on GitHub')
.option('--target [target[,target...]]', 'The comma-separated deployment targets, defaults to "github"')
.option('--dry-run', 'Triggers a publish dry run which saves state and doesn\'t upload anything')
.option('--from-dry-run', 'Attempts to publish artifacts from the last saved dry run')
.allowUnknownOption(true)
.action((cwd) => {
if (!cwd) return;
@@ -30,6 +32,8 @@ import { getMakeOptions } from './electron-forge-make';
interactive: true,
authToken: program.authToken,
tag: program.tag,
dryRun: program.dryRun,
dryRunResume: program.fromDryRun,
};
if (program.target) publishOpts.publishTargets = program.target.split(',');

@@ -12,7 +12,7 @@ export default async (artifacts, packageJSON, forgeConfig, authToken, tag) => {
const github = new GitHub(authToken, true);

let release;
await asyncOra('Searching for target Release', async () => {
await asyncOra('Searching for target release', async () => {
try {
release = (await github.getGitHub().repos.getReleases({
owner: forgeConfig.github_repository.owner,
@@ -0,0 +1,76 @@
import crypto from 'crypto';
import fs from 'fs-extra';
import path from 'path';

const EXTENSION = '.forge.publish';

export default class PublishState {
static async loadFromDirectory(directory) {
if (!await fs.exists(directory)) {
throw new Error(`Attempted to load publish state from a missing directory: ${directory}`);
}

const publishes = [];
for (const dirName of await fs.readdir(directory)) {
const subDir = path.resolve(directory, dirName);
const states = [];
if ((await fs.stat(subDir)).isDirectory()) {
const filePaths = (await fs.readdir(subDir))
.filter(fileName => fileName.endsWith(EXTENSION))
.map(fileName => path.resolve(subDir, fileName));

for (const filePath of filePaths) {
const state = new PublishState(filePath);
await state.load();
states.push(state);
}
}
publishes.push(states);
}
return publishes;
}

static async saveToDirectory(directory, artifacts) {
const id = crypto.createHash('md5').update(JSON.stringify(artifacts)).digest('hex');
for (const artifact of artifacts) {
const state = new PublishState(path.resolve(directory, id, 'null'), '', false);
state.setState({
paths: Array.from(artifact),
platform: artifact.platform,
arch: artifact.arch,
packageJSON: artifact.packageJSON,
forgeConfig: artifact.forgeConfig,
});
await state.saveToDisk();
}
}

constructor(filePath, hasHash = true) {
this.dir = path.dirname(filePath);
this.path = filePath;
this.hasHash = hasHash;
}

generateHash() {
const content = JSON.stringify(this.state || {});
return crypto.createHash('md5').update(content).digest('hex');
}

setState(state) {
this.state = state;
}

async load() {
this.state = await fs.readJson(this.path);
}

async saveToDisk() {
if (!this.hasHash) {
this.path = path.resolve(this.dir, `${this.generateHash()}${EXTENSION}`);
this.hasHash = true;
}

await fs.mkdirs(path.dirname(this.path));
await fs.writeJson(this.path, this.state);
}
}

0 comments on commit 288edbc

Please sign in to comment.
You can’t perform that action at this time.