@fromeroc9/testform 1.0.3 → 1.0.5

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.
Files changed (50) hide show
  1. package/dist/action/index.js +1 -1
  2. package/dist/action.js +60 -0
  3. package/dist/adapters/github.js +467 -0
  4. package/dist/adapters/resources.js +363 -0
  5. package/dist/cli/index.js +3 -3
  6. package/dist/commands/apply.js +390 -0
  7. package/dist/commands/destroy.js +85 -0
  8. package/dist/commands/diff.js +131 -0
  9. package/dist/commands/fmt.js +166 -0
  10. package/dist/commands/force-unlock.js +55 -0
  11. package/dist/commands/generate.js +143 -0
  12. package/dist/commands/graph.js +159 -0
  13. package/dist/commands/import.js +222 -0
  14. package/dist/commands/init.js +167 -0
  15. package/dist/commands/login.js +71 -0
  16. package/dist/commands/logout.js +20 -0
  17. package/dist/commands/plan.js +250 -0
  18. package/dist/commands/refresh.js +165 -0
  19. package/dist/commands/report.js +724 -0
  20. package/dist/commands/show.js +61 -0
  21. package/dist/commands/state.js +197 -0
  22. package/dist/commands/taint.js +49 -0
  23. package/dist/commands/validate.js +128 -0
  24. package/dist/commands/workspace.js +102 -0
  25. package/dist/const.js +105 -0
  26. package/dist/core/backends/azurerm.js +201 -0
  27. package/dist/core/backends/backend.js +2 -0
  28. package/dist/core/backends/gcs.js +200 -0
  29. package/dist/core/backends/local.js +162 -0
  30. package/dist/core/backends/s3.js +224 -0
  31. package/dist/core/command-context.js +59 -0
  32. package/dist/core/config.js +131 -0
  33. package/dist/core/credentials.js +53 -0
  34. package/dist/core/parser.js +62 -0
  35. package/dist/core/parsers/base-parser.js +215 -0
  36. package/dist/core/parsers/testcase-parser.js +115 -0
  37. package/dist/core/parsers/testplan-parser.js +41 -0
  38. package/dist/core/parsers/testrun-parser.js +43 -0
  39. package/dist/core/policy.js +341 -0
  40. package/dist/core/prompt.js +109 -0
  41. package/dist/core/state.js +185 -0
  42. package/dist/core/utils.js +94 -0
  43. package/dist/core/variables.js +108 -0
  44. package/dist/core/workspace.js +56 -0
  45. package/dist/help.js +797 -0
  46. package/dist/index.js +650 -0
  47. package/dist/logger.js +134 -0
  48. package/dist/notify.js +36 -0
  49. package/dist/types.js +2 -0
  50. package/package.json +1 -1
