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.
@@ -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
- let escapedName = update.action.name.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`);
16
- let escapedVersion = update.currentVersion?.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`);
17
- let pattern = new RegExp(String.raw`(^\s*-?\s*uses:\s*)(['"]?)(${escapedName})@${escapedVersion}\2(\s*#[^\n]*)?`, "gm");
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 githubPath = join(rootPath, GITHUB_DIRECTORY);
14
- try {
15
- await stat(githubPath);
16
- } catch {
17
- return result;
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) => isYamlFile(file)).map(async (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, "actions");
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 = "0.1.0";
1
+ const version = "1.0.0";
2
2
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actions-up",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Interactive CLI tool to update GitHub Actions to latest versions with SHA pinning",
5
5
  "keywords": [
6
6
  "github-actions",
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** - 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
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="Token Limit CLI Example"
44
- width="600"
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
- Or use directly with npx:
95
+ Per-project
55
96
 
56
97
  ```bash
57
- npx actions-up
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
- ### With GitHub Token
128
+ ## Pro Tips
129
+
130
+ ### Shell Aliases
88
131
 
89
- To avoid rate limits [create a GitHub personal access token](https://github.com/settings/tokens/new?scopes=public_repo&description=actions-up) and set it as an environment variable:
132
+ Add to your `.zshrc`, `.bashrc` or `.config/fish/config.fish`:
90
133
 
91
134
  ```bash
92
- GITHUB_TOKEN=ghp_xxxx actions-up
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: