skuba 9.2.0-wat-20250115035523 → 10.0.0-main-20250312042528
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli/configure/analyseDependencies.js +4 -1
- package/lib/cli/configure/analyseDependencies.js.map +2 -2
- package/lib/cli/init/index.js +2 -2
- package/lib/cli/init/index.js.map +2 -2
- package/lib/cli/init/prompts.d.ts +1 -1
- package/lib/cli/lint/internal.js +2 -1
- package/lib/cli/lint/internal.js.map +2 -2
- package/lib/cli/lint/internalLints/detectBadCodeowners.d.ts +4 -0
- package/lib/cli/lint/internalLints/detectBadCodeowners.js +72 -0
- package/lib/cli/lint/internalLints/detectBadCodeowners.js.map +7 -0
- package/lib/cli/lint/internalLints/upgrade/patches/9.1.0/index.d.ts +2 -0
- package/lib/cli/lint/internalLints/upgrade/patches/9.1.0/index.js +35 -0
- package/lib/cli/lint/internalLints/upgrade/patches/9.1.0/index.js.map +7 -0
- package/lib/cli/lint/internalLints/upgrade/patches/9.1.0/upgradeNode.d.ts +2 -0
- package/lib/cli/lint/internalLints/upgrade/patches/9.1.0/upgradeNode.js +59 -0
- package/lib/cli/lint/internalLints/upgrade/patches/9.1.0/upgradeNode.js.map +7 -0
- package/lib/cli/migrate/index.js +10 -1
- package/lib/cli/migrate/index.js.map +2 -2
- package/lib/cli/migrate/nodeVersion/checks.d.ts +14 -0
- package/lib/cli/migrate/nodeVersion/checks.js +165 -0
- package/lib/cli/migrate/nodeVersion/checks.js.map +7 -0
- package/lib/cli/migrate/nodeVersion/getNodeTypesVersion.d.ts +6 -0
- package/lib/cli/migrate/nodeVersion/getNodeTypesVersion.js +58 -0
- package/lib/cli/migrate/nodeVersion/getNodeTypesVersion.js.map +7 -0
- package/lib/cli/migrate/nodeVersion/index.d.ts +5 -1
- package/lib/cli/migrate/nodeVersion/index.js +119 -36
- package/lib/cli/migrate/nodeVersion/index.js.map +2 -2
- package/lib/index.js +3 -3
- package/lib/utils/copy.d.ts +1 -1
- package/lib/utils/copy.js +11 -3
- package/lib/utils/copy.js.map +2 -2
- package/lib/utils/template.d.ts +2 -2
- package/lib/utils/template.js +0 -5
- package/lib/utils/template.js.map +2 -2
- package/lib/utils/version.d.ts +17 -1
- package/lib/utils/version.js +48 -18
- package/lib/utils/version.js.map +3 -3
- package/package.json +17 -19
- package/template/base/tsconfig.json +2 -2
- package/template/express-rest-api/.buildkite/pipeline.yml +4 -4
- package/template/express-rest-api/.gantry/common.yml +1 -3
- package/template/express-rest-api/.nvmrc +1 -1
- package/template/express-rest-api/Dockerfile +1 -1
- package/template/express-rest-api/Dockerfile.dev-deps +2 -2
- package/template/express-rest-api/package.json +4 -4
- package/template/greeter/.buildkite/pipeline.yml +1 -1
- package/template/greeter/.nvmrc +1 -1
- package/template/greeter/Dockerfile +2 -2
- package/template/greeter/README.md +1 -1
- package/template/greeter/package.json +4 -4
- package/template/koa-rest-api/.buildkite/pipeline.yml +4 -4
- package/template/koa-rest-api/.gantry/common.yml +1 -3
- package/template/koa-rest-api/.nvmrc +1 -1
- package/template/koa-rest-api/Dockerfile +1 -1
- package/template/koa-rest-api/Dockerfile.dev-deps +2 -2
- package/template/koa-rest-api/package.json +3 -3
- package/template/koa-rest-api/tsconfig.json +2 -2
- package/template/lambda-sqs-worker-cdk/.buildkite/pipeline.yml +2 -2
- package/template/lambda-sqs-worker-cdk/.nvmrc +1 -1
- package/template/lambda-sqs-worker-cdk/Dockerfile +3 -3
- package/template/lambda-sqs-worker-cdk/infra/__snapshots__/appStack.test.ts.snap +2 -2
- package/template/lambda-sqs-worker-cdk/infra/appStack.ts +4 -4
- package/template/lambda-sqs-worker-cdk/infra/config.ts +1 -1
- package/template/lambda-sqs-worker-cdk/infra/index.ts +3 -5
- package/template/lambda-sqs-worker-cdk/package.json +6 -6
- package/template/lambda-sqs-worker-cdk/tsconfig.json +2 -2
- package/template/oss-npm-package/.github/workflows/release.yml +1 -1
- package/template/oss-npm-package/.github/workflows/validate.yml +1 -1
- package/template/oss-npm-package/.nvmrc +1 -1
- package/template/oss-npm-package/_package.json +1 -1
- package/template/private-npm-package/.nvmrc +1 -1
- package/template/private-npm-package/_package.json +2 -2
- package/template/lambda-sqs-worker/.buildkite/pipeline.yml +0 -108
- package/template/lambda-sqs-worker/.env +0 -1
- package/template/lambda-sqs-worker/.nvmrc +0 -1
- package/template/lambda-sqs-worker/Dockerfile +0 -17
- package/template/lambda-sqs-worker/README.md +0 -132
- package/template/lambda-sqs-worker/_.npmrc +0 -13
- package/template/lambda-sqs-worker/docker-compose.yml +0 -10
- package/template/lambda-sqs-worker/package.json +0 -45
- package/template/lambda-sqs-worker/serverless.yml +0 -213
- package/template/lambda-sqs-worker/skuba.template.js +0 -33
- package/template/lambda-sqs-worker/src/app.test.ts +0 -116
- package/template/lambda-sqs-worker/src/app.ts +0 -57
- package/template/lambda-sqs-worker/src/config.ts +0 -62
- package/template/lambda-sqs-worker/src/framework/handler.test.ts +0 -61
- package/template/lambda-sqs-worker/src/framework/handler.ts +0 -43
- package/template/lambda-sqs-worker/src/framework/logging.ts +0 -27
- package/template/lambda-sqs-worker/src/framework/metrics.ts +0 -14
- package/template/lambda-sqs-worker/src/framework/validation.test.ts +0 -84
- package/template/lambda-sqs-worker/src/framework/validation.ts +0 -10
- package/template/lambda-sqs-worker/src/hooks.ts +0 -95
- package/template/lambda-sqs-worker/src/mapping/jobScorer.ts +0 -22
- package/template/lambda-sqs-worker/src/services/aws.ts +0 -5
- package/template/lambda-sqs-worker/src/services/jobScorer.test.ts +0 -44
- package/template/lambda-sqs-worker/src/services/jobScorer.ts +0 -59
- package/template/lambda-sqs-worker/src/services/pipelineEventSender.test.ts +0 -40
- package/template/lambda-sqs-worker/src/services/pipelineEventSender.ts +0 -33
- package/template/lambda-sqs-worker/src/testing/handler.ts +0 -13
- package/template/lambda-sqs-worker/src/testing/logging.ts +0 -19
- package/template/lambda-sqs-worker/src/testing/services.ts +0 -28
- package/template/lambda-sqs-worker/src/testing/types.ts +0 -33
- package/template/lambda-sqs-worker/src/types/jobScorer.ts +0 -15
- package/template/lambda-sqs-worker/src/types/pipelineEvents.ts +0 -21
- package/template/lambda-sqs-worker/tsconfig.json +0 -13
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import 'skuba-dive/register';
|
|
2
|
-
|
|
3
|
-
import type { SQSEvent } from 'aws-lambda';
|
|
4
|
-
|
|
5
|
-
import { createHandler } from 'src/framework/handler';
|
|
6
|
-
import { logger } from 'src/framework/logging';
|
|
7
|
-
import { metricsClient } from 'src/framework/metrics';
|
|
8
|
-
import { validateJson } from 'src/framework/validation';
|
|
9
|
-
import { scoreJobPublishedEvent, scoringService } from 'src/services/jobScorer';
|
|
10
|
-
import { sendPipelineEvent } from 'src/services/pipelineEventSender';
|
|
11
|
-
import { JobPublishedEventSchema } from 'src/types/pipelineEvents';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Tests connectivity to ensure appropriate access and network configuration.
|
|
15
|
-
*/
|
|
16
|
-
const smokeTest = async () => {
|
|
17
|
-
await Promise.all([scoringService.smokeTest(), sendPipelineEvent({}, true)]);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const handler = createHandler<SQSEvent>(async (event) => {
|
|
21
|
-
// Treat an empty object as our smoke test event.
|
|
22
|
-
if (!Object.keys(event).length) {
|
|
23
|
-
logger.debug('Received smoke test request');
|
|
24
|
-
return smokeTest();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const count = event.Records.length;
|
|
28
|
-
|
|
29
|
-
if (count !== 1) {
|
|
30
|
-
throw Error(`Received ${count} records`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
logger.debug({ count }, 'Received jobs');
|
|
34
|
-
|
|
35
|
-
metricsClient.distribution('job.received', event.Records.length);
|
|
36
|
-
|
|
37
|
-
const record = event.Records[0];
|
|
38
|
-
if (!record) {
|
|
39
|
-
throw new Error('Malformed SQS event with no records');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const { body } = record;
|
|
43
|
-
|
|
44
|
-
// TODO: this throws an error, which will cause the Lambda function to retry
|
|
45
|
-
// the event and eventually send it to your dead-letter queue. If you don't
|
|
46
|
-
// trust your source to provide consistently well-formed input, consider
|
|
47
|
-
// catching and handling this error in code.
|
|
48
|
-
const publishedJob = validateJson(body, JobPublishedEventSchema);
|
|
49
|
-
|
|
50
|
-
const scoredJob = await scoreJobPublishedEvent(publishedJob);
|
|
51
|
-
|
|
52
|
-
const snsMessageId = await sendPipelineEvent(scoredJob);
|
|
53
|
-
|
|
54
|
-
logger.debug({ snsMessageId }, 'Scored job');
|
|
55
|
-
|
|
56
|
-
metricsClient.distribution('job.scored', 1);
|
|
57
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Env } from 'skuba-dive';
|
|
2
|
-
|
|
3
|
-
interface Config {
|
|
4
|
-
environment: Environment;
|
|
5
|
-
|
|
6
|
-
logLevel: string;
|
|
7
|
-
metrics: boolean;
|
|
8
|
-
name: string;
|
|
9
|
-
version: string;
|
|
10
|
-
|
|
11
|
-
destinationSnsTopicArn: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type Environment = (typeof environments)[number];
|
|
15
|
-
|
|
16
|
-
const environments = ['local', 'test', 'dev', 'prod'] as const;
|
|
17
|
-
|
|
18
|
-
const environment = Env.oneOf(environments)('ENVIRONMENT');
|
|
19
|
-
|
|
20
|
-
/* istanbul ignore next: config verification makes more sense in a smoke test */
|
|
21
|
-
const configs: Record<Environment, () => Omit<Config, 'environment'>> = {
|
|
22
|
-
local: () => ({
|
|
23
|
-
logLevel: 'debug',
|
|
24
|
-
metrics: false,
|
|
25
|
-
name: '<%- serviceName %>',
|
|
26
|
-
version: 'local',
|
|
27
|
-
|
|
28
|
-
destinationSnsTopicArn: 'arn:aws:sns:us-east-2:123456789012:destination',
|
|
29
|
-
}),
|
|
30
|
-
|
|
31
|
-
test: () => ({
|
|
32
|
-
logLevel: Env.string('LOG_LEVEL', { default: 'silent' }),
|
|
33
|
-
metrics: false,
|
|
34
|
-
name: '<%- serviceName %>',
|
|
35
|
-
version: 'test',
|
|
36
|
-
|
|
37
|
-
destinationSnsTopicArn: 'arn:aws:sns:us-east-2:123456789012:destination',
|
|
38
|
-
}),
|
|
39
|
-
|
|
40
|
-
dev: () => ({
|
|
41
|
-
logLevel: 'debug',
|
|
42
|
-
metrics: true,
|
|
43
|
-
name: Env.string('SERVICE'),
|
|
44
|
-
version: Env.string('VERSION'),
|
|
45
|
-
|
|
46
|
-
destinationSnsTopicArn: Env.string('DESTINATION_SNS_TOPIC_ARN'),
|
|
47
|
-
}),
|
|
48
|
-
|
|
49
|
-
prod: () => ({
|
|
50
|
-
logLevel: 'info',
|
|
51
|
-
metrics: true,
|
|
52
|
-
name: Env.string('SERVICE'),
|
|
53
|
-
version: Env.string('VERSION'),
|
|
54
|
-
|
|
55
|
-
destinationSnsTopicArn: Env.string('DESTINATION_SNS_TOPIC_ARN'),
|
|
56
|
-
}),
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export const config: Config = {
|
|
60
|
-
...configs[environment](),
|
|
61
|
-
environment,
|
|
62
|
-
};
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { createCtx } from 'src/testing/handler';
|
|
2
|
-
import { logger } from 'src/testing/logging';
|
|
3
|
-
import { chance } from 'src/testing/types';
|
|
4
|
-
|
|
5
|
-
import { createHandler } from './handler';
|
|
6
|
-
|
|
7
|
-
describe('createHandler', () => {
|
|
8
|
-
const ctx = createCtx();
|
|
9
|
-
const input = chance.paragraph();
|
|
10
|
-
|
|
11
|
-
beforeAll(logger.spy);
|
|
12
|
-
|
|
13
|
-
afterEach(logger.clear);
|
|
14
|
-
|
|
15
|
-
it('handles happy path', async () => {
|
|
16
|
-
const output = chance.paragraph();
|
|
17
|
-
|
|
18
|
-
const handler = createHandler((event) => {
|
|
19
|
-
expect(event).toBe(input);
|
|
20
|
-
|
|
21
|
-
logger.debug('Handler invoked');
|
|
22
|
-
|
|
23
|
-
return Promise.resolve(output);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
await expect(handler(input, ctx)).resolves.toBe(output);
|
|
27
|
-
|
|
28
|
-
expect(logger.error).not.toHaveBeenCalled();
|
|
29
|
-
|
|
30
|
-
expect(logger.debug.mock.calls).toEqual([
|
|
31
|
-
['Handler invoked'],
|
|
32
|
-
['Function succeeded'],
|
|
33
|
-
]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('handles async error', async () => {
|
|
37
|
-
const err = Error(chance.sentence());
|
|
38
|
-
|
|
39
|
-
const handler = createHandler(() => Promise.reject(err));
|
|
40
|
-
|
|
41
|
-
await expect(handler(input, ctx)).rejects.toThrow('Function failed');
|
|
42
|
-
|
|
43
|
-
expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
|
|
44
|
-
|
|
45
|
-
expect(logger.debug).not.toHaveBeenCalled();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('handles sync error', async () => {
|
|
49
|
-
const err = Error(chance.sentence());
|
|
50
|
-
|
|
51
|
-
const handler = createHandler(() => {
|
|
52
|
-
throw err;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
await expect(handler(input, ctx)).rejects.toThrow('Function failed');
|
|
56
|
-
|
|
57
|
-
expect(logger.error).toHaveBeenCalledWith({ err }, 'Function failed');
|
|
58
|
-
|
|
59
|
-
expect(logger.debug).not.toHaveBeenCalled();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { datadog } from 'datadog-lambda-js';
|
|
2
|
-
|
|
3
|
-
import { config } from 'src/config';
|
|
4
|
-
import { logger, loggerContext } from 'src/framework/logging';
|
|
5
|
-
|
|
6
|
-
interface LambdaContext {
|
|
7
|
-
awsRequestId: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type Handler<Event, Output> = (
|
|
11
|
-
event: Event,
|
|
12
|
-
ctx: LambdaContext,
|
|
13
|
-
) => Promise<Output>;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Conditionally applies the Datadog wrapper to a Lambda handler.
|
|
17
|
-
*
|
|
18
|
-
* This also "fixes" its broken type definitions.
|
|
19
|
-
*/
|
|
20
|
-
const withDatadog = <Event, Output = unknown>(
|
|
21
|
-
fn: Handler<Event, Output>,
|
|
22
|
-
): Handler<Event, Output> =>
|
|
23
|
-
// istanbul ignore next
|
|
24
|
-
config.metrics ? (datadog(fn) as Handler<Event, Output>) : fn;
|
|
25
|
-
|
|
26
|
-
export const createHandler = <Event, Output = unknown>(
|
|
27
|
-
fn: (event: Event) => Promise<Output>,
|
|
28
|
-
) =>
|
|
29
|
-
withDatadog<Event>((event, { awsRequestId }) =>
|
|
30
|
-
loggerContext.run({ awsRequestId }, async () => {
|
|
31
|
-
try {
|
|
32
|
-
const output = await fn(event);
|
|
33
|
-
|
|
34
|
-
logger.debug('Function succeeded');
|
|
35
|
-
|
|
36
|
-
return output;
|
|
37
|
-
} catch (err) {
|
|
38
|
-
logger.error({ err }, 'Function failed');
|
|
39
|
-
|
|
40
|
-
throw new Error('Function failed');
|
|
41
|
-
}
|
|
42
|
-
}),
|
|
43
|
-
);
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
-
|
|
3
|
-
import createLogger from '@seek/logger';
|
|
4
|
-
|
|
5
|
-
import { config } from 'src/config';
|
|
6
|
-
|
|
7
|
-
interface LoggerContext {
|
|
8
|
-
awsRequestId: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const loggerContext = new AsyncLocalStorage<LoggerContext>();
|
|
12
|
-
|
|
13
|
-
export const logger = createLogger({
|
|
14
|
-
base: {
|
|
15
|
-
environment: config.environment,
|
|
16
|
-
version: config.version,
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
level: config.logLevel,
|
|
20
|
-
|
|
21
|
-
mixin: () => ({ ...loggerContext.getStore() }),
|
|
22
|
-
|
|
23
|
-
name: config.name,
|
|
24
|
-
|
|
25
|
-
transport:
|
|
26
|
-
config.environment === 'local' ? { target: 'pino-pretty' } : undefined,
|
|
27
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { sendDistributionMetric } from 'datadog-lambda-js';
|
|
2
|
-
|
|
3
|
-
import { config } from 'src/config';
|
|
4
|
-
|
|
5
|
-
const prefix = `${config.name}.`;
|
|
6
|
-
|
|
7
|
-
export const metricsClient = {
|
|
8
|
-
distribution: (
|
|
9
|
-
...[name, ...rest]: Parameters<typeof sendDistributionMetric>
|
|
10
|
-
) =>
|
|
11
|
-
config.metrics
|
|
12
|
-
? sendDistributionMetric(`${prefix}${name}`, ...rest)
|
|
13
|
-
: undefined,
|
|
14
|
-
};
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IdDescriptionSchema,
|
|
3
|
-
chance,
|
|
4
|
-
mockIdDescription,
|
|
5
|
-
} from 'src/testing/types';
|
|
6
|
-
|
|
7
|
-
import { validateJson } from './validation';
|
|
8
|
-
|
|
9
|
-
describe('validateJson', () => {
|
|
10
|
-
const idDescription = mockIdDescription();
|
|
11
|
-
|
|
12
|
-
it('permits valid input', () => {
|
|
13
|
-
const input = JSON.stringify(idDescription);
|
|
14
|
-
|
|
15
|
-
expect(validateJson(input, IdDescriptionSchema)).toStrictEqual(
|
|
16
|
-
idDescription,
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('filters additional properties', () => {
|
|
21
|
-
const input = JSON.stringify({ ...idDescription, hacker: chance.name() });
|
|
22
|
-
|
|
23
|
-
expect(validateJson(input, IdDescriptionSchema)).toStrictEqual(
|
|
24
|
-
idDescription,
|
|
25
|
-
);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('blocks mistyped prop', () => {
|
|
29
|
-
const input = JSON.stringify({ ...idDescription, id: null });
|
|
30
|
-
|
|
31
|
-
expect(() => validateJson(input, IdDescriptionSchema))
|
|
32
|
-
.toThrowErrorMatchingInlineSnapshot(`
|
|
33
|
-
"[
|
|
34
|
-
{
|
|
35
|
-
"code": "invalid_type",
|
|
36
|
-
"expected": "string",
|
|
37
|
-
"received": "null",
|
|
38
|
-
"path": [
|
|
39
|
-
"id"
|
|
40
|
-
],
|
|
41
|
-
"message": "Expected string, received null"
|
|
42
|
-
}
|
|
43
|
-
]"
|
|
44
|
-
`);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('blocks missing prop', () => {
|
|
48
|
-
const input = '{}';
|
|
49
|
-
|
|
50
|
-
expect(() => validateJson(input, IdDescriptionSchema))
|
|
51
|
-
.toThrowErrorMatchingInlineSnapshot(`
|
|
52
|
-
"[
|
|
53
|
-
{
|
|
54
|
-
"code": "invalid_type",
|
|
55
|
-
"expected": "string",
|
|
56
|
-
"received": "undefined",
|
|
57
|
-
"path": [
|
|
58
|
-
"id"
|
|
59
|
-
],
|
|
60
|
-
"message": "Required"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"code": "invalid_type",
|
|
64
|
-
"expected": "string",
|
|
65
|
-
"received": "undefined",
|
|
66
|
-
"path": [
|
|
67
|
-
"description"
|
|
68
|
-
],
|
|
69
|
-
"message": "Required"
|
|
70
|
-
}
|
|
71
|
-
]"
|
|
72
|
-
`);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('blocks invalid JSON', () => {
|
|
76
|
-
const input = '}';
|
|
77
|
-
|
|
78
|
-
expect(() =>
|
|
79
|
-
validateJson(input, IdDescriptionSchema),
|
|
80
|
-
).toThrowErrorMatchingInlineSnapshot(
|
|
81
|
-
`"Unexpected token '}', "}" is not valid JSON"`,
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
/* istanbul ignore file */
|
|
3
|
-
|
|
4
|
-
// Use minimal dependencies to reduce the chance of crashes on module load.
|
|
5
|
-
import {
|
|
6
|
-
CodeDeployClient,
|
|
7
|
-
PutLifecycleEventHookExecutionStatusCommand,
|
|
8
|
-
} from '@aws-sdk/client-codedeploy';
|
|
9
|
-
import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda';
|
|
10
|
-
|
|
11
|
-
const codeDeploy = new CodeDeployClient({
|
|
12
|
-
apiVersion: '2014-10-06',
|
|
13
|
-
maxAttempts: 5,
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const lambda = new LambdaClient({
|
|
17
|
-
apiVersion: '2015-03-31',
|
|
18
|
-
maxAttempts: 5,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
type Status = 'Succeeded' | 'Failed';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Synchronously invokes a Lambda function with a smoke test event.
|
|
25
|
-
*
|
|
26
|
-
* Any non-error response is treated as a success.
|
|
27
|
-
*/
|
|
28
|
-
const smokeTestLambdaFunction = async (): Promise<Status> => {
|
|
29
|
-
const functionName = process.env.FUNCTION_NAME_TO_INVOKE;
|
|
30
|
-
|
|
31
|
-
if (!functionName) {
|
|
32
|
-
console.error('Missing process.env.FUNCTION_NAME_TO_INVOKE');
|
|
33
|
-
return 'Failed';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
console.info('Function:', functionName);
|
|
37
|
-
|
|
38
|
-
const response = await lambda.send(
|
|
39
|
-
new InvokeCommand({
|
|
40
|
-
FunctionName: functionName,
|
|
41
|
-
InvocationType: 'RequestResponse',
|
|
42
|
-
// Treat an empty object as our smoke test event.
|
|
43
|
-
Payload: Buffer.from('{}'),
|
|
44
|
-
}),
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
console.info('Version:', response.ExecutedVersion ?? '?');
|
|
48
|
-
console.info('Status', response.StatusCode ?? '?');
|
|
49
|
-
|
|
50
|
-
if (response.FunctionError) {
|
|
51
|
-
console.error('Error:', response.FunctionError);
|
|
52
|
-
if (response.Payload) {
|
|
53
|
-
console.error(response.Payload.transformToString());
|
|
54
|
-
}
|
|
55
|
-
return 'Failed';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return response.StatusCode === 200 ? 'Succeeded' : 'Failed';
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* The event supplied to a CodeDeploy lifecycle hook Lambda function.
|
|
63
|
-
*
|
|
64
|
-
* {@link https://docs.aws.amazon.com/codedeploy/latest/userguide/tutorial-ecs-with-hooks-create-hooks.html}
|
|
65
|
-
*/
|
|
66
|
-
interface CodeDeployLifecycleHookEvent {
|
|
67
|
-
DeploymentId: string;
|
|
68
|
-
LifecycleEventHookExecutionId: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* A handler to smoke test a new Lambda function version before it goes live.
|
|
73
|
-
*
|
|
74
|
-
* This tries to be exception safe so that a status reaches CodeDeploy. If we
|
|
75
|
-
* crash or otherwise fail to report back, the deployment will hang for an hour.
|
|
76
|
-
*/
|
|
77
|
-
export const pre = async (
|
|
78
|
-
event: CodeDeployLifecycleHookEvent,
|
|
79
|
-
): Promise<void> => {
|
|
80
|
-
let status: Status;
|
|
81
|
-
try {
|
|
82
|
-
status = await smokeTestLambdaFunction();
|
|
83
|
-
} catch (err) {
|
|
84
|
-
console.error('Exception:', err);
|
|
85
|
-
status = 'Failed';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
await codeDeploy.send(
|
|
89
|
-
new PutLifecycleEventHookExecutionStatusCommand({
|
|
90
|
-
deploymentId: event.DeploymentId,
|
|
91
|
-
lifecycleEventHookExecutionId: event.LifecycleEventHookExecutionId,
|
|
92
|
-
status,
|
|
93
|
-
}),
|
|
94
|
-
);
|
|
95
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { JobScorerInput, JobScorerOutput } from 'src/types/jobScorer';
|
|
2
|
-
import type {
|
|
3
|
-
JobPublishedEvent,
|
|
4
|
-
JobScoredEvent,
|
|
5
|
-
} from 'src/types/pipelineEvents';
|
|
6
|
-
|
|
7
|
-
export const jobPublishedEventToScorerInput = (
|
|
8
|
-
record: JobPublishedEvent,
|
|
9
|
-
): JobScorerInput => ({
|
|
10
|
-
details: record.data.details,
|
|
11
|
-
id: record.entityId,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
export const jobScorerOutputToScoredEvent = (
|
|
15
|
-
output: JobScorerOutput,
|
|
16
|
-
): JobScoredEvent => ({
|
|
17
|
-
data: {
|
|
18
|
-
score: output.score,
|
|
19
|
-
},
|
|
20
|
-
entityId: output.id,
|
|
21
|
-
eventType: 'JobScored',
|
|
22
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { scoringService } from 'src/testing/services';
|
|
2
|
-
import { chance, mockJobPublishedEvent } from 'src/testing/types';
|
|
3
|
-
|
|
4
|
-
import * as jobScorer from './jobScorer';
|
|
5
|
-
|
|
6
|
-
describe('scoreJobPublishedEvent', () => {
|
|
7
|
-
beforeAll(scoringService.spy);
|
|
8
|
-
|
|
9
|
-
afterEach(scoringService.clear);
|
|
10
|
-
|
|
11
|
-
it('scores an event', async () => {
|
|
12
|
-
const score = chance.floating({ max: 1, min: 0 });
|
|
13
|
-
|
|
14
|
-
scoringService.request.mockResolvedValue(score);
|
|
15
|
-
|
|
16
|
-
await expect(
|
|
17
|
-
jobScorer.scoreJobPublishedEvent(
|
|
18
|
-
mockJobPublishedEvent({ entityId: '1' }),
|
|
19
|
-
),
|
|
20
|
-
).resolves.toStrictEqual({
|
|
21
|
-
data: {
|
|
22
|
-
score,
|
|
23
|
-
},
|
|
24
|
-
entityId: '1',
|
|
25
|
-
eventType: 'JobScored',
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
expect(scoringService.request).toHaveBeenCalledTimes(1);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('bubbles up scoring service error', async () => {
|
|
32
|
-
const err = Error(chance.sentence());
|
|
33
|
-
|
|
34
|
-
scoringService.request.mockRejectedValue(err);
|
|
35
|
-
|
|
36
|
-
await expect(
|
|
37
|
-
jobScorer.scoreJobPublishedEvent(
|
|
38
|
-
mockJobPublishedEvent({ entityId: '1' }),
|
|
39
|
-
),
|
|
40
|
-
).rejects.toThrow(err);
|
|
41
|
-
|
|
42
|
-
expect(scoringService.request).toHaveBeenCalledTimes(1);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
jobPublishedEventToScorerInput,
|
|
3
|
-
jobScorerOutputToScoredEvent,
|
|
4
|
-
} from 'src/mapping/jobScorer';
|
|
5
|
-
import {
|
|
6
|
-
type JobScorerInput,
|
|
7
|
-
type JobScorerOutput,
|
|
8
|
-
JobScorerOutputSchema,
|
|
9
|
-
} from 'src/types/jobScorer';
|
|
10
|
-
import type {
|
|
11
|
-
JobPublishedEvent,
|
|
12
|
-
JobScoredEvent,
|
|
13
|
-
} from 'src/types/pipelineEvents';
|
|
14
|
-
|
|
15
|
-
/* istanbul ignore next: simulation of an external service */
|
|
16
|
-
export const scoringService = {
|
|
17
|
-
request: (details: string): Promise<unknown> => {
|
|
18
|
-
// Networking woes
|
|
19
|
-
if (Math.random() < 0.05) {
|
|
20
|
-
const err = Error('could not reach scoring service');
|
|
21
|
-
|
|
22
|
-
return Promise.reject(err);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Unexpected behaviour on certain inputs
|
|
26
|
-
if (details.length % 100 === 0) {
|
|
27
|
-
return Promise.resolve(null);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return Promise.resolve(Math.random());
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
smokeTest: async (): Promise<void> => {
|
|
34
|
-
// A connectivity test
|
|
35
|
-
await Promise.resolve();
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const scoreJob = async ({
|
|
40
|
-
details,
|
|
41
|
-
id,
|
|
42
|
-
}: JobScorerInput): Promise<JobScorerOutput> => {
|
|
43
|
-
const score = await scoringService.request(details);
|
|
44
|
-
|
|
45
|
-
return JobScorerOutputSchema.parse({
|
|
46
|
-
id,
|
|
47
|
-
score,
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const scoreJobPublishedEvent = async (
|
|
52
|
-
publishedJob: JobPublishedEvent,
|
|
53
|
-
): Promise<JobScoredEvent> => {
|
|
54
|
-
const scorerInput = jobPublishedEventToScorerInput(publishedJob);
|
|
55
|
-
|
|
56
|
-
const scorerOutput = await scoreJob(scorerInput);
|
|
57
|
-
|
|
58
|
-
return jobScorerOutputToScoredEvent(scorerOutput);
|
|
59
|
-
};
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { PublishCommand } from '@aws-sdk/client-sns';
|
|
2
|
-
|
|
3
|
-
import { sns } from 'src/testing/services';
|
|
4
|
-
import { chance } from 'src/testing/types';
|
|
5
|
-
|
|
6
|
-
import { sendPipelineEvent } from './pipelineEventSender';
|
|
7
|
-
|
|
8
|
-
describe('sendPipelineEvent', () => {
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
jest.clearAllMocks();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('handles happy path', async () => {
|
|
14
|
-
const messageId = chance.guid({ version: 4 });
|
|
15
|
-
|
|
16
|
-
sns.publish.resolves({ MessageId: messageId });
|
|
17
|
-
|
|
18
|
-
await expect(sendPipelineEvent({})).resolves.toBe(messageId);
|
|
19
|
-
|
|
20
|
-
expect(sns.client).toReceiveCommandTimes(PublishCommand, 1);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('bubbles up SNS error', () => {
|
|
24
|
-
const err = Error(chance.sentence());
|
|
25
|
-
|
|
26
|
-
sns.publish.rejects(err);
|
|
27
|
-
|
|
28
|
-
return expect(sendPipelineEvent({})).rejects.toThrow(err);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('throws on missing message ID', () => {
|
|
32
|
-
sns.publish.resolves({});
|
|
33
|
-
|
|
34
|
-
return expect(
|
|
35
|
-
sendPipelineEvent({}),
|
|
36
|
-
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
37
|
-
`"SNS did not return a message ID"`,
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { PublishCommand } from '@aws-sdk/client-sns';
|
|
2
|
-
|
|
3
|
-
import { config } from 'src/config';
|
|
4
|
-
|
|
5
|
-
import { sns } from './aws';
|
|
6
|
-
|
|
7
|
-
export const sendPipelineEvent = async (
|
|
8
|
-
event: unknown,
|
|
9
|
-
smokeTest: boolean = false,
|
|
10
|
-
): Promise<string> => {
|
|
11
|
-
const snsResponse = await sns.send(
|
|
12
|
-
new PublishCommand({
|
|
13
|
-
Message: JSON.stringify(event),
|
|
14
|
-
...(smokeTest && {
|
|
15
|
-
MessageAttributes: {
|
|
16
|
-
// Used for connectivity tests.
|
|
17
|
-
// Subscribers should filter out messages containing this attribute.
|
|
18
|
-
SmokeTest: {
|
|
19
|
-
DataType: 'String',
|
|
20
|
-
StringValue: 'true',
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
}),
|
|
24
|
-
TopicArn: config.destinationSnsTopicArn,
|
|
25
|
-
}),
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
if (snsResponse.MessageId === undefined) {
|
|
29
|
-
throw Error('SNS did not return a message ID');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return snsResponse.MessageId;
|
|
33
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Context, SQSEvent } from 'aws-lambda';
|
|
2
|
-
|
|
3
|
-
import { chance } from './types';
|
|
4
|
-
|
|
5
|
-
export const createCtx = () =>
|
|
6
|
-
({
|
|
7
|
-
awsRequestId: chance.guid({ version: 4 }),
|
|
8
|
-
}) as Context;
|
|
9
|
-
|
|
10
|
-
export const createSqsEvent = (bodies: string[]) =>
|
|
11
|
-
({
|
|
12
|
-
Records: bodies.map((body) => ({ body })),
|
|
13
|
-
}) as SQSEvent;
|