package/dist/action.js ADDED
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const core = __importStar(require("@actions/core"));
37
+ const index_1 = require("./index");
38
+ async function run() {
39
+ try {
40
+ const command = core.getInput('command', { required: true });
41
+ const workingDirectory = core.getInput('working-directory');
42
+ const argsStr = core.getInput('args');
43
+ // Parse arguments
44
+ const args = argsStr ? argsStr.split(' ') : [];
45
+ // Construct process.argv simulating CLI
46
+ // argv[0] = node, argv[1] = testform, argv[2] = command
47
+ const mockArgv = ['node', 'testform'];
48
+ if (workingDirectory && workingDirectory !== '.') {
49
+ mockArgv.push(`-chdir=${workingDirectory}`);
50
+ }
51
+ mockArgv.push(command);
52
+ mockArgv.push(...args);
53
+ process.argv = mockArgv;
54
+ await (0, index_1.main)();
55
+ }
56
+ catch (error) {
57
+ core.setFailed(error.message);
58
+ }
59
+ }
60
+ run();
@@ -0,0 +1,467 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GitHubAdapter = void 0;
4
+ const notify_1 = require("../notify");
5
+ const credentials_1 = require("../core/credentials");
6
+ class GitHubAdapter {
7
+ token;
8
+ owner;
9
+ repo;
10
+ projectId;
11
+ constructor(config) {
12
+ const creds = new credentials_1.Credentials();
13
+ const envToken = Object.prototype.hasOwnProperty.call(process.env, config.tokenEnv) ? process.env[config.tokenEnv] : undefined;
14
+ let token = creds.getToken('github.com') || envToken || process.env.GITHUB_TOKEN;
15
+ // Allow user to provide token directly instead of env var name
16
+ if (!token && (config.tokenEnv.startsWith('ghp_') || config.tokenEnv.startsWith('github_pat_'))) {
17
+ token = config.tokenEnv;
18
+ }
19
+ if (!token) {
20
+ notify_1.notify.push({ type: 'error', title: `GitHub token not found`, detail: [`Environment variable "${config.tokenEnv}" is not set.`], close: true });
21
+ }
22
+ this.token = token || '';
23
+ this.owner = config.owner;
24
+ this.repo = config.repository;
25
+ this.projectId = config.projectId;
26
+ }
27
+ async request(method, path, body) {
28
+ const url = path.startsWith('http') ? path : `https://api.github.com${path}`;
29
+ const response = await fetch(url, {
30
+ method,
31
+ headers: {
32
+ 'Authorization': `Bearer ${this.token}`,
33
+ 'Accept': 'application/vnd.github.v3+json',
34
+ 'X-GitHub-Api-Version': '2022-11-28',
35
+ 'Content-Type': 'application/json',
36
+ },
37
+ body: body ? JSON.stringify(body) : undefined,
38
+ });
39
+ if (!response.ok) {
40
+ const errorText = await response.text();
41
+ const error = new Error(errorText);
42
+ error.name = `GitHub API Error [${response.status}]`;
43
+ throw error;
44
+ }
45
+ // Handle empty responses (like 204 No Content)
46
+ if (response.status === 204) {
47
+ return null;
48
+ }
49
+ return await response.json();
50
+ }
51
+ async graphql(query, variables = {}) {
52
+ const response = await this.request('POST', 'https://api.github.com/graphql', { query, variables });
53
+ if (response.errors && response.errors.length > 0) {
54
+ const error = new Error(JSON.stringify(response.errors));
55
+ error.name = 'GitHub API Error';
56
+ throw error;
57
+ }
58
+ return response.data;
59
+ }
60
+ /**
61
+ * Create a GitHub Issue.
62
+ */
63
+ async createIssue(payload) {
64
+ const data = await this.request('POST', `/repos/${this.owner}/${this.repo}/issues`, {
65
+ title: payload.title,
66
+ body: payload.body,
67
+ labels: payload.labels,
68
+ assignees: payload.assignees,
69
+ milestone: payload.milestone,
70
+ });
71
+ return {
72
+ id: data.id,
73
+ number: data.number,
74
+ title: data.title ?? '',
75
+ state: data.state,
76
+ node_id: data.node_id,
77
+ created_at: data.created_at,
78
+ updated_at: data.updated_at,
79
+ };
80
+ }
81
+ /**
82
+ * Update an existing GitHub Issue.
83
+ */
84
+ async updateIssue(issueNumber, payload) {
85
+ const data = await this.request('PATCH', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}`, {
86
+ title: payload.title,
87
+ body: payload.body,
88
+ labels: payload.labels,
89
+ assignees: payload.assignees,
90
+ milestone: payload.milestone,
91
+ });
92
+ return {
93
+ id: data.id,
94
+ number: data.number,
95
+ title: data.title ?? '',
96
+ state: data.state,
97
+ node_id: data.node_id,
98
+ created_at: data.created_at,
99
+ updated_at: data.updated_at,
100
+ };
101
+ }
102
+ /**
103
+ * Close a GitHub Issue (soft delete).
104
+ */
105
+ async closeIssue(issueNumber) {
106
+ await this.request('PATCH', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}`, {
107
+ state: 'closed',
108
+ });
109
+ }
110
+ /**
111
+ * Get a specific issue by number (for refresh).
112
+ */
113
+ async getIssue(issueNumber) {
114
+ try {
115
+ const data = await this.request('GET', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}`);
116
+ return {
117
+ id: data.id,
118
+ number: data.number,
119
+ title: data.title ?? '',
120
+ body: data.body ?? '',
121
+ state: data.state,
122
+ node_id: data.node_id,
123
+ labels: data.labels?.map((l) => l.name) ?? [],
124
+ assignees: data.assignees?.map((a) => a.login) ?? [],
125
+ milestone: data.milestone?.title ?? '',
126
+ };
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ /**
133
+ * List all issues with a specific label (for refresh/diff).
134
+ */
135
+ async listIssuesByLabel(label) {
136
+ const results = [];
137
+ let page = 1;
138
+ while (true) {
139
+ const data = await this.request('GET', `/repos/${this.owner}/${this.repo}/issues?labels=${encodeURIComponent(label)}&state=all&per_page=100&page=${page}`);
140
+ if (data.length === 0)
141
+ break;
142
+ for (const issue of data) {
143
+ results.push({
144
+ id: issue.id,
145
+ number: issue.number,
146
+ title: issue.title ?? '',
147
+ state: issue.state,
148
+ });
149
+ }
150
+ if (data.length < 100)
151
+ break;
152
+ page++;
153
+ }
154
+ return results;
155
+ }
156
+ /**
157
+ * Look up a milestone's ID by its title (case-insensitive).
158
+ */
159
+ async getMilestoneByTitle(title) {
160
+ if (!title)
161
+ return undefined;
162
+ try {
163
+ // Note: In a real-world scenario with many milestones, we'd need pagination.
164
+ const milestones = await this.request('GET', `/repos/${this.owner}/${this.repo}/milestones?state=all&per_page=100`);
165
+ const match = milestones.find((m) => m.title.toLowerCase() === title.toLowerCase());
166
+ return match ? match.number : undefined;
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ }
172
+ /**
173
+ * Create a comment on an issue.
174
+ */
175
+ async createIssueComment(issueNumber, body) {
176
+ const data = await this.request('POST', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`, { body });
177
+ return { id: data.id };
178
+ }
179
+ /**
180
+ * Update an existing comment.
181
+ */
182
+ async updateIssueComment(commentId, body) {
183
+ await this.request('PATCH', `/repos/${this.owner}/${this.repo}/issues/comments/${commentId}`, { body });
184
+ }
185
+ /**
186
+ * List all comments on an issue.
187
+ */
188
+ async listIssueComments(issueNumber) {
189
+ const results = [];
190
+ let page = 1;
191
+ while (true) {
192
+ const data = await this.request('GET', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments?per_page=100&page=${page}`);
193
+ if (data.length === 0)
194
+ break;
195
+ for (const comment of data) {
196
+ results.push({ id: comment.id, body: comment.body });
197
+ }
198
+ if (data.length < 100)
199
+ break;
200
+ page++;
201
+ }
202
+ return results;
203
+ }
204
+ projectNodeId;
205
+ projectFields;
206
+ /**
207
+ * Resolve the GraphQL node ID for a Project V2 and cache its fields.
208
+ */
209
+ async resolveProjectMetadata() {
210
+ if (this.projectNodeId && this.projectFields)
211
+ return { id: this.projectNodeId, fields: this.projectFields };
212
+ if (!this.projectId)
213
+ return undefined;
214
+ const fieldsQuery = `
215
+ id
216
+ fields(first: 100) {
217
+ nodes {
218
+ ... on ProjectV2Field { id name dataType }
219
+ ... on ProjectV2SingleSelectField { id name dataType options { id name } }
220
+ ... on ProjectV2IterationField { id name dataType configuration { iterations { id title } } }
221
+ }
222
+ }
223
+ `;
224
+ try {
225
+ // Try as a user first
226
+ const userResult = await this.graphql(`
227
+ query($login: String!, $number: Int!) {
228
+ user(login: $login) {
229
+ projectV2(number: $number) {
230
+ ${fieldsQuery}
231
+ }
232
+ }
233
+ }
234
+ `, { login: this.owner, number: this.projectId });
235
+ if (userResult?.user?.projectV2?.id) {
236
+ this.projectNodeId = userResult.user.projectV2.id;
237
+ this.projectFields = userResult.user.projectV2.fields.nodes;
238
+ return { id: this.projectNodeId, fields: this.projectFields };
239
+ }
240
+ }
241
+ catch (error) {
242
+ // Not a user or project not found
243
+ }
244
+ try {
245
+ // Fallback to organization
246
+ const orgResult = await this.graphql(`
247
+ query($login: String!, $number: Int!) {
248
+ organization(login: $login) {
249
+ projectV2(number: $number) {
250
+ ${fieldsQuery}
251
+ }
252
+ }
253
+ }
254
+ `, { login: this.owner, number: this.projectId });
255
+ if (orgResult?.organization?.projectV2?.id) {
256
+ this.projectNodeId = orgResult.organization.projectV2.id;
257
+ this.projectFields = orgResult.organization.projectV2.fields.nodes;
258
+ return { id: this.projectNodeId, fields: this.projectFields };
259
+ }
260
+ }
261
+ catch (error) {
262
+ // Not an org or project not found
263
+ }
264
+ return undefined;
265
+ }
266
+ /**
267
+ * Add an issue to a GitHub Project v2 (GraphQL).
268
+ * Only called if projectId is configured. Returns the Item ID inside the project.
269
+ */
270
+ async addToProject(issueNodeId) {
271
+ const meta = await this.resolveProjectMetadata();
272
+ if (!meta)
273
+ return undefined;
274
+ try {
275
+ const res = await this.graphql(`
276
+ mutation($projectId: ID!, $contentId: ID!) {
277
+ addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
278
+ item { id }
279
+ }
280
+ }
281
+ `, {
282
+ projectId: meta.id,
283
+ contentId: issueNodeId,
284
+ });
285
+ return res?.addProjectV2ItemById?.item?.id;
286
+ }
287
+ catch (e) {
288
+ const error = new Error('Failed to link issue to GitHub Project');
289
+ error.name = e.message;
290
+ throw error;
291
+ }
292
+ }
293
+ /**
294
+ * Update fields of a Project V2 item (GraphQL).
295
+ */
296
+ async updateProjectItemFields(itemId, customFields) {
297
+ const meta = await this.resolveProjectMetadata();
298
+ if (!meta || !meta.fields)
299
+ return;
300
+ for (const [key, val] of Object.entries(customFields)) {
301
+ // Find field in project by name (case insensitive)
302
+ const field = meta.fields.find(f => f.name && f.name.toLowerCase() === key.toLowerCase());
303
+ if (!field)
304
+ continue; // Field not mapped or doesn't exist in project
305
+ let fieldValue = undefined;
306
+ if (field.dataType === 'TEXT') {
307
+ if (val !== undefined && val !== null) {
308
+ fieldValue = { text: String(val) };
309
+ }
310
+ }
311
+ else if (field.dataType === 'NUMBER') {
312
+ if (val !== undefined && val !== null) {
313
+ const parsedNumber = Number(val);
314
+ if (!isNaN(parsedNumber)) {
315
+ fieldValue = { number: parsedNumber };
316
+ }
317
+ }
318
+ }
319
+ else if (field.dataType === 'DATE') {
320
+ if (val !== undefined && val !== null) {
321
+ // Try to extract YYYY-MM-DD
322
+ const dateStr = String(val).split('T')[0];
323
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
324
+ fieldValue = { date: dateStr };
325
+ }
326
+ else {
327
+ // Fallback, let GitHub API reject if invalid
328
+ fieldValue = { date: String(val) };
329
+ }
330
+ }
331
+ }
332
+ else if (field.dataType === 'SINGLE_SELECT' && field.options) {
333
+ // Find option ID by name (case insensitive, ignoring '@' prefix)
334
+ if (typeof val !== 'string')
335
+ continue;
336
+ const cleanVal = val.startsWith('@') ? val.substring(1) : val;
337
+ const opt = field.options.find((o) => o.name.toLowerCase() === cleanVal.toLowerCase());
338
+ if (opt) {
339
+ fieldValue = { singleSelectOptionId: opt.id };
340
+ }
341
+ }
342
+ else if (field.dataType === 'ITERATION' && field.configuration?.iterations) {
343
+ if (typeof val !== 'string')
344
+ continue;
345
+ const cleanVal = val.startsWith('@') ? val.substring(1) : val;
346
+ const opt = field.configuration.iterations.find((o) => o.title.toLowerCase() === cleanVal.toLowerCase());
347
+ if (opt) {
348
+ fieldValue = { iterationId: opt.id };
349
+ }
350
+ }
351
+ if (!fieldValue)
352
+ continue;
353
+ try {
354
+ await this.graphql(`
355
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {
356
+ updateProjectV2ItemFieldValue(input: {
357
+ projectId: $projectId
358
+ itemId: $itemId
359
+ fieldId: $fieldId
360
+ value: $value
361
+ }) { projectV2Item { id } }
362
+ }
363
+ `, {
364
+ projectId: meta.id,
365
+ itemId: itemId,
366
+ fieldId: field.id,
367
+ value: fieldValue
368
+ });
369
+ }
370
+ catch (e) {
371
+ const error = new Error(`Failed to assign custom field`);
372
+ error.name = `${field.name}: ${e.message}`;
373
+ throw error;
374
+ }
375
+ }
376
+ }
377
+ /**
378
+ * Get fields of a Project V2 item (GraphQL).
379
+ * Returns a map of field name (lowercase) to its value.
380
+ */
381
+ async getProjectItemFields(issueNodeId) {
382
+ const meta = await this.resolveProjectMetadata();
383
+ if (!meta || !meta.fields)
384
+ return {};
385
+ try {
386
+ const res = await this.graphql(`
387
+ query($id: ID!) {
388
+ node(id: $id) {
389
+ ... on Issue {
390
+ projectItems(first: 10) {
391
+ nodes {
392
+ project { id }
393
+ fieldValues(first: 100) {
394
+ nodes {
395
+ ... on ProjectV2ItemFieldTextValue { text field { ... on ProjectV2FieldCommon { name } } }
396
+ ... on ProjectV2ItemFieldSingleSelectValue { name field { ... on ProjectV2FieldCommon { name } } }
397
+ ... on ProjectV2ItemFieldIterationValue { title field { ... on ProjectV2FieldCommon { name } } }
398
+ }
399
+ }
400
+ }
401
+ }
402
+ }
403
+ }
404
+ }
405
+ `, { id: issueNodeId });
406
+ const projectItems = res?.node?.projectItems?.nodes || [];
407
+ // Find the project item that matches our project ID
408
+ const item = projectItems.find((i) => i.project?.id === meta.id);
409
+ if (!item || !item.fieldValues || !item.fieldValues.nodes)
410
+ return {};
411
+ const customFields = Object.create(null);
412
+ for (const val of item.fieldValues.nodes) {
413
+ const fieldName = val.field?.name;
414
+ if (!fieldName)
415
+ continue;
416
+ let stringValue = '';
417
+ if (val.text !== undefined)
418
+ stringValue = val.text;
419
+ else if (val.name !== undefined)
420
+ stringValue = val.name;
421
+ else if (val.title !== undefined)
422
+ stringValue = val.title;
423
+ customFields[fieldName.toLowerCase()] = stringValue;
424
+ }
425
+ return customFields;
426
+ }
427
+ catch {
428
+ return {};
429
+ }
430
+ }
431
+ /**
432
+ * Get the node_id of an issue (needed for GraphQL Project v2 operations).
433
+ */
434
+ async getIssueNodeId(issueNumber) {
435
+ try {
436
+ const data = await this.request('GET', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}`);
437
+ return data.node_id ?? null;
438
+ }
439
+ catch {
440
+ return null;
441
+ }
442
+ }
443
+ /**
444
+ * Get the remote ID format: "owner/repo:issueNumber"
445
+ */
446
+ formatRemoteId(issueNumber) {
447
+ return `${this.repo}:${issueNumber}`;
448
+ }
449
+ /**
450
+ * Add a sub-issue to a parent issue (REST API).
451
+ */
452
+ async addSubIssue(parentIssueNumber, subIssueId) {
453
+ try {
454
+ await this.request('POST', `/repos/${this.owner}/${this.repo}/issues/${parentIssueNumber}/sub_issues`, {
455
+ sub_issue_id: subIssueId,
456
+ replace_parent: true
457
+ });
458
+ }
459
+ catch (error) {
460
+ // Ignore if it's already a sub-issue or other non-fatal errors
461
+ if (!error.message.includes('422')) {
462
+ throw error;
463
+ }
464
+ }
465
+ }
466
+ }
467
+ exports.GitHubAdapter = GitHubAdapter;