Skip to content

Commit

Permalink
feat(azure-func): publish executor
Browse files Browse the repository at this point in the history
  • Loading branch information
ziacik-resco authored and ziacik committed Sep 16, 2023
1 parent 4cd1ae0 commit 69b72bd
Show file tree
Hide file tree
Showing 24 changed files with 538 additions and 38 deletions.
9 changes: 8 additions & 1 deletion packages/azure-func/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2023-09-16

### Added

- `publish` executor added.

## [0.2.0] - 2023-09-15

### Changed

- `serve` now watches the project for changes.

[unreleased]: https://github.com/ziacik/nx-tools/compare/azure-func-0.2.0...HEAD
[unreleased]: https://github.com/ziacik/nx-tools/compare/azure-func-0.3.0...HEAD
[0.3.0]: https://github.com/ziacik/nx-tools/compare/azure-func-0.3.0...azure-func-0.2.0
[0.2.0]: https://github.com/ziacik/nx-tools/releases/tag/azure-func-0.2.0
5 changes: 5 additions & 0 deletions packages/azure-func/executors.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"implementation": "./src/executors/serve/executor",
"schema": "./src/executors/serve/schema.json",
"description": "serve executor"
},
"publish": {
"implementation": "./src/executors/publish/executor",
"schema": "./src/executors/publish/schema.json",
"description": "Publishes the azure func application to Azure."
}
}
}
4 changes: 2 additions & 2 deletions packages/azure-func/package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "@ziacik/azure-func",
"version": "0.2.0",
"version": "0.3.0",
"dependencies": {
"@nx/devkit": "16.8.1",
"@nx/js": "16.8.1",
"@nx/linter": "16.8.1",
"@nx/node": "16.8.1",
"@swc/helpers": "~0.5.0",
"chalk": "^4.1.2"
"@ziacik/util": "1.0.0"
},
"type": "commonjs",
"main": "./src/index.js",
Expand Down
225 changes: 225 additions & 0 deletions packages/azure-func/src/executors/publish/executor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import * as devkit from '@nx/devkit';
import { ExecutorContext, Target } from '@nx/devkit';
import * as childProcess from 'child_process';
import executor from './executor';
import { PublishExecutorSchema } from './schema';

describe('Publish Executor', () => {
let context: ExecutorContext;
let options: PublishExecutorSchema;
let npmProcessResult: BuildResult;
let funcProcessResult: BuildResult;

beforeEach(() => {
npmProcessResult = 'succeed';
funcProcessResult = 'succeed';

context = {
root: '/root',
cwd: '/current',
isVerbose: false,
projectName: 'my-app',
targetName: 'build',
configurationName: 'production',
taskGraph: {
roots: [],
dependencies: {},
tasks: {},
},
projectGraph: {
nodes: {
'my-app': {
type: 'app',
name: 'my-app',
data: {
root: '/root',
targets: {
build: {
options: {
some: 'option',
},
},
},
},
},
},
dependencies: {},
},
projectsConfigurations: {
version: 1,
projects: {
'my-app': {
projectType: 'application',
root: '/root',
targets: {
build: {
executor: '@ziacik/azure-func:serve',
},
},
},
},
},
};

options = {
azureAppName: 'some-azure-app',
buildTarget: 'my-app:build:production',
buildTargetOptions: {
some: 'build-option',
},
};

jest.spyOn(console, 'error').mockImplementation((e) => {
throw new Error('Console error: ' + e);
});

jest.spyOn(devkit, 'readTargetOptions').mockImplementation((target: Target) => ({
outputPath: `/some/path/dist/${target.project}`,
}));

jest.spyOn(childProcess, 'spawnSync').mockImplementation((command) => {
const result = command === 'npm' ? npmProcessResult : command === 'func' ? funcProcessResult : 'terminate';

if (result === 'succeed') {
return { pid: 123, status: 0, output: [], stdout: '', stderr: '', signal: null };
} else if (result === 'fail') {
return { pid: 123, status: 1, output: [], stdout: '', stderr: '', signal: null };
} else {
throw new Error('Process spawn error.');
}
});
});

it('runs the build target first', async () => {
buildWill('succeed');
npmProcessWill('succeed');
const { success } = await executor(options, context);
expect(success).toBe(true);
expect(devkit.runExecutor).toHaveBeenCalledTimes(1);
expect(devkit.runExecutor).toHaveBeenCalledWith(
{
configuration: 'production',
project: 'my-app',
target: 'build',
},
{
some: 'build-option',
watch: false,
},
context
);
});

it('if the build fails, we fail', async () => {
buildWill('fail');
const { success } = await executor(options, context);
expect(success).toBe(false);
});

it('if subsequent build fails, we fail', async () => {
buildWill('succeed', 'succeed', 'fail');
const { success } = await executor(options, context);
expect(success).toBe(false);
});

it('will not start dependency installation if the build fails', async () => {
buildWill('fail');
await executor(options, context);
expect(childProcess.spawnSync).not.toHaveBeenCalled();
});

it('installs dependencies in the dist dir after build', async () => {
buildWill('succeed');
npmProcessWill('fail');
await executor(options, context);
expect(childProcess.spawnSync).toHaveBeenCalledWith('npm', ['install', '--omit=dev'], {
cwd: '/root/some/path/dist/my-app',
stdio: 'inherit',
});
});

it('if installing dependencies fails, we fail', async () => {
buildWill('succeed');
npmProcessWill('fail');
const { success } = await executor(options, context);
expect(success).toBe(false);
});

it('if installing dependencies throws, we fail', async () => {
buildWill('succeed');
npmProcessWill('terminate');
expectConsoleError();
const { success } = await executor(options, context);
expect(success).toBe(false);
expect(console.error).toHaveBeenCalledWith(new Error('Process spawn error.'));
});

it('will not start publish if npm i fails', async () => {
buildWill('succeed');
npmProcessWill('fail');
await executor(options, context);
expect(childProcess.spawnSync).toHaveBeenCalledWith('npm', expect.anything(), expect.anything());
expect(childProcess.spawnSync).not.toHaveBeenCalledWith('func', expect.anything(), expect.anything());
});

it('runs func to publish the app to azure', async () => {
buildWill('succeed');
npmProcessWill('succeed');
funcProcessWill('succeed');
await executor(options, context);
expect(childProcess.spawnSync).toHaveBeenCalledWith('func', ['azure', 'functionapp', 'publish', 'some-azure-app'], {
cwd: '/root/some/path/dist/my-app',
stdio: 'inherit',
});
});

it('if publish terminates, we fail', async () => {
buildWill('succeed');
npmProcessWill('succeed');
funcProcessWill('terminate');
expectConsoleError();
const output = await executor(options, context);
expect(output.success).toBe(false);
expect(console.error).toHaveBeenCalledWith(new Error('Process spawn error.'));
});

it('if publish fails, we fail', async () => {
buildWill('succeed');
npmProcessWill('succeed');
funcProcessWill('fail');
const output = await executor(options, context);
expect(output.success).toBe(false);
});

type BuildResult = 'succeed' | 'fail' | 'terminate';

function npmProcessWill(what: BuildResult): void {
npmProcessResult = what;
}

function funcProcessWill(what: BuildResult): void {
funcProcessResult = what;
}

function buildWill(...what: BuildResult[]): void {
jest.spyOn(devkit, 'runExecutor').mockResolvedValue(
(async function* () {
for (const buildResult of what) {
if (buildResult === 'terminate') {
return { success: false };
} else {
yield {
success: buildResult === 'succeed',
};
}
}

return { success: true };
})()
);
}
});

