@codfish/actions 0.0.0-PR-58--24ced07

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,432 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import yml from 'js-yaml';
5
+ import path from 'path';
6
+
7
+ /**
8
+ * Generate documentation for all GitHub Actions in the repository
9
+ */
10
+ class DocumentationGenerator {
11
+ constructor() {
12
+ this.rootDir = process.cwd();
13
+ this.actions = [];
14
+ }
15
+
16
+ /**
17
+ * Find all action directories by looking for action.yml files
18
+ */
19
+ findActionDirectories() {
20
+ const entries = fs.readdirSync(this.rootDir, { withFileTypes: true });
21
+
22
+ return entries
23
+ .filter(entry => entry.isDirectory())
24
+ .filter(entry => !entry.name.startsWith('.') && entry.name !== 'node_modules')
25
+ .map(entry => entry.name)
26
+ .filter(dirName => {
27
+ const actionFile = path.join(this.rootDir, dirName, 'action.yml');
28
+ return fs.existsSync(actionFile);
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Parse action.yml file to extract metadata
34
+ */
35
+ parseActionFile(dirName) {
36
+ const actionFile = path.join(this.rootDir, dirName, 'action.yml');
37
+
38
+ try {
39
+ const content = fs.readFileSync(actionFile, 'utf8');
40
+ const actionData = yml.load(content);
41
+
42
+ return {
43
+ directory: dirName,
44
+ name: actionData.name || dirName,
45
+ description: actionData.description || 'No description available',
46
+ inputs: actionData.inputs || {},
47
+ outputs: actionData.outputs || {},
48
+ rawData: actionData,
49
+ };
50
+ } catch (error) {
51
+ console.error(`Error parsing ${actionFile}:`, error.message);
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Extract usage example from README.md
58
+ */
59
+ extractUsageExample(dirName) {
60
+ const readmeFile = path.join(this.rootDir, dirName, 'README.md');
61
+
62
+ if (!fs.existsSync(readmeFile)) {
63
+ return null;
64
+ }
65
+
66
+ try {
67
+ const content = fs.readFileSync(readmeFile, 'utf8');
68
+
69
+ // Look for usage examples in various sections
70
+ const patterns = [
71
+ // Look for "## Usage" section with yml code block
72
+ /## Usage[\s\S]*?```yml\n([\s\S]*?)\n```/i,
73
+ // Look for any yml code block with "uses: "
74
+ /```yml\n([\s\S]*?uses:\s*[.\w/-]+[\s\S]*?)\n```/i,
75
+ // Look for specific action usage
76
+ new RegExp(`\`\`\`yml\\n([\\s\\S]*?uses:\\s*[^\\n]*${dirName}[\\s\\S]*?)\\n\`\`\``, 'i'),
77
+ ];
78
+
79
+ for (const pattern of patterns) {
80
+ const match = content.match(pattern);
81
+ if (match && match[1]) {
82
+ // Clean up the example and ensure it's properly formatted
83
+ const example = match[1].trim();
84
+
85
+ // If it doesn't start with a step name, add one
86
+ if (!example.match(/^\s*-\s*name:/m) && !example.match(/^\s*-\s*uses:/m)) {
87
+ return `- uses: codfish/actions/${dirName}@v3\n${example.replace(/^/gm, ' ')}`;
88
+ }
89
+
90
+ return example;
91
+ }
92
+ }
93
+
94
+ // Fallback: create a basic example based on inputs
95
+ return this.generateBasicExample(dirName);
96
+ } catch (error) {
97
+ console.error(`Error reading README for ${dirName}:`, error.message);
98
+ return this.generateBasicExample(dirName);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Generate a basic usage example based on action inputs
104
+ */
105
+ generateBasicExample(dirName, inputs = {}) {
106
+ let example = `- uses: codfish/actions/${dirName}@v3`;
107
+
108
+ const inputKeys = Object.keys(inputs);
109
+ if (inputKeys.length > 0) {
110
+ example += '\n with:';
111
+
112
+ // Add required inputs first
113
+ const requiredInputs = inputKeys.filter(key => inputs[key].required);
114
+ const optionalInputs = inputKeys.filter(key => !inputs[key].required);
115
+
116
+ [...requiredInputs, ...optionalInputs.slice(0, 2)].forEach(key => {
117
+ const input = inputs[key];
118
+ let value = 'value';
119
+
120
+ // Smart defaults based on input name
121
+ if (key.includes('token')) value = '${{ secrets.TOKEN_NAME }}';
122
+ else if (key.includes('version')) value = 'lts/*';
123
+ else if (key.includes('message')) value = 'Your message here';
124
+ else if (key.includes('tag')) value = 'tag-name';
125
+ else if (input.default) value = input.default;
126
+
127
+ example += `\n ${key}: ${value}`;
128
+ });
129
+ }
130
+
131
+ return example;
132
+ }
133
+
134
+ /**
135
+ * Generate markdown table for inputs or outputs
136
+ */
137
+ generateTable(items, type = 'inputs') {
138
+ if (!items || Object.keys(items).length === 0) {
139
+ return `*No ${type}*`;
140
+ }
141
+
142
+ const headers = type === 'inputs' ? '| Input | Description | Required | Default |' : '| Output | Description |';
143
+
144
+ const separator = type === 'inputs' ? '|-------|-------------|----------|---------|' : '|--------|-------------|';
145
+
146
+ let table = `${headers}\n${separator}`;
147
+
148
+ Object.entries(items).forEach(([key, config]) => {
149
+ const description = config.description || 'No description';
150
+
151
+ if (type === 'inputs') {
152
+ const required = config.required ? 'Yes' : 'No';
153
+ const defaultValue = config.default ? `\`${config.default}\` ` : '-';
154
+ table += `\n| \`${key}\` | ${description} | ${required} | ${defaultValue} |`;
155
+ } else {
156
+ table += `\n| \`${key}\` | ${description} |`;
157
+ }
158
+ });
159
+
160
+ return table;
161
+ }
162
+
163
+ /**
164
+ * Generate markdown section for a single action
165
+ */
166
+ generateActionSection(action) {
167
+ const { directory, name, description, inputs, outputs } = action;
168
+ const usageExample = this.extractUsageExample(directory);
169
+
170
+ let section = `### [${name}](./${directory}/)\n\n`;
171
+ section += `${description}\n\n`;
172
+
173
+ // Add inputs table
174
+ section += `**Inputs:**\n\n${this.generateTable(inputs, 'inputs')}\n\n`;
175
+
176
+ // Add outputs table if there are outputs
177
+ if (outputs && Object.keys(outputs).length > 0) {
178
+ section += `**Outputs:**\n\n${this.generateTable(outputs, 'outputs')}\n\n`;
179
+ }
180
+
181
+ // Add usage example
182
+ if (usageExample) {
183
+ section += `**Usage:**\n\n\`\`\`yml\n${usageExample}\n\`\`\`\n\n`;
184
+ }
185
+
186
+ return section;
187
+ }
188
+
189
+ /**
190
+ * Generate just the content for actions (without the section header)
191
+ */
192
+ generateAvailableActionsContent() {
193
+ const actionDirs = this.findActionDirectories();
194
+
195
+ console.log(`Found ${actionDirs.length} action directories:`, actionDirs);
196
+
197
+ this.actions = actionDirs
198
+ .map(dir => this.parseActionFile(dir))
199
+ .filter(action => action !== null)
200
+ .sort((a, b) => a.name.localeCompare(b.name));
201
+
202
+ let content = '';
203
+
204
+ this.actions.forEach(action => {
205
+ content += this.generateActionSection(action);
206
+ });
207
+
208
+ return content.trim(); // Remove trailing newlines
209
+ }
210
+
211
+ /**
212
+ * Update the main README.md file using file descriptors for security
213
+ */
214
+ updateReadme() {
215
+ const readmePath = path.join(this.rootDir, 'README.md');
216
+
217
+ if (!fs.existsSync(readmePath)) {
218
+ console.error('README.md not found');
219
+ return false;
220
+ }
221
+
222
+ let fd;
223
+ try {
224
+ // Open file descriptor for reading and writing
225
+ fd = fs.openSync(readmePath, 'r+');
226
+
227
+ // Read content using file descriptor
228
+ const stats = fs.fstatSync(fd);
229
+ const buffer = Buffer.alloc(stats.size);
230
+ fs.readSync(fd, buffer, 0, stats.size, 0);
231
+ let content = buffer.toString('utf8');
232
+
233
+ // Find the action docs markers
234
+ const startMarker = '<!-- start action docs -->';
235
+ const endMarker = '<!-- end action docs -->';
236
+
237
+ const startIndex = content.indexOf(startMarker);
238
+ const endIndex = content.indexOf(endMarker);
239
+
240
+ if (startIndex === -1) {
241
+ console.error(`Could not find "${startMarker}" in README.md`);
242
+ console.error('Please add the marker where you want action documentation to be generated');
243
+ return false;
244
+ }
245
+
246
+ if (endIndex === -1) {
247
+ console.error(`Could not find "${endMarker}" in README.md`);
248
+ console.error('Please add the end marker after the start marker');
249
+ return false;
250
+ }
251
+
252
+ if (endIndex <= startIndex) {
253
+ console.error('End marker must come after start marker');
254
+ return false;
255
+ }
256
+
257
+ // Replace content between markers
258
+ const beforeMarker = content.substring(0, startIndex + startMarker.length);
259
+ const afterMarker = content.substring(endIndex);
260
+
261
+ const newContent = this.generateAvailableActionsContent();
262
+ const updatedContent = beforeMarker + '\n' + newContent + '\n' + afterMarker;
263
+
264
+ // Truncate and write back using file descriptor
265
+ fs.ftruncateSync(fd, 0);
266
+ fs.writeSync(fd, updatedContent, 0, 'utf8');
267
+
268
+ console.log('✅ README.md updated successfully!');
269
+ console.log(`📝 Generated documentation for ${this.actions.length} actions`);
270
+
271
+ return true;
272
+ } catch (error) {
273
+ console.error('Error updating README.md:', error.message);
274
+ return false;
275
+ } finally {
276
+ // Always close the file descriptor
277
+ if (fd !== undefined) {
278
+ try {
279
+ fs.closeSync(fd);
280
+ } catch (closeError) {
281
+ console.error('Error closing README.md file descriptor:', closeError.message);
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Update individual action README files with inputs/outputs using file descriptors for security
289
+ */
290
+ updateActionReadmes() {
291
+ const actionDirs = this.findActionDirectories();
292
+ let updatedCount = 0;
293
+
294
+ actionDirs.forEach(dirName => {
295
+ const readmePath = path.join(this.rootDir, dirName, 'README.md');
296
+
297
+ if (!fs.existsSync(readmePath)) {
298
+ console.log(`⚠️ No README.md found in ${dirName}, skipping`);
299
+ return;
300
+ }
301
+
302
+ const actionData = this.parseActionFile(dirName);
303
+ if (!actionData) {
304
+ console.log(`⚠️ Could not parse action.yml for ${dirName}, skipping`);
305
+ return;
306
+ }
307
+
308
+ let fd;
309
+ try {
310
+ // Open file descriptor for reading and writing
311
+ fd = fs.openSync(readmePath, 'r+');
312
+
313
+ // Read content using file descriptor
314
+ const stats = fs.fstatSync(fd);
315
+ const buffer = Buffer.alloc(stats.size);
316
+ fs.readSync(fd, buffer, 0, stats.size, 0);
317
+ let content = buffer.toString('utf8');
318
+ let modified = false;
319
+
320
+ // Update inputs section
321
+ const inputsStartMarker = '<!-- start inputs -->';
322
+ const inputsEndMarker = '<!-- end inputs -->';
323
+ const inputsStart = content.indexOf(inputsStartMarker);
324
+ const inputsEnd = content.indexOf(inputsEndMarker);
325
+
326
+ if (inputsStart !== -1 && inputsEnd !== -1 && inputsEnd > inputsStart) {
327
+ const inputsTable = this.generateTable(actionData.inputs, 'inputs');
328
+ const beforeInputs = content.substring(0, inputsStart + inputsStartMarker.length);
329
+ const afterInputs = content.substring(inputsEnd);
330
+ content = beforeInputs + '\n\n' + inputsTable + '\n\n' + afterInputs;
331
+ modified = true;
332
+ console.log(`✅ Updated inputs section in ${dirName}/README.md`);
333
+ }
334
+
335
+ // Update outputs section
336
+ const outputsStartMarker = '<!-- start outputs -->';
337
+ const outputsEndMarker = '<!-- end outputs -->';
338
+ const outputsStart = content.indexOf(outputsStartMarker);
339
+ const outputsEnd = content.indexOf(outputsEndMarker);
340
+
341
+ if (outputsStart !== -1 && outputsEnd !== -1 && outputsEnd > outputsStart) {
342
+ const outputsTable = this.generateTable(actionData.outputs, 'outputs');
343
+ const beforeOutputs = content.substring(0, outputsStart + outputsStartMarker.length);
344
+ const afterOutputs = content.substring(outputsEnd);
345
+ content = beforeOutputs + '\n\n' + outputsTable + '\n\n' + afterOutputs;
346
+ modified = true;
347
+ console.log(`✅ Updated outputs section in ${dirName}/README.md`);
348
+ }
349
+
350
+ if (modified) {
351
+ // Truncate and write back using file descriptor
352
+ fs.ftruncateSync(fd, 0);
353
+ fs.writeSync(fd, content, 0, 'utf8');
354
+ updatedCount++;
355
+ }
356
+ } catch (error) {
357
+ console.error(`Error updating ${dirName}/README.md:`, error.message);
358
+ } finally {
359
+ // Always close the file descriptor
360
+ if (fd !== undefined) {
361
+ try {
362
+ fs.closeSync(fd);
363
+ } catch (closeError) {
364
+ console.error(`Error closing ${dirName}/README.md file descriptor:`, closeError.message);
365
+ }
366
+ }
367
+ }
368
+ });
369
+
370
+ return updatedCount;
371
+ }
372
+
373
+ /**
374
+ * Run prettier formatting on all documentation files
375
+ */
376
+ async formatDocs() {
377
+ const { execSync } = await import('child_process');
378
+
379
+ try {
380
+ console.log('\n🎨 Formatting documentation with prettier...');
381
+ execSync('pnpm format', {
382
+ stdio: 'inherit',
383
+ cwd: this.rootDir,
384
+ });
385
+ console.log('✅ Documentation formatting complete!');
386
+ return true;
387
+ } catch (error) {
388
+ console.error('❌ Prettier formatting failed:', error.message);
389
+ return false;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Run the documentation generation
395
+ */
396
+ async run() {
397
+ console.log('🔍 Scanning for GitHub Actions...');
398
+
399
+ // Update main README
400
+ const mainSuccess = this.updateReadme();
401
+
402
+ // Update individual action READMEs
403
+ console.log('\n🔍 Updating individual action README files...');
404
+ const updatedActionCount = this.updateActionReadmes();
405
+
406
+ if (mainSuccess) {
407
+ console.log(`\n📚 Documentation generation complete!`);
408
+ console.log(`📝 Updated main README.md with ${this.actions.length} actions`);
409
+ if (updatedActionCount > 0) {
410
+ console.log(`📝 Updated ${updatedActionCount} action README files`);
411
+ }
412
+
413
+ // Format the documentation
414
+ const formatSuccess = await this.formatDocs();
415
+
416
+ if (formatSuccess) {
417
+ console.log('\n🎉 All documentation updated and formatted successfully!');
418
+ console.log('Run `git diff` to see all changes.');
419
+ } else {
420
+ console.log('\n⚠️ Documentation updated but formatting failed.');
421
+ console.log('You may want to run `pnpm format` manually.');
422
+ }
423
+ } else {
424
+ console.error('\n❌ Main README documentation generation failed!');
425
+ process.exit(1);
426
+ }
427
+ }
428
+ }
429
+
430
+ // Run the generator
431
+ const generator = new DocumentationGenerator();
432
+ generator.run();
@@ -0,0 +1,82 @@
1
+ # comment
2
+
3
+ Creates or updates pull request comments with intelligent upsert functionality using unique tags.
4
+
5
+ <!-- DOCTOC SKIP -->
6
+
7
+ ## Usage
8
+
9
+ See [action.yml](action.yml).
10
+
11
+ ```yml
12
+ - name: Comment on PR
13
+ uses: codfish/actions/comment@v3
14
+ with:
15
+ message: '✅ Build successful!'
16
+ tag: 'build-status'
17
+ upsert: true
18
+ ```
19
+
20
+ ## Inputs
21
+
22
+ <!-- start inputs -->
23
+
24
+ | Input | Description | Required | Default |
25
+ | --------- | ------------------------------------------------------------------------------------- | -------- | ------- |
26
+ | `message` | The comment message content (supports markdown formatting) | Yes | - |
27
+ | `tag` | Unique identifier to find and update existing comments (required when upsert is true) | No | - |
28
+ | `upsert` | Update existing comment with matching tag instead of creating new comment | No | `false` |
29
+
30
+ <!-- end inputs -->
31
+
32
+ ## Examples
33
+
34
+ ### Basic comment
35
+
36
+ ```yml
37
+ - uses: codfish/actions/comment@v3
38
+ with:
39
+ message: 'Hello from GitHub Actions! 👋'
40
+ ```
41
+
42
+ ### Updating comments with upsert
43
+
44
+ Use the `upsert` feature to update the same comment instead of creating multiple comments:
45
+
46
+ ```yml
47
+ - name: Update build status
48
+ uses: codfish/actions/comment@v3
49
+ with:
50
+ message: |
51
+ ## Build Status
52
+ ⏳ Build in progress...
53
+ tag: 'build-status'
54
+ upsert: true
55
+
56
+ # Later in the workflow...
57
+ - name: Update build status
58
+ uses: codfish/actions/comment@v3
59
+ with:
60
+ message: |
61
+ ## Build Status
62
+ ✅ Build completed successfully!
63
+ tag: 'build-status'
64
+ upsert: true
65
+ ```
66
+
67
+ ### Multi-line markdown comment
68
+
69
+ ```yml
70
+ - uses: codfish/actions/comment@v3
71
+ with:
72
+ message: |
73
+ ## 📊 Test Results
74
+
75
+ - ✅ Unit tests: 42 passed
76
+ - ✅ Integration tests: 12 passed
77
+ - 📦 Coverage: 98%
78
+
79
+ Great work! 🎉
80
+ tag: 'test-results'
81
+ upsert: true
82
+ ```
@@ -0,0 +1,102 @@
1
+ name: comment
2
+
3
+ description: Creates or updates a comment in a pull request with optional tagging for upsert functionality
4
+
5
+ inputs:
6
+ message:
7
+ description: The comment message content (supports markdown formatting)
8
+ required: true
9
+ tag:
10
+ description: Unique identifier to find and update existing comments (required when upsert is true)
11
+ required: false
12
+ upsert:
13
+ description: Update existing comment with matching tag instead of creating new comment
14
+ required: false
15
+ default: 'false'
16
+
17
+ runs:
18
+ using: composite
19
+
20
+ steps:
21
+ - name: Validate inputs and set globals
22
+ id: globals
23
+ shell: bash
24
+ run: |
25
+ # Validate required inputs
26
+ if [ -z "${{ inputs.message }}" ]; then
27
+ echo "❌ ERROR: 'message' input is required"
28
+ exit 1
29
+ fi
30
+
31
+ # Validate upsert logic
32
+ if [ "${{ inputs.upsert }}" = "true" ] && [ -z "${{ inputs.tag }}" ]; then
33
+ echo "❌ ERROR: 'tag' input is required when upsert is true"
34
+ exit 1
35
+ fi
36
+
37
+ # Create dynamic tag based on repository name
38
+ repo_namespace="${{ github.repository }}"
39
+ tag="<!-- ${repo_namespace}/comment ${{ inputs.tag }} -->"
40
+ body=$(printf '${{ inputs.message }}')
41
+
42
+ echo "💬 Comment tag: $tag"
43
+ echo "tag=$tag" >> $GITHUB_OUTPUT
44
+ echo "body<<EOF"$'\n'"$body"'\n'"$tag"$'\n'EOF >> "$GITHUB_OUTPUT"
45
+
46
+ - name: Check existing comments
47
+ id: check-comments
48
+ if: inputs.upsert == 'true'
49
+ uses: actions/github-script@v8
50
+ with:
51
+ script: |
52
+ try {
53
+ const comments = await github.rest.issues.listComments({
54
+ owner: context.repo.owner,
55
+ repo: context.repo.repo,
56
+ issue_number: context.issue.number,
57
+ });
58
+ const existingComment = comments.data.find(comment => comment.body.includes('${{ steps.globals.outputs.tag }}'));
59
+ core.setOutput('comment-id', existingComment ? existingComment.id : null);
60
+
61
+ if (existingComment) {
62
+ console.log(`Found existing comment with ID: ${existingComment.id}`);
63
+ } else {
64
+ console.log('No existing comment found, will create new one');
65
+ }
66
+ } catch (error) {
67
+ core.setFailed(`Failed to check existing comments: ${error.message}`);
68
+ }
69
+
70
+ - name: Update existing comment
71
+ if: steps.check-comments.outputs.comment-id != null
72
+ uses: actions/github-script@v8
73
+ with:
74
+ script: |
75
+ try {
76
+ await github.rest.issues.updateComment({
77
+ owner: context.repo.owner,
78
+ repo: context.repo.repo,
79
+ comment_id: ${{ steps.check-comments.outputs.comment-id }},
80
+ body: `${{ steps.globals.outputs.body }}`,
81
+ });
82
+ console.log('✅ Successfully updated existing comment');
83
+ } catch (error) {
84
+ core.setFailed(`Failed to update comment: ${error.message}`);
85
+ }
86
+
87
+ - name: Create new comment
88
+ if: steps.check-comments.outputs.comment-id == null
89
+ uses: actions/github-script@v8
90
+ with:
91
+ script: |
92
+ try {
93
+ const response = await github.rest.issues.createComment({
94
+ owner: context.repo.owner,
95
+ repo: context.repo.repo,
96
+ issue_number: context.issue.number,
97
+ body: `${{ steps.globals.outputs.body }}`,
98
+ });
99
+ console.log(`✅ Successfully created new comment with ID: ${response.data.id}`);
100
+ } catch (error) {
101
+ core.setFailed(`Failed to create comment: ${error.message}`);
102
+ }