declapract-typescript-ehmpathy 0.47.19 → 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.
@@ -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
+ };
@@ -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.19",
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.8",
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",
@@ -1,4 +0,0 @@
1
- import { FileCheckType } from 'declapract';
2
-
3
- // check that the file exists; contents are user-defined
4
- export const check = FileCheckType.EXISTS;