@walker-di/fortalece-ai-sdk 0.3.7 → 0.3.9
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/ci/__tests__/cli.test.d.ts +2 -0
- package/dist/ci/__tests__/cli.test.d.ts.map +1 -0
- package/dist/ci/__tests__/cli.test.js +23 -0
- package/dist/ci/__tests__/notify.test.d.ts +2 -0
- package/dist/ci/__tests__/notify.test.d.ts.map +1 -0
- package/dist/ci/__tests__/notify.test.js +130 -0
- package/dist/ci/cli.d.ts +20 -0
- package/dist/ci/cli.d.ts.map +1 -1
- package/dist/ci/cli.js +57 -6
- package/dist/ci/notify.d.ts +3 -1
- package/dist/ci/notify.d.ts.map +1 -1
- package/dist/ci/notify.js +173 -19
- package/dist/ci/types.d.ts +27 -0
- package/dist/ci/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../../../src/ci/__tests__/cli.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseArgs } from '../cli.js';
|
|
3
|
+
describe('notify-ci CLI argument parsing', () => {
|
|
4
|
+
it('parses manual override flags for local runs', () => {
|
|
5
|
+
const parsed = parseArgs([
|
|
6
|
+
'--status',
|
|
7
|
+
'success',
|
|
8
|
+
'--repository',
|
|
9
|
+
'acme/repo',
|
|
10
|
+
'--branch',
|
|
11
|
+
'fortalece/wi_123',
|
|
12
|
+
'--commit-sha',
|
|
13
|
+
'abc123',
|
|
14
|
+
'--delivery-id',
|
|
15
|
+
'delivery-1',
|
|
16
|
+
]);
|
|
17
|
+
expect(parsed.status).toBe('success');
|
|
18
|
+
expect(parsed.repository).toBe('acme/repo');
|
|
19
|
+
expect(parsed.branch).toBe('fortalece/wi_123');
|
|
20
|
+
expect(parsed.commitSha).toBe('abc123');
|
|
21
|
+
expect(parsed.deliveryId).toBe('delivery-1');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.test.d.ts","sourceRoot":"","sources":["../../../src/ci/__tests__/notify.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { notifyCi } from '../notify.js';
|
|
4
|
+
vi.mock('node:child_process', () => ({
|
|
5
|
+
execSync: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
const execSyncMock = vi.mocked(execSync);
|
|
8
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
9
|
+
function createSuccessResponse() {
|
|
10
|
+
return new Response('{}', {
|
|
11
|
+
status: 200,
|
|
12
|
+
headers: {
|
|
13
|
+
'content-type': 'application/json',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function clearGitHubEnv() {
|
|
18
|
+
delete process.env.GITHUB_EVENT_PATH;
|
|
19
|
+
delete process.env.GITHUB_REPOSITORY;
|
|
20
|
+
delete process.env.GITHUB_SHA;
|
|
21
|
+
delete process.env.GITHUB_HEAD_REF;
|
|
22
|
+
delete process.env.GITHUB_REF;
|
|
23
|
+
delete process.env.GITHUB_REF_NAME;
|
|
24
|
+
delete process.env.GITHUB_RUN_ID;
|
|
25
|
+
delete process.env.GITHUB_RUN_NUMBER;
|
|
26
|
+
delete process.env.GITHUB_RUN_ATTEMPT;
|
|
27
|
+
delete process.env.GITHUB_WORKFLOW;
|
|
28
|
+
delete process.env.GITHUB_ACTOR;
|
|
29
|
+
delete process.env.GITHUB_EVENT_NAME;
|
|
30
|
+
delete process.env.GITHUB_SERVER_URL;
|
|
31
|
+
}
|
|
32
|
+
describe('notifyCi local/manual fallback behavior', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
process.env = { ...ORIGINAL_ENV };
|
|
35
|
+
clearGitHubEnv();
|
|
36
|
+
execSyncMock.mockReset();
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.env = { ...ORIGINAL_ENV };
|
|
40
|
+
vi.unstubAllGlobals();
|
|
41
|
+
});
|
|
42
|
+
it('uses local git fallback when GitHub environment is absent', async () => {
|
|
43
|
+
execSyncMock.mockImplementation((command) => {
|
|
44
|
+
if (command === 'git rev-parse --abbrev-ref HEAD') {
|
|
45
|
+
return 'fortalece/wi_local\n';
|
|
46
|
+
}
|
|
47
|
+
if (command === 'git rev-parse HEAD') {
|
|
48
|
+
return 'abc123local\n';
|
|
49
|
+
}
|
|
50
|
+
if (command === 'git config --get remote.origin.url') {
|
|
51
|
+
return 'git@github.com:acme/fallback-repo.git\n';
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Unexpected command: ${command}`);
|
|
54
|
+
});
|
|
55
|
+
const fetchMock = vi.fn().mockResolvedValue(createSuccessResponse());
|
|
56
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
57
|
+
const result = await notifyCi({
|
|
58
|
+
webhookUrl: 'https://example.com/webhook',
|
|
59
|
+
webhookSecret: 'secret',
|
|
60
|
+
status: 'success',
|
|
61
|
+
});
|
|
62
|
+
expect(result.success).toBe(true);
|
|
63
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
64
|
+
const fetchInit = fetchMock.mock.calls[0]?.[1];
|
|
65
|
+
const payload = JSON.parse(String(fetchInit.body));
|
|
66
|
+
expect(payload.repository).toBe('acme/fallback-repo');
|
|
67
|
+
expect(payload.branch).toBe('fortalece/wi_local');
|
|
68
|
+
expect(payload.commit_sha).toBe('abc123local');
|
|
69
|
+
expect(payload.work_item_id).toBe('wi_local');
|
|
70
|
+
});
|
|
71
|
+
it('prefers manual overrides over GitHub env and local git fallback', async () => {
|
|
72
|
+
process.env.GITHUB_REPOSITORY = 'env/repo';
|
|
73
|
+
process.env.GITHUB_SHA = 'env-sha';
|
|
74
|
+
process.env.GITHUB_REF_NAME = 'env-branch';
|
|
75
|
+
execSyncMock.mockImplementation((command) => {
|
|
76
|
+
if (command === 'git rev-parse --abbrev-ref HEAD') {
|
|
77
|
+
return 'fortalece/wi_from_git\n';
|
|
78
|
+
}
|
|
79
|
+
if (command === 'git rev-parse HEAD') {
|
|
80
|
+
return 'git-sha';
|
|
81
|
+
}
|
|
82
|
+
if (command === 'git config --get remote.origin.url') {
|
|
83
|
+
return 'git@github.com:git/fallback.git\n';
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Unexpected command: ${command}`);
|
|
86
|
+
});
|
|
87
|
+
const fetchMock = vi.fn().mockResolvedValue(createSuccessResponse());
|
|
88
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
89
|
+
await notifyCi({
|
|
90
|
+
webhookUrl: 'https://example.com/webhook',
|
|
91
|
+
webhookSecret: 'secret',
|
|
92
|
+
status: 'success',
|
|
93
|
+
repository: 'override/repo',
|
|
94
|
+
branch: 'fortalece/wi_override',
|
|
95
|
+
commitSha: 'override-sha',
|
|
96
|
+
prTitle: 'Fixes #wi_from_title',
|
|
97
|
+
});
|
|
98
|
+
const fetchInit = fetchMock.mock.calls[0]?.[1];
|
|
99
|
+
const payload = JSON.parse(String(fetchInit.body));
|
|
100
|
+
expect(payload.repository).toBe('override/repo');
|
|
101
|
+
expect(payload.branch).toBe('fortalece/wi_override');
|
|
102
|
+
expect(payload.commit_sha).toBe('override-sha');
|
|
103
|
+
expect(payload.work_item_id).toBe('wi_override');
|
|
104
|
+
});
|
|
105
|
+
it('keeps inference behavior from PR title when branch has no token', async () => {
|
|
106
|
+
execSyncMock.mockImplementation((command) => {
|
|
107
|
+
if (command === 'git rev-parse --abbrev-ref HEAD') {
|
|
108
|
+
return 'feature/no-token\n';
|
|
109
|
+
}
|
|
110
|
+
if (command === 'git rev-parse HEAD') {
|
|
111
|
+
return 'abc123title\n';
|
|
112
|
+
}
|
|
113
|
+
if (command === 'git config --get remote.origin.url') {
|
|
114
|
+
return 'https://github.com/acme/title-fallback.git\n';
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Unexpected command: ${command}`);
|
|
117
|
+
});
|
|
118
|
+
const fetchMock = vi.fn().mockResolvedValue(createSuccessResponse());
|
|
119
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
120
|
+
await notifyCi({
|
|
121
|
+
webhookUrl: 'https://example.com/webhook',
|
|
122
|
+
webhookSecret: 'secret',
|
|
123
|
+
status: 'building',
|
|
124
|
+
prTitle: 'Do thing #wi_from_title',
|
|
125
|
+
});
|
|
126
|
+
const fetchInit = fetchMock.mock.calls[0]?.[1];
|
|
127
|
+
const payload = JSON.parse(String(fetchInit.body));
|
|
128
|
+
expect(payload.work_item_id).toBe('wi_from_title');
|
|
129
|
+
});
|
|
130
|
+
});
|
package/dist/ci/cli.d.ts
CHANGED
|
@@ -8,5 +8,25 @@
|
|
|
8
8
|
* npx @walker-di/fortalece-ai-sdk notify-ci --status success --preview-url https://preview.example.com
|
|
9
9
|
* npx @walker-di/fortalece-ai-sdk notify-ci --status failed --error-message "Build failed"
|
|
10
10
|
*/
|
|
11
|
+
import type { CiStatus } from './types.js';
|
|
12
|
+
interface CliArgs {
|
|
13
|
+
status?: CiStatus;
|
|
14
|
+
webhookUrl?: string;
|
|
15
|
+
webhookSecret?: string;
|
|
16
|
+
previewUrl?: string;
|
|
17
|
+
logsUrl?: string;
|
|
18
|
+
errorMessage?: string;
|
|
19
|
+
workItemId?: string;
|
|
20
|
+
serviceName?: string;
|
|
21
|
+
prUrl?: string;
|
|
22
|
+
prTitle?: string;
|
|
23
|
+
prNumber?: string;
|
|
24
|
+
branch?: string;
|
|
25
|
+
repository?: string;
|
|
26
|
+
commitSha?: string;
|
|
27
|
+
deliveryId?: string;
|
|
28
|
+
help?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function parseArgs(args: string[]): CliArgs;
|
|
11
31
|
export {};
|
|
12
32
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/ci/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/ci/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG"}
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/ci/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,UAAU,OAAO;IACf,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAmFjD"}
|
package/dist/ci/cli.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* npx @walker-di/fortalece-ai-sdk notify-ci --status failed --error-message "Build failed"
|
|
10
10
|
*/
|
|
11
11
|
import { notifyCi } from './notify.js';
|
|
12
|
-
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
export function parseArgs(args) {
|
|
13
14
|
const result = {};
|
|
14
15
|
for (let i = 0; i < args.length; i++) {
|
|
15
16
|
const arg = args[i];
|
|
@@ -54,6 +55,34 @@ function parseArgs(args) {
|
|
|
54
55
|
result.serviceName = nextArg;
|
|
55
56
|
i++;
|
|
56
57
|
break;
|
|
58
|
+
case '--pr-url':
|
|
59
|
+
result.prUrl = nextArg;
|
|
60
|
+
i++;
|
|
61
|
+
break;
|
|
62
|
+
case '--pr-title':
|
|
63
|
+
result.prTitle = nextArg;
|
|
64
|
+
i++;
|
|
65
|
+
break;
|
|
66
|
+
case '--pr-number':
|
|
67
|
+
result.prNumber = nextArg;
|
|
68
|
+
i++;
|
|
69
|
+
break;
|
|
70
|
+
case '--branch':
|
|
71
|
+
result.branch = nextArg;
|
|
72
|
+
i++;
|
|
73
|
+
break;
|
|
74
|
+
case '--repository':
|
|
75
|
+
result.repository = nextArg;
|
|
76
|
+
i++;
|
|
77
|
+
break;
|
|
78
|
+
case '--commit-sha':
|
|
79
|
+
result.commitSha = nextArg;
|
|
80
|
+
i++;
|
|
81
|
+
break;
|
|
82
|
+
case '--delivery-id':
|
|
83
|
+
result.deliveryId = nextArg;
|
|
84
|
+
i++;
|
|
85
|
+
break;
|
|
57
86
|
case '--help':
|
|
58
87
|
case '-h':
|
|
59
88
|
result.help = true;
|
|
@@ -78,6 +107,13 @@ Options:
|
|
|
78
107
|
-e, --error-message <msg> Error message for failures (optional)
|
|
79
108
|
-w, --work-item-id <id> Link to a specific work item (optional)
|
|
80
109
|
--service-name <name> Service name (optional)
|
|
110
|
+
--pr-url <url> Pull request URL (optional)
|
|
111
|
+
--pr-title <title> Pull request title (optional)
|
|
112
|
+
--pr-number <number> Pull request number (optional)
|
|
113
|
+
--branch <name> Branch override for manual/local runs (optional)
|
|
114
|
+
--repository <owner/repo> Repository override for manual/local runs (optional)
|
|
115
|
+
--commit-sha <sha> Commit SHA override for manual/local runs (optional)
|
|
116
|
+
--delivery-id <id> Delivery ID override for idempotency (optional)
|
|
81
117
|
-h, --help Show this help message
|
|
82
118
|
|
|
83
119
|
Environment Variables:
|
|
@@ -94,6 +130,12 @@ Examples:
|
|
|
94
130
|
# Notify build failure with error message
|
|
95
131
|
npx @walker-di/fortalece-ai-sdk notify-ci --status failed --error-message "Tests failed"
|
|
96
132
|
|
|
133
|
+
# Notify with PR metadata
|
|
134
|
+
npx @walker-di/fortalece-ai-sdk notify-ci --status success --pr-url "https://github.com/org/repo/pull/42" --pr-title "Fix bug #wi_123"
|
|
135
|
+
|
|
136
|
+
# Manual/local run overrides
|
|
137
|
+
npx @walker-di/fortalece-ai-sdk notify-ci --status success --repository org/repo --branch "fortalece/wi_123" --commit-sha "$(git rev-parse HEAD)"
|
|
138
|
+
|
|
97
139
|
# Using environment variables (recommended for secrets)
|
|
98
140
|
FORTALECE_WEBHOOK_URL="\${{ secrets.FORTALECE_WEBHOOK_URL }}" \\
|
|
99
141
|
FORTALECE_WEBHOOK_SECRET="\${{ secrets.FORTALECE_WEBHOOK_SECRET }}" \\
|
|
@@ -140,6 +182,13 @@ async function main() {
|
|
|
140
182
|
errorMessage: parsed.errorMessage,
|
|
141
183
|
workItemId: parsed.workItemId,
|
|
142
184
|
serviceName: parsed.serviceName,
|
|
185
|
+
prUrl: parsed.prUrl,
|
|
186
|
+
prTitle: parsed.prTitle,
|
|
187
|
+
prNumber: parsed.prNumber,
|
|
188
|
+
branch: parsed.branch,
|
|
189
|
+
repository: parsed.repository,
|
|
190
|
+
commitSha: parsed.commitSha,
|
|
191
|
+
deliveryId: parsed.deliveryId,
|
|
143
192
|
});
|
|
144
193
|
if (result.success) {
|
|
145
194
|
console.log('✅ CI notification sent successfully');
|
|
@@ -150,8 +199,10 @@ async function main() {
|
|
|
150
199
|
process.exit(1);
|
|
151
200
|
}
|
|
152
201
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
202
|
+
const invokedPath = process.argv[1];
|
|
203
|
+
if (invokedPath && pathToFileURL(invokedPath).href === import.meta.url) {
|
|
204
|
+
main().catch((error) => {
|
|
205
|
+
console.error('Fatal error:', error);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|
|
208
|
+
}
|
package/dist/ci/notify.d.ts
CHANGED
|
@@ -10,7 +10,9 @@ import type { CiEventPayload, CiNotifyConfig, CiNotifyResponse, NotifyOptions }
|
|
|
10
10
|
* @param payload - Event payload
|
|
11
11
|
* @returns Response from the webhook
|
|
12
12
|
*/
|
|
13
|
-
export declare function notifyCiEvent(config: CiNotifyConfig, payload: CiEventPayload
|
|
13
|
+
export declare function notifyCiEvent(config: CiNotifyConfig, payload: CiEventPayload, options?: {
|
|
14
|
+
deliveryId?: string;
|
|
15
|
+
}): Promise<CiNotifyResponse>;
|
|
14
16
|
/**
|
|
15
17
|
* Notify Fortalece about a CI event using GitHub Actions environment
|
|
16
18
|
* Automatically reads environment variables for context
|
package/dist/ci/notify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/ci/notify.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/ci/notify.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,cAAc,EAEd,cAAc,EACd,gBAAgB,EAEhB,aAAa,EACd,MAAM,YAAY,CAAC;AA2KpB;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,OAAO,CAAC,gBAAgB,CAAC,CA0C3B;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2EhF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAExF;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE,OAAO,CAAC,gBAAgB,CAAC,CAQ3B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACzE,OAAO,CAAC,gBAAgB,CAAC,CAQ3B"}
|
package/dist/ci/notify.js
CHANGED
|
@@ -2,23 +2,146 @@
|
|
|
2
2
|
* Fortalece AI CI - Notify Functions
|
|
3
3
|
* Send CI events to Fortalece webhooks from GitHub Actions
|
|
4
4
|
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
5
7
|
import { signWebhookBody } from './hmac.js';
|
|
8
|
+
function normalizeValue(value) {
|
|
9
|
+
if (typeof value !== 'string') {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
14
|
+
}
|
|
15
|
+
function readGitHubEventPayload() {
|
|
16
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
17
|
+
if (!eventPath) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const raw = fs.readFileSync(eventPath, 'utf-8');
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function runGitCommand(args) {
|
|
30
|
+
try {
|
|
31
|
+
const output = execSync(`git ${args}`, {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
34
|
+
});
|
|
35
|
+
return normalizeValue(output);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function parseRepositorySlug(remoteUrl) {
|
|
42
|
+
const normalizedRemote = normalizeValue(remoteUrl);
|
|
43
|
+
if (!normalizedRemote) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const sshMatch = normalizedRemote.match(/^[^@]+@[^:]+:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
47
|
+
if (sshMatch?.[1]) {
|
|
48
|
+
return sshMatch[1];
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const parsed = new URL(normalizedRemote);
|
|
52
|
+
const pathSegments = parsed.pathname.replace(/\.git$/, '').split('/').filter(Boolean);
|
|
53
|
+
if (pathSegments.length < 2) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const owner = pathSegments[pathSegments.length - 2];
|
|
57
|
+
const repository = pathSegments[pathSegments.length - 1];
|
|
58
|
+
return `${owner}/${repository}`;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function extractBranchFromRef(ref) {
|
|
65
|
+
if (!ref)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (ref.startsWith('refs/heads/')) {
|
|
68
|
+
return ref.slice('refs/heads/'.length);
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function extractWorkItemIdFromBranch(branch) {
|
|
73
|
+
if (!branch)
|
|
74
|
+
return undefined;
|
|
75
|
+
const match = branch.match(/(?:^|\/)fortalece\/([a-zA-Z0-9_-]+)(?:$|\/)/i);
|
|
76
|
+
return match?.[1];
|
|
77
|
+
}
|
|
78
|
+
function extractWorkItemIdFromPrTitle(title) {
|
|
79
|
+
if (!title)
|
|
80
|
+
return undefined;
|
|
81
|
+
const match = title.match(/#([a-zA-Z0-9_-]+)/);
|
|
82
|
+
return match?.[1];
|
|
83
|
+
}
|
|
84
|
+
function resolveDeliveryId(payload, fallbackStatus) {
|
|
85
|
+
const statusEvent = payload.event || statusToEventType(fallbackStatus);
|
|
86
|
+
const runId = payload.github_run_id || payload.build_id || 'manual';
|
|
87
|
+
const runAttempt = payload.github_run_attempt || 1;
|
|
88
|
+
const serviceKey = payload.service_name || payload.repository || 'default';
|
|
89
|
+
return `${runId}:${runAttempt}:${statusEvent}:${serviceKey}`;
|
|
90
|
+
}
|
|
6
91
|
/**
|
|
7
92
|
* Get GitHub Actions environment variables
|
|
8
93
|
*/
|
|
9
94
|
function getGitHubEnv() {
|
|
95
|
+
const eventPayload = readGitHubEventPayload();
|
|
96
|
+
const pullRequest = eventPayload?.pull_request || undefined;
|
|
97
|
+
const prUrl = typeof pullRequest?.html_url === 'string' ? pullRequest.html_url : undefined;
|
|
98
|
+
const prTitle = typeof pullRequest?.title === 'string' ? pullRequest.title : undefined;
|
|
99
|
+
const prNumberValue = pullRequest?.number;
|
|
100
|
+
const prNumber = typeof prNumberValue === 'number' || typeof prNumberValue === 'string'
|
|
101
|
+
? String(prNumberValue)
|
|
102
|
+
: undefined;
|
|
103
|
+
const prHead = pullRequest?.head || undefined;
|
|
104
|
+
const prHeadRef = typeof prHead?.ref === 'string' ? prHead.ref : undefined;
|
|
105
|
+
const githubRef = process.env.GITHUB_REF || '';
|
|
106
|
+
const githubRefName = process.env.GITHUB_REF_NAME || '';
|
|
107
|
+
const branch = process.env.GITHUB_HEAD_REF ||
|
|
108
|
+
prHeadRef ||
|
|
109
|
+
githubRefName ||
|
|
110
|
+
extractBranchFromRef(githubRef) ||
|
|
111
|
+
'';
|
|
10
112
|
return {
|
|
11
|
-
repository: process.env.GITHUB_REPOSITORY || '',
|
|
12
|
-
branch:
|
|
13
|
-
commit_sha: process.env.GITHUB_SHA || '',
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
113
|
+
repository: normalizeValue(process.env.GITHUB_REPOSITORY) || '',
|
|
114
|
+
branch: normalizeValue(branch) || '',
|
|
115
|
+
commit_sha: normalizeValue(process.env.GITHUB_SHA) || '',
|
|
116
|
+
pr_url: prUrl,
|
|
117
|
+
pr_title: prTitle,
|
|
118
|
+
pr_number: prNumber,
|
|
119
|
+
github_run_id: normalizeValue(process.env.GITHUB_RUN_ID),
|
|
120
|
+
github_run_number: normalizeValue(process.env.GITHUB_RUN_NUMBER),
|
|
121
|
+
github_run_attempt: process.env.GITHUB_RUN_ATTEMPT
|
|
122
|
+
? Number(process.env.GITHUB_RUN_ATTEMPT)
|
|
123
|
+
: undefined,
|
|
124
|
+
github_workflow: normalizeValue(process.env.GITHUB_WORKFLOW),
|
|
125
|
+
github_actor: normalizeValue(process.env.GITHUB_ACTOR),
|
|
126
|
+
github_head_ref: normalizeValue(process.env.GITHUB_HEAD_REF) || normalizeValue(prHeadRef),
|
|
127
|
+
github_ref: normalizeValue(githubRef),
|
|
128
|
+
github_ref_name: normalizeValue(githubRefName),
|
|
129
|
+
github_event_name: normalizeValue(process.env.GITHUB_EVENT_NAME),
|
|
18
130
|
build_url: process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID
|
|
19
131
|
? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
|
|
20
132
|
: undefined,
|
|
21
|
-
build_id: process.env.GITHUB_RUN_ID,
|
|
133
|
+
build_id: normalizeValue(process.env.GITHUB_RUN_ID),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function getLocalGitEnv() {
|
|
137
|
+
const branch = runGitCommand('rev-parse --abbrev-ref HEAD');
|
|
138
|
+
const commitSha = runGitCommand('rev-parse HEAD');
|
|
139
|
+
const remoteOrigin = runGitCommand('config --get remote.origin.url');
|
|
140
|
+
const repository = parseRepositorySlug(remoteOrigin);
|
|
141
|
+
return {
|
|
142
|
+
repository,
|
|
143
|
+
branch,
|
|
144
|
+
commit_sha: commitSha,
|
|
22
145
|
};
|
|
23
146
|
}
|
|
24
147
|
/**
|
|
@@ -43,7 +166,7 @@ function statusToEventType(status) {
|
|
|
43
166
|
* @param payload - Event payload
|
|
44
167
|
* @returns Response from the webhook
|
|
45
168
|
*/
|
|
46
|
-
export async function notifyCiEvent(config, payload) {
|
|
169
|
+
export async function notifyCiEvent(config, payload, options) {
|
|
47
170
|
const { webhookUrl, webhookSecret } = config;
|
|
48
171
|
if (!webhookUrl) {
|
|
49
172
|
return { success: false, error: 'Missing webhook URL' };
|
|
@@ -53,6 +176,7 @@ export async function notifyCiEvent(config, payload) {
|
|
|
53
176
|
}
|
|
54
177
|
const body = JSON.stringify(payload);
|
|
55
178
|
const signature = signWebhookBody(webhookSecret, body);
|
|
179
|
+
const deliveryId = options?.deliveryId;
|
|
56
180
|
try {
|
|
57
181
|
const response = await fetch(webhookUrl, {
|
|
58
182
|
method: 'POST',
|
|
@@ -60,6 +184,7 @@ export async function notifyCiEvent(config, payload) {
|
|
|
60
184
|
'Content-Type': 'application/json',
|
|
61
185
|
'X-Hub-Signature-256': signature,
|
|
62
186
|
'X-Fortalece-Event': `ci:${payload.event}`,
|
|
187
|
+
...(deliveryId ? { 'X-Webhook-ID': deliveryId } : {}),
|
|
63
188
|
},
|
|
64
189
|
body,
|
|
65
190
|
});
|
|
@@ -87,27 +212,56 @@ export async function notifyCiEvent(config, payload) {
|
|
|
87
212
|
* @returns Response from the webhook
|
|
88
213
|
*/
|
|
89
214
|
export async function notifyCi(options) {
|
|
90
|
-
const { webhookUrl, webhookSecret, status, previewUrl, logsUrl, errorMessage, workItemId, serviceName, } = options;
|
|
215
|
+
const { webhookUrl, webhookSecret, status, previewUrl, logsUrl, errorMessage, workItemId, serviceName, prUrl, prTitle, prNumber, branch, repository, commitSha, commit_sha: commitShaLegacy, deliveryId, } = options;
|
|
91
216
|
const githubEnv = getGitHubEnv();
|
|
217
|
+
const localGitEnv = getLocalGitEnv();
|
|
218
|
+
const resolvedRepository = normalizeValue(repository) ||
|
|
219
|
+
normalizeValue(githubEnv.repository) ||
|
|
220
|
+
normalizeValue(localGitEnv.repository) ||
|
|
221
|
+
'unknown/unknown';
|
|
222
|
+
const resolvedBranch = normalizeValue(branch) ||
|
|
223
|
+
normalizeValue(githubEnv.branch) ||
|
|
224
|
+
normalizeValue(localGitEnv.branch) ||
|
|
225
|
+
'unknown';
|
|
226
|
+
const resolvedCommitSha = normalizeValue(commitSha) ||
|
|
227
|
+
normalizeValue(commitShaLegacy) ||
|
|
228
|
+
normalizeValue(githubEnv.commit_sha) ||
|
|
229
|
+
normalizeValue(localGitEnv.commit_sha) ||
|
|
230
|
+
'unknown';
|
|
231
|
+
const resolvedPrTitle = normalizeValue(prTitle) || normalizeValue(githubEnv.pr_title);
|
|
232
|
+
const resolvedWorkItemId = normalizeValue(workItemId) ||
|
|
233
|
+
extractWorkItemIdFromBranch(resolvedBranch) ||
|
|
234
|
+
extractWorkItemIdFromPrTitle(resolvedPrTitle);
|
|
92
235
|
const payload = {
|
|
93
236
|
event: statusToEventType(status),
|
|
94
237
|
status,
|
|
95
|
-
repository:
|
|
96
|
-
branch:
|
|
97
|
-
commit_sha:
|
|
98
|
-
work_item_id:
|
|
238
|
+
repository: resolvedRepository,
|
|
239
|
+
branch: resolvedBranch,
|
|
240
|
+
commit_sha: resolvedCommitSha,
|
|
241
|
+
work_item_id: resolvedWorkItemId,
|
|
242
|
+
pr_url: normalizeValue(prUrl) || normalizeValue(githubEnv.pr_url),
|
|
243
|
+
pr_title: resolvedPrTitle,
|
|
244
|
+
pr_number: normalizeValue(prNumber) || normalizeValue(githubEnv.pr_number),
|
|
99
245
|
build_id: githubEnv.build_id,
|
|
100
246
|
build_url: githubEnv.build_url,
|
|
101
|
-
logs_url: logsUrl,
|
|
102
|
-
preview_url: previewUrl,
|
|
103
|
-
service_name: serviceName,
|
|
104
|
-
error_message: errorMessage,
|
|
247
|
+
logs_url: normalizeValue(logsUrl),
|
|
248
|
+
preview_url: normalizeValue(previewUrl),
|
|
249
|
+
service_name: normalizeValue(serviceName),
|
|
250
|
+
error_message: normalizeValue(errorMessage),
|
|
105
251
|
github_run_id: githubEnv.github_run_id,
|
|
106
252
|
github_run_number: githubEnv.github_run_number,
|
|
253
|
+
github_run_attempt: githubEnv.github_run_attempt,
|
|
107
254
|
github_workflow: githubEnv.github_workflow,
|
|
108
255
|
github_actor: githubEnv.github_actor,
|
|
256
|
+
github_head_ref: githubEnv.github_head_ref,
|
|
257
|
+
github_ref: githubEnv.github_ref,
|
|
258
|
+
github_ref_name: githubEnv.github_ref_name,
|
|
259
|
+
github_event_name: githubEnv.github_event_name,
|
|
109
260
|
};
|
|
110
|
-
|
|
261
|
+
const resolvedDeliveryId = deliveryId || resolveDeliveryId(payload, status);
|
|
262
|
+
return notifyCiEvent({ webhookUrl, webhookSecret }, payload, {
|
|
263
|
+
deliveryId: resolvedDeliveryId,
|
|
264
|
+
});
|
|
111
265
|
}
|
|
112
266
|
/**
|
|
113
267
|
* Convenience function: Notify build started
|
package/dist/ci/types.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface CiEventPayload {
|
|
|
21
21
|
commit_sha: string;
|
|
22
22
|
/** Optional: Link to a specific work item */
|
|
23
23
|
work_item_id?: string;
|
|
24
|
+
/** Optional PR URL for direct correlation */
|
|
25
|
+
pr_url?: string;
|
|
26
|
+
/** Optional PR title (can contain #<work_item_id>) */
|
|
27
|
+
pr_title?: string;
|
|
28
|
+
/** Optional PR number */
|
|
29
|
+
pr_number?: string;
|
|
24
30
|
/** Build ID */
|
|
25
31
|
build_id?: string;
|
|
26
32
|
/** Build URL (GitHub Actions run) */
|
|
@@ -36,8 +42,13 @@ export interface CiEventPayload {
|
|
|
36
42
|
/** GitHub Actions environment metadata */
|
|
37
43
|
github_run_id?: string;
|
|
38
44
|
github_run_number?: string;
|
|
45
|
+
github_run_attempt?: number;
|
|
39
46
|
github_workflow?: string;
|
|
40
47
|
github_actor?: string;
|
|
48
|
+
github_head_ref?: string;
|
|
49
|
+
github_ref?: string;
|
|
50
|
+
github_ref_name?: string;
|
|
51
|
+
github_event_name?: string;
|
|
41
52
|
}
|
|
42
53
|
/**
|
|
43
54
|
* Configuration for CI notifications
|
|
@@ -64,6 +75,22 @@ export interface NotifyOptions extends CiNotifyConfig {
|
|
|
64
75
|
workItemId?: string;
|
|
65
76
|
/** Optional service name */
|
|
66
77
|
serviceName?: string;
|
|
78
|
+
/** Optional PR URL override */
|
|
79
|
+
prUrl?: string;
|
|
80
|
+
/** Optional PR title override */
|
|
81
|
+
prTitle?: string;
|
|
82
|
+
/** Optional PR number override */
|
|
83
|
+
prNumber?: string;
|
|
84
|
+
/** Optional branch override for manual/local runs */
|
|
85
|
+
branch?: string;
|
|
86
|
+
/** Optional repository override for manual/local runs */
|
|
87
|
+
repository?: string;
|
|
88
|
+
/** Optional commit SHA override for manual/local runs */
|
|
89
|
+
commitSha?: string;
|
|
90
|
+
/** Optional legacy snake_case commit SHA override */
|
|
91
|
+
commit_sha?: string;
|
|
92
|
+
/** Optional delivery ID override for idempotency */
|
|
93
|
+
deliveryId?: string;
|
|
67
94
|
}
|
|
68
95
|
/**
|
|
69
96
|
* Response from Fortalece CI webhook
|
package/dist/ci/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ci/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,eAAe,GAAG,eAAe,GAAG,SAAS,CAAC;AACxF,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,iBAAiB;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,mBAAmB;IACnB,MAAM,EAAE,QAAQ,CAAC;IAEjB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB;IACjB,UAAU,EAAE,MAAM,CAAC;IAEnB,6CAA6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,0CAA0C;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ci/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,eAAe,GAAG,eAAe,GAAG,SAAS,CAAC;AACxF,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,iBAAiB;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,mBAAmB;IACnB,MAAM,EAAE,QAAQ,CAAC;IAEjB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB;IACjB,UAAU,EAAE,MAAM,CAAC;IAEnB,6CAA6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,0CAA0C;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,mBAAmB;IACnB,MAAM,EAAE,QAAQ,CAAC;IACjB,2BAA2B;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|