declapract-typescript-ehmpathy 0.49.2 → 0.49.4
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/node-service/bad-practices/simple-lambda-client/.declapract.readme.md +55 -0
- package/dist/practices/node-service/bad-practices/simple-lambda-client/package.json +5 -0
- package/dist/practices/node-service/bad-practices/simple-lambda-client/package.json.declapract.ts +27 -0
- package/dist/practices/node-service/bad-practices/simple-lambda-client/src/<star><star>/<star>.ts.declapract.ts +44 -0
- package/dist/practices/node-service/bad-practices/simple-lambda-handlers/.declapract.readme.md +52 -0
- package/dist/practices/node-service/bad-practices/simple-lambda-handlers/package.json +5 -0
- package/dist/practices/node-service/bad-practices/simple-lambda-handlers/package.json.declapract.ts +27 -0
- package/dist/practices/node-service/bad-practices/simple-lambda-handlers/src/<star><star>/<star>.ts.declapract.ts +50 -0
- package/dist/practices/node-service/best-practice/package.json +4 -1
- 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/rhachet/bad-practices/review-peer-artifacts/.behavior/<star><star>/.reviews/<star><star>/<star>peer-review<star>.md.declapract.ts +16 -0
- package/dist/practices/rhachet/bad-practices/review-peer-artifacts/.behavior/<star><star>/.reviews/peer/<star><star>/<star>.md.declapract.ts +16 -0
- package/dist/practices/rhachet/bad-practices/review-peer-artifacts/.declapract.readme.md +12 -0
- package/dist/practices/{runtime-type-checking → runtime-schemas}/bad-practices/joi/package.json.declapract.ts +4 -0
- package/dist/practices/runtime-schemas/bad-practices/joi/src/<star><star>/<star>.ts.declapract.ts +50 -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/dist/useCases.yml +1 -3
- package/package.json +6 -5
- package/dist/practices/lambda-clients/bad-practices/lambda-service-client-package/.declapract.readme.md +0 -1
- package/dist/practices/lambda-clients/bad-practices/lambda-service-client-package/package.json +0 -5
- package/dist/practices/lambda-clients/bad-practices/lambda-service-client-package/package.json.declapract.ts +0 -16
- package/dist/practices/lambda-clients/best-practice/package.json +0 -5
- package/dist/practices/lambda-clients/best-practice/src/data/clients/<star>.ts +0 -1
- package/dist/practices/lambda-clients/best-practice/src/data/clients/<star>.ts.declapract.ts +0 -3
- package/dist/practices/lambda-handlers/best-practice/package.json +0 -6
- package/dist/practices/lambda-handlers/best-practice/package.json.declapract.ts +0 -3
- package/dist/practices/lambda-handlers/best-practice/src/contract/handlers/<star><star>/<star>.declapract.ts +0 -3
- package/dist/practices/runtime-type-checking/bad-practices/joi-types/package.json +0 -5
- package/dist/practices/runtime-type-checking/bad-practices/joi-types/package.json.declapract.ts +0 -18
- package/dist/practices/runtime-type-checking/bad-practices/old-joi-syntax-valid-input-cant-be-array/.declapract.readme.md +0 -1
- package/dist/practices/runtime-type-checking/bad-practices/old-joi-syntax-valid-input-cant-be-array/src/<star><star>/<star>.ts.declapract.ts +0 -15
- package/dist/practices/runtime-type-checking/best-practice/package.json.declapract.ts +0 -3
- /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/{runtime-type-checking → runtime-schemas}/bad-practices/joi/.declapract.readme.md +0 -0
- /package/dist/practices/{runtime-type-checking → runtime-schemas}/bad-practices/joi/package.json +0 -0
- /package/dist/practices/{runtime-type-checking → runtime-schemas}/best-practice/package.json +0 -0
- /package/dist/practices/{lambda-clients → runtime-schemas}/best-practice/package.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
|
+
});
|