actions-up 0.0.1 → 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/bin/actions-up.js +5 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +67 -0
- package/dist/core/api/check-updates.d.ts +10 -0
- package/dist/core/api/check-updates.js +139 -0
- package/dist/core/api/client.d.ts +79 -0
- package/dist/core/api/client.js +187 -0
- package/dist/core/ast/guards/has-range.d.ts +10 -0
- package/dist/core/ast/guards/has-range.js +4 -0
- package/dist/core/ast/guards/is-node.d.ts +8 -0
- package/dist/core/ast/guards/is-node.js +4 -0
- package/dist/core/ast/guards/is-pair.d.ts +8 -0
- package/dist/core/ast/guards/is-pair.js +4 -0
- package/dist/core/ast/guards/is-scalar.d.ts +8 -0
- package/dist/core/ast/guards/is-scalar.js +4 -0
- package/dist/core/ast/guards/is-yaml-map.d.ts +8 -0
- package/dist/core/ast/guards/is-yaml-map.js +4 -0
- package/dist/core/ast/guards/is-yaml-sequence.d.ts +8 -0
- package/dist/core/ast/guards/is-yaml-sequence.js +4 -0
- package/dist/core/ast/scanners/scan-composite-action-ast.d.ts +14 -0
- package/dist/core/ast/scanners/scan-composite-action-ast.js +18 -0
- package/dist/core/ast/scanners/scan-workflow-ast.d.ts +14 -0
- package/dist/core/ast/scanners/scan-workflow-ast.js +23 -0
- package/dist/core/ast/update/apply-updates.d.ts +7 -0
- package/dist/core/ast/update/apply-updates.js +40 -0
- package/dist/core/ast/utils/extract-uses-from-steps.d.ts +13 -0
- package/dist/core/ast/utils/extract-uses-from-steps.js +24 -0
- package/dist/core/ast/utils/find-map-pair.d.ts +12 -0
- package/dist/core/ast/utils/find-map-pair.js +10 -0
- package/dist/core/ast/utils/get-line-number.d.ts +10 -0
- package/dist/core/ast/utils/get-line-number.js +9 -0
- package/dist/core/constants.d.ts +4 -0
- package/dist/core/constants.js +4 -0
- package/dist/core/fs/is-yaml-file.d.ts +7 -0
- package/dist/core/fs/is-yaml-file.js +4 -0
- package/dist/core/fs/read-yaml-document.d.ts +11 -0
- package/dist/core/fs/read-yaml-document.js +11 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +4 -0
- package/dist/core/interactive/format-version.d.ts +7 -0
- package/dist/core/interactive/format-version.js +5 -0
- package/dist/core/interactive/pad-string.d.ts +8 -0
- package/dist/core/interactive/pad-string.js +9 -0
- package/dist/core/interactive/prompt-update-selection.d.ts +2 -0
- package/dist/core/interactive/prompt-update-selection.js +203 -0
- package/dist/core/interactive/strip-ansi.d.ts +7 -0
- package/dist/core/interactive/strip-ansi.js +21 -0
- package/dist/core/parsing/parse-action-reference.d.ts +30 -0
- package/dist/core/parsing/parse-action-reference.js +34 -0
- package/dist/core/scan-action-file.d.ts +10 -0
- package/dist/core/scan-action-file.js +7 -0
- package/dist/core/scan-github-actions.d.ts +17 -0
- package/dist/core/scan-github-actions.js +116 -0
- package/dist/core/scan-workflow-file.d.ts +9 -0
- package/dist/core/scan-workflow-file.js +7 -0
- package/dist/core/schema/composite/is-composite-action-runs.d.ts +8 -0
- package/dist/core/schema/composite/is-composite-action-runs.js +6 -0
- package/dist/core/schema/composite/is-composite-action-step.d.ts +8 -0
- package/dist/core/schema/composite/is-composite-action-structure.d.ts +9 -0
- package/dist/core/schema/composite/is-composite-action-structure.js +6 -0
- package/dist/core/schema/workflow/is-workflow-job.d.ts +8 -0
- package/dist/core/schema/workflow/is-workflow-step.d.ts +8 -0
- package/dist/core/schema/workflow/is-workflow-structure.d.ts +8 -0
- package/dist/core/schema/workflow/is-workflow-structure.js +6 -0
- package/dist/package.js +2 -0
- package/dist/types/action-update.d.ts +21 -0
- package/dist/types/composite-action-runs.d.ts +12 -0
- package/dist/types/composite-action-step.d.ts +23 -0
- package/dist/types/composite-action-structure.d.ts +21 -0
- package/dist/types/github-action.d.ts +23 -0
- package/dist/types/scan-result.d.ts +12 -0
- package/dist/types/workflow-job.d.ts +18 -0
- package/dist/types/workflow-step.d.ts +20 -0
- package/dist/types/workflow-structure.d.ts +15 -0
- package/license.md +20 -0
- package/package.json +52 -1
- package/readme.md +175 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { promptUpdateSelection } from "../core/interactive/prompt-update-selection.js";
|
|
2
|
+
import { applyUpdates } from "../core/ast/update/apply-updates.js";
|
|
3
|
+
import { checkUpdates } from "../core/api/check-updates.js";
|
|
4
|
+
import { scanGitHubActions } from "../core/scan-github-actions.js";
|
|
5
|
+
import "../core/index.js";
|
|
6
|
+
import { version } from "../package.js";
|
|
7
|
+
import { createSpinner } from "nanospinner";
|
|
8
|
+
import "node:worker_threads";
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
import cac from "cac";
|
|
11
|
+
function run() {
|
|
12
|
+
let cli = cac("actions-up");
|
|
13
|
+
cli.help().version(version).option("--yes, -y", "Skip all confirmations").command("", "Update GitHub Actions").action(async (options) => {
|
|
14
|
+
console.info(pc.cyan("\n🚀 Actions Up!\n"));
|
|
15
|
+
let spinner = createSpinner("Scanning GitHub Actions...").start();
|
|
16
|
+
try {
|
|
17
|
+
let scanResult = await scanGitHubActions(process.cwd());
|
|
18
|
+
let totalActions = scanResult.actions.length;
|
|
19
|
+
let totalWorkflows = scanResult.workflows.size;
|
|
20
|
+
let totalCompositeActions = scanResult.compositeActions.size;
|
|
21
|
+
spinner.success(`Found ${pc.yellow(totalActions)} actions in ${pc.yellow(totalWorkflows)} workflows and ${pc.yellow(totalCompositeActions)} composite actions`);
|
|
22
|
+
if (totalActions === 0) {
|
|
23
|
+
console.info(pc.green("\n✨ No GitHub Actions found in this repository"));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
spinner = createSpinner("Checking for updates...").start();
|
|
27
|
+
let updates = await checkUpdates(scanResult.actions, process.env["GITHUB_TOKEN"]);
|
|
28
|
+
let outdated = updates.filter((update) => update.hasUpdate);
|
|
29
|
+
let breaking = outdated.filter((update) => update.isBreaking);
|
|
30
|
+
if (outdated.length === 0) {
|
|
31
|
+
spinner.success("All actions are up to date!");
|
|
32
|
+
console.info(pc.green("\n✨ Everything is already at the latest version!\n"));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
spinner.success(`Found ${pc.yellow(outdated.length)} updates available${breaking.length > 0 ? ` (${pc.red(breaking.length)} breaking)` : ""}`);
|
|
36
|
+
if (options.yes) {
|
|
37
|
+
let toUpdate = outdated.filter((update) => update.latestSha);
|
|
38
|
+
if (toUpdate.length === 0) {
|
|
39
|
+
console.info(pc.yellow("\n⚠️ No actions with SHA available for update\n"));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.info(pc.yellow(`\n🔄 Updating ${toUpdate.length} actions...\n`));
|
|
43
|
+
await applyUpdates(toUpdate);
|
|
44
|
+
console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
45
|
+
} else {
|
|
46
|
+
let selected = await promptUpdateSelection(updates);
|
|
47
|
+
if (!selected || selected.length === 0) {
|
|
48
|
+
console.info(pc.gray("\nNo updates applied"));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.info(pc.yellow(`\n🔄 Updating ${selected.length} selected actions...\n`));
|
|
52
|
+
await applyUpdates(selected);
|
|
53
|
+
console.info(pc.green("\n✓ Updates applied successfully!"));
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
spinner.error("Failed");
|
|
57
|
+
if (error instanceof Error && error.name === "GitHubRateLimitError") {
|
|
58
|
+
console.error(pc.yellow("\n⚠️ Rate Limit Exceeded\n"));
|
|
59
|
+
console.error(error.message);
|
|
60
|
+
console.error(pc.gray("\nExample: GITHUB_TOKEN=ghp_xxxx actions-up\n"));
|
|
61
|
+
} else console.error(pc.red("\nError:"), error instanceof Error ? error.message : String(error));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
cli.parse();
|
|
66
|
+
}
|
|
67
|
+
export { run };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GitHubAction } from '../../types/github-action';
|
|
2
|
+
import { ActionUpdate } from '../../types/action-update';
|
|
3
|
+
/**
|
|
4
|
+
* Check for updates for GitHub Actions.
|
|
5
|
+
*
|
|
6
|
+
* @param actions - Array of GitHub Actions to check.
|
|
7
|
+
* @param token - Optional GitHub token for authentication.
|
|
8
|
+
* @returns Array of update information.
|
|
9
|
+
*/
|
|
10
|
+
export declare function checkUpdates(actions: GitHubAction[], token?: string): Promise<ActionUpdate[]>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Client } from "./client.js";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
async function checkUpdates(actions, token) {
|
|
4
|
+
let client = new Client(token);
|
|
5
|
+
let externalActions = actions.filter((action) => action.type === "external");
|
|
6
|
+
if (externalActions.length === 0) return [];
|
|
7
|
+
let uniqueActions = /* @__PURE__ */ new Map();
|
|
8
|
+
for (let action of externalActions) {
|
|
9
|
+
let group = uniqueActions.get(action.name) ?? [];
|
|
10
|
+
group.push(action);
|
|
11
|
+
uniqueActions.set(action.name, group);
|
|
12
|
+
}
|
|
13
|
+
let sharedState = {
|
|
14
|
+
rateLimitError: null,
|
|
15
|
+
rateLimitHit: false
|
|
16
|
+
};
|
|
17
|
+
let releaseResults = await [...uniqueActions.keys()].reduce((promise, actionName) => promise.then(async (results) => {
|
|
18
|
+
if (sharedState.rateLimitHit) return [...results, {
|
|
19
|
+
version: null,
|
|
20
|
+
actionName,
|
|
21
|
+
sha: null
|
|
22
|
+
}];
|
|
23
|
+
let [owner, repo] = actionName.split("/");
|
|
24
|
+
if (!owner || !repo) return [...results, {
|
|
25
|
+
version: null,
|
|
26
|
+
actionName,
|
|
27
|
+
sha: null
|
|
28
|
+
}];
|
|
29
|
+
try {
|
|
30
|
+
let release = await client.getLatestRelease(owner, repo);
|
|
31
|
+
if (!release) {
|
|
32
|
+
let allReleases = await client.getAllReleases(owner, repo, 10);
|
|
33
|
+
let stableRelease = allReleases.find((currentRelease) => !currentRelease.isPrerelease);
|
|
34
|
+
release = stableRelease ?? allReleases[0] ?? null;
|
|
35
|
+
}
|
|
36
|
+
if (release) {
|
|
37
|
+
let { version, sha } = release;
|
|
38
|
+
if (!sha) try {
|
|
39
|
+
let tagInfo = await client.getTagInfo(owner, repo, version);
|
|
40
|
+
sha = tagInfo?.sha ?? null;
|
|
41
|
+
} catch {}
|
|
42
|
+
return [...results, {
|
|
43
|
+
actionName,
|
|
44
|
+
version,
|
|
45
|
+
sha
|
|
46
|
+
}];
|
|
47
|
+
}
|
|
48
|
+
return [...results, {
|
|
49
|
+
version: null,
|
|
50
|
+
actionName,
|
|
51
|
+
sha: null
|
|
52
|
+
}];
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof Error && error.name === "GitHubRateLimitError") {
|
|
55
|
+
sharedState.rateLimitHit = true;
|
|
56
|
+
sharedState.rateLimitError = error;
|
|
57
|
+
return [...results, {
|
|
58
|
+
version: null,
|
|
59
|
+
actionName,
|
|
60
|
+
sha: null
|
|
61
|
+
}];
|
|
62
|
+
}
|
|
63
|
+
console.warn(`Failed to check ${actionName}:`, error);
|
|
64
|
+
return [...results, {
|
|
65
|
+
version: null,
|
|
66
|
+
actionName,
|
|
67
|
+
sha: null
|
|
68
|
+
}];
|
|
69
|
+
}
|
|
70
|
+
}), Promise.resolve([]));
|
|
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#with-github-token");
|
|
73
|
+
error.name = "GitHubRateLimitError";
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
let cache = /* @__PURE__ */ new Map();
|
|
77
|
+
for (let result of releaseResults) cache.set(result.actionName, {
|
|
78
|
+
version: result.version,
|
|
79
|
+
sha: result.sha
|
|
80
|
+
});
|
|
81
|
+
let updates = [];
|
|
82
|
+
for (let action of externalActions) {
|
|
83
|
+
let cached = cache.get(action.name);
|
|
84
|
+
if (cached) updates.push(createUpdate(action, cached.version, cached.sha));
|
|
85
|
+
else updates.push(createUpdate(action, null, null));
|
|
86
|
+
}
|
|
87
|
+
return updates;
|
|
88
|
+
}
|
|
89
|
+
function createUpdate(action, latestVersion, latestSha) {
|
|
90
|
+
let currentVersion = normalizeVersion(action.version ?? "");
|
|
91
|
+
let normalized = latestVersion ? normalizeVersion(latestVersion) : null;
|
|
92
|
+
let hasUpdate = false;
|
|
93
|
+
let isBreaking = false;
|
|
94
|
+
if (currentVersion && isSha(currentVersion)) {
|
|
95
|
+
if (latestSha) hasUpdate = !compareSha(currentVersion, latestSha);
|
|
96
|
+
else if (normalized) hasUpdate = true;
|
|
97
|
+
} else if (currentVersion && normalized) {
|
|
98
|
+
let current = semver.valid(currentVersion);
|
|
99
|
+
let latest = semver.valid(normalized);
|
|
100
|
+
if (current && latest) {
|
|
101
|
+
hasUpdate = semver.lt(current, latest);
|
|
102
|
+
if (hasUpdate) {
|
|
103
|
+
let currentMajor = semver.major(current);
|
|
104
|
+
let latestMajor = semver.major(latest);
|
|
105
|
+
isBreaking = latestMajor > currentMajor;
|
|
106
|
+
}
|
|
107
|
+
} else if (currentVersion !== normalized) hasUpdate = true;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
currentVersion: action.version ?? "unknown",
|
|
111
|
+
latestVersion,
|
|
112
|
+
isBreaking,
|
|
113
|
+
latestSha,
|
|
114
|
+
hasUpdate,
|
|
115
|
+
action
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function compareSha(sha1, sha2) {
|
|
119
|
+
if (!sha1 || !sha2) return false;
|
|
120
|
+
let normalized1 = sha1.replace(/^v/u, "");
|
|
121
|
+
let normalized2 = sha2.replace(/^v/u, "");
|
|
122
|
+
let minLength = Math.min(normalized1.length, normalized2.length);
|
|
123
|
+
if (minLength < 7) return false;
|
|
124
|
+
return normalized1.slice(0, Math.max(0, minLength)).toLowerCase() === normalized2.slice(0, Math.max(0, minLength)).toLowerCase();
|
|
125
|
+
}
|
|
126
|
+
function normalizeVersion(version) {
|
|
127
|
+
if (!version) return null;
|
|
128
|
+
let normalized = version.replace(/^v/u, "");
|
|
129
|
+
if (/^[0-9a-f]{7,40}$/iu.test(normalized)) return version;
|
|
130
|
+
let coerced = semver.coerce(normalized);
|
|
131
|
+
if (coerced) return coerced.version;
|
|
132
|
+
return version;
|
|
133
|
+
}
|
|
134
|
+
function isSha(value) {
|
|
135
|
+
if (!value) return false;
|
|
136
|
+
let normalized = value.replace(/^v/u, "");
|
|
137
|
+
return /^[0-9a-f]{7,40}$/iu.test(normalized);
|
|
138
|
+
}
|
|
139
|
+
export { checkUpdates };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Processed release information with normalized types. */
|
|
2
|
+
interface ReleaseInfo {
|
|
3
|
+
/** Release description or null if not provided. */
|
|
4
|
+
description: string | null;
|
|
5
|
+
/** Whether this release is marked as a pre-release. */
|
|
6
|
+
isPrerelease: boolean;
|
|
7
|
+
/** Git commit SHA for this release. */
|
|
8
|
+
sha: string | null;
|
|
9
|
+
/** Date when the release was published. */
|
|
10
|
+
publishedAt: Date;
|
|
11
|
+
/** Version tag name (e.g., 'v1.2.3'). */
|
|
12
|
+
version: string;
|
|
13
|
+
/** Release name or tag name if name not provided. */
|
|
14
|
+
name: string;
|
|
15
|
+
/** GitHub URL for this release. */
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
/** Processed tag information with normalized types. */
|
|
19
|
+
interface TagInfo {
|
|
20
|
+
/** Tag or commit message, null if not provided. */
|
|
21
|
+
message: string | null;
|
|
22
|
+
/** Date when the tag was created or committed. */
|
|
23
|
+
date: Date | null;
|
|
24
|
+
/** Tag name (e.g., 'v1.2.3'). */
|
|
25
|
+
tag: string;
|
|
26
|
+
/** Git commit SHA that this tag points to. */
|
|
27
|
+
sha: string;
|
|
28
|
+
}
|
|
29
|
+
/** GitHub GraphQL client with optional authentication. */
|
|
30
|
+
export declare class Client {
|
|
31
|
+
private readonly graphqlWithAuth;
|
|
32
|
+
private rateLimitRemaining;
|
|
33
|
+
private rateLimitReset;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new GitHub API client.
|
|
36
|
+
*
|
|
37
|
+
* @param token - Optional GitHub token for authentication.
|
|
38
|
+
*/
|
|
39
|
+
constructor(token?: string);
|
|
40
|
+
private static isRateLimitError;
|
|
41
|
+
/**
|
|
42
|
+
* Get specific tag/version information.
|
|
43
|
+
*
|
|
44
|
+
* @param owner - The repository owner.
|
|
45
|
+
* @param repo - The repository name.
|
|
46
|
+
* @param tag - The tag name to fetch.
|
|
47
|
+
* @returns Tag information or null if not found.
|
|
48
|
+
*/
|
|
49
|
+
getTagInfo(owner: string, repo: string, tag: string): Promise<TagInfo | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Get all releases for a repository.
|
|
52
|
+
*
|
|
53
|
+
* @param owner - The repository owner.
|
|
54
|
+
* @param repo - The repository name.
|
|
55
|
+
* @param limit - Maximum number of releases to fetch.
|
|
56
|
+
* @returns Array of release information.
|
|
57
|
+
*/
|
|
58
|
+
getAllReleases(owner: string, repo: string, limit?: number): Promise<ReleaseInfo[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Get the latest release for a GitHub repository.
|
|
61
|
+
*
|
|
62
|
+
* @param owner - The repository owner.
|
|
63
|
+
* @param repo - The repository name.
|
|
64
|
+
* @returns Latest release information or null if not found.
|
|
65
|
+
*/
|
|
66
|
+
getLatestRelease(owner: string, repo: string): Promise<ReleaseInfo | null>;
|
|
67
|
+
getRateLimitStatus(): {
|
|
68
|
+
remaining: number;
|
|
69
|
+
resetAt: Date;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Check if we should wait before making more requests.
|
|
73
|
+
*
|
|
74
|
+
* @param threshold - Minimum remaining requests before waiting.
|
|
75
|
+
* @returns True if rate limit is below threshold.
|
|
76
|
+
*/
|
|
77
|
+
shouldWaitForRateLimit(threshold?: number): boolean;
|
|
78
|
+
}
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { GraphqlResponseError, graphql } from "@octokit/graphql";
|
|
2
|
+
var GitHubRateLimitError = class extends Error {
|
|
3
|
+
constructor(resetAt) {
|
|
4
|
+
let resetTime = resetAt.toLocaleTimeString();
|
|
5
|
+
super(`GitHub API rate limit exceeded. Resets at ${resetTime}`);
|
|
6
|
+
this.name = "GitHubRateLimitError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var Client = class Client {
|
|
10
|
+
graphqlWithAuth;
|
|
11
|
+
rateLimitRemaining = 5e3;
|
|
12
|
+
rateLimitReset = /* @__PURE__ */ new Date();
|
|
13
|
+
constructor(token) {
|
|
14
|
+
let authToken = token ?? process.env["GITHUB_TOKEN"];
|
|
15
|
+
this.graphqlWithAuth = graphql.defaults({ headers: authToken ? { authorization: `token ${authToken}` } : {} });
|
|
16
|
+
if (!authToken) console.warn("No GitHub token found. API rate limits will be restricted.");
|
|
17
|
+
}
|
|
18
|
+
static isRateLimitError(error) {
|
|
19
|
+
if (error instanceof GraphqlResponseError) return error.errors.some((graphQLError) => graphQLError.type === "RATE_LIMITED" || typeof graphQLError.message === "string" && /rate limit/iu.test(graphQLError.message));
|
|
20
|
+
if (error instanceof Error) return /rate limit/iu.test(error.message);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
async getTagInfo(owner, repo, tag) {
|
|
24
|
+
try {
|
|
25
|
+
let qualifiedTag = tag.startsWith("refs/tags/") ? tag : `refs/tags/${tag}`;
|
|
26
|
+
let displayTag = tag.replace(/^refs\/tags\//u, "");
|
|
27
|
+
let query = `
|
|
28
|
+
query getTagInfo($owner: String!, $repo: String!, $tag: String!) {
|
|
29
|
+
repository(owner: $owner, name: $repo) {
|
|
30
|
+
ref(qualifiedName: $tag) {
|
|
31
|
+
target {
|
|
32
|
+
oid
|
|
33
|
+
... on Commit {
|
|
34
|
+
committedDate
|
|
35
|
+
message
|
|
36
|
+
}
|
|
37
|
+
... on Tag {
|
|
38
|
+
tagger {
|
|
39
|
+
date
|
|
40
|
+
}
|
|
41
|
+
message
|
|
42
|
+
target {
|
|
43
|
+
oid
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
rateLimit {
|
|
50
|
+
remaining
|
|
51
|
+
resetAt
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
let response = await this.graphqlWithAuth(query, {
|
|
56
|
+
tag: qualifiedTag,
|
|
57
|
+
owner,
|
|
58
|
+
repo
|
|
59
|
+
});
|
|
60
|
+
this.rateLimitRemaining = response.rateLimit.remaining;
|
|
61
|
+
this.rateLimitReset = new Date(response.rateLimit.resetAt);
|
|
62
|
+
if (!response.repository?.ref?.target) return null;
|
|
63
|
+
let { target } = response.repository.ref;
|
|
64
|
+
if ("committedDate" in target) return {
|
|
65
|
+
date: target.committedDate ? new Date(target.committedDate) : null,
|
|
66
|
+
message: target.message || null,
|
|
67
|
+
sha: target.oid,
|
|
68
|
+
tag: displayTag
|
|
69
|
+
};
|
|
70
|
+
let tagObject = target.target;
|
|
71
|
+
return {
|
|
72
|
+
date: target.tagger?.date ? new Date(target.tagger.date) : null,
|
|
73
|
+
sha: tagObject?.oid ?? target.oid,
|
|
74
|
+
message: target.message ?? null,
|
|
75
|
+
tag: displayTag
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (Client.isRateLimitError(error)) throw new GitHubRateLimitError(this.rateLimitReset);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async getAllReleases(owner, repo, limit = 10) {
|
|
83
|
+
try {
|
|
84
|
+
let query = `
|
|
85
|
+
query getAllReleases($owner: String!, $repo: String!, $limit: Int!) {
|
|
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, {
|
|
111
|
+
owner,
|
|
112
|
+
limit,
|
|
113
|
+
repo
|
|
114
|
+
});
|
|
115
|
+
this.rateLimitRemaining = response.rateLimit.remaining;
|
|
116
|
+
this.rateLimitReset = new Date(response.rateLimit.resetAt);
|
|
117
|
+
if (!response.repository?.releases?.nodes) return [];
|
|
118
|
+
return response.repository.releases.nodes.map((release) => ({
|
|
119
|
+
sha: release.tagCommit?.oid ?? null,
|
|
120
|
+
publishedAt: new Date(release.publishedAt),
|
|
121
|
+
description: release.description ?? null,
|
|
122
|
+
name: release.name ?? release.tagName,
|
|
123
|
+
isPrerelease: release.isPrerelease,
|
|
124
|
+
url: release.url,
|
|
125
|
+
version: release.tagName
|
|
126
|
+
}));
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (Client.isRateLimitError(error)) throw new GitHubRateLimitError(this.rateLimitReset);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async getLatestRelease(owner, repo) {
|
|
133
|
+
try {
|
|
134
|
+
let query = `
|
|
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, {
|
|
156
|
+
owner,
|
|
157
|
+
repo
|
|
158
|
+
});
|
|
159
|
+
this.rateLimitRemaining = response.rateLimit.remaining;
|
|
160
|
+
this.rateLimitReset = new Date(response.rateLimit.resetAt);
|
|
161
|
+
if (!response.repository?.latestRelease) return null;
|
|
162
|
+
let release = response.repository.latestRelease;
|
|
163
|
+
return {
|
|
164
|
+
sha: release.tagCommit?.oid ?? null,
|
|
165
|
+
publishedAt: new Date(release.publishedAt),
|
|
166
|
+
description: release.description ?? null,
|
|
167
|
+
name: release.name ?? release.tagName,
|
|
168
|
+
isPrerelease: release.isPrerelease,
|
|
169
|
+
url: release.url,
|
|
170
|
+
version: release.tagName
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (Client.isRateLimitError(error)) throw new GitHubRateLimitError(this.rateLimitReset);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
getRateLimitStatus() {
|
|
178
|
+
return {
|
|
179
|
+
remaining: this.rateLimitRemaining,
|
|
180
|
+
resetAt: this.rateLimitReset
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
shouldWaitForRateLimit(threshold = 100) {
|
|
184
|
+
return this.rateLimitRemaining < threshold;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
export { Client };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard to check if a node has a range property for line number
|
|
3
|
+
* calculation.
|
|
4
|
+
*
|
|
5
|
+
* @param node - The node to check.
|
|
6
|
+
* @returns True if the node has a range property.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hasRange(node: unknown): node is {
|
|
9
|
+
range?: [number, number, number];
|
|
10
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Document } from 'yaml';
|
|
2
|
+
import { GitHubAction } from '../../../types/github-action';
|
|
3
|
+
/**
|
|
4
|
+
* Scans a parsed composite action YAML document for action references.
|
|
5
|
+
*
|
|
6
|
+
* Navigates AST structure `runs -> steps` and extracts `uses` entries with
|
|
7
|
+
* corresponding line numbers when the action defines `using: composite`.
|
|
8
|
+
*
|
|
9
|
+
* @param document - Parsed YAML document of a composite action file.
|
|
10
|
+
* @param content - Original file content.
|
|
11
|
+
* @param filePath - Path of the action file.
|
|
12
|
+
* @returns List of discovered actions.
|
|
13
|
+
*/
|
|
14
|
+
export declare function scanCompositeActionAst(document: Document, content: string, filePath: string): GitHubAction[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isYAMLMap } from "../guards/is-yaml-map.js";
|
|
2
|
+
import { extractUsesFromSteps } from "../utils/extract-uses-from-steps.js";
|
|
3
|
+
import { findMapPair } from "../utils/find-map-pair.js";
|
|
4
|
+
import { isCompositeActionStructure } from "../../schema/composite/is-composite-action-structure.js";
|
|
5
|
+
import { isCompositeActionRuns } from "../../schema/composite/is-composite-action-runs.js";
|
|
6
|
+
function scanCompositeActionAst(document, content, filePath) {
|
|
7
|
+
let action = document.toJSON();
|
|
8
|
+
if (!isCompositeActionStructure(action)) return [];
|
|
9
|
+
if (!document.contents || !isYAMLMap(document.contents)) return [];
|
|
10
|
+
let runsPair = findMapPair(document.contents, "runs");
|
|
11
|
+
if (!runsPair?.value || !isYAMLMap(runsPair.value)) return [];
|
|
12
|
+
let runsJson = action["runs"];
|
|
13
|
+
if (!runsJson || !isCompositeActionRuns(runsJson) || !runsJson["steps"] || !Array.isArray(runsJson["steps"])) return [];
|
|
14
|
+
let stepsPair = findMapPair(runsPair.value, "steps");
|
|
15
|
+
if (!stepsPair?.value) return [];
|
|
16
|
+
return extractUsesFromSteps(stepsPair.value, filePath, content);
|
|
17
|
+
}
|
|
18
|
+
export { scanCompositeActionAst };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Document } from 'yaml';
|
|
2
|
+
import { GitHubAction } from '../../../types/github-action';
|
|
3
|
+
/**
|
|
4
|
+
* Scans a parsed workflow YAML document for action references.
|
|
5
|
+
*
|
|
6
|
+
* Navigates AST structure `jobs -> <job> -> steps` and extracts `uses` entries
|
|
7
|
+
* with corresponding line numbers.
|
|
8
|
+
*
|
|
9
|
+
* @param document - Parsed YAML document of a workflow file.
|
|
10
|
+
* @param content - Original file content.
|
|
11
|
+
* @param filePath - Path of the workflow file to scan.
|
|
12
|
+
* @returns List of discovered actions.
|
|
13
|
+
*/
|
|
14
|
+
export declare function scanWorkflowAst(document: Document, content: string, filePath: string): GitHubAction[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isWorkflowStructure } from "../../schema/workflow/is-workflow-structure.js";
|
|
2
|
+
import { isYAMLMap } from "../guards/is-yaml-map.js";
|
|
3
|
+
import { isNode } from "../guards/is-node.js";
|
|
4
|
+
import { isPair } from "../guards/is-pair.js";
|
|
5
|
+
import { extractUsesFromSteps } from "../utils/extract-uses-from-steps.js";
|
|
6
|
+
import { findMapPair } from "../utils/find-map-pair.js";
|
|
7
|
+
function scanWorkflowAst(document, content, filePath) {
|
|
8
|
+
let workflow = document.toJSON();
|
|
9
|
+
if (!isWorkflowStructure(workflow)) return [];
|
|
10
|
+
if (!document.contents || !isYAMLMap(document.contents)) return [];
|
|
11
|
+
let jobsPair = findMapPair(document.contents, "jobs");
|
|
12
|
+
if (!jobsPair?.value || !isYAMLMap(jobsPair.value)) return [];
|
|
13
|
+
let actions = [];
|
|
14
|
+
for (let jobNode of jobsPair.value.items) {
|
|
15
|
+
if (!isPair(jobNode) || !jobNode.value || !isNode(jobNode.value)) continue;
|
|
16
|
+
if (!isYAMLMap(jobNode.value)) continue;
|
|
17
|
+
let stepsPair = findMapPair(jobNode.value, "steps");
|
|
18
|
+
if (!stepsPair?.value) continue;
|
|
19
|
+
actions.push(...extractUsesFromSteps(stepsPair.value, filePath, content));
|
|
20
|
+
}
|
|
21
|
+
return actions;
|
|
22
|
+
}
|
|
23
|
+
export { scanWorkflowAst };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ActionUpdate } from '../../../types/action-update';
|
|
2
|
+
/**
|
|
3
|
+
* Apply updates using SHA with version in comment for readability.
|
|
4
|
+
*
|
|
5
|
+
* @param updates - Array of updates to apply.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyUpdates(updates: ActionUpdate[]): Promise<void>;
|