Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(publisher-ers): support flavor config #2766

Merged
merged 3 commits into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/publisher/electron-release-server/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ export interface PublisherERSConfig {
* Default: stable
*/
channel?: string;

/**
* The "flavor" of the binary that you want to release to.
* This is useful if you want to provide multiple versions
* of the same application version (e.g. full and lite)
* to end users.
*/
flavor?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const d = debug('electron-forge:publish:ers');
interface ERSVersion {
name: string;
assets: { name: string }[];
flavor?: string;
}

const fetchAndCheckStatus = async (url: RequestInfo, init?: RequestInit): Promise<Response> => {
Expand Down Expand Up @@ -73,12 +74,15 @@ export default class PublisherERS extends PublisherBase<PublisherERSConfig> {
fetchAndCheckStatus(api(apiPath), { ...(options || {}), headers: { ...(options || {}).headers, Authorization: `Bearer ${token}` } });

const versions: ERSVersion[] = await (await authFetch('api/version')).json();
const flavor = config.flavor || 'default';

for (const makeResult of makeResults) {
const { packageJSON } = makeResult;
const artifacts = makeResult.artifacts.filter((artifactPath) => path.basename(artifactPath).toLowerCase() !== 'releases');

const existingVersion = versions.find((version) => version.name === packageJSON.version);
const existingVersion = versions.find((version) => {
return version.name === packageJSON.version && (!version.flavor || version.flavor === flavor);
});

let channel = 'stable';
if (config.channel) {
Expand All @@ -97,6 +101,7 @@ export default class PublisherERS extends PublisherBase<PublisherERSConfig> {
channel: {
name: channel,
},
flavor: config.flavor,
name: packageJSON.version,
notes: '',
}),
Expand Down
207 changes: 205 additions & 2 deletions packages/publisher/electron-release-server/test/PublisherERS_spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,217 @@
import { expect } from 'chai';
import { ForgeConfig } from '@electron-forge/shared-types';
import { ForgeConfig, ForgeMakeResult } from '@electron-forge/shared-types';
import fetchMock from 'fetch-mock';
import proxyquire from 'proxyquire';

describe('PublisherERS', () => {
let fetch: typeof fetchMock;

beforeEach(() => {
fetch = fetchMock.sandbox();
});
it('fail if the server returns 4xx', async () => {

describe('new version', () => {
it('can publish a new version to ERS', async () => {
const baseUrl = 'https://example.com';
const token = 'FAKE_TOKEN';
const flavor = 'lite';
const version = '3.0.0';

// mock login
fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
// mock fetch all existing versions
fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [], flavor: 'default' }], status: 200 });
// mock creating a new version
fetch.postOnce('path:/api/version', { status: 200 });
// mock asset upload
fetch.post('path:/api/asset', { status: 200 });
const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({
baseUrl,
username: 'test',
password: 'test',
flavor,
});

const makeResults: ForgeMakeResult[] = [
{
artifacts: ['/path/to/artifact'],
packageJSON: {
version,
},
platform: 'linux',
arch: 'x64',
},
];

await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ForgeConfig });

const calls = fetch.calls();

// creates a new version with the correct flavor, name, and channel
expect(calls[2][0]).to.equal(`${baseUrl}/api/version`);
expect(calls[2][1]?.body).to.equal(`{"channel":{"name":"stable"},"flavor":"${flavor}","name":"${version}","notes":""}`);

// uploads asset successfully
expect(calls[3][0]).to.equal(`${baseUrl}/api/asset`);
});
});

describe('existing version', () => {
it('can add new assets', async () => {
const baseUrl = 'https://example.com';
const token = 'FAKE_TOKEN';
const channel = 'stable';
const flavor = 'lite';
const version = '2.0.0';

// mock login
fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
// mock fetch all existing versions
fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [], flavor: 'lite' }], status: 200 });
// mock asset upload
fetch.post('path:/api/asset', { status: 200 });

const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({
baseUrl,
username: 'test',
password: 'test',
channel,
flavor,
});

const makeResults: ForgeMakeResult[] = [
{
artifacts: ['/path/to/artifact'],
packageJSON: {
version,
},
platform: 'linux',
arch: 'x64',
},
];

await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ForgeConfig });

const calls = fetch.calls();

// uploads asset successfully
expect(calls[2][0]).to.equal(`${baseUrl}/api/asset`);
});

it('does not replace assets for existing version', async () => {
const baseUrl = 'https://example.com';
const token = 'FAKE_TOKEN';
const channel = 'stable';
const version = '2.0.0';

// mock login
fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
// mock fetch all existing versions
fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [{ name: 'existing-artifact' }], flavor: 'default' }], status: 200 });

const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({
baseUrl,
username: 'test',
password: 'test',
channel,
});

const makeResults: ForgeMakeResult[] = [
{
artifacts: ['/path/to/existing-artifact'],
packageJSON: {
version,
},
platform: 'linux',
arch: 'x64',
},
];

await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ForgeConfig });

const calls = fetch.calls();
expect(calls).to.have.length(2);
});

it('can upload a new flavor for an existing version', async () => {
const baseUrl = 'https://example.com';
const token = 'FAKE_TOKEN';
const version = '2.0.0';
const flavor = 'lite';

// mock login
fetch.postOnce('path:/api/auth/login', { body: { token }, status: 200 });
// mock fetch all existing versions
fetch.getOnce('path:/api/version', { body: [{ name: '2.0.0', assets: [{ name: 'existing-artifact' }], flavor: 'default' }], status: 200 });
// mock creating a new version
fetch.postOnce('path:/api/version', { status: 200 });
// mock asset upload
fetch.post('path:/api/asset', { status: 200 });

const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({
baseUrl,
username: 'test',
password: 'test',
flavor,
});

const makeResults: ForgeMakeResult[] = [
{
artifacts: ['/path/to/artifact'],
packageJSON: {
version,
},
platform: 'linux',
arch: 'x64',
},
];

await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ForgeConfig });

const calls = fetch.calls();

// creates a new version with the correct flavor, name, and channel
expect(calls[2][0]).to.equal(`${baseUrl}/api/version`);
expect(calls[2][1]?.body).to.equal(`{"channel":{"name":"stable"},"flavor":"${flavor}","name":"${version}","notes":""}`);

// uploads asset successfully
expect(calls[3][0]).to.equal(`${baseUrl}/api/asset`);
});

// TODO: implement edge cases
it('can read the channel from the package.json version');
it('does not upload the RELEASES file');
});

it('fails if username and password are not provided', () => {
const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
}).default;

const publisher = new PublisherERS({});

expect(publisher.publish({ makeResults: [], dir: '', forgeConfig: {} as ForgeConfig })).to.eventually.be.rejectedWith(
'In order to publish to ERS you must set the "electronReleaseServer.baseUrl", "electronReleaseServer.username" and "electronReleaseServer.password" properties in your Forge config. See the docs for more info'
);
});

it('fails if the server returns 4xx', async () => {
fetch.mock('begin:http://example.com', { body: {}, status: 400 });
const PublisherERS = proxyquire.noCallThru().load('../src/PublisherERS', {
'node-fetch': fetch,
Expand Down