function expectConsoleError() {
jest.mocked(console.error).mockImplementation();
}
43 changes: 43 additions & 0 deletions packages/azure-func/src/executors/publish/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ExecutorContext, runExecutor } from '@nx/devkit';
import { getBuildOptions, getBuildTarget, spawnSyncChecked } from '@ziacik/util';
import { join } from 'path';
import { PublishExecutorSchema } from './schema';

export default async function runPublishExecutor(options: PublishExecutorSchema, context: ExecutorContext): Promise<{ success: boolean }> {
process.env['NODE_ENV'] ??= context?.configurationName ?? 'development';

const buildTarget = getBuildTarget(options, context);

const buildIterator = await runExecutor(buildTarget, { ...options.buildTargetOptions, watch: false }, context);

for await (const buildResult of buildIterator) {
if (!buildResult.success) {
return buildResult;
}
}

const buildOptions = getBuildOptions(buildTarget, options, context);
const distDir = join(context.root, buildOptions.outputPath);

const npmResult = buildDependenciesInDist(distDir);

if (!npmResult.success) {
return npmResult;
}

return publishDist(distDir, options.azureAppName);
}

function buildDependenciesInDist(distDir: string): { success: boolean } {
return spawnSyncChecked('npm', ['install', '--omit=dev'], {
cwd: distDir,
stdio: 'inherit',
});
}

function publishDist(distDir: string, azureAppName: string): { success: boolean } {
return spawnSyncChecked('func', ['azure', 'functionapp', 'publish', azureAppName], {
cwd: distDir,
stdio: 'inherit',
});
}
5 changes: 5 additions & 0 deletions packages/azure-func/src/executors/publish/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SchemaWithBuildTarget } from '@ziacik/util';

export interface PublishExecutorSchema extends SchemaWithBuildTarget {
azureAppName: string;
}
24 changes: 24 additions & 0 deletions packages/azure-func/src/executors/publish/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/schema",
"version": 2,
"title": "Publish executor",
"description": "",
"type": "object",
"properties": {
"azureAppName": {
"type": "string",
"description": "Name of the target azure function application on Azure."
},
"buildTarget": {
"type": "string",
"description": "The target to run to build you the app."
},
"buildTargetOptions": {
"type": "object",
"description": "Additional options to pass into the build target.",
"default": {}
}
},
"additionalProperties": false,
"required": ["azureAppName", "buildTarget"]
}
Loading

0 comments on commit 69b72bd

Please sign in to comment.