declapract-typescript-ehmpathy 0.47.57 → 0.47.58
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/withKeyrackContext.ts +50 -0
- package/dist/practices/cicd-common/bad-practices/old-use-apikeys/.agent/repo=.this/role=any/skills/use.apikeys.json.declapract.ts +7 -0
- package/dist/practices/cicd-common/bad-practices/old-use-apikeys/.agent/repo=.this/role=any/skills/use.apikeys.sh.declapract.ts +7 -0
- package/dist/practices/cicd-common/best-practice/.github/workflows/.test.yml +10 -0
- package/dist/practices/cicd-common/best-practice/.github/workflows/.test.yml.declapract.ts +45 -22
- package/dist/practices/tests/best-practice/jest.acceptance.env.ts +7 -31
- package/dist/practices/tests/best-practice/jest.integration.env.ts +9 -32
- package/dist/practices/tests/best-practice/package.json +1 -2
- package/dist/utils/buildWorkflowSecretsBlock.ts +27 -16
- package/package.json +7 -7
- package/dist/.test/infra/withApikeysContext.ts +0 -31
- package/dist/practices/cicd-common/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.json +0 -5
- package/dist/practices/cicd-common/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.json.declapract.ts +0 -32
- package/dist/practices/cicd-common/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.sh +0 -63
- package/dist/practices/cicd-common/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.sh.declapract.ts +0 -19
- package/dist/utils/readUseApikeysConfig.ts +0 -43
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import type { KeyrackGrantAttempt } from 'rhachet/keyrack';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* .what = creates a mock context with keyrack config in a temp directory
|
|
9
|
+
* .why = enables test of buildWorkflowSecretsBlock with different keyrack configs
|
|
10
|
+
*/
|
|
11
|
+
export const withKeyrackContext = async (
|
|
12
|
+
input: { keys: string[] },
|
|
13
|
+
fn: (context: { getProjectRootDirectory: () => string }) => Promise<void>,
|
|
14
|
+
): Promise<void> => {
|
|
15
|
+
// create temp dir with keyrack.yml
|
|
16
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
|
17
|
+
const agentDir = path.join(tempDir, '.agent');
|
|
18
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
path.join(agentDir, 'keyrack.yml'),
|
|
21
|
+
`org: test\nenv.test:\n${input.keys.map((k) => ` - ${k}`).join('\n')}`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// mock keyrack.get to return grant attempts
|
|
25
|
+
const mockGrants: KeyrackGrantAttempt[] = input.keys.map((key) => ({
|
|
26
|
+
status: 'granted' as const,
|
|
27
|
+
grant: {
|
|
28
|
+
slug: `test.test.${key}`,
|
|
29
|
+
value: 'mock-value',
|
|
30
|
+
mech: 'PERMANENT_VIA_REPLICA' as const,
|
|
31
|
+
vault: 'os.direct' as const,
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// store original module
|
|
36
|
+
const keyrackModule = await import('rhachet/keyrack');
|
|
37
|
+
const originalGet = keyrackModule.keyrack.get;
|
|
38
|
+
|
|
39
|
+
// replace with mock
|
|
40
|
+
(keyrackModule.keyrack as { get: typeof originalGet }).get = async () =>
|
|
41
|
+
mockGrants;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await fn({ getProjectRootDirectory: () => tempDir });
|
|
45
|
+
} finally {
|
|
46
|
+
// restore original
|
|
47
|
+
(keyrackModule.keyrack as { get: typeof originalGet }).get = originalGet;
|
|
48
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -201,6 +201,11 @@ jobs:
|
|
|
201
201
|
path: ./node_modules
|
|
202
202
|
key: ${{ needs.install.outputs.node-modules-cache-key }}
|
|
203
203
|
|
|
204
|
+
# .why = keyrack.yml can extend other manifests via symlinks (e.g., .agent/repo=ehmpathy/role=mechanic/keyrack.yml)
|
|
205
|
+
# prepare:rhachet creates these symlinks so keyrack.source() can hydrate extended keys
|
|
206
|
+
- name: prepare:rhachet
|
|
207
|
+
run: npm run prepare:rhachet --if-present
|
|
208
|
+
|
|
204
209
|
- name: get aws auth, if creds supplied
|
|
205
210
|
if: ${{ inputs.creds-aws-role-arn }}
|
|
206
211
|
uses: aws-actions/configure-aws-credentials@v4
|
|
@@ -270,6 +275,11 @@ jobs:
|
|
|
270
275
|
path: ./node_modules
|
|
271
276
|
key: ${{ needs.install.outputs.node-modules-cache-key }}
|
|
272
277
|
|
|
278
|
+
# .why = keyrack.yml can extend other manifests via symlinks (e.g., .agent/repo=ehmpathy/role=mechanic/keyrack.yml)
|
|
279
|
+
# prepare:rhachet creates these symlinks so keyrack.source() can hydrate extended keys
|
|
280
|
+
- name: prepare:rhachet
|
|
281
|
+
run: npm run prepare:rhachet --if-present
|
|
282
|
+
|
|
273
283
|
- name: get aws auth, if creds supplied
|
|
274
284
|
if: ${{ inputs.creds-aws-role-arn }}
|
|
275
285
|
uses: aws-actions/configure-aws-credentials@v4
|
|
@@ -1,24 +1,49 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
1
4
|
import type { FileCheckFunction, FileFixFunction } from 'declapract';
|
|
5
|
+
import { keyrack, type KeyrackGrantAttempt } from 'rhachet/keyrack';
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
/**
|
|
8
|
+
* .what = gets keyrack key names for test env from target project
|
|
9
|
+
* .why = single source of truth for key discovery
|
|
10
|
+
*/
|
|
11
|
+
const getKeyrackKeys = async (projectRootDir: string): Promise<string[]> => {
|
|
12
|
+
const keyrackYmlPath = join(projectRootDir, '.agent/keyrack.yml');
|
|
13
|
+
if (!existsSync(keyrackYmlPath)) return [];
|
|
14
|
+
|
|
15
|
+
const keys = (await keyrack.get({
|
|
16
|
+
for: { repo: true },
|
|
17
|
+
env: 'test',
|
|
18
|
+
})) as KeyrackGrantAttempt[];
|
|
19
|
+
if (!keys.length) return [];
|
|
20
|
+
|
|
21
|
+
// extract key names from slugs (format: org.env.KEY_NAME)
|
|
22
|
+
// .note = KeyrackGrantAttempt is a union of 4 variants (granted/absent/locked/blocked)
|
|
23
|
+
// per rhachet/dist/domain.objects/keyrack/KeyrackGrantAttempt.d.ts:8
|
|
24
|
+
// granted has slug at grant.slug; others have slug at top level
|
|
25
|
+
return keys.map((k) =>
|
|
26
|
+
(k.status === 'granted' ? k.grant.slug : k.slug).split('.').pop()!,
|
|
27
|
+
);
|
|
28
|
+
};
|
|
4
29
|
|
|
5
30
|
/**
|
|
6
|
-
* .what = builds the expected .test.yml content with
|
|
31
|
+
* .what = builds the expected .test.yml content with keyrack secrets injected
|
|
7
32
|
* .why = single source of truth for both check and fix
|
|
8
33
|
*/
|
|
9
34
|
export const buildExpectedContent = (input: {
|
|
10
35
|
template: string;
|
|
11
|
-
|
|
36
|
+
keys: string[];
|
|
12
37
|
}): string => {
|
|
13
38
|
let result = input.template;
|
|
14
39
|
|
|
15
|
-
// if no
|
|
16
|
-
if (!input.
|
|
40
|
+
// if no keys, return template as-is
|
|
41
|
+
if (!input.keys.length) {
|
|
17
42
|
return result;
|
|
18
43
|
}
|
|
19
44
|
|
|
20
45
|
// build secrets declaration block for workflow_call
|
|
21
|
-
const secretsDeclaration = input.
|
|
46
|
+
const secretsDeclaration = input.keys
|
|
22
47
|
.map(
|
|
23
48
|
(key) =>
|
|
24
49
|
` ${key}:\n description: "api key for ${key.toLowerCase().replace(/_/g, ' ')}"\n required: false`,
|
|
@@ -26,7 +51,7 @@ export const buildExpectedContent = (input: {
|
|
|
26
51
|
.join('\n');
|
|
27
52
|
|
|
28
53
|
// build env block for test-integration job
|
|
29
|
-
const envBlock = input.
|
|
54
|
+
const envBlock = input.keys
|
|
30
55
|
.map((key) => ` ${key}: \${{ secrets.${key} }}`)
|
|
31
56
|
.join('\n');
|
|
32
57
|
|
|
@@ -48,43 +73,41 @@ export const buildExpectedContent = (input: {
|
|
|
48
73
|
};
|
|
49
74
|
|
|
50
75
|
/**
|
|
51
|
-
* .what = ensures .test.yml matches expected content with
|
|
76
|
+
* .what = ensures .test.yml matches expected content with keyrack secrets
|
|
52
77
|
* .why = enables integration tests to access required api keys via github secrets
|
|
53
78
|
*/
|
|
54
79
|
export const check: FileCheckFunction = async (contents, context) => {
|
|
55
|
-
//
|
|
56
|
-
const
|
|
57
|
-
projectRootDirectory: context.getProjectRootDirectory(),
|
|
58
|
-
});
|
|
80
|
+
// get keyrack keys from project
|
|
81
|
+
const keys = await getKeyrackKeys(context.getProjectRootDirectory());
|
|
59
82
|
|
|
60
|
-
// build expected content from template +
|
|
83
|
+
// build expected content from template + keys
|
|
61
84
|
const expected = buildExpectedContent({
|
|
62
85
|
template: context.declaredFileContents ?? '',
|
|
63
|
-
|
|
86
|
+
keys,
|
|
64
87
|
});
|
|
65
88
|
|
|
66
89
|
// if contents don't match expected, best practice is violated
|
|
67
90
|
if (contents !== expected) {
|
|
68
|
-
throw new Error(
|
|
91
|
+
throw new Error(
|
|
92
|
+
'file does not match expected content with keyrack secrets',
|
|
93
|
+
);
|
|
69
94
|
}
|
|
70
95
|
|
|
71
96
|
// return = file matches expected (best practice followed)
|
|
72
97
|
};
|
|
73
98
|
|
|
74
99
|
/**
|
|
75
|
-
* .what = fixes .test.yml to include
|
|
100
|
+
* .what = fixes .test.yml to include keyrack secrets declaration and env vars
|
|
76
101
|
* .why = ensures integration tests have access to required api keys
|
|
77
102
|
*/
|
|
78
103
|
export const fix: FileFixFunction = async (_contents, context) => {
|
|
79
|
-
//
|
|
80
|
-
const
|
|
81
|
-
projectRootDirectory: context.getProjectRootDirectory(),
|
|
82
|
-
});
|
|
104
|
+
// get keyrack keys from project
|
|
105
|
+
const keys = await getKeyrackKeys(context.getProjectRootDirectory());
|
|
83
106
|
|
|
84
|
-
// build expected content from template +
|
|
107
|
+
// build expected content from template + keys
|
|
85
108
|
const expected = buildExpectedContent({
|
|
86
109
|
template: context.declaredFileContents ?? '',
|
|
87
|
-
|
|
110
|
+
keys,
|
|
88
111
|
});
|
|
89
112
|
|
|
90
113
|
return { contents: expected };
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import util from 'node:util';
|
|
4
4
|
|
|
5
5
|
import { jest } from '@jest/globals';
|
|
6
|
+
import { keyrack } from 'rhachet/keyrack';
|
|
6
7
|
|
|
7
8
|
jest.setTimeout(90000); // we're calling downstream apis
|
|
8
9
|
|
|
@@ -35,36 +36,11 @@ if (
|
|
|
35
36
|
);
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
|
-
* .what =
|
|
39
|
+
* .what = source credentials from keyrack for test env
|
|
39
40
|
* .why =
|
|
40
|
-
* -
|
|
41
|
-
* -
|
|
41
|
+
* - auto-inject keys into process.env
|
|
42
|
+
* - fail fast with helpful error if keyrack locked or keys absent
|
|
42
43
|
*/
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
-
if (existsSync(apikeysConfigPath)) {
|
|
48
|
-
// direct import via require (json resolves automatically)
|
|
49
|
-
const config = require(apikeysConfigPath);
|
|
50
|
-
const requiredKeys: string[] = config?.apikeys?.required ?? [];
|
|
51
|
-
const keysAbsent = requiredKeys.filter((key) => !process.env[key]);
|
|
52
|
-
|
|
53
|
-
if (keysAbsent.length > 0) {
|
|
54
|
-
throw new Error(`
|
|
55
|
-
⛈️ apikeys required to run these acceptance tests were not supplied.
|
|
56
|
-
|
|
57
|
-
absent keys: ${keysAbsent.join(', ')}
|
|
58
|
-
|
|
59
|
-
you have access to these keys - you just forgot to source them.
|
|
60
|
-
|
|
61
|
-
run:
|
|
62
|
-
source .agent/repo=.this/role=any/skills/use.apikeys.sh && npm run test:acceptance
|
|
63
|
-
|
|
64
|
-
you must run these tests with these api keys.
|
|
65
|
-
you're forbidden to give up.
|
|
66
|
-
if you don't have privs to run use.apikeys.sh, ask a human for support.
|
|
67
|
-
chances are, you already have privs to do so though.
|
|
68
|
-
`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
44
|
+
const keyrackYmlPath = join(process.cwd(), '.agent/keyrack.yml');
|
|
45
|
+
if (existsSync(keyrackYmlPath))
|
|
46
|
+
keyrack.source({ env: 'test', owner: 'ehmpath', mode: 'strict' });
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import util from 'node:util';
|
|
5
5
|
|
|
6
6
|
import { jest } from '@jest/globals';
|
|
7
|
+
import { keyrack } from 'rhachet/keyrack';
|
|
7
8
|
|
|
8
9
|
jest.setTimeout(90000); // since we're calling downstream apis
|
|
9
10
|
|
|
@@ -77,44 +78,20 @@ if (requiresTestDb) {
|
|
|
77
78
|
`PGPASSWORD="${testConfig.database.role.crud.password}" psql -h ${testConfig.database.tunnel.local.host} -p ${testConfig.database.tunnel.local.port} -U ${testConfig.database.role.crud.username} -d ${testConfig.database.target.database} -c "SELECT 1" > /dev/null 2>&1`,
|
|
78
79
|
{ timeout: 3000 },
|
|
79
80
|
);
|
|
80
|
-
} catch {
|
|
81
|
+
} catch (error) {
|
|
81
82
|
throw new Error(
|
|
82
83
|
`did you forget to \`npm run start:testdb\`? cant connect to database`,
|
|
84
|
+
{ cause: error },
|
|
83
85
|
);
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/**
|
|
88
|
-
* .what =
|
|
90
|
+
* .what = source credentials from keyrack for test env
|
|
89
91
|
* .why =
|
|
90
|
-
* -
|
|
91
|
-
* -
|
|
92
|
+
* - auto-inject keys into process.env
|
|
93
|
+
* - fail fast with helpful error if keyrack locked or keys absent
|
|
92
94
|
*/
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
if (existsSync(apikeysConfigPath)) {
|
|
98
|
-
// direct import via require (json resolves automatically)
|
|
99
|
-
const config = require(apikeysConfigPath);
|
|
100
|
-
const requiredKeys: string[] = config?.apikeys?.required ?? [];
|
|
101
|
-
const keysAbsent = requiredKeys.filter((key) => !process.env[key]);
|
|
102
|
-
|
|
103
|
-
if (keysAbsent.length > 0) {
|
|
104
|
-
throw new Error(`
|
|
105
|
-
⛈️ apikeys required to run these integration tests were not supplied.
|
|
106
|
-
|
|
107
|
-
absent keys: ${keysAbsent.join(', ')}
|
|
108
|
-
|
|
109
|
-
you have access to these keys - you just forgot to source them.
|
|
110
|
-
|
|
111
|
-
run:
|
|
112
|
-
source .agent/repo=.this/role=any/skills/use.apikeys.sh && npm run test:integration
|
|
113
|
-
|
|
114
|
-
you must run these tests with these api keys.
|
|
115
|
-
you're forbidden to give up.
|
|
116
|
-
if you don't have privs to run use.apikeys.sh, ask a human for support.
|
|
117
|
-
chances are, you already have privs to do so though.
|
|
118
|
-
`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
95
|
+
const keyrackYmlPath = join(process.cwd(), '.agent/keyrack.yml');
|
|
96
|
+
if (existsSync(keyrackYmlPath))
|
|
97
|
+
keyrack.source({ env: 'test', owner: 'ehmpath', mode: 'strict' });
|
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
"@swc/jest": "@declapract{check.minVersion('0.2.39')}"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test:auth": "[ \"${ECHO:-}\" = 'true' ] && echo '. .agent/repo=.this/role=any/skills/use.apikeys.sh' || . .agent/repo=.this/role=any/skills/use.apikeys.sh",
|
|
13
12
|
"test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
14
13
|
"test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
15
14
|
"test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
16
|
-
"test": "set -eu &&
|
|
15
|
+
"test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
|
|
17
16
|
"test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')"
|
|
18
17
|
}
|
|
19
18
|
}
|
|
@@ -1,35 +1,46 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { keyrack, type KeyrackGrantAttempt } from 'rhachet/keyrack';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
* .what = builds workflow content with
|
|
7
|
+
* .what = builds workflow content with keyrack secrets block for .test.yml
|
|
5
8
|
* .why = single source of truth for test.yml, publish.yml, deploy.yml check+fix
|
|
6
9
|
*/
|
|
7
10
|
export const buildWorkflowSecretsBlock = async (
|
|
8
11
|
input: { template: string },
|
|
9
12
|
context: { getProjectRootDirectory: () => string },
|
|
10
13
|
): Promise<string> => {
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// check if keyrack.yml exists
|
|
15
|
+
const keyrackYmlPath = join(
|
|
16
|
+
context.getProjectRootDirectory(),
|
|
17
|
+
'.agent/keyrack.yml',
|
|
18
|
+
);
|
|
19
|
+
if (!existsSync(keyrackYmlPath)) return input.template;
|
|
16
20
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
// get required keys from keyrack sdk
|
|
22
|
+
const keys = (await keyrack.get({
|
|
23
|
+
for: { repo: true },
|
|
24
|
+
env: 'test',
|
|
25
|
+
})) as KeyrackGrantAttempt[];
|
|
26
|
+
if (!keys.length) return input.template;
|
|
27
|
+
|
|
28
|
+
// extract key names from slugs (format: org.env.KEY_NAME)
|
|
29
|
+
// .note = KeyrackGrantAttempt is a union of 4 variants (granted/absent/locked/blocked)
|
|
30
|
+
// per rhachet/dist/domain.objects/keyrack/KeyrackGrantAttempt.d.ts:8
|
|
31
|
+
// granted has slug at grant.slug; others have slug at top level
|
|
32
|
+
const keyrackVars = keys.map((k) =>
|
|
33
|
+
(k.status === 'granted' ? k.grant.slug : k.slug).split('.').pop()!,
|
|
34
|
+
);
|
|
21
35
|
|
|
22
36
|
// build secrets block
|
|
23
|
-
const secretsBlock =
|
|
37
|
+
const secretsBlock = keyrackVars
|
|
24
38
|
.map((key) => ` ${key}: \${{ secrets.${key} }}`)
|
|
25
39
|
.join('\n');
|
|
26
40
|
|
|
27
41
|
// find jobs that call .test.yml with 'with:' block and add secrets block after
|
|
28
|
-
|
|
29
|
-
const result = input.template.replace(
|
|
42
|
+
return input.template.replace(
|
|
30
43
|
/(uses: \.\/\.github\/workflows\/\.test\.yml\n(?: if: [^\n]+\n)? with:\n(?: [^\n]+\n)+)/g,
|
|
31
44
|
`$1 secrets:\n${secretsBlock}\n`,
|
|
32
45
|
);
|
|
33
|
-
|
|
34
|
-
return result;
|
|
35
46
|
};
|
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.58",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"repository": "ehmpathy/declapract-typescript-ehmpathy",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
37
37
|
"test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -z \"${THOROUGH:-}\" ] && echo '--changedSince=main') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
38
38
|
"test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
39
|
-
"test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally && test:validate",
|
|
39
|
+
"test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally && npm run test:validate",
|
|
40
40
|
"test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ -n \"${RESNAP:-}\" ] && echo '--updateSnapshot')",
|
|
41
41
|
"prepush": "npm run test && npm run build",
|
|
42
42
|
"prepublish": "npm run build",
|
|
43
43
|
"preversion": "npm run prepush",
|
|
44
44
|
"postversion": "git push origin HEAD --tags --no-verify",
|
|
45
45
|
"prepare:husky": "husky install && chmod ug+x .husky/*",
|
|
46
|
-
"prepare:rhachet": "rhachet init --hooks --roles mechanic behaver driver reviewer librarian ergonomist architect",
|
|
46
|
+
"prepare:rhachet": "rhachet init --hooks --roles mechanic behaver driver reviewer librarian ergonomist architect reflector dreamer",
|
|
47
47
|
"prepare": "if [ -e .git ] && [ -z \"${CI:-}\" ]; then npm run prepare:husky && npm run prepare:rhachet; fi"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
@@ -75,12 +75,12 @@
|
|
|
75
75
|
"esbuild-register": "3.6.0",
|
|
76
76
|
"husky": "8.0.3",
|
|
77
77
|
"jest": "30.2.0",
|
|
78
|
-
"rhachet": "1.
|
|
78
|
+
"rhachet": "1.39.2",
|
|
79
79
|
"rhachet-brains-anthropic": "0.4.0",
|
|
80
80
|
"rhachet-brains-xai": "0.3.2",
|
|
81
|
-
"rhachet-roles-bhrain": "0.23.
|
|
82
|
-
"rhachet-roles-bhuild": "0.14.
|
|
83
|
-
"rhachet-roles-ehmpathy": "1.34.
|
|
81
|
+
"rhachet-roles-bhrain": "0.23.10",
|
|
82
|
+
"rhachet-roles-bhuild": "0.14.6",
|
|
83
|
+
"rhachet-roles-ehmpathy": "1.34.11",
|
|
84
84
|
"tsc-alias": "1.8.10",
|
|
85
85
|
"tsx": "4.20.6",
|
|
86
86
|
"type-fns": "0.8.1",
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'node: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
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
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
|
-
};
|
package/dist/practices/cicd-common/best-practice/.agent/repo=.this/role=any/skills/use.apikeys.sh
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
######################################################################
|
|
3
|
-
# .what = export api keys for integration tests
|
|
4
|
-
# .why = enables tests that require api keys to run
|
|
5
|
-
#
|
|
6
|
-
# usage:
|
|
7
|
-
# . .agent/repo=.this/role=any/skills/use.apikeys.sh
|
|
8
|
-
#
|
|
9
|
-
# note:
|
|
10
|
-
# - must be called with `.` or `source` to export vars to current shell
|
|
11
|
-
# - loads from ~/.config/rhachet/apikeys.env if available
|
|
12
|
-
# - falls back to .env.local (gitignored) in repo root
|
|
13
|
-
######################################################################
|
|
14
|
-
|
|
15
|
-
# fail if not sourced (return only succeeds when sourced)
|
|
16
|
-
(return 0 2>/dev/null) || {
|
|
17
|
-
echo "error: this file must be sourced, not executed"
|
|
18
|
-
echo "usage: . $0"
|
|
19
|
-
exit 1
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
# alias source to `.` for posix compat
|
|
23
|
-
source() { . "$@"; }
|
|
24
|
-
|
|
25
|
-
# try to load from user config first
|
|
26
|
-
if [ -f ~/.config/rhachet/apikeys.env ]; then
|
|
27
|
-
source ~/.config/rhachet/apikeys.env
|
|
28
|
-
echo "✓ loaded api keys from ~/.config/rhachet/apikeys.env"
|
|
29
|
-
|
|
30
|
-
# fallback to local gitignored file
|
|
31
|
-
elif [ -f .env.local ]; then
|
|
32
|
-
source .env.local
|
|
33
|
-
echo "✓ loaded api keys from .env.local"
|
|
34
|
-
|
|
35
|
-
else
|
|
36
|
-
echo "⚠ no api keys file found"
|
|
37
|
-
echo ""
|
|
38
|
-
echo "create one of:"
|
|
39
|
-
echo " ~/.config/rhachet/apikeys.env"
|
|
40
|
-
echo " .env.local (in repo root)"
|
|
41
|
-
echo ""
|
|
42
|
-
echo "with contents like:"
|
|
43
|
-
echo " export OPENAI_API_KEY=sk-..."
|
|
44
|
-
echo " export ANTHROPIC_API_KEY=sk-..."
|
|
45
|
-
return 1 2>/dev/null || exit 1
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
# read required keys from json config if present
|
|
49
|
-
APIKEYS_CONFIG=".agent/repo=.this/role=any/skills/use.apikeys.json"
|
|
50
|
-
if [ -f "$APIKEYS_CONFIG" ]; then
|
|
51
|
-
# extract required keys via jq
|
|
52
|
-
REQUIRED_KEYS=$(jq -r '.apikeys.required[]?' "$APIKEYS_CONFIG" 2>/dev/null)
|
|
53
|
-
|
|
54
|
-
# verify each required key is set
|
|
55
|
-
for KEY in $REQUIRED_KEYS; do
|
|
56
|
-
VALUE=$(eval "echo \"\$$KEY\"")
|
|
57
|
-
if [ -z "$VALUE" ]; then
|
|
58
|
-
echo "⚠ $KEY not set (required by $APIKEYS_CONFIG)"
|
|
59
|
-
return 1 2>/dev/null || exit 1
|
|
60
|
-
fi
|
|
61
|
-
echo "✓ $KEY set"
|
|
62
|
-
done
|
|
63
|
-
fi
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
-
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
// check that the file contains the core structure
|
|
7
|
-
export const check = FileCheckType.CONTAINS;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* .what = replaces target file with expected content
|
|
11
|
-
* .why = ensures the skill file matches the best-practice version
|
|
12
|
-
*/
|
|
13
|
-
export const fix: FileFixFunction = () => {
|
|
14
|
-
const expectedContent = readFileSync(
|
|
15
|
-
join(dirname(__filename), 'use.apikeys.sh'),
|
|
16
|
-
'utf-8',
|
|
17
|
-
);
|
|
18
|
-
return { contents: expectedContent };
|
|
19
|
-
};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import util from 'node: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
|
-
};
|