@codfish/actions 1.1.0
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/.github/codeql-config.yml +21 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/claude-code-review.yml +43 -0
- package/.github/workflows/claude.yml +39 -0
- package/.github/workflows/release.yml +48 -0
- package/.github/workflows/security.yml +103 -0
- package/.github/workflows/update-docs.yml +38 -0
- package/.github/workflows/validate.yml +210 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/AGENT.md +129 -0
- package/CLAUDE.md +3 -0
- package/CONTRIBUTING.md +316 -0
- package/README.md +207 -0
- package/SECURITY.md +208 -0
- package/bin/generate-docs.js +432 -0
- package/comment/README.md +82 -0
- package/comment/action.yml +102 -0
- package/eslint.config.js +8 -0
- package/npm-publish-pr/README.md +145 -0
- package/npm-publish-pr/action.yml +171 -0
- package/package.json +52 -0
- package/setup-node-and-install/README.md +139 -0
- package/setup-node-and-install/action.yml +220 -0
- package/tests/fixtures/.node-version +1 -0
- package/tests/fixtures/.nvmrc +1 -0
- package/tests/fixtures/lockfiles/package-lock.json +12 -0
- package/tests/fixtures/lockfiles/pnpm-lock.yaml +9 -0
- package/tests/fixtures/lockfiles/yarn.lock +7 -0
- package/tests/fixtures/package-json/minimal.json +4 -0
- package/tests/fixtures/package-json/scoped.json +6 -0
- package/tests/fixtures/package-json/valid.json +13 -0
- package/tests/integration/comment/basic.bats +95 -0
- package/tests/integration/npm-pr-version/basic.bats +353 -0
- package/tests/integration/setup-node-and-install/basic.bats +200 -0
- package/tests/scripts/test-helpers.sh +113 -0
- package/tests/scripts/test-runner.sh +115 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
<!-- prettier-ignore-start -->
|
|
4
|
+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
5
|
+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
6
|
+
## Table of Contents
|
|
7
|
+
|
|
8
|
+
- [Supported Versions](#supported-versions)
|
|
9
|
+
- [Reporting a Vulnerability](#reporting-a-vulnerability)
|
|
10
|
+
- [š Private Disclosure](#-private-disclosure)
|
|
11
|
+
- [š What to Include](#-what-to-include)
|
|
12
|
+
- [š Response Timeline](#-response-timeline)
|
|
13
|
+
- [Security Best Practices for Users](#security-best-practices-for-users)
|
|
14
|
+
- [š Secrets Management](#-secrets-management)
|
|
15
|
+
- [š·ļø Action Versioning](#-action-versioning)
|
|
16
|
+
- [š Workflow Permissions](#-workflow-permissions)
|
|
17
|
+
- [š”ļø Input Validation](#-input-validation)
|
|
18
|
+
- [Security Features](#security-features)
|
|
19
|
+
- [š Automated Security Scanning](#-automated-security-scanning)
|
|
20
|
+
- [š”ļø Secure Development Practices](#-secure-development-practices)
|
|
21
|
+
- [š Supply Chain Security](#-supply-chain-security)
|
|
22
|
+
- [Known Security Considerations](#known-security-considerations)
|
|
23
|
+
- [GitHub Actions Environment](#github-actions-environment)
|
|
24
|
+
- [npm Publishing (npm-pr-version)](#npm-publishing-npm-pr-version)
|
|
25
|
+
- [Comment Actions](#comment-actions)
|
|
26
|
+
- [Incident Response](#incident-response)
|
|
27
|
+
- [Security Contact](#security-contact)
|
|
28
|
+
- [Acknowledgments](#acknowledgments)
|
|
29
|
+
|
|
30
|
+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
31
|
+
<!-- prettier-ignore-end -->
|
|
32
|
+
|
|
33
|
+
## Supported Versions
|
|
34
|
+
|
|
35
|
+
This project follows a rolling release model. We provide security updates for:
|
|
36
|
+
|
|
37
|
+
| Version | Supported |
|
|
38
|
+
| ------------------- | ------------------- |
|
|
39
|
+
| main | ā
Always supported |
|
|
40
|
+
| Latest release tags | ā
Supported |
|
|
41
|
+
| Older releases | ā Not supported |
|
|
42
|
+
|
|
43
|
+
## Reporting a Vulnerability
|
|
44
|
+
|
|
45
|
+
If you discover a security issue, please follow these steps:
|
|
46
|
+
|
|
47
|
+
### š Private Disclosure
|
|
48
|
+
|
|
49
|
+
**Do NOT create a public issue for security vulnerabilities.**
|
|
50
|
+
|
|
51
|
+
Instead, please report security issues privately using one of these methods:
|
|
52
|
+
|
|
53
|
+
1. **GitHub Security Advisories** (preferred)
|
|
54
|
+
- Go to the [Security tab](https://github.com/codfish/actions/security/advisories)
|
|
55
|
+
- Click "Report a vulnerability"
|
|
56
|
+
- Fill out the form with details
|
|
57
|
+
|
|
58
|
+
2. **Email**
|
|
59
|
+
- Send details to: [chris@codfish.dev](mailto:chris@codfish.dev)
|
|
60
|
+
- Include "SECURITY" in the subject line
|
|
61
|
+
|
|
62
|
+
### š What to Include
|
|
63
|
+
|
|
64
|
+
When reporting a vulnerability, please include:
|
|
65
|
+
|
|
66
|
+
- **Description** of the vulnerability
|
|
67
|
+
- **Steps to reproduce** the issue
|
|
68
|
+
- **Potential impact** of the vulnerability
|
|
69
|
+
- **Suggested fix** (if you have one)
|
|
70
|
+
- **Your contact information** for follow-up
|
|
71
|
+
|
|
72
|
+
### š Response Timeline
|
|
73
|
+
|
|
74
|
+
We aim to respond to security reports within:
|
|
75
|
+
|
|
76
|
+
- **Initial response**: 24-48 hours
|
|
77
|
+
- **Confirmation/triage**: 2-5 business days
|
|
78
|
+
- **Resolution**: Varies based on complexity
|
|
79
|
+
|
|
80
|
+
## Security Best Practices for Users
|
|
81
|
+
|
|
82
|
+
When using these GitHub Actions in your workflows:
|
|
83
|
+
|
|
84
|
+
### š Secrets Management
|
|
85
|
+
|
|
86
|
+
- **Never log secrets** in workflows that use these actions
|
|
87
|
+
- Use **GitHub Secrets** for sensitive information
|
|
88
|
+
- **Limit secret scope** to only necessary workflows
|
|
89
|
+
- **Rotate secrets** regularly
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
# ā
Good - Using secrets properly
|
|
93
|
+
- uses: codfish/actions/npm-pr-version@v1
|
|
94
|
+
with:
|
|
95
|
+
npm-token: ${{ secrets.NPM_TOKEN }}
|
|
96
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
97
|
+
|
|
98
|
+
# ā Bad - Exposing secrets
|
|
99
|
+
- name: Debug
|
|
100
|
+
run: echo "Token: ${{ secrets.NPM_TOKEN }}"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### š·ļø Action Versioning
|
|
104
|
+
|
|
105
|
+
- **Pin to specific versions or commit hashes** for production workflows
|
|
106
|
+
- **Avoid using `@main`** in production (use for testing only)
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
# ā
Good - Pinned version
|
|
110
|
+
- uses: codfish/actions/setup-node-and-install@v1.2.3
|
|
111
|
+
|
|
112
|
+
# ā ļø Caution - Latest main (testing only)
|
|
113
|
+
- uses: codfish/actions/setup-node-and-install@main
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### š Workflow Permissions
|
|
117
|
+
|
|
118
|
+
- **Use minimal permissions** required
|
|
119
|
+
- **Specify explicit permissions** when possible
|
|
120
|
+
- **Avoid using `write-all`** permissions
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
# ā
Good - Minimal permissions
|
|
124
|
+
permissions:
|
|
125
|
+
contents: read
|
|
126
|
+
issues: write
|
|
127
|
+
pull-requests: write
|
|
128
|
+
|
|
129
|
+
# ā Bad - Excessive permissions
|
|
130
|
+
permissions: write-all
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### š”ļø Input Validation
|
|
134
|
+
|
|
135
|
+
- **Validate user inputs** before using them in actions
|
|
136
|
+
- **Sanitize outputs** when displaying them
|
|
137
|
+
- **Be cautious with dynamic expressions**
|
|
138
|
+
|
|
139
|
+
## Security Features
|
|
140
|
+
|
|
141
|
+
This project implements several security measures:
|
|
142
|
+
|
|
143
|
+
### š Automated Security Scanning
|
|
144
|
+
|
|
145
|
+
- **Dependabot** for dependency updates
|
|
146
|
+
- **CodeQL** for static analysis
|
|
147
|
+
- **Dependency Review** for PR security checks
|
|
148
|
+
- **Secret scanning** with TruffleHog
|
|
149
|
+
- **npm audit** for vulnerability detection
|
|
150
|
+
|
|
151
|
+
### š”ļø Secure Development Practices
|
|
152
|
+
|
|
153
|
+
- **Input validation** in all actions
|
|
154
|
+
- **Error handling** without information disclosure
|
|
155
|
+
- **No secret logging** in any action
|
|
156
|
+
- **Least privilege** principle in action permissions
|
|
157
|
+
|
|
158
|
+
### š Supply Chain Security
|
|
159
|
+
|
|
160
|
+
- **Minimal dependencies** to reduce attack surface
|
|
161
|
+
- **Regular dependency updates** via Dependabot
|
|
162
|
+
- **Verified action references** in workflows
|
|
163
|
+
|
|
164
|
+
## Known Security Considerations
|
|
165
|
+
|
|
166
|
+
### GitHub Actions Environment
|
|
167
|
+
|
|
168
|
+
- **Actions run in GitHub's infrastructure** - we cannot control the runner environment
|
|
169
|
+
- **Secrets are available** to all steps in a job that has access
|
|
170
|
+
- **Workflow logs are visible** to users with read access to the repository
|
|
171
|
+
|
|
172
|
+
### npm Publishing (npm-pr-version)
|
|
173
|
+
|
|
174
|
+
- **NPM tokens have broad permissions** - ensure tokens are scoped appropriately
|
|
175
|
+
- **Published packages are public** by default - review package contents
|
|
176
|
+
- **Version immutability** - published versions cannot be unpublished
|
|
177
|
+
|
|
178
|
+
### Comment Actions
|
|
179
|
+
|
|
180
|
+
- **GitHub tokens can comment** on behalf of the workflow user
|
|
181
|
+
- **Comment content is public** - avoid including sensitive information
|
|
182
|
+
- **Rate limiting applies** - excessive commenting may be throttled
|
|
183
|
+
|
|
184
|
+
## Incident Response
|
|
185
|
+
|
|
186
|
+
In case of a confirmed security vulnerability:
|
|
187
|
+
|
|
188
|
+
1. **Assessment** - Evaluate severity and impact
|
|
189
|
+
2. **Mitigation** - Develop and test fixes
|
|
190
|
+
3. **Disclosure** - Coordinate with reporter on disclosure timeline
|
|
191
|
+
4. **Release** - Deploy security fixes
|
|
192
|
+
5. **Communication** - Notify users through appropriate channels
|
|
193
|
+
|
|
194
|
+
## Security Contact
|
|
195
|
+
|
|
196
|
+
- **Primary**: [security@codfish.dev](mailto:security@codfish.dev)
|
|
197
|
+
- **GitHub**: [@codfish](https://github.com/codfish)
|
|
198
|
+
|
|
199
|
+
## Acknowledgments
|
|
200
|
+
|
|
201
|
+
We appreciate security researchers and users who responsibly disclose vulnerabilities. Contributors who report valid
|
|
202
|
+
security issues will be acknowledged (with permission) in:
|
|
203
|
+
|
|
204
|
+
- Security advisories
|
|
205
|
+
- Release notes
|
|
206
|
+
- This security policy
|
|
207
|
+
|
|
208
|
+
Thank you for helping keep this project secure! š
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import yaml 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 = yaml.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 yaml code block
|
|
72
|
+
/## Usage[\s\S]*?```yaml\n([\s\S]*?)\n```/i,
|
|
73
|
+
// Look for any yaml code block with "uses: "
|
|
74
|
+
/```yaml\n([\s\S]*?uses:\s*[.\w/-]+[\s\S]*?)\n```/i,
|
|
75
|
+
// Look for specific action usage
|
|
76
|
+
new RegExp(`\`\`\`yaml\\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}@v1\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}@v1`;
|
|
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\`\`\`yaml\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();
|