actions-up 0.1.0 ā 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/dist/cli/index.js +7 -1
- package/dist/core/api/check-updates.js +1 -1
- package/dist/core/api/client.d.ts +4 -3
- package/dist/core/api/client.js +125 -129
- 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 +2 -2
- package/readme.md +50 -24
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import pc from "picocolors";
|
|
|
10
10
|
import cac from "cac";
|
|
11
11
|
function run() {
|
|
12
12
|
let cli = cac("actions-up");
|
|
13
|
-
cli.help().version(version).option("--yes, -y", "Skip all confirmations").command("", "Update GitHub Actions").action(async (options) => {
|
|
13
|
+
cli.help().version(version).option("--yes, -y", "Skip all confirmations").option("--dry-run", "Preview changes without applying them").command("", "Update GitHub Actions").action(async (options) => {
|
|
14
14
|
console.info(pc.cyan("\nš Actions Up!\n"));
|
|
15
15
|
let spinner = createSpinner("Scanning GitHub Actions...").start();
|
|
16
16
|
try {
|
|
@@ -33,6 +33,12 @@ function run() {
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
spinner.success(`Found ${pc.yellow(outdated.length)} updates available${breaking.length > 0 ? ` (${pc.red(breaking.length)} breaking)` : ""}`);
|
|
36
|
+
if (options.dryRun) {
|
|
37
|
+
console.info(pc.yellow("\nš Dry Run - No changes will be made\n"));
|
|
38
|
+
for (let update of outdated) console.info(`${pc.cyan(update.action.file ?? "unknown")}:\n${update.action.name}: ${pc.red(update.currentVersion)} ā ${pc.green(update.latestVersion)} ${update.latestSha ? pc.gray(`(${update.latestSha.slice(0, 7)})`) : ""}\n`);
|
|
39
|
+
console.info(pc.gray(`\n${outdated.length} actions would be updated\n`));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
36
42
|
if (options.yes) {
|
|
37
43
|
let toUpdate = outdated.filter((update) => update.latestSha);
|
|
38
44
|
if (toUpdate.length === 0) {
|
|
@@ -69,7 +69,7 @@ async function checkUpdates(actions, token) {
|
|
|
69
69
|
}
|
|
70
70
|
}), Promise.resolve([]));
|
|
71
71
|
if (sharedState.rateLimitError) {
|
|
72
|
-
let error = /* @__PURE__ */ new Error("GitHub API rate limit exceeded. Please set GITHUB_TOKEN environment variable to increase the limit.\nSee: https://github.com/azat-io/actions-up?tab=readme-ov-file#
|
|
72
|
+
let error = /* @__PURE__ */ new Error("GitHub API rate limit exceeded. Please set GITHUB_TOKEN environment variable to increase the limit.\nSee: https://github.com/azat-io/actions-up?tab=readme-ov-file#using-github-token-for-higher-rate-limits");
|
|
73
73
|
error.name = "GitHubRateLimitError";
|
|
74
74
|
throw error;
|
|
75
75
|
}
|
|
@@ -26,11 +26,11 @@ interface TagInfo {
|
|
|
26
26
|
/** Git commit SHA that this tag points to. */
|
|
27
27
|
sha: string;
|
|
28
28
|
}
|
|
29
|
-
/** GitHub
|
|
29
|
+
/** GitHub REST API client with optional authentication. */
|
|
30
30
|
export declare class Client {
|
|
31
|
-
private readonly graphqlWithAuth;
|
|
32
|
-
private rateLimitRemaining;
|
|
33
31
|
private rateLimitReset;
|
|
32
|
+
private rateLimitRemaining;
|
|
33
|
+
private readonly octokit;
|
|
34
34
|
/**
|
|
35
35
|
* Creates a new GitHub API client.
|
|
36
36
|
*
|
|
@@ -75,5 +75,6 @@ export declare class Client {
|
|
|
75
75
|
* @returns True if rate limit is below threshold.
|
|
76
76
|
*/
|
|
77
77
|
shouldWaitForRateLimit(threshold?: number): boolean;
|
|
78
|
+
private updateRateLimitInfo;
|
|
78
79
|
}
|
|
79
80
|
export {};
|
package/dist/core/api/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
2
2
|
var GitHubRateLimitError = class extends Error {
|
|
3
3
|
constructor(resetAt) {
|
|
4
4
|
let resetTime = resetAt.toLocaleTimeString();
|
|
@@ -7,73 +7,93 @@ var GitHubRateLimitError = class extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
var Client = class Client {
|
|
10
|
-
graphqlWithAuth;
|
|
11
|
-
rateLimitRemaining = 5e3;
|
|
12
10
|
rateLimitReset = /* @__PURE__ */ new Date();
|
|
11
|
+
rateLimitRemaining = 60;
|
|
12
|
+
octokit;
|
|
13
13
|
constructor(token) {
|
|
14
14
|
let authToken = token ?? process.env["GITHUB_TOKEN"];
|
|
15
|
-
this.
|
|
15
|
+
this.octokit = new Octokit({ auth: authToken ?? void 0 });
|
|
16
16
|
if (!authToken) console.warn("No GitHub token found. API rate limits will be restricted.");
|
|
17
|
+
this.rateLimitRemaining = authToken ? 5e3 : 60;
|
|
17
18
|
}
|
|
18
19
|
static isRateLimitError(error) {
|
|
19
|
-
if (error
|
|
20
|
-
|
|
20
|
+
if (error && typeof error === "object") {
|
|
21
|
+
let maybeAny = error;
|
|
22
|
+
let message = typeof maybeAny.message === "string" ? maybeAny.message.toLowerCase() : "";
|
|
23
|
+
let status = typeof maybeAny.status === "number" ? maybeAny.status : void 0;
|
|
24
|
+
return message.includes("rate limit") || message.includes("api rate limit") || status === 403;
|
|
25
|
+
}
|
|
21
26
|
return false;
|
|
22
27
|
}
|
|
23
28
|
async getTagInfo(owner, repo, tag) {
|
|
24
29
|
try {
|
|
25
|
-
let qualifiedTag = tag.startsWith("refs/tags/") ? tag : `refs/tags/${tag}`;
|
|
26
30
|
let displayTag = tag.replace(/^refs\/tags\//u, "");
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
31
|
+
try {
|
|
32
|
+
let { headers: releaseHeaders, data: releaseData } = await this.octokit.repos.getReleaseByTag({
|
|
33
|
+
tag: displayTag,
|
|
34
|
+
owner,
|
|
35
|
+
repo
|
|
36
|
+
});
|
|
37
|
+
this.updateRateLimitInfo(releaseHeaders);
|
|
38
|
+
let sha = null;
|
|
39
|
+
if (releaseData.target_commitish) try {
|
|
40
|
+
let { data: commitData } = await this.octokit.repos.getCommit({
|
|
41
|
+
ref: releaseData.target_commitish,
|
|
42
|
+
owner,
|
|
43
|
+
repo
|
|
44
|
+
});
|
|
45
|
+
({sha} = commitData);
|
|
46
|
+
} catch {
|
|
47
|
+
sha = releaseData.target_commitish;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
date: releaseData.published_at ? new Date(releaseData.published_at) : null,
|
|
51
|
+
sha: sha ?? releaseData.target_commitish,
|
|
52
|
+
message: releaseData.body ?? null,
|
|
53
|
+
tag: displayTag
|
|
54
|
+
};
|
|
55
|
+
} catch (releaseError) {
|
|
56
|
+
if (releaseError instanceof Error && "status" in releaseError && releaseError.status === 404) try {
|
|
57
|
+
let { headers: referenceHeaders, data: referenceData } = await this.octokit.git.getRef({
|
|
58
|
+
ref: `tags/${displayTag}`,
|
|
59
|
+
owner,
|
|
60
|
+
repo
|
|
61
|
+
});
|
|
62
|
+
this.updateRateLimitInfo(referenceHeaders);
|
|
63
|
+
let { sha } = referenceData.object;
|
|
64
|
+
let message = null;
|
|
65
|
+
let date = null;
|
|
66
|
+
if (referenceData.object.type === "tag") try {
|
|
67
|
+
let { data: tagData } = await this.octokit.git.getTag({
|
|
68
|
+
tag_sha: sha,
|
|
69
|
+
owner,
|
|
70
|
+
repo
|
|
71
|
+
});
|
|
72
|
+
({sha} = tagData.object);
|
|
73
|
+
message = tagData.message || null;
|
|
74
|
+
date = tagData.tagger.date ? new Date(tagData.tagger.date) : null;
|
|
75
|
+
} catch {}
|
|
76
|
+
else if (referenceData.object.type === "commit") try {
|
|
77
|
+
let { data: commitData } = await this.octokit.git.getCommit({
|
|
78
|
+
commit_sha: sha,
|
|
79
|
+
owner,
|
|
80
|
+
repo
|
|
81
|
+
});
|
|
82
|
+
({message} = commitData);
|
|
83
|
+
date = commitData.author.date ? new Date(commitData.author.date) : null;
|
|
84
|
+
} catch {}
|
|
85
|
+
return {
|
|
86
|
+
tag: displayTag,
|
|
87
|
+
message,
|
|
88
|
+
date,
|
|
89
|
+
sha
|
|
90
|
+
};
|
|
91
|
+
} catch (tagError) {
|
|
92
|
+
if (tagError instanceof Error && "status" in tagError && tagError.status === 404) return null;
|
|
93
|
+
throw tagError;
|
|
94
|
+
}
|
|
95
|
+
throw releaseError;
|
|
96
|
+
}
|
|
77
97
|
} catch (error) {
|
|
78
98
|
if (Client.isRateLimitError(error)) throw new GitHubRateLimitError(this.rateLimitReset);
|
|
79
99
|
throw error;
|
|
@@ -81,49 +101,32 @@ var Client = class Client {
|
|
|
81
101
|
}
|
|
82
102
|
async getAllReleases(owner, repo, limit = 10) {
|
|
83
103
|
try {
|
|
84
|
-
let
|
|
85
|
-
|
|
86
|
-
repository(owner: $owner, name: $repo) {
|
|
87
|
-
releases(
|
|
88
|
-
first: $limit
|
|
89
|
-
orderBy: { field: CREATED_AT, direction: DESC }
|
|
90
|
-
) {
|
|
91
|
-
nodes {
|
|
92
|
-
tagName
|
|
93
|
-
tagCommit {
|
|
94
|
-
oid
|
|
95
|
-
}
|
|
96
|
-
name
|
|
97
|
-
description
|
|
98
|
-
isPrerelease
|
|
99
|
-
publishedAt
|
|
100
|
-
url
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
rateLimit {
|
|
105
|
-
remaining
|
|
106
|
-
resetAt
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
`;
|
|
110
|
-
let response = await this.graphqlWithAuth(query, {
|
|
104
|
+
let { data: releases, headers } = await this.octokit.repos.listReleases({
|
|
105
|
+
per_page: limit,
|
|
111
106
|
owner,
|
|
112
|
-
limit,
|
|
113
107
|
repo
|
|
114
108
|
});
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
109
|
+
this.updateRateLimitInfo(headers);
|
|
110
|
+
let releaseInfos = [];
|
|
111
|
+
await Promise.all(releases.map(async (release) => {
|
|
112
|
+
let sha = null;
|
|
113
|
+
if (release.tag_name) try {
|
|
114
|
+
let tagInfo = await this.getTagInfo(owner, repo, release.tag_name);
|
|
115
|
+
if (tagInfo) ({sha} = tagInfo);
|
|
116
|
+
} catch {
|
|
117
|
+
sha = release.target_commitish || null;
|
|
118
|
+
}
|
|
119
|
+
releaseInfos.push({
|
|
120
|
+
publishedAt: new Date(release.published_at),
|
|
121
|
+
name: release.name ?? release.tag_name,
|
|
122
|
+
description: release.body ?? null,
|
|
123
|
+
isPrerelease: release.prerelease,
|
|
124
|
+
version: release.tag_name,
|
|
125
|
+
url: release.html_url,
|
|
126
|
+
sha
|
|
127
|
+
});
|
|
126
128
|
}));
|
|
129
|
+
return releaseInfos;
|
|
127
130
|
} catch (error) {
|
|
128
131
|
if (Client.isRateLimitError(error)) throw new GitHubRateLimitError(this.rateLimitReset);
|
|
129
132
|
throw error;
|
|
@@ -131,45 +134,29 @@ var Client = class Client {
|
|
|
131
134
|
}
|
|
132
135
|
async getLatestRelease(owner, repo) {
|
|
133
136
|
try {
|
|
134
|
-
let
|
|
135
|
-
query getLatestRelease($owner: String!, $repo: String!) {
|
|
136
|
-
repository(owner: $owner, name: $repo) {
|
|
137
|
-
latestRelease {
|
|
138
|
-
tagName
|
|
139
|
-
tagCommit {
|
|
140
|
-
oid
|
|
141
|
-
}
|
|
142
|
-
name
|
|
143
|
-
description
|
|
144
|
-
isPrerelease
|
|
145
|
-
publishedAt
|
|
146
|
-
url
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
rateLimit {
|
|
150
|
-
remaining
|
|
151
|
-
resetAt
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
`;
|
|
155
|
-
let response = await this.graphqlWithAuth(query, {
|
|
137
|
+
let { data: release, headers } = await this.octokit.repos.getLatestRelease({
|
|
156
138
|
owner,
|
|
157
139
|
repo
|
|
158
140
|
});
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
141
|
+
this.updateRateLimitInfo(headers);
|
|
142
|
+
let sha = null;
|
|
143
|
+
if (release.tag_name) try {
|
|
144
|
+
let tagInfo = await this.getTagInfo(owner, repo, release.tag_name);
|
|
145
|
+
if (tagInfo) ({sha} = tagInfo);
|
|
146
|
+
} catch {
|
|
147
|
+
sha = release.target_commitish || null;
|
|
148
|
+
}
|
|
163
149
|
return {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
description: release.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
url: release.
|
|
170
|
-
|
|
150
|
+
publishedAt: new Date(release.published_at),
|
|
151
|
+
name: release.name ?? release.tag_name,
|
|
152
|
+
description: release.body ?? null,
|
|
153
|
+
isPrerelease: release.prerelease,
|
|
154
|
+
version: release.tag_name,
|
|
155
|
+
url: release.html_url,
|
|
156
|
+
sha
|
|
171
157
|
};
|
|
172
158
|
} catch (error) {
|
|
159
|
+
if (error instanceof Error && "status" in error && error.status === 404) return null;
|
|
173
160
|
if (Client.isRateLimitError(error)) throw new GitHubRateLimitError(this.rateLimitReset);
|
|
174
161
|
throw error;
|
|
175
162
|
}
|
|
@@ -183,5 +170,14 @@ var Client = class Client {
|
|
|
183
170
|
shouldWaitForRateLimit(threshold = 100) {
|
|
184
171
|
return this.rateLimitRemaining < threshold;
|
|
185
172
|
}
|
|
173
|
+
updateRateLimitInfo(headers) {
|
|
174
|
+
let remaining = headers["x-ratelimit-remaining"];
|
|
175
|
+
if (remaining !== void 0) this.rateLimitRemaining = typeof remaining === "string" ? Number.parseInt(remaining, 10) : remaining;
|
|
176
|
+
let reset = headers["x-ratelimit-reset"];
|
|
177
|
+
if (reset !== void 0) {
|
|
178
|
+
let resetTime = typeof reset === "string" ? Number.parseInt(reset, 10) : reset;
|
|
179
|
+
this.rateLimitReset = /* @__PURE__ */ new Date(resetTime * 1e3);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
186
182
|
};
|
|
187
183
|
export { Client };
|
|
@@ -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.1.0";
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actions-up",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Interactive CLI tool to update GitHub Actions to latest versions with SHA pinning",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"./dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@octokit/
|
|
39
|
+
"@octokit/rest": "^22.0.0",
|
|
40
40
|
"cac": "^6.7.14",
|
|
41
41
|
"enquirer": "^2.4.1",
|
|
42
42
|
"nanospinner": "^1.2.2",
|
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,21 +40,50 @@ 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
|
+
|
|
48
69
|
## Installation
|
|
49
70
|
|
|
71
|
+
Quick use (no installation)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx actions-up
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Global installation
|
|
78
|
+
|
|
50
79
|
```bash
|
|
51
80
|
npm install -g actions-up
|
|
52
81
|
```
|
|
53
82
|
|
|
54
|
-
|
|
83
|
+
Per-project
|
|
55
84
|
|
|
56
85
|
```bash
|
|
57
|
-
|
|
86
|
+
npm install --save-dev actions-up
|
|
58
87
|
```
|
|
59
88
|
|
|
60
89
|
## Usage
|
|
@@ -64,7 +93,7 @@ npx actions-up
|
|
|
64
93
|
Run in your repository root:
|
|
65
94
|
|
|
66
95
|
```bash
|
|
67
|
-
actions-up
|
|
96
|
+
npx actions-up
|
|
68
97
|
```
|
|
69
98
|
|
|
70
99
|
This will:
|
|
@@ -79,17 +108,9 @@ This will:
|
|
|
79
108
|
Skip all prompts and update everything:
|
|
80
109
|
|
|
81
110
|
```bash
|
|
82
|
-
actions-up --yes
|
|
111
|
+
npx actions-up --yes
|
|
83
112
|
# or
|
|
84
|
-
actions-up -y
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### With GitHub Token
|
|
88
|
-
|
|
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:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
GITHUB_TOKEN=ghp_xxxx actions-up
|
|
113
|
+
npx actions-up -y
|
|
93
114
|
```
|
|
94
115
|
|
|
95
116
|
## Example
|
|
@@ -104,11 +125,16 @@ GITHUB_TOKEN=ghp_xxxx actions-up
|
|
|
104
125
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
105
126
|
```
|
|
106
127
|
|
|
107
|
-
##
|
|
128
|
+
## Advanced Usage
|
|
129
|
+
|
|
130
|
+
### Using GitHub Token for Higher Rate Limits
|
|
131
|
+
|
|
132
|
+
While Actions Up works without authentication, providing a GitHub token increases API rate limits from 60 to 5000 requests per hour, useful for large projects:
|
|
108
133
|
|
|
109
|
-
|
|
134
|
+
[Create a GitHub Personal Access Token](https://github.com/settings/tokens/new?scopes=public_repo&description=actions-up).
|
|
110
135
|
|
|
111
|
-
-
|
|
136
|
+
- For public repositories: Select `public_repo` scope
|
|
137
|
+
- For private repositories: Select `repo` scope
|
|
112
138
|
|
|
113
139
|
## Security
|
|
114
140
|
|