declapract-typescript-ehmpathy 0.47.67 → 0.47.69
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/dist/practices/cicd-service/best-practice/.agent/repo=.this/skills/use.rds.capacity.sh +4 -4
- package/dist/practices/cicd-service/best-practice/.github/workflows/.deploy-sls.yml +4 -4
- package/dist/practices/cicd-service/best-practice/.github/workflows/.sql-schema-control.yml +4 -4
- package/dist/practices/cicd-service/best-practice/.github/workflows/provision.yml +1 -1
- package/dist/practices/commands/best-practice/src/contract/commands/sayHello.ts +1 -1
- package/dist/practices/config/bad-practices/old-config-with-paramstore-dep/package.json +5 -0
- package/dist/practices/config/bad-practices/old-config-with-paramstore-dep/package.json.declapract.ts +18 -0
- package/dist/practices/config/bad-practices/old-config-with-paramstore-dts/nontyped_modules/config-with-paramstore.d.ts.declapract.ts +8 -0
- package/dist/practices/config/bad-practices/old-dev-config-location/config/dev.json.declapract.ts +18 -0
- package/dist/practices/config/bad-practices/old-parameterstorenamespace-key/config/prep.json +3 -0
- package/dist/practices/config/bad-practices/old-parameterstorenamespace-key/config/prep.json.declapract.ts +12 -0
- package/dist/practices/config/bad-practices/old-parameterstorenamespace-key/config/prod.json +3 -0
- package/dist/practices/config/bad-practices/old-parameterstorenamespace-key/config/prod.json.declapract.ts +12 -0
- package/dist/practices/config/best-practice/config/{dev.json → prep.json} +1 -2
- package/dist/practices/config/best-practice/config/prod.json +0 -1
- package/dist/practices/config/best-practice/config/test.json +0 -1
- package/dist/practices/config/best-practice/package.json +3 -1
- package/dist/practices/config/best-practice/src/utils/config/Config.ts +1 -1
- package/dist/practices/config/best-practice/src/utils/config/config.schema.ts +15 -0
- package/dist/practices/config/best-practice/src/utils/config/getConfig.ts +17 -7
- package/dist/practices/environments/bad-practices/old-stage-enum/src/<star><star>/<star>.ts.declapract.ts +37 -0
- package/dist/practices/environments/best-practice/package.json +1 -3
- package/dist/practices/environments/best-practice/src/utils/environment.ts +5 -163
- package/dist/practices/persist-with-dynamodb/best-practice/jest.integration.env.ts +3 -1
- package/dist/practices/persist-with-rds/bad-practices/old-dev-config-location/config/dev.json.declapract.ts +19 -0
- package/dist/practices/persist-with-rds/bad-practices/old-param-placeholder/config/<star>.json.declapract.ts +14 -0
- package/dist/practices/persist-with-rds/best-practice/config/{dev.json → prep.json} +4 -4
- package/dist/practices/persist-with-rds/best-practice/config/prod.json +2 -2
- package/dist/practices/persist-with-rds/best-practice/package.json +1 -1
- package/dist/practices/persist-with-rds/best-practice/provision/schema/connection.config.js +7 -6
- package/dist/practices/persist-with-rds/best-practice/src/utils/config/config.schema.ts +43 -0
- package/dist/practices/persist-with-rds/best-practice/src/utils/config/config.schema.ts.declapract.ts +47 -0
- package/dist/practices/persist-with-rds/best-practice/src/utils/database/getDatabaseConnection.ts +1 -1
- package/dist/practices/serverless/best-practice/package.json +5 -5
- package/dist/practices/serverless/best-practice/serverless.yml +14 -4
- package/dist/practices/serverless/best-practice/serverless.yml.declapract.ts +57 -7
- package/dist/practices/tests/best-practice/jest.integration.env.ts +35 -24
- package/dist/practices/tests/best-practice/jest.unit.env.ts +7 -8
- package/dist/practices/tests/best-practice/package.json +4 -4
- package/package.json +9 -9
- package/dist/practices/config/best-practice/nontyped_modules/config-with-paramstore.d.ts +0 -1
- /package/dist/practices/config/best-practice/config/{dev.json.declapract.ts → prep.json.declapract.ts} +0 -0
- /package/dist/practices/persist-with-rds/best-practice/config/{dev.json.declapract.ts → prep.json.declapract.ts} +0 -0
package/dist/practices/cicd-service/best-practice/.agent/repo=.this/skills/use.rds.capacity.sh
CHANGED
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
# - Any time you need to ensure the database is ready before proceeding
|
|
16
16
|
#
|
|
17
17
|
# Usage:
|
|
18
|
-
#
|
|
18
|
+
# ACCESS=prep ./.agent/repo=.this/skills/use.rds.capacity.sh
|
|
19
19
|
#
|
|
20
20
|
# Prerequisites:
|
|
21
|
-
# -
|
|
21
|
+
# - ACCESS environment variable must be set
|
|
22
22
|
# - AWS credentials configured with SSM access
|
|
23
23
|
# - sudo access (for /etc/hosts modification via vpc tunnel)
|
|
24
24
|
# - pg_isready command available (postgresql-client)
|
|
25
25
|
#
|
|
26
26
|
set -eo pipefail
|
|
27
27
|
|
|
28
|
-
# failfast if
|
|
29
|
-
[[ -z "${
|
|
28
|
+
# failfast if ACCESS is not declared
|
|
29
|
+
[[ -z "${ACCESS:-}" ]] && echo "ACCESS is not set" && exit 1
|
|
30
30
|
|
|
31
31
|
set -u
|
|
32
32
|
|
|
@@ -63,7 +63,7 @@ jobs:
|
|
|
63
63
|
fail-on-cache-miss: true
|
|
64
64
|
|
|
65
65
|
- name: deploy
|
|
66
|
-
run:
|
|
66
|
+
run: ACCESS=${{ inputs.stage }} DEPLOYER_NAME=$GITHUB_ACTOR npm run deploy
|
|
67
67
|
|
|
68
68
|
assure:
|
|
69
69
|
runs-on: ubuntu-24.04
|
|
@@ -92,10 +92,10 @@ jobs:
|
|
|
92
92
|
|
|
93
93
|
- name: vpc:tunnel:open
|
|
94
94
|
if: inputs.needs-vpn-for-acceptance
|
|
95
|
-
run:
|
|
95
|
+
run: ACCESS=${{ inputs.stage }} .agent/repo=.this/skills/use.vpc.tunnel.ts
|
|
96
96
|
|
|
97
97
|
- name: test:acceptance
|
|
98
|
-
run:
|
|
98
|
+
run: ACCESS=${{ inputs.stage }} npm run test:acceptance
|
|
99
99
|
|
|
100
100
|
- name: alarm on failure
|
|
101
101
|
env:
|
|
@@ -132,4 +132,4 @@ jobs:
|
|
|
132
132
|
fail-on-cache-miss: true
|
|
133
133
|
|
|
134
134
|
- name: prune
|
|
135
|
-
run:
|
|
135
|
+
run: ACCESS=${{ inputs.stage }} DEPLOYER_NAME=$GITHUB_ACTOR npm run deploy:prune
|
|
@@ -60,10 +60,10 @@ jobs:
|
|
|
60
60
|
aws-region: ${{ inputs.creds-aws-region }}
|
|
61
61
|
|
|
62
62
|
- name: vpc:tunnel:open
|
|
63
|
-
run:
|
|
63
|
+
run: ACCESS=${{ inputs.stage }} .agent/repo=.this/skills/use.vpc.tunnel.ts
|
|
64
64
|
|
|
65
65
|
- name: plan
|
|
66
|
-
run:
|
|
66
|
+
run: ACCESS=${{ inputs.stage }} npm run provision:schema:plan | tee ./plan.log
|
|
67
67
|
|
|
68
68
|
- name: evaluate plan
|
|
69
69
|
id: evaluate-plan
|
|
@@ -115,7 +115,7 @@ jobs:
|
|
|
115
115
|
aws-region: ${{ inputs.creds-aws-region }}
|
|
116
116
|
|
|
117
117
|
- name: vpc:tunnel:open
|
|
118
|
-
run:
|
|
118
|
+
run: ACCESS=${{ inputs.stage }} .agent/repo=.this/skills/use.vpc.tunnel.ts
|
|
119
119
|
|
|
120
120
|
- name: apply
|
|
121
|
-
run:
|
|
121
|
+
run: ACCESS=${{ inputs.stage }} npm run provision:schema:apply
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check = FileCheckType.CONTAINS;
|
|
4
|
+
|
|
5
|
+
export const fix: FileFixFunction = (contents) => {
|
|
6
|
+
if (!contents) return { contents };
|
|
7
|
+
const packageJSON = JSON.parse(contents);
|
|
8
|
+
const updatedPackageJSON = {
|
|
9
|
+
...packageJSON,
|
|
10
|
+
dependencies: {
|
|
11
|
+
...packageJSON.dependencies,
|
|
12
|
+
'config-with-paramstore': undefined,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
contents: JSON.stringify(updatedPackageJSON, null, 2),
|
|
17
|
+
};
|
|
18
|
+
};
|
package/dist/practices/config/bad-practices/old-dev-config-location/config/dev.json.declapract.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check = FileCheckType.EXISTS;
|
|
4
|
+
|
|
5
|
+
export const fix: FileFixFunction = (contents, context) => {
|
|
6
|
+
// move config/dev.json → config/prep.json and update access: dev → prep
|
|
7
|
+
let fixed = contents;
|
|
8
|
+
if (fixed) {
|
|
9
|
+
fixed = fixed.replace(/"access":\s*"dev"/, '"access": "prep"');
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
contents: fixed ?? null,
|
|
13
|
+
relativeFilePath: context.relativeFilePath.replace(
|
|
14
|
+
/^config\/dev\.json$/,
|
|
15
|
+
'config/prep.json',
|
|
16
|
+
),
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check = FileCheckType.CONTAINS;
|
|
4
|
+
|
|
5
|
+
export const fix: FileFixFunction = (contents) => {
|
|
6
|
+
if (!contents) return { contents };
|
|
7
|
+
const config = JSON.parse(contents);
|
|
8
|
+
const { parameterStoreNamespace: _, ...rest } = config;
|
|
9
|
+
return {
|
|
10
|
+
contents: JSON.stringify(rest, null, 2),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check = FileCheckType.CONTAINS;
|
|
4
|
+
|
|
5
|
+
export const fix: FileFixFunction = (contents) => {
|
|
6
|
+
if (!contents) return { contents };
|
|
7
|
+
const config = JSON.parse(contents);
|
|
8
|
+
const { parameterStoreNamespace: _, ...rest } = config;
|
|
9
|
+
return {
|
|
10
|
+
contents: JSON.stringify(rest, null, 2),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"parameterStoreNamespace": "@declapract{variable.organizationName}.@declapract{variable.projectName}.dev",
|
|
3
2
|
"organization": "@declapract{variable.organizationName}",
|
|
4
3
|
"project": "@declapract{variable.projectName}",
|
|
5
4
|
"environment": {
|
|
6
|
-
"access": "
|
|
5
|
+
"access": "prep"
|
|
7
6
|
},
|
|
8
7
|
"aws": {
|
|
9
8
|
"account": "@declapract{variable.awsAccountId.dev}",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"config
|
|
3
|
+
"sdk-config": "@declapract{check.minVersion('0.1.1')}",
|
|
4
|
+
"simple-in-memory-cache": "@declapract{check.minVersion('0.4.0')}",
|
|
5
|
+
"zod": "@declapract{check.minVersion('4.0.0')}"
|
|
4
6
|
}
|
|
5
7
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
organization: z.string(),
|
|
5
|
+
project: z.string(),
|
|
6
|
+
environment: z.object({
|
|
7
|
+
access: z.enum(['test', 'prep', 'prod']),
|
|
8
|
+
}),
|
|
9
|
+
aws: z.object({
|
|
10
|
+
account: z.string(),
|
|
11
|
+
namespace: z.string(),
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type Config = z.infer<typeof schema>;
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
genGetConfig,
|
|
3
|
+
genSdkConfigSupplierAwsParameterStore,
|
|
4
|
+
} from 'sdk-config';
|
|
5
|
+
import { createCache } from 'simple-in-memory-cache';
|
|
3
6
|
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
7
|
+
import { envStatic } from '../environment';
|
|
8
|
+
import { schema } from './config.schema';
|
|
6
9
|
|
|
7
|
-
export const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
export const getConfig = genGetConfig({
|
|
11
|
+
schema,
|
|
12
|
+
statics: 'config/*.json',
|
|
13
|
+
cache: createCache({ expiration: { minutes: 5 } }),
|
|
14
|
+
suppliers: [genSdkConfigSupplierAwsParameterStore()],
|
|
15
|
+
environment: {
|
|
16
|
+
config: envStatic.config,
|
|
17
|
+
server: envStatic.server,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FileCheckFunction, FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check: FileCheckFunction = (contents) => {
|
|
4
|
+
if (!contents) throw new Error('does not match bad practice');
|
|
5
|
+
|
|
6
|
+
// check for Stage enum usage
|
|
7
|
+
if (
|
|
8
|
+
contents.includes('Stage.PRODUCTION') ||
|
|
9
|
+
contents.includes('Stage.DEVELOPMENT') ||
|
|
10
|
+
contents.includes('Stage.TEST')
|
|
11
|
+
)
|
|
12
|
+
return; // bad practice detected
|
|
13
|
+
|
|
14
|
+
throw new Error('does not match bad practice');
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const fix: FileFixFunction = (contents) => {
|
|
18
|
+
if (!contents) return { contents };
|
|
19
|
+
|
|
20
|
+
let fixed = contents
|
|
21
|
+
// replace enum values with string literals
|
|
22
|
+
.replace(/Stage\.PRODUCTION/g, "'prod'")
|
|
23
|
+
.replace(/Stage\.DEVELOPMENT/g, "'prep'")
|
|
24
|
+
.replace(/Stage\.TEST/g, "'test'")
|
|
25
|
+
// remove Stage import if no longer used
|
|
26
|
+
.replace(/import\s*\{\s*Stage\s*\}\s*from\s*['"][^'"]+['"];\n?/g, '')
|
|
27
|
+
.replace(
|
|
28
|
+
/import\s*\{\s*([^}]*),\s*Stage\s*\}\s*from/g,
|
|
29
|
+
'import { $1 } from',
|
|
30
|
+
)
|
|
31
|
+
.replace(
|
|
32
|
+
/import\s*\{\s*Stage\s*,\s*([^}]*)\}\s*from/g,
|
|
33
|
+
'import { $1 } from',
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return { contents: fixed };
|
|
37
|
+
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"
|
|
4
|
-
"with-simple-cache": "@declapract{check.minVersion('0.15.1')}",
|
|
5
|
-
"simple-in-memory-cache": "@declapract{check.minVersion('0.4.0')}"
|
|
3
|
+
"sdk-environment": "@declapract{check.minVersion('0.1.3')}"
|
|
6
4
|
}
|
|
7
5
|
}
|
|
@@ -1,166 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IAMClient,
|
|
3
|
-
ListAccountAliasesCommand,
|
|
4
|
-
type ListAccountAliasesCommandOutput,
|
|
5
|
-
} from '@aws-sdk/client-iam';
|
|
6
|
-
import { UnexpectedCodePathError } from 'helpful-errors';
|
|
7
|
-
import { createCache } from 'simple-in-memory-cache';
|
|
8
|
-
import { createIsOfEnum, type Literalize } from 'type-fns';
|
|
9
|
-
import { withSimpleCache } from 'with-simple-cache';
|
|
1
|
+
import { getEnvironment } from 'sdk-environment';
|
|
10
2
|
|
|
11
|
-
|
|
3
|
+
export { getEnvironment };
|
|
12
4
|
|
|
13
|
-
export
|
|
14
|
-
PRODUCTION = 'prod',
|
|
15
|
-
DEVELOPMENT = 'dev',
|
|
16
|
-
TEST = 'test',
|
|
17
|
-
}
|
|
18
|
-
export const isOfConfigChoice = createIsOfEnum(ConfigChoice);
|
|
5
|
+
export const envStatic = getEnvironment.static();
|
|
19
6
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
DEVELOPMENT = 'dev',
|
|
23
|
-
}
|
|
24
|
-
export const isOfAccess = createIsOfEnum(Access);
|
|
25
|
-
|
|
26
|
-
export enum Stage { // todo: deprecate stage
|
|
27
|
-
PRODUCTION = 'prod',
|
|
28
|
-
DEVELOPMENT = 'dev',
|
|
29
|
-
TEST = 'test',
|
|
30
|
-
}
|
|
31
|
-
export const isOfStage = createIsOfEnum(Stage);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* ensure that the server is on UTC timezone
|
|
35
|
-
*
|
|
36
|
-
* why?
|
|
37
|
-
* - non UTC timezone usage causes consistency problems and takes a while to track down
|
|
38
|
-
* - by warning and correcting if the server the code runs on in is not in UTC, we avoid these issues
|
|
39
|
-
* - instead, users should push down converting from UTC into the users TZ as far as possible
|
|
40
|
-
*/
|
|
41
|
-
const TIMEZONE = process.env.TZ;
|
|
42
|
-
if (TIMEZONE !== 'UTC') {
|
|
43
|
-
log.debug(
|
|
44
|
-
'env.TZ is not set to UTC. this can cause issues. updating this on your behalf',
|
|
45
|
-
{ found: TIMEZONE, desire: 'UTC' },
|
|
46
|
-
);
|
|
47
|
-
process.env.TZ = 'UTC';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* this allows us to infer what the stage should be in environments that do not have STAGE specified
|
|
52
|
-
* - e.g., when running locally
|
|
53
|
-
* - e.g., when running tests
|
|
54
|
-
*/
|
|
55
|
-
const inferStageFromNodeEnv = () => {
|
|
56
|
-
const nodeEnv = process.env.NODE_ENV; // default to test if not defined
|
|
57
|
-
if (!nodeEnv) throw new Error('process.env.NODE_ENV must be defined');
|
|
58
|
-
if (nodeEnv === 'production') return Stage.PRODUCTION;
|
|
59
|
-
if (nodeEnv === 'development') return Stage.DEVELOPMENT;
|
|
60
|
-
if (nodeEnv === 'test') return Stage.TEST;
|
|
61
|
-
throw new Error(`unexpected nodeEnv '${nodeEnv}'`);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* a method that exposes relevant environmental variables in a standard way
|
|
66
|
-
*/
|
|
67
|
-
export const getStage = (): Stage => {
|
|
68
|
-
const stage = process.env.STAGE ?? inferStageFromNodeEnv(); // figure it out from NODE_ENV if not explicitly defined
|
|
69
|
-
if (!stage) throw new Error('process.env.STAGE must be defined');
|
|
70
|
-
if (!isOfStage(stage)) throw new Error(`invalid stage defined '${stage}'`);
|
|
71
|
-
return stage;
|
|
72
|
-
};
|
|
73
|
-
export const stage: Stage = getStage(); // todo: deprecate
|
|
74
|
-
|
|
75
|
-
// export service client stage // todo: deprecate
|
|
76
|
-
export const serviceClientStage =
|
|
77
|
-
stage === Stage.PRODUCTION ? Stage.PRODUCTION : Stage.DEVELOPMENT; // i.e., if its prod, hit prod. otherwise, dev
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* .what = infer access (prod/dev) from curernt credentials
|
|
81
|
-
*
|
|
82
|
-
* .why?
|
|
83
|
-
* - allows detection of the access grain we are currently authenticated into, if authenticated
|
|
84
|
-
* - account aliases typically contain the environment (e.g., 'ahbode-dev', 'ahbode-prod')
|
|
85
|
-
*/
|
|
86
|
-
export const inferAccess = withSimpleCache(
|
|
87
|
-
async (): Promise<Access | null> => {
|
|
88
|
-
// skip aws api calls if no credentials are configured
|
|
89
|
-
if (!process.env.AWS_PROFILE && !process.env.AWS_ACCESS_KEY_ID) return null;
|
|
90
|
-
|
|
91
|
-
// grab the alias of the current account
|
|
92
|
-
const iam = new IAMClient({});
|
|
93
|
-
const response: ListAccountAliasesCommandOutput = await iam.send(
|
|
94
|
-
new ListAccountAliasesCommand({}),
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
// infer access from alias
|
|
98
|
-
const alias = response.AccountAliases?.[0];
|
|
99
|
-
if (alias?.includes('prod')) return Access.PRODUCTION;
|
|
100
|
-
if (alias?.includes('dev')) return Access.DEVELOPMENT;
|
|
101
|
-
throw new Error(`Could not infer access from account alias '${alias}'`);
|
|
102
|
-
},
|
|
103
|
-
{ cache: createCache() },
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* infer stage from access (prod/dev)
|
|
108
|
-
*
|
|
109
|
-
* logic:
|
|
110
|
-
* - if NODE_ENV === 'test', return test (test environment always uses test config)
|
|
111
|
-
* - otherwise, defer to access from AWS credentials
|
|
112
|
-
*/
|
|
113
|
-
const inferConfigChoice = async (): Promise<ConfigChoice> => {
|
|
114
|
-
// grab some facts
|
|
115
|
-
const access = await inferAccess();
|
|
116
|
-
const envarNode = process.env.NODE_ENV;
|
|
117
|
-
const envarConfig = process.env.CONFIG ?? process.env.STAGE; // fallback to stage as alias to config choice
|
|
118
|
-
|
|
119
|
-
// if access is against prod, then must use prod config; no exceptions
|
|
120
|
-
if (access === Access.PRODUCTION) return ConfigChoice.PRODUCTION;
|
|
121
|
-
|
|
122
|
-
// if access is against prep and asked for dev, then use dev config
|
|
123
|
-
if (access === Access.DEVELOPMENT && envarConfig === 'dev')
|
|
124
|
-
return ConfigChoice.DEVELOPMENT;
|
|
125
|
-
|
|
126
|
-
// if access is against prep and asked for test, then use test config
|
|
127
|
-
if (access === Access.DEVELOPMENT && envarNode === 'test')
|
|
128
|
-
return ConfigChoice.TEST;
|
|
129
|
-
|
|
130
|
-
// if access is against prep and not asked for test, then must use dev config
|
|
131
|
-
if (access === Access.DEVELOPMENT) return ConfigChoice.DEVELOPMENT;
|
|
132
|
-
|
|
133
|
-
// otherwise, unsupported
|
|
134
|
-
throw new UnexpectedCodePathError(
|
|
135
|
-
'Could not infer config choice: NODE_ENV is not test and no valid access could be determined',
|
|
136
|
-
{ access, envarNode, envarConfig },
|
|
137
|
-
);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// export the v3 environmental variables
|
|
141
|
-
export interface Environment {
|
|
142
|
-
/**
|
|
143
|
-
* .what = the choice of config to execute against
|
|
144
|
-
*/
|
|
145
|
-
config: Literalize<ConfigChoice>;
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* .what = the resources accessible from this environment, if any
|
|
149
|
-
*/
|
|
150
|
-
access: Literalize<Access> | null;
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* .what = the server that hosts this environment
|
|
154
|
-
*/
|
|
155
|
-
server: 'CICD' | 'AWS:LAMBDA' | 'LOCAL';
|
|
156
|
-
}
|
|
157
|
-
export const getEnvironment = async (): Promise<Environment> => ({
|
|
158
|
-
config: await inferConfigChoice(),
|
|
159
|
-
access: await inferAccess(),
|
|
160
|
-
server: (() => {
|
|
161
|
-
if (process.env.CI) return 'CICD' as const;
|
|
162
|
-
if (process.env.LAMBDA_TASK_ROOT) return 'AWS:LAMBDA' as const;
|
|
163
|
-
return 'LOCAL' as const; // default to local
|
|
164
|
-
})(),
|
|
165
|
-
// region: // todo
|
|
166
|
-
});
|
|
7
|
+
export const stage = envStatic.access === 'prep' ? 'dev' : envStatic.access;
|
|
8
|
+
export const serviceClientStage = stage === 'prod' ? 'prod' : 'dev';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { stage } from './src/utils/environment';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* specify that dynamodb should use the local dynamodb database, if running in test env
|
|
3
5
|
*/
|
|
4
|
-
if (stage ===
|
|
6
|
+
if (stage === 'test')
|
|
5
7
|
process.env.USE_CUSTOM_DYNAMODB_ENDPOINT = 'http://localhost:7337';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check = FileCheckType.EXISTS;
|
|
4
|
+
|
|
5
|
+
export const fix: FileFixFunction = (contents, context) => {
|
|
6
|
+
// move config/dev.json → config/prep.json and update .dev → .prep references
|
|
7
|
+
const updatedContents = contents
|
|
8
|
+
?.replace(/\.dev\b/g, '.prep')
|
|
9
|
+
.replace(/"access":\s*"dev"/, '"access": "prep"')
|
|
10
|
+
.replace(/"__CHANG3_ME__"/g, '"$.at(aws::param)"');
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
contents: updatedContents ?? null,
|
|
14
|
+
relativeFilePath: context.relativeFilePath.replace(
|
|
15
|
+
/^config\/dev\.json$/,
|
|
16
|
+
'config/prep.json',
|
|
17
|
+
),
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FileCheckFunction, FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check: FileCheckFunction = (contents) => {
|
|
4
|
+
if (contents?.includes('__PARAM__')) return; // bad practice detected
|
|
5
|
+
throw new Error('does not match bad practice');
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const fix: FileFixFunction = (contents) => {
|
|
9
|
+
if (!contents) return { contents };
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
contents: contents.replace(/"__PARAM__"/g, '"$.at(aws::param)"'),
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -7,20 +7,20 @@
|
|
|
7
7
|
"role": {
|
|
8
8
|
"cicd": {
|
|
9
9
|
"username": "@declapract{variable.databaseUserName.cicdUser}",
|
|
10
|
-
"password": "
|
|
10
|
+
"password": "$.at(aws::param)"
|
|
11
11
|
},
|
|
12
12
|
"crud": {
|
|
13
13
|
"username": "@declapract{variable.databaseUserName.serviceUser}",
|
|
14
|
-
"password": "
|
|
14
|
+
"password": "$.at(aws::param)"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"tunnel": {
|
|
18
18
|
"local": {
|
|
19
|
-
"host": "@declapract{variable.databaseTunnelHost.
|
|
19
|
+
"host": "@declapract{variable.databaseTunnelHost.prep}",
|
|
20
20
|
"port": 15432
|
|
21
21
|
},
|
|
22
22
|
"lambda": {
|
|
23
|
-
"host": "@declapract{variable.databaseClusterHost.
|
|
23
|
+
"host": "@declapract{variable.databaseClusterHost.prep}",
|
|
24
24
|
"port": 5432
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"role": {
|
|
8
8
|
"cicd": {
|
|
9
9
|
"username": "@declapract{variable.databaseUserName.cicdUser}",
|
|
10
|
-
"password": "
|
|
10
|
+
"password": "$.at(aws::param)"
|
|
11
11
|
},
|
|
12
12
|
"crud": {
|
|
13
13
|
"username": "@declapract{variable.databaseUserName.serviceUser}",
|
|
14
|
-
"password": "
|
|
14
|
+
"password": "$.at(aws::param)"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"tunnel": {
|
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
"provision:testdb:docker:down": "docker compose -f ./provision/docker/testdb/docker-compose.yml down",
|
|
26
26
|
"provision:testdb": "npm run provision:testdb:docker:clear && npm run provision:testdb:docker:prepare && npm run provision:testdb:docker:up && npm run provision:testdb:docker:await && npm run provision:schema:plan && npm run provision:schema:apply && npm run provision:schema:plan",
|
|
27
27
|
"start:testdb": "npm run provision:testdb",
|
|
28
|
-
"start:livedb:
|
|
28
|
+
"start:livedb:prep": "echo 'will ping the database until assured its not asleep' && ACCESS=prep .agent/repo=.this/skills/use.rds.capacity.sh"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* .what = database credentials for sql-schema-control
|
|
3
|
+
* .why = reuses app's getConfig for consistent config resolution
|
|
4
|
+
*/
|
|
5
|
+
require('esbuild-register');
|
|
6
|
+
const { getConfig } = require('../../src/utils/config/getConfig');
|
|
6
7
|
|
|
7
8
|
const promiseSchemaControlCredentials = async () => {
|
|
8
9
|
const config = await getConfig();
|
|
9
10
|
const credentials = {
|
|
10
11
|
host: config.database.tunnel.local.host,
|
|
11
12
|
port: config.database.tunnel.local.port,
|
|
12
|
-
database: config.database.target.database,
|
|
13
|
+
database: config.database.target.database,
|
|
13
14
|
schema: config.database.target.schema,
|
|
14
15
|
username: config.database.role.cicd.username,
|
|
15
16
|
password: config.database.role.cicd.password,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
organization: z.string(),
|
|
5
|
+
project: z.string(),
|
|
6
|
+
environment: z.object({
|
|
7
|
+
access: z.enum(['test', 'prep', 'prod']),
|
|
8
|
+
}),
|
|
9
|
+
aws: z.object({
|
|
10
|
+
account: z.string(),
|
|
11
|
+
namespace: z.string(),
|
|
12
|
+
}),
|
|
13
|
+
database: z.object({
|
|
14
|
+
target: z.object({
|
|
15
|
+
database: z.string(),
|
|
16
|
+
schema: z.string(),
|
|
17
|
+
}),
|
|
18
|
+
role: z.object({
|
|
19
|
+
cicd: z.object({
|
|
20
|
+
username: z.string(),
|
|
21
|
+
password: z.string(),
|
|
22
|
+
}),
|
|
23
|
+
crud: z.object({
|
|
24
|
+
username: z.string(),
|
|
25
|
+
password: z.string(),
|
|
26
|
+
}),
|
|
27
|
+
}),
|
|
28
|
+
tunnel: z.object({
|
|
29
|
+
local: z.object({
|
|
30
|
+
host: z.string(),
|
|
31
|
+
port: z.number(),
|
|
32
|
+
}),
|
|
33
|
+
lambda: z
|
|
34
|
+
.object({
|
|
35
|
+
host: z.string(),
|
|
36
|
+
port: z.number(),
|
|
37
|
+
})
|
|
38
|
+
.nullable(),
|
|
39
|
+
}),
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export type Config = z.infer<typeof schema>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check = FileCheckType.CONTAINS;
|
|
4
|
+
|
|
5
|
+
const databaseSchema = ` database: z.object({
|
|
6
|
+
target: z.object({
|
|
7
|
+
database: z.string(),
|
|
8
|
+
schema: z.string(),
|
|
9
|
+
}),
|
|
10
|
+
role: z.object({
|
|
11
|
+
cicd: z.object({
|
|
12
|
+
username: z.string(),
|
|
13
|
+
password: z.string(),
|
|
14
|
+
}),
|
|
15
|
+
crud: z.object({
|
|
16
|
+
username: z.string(),
|
|
17
|
+
password: z.string(),
|
|
18
|
+
}),
|
|
19
|
+
}),
|
|
20
|
+
tunnel: z.object({
|
|
21
|
+
local: z.object({
|
|
22
|
+
host: z.string(),
|
|
23
|
+
port: z.number(),
|
|
24
|
+
}),
|
|
25
|
+
lambda: z
|
|
26
|
+
.object({
|
|
27
|
+
host: z.string(),
|
|
28
|
+
port: z.number(),
|
|
29
|
+
})
|
|
30
|
+
.nullable(),
|
|
31
|
+
}),
|
|
32
|
+
}),`;
|
|
33
|
+
|
|
34
|
+
export const fix: FileFixFunction = (contents) => {
|
|
35
|
+
if (!contents) return { contents };
|
|
36
|
+
|
|
37
|
+
// if database schema already present, return as-is
|
|
38
|
+
if (contents.includes('database: z.object(')) return { contents };
|
|
39
|
+
|
|
40
|
+
// add database schema before the close of the main object
|
|
41
|
+
const fixed = contents.replace(
|
|
42
|
+
/(\s*)\}\);(\s*\n\s*export type Config)/,
|
|
43
|
+
`$1${databaseSchema}\n});$2`,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return { contents: fixed };
|
|
47
|
+
};
|
package/dist/practices/persist-with-rds/best-practice/src/utils/database/getDatabaseConnection.ts
CHANGED
|
@@ -47,7 +47,7 @@ export const getDatabaseConnection = async (): Promise<DatabaseConnection> => {
|
|
|
47
47
|
|
|
48
48
|
// determine which tunnel to use based on environment.server
|
|
49
49
|
const tunnel =
|
|
50
|
-
environment.server === '
|
|
50
|
+
environment.server === 'cloud@aws.lambda'
|
|
51
51
|
? config.database.tunnel.lambda
|
|
52
52
|
: config.database.tunnel.local;
|
|
53
53
|
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
"if-env": "@declapract{check.minVersion('1.0.4')}"
|
|
6
6
|
},
|
|
7
7
|
"scripts": {
|
|
8
|
-
"deploy:prune": "npx sls prune -n 7 --stage $
|
|
9
|
-
"deploy:release": "npm run build && sls deploy --verbose --stage $
|
|
8
|
+
"deploy:prune": "SLS_STAGE=$(if [ \"$ACCESS\" = 'prep' ]; then echo 'dev'; else echo \"$ACCESS\"; fi) && export COMMIT=$(npx sdk-environment get commit) && npx sls prune -n 7 --stage $SLS_STAGE",
|
|
9
|
+
"deploy:release": "SLS_STAGE=$(if [ \"$ACCESS\" = 'prep' ]; then echo 'dev'; else echo \"$ACCESS\"; fi) && export COMMIT=$(npx sdk-environment get commit) && npm run build && sls deploy --verbose --stage $SLS_STAGE",
|
|
10
10
|
"deploy:send-notification": "curl -X POST -H 'Content-type: application/json' --data \"{\\\"text\\\":\\\"$([ -z $DEPLOYER_NAME ] && git config user.name || echo $DEPLOYER_NAME) has deployed $npm_package_name@v$npm_package_version:\nhttps://github.com/@declapract{variable.organizationName}/$npm_package_name/tree/v$npm_package_version\\\"}\" @declapract{variable.slackWebhookUrl}",
|
|
11
|
-
"deploy:
|
|
12
|
-
"deploy:prod": "
|
|
13
|
-
"deploy": "if-env
|
|
11
|
+
"deploy:prep": "ACCESS=prep npm run deploy:release",
|
|
12
|
+
"deploy:prod": "ACCESS=prod npm run deploy:release && npm run deploy:send-notification",
|
|
13
|
+
"deploy": "if-env ACCESS=prod && npm run deploy:prod && exit 0 || if-env ACCESS=prep && npm run deploy:prep && exit 0 || echo '🛑 invalid ACCESS, must be prod or prep' && exit 1"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
service: @declapract{variable.projectName}
|
|
2
2
|
|
|
3
|
+
variablesResolutionMode: 20210326
|
|
4
|
+
|
|
3
5
|
package:
|
|
4
6
|
artifact: .artifact/contents.zip
|
|
5
7
|
|
|
6
8
|
plugins:
|
|
7
9
|
- serverless-prune-plugin
|
|
8
10
|
|
|
11
|
+
custom:
|
|
12
|
+
accessByStage:
|
|
13
|
+
dev: prep
|
|
14
|
+
prod: prod
|
|
15
|
+
|
|
9
16
|
provider:
|
|
10
17
|
name: aws
|
|
11
18
|
runtime: nodejs22.x
|
|
@@ -19,7 +26,8 @@ provider:
|
|
|
19
26
|
environment:
|
|
20
27
|
TZ: UTC # guarantee that utc timezone will be used explicitly, to facilitate a pit of success
|
|
21
28
|
NODE_ENV: production # deploy with production optimizations of all resources, to make `dev` and `prod` stage deployments equivalent functionally (i.e., the same code paths in dev and prod)
|
|
22
|
-
|
|
29
|
+
ACCESS: ${self:custom.accessByStage.${opt:stage}, 'prep'} # sdk-environment access tier, to target the correct config + resources (e.g., hit dev db -vs- prod db)
|
|
30
|
+
COMMIT: ${env:COMMIT} # sdk-environment commit slug, must be set by deploy command
|
|
23
31
|
AWS_NODEJS_CONNECTION_REUSE_ENABLED: true # https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-reusing-connections.html
|
|
24
32
|
deploymentBucket: serverless-deployment-@declapract{variable.infrastructureNamespaceId}-${self:provider.stage}
|
|
25
33
|
vpc:
|
|
@@ -35,7 +43,9 @@ provider:
|
|
|
35
43
|
Action: 'ssm:DescribeParameters'
|
|
36
44
|
Resource: '*'
|
|
37
45
|
- Effect: 'Allow'
|
|
38
|
-
Action:
|
|
46
|
+
Action:
|
|
47
|
+
- ssm:GetParameter
|
|
48
|
+
- ssm:GetParameters
|
|
39
49
|
Resource: arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/*
|
|
40
50
|
- Effect: 'Allow'
|
|
41
51
|
Action: 'kms:Decrypt'
|
|
@@ -110,8 +120,8 @@ provider:
|
|
|
110
120
|
- athena:GetQueryExecution
|
|
111
121
|
- athena:GetQueryResults
|
|
112
122
|
Resource: '*'
|
|
113
|
-
# allow
|
|
123
|
+
# allow access inference from account name
|
|
114
124
|
- Effect: Allow
|
|
115
125
|
Action:
|
|
116
|
-
-
|
|
126
|
+
- account:GetAccountInformation
|
|
117
127
|
Resource: '*'
|
|
@@ -3,16 +3,23 @@ import { FileCheckType } from 'declapract';
|
|
|
3
3
|
|
|
4
4
|
export const check = FileCheckType.CONTAINS; // i.e., check that the contents of the file contains what's declared (default is equals)
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const accessInferencePolicy = ` # allow access inference from account name
|
|
7
7
|
- Effect: Allow
|
|
8
8
|
Action:
|
|
9
|
-
-
|
|
9
|
+
- account:GetAccountInformation
|
|
10
10
|
Resource: '*'`;
|
|
11
11
|
|
|
12
|
+
const accessByStageCustom = `custom:
|
|
13
|
+
accessByStage:
|
|
14
|
+
dev: prep
|
|
15
|
+
prod: prod
|
|
16
|
+
|
|
17
|
+
`;
|
|
18
|
+
|
|
12
19
|
export const fix: FileFixFunction = (contents) => {
|
|
13
|
-
if (!contents) return { contents }; // do nothing if file dne
|
|
20
|
+
if (!contents) return { contents }; // do nothing if file dne
|
|
14
21
|
let fixed = contents
|
|
15
|
-
.replace(/runtime: nodejs\d\d.x/, 'runtime:
|
|
22
|
+
.replace(/runtime: nodejs\d\d.x/, 'runtime: nodejs22.x')
|
|
16
23
|
.replace(/ {2}- serverless-offline .*\n/, '') // a plugin we no longer use (never used it, no need to have it)
|
|
17
24
|
.replace(/ {2}- serverless-pseudo-parameters .*\n/, '') // a plugin we no longer use (serverless supports variables natively now)
|
|
18
25
|
.replace(/#\{AWS::Region\}/g, '${aws:region}') // use the serverless native variables, instead of the pseudo-parameters format
|
|
@@ -43,15 +50,58 @@ export const fix: FileFixFunction = (contents) => {
|
|
|
43
50
|
' timeout: 60 # default timeout to 1min, for resilience against increased cold start times; individual functions can override this', // bump the timeout
|
|
44
51
|
);
|
|
45
52
|
|
|
46
|
-
//
|
|
47
|
-
if (!fixed.includes('
|
|
53
|
+
// add variablesResolutionMode after service line if not present
|
|
54
|
+
if (!fixed.includes('variablesResolutionMode:')) {
|
|
55
|
+
fixed = fixed.replace(
|
|
56
|
+
/^(service: [a-zA-Z0-9-]+)\n/m,
|
|
57
|
+
'$1\n\nvariablesResolutionMode: 20210326\n',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// add custom accessByStage block before provider if not present
|
|
62
|
+
if (!fixed.includes('accessByStage:')) {
|
|
63
|
+
fixed = fixed.replace(/\nprovider:/, `\n${accessByStageCustom}provider:`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// replace STAGE env var with ACCESS + COMMIT pattern
|
|
67
|
+
if (
|
|
68
|
+
fixed.includes('STAGE: ${self:provider.stage}') &&
|
|
69
|
+
!fixed.includes('ACCESS:')
|
|
70
|
+
) {
|
|
71
|
+
fixed = fixed.replace(
|
|
72
|
+
/STAGE: \$\{self:provider\.stage\}[^\n]*/,
|
|
73
|
+
"ACCESS: ${self:custom.accessByStage.${opt:stage}, 'prep'} # sdk-environment access tier, to target the correct config + resources (e.g., hit dev db -vs- prod db)\n COMMIT: ${env:COMMIT} # sdk-environment commit slug, must be set by deploy command",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// replace old iam:ListAccountAliases with account:GetAccountInformation
|
|
78
|
+
if (fixed.includes('iam:ListAccountAliases')) {
|
|
79
|
+
fixed = fixed.replace(
|
|
80
|
+
/# allow inferring access from account alias\n\s+- Effect: Allow\n\s+Action:\n\s+- iam:ListAccountAliases\n\s+Resource: '\*'/,
|
|
81
|
+
accessInferencePolicy.trim(),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// add account:GetAccountInformation policy if not present
|
|
86
|
+
if (!fixed.includes('account:GetAccountInformation')) {
|
|
48
87
|
if (/iamRoleStatements:\s*\n/.test(fixed)) {
|
|
49
88
|
fixed = fixed.replace(
|
|
50
89
|
/iamRoleStatements:\s*\n/,
|
|
51
|
-
`iamRoleStatements:\n${
|
|
90
|
+
`iamRoleStatements:\n${accessInferencePolicy}\n`,
|
|
52
91
|
);
|
|
53
92
|
}
|
|
54
93
|
}
|
|
55
94
|
|
|
95
|
+
// add ssm:GetParameter to SSM permissions if only GetParameters exists
|
|
96
|
+
if (
|
|
97
|
+
fixed.includes('ssm:GetParameters') &&
|
|
98
|
+
!fixed.includes('ssm:GetParameter\n')
|
|
99
|
+
) {
|
|
100
|
+
fixed = fixed.replace(
|
|
101
|
+
/Action: 'ssm:GetParameters'/,
|
|
102
|
+
'Action:\n - ssm:GetParameter\n - ssm:GetParameters',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
56
106
|
return { contents: fixed };
|
|
57
107
|
};
|
|
@@ -27,29 +27,50 @@ if (!existsSync(join(process.cwd(), 'package.json')))
|
|
|
27
27
|
*/
|
|
28
28
|
if (
|
|
29
29
|
(process.env.NODE_ENV !== 'test' ||
|
|
30
|
-
(process.env.
|
|
31
|
-
process.env.
|
|
30
|
+
(process.env.CONFIG && process.env.CONFIG !== 'test')) &&
|
|
31
|
+
process.env.I_KNOW_THE_RISKS !== 'true'
|
|
32
32
|
)
|
|
33
|
-
throw new Error(`integration.test
|
|
33
|
+
throw new Error(`integration.test config must be 'test'`);
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* .what =
|
|
37
|
-
* .why =
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
* .what = source aws profile from keyrack if available
|
|
37
|
+
* .why = keyrack manages which profile to use per environment
|
|
38
|
+
*/
|
|
39
|
+
const keyrackYmlPath = join(process.cwd(), '.agent/keyrack.yml');
|
|
40
|
+
if (existsSync(keyrackYmlPath) && !process.env.CI)
|
|
41
|
+
keyrack.source({ env: 'test', owner: 'ehmpath', mode: 'lenient' });
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* .what = export aws credentials from sso profile if aws is required
|
|
45
|
+
* .why = aws sdk v2 doesn't handle sso_session profiles natively
|
|
40
46
|
*/
|
|
41
47
|
const declapractUsePath = join(process.cwd(), 'declapract.use.yml');
|
|
42
48
|
const declapractUseContent = existsSync(declapractUsePath)
|
|
43
49
|
? readFileSync(declapractUsePath, 'utf8')
|
|
44
50
|
: '';
|
|
45
51
|
const requiresAwsAuth = declapractUseContent.includes('awsAccountId');
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
!
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
if (requiresAwsAuth && !process.env.AWS_ACCESS_KEY_ID && !process.env.CI) {
|
|
53
|
+
const awsSsoProfile = process.env.AWS_PROFILE;
|
|
54
|
+
if (!awsSsoProfile)
|
|
55
|
+
throw new Error(
|
|
56
|
+
'AWS_PROFILE not set. keyrack.source() should have set it.',
|
|
57
|
+
);
|
|
58
|
+
try {
|
|
59
|
+
const credOutput = execSync(
|
|
60
|
+
`aws configure export-credentials --profile ${awsSsoProfile} --format env`,
|
|
61
|
+
{ encoding: 'utf8', timeout: 10000 },
|
|
62
|
+
);
|
|
63
|
+
// parse and set env vars from output like "export AWS_ACCESS_KEY_ID=..."
|
|
64
|
+
credOutput.split('\n').forEach((line) => {
|
|
65
|
+
const match = line.match(/^export\s+(\w+)=(.*)$/);
|
|
66
|
+
if (match) process.env[match[1]!] = match[2]!;
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`failed to export aws credentials from sso profile '${awsSsoProfile}'. run: aws sso login --profile ${awsSsoProfile}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
53
74
|
|
|
54
75
|
/**
|
|
55
76
|
* .what = verify that the testdb has been provisioned if a databaseUserName is declared
|
|
@@ -85,13 +106,3 @@ if (requiresTestDb) {
|
|
|
85
106
|
);
|
|
86
107
|
}
|
|
87
108
|
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* .what = source credentials from keyrack for test env
|
|
91
|
-
* .why =
|
|
92
|
-
* - auto-inject keys into process.env
|
|
93
|
-
* - fail fast with helpful error if keyrack locked or keys absent
|
|
94
|
-
*/
|
|
95
|
-
const keyrackYmlPath = join(process.cwd(), '.agent/keyrack.yml');
|
|
96
|
-
if (existsSync(keyrackYmlPath))
|
|
97
|
-
keyrack.source({ env: 'test', owner: 'ehmpath', mode: 'strict' });
|
|
@@ -4,6 +4,8 @@ import util from 'node:util';
|
|
|
4
4
|
|
|
5
5
|
import { jest } from '@jest/globals';
|
|
6
6
|
|
|
7
|
+
import { stage } from './src/utils/environment';
|
|
8
|
+
|
|
7
9
|
// mock that getConfig just returns plaintext test env config in unit tests
|
|
8
10
|
jest.mock('./src/utils/config/getConfig', () => ({
|
|
9
11
|
getConfig: jest.fn().mockImplementation(() => require('./config/test.json')),
|
|
@@ -13,7 +15,7 @@ jest.mock('./src/utils/config/getConfig', () => ({
|
|
|
13
15
|
util.inspect.defaultOptions.depth = 5;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* .what = verify that we're
|
|
18
|
+
* .what = verify that we're at a valid project directory; otherwise, fail fast
|
|
17
19
|
* .why = prevent confusion and hard-to-debug errors from running tests in the wrong directory
|
|
18
20
|
*/
|
|
19
21
|
if (!existsSync(join(process.cwd(), 'package.json')))
|
|
@@ -23,11 +25,8 @@ if (!existsSync(join(process.cwd(), 'package.json')))
|
|
|
23
25
|
* sanity check that unit tests are only run the 'test' environment
|
|
24
26
|
*
|
|
25
27
|
* usecases
|
|
26
|
-
* - prevent
|
|
27
|
-
* - prevent
|
|
28
|
+
* - prevent prod state pollution with test data
|
|
29
|
+
* - prevent financial mutations
|
|
28
30
|
*/
|
|
29
|
-
if (
|
|
30
|
-
(
|
|
31
|
-
process.env.I_KNOW_WHAT_IM_DOING !== 'true'
|
|
32
|
-
)
|
|
33
|
-
throw new Error(`unit.test is not targeting stage 'test'`);
|
|
31
|
+
if (stage !== 'test' && process.env.I_KNOW_THE_RISKS !== 'true')
|
|
32
|
+
throw new Error(`unit-test does not target stage 'test'`);
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
"@swc/jest": "@declapract{check.minVersion('0.2.39')}"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([
|
|
14
|
-
"test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([
|
|
15
|
-
"test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([
|
|
13
|
+
"test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ \"${THOROUGH:-}\" != \"true\" ] && echo '--changedSince=main') $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
14
|
+
"test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ \"${THOROUGH:-}\" != \"true\" ] && echo '--changedSince=main') $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
15
|
+
"test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
16
16
|
"test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
|
|
17
|
-
"test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([
|
|
17
|
+
"test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')"
|
|
18
18
|
}
|
|
19
19
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "declapract-typescript-ehmpathy",
|
|
3
3
|
"author": "ehmpathy",
|
|
4
4
|
"description": "declapract best practices declarations for typescript",
|
|
5
|
-
"version": "0.47.
|
|
5
|
+
"version": "0.47.69",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"repository": "ehmpathy/declapract-typescript-ehmpathy",
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"test:lint:biome": "npm run biome:nested:hide && biome check --diagnostic-level=error; EXIT=$?; npm run biome:nested:restore; exit $EXIT",
|
|
34
34
|
"test:lint:biome:all": "npm run biome:nested:hide && biome check; EXIT=$?; npm run biome:nested:restore; exit $EXIT",
|
|
35
35
|
"test:lint": "npm run test:lint:biome && npm run test:lint:deps",
|
|
36
|
-
"test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([
|
|
37
|
-
"test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([
|
|
38
|
-
"test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([
|
|
36
|
+
"test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ \"${THOROUGH:-}\" != \"true\" ] && echo '--changedSince=main') $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
37
|
+
"test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ \"${THOROUGH:-}\" != \"true\" ] && echo '--changedSince=main') $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
38
|
+
"test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
39
39
|
"test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally && npm run test:validate",
|
|
40
|
-
"test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([
|
|
40
|
+
"test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
41
41
|
"prepush": "npm run test && npm run build",
|
|
42
42
|
"prepublish": "npm run build",
|
|
43
43
|
"preversion": "npm run prepush",
|
|
@@ -75,12 +75,12 @@
|
|
|
75
75
|
"esbuild-register": "3.6.0",
|
|
76
76
|
"husky": "8.0.3",
|
|
77
77
|
"jest": "30.2.0",
|
|
78
|
-
"rhachet": "1.41.
|
|
78
|
+
"rhachet": "1.41.10",
|
|
79
79
|
"rhachet-brains-anthropic": "0.4.1",
|
|
80
80
|
"rhachet-brains-xai": "0.3.3",
|
|
81
|
-
"rhachet-roles-bhrain": "0.27.
|
|
82
|
-
"rhachet-roles-bhuild": "0.21.
|
|
83
|
-
"rhachet-roles-ehmpathy": "1.35.
|
|
81
|
+
"rhachet-roles-bhrain": "0.27.6",
|
|
82
|
+
"rhachet-roles-bhuild": "0.21.9",
|
|
83
|
+
"rhachet-roles-ehmpathy": "1.35.5",
|
|
84
84
|
"tsc-alias": "1.8.10",
|
|
85
85
|
"tsx": "4.20.6",
|
|
86
86
|
"type-fns": "1.21.2",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
declare module 'config-with-paramstore';
|
|
File without changes
|
|
File without changes
|