declapract-typescript-ehmpathy 0.47.19 → 0.47.21
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.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/practices/tests/bad-practices/old-acceptance-dir-location/.declapract.readme.md +3 -3
- package/dist/practices/tests/bad-practices/old-acceptance-dir-location/accept.blackbox/<star><star>/<star>.ts.declapract.ts +19 -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
|
+
};
|
|
@@ -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
|
+
};
|
package/dist/practices/tests/bad-practices/old-acceptance-dir-location/.declapract.readme.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## .what
|
|
4
4
|
|
|
5
|
-
detects test files located in `acceptance/`
|
|
5
|
+
detects test files located in `acceptance/` or `accept.blackbox/` directories and migrates them to `blackbox/`.
|
|
6
6
|
|
|
7
7
|
## .why
|
|
8
8
|
|
|
@@ -11,11 +11,11 @@ the term "blackbox" makes the testing philosophy explicit:
|
|
|
11
11
|
- no access to internal implementation details
|
|
12
12
|
- tests validate behavior from the caller's perspective
|
|
13
13
|
|
|
14
|
-
the rename from `acceptance/` to `blackbox/` clarifies this intent.
|
|
14
|
+
the rename from `acceptance/` or `accept.blackbox/` to `blackbox/` clarifies this intent and simplifies the naming.
|
|
15
15
|
|
|
16
16
|
## .fix
|
|
17
17
|
|
|
18
|
-
moves all files from `acceptance/` to `blackbox/` while preserving the directory structure.
|
|
18
|
+
moves all files from `acceptance/` or `accept.blackbox/` to `blackbox/` while preserving the directory structure.
|
|
19
19
|
|
|
20
20
|
## .note
|
|
21
21
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FileCheckType, type FileFixFunction } from 'declapract';
|
|
2
|
+
|
|
3
|
+
export const check = FileCheckType.EXISTS; // if any ts files exist in accept.blackbox/, flag as bad practice
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* .what = moves ts files from accept.blackbox/ to blackbox/
|
|
7
|
+
* .why = blackbox/ naming is simpler and makes the testing philosophy explicit
|
|
8
|
+
*/
|
|
9
|
+
export const fix: FileFixFunction = (contents, context) => {
|
|
10
|
+
// move from accept.blackbox/ to blackbox/
|
|
11
|
+
const newPath = context.relativeFilePath.replace(
|
|
12
|
+
/^accept\.blackbox\//,
|
|
13
|
+
'blackbox/',
|
|
14
|
+
);
|
|
15
|
+
return {
|
|
16
|
+
contents: contents ?? null,
|
|
17
|
+
relativeFilePath: newPath,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -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.21",
|
|
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
|