actions-up 0.1.0 → 1.0.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/dist/core/ast/update/apply-updates.js +18 -3
- package/dist/core/scan-github-actions.js +37 -9
- package/dist/package.js +1 -1
- package/package.json +1 -1
- package/readme.md +71 -23
|
@@ -12,9 +12,24 @@ async function applyUpdates(updates) {
|
|
|
12
12
|
let content = await readFile(filePath, "utf8");
|
|
13
13
|
for (let update of fileUpdates) {
|
|
14
14
|
if (!update.latestSha) continue;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
function escapeRegExp(string_) {
|
|
16
|
+
return string_.replaceAll(/[$()*+\-./?[\\\]^{|}]/gu, String.raw`\$&`);
|
|
17
|
+
}
|
|
18
|
+
let escapedName = escapeRegExp(update.action.name);
|
|
19
|
+
let escapedVersion = update.currentVersion ? escapeRegExp(update.currentVersion) : "";
|
|
20
|
+
if (escapedName.includes("\n") || escapedName.includes("\r")) {
|
|
21
|
+
console.error(`Invalid action name: ${update.action.name}`);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (escapedVersion && (escapedVersion.includes("\n") || escapedVersion.includes("\r"))) {
|
|
25
|
+
console.error(`Invalid version: ${update.currentVersion}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!/^[\da-f]{40}$/iu.test(update.latestSha)) {
|
|
29
|
+
console.error(`Invalid SHA format: ${update.latestSha}`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
let pattern = new RegExp(`(^\\s*-?\\s*uses:\\s*)(['"]?)(${escapedName})@${escapedVersion}\\2(\\s*#[^\\n]*)?`, "gm");
|
|
18
33
|
let replacement = `$1$2$3@${update.latestSha}$2 # ${update.latestVersion}`;
|
|
19
34
|
content = content.replace(pattern, replacement);
|
|
20
35
|
}
|
|
@@ -2,7 +2,7 @@ import { ACTIONS_DIRECTORY, GITHUB_DIRECTORY, WORKFLOWS_DIRECTORY } from "./cons
|
|
|
2
2
|
import { scanWorkflowFile } from "./scan-workflow-file.js";
|
|
3
3
|
import { scanActionFile } from "./scan-action-file.js";
|
|
4
4
|
import { isYamlFile } from "./fs/is-yaml-file.js";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { readdir, stat } from "node:fs/promises";
|
|
7
7
|
async function scanGitHubActions(rootPath = process.cwd()) {
|
|
8
8
|
let result = {
|
|
@@ -10,19 +10,39 @@ async function scanGitHubActions(rootPath = process.cwd()) {
|
|
|
10
10
|
workflows: /* @__PURE__ */ new Map(),
|
|
11
11
|
actions: []
|
|
12
12
|
};
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
let normalizedRoot = resolve(rootPath);
|
|
14
|
+
function isWithin(root, candidate) {
|
|
15
|
+
let relativePath = relative(root, candidate);
|
|
16
|
+
return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
17
|
+
}
|
|
18
|
+
let githubPath = join(normalizedRoot, GITHUB_DIRECTORY);
|
|
19
|
+
if (!isWithin(normalizedRoot, githubPath)) throw new Error("Invalid path: detected path traversal attempt");
|
|
20
|
+
function isValidName(name) {
|
|
21
|
+
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
22
|
+
console.warn(`Skipping invalid name: ${name}`);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
18
26
|
}
|
|
19
27
|
let workflowsPath = join(githubPath, WORKFLOWS_DIRECTORY);
|
|
28
|
+
if (!isWithin(normalizedRoot, workflowsPath)) return result;
|
|
20
29
|
try {
|
|
21
30
|
let workflowsStat = await stat(workflowsPath);
|
|
22
31
|
if (workflowsStat.isDirectory()) {
|
|
23
32
|
let files = await readdir(workflowsPath);
|
|
24
|
-
let workflowPromises = files.filter((file) =>
|
|
33
|
+
let workflowPromises = files.filter((file) => {
|
|
34
|
+
if (!isValidName(file)) return false;
|
|
35
|
+
return isYamlFile(file);
|
|
36
|
+
}).map(async (file) => {
|
|
25
37
|
let filePath = join(workflowsPath, file);
|
|
38
|
+
if (!isWithin(workflowsPath, filePath)) {
|
|
39
|
+
console.warn(`Skipping file outside workflows directory: ${file}`);
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
actions: [],
|
|
43
|
+
path: ""
|
|
44
|
+
};
|
|
45
|
+
}
|
|
26
46
|
try {
|
|
27
47
|
let actions = await scanWorkflowFile(filePath);
|
|
28
48
|
return {
|
|
@@ -39,29 +59,37 @@ async function scanGitHubActions(rootPath = process.cwd()) {
|
|
|
39
59
|
}
|
|
40
60
|
});
|
|
41
61
|
let workflowResults = await Promise.all(workflowPromises);
|
|
42
|
-
for (let workflow of workflowResults) if (workflow.success) if (workflow.actions.length > 0) {
|
|
62
|
+
for (let workflow of workflowResults) if (workflow.success && workflow.path) if (workflow.actions.length > 0) {
|
|
43
63
|
result.workflows.set(workflow.path, workflow.actions);
|
|
44
64
|
result.actions.push(...workflow.actions);
|
|
45
65
|
} else result.workflows.set(workflow.path, []);
|
|
46
66
|
}
|
|
47
67
|
} catch {}
|
|
48
|
-
let actionsPath = join(githubPath,
|
|
68
|
+
let actionsPath = join(githubPath, ACTIONS_DIRECTORY);
|
|
69
|
+
if (!isWithin(normalizedRoot, actionsPath)) return result;
|
|
49
70
|
try {
|
|
50
71
|
let actionsStat = await stat(actionsPath);
|
|
51
72
|
if (actionsStat.isDirectory()) {
|
|
52
73
|
let subdirectories = await readdir(actionsPath);
|
|
53
74
|
let actionPromises = subdirectories.map(async (subdir) => {
|
|
75
|
+
if (!isValidName(subdir)) return null;
|
|
54
76
|
let subdirPath = join(actionsPath, subdir);
|
|
77
|
+
if (!isWithin(actionsPath, subdirPath)) {
|
|
78
|
+
console.warn(`Skipping subdirectory outside actions path: ${subdir}`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
55
81
|
try {
|
|
56
82
|
let subdirectoryStat = await stat(subdirPath);
|
|
57
83
|
if (!subdirectoryStat.isDirectory()) return null;
|
|
58
84
|
let actionFilePath = join(subdirPath, "action.yml");
|
|
85
|
+
if (!isWithin(subdirPath, actionFilePath)) return null;
|
|
59
86
|
let actions = [];
|
|
60
87
|
try {
|
|
61
88
|
actions = await scanActionFile(actionFilePath);
|
|
62
89
|
} catch {
|
|
63
90
|
try {
|
|
64
91
|
actionFilePath = join(subdirPath, "action.yaml");
|
|
92
|
+
if (!isWithin(subdirPath, actionFilePath)) return null;
|
|
65
93
|
actions = await scanActionFile(actionFilePath);
|
|
66
94
|
} catch {
|
|
67
95
|
return null;
|
package/dist/package.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = "
|
|
1
|
+
const version = "1.0.0";
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -18,12 +18,12 @@ Interactively upgrade and pin actions to exact commit SHAs for secure, reproduci
|
|
|
18
18
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
|
-
- **Auto-discovery
|
|
22
|
-
- **SHA Pinning
|
|
23
|
-
- **Batch Updates
|
|
24
|
-
- **Interactive Selection
|
|
25
|
-
- **Breaking Changes Detection
|
|
26
|
-
- **Fast & Efficient
|
|
21
|
+
- **Auto-discovery**: Scans all workflows (`.github/workflows/*.yml`) and composite actions (`.github/actions/*/action.yml`)
|
|
22
|
+
- **SHA Pinning**: Updates actions to use commit SHA instead of tags for better security
|
|
23
|
+
- **Batch Updates**: Update multiple actions at once
|
|
24
|
+
- **Interactive Selection**: Choose which actions to update
|
|
25
|
+
- **Breaking Changes Detection**: Warns about major version updates
|
|
26
|
+
- **Fast & Efficient**: Parallel processing with optimized API calls
|
|
27
27
|
|
|
28
28
|
###
|
|
29
29
|
|
|
@@ -40,31 +40,72 @@ Interactively upgrade and pin actions to exact commit SHAs for secure, reproduci
|
|
|
40
40
|
/>
|
|
41
41
|
<img
|
|
42
42
|
src="https://raw.githubusercontent.com/azat-io/actions-up/main/assets/example-light.webp"
|
|
43
|
-
alt="
|
|
44
|
-
width="
|
|
43
|
+
alt="Actions Up interactive example"
|
|
44
|
+
width="820"
|
|
45
45
|
/>
|
|
46
46
|
</picture>
|
|
47
47
|
|
|
48
|
+
## Why
|
|
49
|
+
|
|
50
|
+
### The Problem
|
|
51
|
+
|
|
52
|
+
Keeping GitHub Actions updated is a critical but tedious task:
|
|
53
|
+
|
|
54
|
+
- **Security Risk**: Using outdated actions with known vulnerabilities
|
|
55
|
+
- **Manual Hell**: Checking dozens of actions across multiple workflows by hand
|
|
56
|
+
- **Version Tags Are Mutable**: v1 or v2 tags can change without notice, breaking reproducibility
|
|
57
|
+
- **Time Sink**: Hours spent on maintenance that could be used for actual development
|
|
58
|
+
|
|
59
|
+
### The Solution
|
|
60
|
+
|
|
61
|
+
Actions Up transforms a painful manual process into a delightful experience:
|
|
62
|
+
|
|
63
|
+
| Without Actions Up | With Actions Up |
|
|
64
|
+
| :----------------------------- | :------------------------------- |
|
|
65
|
+
| Check each action manually | Scan all workflows in seconds |
|
|
66
|
+
| Risk using vulnerable versions | SHA pinning for maximum security |
|
|
67
|
+
| 30+ minutes per repository | Under 1 minute total |
|
|
68
|
+
|
|
69
|
+
## GitHub Token Required
|
|
70
|
+
|
|
71
|
+
> **Important**: GitHub API has strict rate limits (60 requests/hour without token vs 5000 with token).
|
|
72
|
+
> A GitHub token is **practically required** for using Actions Up.
|
|
73
|
+
|
|
74
|
+
### Quick Token Setup
|
|
75
|
+
|
|
76
|
+
[Create a GitHub Personal Access Token](https://github.com/settings/tokens/new?scopes=public_repo&description=actions-up).
|
|
77
|
+
|
|
78
|
+
- For public repositories: Select `public_repo` scope
|
|
79
|
+
- For private repositories: Select `repo` scope
|
|
80
|
+
|
|
48
81
|
## Installation
|
|
49
82
|
|
|
83
|
+
Quick use (no installation)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx actions-up
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Global installation
|
|
90
|
+
|
|
50
91
|
```bash
|
|
51
92
|
npm install -g actions-up
|
|
52
93
|
```
|
|
53
94
|
|
|
54
|
-
|
|
95
|
+
Per-project
|
|
55
96
|
|
|
56
97
|
```bash
|
|
57
|
-
|
|
98
|
+
npm install --save-dev actions-up
|
|
58
99
|
```
|
|
59
100
|
|
|
60
101
|
## Usage
|
|
61
102
|
|
|
62
103
|
### Interactive Mode (Default)
|
|
63
104
|
|
|
64
|
-
Run in your repository root:
|
|
105
|
+
Run in your repository root with GitHub token:
|
|
65
106
|
|
|
66
107
|
```bash
|
|
67
|
-
actions-up
|
|
108
|
+
GITHUB_TOKEN=ghp_xxxx npx actions-up
|
|
68
109
|
```
|
|
69
110
|
|
|
70
111
|
This will:
|
|
@@ -79,17 +120,30 @@ This will:
|
|
|
79
120
|
Skip all prompts and update everything:
|
|
80
121
|
|
|
81
122
|
```bash
|
|
82
|
-
actions-up --yes
|
|
123
|
+
GITHUB_TOKEN=ghp_xxxx npx actions-up --yes
|
|
83
124
|
# or
|
|
84
|
-
actions-up -y
|
|
125
|
+
GITHUB_TOKEN=ghp_xxxx npx actions-up -y
|
|
85
126
|
```
|
|
86
127
|
|
|
87
|
-
|
|
128
|
+
## Pro Tips
|
|
129
|
+
|
|
130
|
+
### Shell Aliases
|
|
88
131
|
|
|
89
|
-
|
|
132
|
+
Add to your `.zshrc`, `.bashrc` or `.config/fish/config.fish`:
|
|
90
133
|
|
|
91
134
|
```bash
|
|
92
|
-
|
|
135
|
+
# Basic alias with token from environment
|
|
136
|
+
export GITHUB_TOKEN=ghp_xxxx # Add this once to your shell config
|
|
137
|
+
alias actions-up='GITHUB_TOKEN=$GITHUB_TOKEN npx actions-up'
|
|
138
|
+
|
|
139
|
+
# With token from file
|
|
140
|
+
alias actions-up='GITHUB_TOKEN=$(cat ~/.github-token) npx actions-up'
|
|
141
|
+
|
|
142
|
+
# With 1Password CLI
|
|
143
|
+
alias actions-up='GITHUB_TOKEN=$(op read "op://Personal/GitHub/token") npx actions-up'
|
|
144
|
+
|
|
145
|
+
# With macOS Keychain
|
|
146
|
+
alias actions-up='GITHUB_TOKEN=$(security find-generic-password -w -s "github-token") npx actions-up'
|
|
93
147
|
```
|
|
94
148
|
|
|
95
149
|
## Example
|
|
@@ -104,12 +158,6 @@ GITHUB_TOKEN=ghp_xxxx actions-up
|
|
|
104
158
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
105
159
|
```
|
|
106
160
|
|
|
107
|
-
## Configuration
|
|
108
|
-
|
|
109
|
-
### Environment Variables
|
|
110
|
-
|
|
111
|
-
- `GITHUB_TOKEN` - GitHub personal access token for API requests (optional but recommended)
|
|
112
|
-
|
|
113
161
|
## Security
|
|
114
162
|
|
|
115
163
|
Actions Up promotes security best practices:
|