Skip to content

Commit

Permalink
✨ changes for creating executable binaries of percy CLI (#1343)
Browse files Browse the repository at this point in the history
* initial commit

* Fix install.js

* Fix lint

* Revert for oclif

* Revert code

* Add build exec script

* upload artifacts

* Mac env for creating executable

* Update output filename

* Update output filename

* Add signing & notrizing steps

* Install gon

* Update env name

* Update permissions

* Run on release only

* Mac & linux x64, arm64 executables

* Update to include current src

* Update notrize config template

* Use node 14

* Code cleanup

* Fix code for codesign

* Remove signing

* Add entitlement.plist

* Run only on publish

* Use yarn build to generate dist folder

* Add cleanup

* Use code based on env

* Add steps to create .env file

* Skip coverage for Win path

* add tests for install.js

* add tests for install.js

* remove dotenv installation

* Update env var name

* Add executable.js as entrypoint

* Fix win executable and tests

* Resolve comments

* Resolve comments

* convert artifacts to tar to retain permissions

* convert artifacts to tar to retain permissions

* Update script for correct naming

* debug unit test

* remove console log

* update logic based on platform

* Fix lint

* refactoring code

* Update to use gsed

* Fix empty elements in array

* Run executable version before uploading

* Run executable version before uploading

* Run on publish

* Verify executable in seprate step
  • Loading branch information
pankaj443 authored Aug 31, 2023
1 parent 3b92a89 commit 0592afb
Show file tree
Hide file tree
Showing 13 changed files with 513 additions and 2 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/executable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Build Executables
on:
release:
types: [published]
jobs:
lint:
name: Build Executables
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v3
with:
node-version: 14
- name: Install gon via HomeBrew for code signing and app notarization
run: |
brew tap mitchellh/gon
brew install mitchellh/gon/gon
- run: ./scripts/executable.sh
env:
APPLE_DEV_CERT: ${{secrets.APPLE_DEV_CERT}}
APPLE_ID_USERNAME: ${{secrets.APPLE_ID_USERNAME}}
APPLE_ID_KEY: ${{secrets.APPLE_ID_KEY}}
- name: Create tar files
run: |
tar -cvf percy-linux.tar percy
mv percy-osx percy
tar -cvf percy-osx.tar percy
tar -cvf percy-win.tar percy.exe
- name: Verify executable
run: ./percy --version
- uses: actions/upload-artifact@v3
with:
name: percy-osx
path: percy-osx.tar
- uses: actions/upload-artifact@v3
with:
name: percy-linux
path: percy-linux.tar
- uses: actions/upload-artifact@v3
with:
name: percy-win
path: percy-win.tar
4 changes: 4 additions & 0 deletions babel.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ module.exports = {
exclude: ['dist', 'test']
}]
]
},
dev: {
presets: ["@babel/preset-env"],
plugins: ["babel-plugin-transform-import-meta"]
}
}
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"url": "https://github.com/percy/cli"
},
"scripts": {
"build_cjs": "BABEL_ENV=dev babel packages -d build",
"build": "lerna run build --stream",
"build:watch": "lerna run build --parallel -- --watch",
"build:pack": "mkdir -p ./packs && lerna exec npm pack && mv ./packages/*/*.tgz ./packs",
Expand All @@ -30,6 +31,7 @@
"@babel/eslint-parser": "^7.14.7",
"@babel/preset-env": "^7.14.7",
"@babel/register": "^7.17.7",
"babel-plugin-transform-import-meta": "^2.2.1",
"@rollup/plugin-alias": "^4.0.0",
"@rollup/plugin-babel": "^6.0.0",
"@rollup/plugin-commonjs": "^21.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/bin/run.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node

// DO NOT REMOVE: Update NODE_ENV for executable
// ensure that we're running within a supported node version
if (parseInt(process.version.split('.')[0].substring(1), 10) < 14) {
console.error(`Node ${process.version} is not supported. Percy only ` + (
Expand Down
16 changes: 15 additions & 1 deletion packages/cli/src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ function importLegacyCommands(commandsPath) {
});
}

function formatFilepath(filepath) {
/* istanbul ignore next */
if (!(process.platform.startsWith('win'))) {
filepath = '/' + filepath;
}
return filepath;
}

// Imports and returns compatibile CLI commands from various sources
export async function importCommands() {
let root = path.resolve(url.fileURLToPath(import.meta.url), '../..');
Expand Down Expand Up @@ -140,7 +148,13 @@ export async function importCommands() {
pkgs.set(pkg.name, () => Promise.all(
pkg['@percy/cli'].commands.map(async cmdPath => {
let modulePath = path.join(pkgPath, cmdPath);
let module = await import(url.pathToFileURL(modulePath).href);
// Below code is used in scripts/build.sh to update href
let module = null;
if (process.env.NODE_ENV === 'executable') {
module = await import(formatFilepath(modulePath));
} else {
module = await import(url.pathToFileURL(modulePath).href);
}
module.default.packageInformation ||= pkg;
return module.default;
})
Expand Down
66 changes: 66 additions & 0 deletions packages/cli/test/commands.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,72 @@ describe('CLI commands', () => {
});
});

describe('from node_modules with executable', () => {
beforeEach(async () => {
process.env.NODE_ENV = 'executable';
});

afterEach(() => {
delete process.env.NODE_ENV;
});

const mockCmds = {
'@percy/cli-exec': { name: 'exec' },
'@percy/cli-config': { name: 'config' },
'@percy/storybook': { name: 'storybook' },
'@percy/core': null,
'@percy/cli': null,
'percy-cli-custom': { name: 'custom' },
'percy-cli-other': null,
'other-dep': null
};

const expectedCmds = [
jasmine.objectContaining({ name: 'config' }),
jasmine.objectContaining({ name: 'custom' }),
jasmine.objectContaining({ name: 'exec' }),
jasmine.objectContaining({ name: 'storybook' })
];

it('imports from dependencies', async () => {
await mockModuleCommands(path.resolve('.'), mockCmds);
await expectAsync(importCommands()).toBeResolvedTo(expectedCmds);
expect(logger.stdout).toEqual([]);
expect(logger.stderr).toEqual([]);
});

it('imports from a parent directory', async () => {
await mockModuleCommands(path.resolve('../..'), mockCmds);
await expectAsync(importCommands()).toBeResolvedTo(expectedCmds);
expect(logger.stdout).toEqual([]);
expect(logger.stderr).toEqual([]);
});

it('imports from the current project', async () => {
await mockModuleCommands(process.cwd(), mockCmds);
await expectAsync(importCommands()).toBeResolvedTo(expectedCmds);
expect(logger.stdout).toEqual([]);
expect(logger.stderr).toEqual([]);
});

it('automatically includes package information', async () => {
await mockModuleCommands(path.resolve('.'), mockCmds);
let cmds = await importCommands();

expect(cmds[0].packageInformation.name).toEqual('@percy/cli-config');
});

it('handles errors and logs debug info', async () => {
fs.$vol.fromJSON({ './node_modules': null });
fs.readdirSync.and.throwError(new Error('EACCES'));
await expectAsync(importCommands()).toBeResolvedTo([]);
expect(logger.stdout).toEqual([]);
expect(logger.stderr).toEqual([
jasmine.stringContaining('[percy:cli:plugins] Error: EACCES')
]);
});
});

describe('from yarn pnp', () => {
let Module, plugPnpApi;

Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import url from 'url';
import path from 'path';
import https from 'https';
import logger from '@percy/logger';
import cp from 'child_process';
import { ProxyHttpsAgent } from '@percy/client/utils';

// Formats a raw byte integer as a string
Expand Down Expand Up @@ -63,7 +64,22 @@ export async function download({
executable
}) {
let outdir = path.join(directory, revision);
if (process.env.NODE_ENV === 'executable') {
if (outdir.charAt(0) === '/') {
outdir = outdir.replace('/', '');
}
}

let command = 'pwd';
let archive = path.join(outdir, decodeURIComponent(url.split('/').pop()));
if (process.env.NODE_ENV === 'executable') {
/* istanbul ignore next */
if (process.platform.startsWith('win')) {
command = 'cd';
}
outdir = outdir.replace('C:\\', '');
archive = archive.replace('C:\\', '');
}
let exec = path.join(outdir, executable);

if (!fs.existsSync(exec)) {
Expand Down Expand Up @@ -106,6 +122,17 @@ export async function download({
);
}).on('error', reject));

if (process.env.NODE_ENV === 'executable') {
let output = cp.execSync(command, { encoding: 'utf-8' }).trim();
let prefix = null;
if (process.platform.startsWith('win')) {
prefix = '\\';
} else {
prefix = '/';
}
archive = output.concat(prefix, archive);
outdir = output.concat(prefix, outdir);
}
// extract the downloaded file
await extract(archive, outdir);

Expand Down
Loading

0 comments on commit 0592afb

Please sign in to comment.