actions-up 1.1.0 → 1.2.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/api/check-updates.js +33 -1
- package/dist/core/api/client.d.ts +22 -4
- package/dist/core/api/client.js +129 -51
- package/dist/core/scan-github-actions.js +79 -1
- package/dist/package.js +1 -1
- package/package.json +1 -2
- package/readme.md +409 -1
- package/dist/core/schema/composite/is-composite-action-step.d.ts +0 -8
- package/dist/core/schema/workflow/is-workflow-job.d.ts +0 -8
- package/dist/core/schema/workflow/is-workflow-step.d.ts +0 -8
|
@@ -20,19 +20,47 @@ async function checkUpdates(actions, token) {
|
|
|
20
20
|
actionName,
|
|
21
21
|
sha: null
|
|
22
22
|
}];
|
|
23
|
-
let
|
|
23
|
+
let segments = actionName.split("/");
|
|
24
|
+
if (segments.length < 2) return [...results, {
|
|
25
|
+
version: null,
|
|
26
|
+
actionName,
|
|
27
|
+
sha: null
|
|
28
|
+
}];
|
|
29
|
+
let [owner, repo] = segments;
|
|
24
30
|
if (!owner || !repo) return [...results, {
|
|
25
31
|
version: null,
|
|
26
32
|
actionName,
|
|
27
33
|
sha: null
|
|
28
34
|
}];
|
|
29
35
|
try {
|
|
36
|
+
let currentVersions = uniqueActions.get(actionName);
|
|
37
|
+
let firstVersion = currentVersions[0]?.version;
|
|
38
|
+
if (firstVersion) {
|
|
39
|
+
let referenceType = await client.getRefType(owner, repo, firstVersion);
|
|
40
|
+
if (referenceType === "branch") return [...results, {
|
|
41
|
+
version: null,
|
|
42
|
+
actionName,
|
|
43
|
+
sha: null
|
|
44
|
+
}];
|
|
45
|
+
}
|
|
30
46
|
let release = await client.getLatestRelease(owner, repo);
|
|
31
47
|
if (!release) {
|
|
32
48
|
let allReleases = await client.getAllReleases(owner, repo, 10);
|
|
33
49
|
let stableRelease = allReleases.find((currentRelease) => !currentRelease.isPrerelease);
|
|
34
50
|
release = stableRelease ?? allReleases[0] ?? null;
|
|
35
51
|
}
|
|
52
|
+
if (!release) {
|
|
53
|
+
let tags = await client.getAllTags(owner, repo, 30);
|
|
54
|
+
if (tags.length > 0) {
|
|
55
|
+
let semverTag = tags.find((tag) => /^v?\d+(?:\.\d+){0,2}/u.test(tag.tag));
|
|
56
|
+
let latestTag = semverTag ?? tags[0];
|
|
57
|
+
if (latestTag) return [...results, {
|
|
58
|
+
version: latestTag.tag,
|
|
59
|
+
sha: latestTag.sha,
|
|
60
|
+
actionName
|
|
61
|
+
}];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
36
64
|
if (release) {
|
|
37
65
|
let { version, sha } = release;
|
|
38
66
|
if (!sha) try {
|
|
@@ -104,6 +132,10 @@ function createUpdate(action, latestVersion, latestSha) {
|
|
|
104
132
|
let latestMajor = semver.major(latest);
|
|
105
133
|
isBreaking = latestMajor > currentMajor;
|
|
106
134
|
}
|
|
135
|
+
if (!hasUpdate && semver.eq(current, latest) && !isSha(action.version) && latestSha) {
|
|
136
|
+
hasUpdate = true;
|
|
137
|
+
isBreaking = false;
|
|
138
|
+
}
|
|
107
139
|
} else if (currentVersion !== normalized) hasUpdate = true;
|
|
108
140
|
}
|
|
109
141
|
return {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Processed release information with normalized types. */
|
|
2
1
|
interface ReleaseInfo {
|
|
3
2
|
/** Release description or null if not provided. */
|
|
4
3
|
description: string | null;
|
|
@@ -19,18 +18,19 @@ interface ReleaseInfo {
|
|
|
19
18
|
interface TagInfo {
|
|
20
19
|
/** Tag or commit message, null if not provided. */
|
|
21
20
|
message: string | null;
|
|
21
|
+
/** Git commit SHA that this tag points to. */
|
|
22
|
+
sha: string | null;
|
|
22
23
|
/** Date when the tag was created or committed. */
|
|
23
24
|
date: Date | null;
|
|
24
25
|
/** Tag name (e.g., 'v1.2.3'). */
|
|
25
26
|
tag: string;
|
|
26
|
-
/** Git commit SHA that this tag points to. */
|
|
27
|
-
sha: string;
|
|
28
27
|
}
|
|
29
28
|
/** GitHub REST API client with optional authentication. */
|
|
30
29
|
export declare class Client {
|
|
30
|
+
private readonly baseUrl;
|
|
31
|
+
private readonly token;
|
|
31
32
|
private rateLimitReset;
|
|
32
33
|
private rateLimitRemaining;
|
|
33
|
-
private readonly octokit;
|
|
34
34
|
/**
|
|
35
35
|
* Creates a new GitHub API client.
|
|
36
36
|
*
|
|
@@ -64,6 +64,8 @@ export declare class Client {
|
|
|
64
64
|
* @returns Latest release information or null if not found.
|
|
65
65
|
*/
|
|
66
66
|
getLatestRelease(owner: string, repo: string): Promise<ReleaseInfo | null>;
|
|
67
|
+
getAllTags(owner: string, repo: string, limit?: number): Promise<TagInfo[]>;
|
|
68
|
+
getRefType(owner: string, repo: string, reference: string): Promise<'branch' | 'tag' | null>;
|
|
67
69
|
getRateLimitStatus(): {
|
|
68
70
|
remaining: number;
|
|
69
71
|
resetAt: Date;
|
|
@@ -75,6 +77,22 @@ export declare class Client {
|
|
|
75
77
|
* @returns True if rate limit is below threshold.
|
|
76
78
|
*/
|
|
77
79
|
shouldWaitForRateLimit(threshold?: number): boolean;
|
|
80
|
+
private makeRequest;
|
|
78
81
|
private updateRateLimitInfo;
|
|
79
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve GitHub token from multiple sources with descending priority.
|
|
85
|
+
*
|
|
86
|
+
* Priority:
|
|
87
|
+
*
|
|
88
|
+
* 1. Env GITHUB_TOKEN
|
|
89
|
+
* 2. Env GH_TOKEN
|
|
90
|
+
* 3. Gh auth token
|
|
91
|
+
* 4. .git/config keys (github.token, github.oauth-token, hub.oauthtoken).
|
|
92
|
+
*
|
|
93
|
+
* This is a synchronous best-effort resolver without throwing on failures.
|
|
94
|
+
*
|
|
95
|
+
* @returns Token string or undefined when not found.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveGitHubTokenSync(): undefined | string;
|
|
80
98
|
export {};
|
package/dist/core/api/client.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
2
4
|
var GitHubRateLimitError = class extends Error {
|
|
3
5
|
constructor(resetAt) {
|
|
4
6
|
let resetTime = resetAt.toLocaleTimeString();
|
|
@@ -7,14 +9,13 @@ var GitHubRateLimitError = class extends Error {
|
|
|
7
9
|
}
|
|
8
10
|
};
|
|
9
11
|
var Client = class Client {
|
|
12
|
+
baseUrl = "https://api.github.com";
|
|
13
|
+
token;
|
|
10
14
|
rateLimitReset = /* @__PURE__ */ new Date();
|
|
11
15
|
rateLimitRemaining = 60;
|
|
12
|
-
octokit;
|
|
13
16
|
constructor(token) {
|
|
14
|
-
|
|
15
|
-
this.
|
|
16
|
-
if (!authToken) console.warn("No GitHub token found. API rate limits will be restricted.");
|
|
17
|
-
this.rateLimitRemaining = authToken ? 5e3 : 60;
|
|
17
|
+
this.token = token ?? process.env["GITHUB_TOKEN"] ?? resolveGitHubTokenSync();
|
|
18
|
+
this.rateLimitRemaining = this.token ? 5e3 : 60;
|
|
18
19
|
}
|
|
19
20
|
static isRateLimitError(error) {
|
|
20
21
|
if (error && typeof error === "object") {
|
|
@@ -29,19 +30,12 @@ var Client = class Client {
|
|
|
29
30
|
try {
|
|
30
31
|
let displayTag = tag.replace(/^refs\/tags\//u, "");
|
|
31
32
|
try {
|
|
32
|
-
let
|
|
33
|
-
|
|
34
|
-
owner,
|
|
35
|
-
repo
|
|
36
|
-
});
|
|
37
|
-
this.updateRateLimitInfo(releaseHeaders);
|
|
33
|
+
let releaseResp = await this.makeRequest(`/repos/${owner}/${repo}/releases/tags/${displayTag}`);
|
|
34
|
+
let releaseData = releaseResp.data;
|
|
38
35
|
let sha = null;
|
|
39
36
|
if (releaseData.target_commitish) try {
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
owner,
|
|
43
|
-
repo
|
|
44
|
-
});
|
|
37
|
+
let commitResp = await this.makeRequest(`/repos/${owner}/${repo}/commits/${releaseData.target_commitish}`);
|
|
38
|
+
let commitData = commitResp.data;
|
|
45
39
|
({sha} = commitData);
|
|
46
40
|
} catch {
|
|
47
41
|
sha = releaseData.target_commitish;
|
|
@@ -53,32 +47,22 @@ var Client = class Client {
|
|
|
53
47
|
tag: displayTag
|
|
54
48
|
};
|
|
55
49
|
} catch (releaseError) {
|
|
56
|
-
if (releaseError
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
owner,
|
|
60
|
-
repo
|
|
61
|
-
});
|
|
62
|
-
this.updateRateLimitInfo(referenceHeaders);
|
|
50
|
+
if (releaseError && typeof releaseError === "object" && "status" in releaseError && releaseError.status === 404) try {
|
|
51
|
+
let referenceResp = await this.makeRequest(`/repos/${owner}/${repo}/git/refs/tags/${displayTag}`);
|
|
52
|
+
let referenceData = referenceResp.data;
|
|
63
53
|
let { sha } = referenceData.object;
|
|
64
54
|
let message = null;
|
|
65
55
|
let date = null;
|
|
66
56
|
if (referenceData.object.type === "tag") try {
|
|
67
|
-
let
|
|
68
|
-
|
|
69
|
-
owner,
|
|
70
|
-
repo
|
|
71
|
-
});
|
|
57
|
+
let tagResp = await this.makeRequest(`/repos/${owner}/${repo}/git/tags/${sha}`);
|
|
58
|
+
let tagData = tagResp.data;
|
|
72
59
|
({sha} = tagData.object);
|
|
73
|
-
message = tagData
|
|
60
|
+
({message} = tagData);
|
|
74
61
|
date = tagData.tagger.date ? new Date(tagData.tagger.date) : null;
|
|
75
62
|
} catch {}
|
|
76
|
-
else
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
owner,
|
|
80
|
-
repo
|
|
81
|
-
});
|
|
63
|
+
else try {
|
|
64
|
+
let commitResp = await this.makeRequest(`/repos/${owner}/${repo}/git/commits/${sha}`);
|
|
65
|
+
let commitData = commitResp.data;
|
|
82
66
|
({message} = commitData);
|
|
83
67
|
date = commitData.author.date ? new Date(commitData.author.date) : null;
|
|
84
68
|
} catch {}
|
|
@@ -89,7 +73,7 @@ var Client = class Client {
|
|
|
89
73
|
sha
|
|
90
74
|
};
|
|
91
75
|
} catch (tagError) {
|
|
92
|
-
if (tagError
|
|
76
|
+
if (tagError && typeof tagError === "object" && "status" in tagError && tagError.status === 404) return null;
|
|
93
77
|
throw tagError;
|
|
94
78
|
}
|
|
95
79
|
throw releaseError;
|
|
@@ -101,12 +85,8 @@ var Client = class Client {
|
|
|
101
85
|
}
|
|
102
86
|
async getAllReleases(owner, repo, limit = 10) {
|
|
103
87
|
try {
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
owner,
|
|
107
|
-
repo
|
|
108
|
-
});
|
|
109
|
-
this.updateRateLimitInfo(headers);
|
|
88
|
+
let releasesResp = await this.makeRequest(`/repos/${owner}/${repo}/releases?per_page=${limit}`);
|
|
89
|
+
let releases = releasesResp.data;
|
|
110
90
|
let releaseInfos = [];
|
|
111
91
|
await Promise.all(releases.map(async (release) => {
|
|
112
92
|
let sha = null;
|
|
@@ -114,7 +94,7 @@ var Client = class Client {
|
|
|
114
94
|
let tagInfo = await this.getTagInfo(owner, repo, release.tag_name);
|
|
115
95
|
if (tagInfo) ({sha} = tagInfo);
|
|
116
96
|
} catch {
|
|
117
|
-
sha = release.target_commitish
|
|
97
|
+
sha = release.target_commitish;
|
|
118
98
|
}
|
|
119
99
|
releaseInfos.push({
|
|
120
100
|
publishedAt: new Date(release.published_at),
|
|
@@ -134,17 +114,14 @@ var Client = class Client {
|
|
|
134
114
|
}
|
|
135
115
|
async getLatestRelease(owner, repo) {
|
|
136
116
|
try {
|
|
137
|
-
let
|
|
138
|
-
|
|
139
|
-
repo
|
|
140
|
-
});
|
|
141
|
-
this.updateRateLimitInfo(headers);
|
|
117
|
+
let releaseResp = await this.makeRequest(`/repos/${owner}/${repo}/releases/latest`);
|
|
118
|
+
let release = releaseResp.data;
|
|
142
119
|
let sha = null;
|
|
143
120
|
if (release.tag_name) try {
|
|
144
121
|
let tagInfo = await this.getTagInfo(owner, repo, release.tag_name);
|
|
145
122
|
if (tagInfo) ({sha} = tagInfo);
|
|
146
123
|
} catch {
|
|
147
|
-
sha = release.target_commitish
|
|
124
|
+
sha = release.target_commitish;
|
|
148
125
|
}
|
|
149
126
|
return {
|
|
150
127
|
publishedAt: new Date(release.published_at),
|
|
@@ -156,11 +133,39 @@ var Client = class Client {
|
|
|
156
133
|
sha
|
|
157
134
|
};
|
|
158
135
|
} catch (error) {
|
|
159
|
-
if (error
|
|
136
|
+
if (error && typeof error === "object" && "status" in error && error.status === 404) return null;
|
|
160
137
|
if (Client.isRateLimitError(error)) throw new GitHubRateLimitError(this.rateLimitReset);
|
|
161
138
|
throw error;
|
|
162
139
|
}
|
|
163
140
|
}
|
|
141
|
+
async getAllTags(owner, repo, limit = 30) {
|
|
142
|
+
try {
|
|
143
|
+
let tagsResp = await this.makeRequest(`/repos/${owner}/${repo}/tags?per_page=${limit}`);
|
|
144
|
+
let tags = tagsResp.data;
|
|
145
|
+
return tags.map((tag) => ({
|
|
146
|
+
sha: tag.commit.sha,
|
|
147
|
+
tag: tag.name,
|
|
148
|
+
message: null,
|
|
149
|
+
date: null
|
|
150
|
+
}));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (Client.isRateLimitError(error)) throw new GitHubRateLimitError(this.rateLimitReset);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async getRefType(owner, repo, reference) {
|
|
157
|
+
try {
|
|
158
|
+
await this.makeRequest(`/repos/${owner}/${repo}/git/refs/tags/${reference}`);
|
|
159
|
+
return "tag";
|
|
160
|
+
} catch {
|
|
161
|
+
try {
|
|
162
|
+
await this.makeRequest(`/repos/${owner}/${repo}/git/refs/heads/${reference}`);
|
|
163
|
+
return "branch";
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
164
169
|
getRateLimitStatus() {
|
|
165
170
|
return {
|
|
166
171
|
remaining: this.rateLimitRemaining,
|
|
@@ -170,6 +175,35 @@ var Client = class Client {
|
|
|
170
175
|
shouldWaitForRateLimit(threshold = 100) {
|
|
171
176
|
return this.rateLimitRemaining < threshold;
|
|
172
177
|
}
|
|
178
|
+
async makeRequest(path$1, options = {}) {
|
|
179
|
+
let headers = {
|
|
180
|
+
Accept: "application/vnd.github.v3+json",
|
|
181
|
+
"User-Agent": "actions-up",
|
|
182
|
+
...options.headers
|
|
183
|
+
};
|
|
184
|
+
if (this.token) headers["Authorization"] = `Bearer ${this.token}`;
|
|
185
|
+
let response = await fetch(`${this.baseUrl}${path$1}`, {
|
|
186
|
+
...options,
|
|
187
|
+
headers
|
|
188
|
+
});
|
|
189
|
+
let responseHeaders = {};
|
|
190
|
+
for (let [key, value] of response.headers.entries()) responseHeaders[key] = value;
|
|
191
|
+
this.updateRateLimitInfo(responseHeaders);
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
let error = /* @__PURE__ */ new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
194
|
+
error.status = response.status;
|
|
195
|
+
if (response.status === 403) {
|
|
196
|
+
let text = await response.text();
|
|
197
|
+
if (text.includes("rate limit") || text.includes("API rate limit")) error.message = "API rate limit exceeded";
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
let data = await response.json();
|
|
202
|
+
return {
|
|
203
|
+
headers: responseHeaders,
|
|
204
|
+
data
|
|
205
|
+
};
|
|
206
|
+
}
|
|
173
207
|
updateRateLimitInfo(headers) {
|
|
174
208
|
let remaining = headers["x-ratelimit-remaining"];
|
|
175
209
|
if (remaining !== void 0) this.rateLimitRemaining = typeof remaining === "string" ? Number.parseInt(remaining, 10) : remaining;
|
|
@@ -180,4 +214,48 @@ var Client = class Client {
|
|
|
180
214
|
}
|
|
181
215
|
}
|
|
182
216
|
};
|
|
217
|
+
function resolveGitHubTokenSync() {
|
|
218
|
+
let fromGithubToken = process.env["GITHUB_TOKEN"];
|
|
219
|
+
if (fromGithubToken && fromGithubToken.trim() !== "") return fromGithubToken.trim();
|
|
220
|
+
let fromGhToken = process.env["GH_TOKEN"];
|
|
221
|
+
if (fromGhToken && fromGhToken.trim() !== "") return fromGhToken.trim();
|
|
222
|
+
try {
|
|
223
|
+
let output = execFileSync("gh", ["auth", "token"], {
|
|
224
|
+
stdio: [
|
|
225
|
+
"ignore",
|
|
226
|
+
"pipe",
|
|
227
|
+
"ignore"
|
|
228
|
+
],
|
|
229
|
+
encoding: "utf8",
|
|
230
|
+
timeout: 500
|
|
231
|
+
});
|
|
232
|
+
let token = output.trim();
|
|
233
|
+
if (token) return token;
|
|
234
|
+
} catch {}
|
|
235
|
+
try {
|
|
236
|
+
let gitConfigPath = join(process.cwd(), ".git", "config");
|
|
237
|
+
let content = readFileSync(gitConfigPath, "utf8");
|
|
238
|
+
let directMatch = content.match(/^\s*(?:github\.(?:oauth-token|token)|hub\.oauthtoken)\s*=\s*(?<token>\S[^\n\r]*)$/mu);
|
|
239
|
+
let directToken = directMatch?.groups?.["token"]?.trim();
|
|
240
|
+
if (directToken) return directToken;
|
|
241
|
+
let currentSection = null;
|
|
242
|
+
for (let rawLine of content.split(/\r?\n/u)) {
|
|
243
|
+
let line = rawLine.trim();
|
|
244
|
+
let sectionMatch = line.match(/^\[(?<name>[^\]]+)\]$/u);
|
|
245
|
+
if (sectionMatch?.groups) {
|
|
246
|
+
currentSection = sectionMatch.groups["name"].toLowerCase();
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (currentSection === "github") {
|
|
250
|
+
let tokenMatch = line.match(/^(?:oauth-token|token)\s*=\s*(?<val>\S[^\n\r]*)$/u);
|
|
251
|
+
if (tokenMatch?.groups?.["val"]) return tokenMatch.groups["val"].trim();
|
|
252
|
+
}
|
|
253
|
+
if (currentSection === "hub") {
|
|
254
|
+
let oauthMatch = line.match(/^oauthtoken\s*=\s*(?<val>\S[^\n\r]*)$/u);
|
|
255
|
+
if (oauthMatch?.groups?.["val"]) return oauthMatch.groups["val"].trim();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch {}
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
183
261
|
export { Client };
|
|
@@ -3,7 +3,7 @@ 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
5
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
-
import { readdir, stat } from "node:fs/promises";
|
|
6
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
7
7
|
async function scanGitHubActions(rootPath = process.cwd()) {
|
|
8
8
|
let result = {
|
|
9
9
|
compositeActions: /* @__PURE__ */ new Map(),
|
|
@@ -111,6 +111,84 @@ async function scanGitHubActions(rootPath = process.cwd()) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
} catch {}
|
|
114
|
+
try {
|
|
115
|
+
let repoSlug = await getCurrentRepoSlug(normalizedRoot);
|
|
116
|
+
if (repoSlug) {
|
|
117
|
+
if (process.env["ACTIONS_UP_TEST_THROW"] === "1") throw new Error("test");
|
|
118
|
+
let seenCompositeDirectories = /* @__PURE__ */ new Set();
|
|
119
|
+
let queue = [];
|
|
120
|
+
for (let action of result.actions) {
|
|
121
|
+
if (action.type !== "external") continue;
|
|
122
|
+
let segs = action.name.split("/");
|
|
123
|
+
if (segs.length < 3) continue;
|
|
124
|
+
let candidateSlug = `${segs[0]}/${segs[1]}`;
|
|
125
|
+
if (candidateSlug !== repoSlug) continue;
|
|
126
|
+
let compositeDirectory = join(normalizedRoot, ...segs.slice(2));
|
|
127
|
+
if (!isWithin(normalizedRoot, compositeDirectory)) continue;
|
|
128
|
+
if (seenCompositeDirectories.has(compositeDirectory)) continue;
|
|
129
|
+
seenCompositeDirectories.add(compositeDirectory);
|
|
130
|
+
queue.push(compositeDirectory);
|
|
131
|
+
}
|
|
132
|
+
async function processQueue() {
|
|
133
|
+
if (queue.length === 0) return;
|
|
134
|
+
let batch = queue.splice(0);
|
|
135
|
+
let discoveredNext = await Promise.all(batch.map(async (directory) => {
|
|
136
|
+
try {
|
|
137
|
+
let ymlPath = join(directory, "action.yml");
|
|
138
|
+
let yamlPath = join(directory, "action.yaml");
|
|
139
|
+
let filePath = ymlPath;
|
|
140
|
+
try {
|
|
141
|
+
let fileInfo = await stat(ymlPath);
|
|
142
|
+
if (!fileInfo.isFile()) throw new Error("not a file");
|
|
143
|
+
} catch {
|
|
144
|
+
let yamlInfo = await stat(yamlPath);
|
|
145
|
+
if (!yamlInfo.isFile()) throw new Error("not a file");
|
|
146
|
+
filePath = yamlPath;
|
|
147
|
+
}
|
|
148
|
+
let nestedActions = await scanActionFile(filePath);
|
|
149
|
+
if (nestedActions.length > 0) result.actions.push(...nestedActions);
|
|
150
|
+
let nextDirectories = [];
|
|
151
|
+
for (let nestedAction of nestedActions) {
|
|
152
|
+
if (nestedAction.type !== "external") continue;
|
|
153
|
+
let nameSegments = nestedAction.name.split("/");
|
|
154
|
+
if (nameSegments.length < 3) continue;
|
|
155
|
+
let nameSlug = `${nameSegments[0]}/${nameSegments[1]}`;
|
|
156
|
+
if (nameSlug !== repoSlug) continue;
|
|
157
|
+
let nextDirectory = join(normalizedRoot, ...nameSegments.slice(2));
|
|
158
|
+
if (!isWithin(normalizedRoot, nextDirectory)) continue;
|
|
159
|
+
if (seenCompositeDirectories.has(nextDirectory)) continue;
|
|
160
|
+
seenCompositeDirectories.add(nextDirectory);
|
|
161
|
+
nextDirectories.push(nextDirectory);
|
|
162
|
+
}
|
|
163
|
+
return nextDirectories;
|
|
164
|
+
} catch {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
}));
|
|
168
|
+
for (let list of discoveredNext) for (let directory of list) queue.push(directory);
|
|
169
|
+
await processQueue();
|
|
170
|
+
}
|
|
171
|
+
await processQueue();
|
|
172
|
+
}
|
|
173
|
+
} catch {}
|
|
114
174
|
return result;
|
|
115
175
|
}
|
|
176
|
+
async function getCurrentRepoSlug(root) {
|
|
177
|
+
let environmentSlug = process.env["GITHUB_REPOSITORY"];
|
|
178
|
+
if (environmentSlug && /^[^\s/]+\/[^\s/]+$/u.test(environmentSlug)) return environmentSlug;
|
|
179
|
+
try {
|
|
180
|
+
let gitConfigPath = join(root, ".git", "config");
|
|
181
|
+
let content = await readFile(gitConfigPath, "utf8");
|
|
182
|
+
let originUrlMatch = content.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(?<url>.+)/u);
|
|
183
|
+
let url = originUrlMatch?.groups?.["url"]?.trim();
|
|
184
|
+
if (!url) {
|
|
185
|
+
let anyUrlMatch = content.match(/url\s*=\s*(?<url>.+)/u);
|
|
186
|
+
url = anyUrlMatch?.groups?.["url"]?.trim();
|
|
187
|
+
}
|
|
188
|
+
if (!url) return null;
|
|
189
|
+
let httpsMatch = url.match(/github\.com[/:](?<owner>[^/]+)\/(?<repo>[^./]+)(?:\.git)?$/u);
|
|
190
|
+
if (httpsMatch?.groups) return `${httpsMatch.groups["owner"]}/${httpsMatch.groups["repo"]}`;
|
|
191
|
+
} catch {}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
116
194
|
export { scanGitHubActions };
|
package/dist/package.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = "1.
|
|
1
|
+
const version = "1.2.0";
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actions-up",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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,6 @@
|
|
|
36
36
|
"./dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@octokit/rest": "^22.0.0",
|
|
40
39
|
"cac": "^6.7.14",
|
|
41
40
|
"enquirer": "^2.4.1",
|
|
42
41
|
"nanospinner": "^1.2.2",
|
package/readme.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
Actions Up scans your workflows and composite actions to discover every referenced GitHub Action, then checks for newer releases.
|
|
16
16
|
|
|
17
|
-
Interactively upgrade and pin actions to exact commit SHAs for secure, reproducible CI and low
|
|
17
|
+
Interactively upgrade and pin actions to exact commit SHAs for secure, reproducible CI and low-friction maintenance.
|
|
18
18
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
@@ -24,6 +24,7 @@ Interactively upgrade and pin actions to exact commit SHAs for secure, reproduci
|
|
|
24
24
|
- **Interactive Selection**: Choose which actions to update
|
|
25
25
|
- **Breaking Changes Detection**: Warns about major version updates
|
|
26
26
|
- **Fast & Efficient**: Parallel processing with optimized API calls
|
|
27
|
+
- **CI/CD Integration**: Can be used as a GitHub Action for automated PR checks
|
|
27
28
|
|
|
28
29
|
###
|
|
29
30
|
|
|
@@ -113,6 +114,371 @@ npx actions-up --yes
|
|
|
113
114
|
npx actions-up -y
|
|
114
115
|
```
|
|
115
116
|
|
|
117
|
+
### Dry Run Mode
|
|
118
|
+
|
|
119
|
+
Check for updates without making any changes:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx actions-up --dry-run
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## GitHub Actions Integration
|
|
126
|
+
|
|
127
|
+
### Automated PR Checks
|
|
128
|
+
|
|
129
|
+
You can integrate Actions Up into your CI/CD pipeline to automatically check for outdated actions on every pull request. This helps maintain security and ensures your team stays aware of available updates.
|
|
130
|
+
|
|
131
|
+
<details>
|
|
132
|
+
<summary>Create <code>.github/workflows/check-actions-updates.yml</code>.</summary>
|
|
133
|
+
|
|
134
|
+
````yaml
|
|
135
|
+
name: Check for outdated GitHub Actions
|
|
136
|
+
on:
|
|
137
|
+
pull_request:
|
|
138
|
+
types: [edited, opened, synchronize, reopened]
|
|
139
|
+
|
|
140
|
+
jobs:
|
|
141
|
+
check-actions:
|
|
142
|
+
name: Check for GHA updates
|
|
143
|
+
runs-on: ubuntu-latest
|
|
144
|
+
steps:
|
|
145
|
+
- name: Checkout repository
|
|
146
|
+
uses: actions/checkout@v4
|
|
147
|
+
|
|
148
|
+
- name: Setup Node.js
|
|
149
|
+
uses: actions/setup-node@v4
|
|
150
|
+
with:
|
|
151
|
+
node-version: '20'
|
|
152
|
+
|
|
153
|
+
- name: Install actions-up
|
|
154
|
+
run: npm install -g actions-up
|
|
155
|
+
|
|
156
|
+
- name: Run actions-up check
|
|
157
|
+
id: actions-check
|
|
158
|
+
run: |
|
|
159
|
+
echo "## GitHub Actions Update Check" >> $GITHUB_STEP_SUMMARY
|
|
160
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
161
|
+
|
|
162
|
+
# Initialize variables
|
|
163
|
+
HAS_UPDATES=false
|
|
164
|
+
UPDATE_COUNT=0
|
|
165
|
+
|
|
166
|
+
# Run actions-up and capture output
|
|
167
|
+
echo "Running actions-up to check for updates..."
|
|
168
|
+
actions-up --dry-run > actions-up-raw.txt 2>&1 || true
|
|
169
|
+
|
|
170
|
+
# Strip ANSI color codes from the output
|
|
171
|
+
sed -i 's/\x1b\[[0-9;]*m//g' actions-up-raw.txt
|
|
172
|
+
|
|
173
|
+
# Also remove any other control characters
|
|
174
|
+
sed -i 's/\x1b\[[0-9;]*[a-zA-Z]//g' actions-up-raw.txt
|
|
175
|
+
|
|
176
|
+
# Parse the output to detect updates
|
|
177
|
+
# Look for patterns like "v3 → v4" or "would be updated"
|
|
178
|
+
if grep -E "(→|would be updated|Update available)" actions-up-raw.txt > /dev/null 2>&1; then
|
|
179
|
+
HAS_UPDATES=true
|
|
180
|
+
# Count the number of updates (lines with arrows)
|
|
181
|
+
UPDATE_COUNT=$(grep -c "→" actions-up-raw.txt || echo "0")
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# Create formatted output
|
|
185
|
+
if [ "$HAS_UPDATES" = true ]; then
|
|
186
|
+
echo "Found $UPDATE_COUNT GitHub Actions with available updates" >> $GITHUB_STEP_SUMMARY
|
|
187
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
188
|
+
echo "<details>" >> $GITHUB_STEP_SUMMARY
|
|
189
|
+
echo "<summary>Click to see details</summary>" >> $GITHUB_STEP_SUMMARY
|
|
190
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
191
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
192
|
+
cat actions-up-raw.txt >> $GITHUB_STEP_SUMMARY
|
|
193
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
194
|
+
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
|
195
|
+
|
|
196
|
+
# Create detailed markdown report with better formatting
|
|
197
|
+
{
|
|
198
|
+
echo "## GitHub Actions Update Report"
|
|
199
|
+
echo ""
|
|
200
|
+
|
|
201
|
+
# Extract summary information
|
|
202
|
+
TOTAL_ACTIONS=$(grep -oP 'Found \K[0-9]+(?= actions)' actions-up-raw.txt | head -1 || echo "0")
|
|
203
|
+
BREAKING_UPDATES=$(grep -oP '\(([0-9]+) breaking\)' actions-up-raw.txt | grep -oP '[0-9]+' || echo "0")
|
|
204
|
+
|
|
205
|
+
echo "### Summary"
|
|
206
|
+
echo "- **Total actions scanned:** $TOTAL_ACTIONS"
|
|
207
|
+
echo "- **Updates available:** $UPDATE_COUNT"
|
|
208
|
+
if [ "$BREAKING_UPDATES" != "0" ]; then
|
|
209
|
+
echo "- **Breaking changes:** $BREAKING_UPDATES"
|
|
210
|
+
fi
|
|
211
|
+
echo ""
|
|
212
|
+
|
|
213
|
+
echo "### Available Updates"
|
|
214
|
+
echo ""
|
|
215
|
+
|
|
216
|
+
# Format the updates in a table
|
|
217
|
+
echo "| Workflow File | Action | Current | Available | Type | Release Notes |"
|
|
218
|
+
echo "|--------------|--------|---------|-----------|------|---------------|"
|
|
219
|
+
|
|
220
|
+
# Parse each update line
|
|
221
|
+
grep "→" actions-up-raw.txt | while IFS= read -r line; do
|
|
222
|
+
# Extract workflow file path (remove leading path)
|
|
223
|
+
if echo "$line" | grep -q "\.github/workflows/"; then
|
|
224
|
+
PREV_FILE=$(echo "$line" | grep -oP '\.github/workflows/[^:]+' | head -1)
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
# Skip file path lines, process only action updates
|
|
228
|
+
if echo "$line" | grep -q ": .* → "; then
|
|
229
|
+
# Extract action name and versions
|
|
230
|
+
ACTION=$(echo "$line" | cut -d: -f1 | xargs)
|
|
231
|
+
CURRENT=$(echo "$line" | grep -oP 'v[0-9]+(\.[0-9]+)*' | head -1)
|
|
232
|
+
NEW=$(echo "$line" | grep -oP '→ \Kv[0-9]+(\.[0-9]+)*' | head -1)
|
|
233
|
+
|
|
234
|
+
# Determine if it's a breaking change
|
|
235
|
+
CURRENT_MAJOR=$(echo "$CURRENT" | grep -oP 'v\K[0-9]+' || echo "0")
|
|
236
|
+
NEW_MAJOR=$(echo "$NEW" | grep -oP 'v\K[0-9]+' || echo "0")
|
|
237
|
+
|
|
238
|
+
if [ "$CURRENT_MAJOR" != "$NEW_MAJOR" ]; then
|
|
239
|
+
TYPE="Breaking"
|
|
240
|
+
# Generate release URL
|
|
241
|
+
# Handle both owner/repo and just repo formats
|
|
242
|
+
if echo "$ACTION" | grep -q "/"; then
|
|
243
|
+
REPO_PATH="$ACTION"
|
|
244
|
+
else
|
|
245
|
+
# For actions without owner, assume it's under 'actions' org
|
|
246
|
+
REPO_PATH="actions/$ACTION"
|
|
247
|
+
fi
|
|
248
|
+
RELEASE_URL="https://github.com/${REPO_PATH}/releases/tag/${NEW}"
|
|
249
|
+
RELEASE_LINK="[Release](${RELEASE_URL})"
|
|
250
|
+
else
|
|
251
|
+
TYPE="Minor"
|
|
252
|
+
RELEASE_LINK="-"
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
# Output table row
|
|
256
|
+
WORKFLOW_NAME=$(basename "$PREV_FILE" 2>/dev/null || echo "workflow.yml")
|
|
257
|
+
echo "| \`$WORKFLOW_NAME\` | $ACTION | $CURRENT | **$NEW** | $TYPE | $RELEASE_LINK |"
|
|
258
|
+
fi
|
|
259
|
+
done
|
|
260
|
+
|
|
261
|
+
echo ""
|
|
262
|
+
echo "### How to Update"
|
|
263
|
+
echo ""
|
|
264
|
+
echo "You have several options to update these actions:"
|
|
265
|
+
echo ""
|
|
266
|
+
echo "#### Option 1: Automatic Update (Recommended)"
|
|
267
|
+
echo '```bash'
|
|
268
|
+
echo "# Run this command locally in your repository"
|
|
269
|
+
echo "npx actions-up"
|
|
270
|
+
echo '```'
|
|
271
|
+
echo ""
|
|
272
|
+
echo "#### Option 2: Manual Update"
|
|
273
|
+
echo "1. Review each update in the table above"
|
|
274
|
+
echo "2. For breaking changes, click the Release Notes link to review changes"
|
|
275
|
+
echo "3. Edit the workflow files and update the version numbers"
|
|
276
|
+
echo "4. Test the changes in your CI/CD pipeline"
|
|
277
|
+
echo ""
|
|
278
|
+
echo "#### Option 3: Selective Update"
|
|
279
|
+
echo '```bash'
|
|
280
|
+
echo "# Update only non-breaking changes"
|
|
281
|
+
echo "npx actions-up --breaking false"
|
|
282
|
+
echo '```'
|
|
283
|
+
echo ""
|
|
284
|
+
|
|
285
|
+
if [ "$BREAKING_UPDATES" != "0" ]; then
|
|
286
|
+
echo "### Breaking Changes Warning"
|
|
287
|
+
echo ""
|
|
288
|
+
echo "This update includes **$BREAKING_UPDATES breaking change(s)**. Please review the release notes before updating:"
|
|
289
|
+
echo ""
|
|
290
|
+
grep "→" actions-up-raw.txt | while IFS= read -r line; do
|
|
291
|
+
if echo "$line" | grep -q ": .* → "; then
|
|
292
|
+
ACTION=$(echo "$line" | cut -d: -f1 | xargs)
|
|
293
|
+
CURRENT=$(echo "$line" | grep -oP 'v[0-9]+' | head -1)
|
|
294
|
+
NEW=$(echo "$line" | grep -oP '→ \Kv[0-9]+(\.[0-9]+)*' | head -1)
|
|
295
|
+
CURRENT_MAJOR=$(echo "$CURRENT" | grep -oP '[0-9]+' || echo "0")
|
|
296
|
+
NEW_MAJOR=$(echo "$NEW" | grep -oP '[0-9]+' || echo "0")
|
|
297
|
+
if [ "$CURRENT_MAJOR" != "$NEW_MAJOR" ]; then
|
|
298
|
+
# Generate release URL
|
|
299
|
+
if echo "$ACTION" | grep -q "/"; then
|
|
300
|
+
REPO_PATH="$ACTION"
|
|
301
|
+
else
|
|
302
|
+
REPO_PATH="actions/$ACTION"
|
|
303
|
+
fi
|
|
304
|
+
RELEASE_URL="https://github.com/${REPO_PATH}/releases/tag/${NEW}"
|
|
305
|
+
echo "- **$ACTION**: $CURRENT → $NEW - [View Release Notes](${RELEASE_URL})"
|
|
306
|
+
fi
|
|
307
|
+
fi
|
|
308
|
+
done
|
|
309
|
+
echo ""
|
|
310
|
+
echo "**Important:** Breaking changes may require modifications to your workflow configuration. Always review the release notes and test thoroughly."
|
|
311
|
+
echo ""
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
echo "---"
|
|
315
|
+
echo ""
|
|
316
|
+
echo "<details>"
|
|
317
|
+
echo "<summary>Raw actions-up output</summary>"
|
|
318
|
+
echo ""
|
|
319
|
+
echo '```'
|
|
320
|
+
cat actions-up-raw.txt
|
|
321
|
+
echo '```'
|
|
322
|
+
echo "</details>"
|
|
323
|
+
} > actions-up-report.md
|
|
324
|
+
|
|
325
|
+
echo "has-updates=true" >> $GITHUB_OUTPUT
|
|
326
|
+
echo "update-count=$UPDATE_COUNT" >> $GITHUB_OUTPUT
|
|
327
|
+
else
|
|
328
|
+
echo "All GitHub Actions are up to date!" >> $GITHUB_STEP_SUMMARY
|
|
329
|
+
|
|
330
|
+
{
|
|
331
|
+
echo "## GitHub Actions Update Report"
|
|
332
|
+
echo ""
|
|
333
|
+
echo "### All GitHub Actions in this repository are up to date!"
|
|
334
|
+
echo ""
|
|
335
|
+
echo "No action required. Your workflow files are using the latest versions of all GitHub Actions."
|
|
336
|
+
} > actions-up-report.md
|
|
337
|
+
|
|
338
|
+
echo "has-updates=false" >> $GITHUB_OUTPUT
|
|
339
|
+
echo "update-count=0" >> $GITHUB_OUTPUT
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
- name: Comment PR with updates
|
|
343
|
+
if: github.event_name == 'pull_request'
|
|
344
|
+
uses: actions/github-script@v7
|
|
345
|
+
with:
|
|
346
|
+
script: |
|
|
347
|
+
const fs = require('fs');
|
|
348
|
+
const report = fs.readFileSync('actions-up-report.md', 'utf8');
|
|
349
|
+
const hasUpdates = '${{ steps.actions-check.outputs.has-updates }}' === 'true';
|
|
350
|
+
const updateCount = '${{ steps.actions-check.outputs.update-count }}';
|
|
351
|
+
|
|
352
|
+
// Check if we already commented
|
|
353
|
+
const comments = await github.rest.issues.listComments({
|
|
354
|
+
owner: context.repo.owner,
|
|
355
|
+
repo: context.repo.repo,
|
|
356
|
+
issue_number: context.issue.number
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const botComment = comments.data.find(comment =>
|
|
360
|
+
comment.user.type === 'Bot' &&
|
|
361
|
+
comment.body.includes('GitHub Actions Update Report')
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const commentBody = `${report}
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
*Generated by [actions-up](https://github.com/azat-io/actions-up) | Last check: ${new Date().toISOString()}*`;
|
|
368
|
+
|
|
369
|
+
// Only comment if there are updates or if we previously commented
|
|
370
|
+
if (hasUpdates || botComment) {
|
|
371
|
+
if (botComment) {
|
|
372
|
+
// Update existing comment
|
|
373
|
+
await github.rest.issues.updateComment({
|
|
374
|
+
owner: context.repo.owner,
|
|
375
|
+
repo: context.repo.repo,
|
|
376
|
+
comment_id: botComment.id,
|
|
377
|
+
body: commentBody
|
|
378
|
+
});
|
|
379
|
+
console.log('Updated existing comment');
|
|
380
|
+
} else {
|
|
381
|
+
// Create new comment only if there are updates
|
|
382
|
+
await github.rest.issues.createComment({
|
|
383
|
+
owner: context.repo.owner,
|
|
384
|
+
repo: context.repo.repo,
|
|
385
|
+
issue_number: context.issue.number,
|
|
386
|
+
body: commentBody
|
|
387
|
+
});
|
|
388
|
+
console.log('Created new comment');
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
console.log('No updates found and no previous comment exists - skipping comment');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Add or update PR labels based on status
|
|
395
|
+
const labels = await github.rest.issues.listLabelsOnIssue({
|
|
396
|
+
owner: context.repo.owner,
|
|
397
|
+
repo: context.repo.repo,
|
|
398
|
+
issue_number: context.issue.number
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const hasOutdatedLabel = labels.data.some(label => label.name === 'outdated-actions');
|
|
402
|
+
|
|
403
|
+
if (hasUpdates && !hasOutdatedLabel) {
|
|
404
|
+
// Add label if updates are found
|
|
405
|
+
try {
|
|
406
|
+
await github.rest.issues.addLabels({
|
|
407
|
+
owner: context.repo.owner,
|
|
408
|
+
repo: context.repo.repo,
|
|
409
|
+
issue_number: context.issue.number,
|
|
410
|
+
labels: ['outdated-actions']
|
|
411
|
+
});
|
|
412
|
+
console.log('Added outdated-actions label');
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.log('Could not add label (might not exist in repo):', error.message);
|
|
415
|
+
}
|
|
416
|
+
} else if (!hasUpdates && hasOutdatedLabel) {
|
|
417
|
+
// Remove label if no updates
|
|
418
|
+
try {
|
|
419
|
+
await github.rest.issues.removeLabel({
|
|
420
|
+
owner: context.repo.owner,
|
|
421
|
+
repo: context.repo.repo,
|
|
422
|
+
issue_number: context.issue.number,
|
|
423
|
+
name: 'outdated-actions'
|
|
424
|
+
});
|
|
425
|
+
console.log('Removed outdated-actions label');
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.log('Could not remove label:', error.message);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
- name: Fail if outdated actions found
|
|
432
|
+
if: steps.actions-check.outputs.has-updates == 'true'
|
|
433
|
+
run: |
|
|
434
|
+
echo "::error:: Found ${{ steps.actions-check.outputs.update-count }} outdated GitHub Actions. Please update them before merging."
|
|
435
|
+
echo ""
|
|
436
|
+
echo "You can update them by running: npx actions-up"
|
|
437
|
+
echo "Or manually update the versions in your workflow files."
|
|
438
|
+
exit 1
|
|
439
|
+
````
|
|
440
|
+
|
|
441
|
+
</details>
|
|
442
|
+
|
|
443
|
+
### Advanced PR Integration with Comments
|
|
444
|
+
|
|
445
|
+
For a more sophisticated integration that comments directly on PRs with detailed update information, check out our [example workflow with PR comments](https://github.com/azat-io/actions-up/blob/main/examples/workflows/check-with-comments.yml).
|
|
446
|
+
|
|
447
|
+
This advanced workflow:
|
|
448
|
+
|
|
449
|
+
- Comments on PRs with a formatted table of available updates
|
|
450
|
+
- Adds labels to PRs with outdated actions
|
|
451
|
+
- Includes links to release notes for breaking changes
|
|
452
|
+
- Updates existing comments instead of creating duplicates
|
|
453
|
+
|
|
454
|
+
### Scheduled Checks
|
|
455
|
+
|
|
456
|
+
You can also set up scheduled checks to stay informed about updates:
|
|
457
|
+
|
|
458
|
+
```yaml
|
|
459
|
+
name: Weekly Actions Update Check
|
|
460
|
+
|
|
461
|
+
on:
|
|
462
|
+
schedule:
|
|
463
|
+
- cron: '0 9 * * 1' # Every Monday at 9 AM
|
|
464
|
+
workflow_dispatch: # Allow manual triggers
|
|
465
|
+
|
|
466
|
+
jobs:
|
|
467
|
+
check-updates:
|
|
468
|
+
runs-on: ubuntu-latest
|
|
469
|
+
steps:
|
|
470
|
+
- uses: actions/checkout@v4
|
|
471
|
+
- uses: actions/setup-node@v4
|
|
472
|
+
with:
|
|
473
|
+
node-version: '20'
|
|
474
|
+
- run: npm install -g actions-up
|
|
475
|
+
- run: |
|
|
476
|
+
if actions-up --dry-run | grep -q "→"; then
|
|
477
|
+
echo "Updates available! Run 'npx actions-up' to update."
|
|
478
|
+
exit 1
|
|
479
|
+
fi
|
|
480
|
+
```
|
|
481
|
+
|
|
116
482
|
## Example
|
|
117
483
|
|
|
118
484
|
```yaml
|
|
@@ -136,6 +502,38 @@ While Actions Up works without authentication, providing a GitHub token increase
|
|
|
136
502
|
- For public repositories: Select `public_repo` scope
|
|
137
503
|
- For private repositories: Select `repo` scope
|
|
138
504
|
|
|
505
|
+
Set the token as an environment variable:
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
export GITHUB_TOKEN=your_token_here
|
|
509
|
+
npx actions-up
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Or in GitHub Actions:
|
|
513
|
+
|
|
514
|
+
```yaml
|
|
515
|
+
- name: Check for updates
|
|
516
|
+
env:
|
|
517
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
518
|
+
run: npx actions-up --dry-run
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Command Line Options
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
# Update all actions without prompts
|
|
525
|
+
npx actions-up --yes
|
|
526
|
+
|
|
527
|
+
# Check for updates without making changes
|
|
528
|
+
npx actions-up --dry-run
|
|
529
|
+
|
|
530
|
+
# Update only non-breaking changes
|
|
531
|
+
npx actions-up --breaking false
|
|
532
|
+
|
|
533
|
+
# Specify custom workflow directory
|
|
534
|
+
npx actions-up --workflows ./custom/workflows
|
|
535
|
+
```
|
|
536
|
+
|
|
139
537
|
## Security
|
|
140
538
|
|
|
141
539
|
Actions Up promotes security best practices:
|
|
@@ -143,6 +541,16 @@ Actions Up promotes security best practices:
|
|
|
143
541
|
- **SHA Pinning**: Uses commit SHA instead of mutable tags
|
|
144
542
|
- **Version Comments**: Adds version as comment for readability
|
|
145
543
|
- **No Auto-Updates**: Full control over what gets updated
|
|
544
|
+
- **Breaking Change Warnings**: Alerts you to major version updates that may require configuration changes
|
|
545
|
+
|
|
546
|
+
## CI/CD Best Practices
|
|
547
|
+
|
|
548
|
+
When using Actions Up in your CI/CD pipeline:
|
|
549
|
+
|
|
550
|
+
1. **Start with warnings**: Begin by running checks without failing builds to gauge the update frequency
|
|
551
|
+
2. **Regular updates**: Schedule weekly or monthly update PRs rather than blocking every PR
|
|
552
|
+
3. **Team education**: Ensure your team understands the security benefits of keeping actions updated
|
|
553
|
+
4. **Gradual adoption**: Roll out to a few repositories first before organization-wide deployment
|
|
146
554
|
|
|
147
555
|
## Contributing
|
|
148
556
|
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { CompositeActionStep } from '../../../types/composite-action-step';
|
|
2
|
-
/**
|
|
3
|
-
* Type guard to check if a value conforms to the CompositeActionStep interface.
|
|
4
|
-
*
|
|
5
|
-
* @param value - The value to check.
|
|
6
|
-
* @returns True if the value is a valid composite action step.
|
|
7
|
-
*/
|
|
8
|
-
export declare function isCompositeActionStep(value: unknown): value is CompositeActionStep;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { WorkflowJob } from '../../../types/workflow-job';
|
|
2
|
-
/**
|
|
3
|
-
* Type guard to check if a value conforms to the WorkflowJob interface.
|
|
4
|
-
*
|
|
5
|
-
* @param value - The value to check.
|
|
6
|
-
* @returns True if the value is a valid workflow job.
|
|
7
|
-
*/
|
|
8
|
-
export declare function isWorkflowJob(value: unknown): value is WorkflowJob;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { WorkflowStep } from '../../../types/workflow-step';
|
|
2
|
-
/**
|
|
3
|
-
* Type guard to check if a value conforms to the WorkflowStep interface.
|
|
4
|
-
*
|
|
5
|
-
* @param value - The value to check.
|
|
6
|
-
* @returns True if the value is a valid workflow step.
|
|
7
|
-
*/
|
|
8
|
-
export declare function isWorkflowStep(value: unknown): value is WorkflowStep;
|