declapract-typescript-ehmpathy 0.49.3 → 0.49.5
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/getVariables.ts +6 -2
- package/dist/practices/cicd-app-react-native-expo/best-practice/.github/workflows/.deploy-expo.yml +1 -1
- package/dist/practices/cicd-app-react-native-expo/best-practice/.github/workflows/deploy.yml +8 -8
- package/dist/practices/cicd-app-react-native-expo/best-practice/.github/workflows/test.yml +3 -3
- package/dist/practices/cicd-app-react-native-expo/best-practice/package.json +9 -9
- package/dist/practices/cicd-common/best-practice/.github/workflows/.test.yml +4 -4
- package/dist/practices/cicd-common/best-practice/.github/workflows/test.yml +1 -1
- package/dist/practices/cicd-package/best-practice/.github/workflows/publish.yml +1 -1
- package/dist/practices/cicd-service/best-practice/.agent/repo=.this/skills/use.rds.capacity.sh +1 -1
- package/dist/practices/cicd-service/best-practice/.declapract.readme.md +1 -1
- package/dist/practices/cicd-service/best-practice/.github/workflows/deploy.yml +3 -3
- package/dist/practices/cicd-service/best-practice/.github/workflows/provision.yml +9 -9
- package/dist/practices/config/bad-practices/configs-contain-wrong-account-id/config/prod.json.declapract.ts +2 -2
- package/dist/practices/config/bad-practices/divergent-config-shapes/.declapract.readme.md +1 -1
- package/dist/practices/config/bad-practices/divergent-config-shapes/config/{dev.json.declapract.ts → prep.json.declapract.ts} +7 -7
- package/dist/practices/config/bad-practices/old-dev-config-location/config/dev.json.declapract.ts +4 -1
- package/dist/practices/config/best-practice/config/prep.json +1 -1
- package/dist/practices/config/best-practice/config/test.json +1 -1
- package/dist/practices/environments/bad-practices/old-dev-scripts/package.json.declapract.ts +57 -0
- package/dist/practices/environments/best-practice/.declapract.readme.md +1 -1
- package/dist/practices/environments/best-practice/src/utils/environment.ts +2 -2
- package/dist/practices/husky/best-practice/.husky/check.timestamps.sh +48 -0
- package/dist/practices/husky/best-practice/.husky/pre-commit +2 -0
- package/dist/practices/husky/check.timestamps.play.declapract.integration.test.ts +148 -0
- package/dist/practices/package-json-order/best-practice/package.json.declapract.ts +11 -11
- package/dist/practices/persist-with-rds/best-practice/.declapract.readme.md +21 -1
- package/dist/practices/persist-with-rds/best-practice/provision/aws/product/parameter-store.tf +39 -0
- package/dist/practices/persist-with-rds/best-practice/provision/schema/deploy.database.sh +9 -9
- package/dist/practices/provision-github/best-practice/.declapract.readme.md +7 -0
- package/dist/practices/provision-github/best-practice/package.json +1 -1
- package/dist/practices/provision-github/best-practice/provision/github.repo/resources.ts +33 -0
- package/dist/practices/serverless/best-practice/package.json +2 -2
- package/dist/practices/serverless/best-practice/serverless.yml +3 -3
- package/dist/practices/serverless/best-practice/serverless.yml.declapract.ts +2 -2
- package/dist/practices/terraform-aws/bad-practices/old-dev-env-dir/provision/aws/environments/dev/<star>.declapract.ts +14 -0
- package/dist/practices/terraform-aws/best-practice/provision/aws/environments/{dev → prep}/main.tf +2 -2
- package/dist/practices/terraform-aws/best-practice/provision/aws/environments/test/main.tf +1 -1
- package/package.json +5 -5
- /package/dist/practices/config/bad-practices/configs-contain-wrong-account-id/config/{dev.json.declapract.ts → prep.json.declapract.ts} +0 -0
- /package/dist/practices/terraform-aws/best-practice/provision/aws/environments/{dev → prep}/versions.tf +0 -0
package/dist/getVariables.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const getServiceVariables = createGetVariables({
|
|
|
10
10
|
infrastructureNamespaceId: 'abcde12345',
|
|
11
11
|
slackWebhookUrl: 'https://...',
|
|
12
12
|
awsAccountId: {
|
|
13
|
-
|
|
13
|
+
prep: '123abc',
|
|
14
14
|
prod: '456def',
|
|
15
15
|
},
|
|
16
16
|
});
|
|
@@ -18,9 +18,13 @@ export const getServiceVariables = createGetVariables({
|
|
|
18
18
|
export const getRdsVariables = createGetVariables({
|
|
19
19
|
databaseName: 'awesomethingdb',
|
|
20
20
|
databaseClusterHost: {
|
|
21
|
-
|
|
21
|
+
prep: 'awesomesdb.cluster-abc123.us-east-1.rds.amazonaws.com',
|
|
22
22
|
prod: 'awesomesdb.cluster-def456.us-east-1.rds.amazonaws.com',
|
|
23
23
|
},
|
|
24
|
+
databaseTunnelHost: {
|
|
25
|
+
prep: 'aws.ssmproxy.awesomesdb.prep',
|
|
26
|
+
prod: 'aws.ssmproxy.awesomesdb.prod',
|
|
27
|
+
},
|
|
24
28
|
databaseUserName: {
|
|
25
29
|
serviceUser: 'svc_awesome_thing_user',
|
|
26
30
|
cicdUser: 'awesomethingdb_cicd',
|
package/dist/practices/cicd-app-react-native-expo/best-practice/.github/workflows/deploy.yml
CHANGED
|
@@ -13,10 +13,10 @@ on:
|
|
|
13
13
|
description: 'which stage do you want to deploy to?'
|
|
14
14
|
type: choice
|
|
15
15
|
options:
|
|
16
|
-
-
|
|
16
|
+
- prep
|
|
17
17
|
- prod
|
|
18
18
|
required: true
|
|
19
|
-
default: '
|
|
19
|
+
default: 'prep'
|
|
20
20
|
thoroughly:
|
|
21
21
|
description: 'should we run tests before this deployment?'
|
|
22
22
|
type: choice
|
|
@@ -36,22 +36,22 @@ jobs:
|
|
|
36
36
|
if: github.event_name != 'workflow_dispatch' || github.event.inputs.thoroughly == 'true'
|
|
37
37
|
with:
|
|
38
38
|
creds-aws-region: us-east-1
|
|
39
|
-
creds-aws-role-arn: arn:aws:iam::@declapract{variable.awsAccountId.
|
|
39
|
+
creds-aws-role-arn: arn:aws:iam::@declapract{variable.awsAccountId.prep}:role/@declapract{variable.projectName}-github-actions-prep
|
|
40
40
|
secrets: inherit # keyrack firewall in .test.yml filters to declared keys
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
prep:
|
|
43
43
|
uses: ./.github/workflows/.deploy-expo.yml
|
|
44
44
|
needs: [test]
|
|
45
45
|
# dont deploy on merge to main by default, since builds are expensive
|
|
46
46
|
if: |
|
|
47
|
-
((github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' && false) || github.event.inputs.stage == '
|
|
47
|
+
((github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' && false) || github.event.inputs.stage == 'prep') &&
|
|
48
48
|
always() && (needs.test.result == 'success' || needs.test.result == 'skipped')
|
|
49
49
|
with:
|
|
50
|
-
stage:
|
|
50
|
+
stage: prep
|
|
51
51
|
build: development
|
|
52
|
-
github-environment:
|
|
52
|
+
github-environment: prep
|
|
53
53
|
creds-aws-region: us-east-1
|
|
54
|
-
creds-aws-role-arn: arn:aws:iam::@declapract{variable.awsAccountId.
|
|
54
|
+
creds-aws-role-arn: arn:aws:iam::@declapract{variable.awsAccountId.prep}:role/@declapract{variable.projectName}-github-actions-prep
|
|
55
55
|
secrets:
|
|
56
56
|
expo-token: ${{ secrets.EXPO_TOKEN }}
|
|
57
57
|
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
uses: ./.github/workflows/.test.yml
|
|
18
18
|
with:
|
|
19
19
|
aws-region: us-east-1
|
|
20
|
-
aws-account-id: '@declapract{variable.awsAccountId.
|
|
20
|
+
aws-account-id: '@declapract{variable.awsAccountId.prep}'
|
|
21
21
|
secrets:
|
|
22
|
-
aws-access-key-id: ${{ secrets.
|
|
23
|
-
aws-secret-access-key: ${{ secrets.
|
|
22
|
+
aws-access-key-id: ${{ secrets.PREP_AWS_ACCESS_KEY_ID }}
|
|
23
|
+
aws-secret-access-key: ${{ secrets.PREP_AWS_SECRET_ACCESS_KEY }}
|
|
@@ -12,28 +12,28 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start:hot:proxy": "npx local-ssl-proxy --source 3447 --target 8081",
|
|
15
|
-
"start:hot:
|
|
16
|
-
"start:hot:
|
|
17
|
-
"start:hot:
|
|
15
|
+
"start:hot:prep": "STAGE=prep npx expo start --clear --dev-client",
|
|
16
|
+
"start:hot:prep:link": "echo '🚀 https://localhost.@declapract{variable.app.web.domain.prep}:@declapract{variable.app.web.port.hot}'",
|
|
17
|
+
"start:hot:prep:proxy": "npm run start:hot:prep:link && npm run start:hot:proxy",
|
|
18
18
|
"start:hot:prod": "STAGE=prod npx expo start --clear --dev-client",
|
|
19
19
|
"start:hot:prod:link": "echo '🚀 https://localhost.@declapract{variable.app.web.domain.prod}:@declapract{variable.app.web.port.hot}'",
|
|
20
20
|
"start:hot:prod:proxy": "npm run start:hot:prod:link && npm run start:hot:proxy",
|
|
21
21
|
"start:cold:proxy": "npx local-ssl-proxy --source 3449 --target 3000",
|
|
22
|
-
"start:cold:
|
|
23
|
-
"start:cold:
|
|
24
|
-
"start:cold:
|
|
22
|
+
"start:cold:prep": "npm run build:web && npx serve dist --single",
|
|
23
|
+
"start:cold:prep:link": "echo '🚀 https://localhost.@declapract{variable.app.web.domain.prep}:@declapract{variable.app.web.port.cold}'",
|
|
24
|
+
"start:cold:prep:proxy": "echo npm run start:cold:prep:link && npm run start:cold:proxy",
|
|
25
25
|
"start:cold:prod": "npm run build:web && npx serve dist --single",
|
|
26
26
|
"start:cold:prod:link": "echo '🚀 https://localhost.@declapract{variable.app.web.domain.prod}:@declapract{variable.app.web.port.cold}'",
|
|
27
27
|
"register:device:apple:info": "echo 'learn more here https://docs.expo.dev/build/internal-distribution/'",
|
|
28
28
|
"register:device:apple:inclusions:rebuild:prod": "npx eas build --platform ios --clear-cache",
|
|
29
|
-
"register:device:apple:inclusions:rebuild:
|
|
29
|
+
"register:device:apple:inclusions:rebuild:prep": "npx eas build --profile development --platform ios --clear-cache",
|
|
30
30
|
"register:device:apple:add": "echo 'register an apple device to allow it to install internal builds' && eas device:create && npm run register:device:apple:inclusions:rebuild",
|
|
31
31
|
"register:device:apple:list": "eas device:list",
|
|
32
32
|
"register:device:apple:delete": "eas device:delete",
|
|
33
33
|
"register:device:apple:rename": "eas device:rename",
|
|
34
34
|
"register:profile:apple:list": "echo 'see list here https://developer.apple.com/account/resources/profiles/list'",
|
|
35
|
-
"build:
|
|
36
|
-
"build:
|
|
35
|
+
"build:prep:ios": "eas build --profile development --platform ios --non-interactive",
|
|
36
|
+
"build:prep:android": "eas build --profile development --platform android --non-interactive",
|
|
37
37
|
"build:web": "npx expo export --platform web",
|
|
38
38
|
"deploy:prod:ios:metadata": "eas metadata:push",
|
|
39
39
|
"deploy:prod": "echo 'use github actions'",
|
|
@@ -226,8 +226,8 @@ jobs:
|
|
|
226
226
|
- name: start:testdb
|
|
227
227
|
run: npm run start:testdb --if-present
|
|
228
228
|
|
|
229
|
-
- name: start:livedb:
|
|
230
|
-
run: npm run start:livedb:
|
|
229
|
+
- name: start:livedb:prep
|
|
230
|
+
run: npm run start:livedb:prep --if-present
|
|
231
231
|
|
|
232
232
|
- name: build
|
|
233
233
|
run: npm run build
|
|
@@ -310,8 +310,8 @@ jobs:
|
|
|
310
310
|
- name: start:testdb
|
|
311
311
|
run: npm run start:testdb --if-present
|
|
312
312
|
|
|
313
|
-
- name: start:livedb:
|
|
314
|
-
run: npm run start:livedb:
|
|
313
|
+
- name: start:livedb:prep
|
|
314
|
+
run: npm run start:livedb:prep --if-present
|
|
315
315
|
|
|
316
316
|
- name: build
|
|
317
317
|
run: npm run build
|
|
@@ -21,5 +21,5 @@ jobs:
|
|
|
21
21
|
uses: ./.github/workflows/.test.yml
|
|
22
22
|
with:
|
|
23
23
|
creds-aws-region: us-east-1
|
|
24
|
-
creds-aws-role-arn: ${{ vars.
|
|
24
|
+
creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_PREP_OIDC_ROLE_ARN }} # use aws auth via oidc, if this repo supplies it
|
|
25
25
|
secrets: inherit # keyrack firewall in .test.yml filters to declared keys
|
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
uses: ./.github/workflows/.test.yml
|
|
19
19
|
with:
|
|
20
20
|
creds-aws-region: us-east-1
|
|
21
|
-
creds-aws-role-arn: ${{ vars.
|
|
21
|
+
creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_PREP_OIDC_ROLE_ARN }} # use aws auth via oidc, if this repo supplies it
|
|
22
22
|
secrets: inherit # keyrack firewall in .test.yml filters to declared keys
|
|
23
23
|
|
|
24
24
|
publish:
|
|
@@ -40,7 +40,7 @@ jobs:
|
|
|
40
40
|
if: github.event_name != 'workflow_dispatch' || github.event.inputs.thoroughly == 'true'
|
|
41
41
|
with:
|
|
42
42
|
creds-aws-region: us-east-1
|
|
43
|
-
creds-aws-role-arn: ${{ vars.
|
|
43
|
+
creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_PREP_OIDC_ROLE_ARN }} # use aws auth via oidc, if this repo supplies it
|
|
44
44
|
secrets: inherit # keyrack firewall in .test.yml filters to declared keys
|
|
45
45
|
|
|
46
46
|
dev:
|
|
@@ -51,9 +51,9 @@ jobs:
|
|
|
51
51
|
always() && (needs.test.result == 'success' || needs.test.result == 'skipped')
|
|
52
52
|
with:
|
|
53
53
|
stage: dev
|
|
54
|
-
github-environment:
|
|
54
|
+
github-environment: prep # github environment stays as prep (where secrets/vars live)
|
|
55
55
|
creds-aws-region: us-east-1
|
|
56
|
-
creds-aws-role-arn: ${{ vars.
|
|
56
|
+
creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_PREP_OIDC_ROLE_ARN }}
|
|
57
57
|
|
|
58
58
|
prod:
|
|
59
59
|
uses: ./.github/workflows/.deploy-sls.yml
|
|
@@ -19,17 +19,17 @@ jobs:
|
|
|
19
19
|
uses: ./.github/workflows/.terraform.yml
|
|
20
20
|
with:
|
|
21
21
|
working-directory: provision/aws/environments/test
|
|
22
|
-
github-environment:
|
|
22
|
+
github-environment: prep
|
|
23
23
|
creds-aws-region: us-east-1
|
|
24
|
-
creds-aws-role-arn: ${{ vars.
|
|
24
|
+
creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_PREP_OIDC_ROLE_ARN }}
|
|
25
25
|
|
|
26
|
-
aws-
|
|
26
|
+
aws-prep:
|
|
27
27
|
uses: ./.github/workflows/.terraform.yml
|
|
28
28
|
with:
|
|
29
|
-
working-directory: provision/aws/environments/
|
|
30
|
-
github-environment:
|
|
29
|
+
working-directory: provision/aws/environments/prep
|
|
30
|
+
github-environment: prep
|
|
31
31
|
creds-aws-region: us-east-1
|
|
32
|
-
creds-aws-role-arn: ${{ vars.
|
|
32
|
+
creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_PREP_OIDC_ROLE_ARN }}
|
|
33
33
|
|
|
34
34
|
aws-prod:
|
|
35
35
|
uses: ./.github/workflows/.terraform.yml
|
|
@@ -50,13 +50,13 @@ jobs:
|
|
|
50
50
|
secrets:
|
|
51
51
|
creds-github-app-private-key: ${{ secrets.DECLASTRUCT_GITHUB_CONFORMER_APP_PRIVATE_KEY }}
|
|
52
52
|
|
|
53
|
-
sql-schema-
|
|
53
|
+
sql-schema-prep:
|
|
54
54
|
uses: ./.github/workflows/.sql-schema-control.yml
|
|
55
55
|
with:
|
|
56
56
|
stage: prep
|
|
57
|
-
github-environment:
|
|
57
|
+
github-environment: prep
|
|
58
58
|
creds-aws-region: us-east-1
|
|
59
|
-
creds-aws-role-arn: ${{ vars.
|
|
59
|
+
creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_PREP_OIDC_ROLE_ARN }}
|
|
60
60
|
|
|
61
61
|
sql-schema-prod:
|
|
62
62
|
uses: ./.github/workflows/.sql-schema-control.yml
|
|
@@ -4,6 +4,6 @@ import { getServiceVariables } from '../../../../../getVariables';
|
|
|
4
4
|
|
|
5
5
|
export const check: FileCheckFunction = (contents, context) => {
|
|
6
6
|
const { awsAccountId } = getServiceVariables(context);
|
|
7
|
-
if (!contents?.includes(awsAccountId.
|
|
8
|
-
throw new Error('does not contain aws
|
|
7
|
+
if (!contents?.includes(awsAccountId.prep))
|
|
8
|
+
throw new Error('does not contain aws prep account id');
|
|
9
9
|
};
|
|
@@ -16,18 +16,18 @@ export const check: FileCheckFunction = async (contents, context) => {
|
|
|
16
16
|
const flattenedTestConfigKeys = Object.keys(flatten(testConfigObject));
|
|
17
17
|
|
|
18
18
|
// grab the found keys
|
|
19
|
-
const
|
|
20
|
-
const
|
|
19
|
+
const prepConfigObject = JSON.parse(contents);
|
|
20
|
+
const flattenedPrepConfigKeys = Object.keys(flatten(prepConfigObject));
|
|
21
21
|
|
|
22
22
|
// determine which keys are not matched
|
|
23
|
-
const
|
|
24
|
-
(key) => !
|
|
23
|
+
const keysInTestButNotInPrep = flattenedTestConfigKeys.filter(
|
|
24
|
+
(key) => !flattenedPrepConfigKeys.includes(key),
|
|
25
25
|
);
|
|
26
|
-
const
|
|
26
|
+
const keysInPrepButNotInTest = flattenedPrepConfigKeys.filter(
|
|
27
27
|
(key) => !flattenedTestConfigKeys.includes(key),
|
|
28
28
|
);
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
29
|
+
if (keysInTestButNotInPrep.length) return; // matches bad practice
|
|
30
|
+
if (keysInPrepButNotInTest.length) return; // matches bad practice
|
|
31
31
|
|
|
32
32
|
// otherwise, does not match a bad practice
|
|
33
33
|
throw new Error('its fine');
|
package/dist/practices/config/bad-practices/old-dev-config-location/config/dev.json.declapract.ts
CHANGED
|
@@ -4,9 +4,12 @@ export const check = FileCheckType.EXISTS;
|
|
|
4
4
|
|
|
5
5
|
export const fix: FileFixFunction = (contents, context) => {
|
|
6
6
|
// move config/dev.json → config/prep.json and update access: dev → prep
|
|
7
|
+
// also update .dev hostnames to .prep (e.g., bastion.dev.example.com → bastion.prep.example.com)
|
|
7
8
|
let fixed = contents;
|
|
8
9
|
if (fixed) {
|
|
9
|
-
fixed = fixed
|
|
10
|
+
fixed = fixed
|
|
11
|
+
.replace(/\.dev\b/g, '.prep')
|
|
12
|
+
.replace(/"access":\s*"dev"/, '"access": "prep"');
|
|
10
13
|
}
|
|
11
14
|
return {
|
|
12
15
|
contents: fixed ?? null,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { FileCheckFunction, FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* .what = detects package.json scripts with `:dev` as a segment
|
|
5
|
+
* .why = standardizing on `prep` over `dev` for pre-production environment naming
|
|
6
|
+
*
|
|
7
|
+
* matches both `:dev` at end (deploy:dev) and `:dev:` in middle (build:dev:ios)
|
|
8
|
+
*/
|
|
9
|
+
export const check: FileCheckFunction = (contents) => {
|
|
10
|
+
if (!contents) throw new Error('fine, does not match bad practice');
|
|
11
|
+
const parsedContents = JSON.parse(contents);
|
|
12
|
+
const scripts = parsedContents.scripts ?? {};
|
|
13
|
+
|
|
14
|
+
// check if any script name contains :dev as a segment (either :dev at end or :dev: in middle)
|
|
15
|
+
const devScripts = Object.keys(scripts).filter(
|
|
16
|
+
(key) => key.endsWith(':dev') || key.includes(':dev:'),
|
|
17
|
+
);
|
|
18
|
+
if (devScripts.length > 0) return; // matches bad practice
|
|
19
|
+
|
|
20
|
+
throw new Error('fine, no scripts with :dev segment');
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* .what = renames scripts from `:dev` segment to `:prep` segment
|
|
25
|
+
* .why = standardizing on `prep` over `dev` for pre-production environment naming
|
|
26
|
+
*
|
|
27
|
+
* handles both `:dev` at end (deploy:dev) and `:dev:` in middle (build:dev:ios)
|
|
28
|
+
*
|
|
29
|
+
* .note = only renames script NAMES, not VALUES. script values may still reference
|
|
30
|
+
* `--stage dev` for infrastructure naming (cloudformation stacks stay named
|
|
31
|
+
* `$service-dev`). the serverless package.json handles this via SLS_STAGE
|
|
32
|
+
* mapping. see brief: define.infrastructure-dev-vs-application-prep.md
|
|
33
|
+
*/
|
|
34
|
+
export const fix: FileFixFunction = (contents) => {
|
|
35
|
+
if (!contents) return {};
|
|
36
|
+
const parsedContents = JSON.parse(contents);
|
|
37
|
+
const scripts = parsedContents.scripts ?? {};
|
|
38
|
+
|
|
39
|
+
// find scripts with :dev segment and create renamed versions
|
|
40
|
+
const updatedScripts = { ...scripts };
|
|
41
|
+
for (const [key, value] of Object.entries(scripts)) {
|
|
42
|
+
if (key.endsWith(':dev') || key.includes(':dev:')) {
|
|
43
|
+
// replace :dev: in middle and :dev at end with :prep
|
|
44
|
+
const newKey = key.replace(/:dev:/g, ':prep:').replace(/:dev$/, ':prep');
|
|
45
|
+
updatedScripts[newKey] = value; // add :prep version
|
|
46
|
+
updatedScripts[key] = undefined; // remove :dev version
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
contents: JSON.stringify(
|
|
52
|
+
{ ...parsedContents, scripts: updatedScripts },
|
|
53
|
+
null,
|
|
54
|
+
2,
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
@@ -4,7 +4,7 @@ a critical part of an application's environment is which stage it is running in
|
|
|
4
4
|
|
|
5
5
|
ehmpathy recognizes three standard stages in which applications run throughout their development cycle
|
|
6
6
|
- `prod` stage - a deployed production stack in which application that the business depends on run, affecting real customers
|
|
7
|
-
- `
|
|
7
|
+
- `prep` stage - a deployed pre-production stack in which applications can run and interact with each other for testing
|
|
8
8
|
- `test` stage - an ephemeral, primarily local, developer-only stack in which developers run local code against for testing
|
|
9
9
|
|
|
10
10
|
this information is expected to be defined in the runtime environment through the following environmental variables
|
|
@@ -4,5 +4,5 @@ export { getEnvironment };
|
|
|
4
4
|
|
|
5
5
|
export const envStatic = getEnvironment.static();
|
|
6
6
|
|
|
7
|
-
export const stage = envStatic.access
|
|
8
|
-
export const serviceClientStage = stage === 'prod' ? 'prod' : '
|
|
7
|
+
export const stage = envStatic.access;
|
|
8
|
+
export const serviceClientStage = stage === 'prod' ? 'prod' : 'prep';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# .what = block commit of files that embed a timestamp (T?HH:MM:SS{tz})
|
|
4
|
+
# .why = timestamps in snapshots permadrift — they change every run, so the
|
|
5
|
+
# snapshot can never be "correct" twice; mask them to keep it stable
|
|
6
|
+
# .how = scan staged files (except .ts/.sh) for the pattern; halt with a
|
|
7
|
+
# constraint error (exit 2) if any are found
|
|
8
|
+
|
|
9
|
+
# fail fast on errors and unbound vars (safe here: scan pipelines end in `head`
|
|
10
|
+
# so a no-match grep does not abort, and all vars are assigned before use)
|
|
11
|
+
set -eu
|
|
12
|
+
|
|
13
|
+
# the timestamp pattern: optional lead T, HH:MM:SS, optional timezone (Z, ±HH:MM, ±HHMM)
|
|
14
|
+
TIMESTAMP_PATTERN='T?[0-9][0-9]:[0-9][0-9]:[0-9][0-9](Z|[+-][0-9][0-9]:?[0-9][0-9])?'
|
|
15
|
+
|
|
16
|
+
# collect staged files (added/copied/modified); skip deletions
|
|
17
|
+
staged=$(git diff --cached --name-only --diff-filter=ACM)
|
|
18
|
+
|
|
19
|
+
# scan each staged file for a raw timestamp
|
|
20
|
+
# note: the here-doc feeds the loop in the current shell (no pipe subshell),
|
|
21
|
+
# so `exit 2` halts the commit directly and a clean pass falls through
|
|
22
|
+
while IFS= read -r file; do
|
|
23
|
+
# skip empty lines
|
|
24
|
+
[ -n "$file" ] || continue
|
|
25
|
+
|
|
26
|
+
# skip exempt code files (.ts and .sh legitimately carry time logic)
|
|
27
|
+
case "$file" in
|
|
28
|
+
*.ts | *.sh) continue ;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
31
|
+
# read the staged content; fail loud if git cannot read the staged blob
|
|
32
|
+
# (2>&1 captures git's error into the var so the halt message can surface it)
|
|
33
|
+
if ! content=$(git show ":$file" 2>&1); then
|
|
34
|
+
printf '💥 check.timestamps: failed to read staged content for %s\n' "$file" >&2
|
|
35
|
+
printf ' └─ %s\n' "$content" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# halt if the staged content embeds a raw timestamp
|
|
40
|
+
match=$(printf '%s\n' "$content" | grep -nIE "$TIMESTAMP_PATTERN" | head -n 1)
|
|
41
|
+
if [ -n "$match" ]; then
|
|
42
|
+
printf '✋ timestamps are forbidden in snapshots. mask them to prevent permadrift\n'
|
|
43
|
+
printf ' └─ %s:%s\n' "$file" "$match"
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
done <<EOF
|
|
47
|
+
$staged
|
|
48
|
+
EOF
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { given, then, when } from 'test-fns';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* .what = integration test for the check.timestamps husky guard
|
|
10
|
+
* .why = proves the guard halts commits of files that embed a timestamp
|
|
11
|
+
* (except .ts/.sh) with exit 2, and allows clean/exempt files with exit 0
|
|
12
|
+
* .how = build a temp git repo, stage fixtures, spawn the guard, assert exit code
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// the guard under test (the distributed best-practice template)
|
|
16
|
+
const guardPath = path.join(
|
|
17
|
+
__dirname,
|
|
18
|
+
'best-practice/.husky/check.timestamps.sh',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// build a fresh temp git repo with the given files staged
|
|
22
|
+
const genStagedRepo = (files: Record<string, string>): string => {
|
|
23
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'check-timestamps-'));
|
|
24
|
+
execFileSync('git', ['init', '-q'], { cwd: dir });
|
|
25
|
+
for (const [name, content] of Object.entries(files)) {
|
|
26
|
+
const full = path.join(dir, name);
|
|
27
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
28
|
+
fs.writeFileSync(full, content);
|
|
29
|
+
execFileSync('git', ['add', '--', name], { cwd: dir });
|
|
30
|
+
}
|
|
31
|
+
return dir;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// spawn the guard against a repo; return its exit code + stdout
|
|
35
|
+
const runGuard = (dir: string): { code: number | null; stdout: string } => {
|
|
36
|
+
const res = spawnSync('bash', [guardPath], { cwd: dir, encoding: 'utf-8' });
|
|
37
|
+
return { code: res.status, stdout: res.stdout };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe('check.timestamps guard', () => {
|
|
41
|
+
given('[case1] a non-code file with an iso timestamp', () => {
|
|
42
|
+
when('[t0] the guard runs', () => {
|
|
43
|
+
const dir = genStagedRepo({
|
|
44
|
+
'snapshot.json': '{ "createdAt": "2026-06-25T14:30:00Z" }\n',
|
|
45
|
+
});
|
|
46
|
+
const result = runGuard(dir);
|
|
47
|
+
|
|
48
|
+
then('it halts with exit 2', () => {
|
|
49
|
+
expect(result.code).toEqual(2);
|
|
50
|
+
});
|
|
51
|
+
then('it prints the verbatim constraint message', () => {
|
|
52
|
+
expect(result.stdout).toContain(
|
|
53
|
+
'timestamps are forbidden in snapshots. mask them to prevent permadrift',
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
then('it names the matched file', () => {
|
|
57
|
+
expect(result.stdout).toContain('snapshot.json');
|
|
58
|
+
});
|
|
59
|
+
then('it renders the matched file:line:content detail line', () => {
|
|
60
|
+
// locks the exact halt-output shape: tree-prefixed detail line
|
|
61
|
+
expect(result.stdout).toContain(
|
|
62
|
+
' └─ snapshot.json:1:{ "createdAt": "2026-06-25T14:30:00Z" }',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
given('[case2] a non-code file with a bare time (no lead T)', () => {
|
|
69
|
+
when('[t0] the guard runs', () => {
|
|
70
|
+
const dir = genStagedRepo({ 'doc.md': 'released at 14:30:00 today\n' });
|
|
71
|
+
const result = runGuard(dir);
|
|
72
|
+
|
|
73
|
+
then('it halts with exit 2 (optional T prefix matches bare time)', () => {
|
|
74
|
+
expect(result.code).toEqual(2);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
given('[case3] a .ts file with a timestamp', () => {
|
|
80
|
+
when('[t0] the guard runs', () => {
|
|
81
|
+
const dir = genStagedRepo({ 'time.ts': "const t = '14:30:00';\n" });
|
|
82
|
+
const result = runGuard(dir);
|
|
83
|
+
|
|
84
|
+
then('it allows the commit with exit 0 (.ts is exempt)', () => {
|
|
85
|
+
expect(result.code).toEqual(0);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
given('[case4] a .sh file with a timestamp', () => {
|
|
91
|
+
when('[t0] the guard runs', () => {
|
|
92
|
+
const dir = genStagedRepo({ 'log.sh': "echo 'at 14:30:00Z'\n" });
|
|
93
|
+
const result = runGuard(dir);
|
|
94
|
+
|
|
95
|
+
then('it allows the commit with exit 0 (.sh is exempt)', () => {
|
|
96
|
+
expect(result.code).toEqual(0);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
given('[case5] a clean non-code file with no timestamp', () => {
|
|
102
|
+
when('[t0] the guard runs', () => {
|
|
103
|
+
const dir = genStagedRepo({ 'notes.md': 'hello world, no times here\n' });
|
|
104
|
+
const result = runGuard(dir);
|
|
105
|
+
|
|
106
|
+
then('it allows the commit with exit 0', () => {
|
|
107
|
+
expect(result.code).toEqual(0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
given('[case6] a non-code file with only a date (no time)', () => {
|
|
113
|
+
when('[t0] the guard runs', () => {
|
|
114
|
+
const dir = genStagedRepo({ 'dated.md': 'shipped on 2026-06-25\n' });
|
|
115
|
+
const result = runGuard(dir);
|
|
116
|
+
|
|
117
|
+
then('it allows the commit with exit 0 (date alone does not match)', () => {
|
|
118
|
+
expect(result.code).toEqual(0);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
given('[case7] a non-code file with a timezone-offset timestamp', () => {
|
|
124
|
+
when('[t0] the guard runs', () => {
|
|
125
|
+
const dir = genStagedRepo({
|
|
126
|
+
'rec.yml': 'when: 2026-06-25T14:30:00+05:00\n',
|
|
127
|
+
});
|
|
128
|
+
const result = runGuard(dir);
|
|
129
|
+
|
|
130
|
+
then('it halts with exit 2 (offset tz form matches)', () => {
|
|
131
|
+
expect(result.code).toEqual(2);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
given('[case8] a snapshot with a masked timestamp placeholder', () => {
|
|
137
|
+
when('[t0] the guard runs', () => {
|
|
138
|
+
const dir = genStagedRepo({
|
|
139
|
+
'render.test.ts.snap': '{ "createdAt": "[timestamp]" }\n',
|
|
140
|
+
});
|
|
141
|
+
const result = runGuard(dir);
|
|
142
|
+
|
|
143
|
+
then('it allows the commit with exit 0 (mask remedy is committable)', () => {
|
|
144
|
+
expect(result.code).toEqual(0);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -53,7 +53,7 @@ export const desiredRelativeKeyOrder = {
|
|
|
53
53
|
'provision:testdb:docker:down',
|
|
54
54
|
'provision:testdb',
|
|
55
55
|
'start:testdb',
|
|
56
|
-
'start:livedb:
|
|
56
|
+
'start:livedb:prep',
|
|
57
57
|
'test:auth',
|
|
58
58
|
'test:commits',
|
|
59
59
|
'test:types',
|
|
@@ -82,34 +82,34 @@ export const desiredRelativeKeyOrder = {
|
|
|
82
82
|
'deploy:prune',
|
|
83
83
|
'deploy:release',
|
|
84
84
|
'deploy:send-notification',
|
|
85
|
-
'deploy:
|
|
85
|
+
'deploy:prep',
|
|
86
86
|
'deploy:prod',
|
|
87
87
|
'deploy',
|
|
88
88
|
|
|
89
89
|
// from app-react-native-expo
|
|
90
90
|
'start:hot:proxy',
|
|
91
|
-
'start:hot:
|
|
92
|
-
'start:hot:
|
|
93
|
-
'start:hot:
|
|
91
|
+
'start:hot:prep',
|
|
92
|
+
'start:hot:prep:link',
|
|
93
|
+
'start:hot:prep:proxy',
|
|
94
94
|
'start:hot:prod',
|
|
95
95
|
'start:hot:prod:link',
|
|
96
96
|
'start:hot:prod:proxy',
|
|
97
97
|
'start:cold:proxy',
|
|
98
|
-
'start:cold:
|
|
99
|
-
'start:cold:
|
|
100
|
-
'start:cold:
|
|
98
|
+
'start:cold:prep',
|
|
99
|
+
'start:cold:prep:link',
|
|
100
|
+
'start:cold:prep:proxy',
|
|
101
101
|
'start:cold:prod',
|
|
102
102
|
'start:cold:prod:link',
|
|
103
103
|
'register:device:apple:info',
|
|
104
104
|
'register:device:apple:inclusions:rebuild:prod',
|
|
105
|
-
'register:device:apple:inclusions:rebuild:
|
|
105
|
+
'register:device:apple:inclusions:rebuild:prep',
|
|
106
106
|
'register:device:apple:add',
|
|
107
107
|
'register:device:apple:list',
|
|
108
108
|
'register:device:apple:delete',
|
|
109
109
|
'register:device:apple:rename',
|
|
110
110
|
'register:profile:apple:list',
|
|
111
|
-
'build:
|
|
112
|
-
'build:
|
|
111
|
+
'build:prep:ios',
|
|
112
|
+
'build:prep:android',
|
|
113
113
|
'build:web',
|
|
114
114
|
'deploy:prod:ios:metadata',
|
|
115
115
|
'open:store:android',
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
we use:
|
|
2
2
|
- postgres database
|
|
3
|
-
- provisioned by aws rds aurora when in
|
|
3
|
+
- provisioned by aws rds aurora when in prep and prod env
|
|
4
4
|
- provisioned by docker when in test env
|
|
5
5
|
- sql-schema-control to manage sql schema
|
|
6
6
|
- sql-dao-generator to generate the sql-schemas + daos
|
|
7
|
+
|
|
8
|
+
## required variables
|
|
9
|
+
|
|
10
|
+
| variable | description | example |
|
|
11
|
+
|----------|-------------|---------|
|
|
12
|
+
| `databaseTunnelHost.{prep,prod}` | ssm proxy host for database tunnel | `aws.ssmproxy.ahbodedb.prep` |
|
|
13
|
+
| `databaseClusterHost.{prep,prod}` | rds cluster endpoint | `ahbodedb.cluster-xxx.us-east-1.rds.amazonaws.com` |
|
|
14
|
+
|
|
15
|
+
## ssm parameters
|
|
16
|
+
|
|
17
|
+
terraform provisions these ssm parameters for database credentials:
|
|
18
|
+
|
|
19
|
+
| parameter name | description | environments |
|
|
20
|
+
|----------------|-------------|--------------|
|
|
21
|
+
| `{namespace}.database.role.cicd.password` | password for cicd database user | prep, prod |
|
|
22
|
+
| `{namespace}.database.role.crud.password` | password for crud database user | prep, prod |
|
|
23
|
+
|
|
24
|
+
legacy parameters (kept for backwards compatibility):
|
|
25
|
+
- `{namespace}.database.admin.password`
|
|
26
|
+
- `{namespace}.database.service.password`
|
package/dist/practices/persist-with-rds/best-practice/provision/aws/product/parameter-store.tf
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# ====================
|
|
2
|
+
# legacy params (kept for backwards compatibility)
|
|
3
|
+
# ====================
|
|
1
4
|
resource "aws_ssm_parameter" "secret_database_admin_password" {
|
|
2
5
|
name = "${local.parameter_store_namespace}.database.admin.password"
|
|
3
6
|
type = "SecureString"
|
|
@@ -22,3 +25,39 @@ resource "aws_ssm_parameter" "secret_database_service_password" {
|
|
|
22
25
|
]
|
|
23
26
|
}
|
|
24
27
|
}
|
|
28
|
+
|
|
29
|
+
# ====================
|
|
30
|
+
# new role-based params (standard naming)
|
|
31
|
+
# ====================
|
|
32
|
+
# read legacy params to copy values (prod only)
|
|
33
|
+
data "aws_ssm_parameter" "legacy_admin_password" {
|
|
34
|
+
name = "${local.parameter_store_namespace}.database.admin.password"
|
|
35
|
+
count = var.environment == "prod" ? 1 : 0
|
|
36
|
+
with_decryption = true
|
|
37
|
+
}
|
|
38
|
+
data "aws_ssm_parameter" "legacy_service_password" {
|
|
39
|
+
name = "${local.parameter_store_namespace}.database.service.password"
|
|
40
|
+
count = var.environment == "prod" ? 1 : 0
|
|
41
|
+
with_decryption = true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
resource "aws_ssm_parameter" "secret_database_role_cicd_password" {
|
|
45
|
+
name = "${local.parameter_store_namespace}.database.role.cicd.password"
|
|
46
|
+
type = "SecureString"
|
|
47
|
+
value = var.environment == "prod" ? data.aws_ssm_parameter.legacy_admin_password[0].value : "__CHANG3_ME__"
|
|
48
|
+
tags = local.tags
|
|
49
|
+
count = var.environment != "test" ? 1 : 0
|
|
50
|
+
lifecycle {
|
|
51
|
+
ignore_changes = [value]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
resource "aws_ssm_parameter" "secret_database_role_crud_password" {
|
|
55
|
+
name = "${local.parameter_store_namespace}.database.role.crud.password"
|
|
56
|
+
type = "SecureString"
|
|
57
|
+
value = var.environment == "prod" ? data.aws_ssm_parameter.legacy_service_password[0].value : "__CHANG3_ME__"
|
|
58
|
+
tags = local.tags
|
|
59
|
+
count = var.environment != "test" ? 1 : 0
|
|
60
|
+
lifecycle {
|
|
61
|
+
ignore_changes = [value]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -13,20 +13,20 @@
|
|
|
13
13
|
##
|
|
14
14
|
## usage example:
|
|
15
15
|
## ```sh
|
|
16
|
-
## ./provision/schema/deploy.database.sh
|
|
16
|
+
## ./provision/schema/deploy.database.sh prep $(op item get ahbodedb.prep.postgres --fields label=password --format json | jq -r .value)
|
|
17
17
|
## ```
|
|
18
18
|
#####################################################
|
|
19
19
|
|
|
20
20
|
# check that user has defined the environment that they want this key created for correctly
|
|
21
21
|
ENVIRONMENT=$1;
|
|
22
|
-
if [ "$ENVIRONMENT" != "prod" ] && [ "$ENVIRONMENT" != "
|
|
23
|
-
echo "\nerror: Environment, the first argument, must be specified as either 'prod' or '
|
|
22
|
+
if [ "$ENVIRONMENT" != "prod" ] && [ "$ENVIRONMENT" != "prep" ]; then
|
|
23
|
+
echo "\nerror: Environment, the first argument, must be specified as either 'prod' or 'prep'. You specified '$ENVIRONMENT'";
|
|
24
24
|
exit 1;
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
27
|
# check that user is authed into correct account
|
|
28
28
|
AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r '.Account');
|
|
29
|
-
EXPECTED_AWS_ACCOUNT_ID=$([ "$ENVIRONMENT" = 'prod' ] && echo "@declapract{variable.awsAccountId.prod}" || echo "@declapract{variable.awsAccountId.
|
|
29
|
+
EXPECTED_AWS_ACCOUNT_ID=$([ "$ENVIRONMENT" = 'prod' ] && echo "@declapract{variable.awsAccountId.prod}" || echo "@declapract{variable.awsAccountId.prep}");
|
|
30
30
|
if [ "$AWS_ACCOUNT_ID" != "$EXPECTED_AWS_ACCOUNT_ID" ]; then
|
|
31
31
|
echo "\nerror: the AWS_ACCOUNT that you are signed into is not correct for the environment you specified. You are authed into account '$AWS_ACCOUNT_ID' but the correct account id for '$ENVIRONMENT' IS '$EXPECTED_AWS_ACCOUNT_ID'";
|
|
32
32
|
exit 1;
|
|
@@ -41,20 +41,20 @@ fi;
|
|
|
41
41
|
|
|
42
42
|
# check that the cicd password was provisioned, if in prod, since this is used to create the cicd user
|
|
43
43
|
if [ "$ENVIRONMENT" = "prod" ]; then
|
|
44
|
-
CICD_USER_PASSWORD=$(aws ssm get-parameter --name "@declapract{variable.organizationName}.@declapract{variable.projectName}.$ENVIRONMENT.database.
|
|
44
|
+
CICD_USER_PASSWORD=$(aws ssm get-parameter --name "@declapract{variable.organizationName}.@declapract{variable.projectName}.$ENVIRONMENT.database.role.cicd.password" --with-decryption --output text --query Parameter.Value)
|
|
45
45
|
if [ -z "$CICD_USER_PASSWORD" ]; then
|
|
46
46
|
echo "\nerror: CICD_USER_PASSWORD must be provisioned with terraform before running this"
|
|
47
47
|
exit 1;
|
|
48
48
|
fi;
|
|
49
|
-
if [ "$CICD_USER_PASSWORD" = "__IGNORED__" ]; then
|
|
50
|
-
echo "\nerror: CICD_USER_PASSWORD must be set to a value other than the default
|
|
49
|
+
if [ "$CICD_USER_PASSWORD" = "__IGNORED__" ] || [ "$CICD_USER_PASSWORD" = "__CHANG3_ME__" ]; then
|
|
50
|
+
echo "\nerror: CICD_USER_PASSWORD must be set to a value other than the default placeholder"
|
|
51
51
|
exit 1;
|
|
52
52
|
fi;
|
|
53
53
|
fi;
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
# define the postgres connecition string
|
|
57
|
-
CLUSTER_HOST=$([ "$ENVIRONMENT" = 'prod' ] && echo "@declapract{variable.databaseTunnelHost.prod}" || echo "@declapract{variable.databaseTunnelHost.
|
|
57
|
+
CLUSTER_HOST=$([ "$ENVIRONMENT" = 'prod' ] && echo "@declapract{variable.databaseTunnelHost.prod}" || echo "@declapract{variable.databaseTunnelHost.prep}");
|
|
58
58
|
CLUSTER_CONNECTION_STRING=postgresql://postgres:$POSTGRES_ADMIN_PASSWORD@$CLUSTER_HOST:5432
|
|
59
59
|
ROOT_DB_CONNECTION_STRING=$CLUSTER_CONNECTION_STRING/postgres
|
|
60
60
|
SVC_DB_CONNECTION_STRING=$CLUSTER_CONNECTION_STRING/@declapract{variable.databaseName}
|
|
@@ -75,7 +75,7 @@ echo "\n 🔨 creating the schema..."
|
|
|
75
75
|
psql $SVC_DB_CONNECTION_STRING -f $INIT_SQLS_DIR/.schema.sql
|
|
76
76
|
|
|
77
77
|
if [ "$ENVIRONMENT" = "prod" ]; then
|
|
78
|
-
echo "\n 🔨 granting reads to the datalakedb user..." # only in prod env; we dont want
|
|
78
|
+
echo "\n 🔨 granting reads to the datalakedb user..." # only in prod env; we dont want prep's test data in our datalake
|
|
79
79
|
psql $SVC_DB_CONNECTION_STRING -f $INIT_SQLS_DIR/.user.datalakedb.sql
|
|
80
80
|
fi;
|
|
81
81
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
provisions github repository settings and environments via declastruct-github-sdk.
|
|
2
|
+
|
|
3
|
+
## required variables
|
|
4
|
+
|
|
5
|
+
| variable | description | example |
|
|
6
|
+
|----------|-------------|---------|
|
|
7
|
+
| `reviewers.users` | github usernames for production deployment reviewers | `['whodisio', 'ehmpathy']` |
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
DeclaredGithubEnvironment,
|
|
6
6
|
DeclaredGithubRepo,
|
|
7
7
|
DeclaredGithubRepoConfig,
|
|
8
|
+
DeclaredGithubRepoRuleset,
|
|
8
9
|
DeclaredGithubTeamRepoAccess,
|
|
9
10
|
getDeclastructGithubProvider,
|
|
10
11
|
} from 'declastruct-github';
|
|
@@ -154,6 +155,37 @@ export const getResources = async (): Promise<DomainEntity<any>[]> => {
|
|
|
154
155
|
preventSelfReview: false, // self-approval allowed if in reviewers list
|
|
155
156
|
});
|
|
156
157
|
|
|
158
|
+
// restrict who may cut `v*` release tags to the rhelease app only
|
|
159
|
+
// .why = prod apply is gated on a version tag cut from main; if anyone could push a
|
|
160
|
+
// `v*` tag, that gate is bypassable. this ruleset blocks creation, update, and
|
|
161
|
+
// deletion of `v*` tags for everyone except the rhelease app — the github half
|
|
162
|
+
// of the prod-apply oidc guarantee, and the immutability of released tags
|
|
163
|
+
const rulesetReleaseTags = DeclaredGithubRepoRuleset.as({
|
|
164
|
+
repo,
|
|
165
|
+
name: 'protect-release-tags',
|
|
166
|
+
target: 'tag',
|
|
167
|
+
enforcement: 'active',
|
|
168
|
+
|
|
169
|
+
// only the rhelease github app may write `v*` tags
|
|
170
|
+
bypassActors: [
|
|
171
|
+
{
|
|
172
|
+
actorId: 2472031, // rhelease github app id (gh api /apps/rhelease)
|
|
173
|
+
actorType: 'Integration',
|
|
174
|
+
bypassMode: 'always',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
|
|
178
|
+
// applies to release tags only
|
|
179
|
+
conditions: {
|
|
180
|
+
refNameInclude: ['refs/tags/v*'],
|
|
181
|
+
refNameExclude: [],
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// enforce release-tag immutability: only the bypass actor may create, move, or delete
|
|
185
|
+
// matched tags. a released `v1.2.3` must never be re-pointed or removed once cut
|
|
186
|
+
rules: [{ type: 'creation' }, { type: 'update' }, { type: 'deletion' }],
|
|
187
|
+
});
|
|
188
|
+
|
|
157
189
|
// and return the full set
|
|
158
190
|
return [
|
|
159
191
|
repo,
|
|
@@ -162,5 +194,6 @@ export const getResources = async (): Promise<DomainEntity<any>[]> => {
|
|
|
162
194
|
teamReleasersAccess, // must come before environments that reference this team
|
|
163
195
|
envProductionOnMain,
|
|
164
196
|
envProductionOnElse,
|
|
197
|
+
rulesetReleaseTags,
|
|
165
198
|
];
|
|
166
199
|
};
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"if-env": "@declapract{check.minVersion('1.0.4')}"
|
|
6
6
|
},
|
|
7
7
|
"scripts": {
|
|
8
|
-
"deploy:prune": "SLS_STAGE=$(
|
|
9
|
-
"deploy:release": "SLS_STAGE=$(
|
|
8
|
+
"deploy:prune": "export COMMIT=$(npx sdk-environment get commit) && export SLS_STAGE=$([ \"$ACCESS\" = 'prep' ] && echo 'dev' || echo \"$ACCESS\") && npx sls prune -n 7 --stage $SLS_STAGE",
|
|
9
|
+
"deploy:release": "export COMMIT=$(npx sdk-environment get commit) && export SLS_STAGE=$([ \"$ACCESS\" = 'prep' ] && echo 'dev' || echo \"$ACCESS\") && 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
11
|
"deploy:prep": "ACCESS=prep npm run deploy:release",
|
|
12
12
|
"deploy:prod": "ACCESS=prod npm run deploy:release && npm run deploy:send-notification",
|
|
@@ -10,7 +10,7 @@ plugins:
|
|
|
10
10
|
|
|
11
11
|
custom:
|
|
12
12
|
accessByStage:
|
|
13
|
-
dev: prep
|
|
13
|
+
dev: prep # stage=dev deploys to $service-dev but uses ACCESS=prep (config/prep.json)
|
|
14
14
|
prod: prod
|
|
15
15
|
|
|
16
16
|
provider:
|
|
@@ -25,8 +25,8 @@ provider:
|
|
|
25
25
|
product: ${self:service}
|
|
26
26
|
environment:
|
|
27
27
|
TZ: UTC # guarantee that utc timezone will be used explicitly, to facilitate a pit of success
|
|
28
|
-
NODE_ENV: production # deploy with production optimizations of all resources, to make `
|
|
29
|
-
ACCESS: ${self:custom.accessByStage.${opt:stage}, 'prep'} # sdk-environment access tier, to target the correct config + resources (e.g., hit
|
|
28
|
+
NODE_ENV: production # deploy with production optimizations of all resources, to make `prep` and `prod` stage deployments equivalent functionally (i.e., the same code paths in prep and prod)
|
|
29
|
+
ACCESS: ${self:custom.accessByStage.${opt:stage}, 'prep'} # sdk-environment access tier, to target the correct config + resources (e.g., hit prep db -vs- prod db)
|
|
30
30
|
COMMIT: ${env:COMMIT} # sdk-environment commit slug, must be set by deploy command
|
|
31
31
|
AWS_NODEJS_CONNECTION_REUSE_ENABLED: true # https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-reusing-connections.html
|
|
32
32
|
deploymentBucket: serverless-deployment-@declapract{variable.infrastructureNamespaceId}-${self:provider.stage}
|
|
@@ -11,7 +11,7 @@ const accessInferencePolicy = ` # allow access inference from account name
|
|
|
11
11
|
|
|
12
12
|
const accessByStageCustom = `custom:
|
|
13
13
|
accessByStage:
|
|
14
|
-
dev: prep
|
|
14
|
+
dev: prep # stage=dev deploys to $service-dev but uses ACCESS=prep (config/prep.json)
|
|
15
15
|
prod: prod
|
|
16
16
|
|
|
17
17
|
`;
|
|
@@ -70,7 +70,7 @@ export const fix: FileFixFunction = (contents) => {
|
|
|
70
70
|
) {
|
|
71
71
|
fixed = fixed.replace(
|
|
72
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
|
|
73
|
+
"ACCESS: ${self:custom.accessByStage.${opt:stage}, 'prep'} # sdk-environment access tier, to target the correct config + resources (e.g., hit prep db -vs- prod db)\n COMMIT: ${env:COMMIT} # sdk-environment commit slug, must be set by deploy command",
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check = FileCheckType.EXISTS;
|
|
4
|
+
|
|
5
|
+
export const fix: FileFixFunction = (contents, context) => {
|
|
6
|
+
const newPath = context.relativeFilePath.replace(
|
|
7
|
+
/^provision\/aws\/environments\/dev\//,
|
|
8
|
+
'provision/aws/environments/prep/',
|
|
9
|
+
);
|
|
10
|
+
return {
|
|
11
|
+
contents: contents ?? null,
|
|
12
|
+
relativeFilePath: newPath,
|
|
13
|
+
};
|
|
14
|
+
};
|
package/dist/practices/terraform-aws/best-practice/provision/aws/environments/{dev → prep}/main.tf
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
provider "aws" {
|
|
2
2
|
region = "us-east-1"
|
|
3
|
-
allowed_account_ids = ["@declapract{variable.awsAccountId.
|
|
3
|
+
allowed_account_ids = ["@declapract{variable.awsAccountId.prep}"] # `prep` access tier resources are provisioned in the `prep` aws account
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
terraform {
|
|
@@ -14,5 +14,5 @@ terraform {
|
|
|
14
14
|
|
|
15
15
|
module "product" {
|
|
16
16
|
source = "../../product"
|
|
17
|
-
environment = "dev"
|
|
17
|
+
environment = "dev" # infrastructure stays named 'dev', application uses ACCESS=prep
|
|
18
18
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
provider "aws" {
|
|
2
2
|
region = "us-east-1"
|
|
3
|
-
allowed_account_ids = ["@declapract{variable.awsAccountId.
|
|
3
|
+
allowed_account_ids = ["@declapract{variable.awsAccountId.prep}"] # `test` environment resources are provisioned in the `prep` aws account
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
terraform {
|
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.49.
|
|
5
|
+
"version": "0.49.5",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"repository": "ehmpathy/declapract-typescript-ehmpathy",
|
|
@@ -76,13 +76,13 @@
|
|
|
76
76
|
"esbuild-register": "3.6.0",
|
|
77
77
|
"husky": "8.0.3",
|
|
78
78
|
"jest": "30.2.0",
|
|
79
|
-
"rhachet": "1.42.
|
|
79
|
+
"rhachet": "1.42.5",
|
|
80
80
|
"rhachet-brains-anthropic": "0.4.1",
|
|
81
81
|
"rhachet-brains-fireworksai": "0.1.3",
|
|
82
82
|
"rhachet-brains-xai": "0.3.3",
|
|
83
|
-
"rhachet-roles-bhrain": "0.29.
|
|
84
|
-
"rhachet-roles-bhuild": "0.21.
|
|
85
|
-
"rhachet-roles-ehmpathy": "1.37.
|
|
83
|
+
"rhachet-roles-bhrain": "0.29.15",
|
|
84
|
+
"rhachet-roles-bhuild": "0.21.19",
|
|
85
|
+
"rhachet-roles-ehmpathy": "1.37.6",
|
|
86
86
|
"tsc-alias": "1.8.10",
|
|
87
87
|
"tsx": "4.20.6",
|
|
88
88
|
"type-fns": "1.21.2",
|
|
File without changes
|
|
File without changes
|