@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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=notify.test.d.ts.map
@@ -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
@@ -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
- function parseArgs(args) {
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
- // Run CLI
154
- main().catch((error) => {
155
- console.error('Fatal error:', error);
156
- process.exit(1);
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
+ }
@@ -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): Promise<CiNotifyResponse>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/ci/notify.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,cAAc,EAEd,cAAc,EACd,gBAAgB,EAEhB,aAAa,EACd,MAAM,YAAY,CAAC;AAqCpB;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAwC3B;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAkChF;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"}
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: process.env.GITHUB_REF_NAME || process.env.GITHUB_HEAD_REF || '',
13
- commit_sha: process.env.GITHUB_SHA || '',
14
- github_run_id: process.env.GITHUB_RUN_ID,
15
- github_run_number: process.env.GITHUB_RUN_NUMBER,
16
- github_workflow: process.env.GITHUB_WORKFLOW,
17
- github_actor: process.env.GITHUB_ACTOR,
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: githubEnv.repository || 'unknown/unknown',
96
- branch: githubEnv.branch || 'unknown',
97
- commit_sha: githubEnv.commit_sha || 'unknown',
98
- work_item_id: workItemId,
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
- return notifyCiEvent({ webhookUrl, webhookSecret }, payload);
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
@@ -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
@@ -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;CACvB;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;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walker-di/fortalece-ai-sdk",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "type": "module",
5
5
  "description": "Fortalece AI Web Components SDK - Support Chat and Feedback widgets",
6
6
  "author": "Fortalece AI",