declapract-typescript-ehmpathy 0.47.18 → 0.47.20
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/.test/infra/withApikeysContext.ts +31 -0
- package/dist/practices/cicd-common/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.json.declapract.ts +32 -0
- package/dist/practices/cicd-common/best-practice/.github/workflows/.test.yml +150 -13
- package/dist/practices/cicd-common/best-practice/.github/workflows/.test.yml.declapract.ts +91 -0
- package/dist/practices/cicd-common/best-practice/.github/workflows/test.yml.declapract.ts +40 -0
- package/dist/practices/cicd-package/best-practice/.github/workflows/publish.yml.declapract.ts +40 -0
- package/dist/practices/cicd-service/best-practice/.github/workflows/deploy.yml.declapract.ts +40 -0
- package/dist/utils/buildWorkflowSecretsBlock.ts +35 -0
- package/dist/utils/readUseApikeysConfig.ts +43 -0
- package/package.json +2 -2
- package/dist/practices/tests/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.json.declapract.ts +0 -4
- /package/dist/practices/{tests → cicd-common}/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.json +0 -0
- /package/dist/practices/{tests → cicd-common}/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.sh +0 -0
- /package/dist/practices/{tests → cicd-common}/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.sh.declapract.ts +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* .what = creates a mock context with apikeys config in a temp directory
|
|
7
|
+
* .why = enables testing buildWorkflowSecretsBlock with different apikey configs
|
|
8
|
+
*/
|
|
9
|
+
export const withApikeysContext = async (
|
|
10
|
+
input: { apikeys: string[] },
|
|
11
|
+
fn: (context: { getProjectRootDirectory: () => string }) => Promise<void>,
|
|
12
|
+
): Promise<void> => {
|
|
13
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
|
14
|
+
const configDir = path.join(
|
|
15
|
+
tempDir,
|
|
16
|
+
'.agent',
|
|
17
|
+
'repo=.this',
|
|
18
|
+
'role=any',
|
|
19
|
+
'skills',
|
|
20
|
+
);
|
|
21
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
22
|
+
fs.writeFileSync(
|
|
23
|
+
path.join(configDir, 'use.apikeys.json'),
|
|
24
|
+
JSON.stringify({ apikeys: { required: input.apikeys } }, null, 2),
|
|
25
|
+
);
|
|
26
|
+
try {
|
|
27
|
+
await fn({ getProjectRootDirectory: () => tempDir });
|
|
28
|
+
} finally {
|
|
29
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* .what = check that use.apikeys.json exists
|
|
5
|
+
* .why = enables projects to declare which api keys are required for integration tests
|
|
6
|
+
*/
|
|
7
|
+
export const check = FileCheckType.EXISTS;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* .what = creates default use.apikeys.json structure if file doesn't exist
|
|
11
|
+
* .why = ensures all projects have the config file with proper schema
|
|
12
|
+
*/
|
|
13
|
+
export const fix: FileFixFunction = (contents) => {
|
|
14
|
+
// if file already exists, preserve its content
|
|
15
|
+
if (contents) {
|
|
16
|
+
return { contents };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// create default structure
|
|
20
|
+
return {
|
|
21
|
+
contents:
|
|
22
|
+
JSON.stringify(
|
|
23
|
+
{
|
|
24
|
+
apikeys: {
|
|
25
|
+
required: [],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
null,
|
|
29
|
+
2,
|
|
30
|
+
) + '\n',
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
# run tests in parallel
|
|
26
26
|
test-commits:
|
|
27
27
|
runs-on: ubuntu-24.04
|
|
28
|
-
needs: [install]
|
|
28
|
+
needs: [install, test-shards-omit]
|
|
29
29
|
steps:
|
|
30
30
|
- name: checkout
|
|
31
31
|
uses: actions/checkout@v4
|
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
|
|
49
49
|
test-types:
|
|
50
50
|
runs-on: ubuntu-24.04
|
|
51
|
-
needs: [install]
|
|
51
|
+
needs: [install, test-shards-omit]
|
|
52
52
|
steps:
|
|
53
53
|
- name: checkout
|
|
54
54
|
uses: actions/checkout@v4
|
|
@@ -69,7 +69,7 @@ jobs:
|
|
|
69
69
|
|
|
70
70
|
test-format:
|
|
71
71
|
runs-on: ubuntu-24.04
|
|
72
|
-
needs: [install]
|
|
72
|
+
needs: [install, test-shards-omit]
|
|
73
73
|
steps:
|
|
74
74
|
- name: checkout
|
|
75
75
|
uses: actions/checkout@v4
|
|
@@ -93,7 +93,7 @@ jobs:
|
|
|
93
93
|
|
|
94
94
|
test-lint:
|
|
95
95
|
runs-on: ubuntu-24.04
|
|
96
|
-
needs: [install]
|
|
96
|
+
needs: [install, test-shards-omit]
|
|
97
97
|
steps:
|
|
98
98
|
- name: checkout
|
|
99
99
|
uses: actions/checkout@v4
|
|
@@ -114,7 +114,7 @@ jobs:
|
|
|
114
114
|
|
|
115
115
|
test-unit:
|
|
116
116
|
runs-on: ubuntu-24.04
|
|
117
|
-
needs: [install]
|
|
117
|
+
needs: [install, test-shards-omit]
|
|
118
118
|
steps:
|
|
119
119
|
- name: checkout
|
|
120
120
|
uses: actions/checkout@v4
|
|
@@ -133,9 +133,56 @@ jobs:
|
|
|
133
133
|
- name: test:unit
|
|
134
134
|
run: THOROUGH=true npm run test:unit
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
# shard setup job
|
|
137
|
+
enshard:
|
|
138
|
+
name: enshard
|
|
139
|
+
runs-on: ubuntu-24.04
|
|
140
|
+
outputs:
|
|
141
|
+
integration-matrix: ${{ steps.integration.outputs.matrix }}
|
|
142
|
+
integration-patterns: ${{ steps.integration.outputs.patterns }}
|
|
143
|
+
acceptance-matrix: ${{ steps.acceptance.outputs.matrix }}
|
|
144
|
+
acceptance-patterns: ${{ steps.acceptance.outputs.patterns }}
|
|
145
|
+
steps:
|
|
146
|
+
- name: checkout
|
|
147
|
+
uses: actions/checkout@v4
|
|
148
|
+
with:
|
|
149
|
+
sparse-checkout: |
|
|
150
|
+
jest.integration.shards.jsonc
|
|
151
|
+
jest.acceptance.shards.jsonc
|
|
152
|
+
.github/actions/test-shards-setup
|
|
153
|
+
sparse-checkout-cone-mode: false
|
|
154
|
+
|
|
155
|
+
- name: setup integration shards
|
|
156
|
+
id: integration
|
|
157
|
+
uses: ./.github/actions/test-shards-setup
|
|
158
|
+
with:
|
|
159
|
+
config-file: jest.integration.shards.jsonc
|
|
160
|
+
test-type: integration
|
|
161
|
+
|
|
162
|
+
- name: setup acceptance shards
|
|
163
|
+
id: acceptance
|
|
164
|
+
uses: ./.github/actions/test-shards-setup
|
|
165
|
+
with:
|
|
166
|
+
config-file: jest.acceptance.shards.jsonc
|
|
167
|
+
test-type: acceptance
|
|
168
|
+
|
|
169
|
+
# placeholder job for github actions visualization alignment
|
|
170
|
+
# note: github actions has no column position controls;
|
|
171
|
+
# this job pushes test-* jobs one column right to align with test-shards-* column
|
|
172
|
+
test-shards-omit:
|
|
137
173
|
runs-on: ubuntu-24.04
|
|
138
174
|
needs: [install]
|
|
175
|
+
steps:
|
|
176
|
+
- run: echo "👌 placeholder for column alignment"
|
|
177
|
+
|
|
178
|
+
# shard runner jobs
|
|
179
|
+
test-shards-integration:
|
|
180
|
+
runs-on: ubuntu-24.04
|
|
181
|
+
needs: [install, enshard]
|
|
182
|
+
strategy:
|
|
183
|
+
fail-fast: false
|
|
184
|
+
matrix:
|
|
185
|
+
shard: ${{ fromJson(needs.enshard.outputs.integration-matrix) }}
|
|
139
186
|
steps:
|
|
140
187
|
- name: checkout
|
|
141
188
|
uses: actions/checkout@v4
|
|
@@ -164,12 +211,44 @@ jobs:
|
|
|
164
211
|
- name: start:livedb:dev
|
|
165
212
|
run: npm run start:livedb:dev --if-present
|
|
166
213
|
|
|
167
|
-
- name:
|
|
168
|
-
run:
|
|
169
|
-
|
|
170
|
-
|
|
214
|
+
- name: build
|
|
215
|
+
run: npm run build
|
|
216
|
+
|
|
217
|
+
- name: test:integration (explicit ${{ matrix.shard.index }})
|
|
218
|
+
if: matrix.shard.type == 'explicit'
|
|
219
|
+
run: |
|
|
220
|
+
patterns='${{ join(matrix.shard.patterns, '|') }}'
|
|
221
|
+
THOROUGH=true npm run test:integration -- --testPathPattern="$patterns" --json --outputFile=jest-results.json
|
|
222
|
+
|
|
223
|
+
- name: test:integration (dynamic ${{ matrix.shard.shard }}/${{ matrix.shard.total }})
|
|
224
|
+
if: matrix.shard.type == 'dynamic'
|
|
225
|
+
run: |
|
|
226
|
+
exclude='${{ needs.enshard.outputs.integration-patterns }}'
|
|
227
|
+
if [[ -n "$exclude" ]]; then
|
|
228
|
+
THOROUGH=true npm run test:integration -- --testPathIgnorePatterns="$exclude" --shard=${{ matrix.shard.shard }}/${{ matrix.shard.total }} --json --outputFile=jest-results.json
|
|
229
|
+
else
|
|
230
|
+
THOROUGH=true npm run test:integration -- --shard=${{ matrix.shard.shard }}/${{ matrix.shard.total }} --json --outputFile=jest-results.json
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
- name: report slow tests
|
|
234
|
+
if: always() && hashFiles('jest-results.json') != ''
|
|
235
|
+
run: |
|
|
236
|
+
jq -r '.testResults[] | "\(.name):\(.perfStats.runtime)"' jest-results.json | while IFS=: read -r name runtime; do
|
|
237
|
+
seconds=$((runtime / 1000))
|
|
238
|
+
if [[ $seconds -ge 30 ]]; then
|
|
239
|
+
echo "::warning file=${name}::slow test: ${seconds}s"
|
|
240
|
+
elif [[ $seconds -ge 10 ]]; then
|
|
241
|
+
echo "::notice file=${name}::test duration: ${seconds}s"
|
|
242
|
+
fi
|
|
243
|
+
done
|
|
244
|
+
|
|
245
|
+
test-shards-acceptance:
|
|
171
246
|
runs-on: ubuntu-24.04
|
|
172
|
-
needs: [install]
|
|
247
|
+
needs: [install, enshard]
|
|
248
|
+
strategy:
|
|
249
|
+
fail-fast: false
|
|
250
|
+
matrix:
|
|
251
|
+
shard: ${{ fromJson(needs.enshard.outputs.acceptance-matrix) }}
|
|
173
252
|
steps:
|
|
174
253
|
- name: checkout
|
|
175
254
|
uses: actions/checkout@v4
|
|
@@ -198,5 +277,63 @@ jobs:
|
|
|
198
277
|
- name: start:livedb:dev
|
|
199
278
|
run: npm run start:livedb:dev --if-present
|
|
200
279
|
|
|
201
|
-
- name:
|
|
202
|
-
run:
|
|
280
|
+
- name: build
|
|
281
|
+
run: npm run build
|
|
282
|
+
|
|
283
|
+
- name: test:acceptance (explicit ${{ matrix.shard.index }})
|
|
284
|
+
if: matrix.shard.type == 'explicit'
|
|
285
|
+
run: |
|
|
286
|
+
patterns='${{ join(matrix.shard.patterns, '|') }}'
|
|
287
|
+
THOROUGH=true npm run test:acceptance -- --testPathPattern="$patterns" --json --outputFile=jest-results.json
|
|
288
|
+
|
|
289
|
+
- name: test:acceptance (dynamic ${{ matrix.shard.shard }}/${{ matrix.shard.total }})
|
|
290
|
+
if: matrix.shard.type == 'dynamic'
|
|
291
|
+
run: |
|
|
292
|
+
exclude='${{ needs.enshard.outputs.acceptance-patterns }}'
|
|
293
|
+
if [[ -n "$exclude" ]]; then
|
|
294
|
+
THOROUGH=true npm run test:acceptance -- --testPathIgnorePatterns="$exclude" --shard=${{ matrix.shard.shard }}/${{ matrix.shard.total }} --json --outputFile=jest-results.json
|
|
295
|
+
else
|
|
296
|
+
THOROUGH=true npm run test:acceptance -- --shard=${{ matrix.shard.shard }}/${{ matrix.shard.total }} --json --outputFile=jest-results.json
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
- name: report slow tests
|
|
300
|
+
if: always() && hashFiles('jest-results.json') != ''
|
|
301
|
+
run: |
|
|
302
|
+
jq -r '.testResults[] | "\(.name):\(.perfStats.runtime)"' jest-results.json | while IFS=: read -r name runtime; do
|
|
303
|
+
seconds=$((runtime / 1000))
|
|
304
|
+
if [[ $seconds -ge 30 ]]; then
|
|
305
|
+
echo "::warning file=${name}::slow test: ${seconds}s"
|
|
306
|
+
elif [[ $seconds -ge 10 ]]; then
|
|
307
|
+
echo "::notice file=${name}::test duration: ${seconds}s"
|
|
308
|
+
fi
|
|
309
|
+
done
|
|
310
|
+
|
|
311
|
+
# union job: aggregates shard results into single status for pr merge constraints
|
|
312
|
+
test-integration:
|
|
313
|
+
runs-on: ubuntu-24.04
|
|
314
|
+
needs: [install, enshard, test-shards-integration]
|
|
315
|
+
if: always() && needs.install.result == 'success' && needs.enshard.result == 'success'
|
|
316
|
+
steps:
|
|
317
|
+
- name: report shard results
|
|
318
|
+
run: |
|
|
319
|
+
if [[ "${{ needs.test-shards-integration.result }}" == "success" ]]; then
|
|
320
|
+
echo "👌 all integration test shards passed"
|
|
321
|
+
else
|
|
322
|
+
echo "::error::some integration test shards failed"
|
|
323
|
+
exit 1
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
# union job: aggregates shard results into single status for pr merge constraints
|
|
327
|
+
test-acceptance-locally:
|
|
328
|
+
runs-on: ubuntu-24.04
|
|
329
|
+
needs: [install, enshard, test-shards-acceptance]
|
|
330
|
+
if: always() && needs.install.result == 'success' && needs.enshard.result == 'success'
|
|
331
|
+
steps:
|
|
332
|
+
- name: report shard results
|
|
333
|
+
run: |
|
|
334
|
+
if [[ "${{ needs.test-shards-acceptance.result }}" == "success" ]]; then
|
|
335
|
+
echo "👌 all acceptance test shards passed"
|
|
336
|
+
else
|
|
337
|
+
echo "::error::some acceptance test shards failed"
|
|
338
|
+
exit 1
|
|
339
|
+
fi
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { FileCheckFunction, FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
import { readUseApikeysConfig } from '../../../../../utils/readUseApikeysConfig';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* .what = builds the expected .test.yml content with apikey secrets injected
|
|
7
|
+
* .why = single source of truth for both check and fix
|
|
8
|
+
*/
|
|
9
|
+
export const buildExpectedContent = (input: {
|
|
10
|
+
template: string;
|
|
11
|
+
apikeys: string[];
|
|
12
|
+
}): string => {
|
|
13
|
+
let result = input.template;
|
|
14
|
+
|
|
15
|
+
// if no apikeys, return template as-is
|
|
16
|
+
if (!input.apikeys.length) {
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// build secrets declaration block for workflow_call
|
|
21
|
+
const secretsDeclaration = input.apikeys
|
|
22
|
+
.map(
|
|
23
|
+
(key) =>
|
|
24
|
+
` ${key}:\n description: "api key for ${key.toLowerCase().replace(/_/g, ' ')}"\n required: false`,
|
|
25
|
+
)
|
|
26
|
+
.join('\n');
|
|
27
|
+
|
|
28
|
+
// build env block for test-integration job
|
|
29
|
+
const envBlock = input.apikeys
|
|
30
|
+
.map((key) => ` ${key}: \${{ secrets.${key} }}`)
|
|
31
|
+
.join('\n');
|
|
32
|
+
|
|
33
|
+
// insert secrets declaration after workflow_call inputs
|
|
34
|
+
// look for the pattern: inputs: ... (multiline) followed by blank line or permissions:
|
|
35
|
+
result = result.replace(
|
|
36
|
+
/(on:\n workflow_call:\n inputs:\n(?: [^\n]+\n)+)/,
|
|
37
|
+
`$1 secrets:\n${secretsDeclaration}\n\n`,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// insert env block to test-integration job
|
|
41
|
+
// add env block after needs: [install] line
|
|
42
|
+
result = result.replace(
|
|
43
|
+
/(test-integration:\n runs-on: ubuntu-24\.04\n needs: \[install\]\n)/,
|
|
44
|
+
`$1 env:\n${envBlock}\n`,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* .what = ensures .test.yml matches expected content with apikey secrets
|
|
52
|
+
* .why = enables integration tests to access required api keys via github secrets
|
|
53
|
+
*/
|
|
54
|
+
export const check: FileCheckFunction = async (contents, context) => {
|
|
55
|
+
// read apikeys from project
|
|
56
|
+
const apikeysConfig = await readUseApikeysConfig({
|
|
57
|
+
projectRootDirectory: context.getProjectRootDirectory(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// build expected content from template + apikeys
|
|
61
|
+
const expected = buildExpectedContent({
|
|
62
|
+
template: context.declaredFileContents ?? '',
|
|
63
|
+
apikeys: apikeysConfig?.apikeys?.required ?? [],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// if contents match expected, file passes (throw)
|
|
67
|
+
if (contents === expected) {
|
|
68
|
+
throw new Error('file matches expected content');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// return = file differs from expected (bad practice detected)
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* .what = fixes .test.yml to include apikey secrets declaration and env vars
|
|
76
|
+
* .why = ensures integration tests have access to required api keys
|
|
77
|
+
*/
|
|
78
|
+
export const fix: FileFixFunction = async (_contents, context) => {
|
|
79
|
+
// read apikeys from project
|
|
80
|
+
const apikeysConfig = await readUseApikeysConfig({
|
|
81
|
+
projectRootDirectory: context.getProjectRootDirectory(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// build expected content from template + apikeys
|
|
85
|
+
const expected = buildExpectedContent({
|
|
86
|
+
template: context.declaredFileContents ?? '',
|
|
87
|
+
apikeys: apikeysConfig?.apikeys?.required ?? [],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { contents: expected };
|
|
91
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FileCheckFunction, FileFixFunction } from 'declapract';
|
|
2
|
+
import { UnexpectedCodePathError } from 'helpful-errors';
|
|
3
|
+
|
|
4
|
+
import { buildWorkflowSecretsBlock } from '../../../../../utils/buildWorkflowSecretsBlock';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* .what = ensures test.yml matches expected content with apikey secrets block
|
|
8
|
+
* .why = enables the reusable workflow to access github secrets
|
|
9
|
+
*/
|
|
10
|
+
export const check: FileCheckFunction = async (contents, context) => {
|
|
11
|
+
// fail fast if template not found
|
|
12
|
+
if (!context.declaredFileContents)
|
|
13
|
+
throw new UnexpectedCodePathError('declaredFileContents not found', {
|
|
14
|
+
relativeFilePath: context.relativeFilePath,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const expected = await buildWorkflowSecretsBlock(
|
|
18
|
+
{ template: context.declaredFileContents },
|
|
19
|
+
context,
|
|
20
|
+
);
|
|
21
|
+
if (contents === expected) throw new Error('file matches expected content');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* .what = fixes test.yml to include apikey secrets block
|
|
26
|
+
* .why = ensures the reusable workflow receives required api keys
|
|
27
|
+
*/
|
|
28
|
+
export const fix: FileFixFunction = async (_contents, context) => {
|
|
29
|
+
// fail fast if template not found
|
|
30
|
+
if (!context.declaredFileContents)
|
|
31
|
+
throw new UnexpectedCodePathError('declaredFileContents not found', {
|
|
32
|
+
relativeFilePath: context.relativeFilePath,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const expected = await buildWorkflowSecretsBlock(
|
|
36
|
+
{ template: context.declaredFileContents },
|
|
37
|
+
context,
|
|
38
|
+
);
|
|
39
|
+
return { contents: expected };
|
|
40
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FileCheckFunction, FileFixFunction } from 'declapract';
|
|
2
|
+
import { UnexpectedCodePathError } from 'helpful-errors';
|
|
3
|
+
|
|
4
|
+
import { buildWorkflowSecretsBlock } from '../../../../../utils/buildWorkflowSecretsBlock';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* .what = ensures publish.yml matches expected content with apikey secrets block
|
|
8
|
+
* .why = enables the reusable workflow to access github secrets during publish
|
|
9
|
+
*/
|
|
10
|
+
export const check: FileCheckFunction = async (contents, context) => {
|
|
11
|
+
// fail fast if template not found
|
|
12
|
+
if (!context.declaredFileContents)
|
|
13
|
+
throw new UnexpectedCodePathError('declaredFileContents not found', {
|
|
14
|
+
relativeFilePath: context.relativeFilePath,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const expected = await buildWorkflowSecretsBlock(
|
|
18
|
+
{ template: context.declaredFileContents },
|
|
19
|
+
context,
|
|
20
|
+
);
|
|
21
|
+
if (contents === expected) throw new Error('file matches expected content');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* .what = fixes publish.yml to include apikey secrets block
|
|
26
|
+
* .why = ensures the reusable workflow receives required api keys during publish
|
|
27
|
+
*/
|
|
28
|
+
export const fix: FileFixFunction = async (_contents, context) => {
|
|
29
|
+
// fail fast if template not found
|
|
30
|
+
if (!context.declaredFileContents)
|
|
31
|
+
throw new UnexpectedCodePathError('declaredFileContents not found', {
|
|
32
|
+
relativeFilePath: context.relativeFilePath,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const expected = await buildWorkflowSecretsBlock(
|
|
36
|
+
{ template: context.declaredFileContents },
|
|
37
|
+
context,
|
|
38
|
+
);
|
|
39
|
+
return { contents: expected };
|
|
40
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FileCheckFunction, FileFixFunction } from 'declapract';
|
|
2
|
+
import { UnexpectedCodePathError } from 'helpful-errors';
|
|
3
|
+
|
|
4
|
+
import { buildWorkflowSecretsBlock } from '../../../../../utils/buildWorkflowSecretsBlock';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* .what = ensures deploy.yml matches expected content with apikey secrets block
|
|
8
|
+
* .why = enables the reusable workflow to access github secrets during deploy
|
|
9
|
+
*/
|
|
10
|
+
export const check: FileCheckFunction = async (contents, context) => {
|
|
11
|
+
// fail fast if template not found
|
|
12
|
+
if (!context.declaredFileContents)
|
|
13
|
+
throw new UnexpectedCodePathError('declaredFileContents not found', {
|
|
14
|
+
relativeFilePath: context.relativeFilePath,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const expected = await buildWorkflowSecretsBlock(
|
|
18
|
+
{ template: context.declaredFileContents },
|
|
19
|
+
context,
|
|
20
|
+
);
|
|
21
|
+
if (contents === expected) throw new Error('file matches expected content');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* .what = fixes deploy.yml to include apikey secrets block
|
|
26
|
+
* .why = enables the reusable workflow receives required api keys during deploy
|
|
27
|
+
*/
|
|
28
|
+
export const fix: FileFixFunction = async (_contents, context) => {
|
|
29
|
+
// fail fast if template not found
|
|
30
|
+
if (!context.declaredFileContents)
|
|
31
|
+
throw new UnexpectedCodePathError('declaredFileContents not found', {
|
|
32
|
+
relativeFilePath: context.relativeFilePath,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const expected = await buildWorkflowSecretsBlock(
|
|
36
|
+
{ template: context.declaredFileContents },
|
|
37
|
+
context,
|
|
38
|
+
);
|
|
39
|
+
return { contents: expected };
|
|
40
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readUseApikeysConfig } from './readUseApikeysConfig';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* .what = builds workflow content with apikey secrets block for .test.yml
|
|
5
|
+
* .why = single source of truth for test.yml, publish.yml, deploy.yml check+fix
|
|
6
|
+
*/
|
|
7
|
+
export const buildWorkflowSecretsBlock = async (
|
|
8
|
+
input: { template: string },
|
|
9
|
+
context: { getProjectRootDirectory: () => string },
|
|
10
|
+
): Promise<string> => {
|
|
11
|
+
// read apikeys from project
|
|
12
|
+
const apikeysConfig = await readUseApikeysConfig({
|
|
13
|
+
projectRootDirectory: context.getProjectRootDirectory(),
|
|
14
|
+
});
|
|
15
|
+
const apikeys = apikeysConfig?.apikeys?.required ?? [];
|
|
16
|
+
|
|
17
|
+
// if no apikeys, return template as-is
|
|
18
|
+
if (!apikeys.length) {
|
|
19
|
+
return input.template;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// build secrets block
|
|
23
|
+
const secretsBlock = apikeys
|
|
24
|
+
.map((key) => ` ${key}: \${{ secrets.${key} }}`)
|
|
25
|
+
.join('\n');
|
|
26
|
+
|
|
27
|
+
// find jobs that call .test.yml with 'with:' block and add secrets block after
|
|
28
|
+
// handle cases with optional 'if:' before 'with:'
|
|
29
|
+
const result = input.template.replace(
|
|
30
|
+
/(uses: \.\/\.github\/workflows\/\.test\.yml\n(?: if: [^\n]+\n)? with:\n(?: [^\n]+\n)+)/g,
|
|
31
|
+
`$1 secrets:\n${secretsBlock}\n`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import util from 'util';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* .what = configuration structure for use.apikeys.json
|
|
7
|
+
* .why = defines which api keys are required for integration tests
|
|
8
|
+
*/
|
|
9
|
+
export interface UseApikeysConfig {
|
|
10
|
+
apikeys: {
|
|
11
|
+
required: string[];
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const USE_APIKEYS_PATH = '.agent/repo=.this/role=any/skills/use.apikeys.json';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* .what = reads and parses use.apikeys.json from target project
|
|
19
|
+
* .why = centralizes apikey config access for all workflow practices
|
|
20
|
+
*/
|
|
21
|
+
export const readUseApikeysConfig = async (input: {
|
|
22
|
+
projectRootDirectory: string;
|
|
23
|
+
}): Promise<UseApikeysConfig | null> => {
|
|
24
|
+
// build full path to config file
|
|
25
|
+
const configPath = path.join(input.projectRootDirectory, USE_APIKEYS_PATH);
|
|
26
|
+
|
|
27
|
+
// check if file exists
|
|
28
|
+
const exists = await util
|
|
29
|
+
.promisify(fs.access)(configPath, fs.constants.F_OK)
|
|
30
|
+
.then(() => true)
|
|
31
|
+
.catch(() => false);
|
|
32
|
+
if (!exists) return null;
|
|
33
|
+
|
|
34
|
+
// read and parse the file
|
|
35
|
+
try {
|
|
36
|
+
const contents = await util.promisify(fs.readFile)(configPath, 'utf-8');
|
|
37
|
+
const parsed = JSON.parse(contents) as UseApikeysConfig;
|
|
38
|
+
return parsed;
|
|
39
|
+
} catch {
|
|
40
|
+
// handle malformed json gracefully
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
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.20",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"repository": "ehmpathy/declapract-typescript-ehmpathy",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"husky": "8.0.3",
|
|
77
77
|
"jest": "30.2.0",
|
|
78
78
|
"rhachet": "1.20.5",
|
|
79
|
-
"rhachet-roles-bhrain": "0.5.
|
|
79
|
+
"rhachet-roles-bhrain": "0.5.9",
|
|
80
80
|
"rhachet-roles-bhuild": "0.5.6",
|
|
81
81
|
"rhachet-roles-ehmpathy": "1.17.11",
|
|
82
82
|
"tsc-alias": "1.8.10",
|
|
File without changes
|
|
File without changes
|