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.
Files changed (77) hide show
  1. package/bin/actions-up.js +5 -0
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +67 -0
  4. package/dist/core/api/check-updates.d.ts +10 -0
  5. package/dist/core/api/check-updates.js +139 -0
  6. package/dist/core/api/client.d.ts +79 -0
  7. package/dist/core/api/client.js +187 -0
  8. package/dist/core/ast/guards/has-range.d.ts +10 -0
  9. package/dist/core/ast/guards/has-range.js +4 -0
  10. package/dist/core/ast/guards/is-node.d.ts +8 -0
  11. package/dist/core/ast/guards/is-node.js +4 -0
  12. package/dist/core/ast/guards/is-pair.d.ts +8 -0
  13. package/dist/core/ast/guards/is-pair.js +4 -0
  14. package/dist/core/ast/guards/is-scalar.d.ts +8 -0
  15. package/dist/core/ast/guards/is-scalar.js +4 -0
  16. package/dist/core/ast/guards/is-yaml-map.d.ts +8 -0
  17. package/dist/core/ast/guards/is-yaml-map.js +4 -0
  18. package/dist/core/ast/guards/is-yaml-sequence.d.ts +8 -0
  19. package/dist/core/ast/guards/is-yaml-sequence.js +4 -0
  20. package/dist/core/ast/scanners/scan-composite-action-ast.d.ts +14 -0
  21. package/dist/core/ast/scanners/scan-composite-action-ast.js +18 -0
  22. package/dist/core/ast/scanners/scan-workflow-ast.d.ts +14 -0
  23. package/dist/core/ast/scanners/scan-workflow-ast.js +23 -0
  24. package/dist/core/ast/update/apply-updates.d.ts +7 -0
  25. package/dist/core/ast/update/apply-updates.js +40 -0
  26. package/dist/core/ast/utils/extract-uses-from-steps.d.ts +13 -0
  27. package/dist/core/ast/utils/extract-uses-from-steps.js +24 -0
  28. package/dist/core/ast/utils/find-map-pair.d.ts +12 -0
  29. package/dist/core/ast/utils/find-map-pair.js +10 -0
  30. package/dist/core/ast/utils/get-line-number.d.ts +10 -0
  31. package/dist/core/ast/utils/get-line-number.js +9 -0
  32. package/dist/core/constants.d.ts +4 -0
  33. package/dist/core/constants.js +4 -0
  34. package/dist/core/fs/is-yaml-file.d.ts +7 -0
  35. package/dist/core/fs/is-yaml-file.js +4 -0
  36. package/dist/core/fs/read-yaml-document.d.ts +11 -0
  37. package/dist/core/fs/read-yaml-document.js +11 -0
  38. package/dist/core/index.d.ts +3 -0
  39. package/dist/core/index.js +4 -0
  40. package/dist/core/interactive/format-version.d.ts +7 -0
  41. package/dist/core/interactive/format-version.js +5 -0
  42. package/dist/core/interactive/pad-string.d.ts +8 -0
  43. package/dist/core/interactive/pad-string.js +9 -0
  44. package/dist/core/interactive/prompt-update-selection.d.ts +2 -0
  45. package/dist/core/interactive/prompt-update-selection.js +203 -0
  46. package/dist/core/interactive/strip-ansi.d.ts +7 -0
  47. package/dist/core/interactive/strip-ansi.js +21 -0
  48. package/dist/core/parsing/parse-action-reference.d.ts +30 -0
  49. package/dist/core/parsing/parse-action-reference.js +34 -0
  50. package/dist/core/scan-action-file.d.ts +10 -0
  51. package/dist/core/scan-action-file.js +7 -0
  52. package/dist/core/scan-github-actions.d.ts +17 -0
  53. package/dist/core/scan-github-actions.js +116 -0
  54. package/dist/core/scan-workflow-file.d.ts +9 -0
  55. package/dist/core/scan-workflow-file.js +7 -0
  56. package/dist/core/schema/composite/is-composite-action-runs.d.ts +8 -0
  57. package/dist/core/schema/composite/is-composite-action-runs.js +6 -0
  58. package/dist/core/schema/composite/is-composite-action-step.d.ts +8 -0
  59. package/dist/core/schema/composite/is-composite-action-structure.d.ts +9 -0
  60. package/dist/core/schema/composite/is-composite-action-structure.js +6 -0
  61. package/dist/core/schema/workflow/is-workflow-job.d.ts +8 -0
  62. package/dist/core/schema/workflow/is-workflow-step.d.ts +8 -0
  63. package/dist/core/schema/workflow/is-workflow-structure.d.ts +8 -0
  64. package/dist/core/schema/workflow/is-workflow-structure.js +6 -0
  65. package/dist/package.js +2 -0
  66. package/dist/types/action-update.d.ts +21 -0
  67. package/dist/types/composite-action-runs.d.ts +12 -0
  68. package/dist/types/composite-action-step.d.ts +23 -0
  69. package/dist/types/composite-action-structure.d.ts +21 -0
  70. package/dist/types/github-action.d.ts +23 -0
  71. package/dist/types/scan-result.d.ts +12 -0
  72. package/dist/types/workflow-job.d.ts +18 -0
  73. package/dist/types/workflow-step.d.ts +20 -0
  74. package/dist/types/workflow-structure.d.ts +15 -0
  75. package/license.md +20 -0
  76. package/package.json +52 -1
  77. package/readme.md +175 -0
@@ -0,0 +1,116 @@
1
+ import { ACTIONS_DIRECTORY, GITHUB_DIRECTORY, WORKFLOWS_DIRECTORY } from "./constants.js";
2
+ import { scanWorkflowFile } from "./scan-workflow-file.js";
3
+ import { scanActionFile } from "./scan-action-file.js";
4
+ import { isYamlFile } from "./fs/is-yaml-file.js";
5
+ import { isAbsolute, join, relative, resolve } from "node:path";
6
+ import { readdir, stat } from "node:fs/promises";
7
+ async function scanGitHubActions(rootPath = process.cwd()) {
8
+ let result = {
9
+ compositeActions: /* @__PURE__ */ new Map(),
10
+ workflows: /* @__PURE__ */ new Map(),
11
+ actions: []
12
+ };
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;
26
+ }
27
+ let workflowsPath = join(githubPath, WORKFLOWS_DIRECTORY);
28
+ if (!isWithin(normalizedRoot, workflowsPath)) return result;
29
+ try {
30
+ let workflowsStat = await stat(workflowsPath);
31
+ if (workflowsStat.isDirectory()) {
32
+ let files = await readdir(workflowsPath);
33
+ let workflowPromises = files.filter((file) => {
34
+ if (!isValidName(file)) return false;
35
+ return isYamlFile(file);
36
+ }).map(async (file) => {
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
+ }
46
+ try {
47
+ let actions = await scanWorkflowFile(filePath);
48
+ return {
49
+ path: `${GITHUB_DIRECTORY}/${WORKFLOWS_DIRECTORY}/${file}`,
50
+ success: true,
51
+ actions
52
+ };
53
+ } catch {
54
+ return {
55
+ path: `${GITHUB_DIRECTORY}/${WORKFLOWS_DIRECTORY}/${file}`,
56
+ success: false,
57
+ actions: []
58
+ };
59
+ }
60
+ });
61
+ let workflowResults = await Promise.all(workflowPromises);
62
+ for (let workflow of workflowResults) if (workflow.success && workflow.path) if (workflow.actions.length > 0) {
63
+ result.workflows.set(workflow.path, workflow.actions);
64
+ result.actions.push(...workflow.actions);
65
+ } else result.workflows.set(workflow.path, []);
66
+ }
67
+ } catch {}
68
+ let actionsPath = join(githubPath, ACTIONS_DIRECTORY);
69
+ if (!isWithin(normalizedRoot, actionsPath)) return result;
70
+ try {
71
+ let actionsStat = await stat(actionsPath);
72
+ if (actionsStat.isDirectory()) {
73
+ let subdirectories = await readdir(actionsPath);
74
+ let actionPromises = subdirectories.map(async (subdir) => {
75
+ if (!isValidName(subdir)) return null;
76
+ let subdirPath = join(actionsPath, subdir);
77
+ if (!isWithin(actionsPath, subdirPath)) {
78
+ console.warn(`Skipping subdirectory outside actions path: ${subdir}`);
79
+ return null;
80
+ }
81
+ try {
82
+ let subdirectoryStat = await stat(subdirPath);
83
+ if (!subdirectoryStat.isDirectory()) return null;
84
+ let actionFilePath = join(subdirPath, "action.yml");
85
+ if (!isWithin(subdirPath, actionFilePath)) return null;
86
+ let actions = [];
87
+ try {
88
+ actions = await scanActionFile(actionFilePath);
89
+ } catch {
90
+ try {
91
+ actionFilePath = join(subdirPath, "action.yaml");
92
+ if (!isWithin(subdirPath, actionFilePath)) return null;
93
+ actions = await scanActionFile(actionFilePath);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ return {
99
+ path: `${GITHUB_DIRECTORY}/${ACTIONS_DIRECTORY}/${subdir}`,
100
+ name: subdir,
101
+ actions
102
+ };
103
+ } catch {
104
+ return null;
105
+ }
106
+ });
107
+ let actionResults = await Promise.all(actionPromises);
108
+ for (let actionResult of actionResults) if (actionResult) {
109
+ result.compositeActions.set(actionResult.name, actionResult.path);
110
+ result.actions.push(...actionResult.actions);
111
+ }
112
+ }
113
+ } catch {}
114
+ return result;
115
+ }
116
+ export { scanGitHubActions };
@@ -0,0 +1,9 @@
1
+ import { GitHubAction } from '../types/github-action';
2
+ /**
3
+ * Scans a GitHub workflow file for all action references.
4
+ *
5
+ * @param filePath - The path to the workflow YAML file to scan.
6
+ * @returns A promise that resolves to an array of GitHubAction objects found in
7
+ * the workflow.
8
+ */
9
+ export declare function scanWorkflowFile(filePath: string): Promise<GitHubAction[]>;
@@ -0,0 +1,7 @@
1
+ import { scanWorkflowAst } from "./ast/scanners/scan-workflow-ast.js";
2
+ import { readYamlDocument } from "./fs/read-yaml-document.js";
3
+ async function scanWorkflowFile(filePath) {
4
+ let { document, content } = await readYamlDocument(filePath);
5
+ return scanWorkflowAst(document, content, filePath);
6
+ }
7
+ export { scanWorkflowFile };
@@ -0,0 +1,8 @@
1
+ import { CompositeActionRuns } from '../../../types/composite-action-runs';
2
+ /**
3
+ * Type guard to check if a value conforms to the CompositeActionRuns interface.
4
+ *
5
+ * @param value - The value to check.
6
+ * @returns True if the value is a valid runs configuration.
7
+ */
8
+ export declare function isCompositeActionRuns(value: unknown): value is CompositeActionRuns;
@@ -0,0 +1,6 @@
1
+ function isCompositeActionRuns(value) {
2
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3
+ let object = value;
4
+ return "using" in object;
5
+ }
6
+ export { isCompositeActionRuns };
@@ -0,0 +1,8 @@
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;
@@ -0,0 +1,9 @@
1
+ import { CompositeActionStructure } from '../../../types/composite-action-structure';
2
+ /**
3
+ * Type guard to check if a value conforms to the CompositeActionStructure
4
+ * interface.
5
+ *
6
+ * @param value - The value to check.
7
+ * @returns True if the value is a valid composite action structure.
8
+ */
9
+ export declare function isCompositeActionStructure(value: unknown): value is CompositeActionStructure;
@@ -0,0 +1,6 @@
1
+ function isCompositeActionStructure(value) {
2
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3
+ let object = value;
4
+ return "name" in object || "description" in object || "runs" in object;
5
+ }
6
+ export { isCompositeActionStructure };
@@ -0,0 +1,8 @@
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;
@@ -0,0 +1,8 @@
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;
@@ -0,0 +1,8 @@
1
+ import { WorkflowStructure } from '../../../types/workflow-structure';
2
+ /**
3
+ * Type guard to check if a value conforms to the WorkflowStructure interface.
4
+ *
5
+ * @param value - The value to check.
6
+ * @returns True if the value is a valid workflow structure.
7
+ */
8
+ export declare function isWorkflowStructure(value: unknown): value is WorkflowStructure;
@@ -0,0 +1,6 @@
1
+ function isWorkflowStructure(value) {
2
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
3
+ let object = value;
4
+ return "on" in object || "name" in object || "jobs" in object;
5
+ }
6
+ export { isWorkflowStructure };
@@ -0,0 +1,2 @@
1
+ const version = "1.0.0";
2
+ export { version };
@@ -0,0 +1,21 @@
1
+ import { GitHubAction } from './github-action';
2
+ /** Update information for a GitHub Action. */
3
+ export interface ActionUpdate {
4
+ /** Current version string. */
5
+ currentVersion: string | null
6
+
7
+ /** Latest available version. */
8
+ latestVersion: string | null
9
+
10
+ /** SHA hash of the latest version. */
11
+ latestSha: string | null
12
+
13
+ /** The original action from scanning. */
14
+ action: GitHubAction
15
+
16
+ /** Whether this is a major version change. */
17
+ isBreaking: boolean
18
+
19
+ /** Whether an update is available. */
20
+ hasUpdate: boolean
21
+ }
@@ -0,0 +1,12 @@
1
+ import { CompositeActionStep } from './composite-action-step';
2
+ /** Represents the runs configuration for a composite action. */
3
+ export interface CompositeActionRuns {
4
+ /** Array of steps to execute. */
5
+ steps?: CompositeActionStep[]
6
+
7
+ /** Allow additional properties. */
8
+ [key: string]: unknown
9
+
10
+ /** Must be 'composite' for composite actions. */
11
+ using?: string
12
+ }
@@ -0,0 +1,23 @@
1
+ /** Represents a step in a composite GitHub Action. */
2
+ export interface CompositeActionStep {
3
+ /** Environment variables for this step. */
4
+ env?: Record<string, unknown>
5
+
6
+ /** Working directory for the step. */
7
+ 'working-directory'?: string
8
+
9
+ /** Allow additional properties. */
10
+ [key: string]: unknown
11
+
12
+ /** Shell to use for the run command. */
13
+ shell?: string
14
+
15
+ /** Action to use for this step. */
16
+ uses?: string
17
+
18
+ /** Display name for this step. */
19
+ name?: string
20
+
21
+ /** Shell command to run for this step. */
22
+ run?: string
23
+ }
@@ -0,0 +1,21 @@
1
+ import { CompositeActionRuns } from './composite-action-runs';
2
+ /** Represents the structure of a composite GitHub Action file. */
3
+ export interface CompositeActionStructure {
4
+ /** Output values from the action. */
5
+ outputs?: Record<string, unknown>
6
+
7
+ /** Input parameters for the action. */
8
+ inputs?: Record<string, unknown>
9
+
10
+ /** Runs configuration for composite actions. */
11
+ runs?: CompositeActionRuns
12
+
13
+ /** Allow additional properties. */
14
+ [key: string]: unknown
15
+
16
+ /** Description of what the action does. */
17
+ description?: string
18
+
19
+ /** Display name of the action. */
20
+ name?: string
21
+ }
@@ -0,0 +1,23 @@
1
+ /** Represents a GitHub Action used in workflows or composite actions. */
2
+ export interface GitHubAction {
3
+ /** Type of the GitHub Action. */
4
+ type: 'composite' | 'external' | 'docker' | 'local'
5
+
6
+ /** Version or tag of the action (e.g., 'v1', 'main', commit SHA). */
7
+ version?: string | null
8
+
9
+ /** Line number where the action is used in the file. */
10
+ line?: number
11
+
12
+ /** Path to the file where this action is used. */
13
+ file?: string
14
+
15
+ /** Original `uses` string from workflow, if available. */
16
+ uses?: string
17
+
18
+ /** Full name of the action (e.g., 'actions/checkout'). */
19
+ name: string
20
+
21
+ /** Original `ref` string from workflow, if available. */
22
+ ref?: string
23
+ }
@@ -0,0 +1,12 @@
1
+ import { GitHubAction } from './github-action';
2
+ /** Result of scanning a repository for GitHub Actions usage. */
3
+ export interface ScanResult {
4
+ /** Map of workflow files to their used GitHub Actions. */
5
+ workflows: Map<string, GitHubAction[]>
6
+
7
+ /** Map of composite action names to their file paths. */
8
+ compositeActions: Map<string, string>
9
+
10
+ /** List of all unique GitHub Actions found in the repository. */
11
+ actions: GitHubAction[]
12
+ }
@@ -0,0 +1,18 @@
1
+ import { WorkflowStep } from './workflow-step';
2
+ /** Represents a job in a GitHub Actions workflow. */
3
+ export interface WorkflowJob {
4
+ /** Runner environment(s) to execute this job on (e.g., 'ubuntu-latest'). */
5
+ 'runs-on'?: string[] | string
6
+
7
+ /** Job IDs that must complete successfully before this job runs. */
8
+ needs?: string[] | string
9
+
10
+ /** Array of steps to execute in this job. */
11
+ steps?: WorkflowStep[]
12
+
13
+ /** Allow additional properties for job configuration. */
14
+ [key: string]: unknown
15
+
16
+ /** Conditional expression to determine if the job should run. */
17
+ if?: string
18
+ }
@@ -0,0 +1,20 @@
1
+ /** Represents a single step in a GitHub Actions workflow job. */
2
+ export interface WorkflowStep {
3
+ /** Input parameters to pass to the action. */
4
+ with?: Record<string, unknown>
5
+
6
+ /** Environment variables to set for this step. */
7
+ env?: Record<string, unknown>
8
+
9
+ /** Allow additional properties for step configuration. */
10
+ [key: string]: unknown
11
+
12
+ /** Action to use for this step (e.g., 'actions/checkout@v4'). */
13
+ uses?: string
14
+
15
+ /** Display name for this step. */
16
+ name?: string
17
+
18
+ /** Shell command to run for this step. */
19
+ run?: string
20
+ }
@@ -0,0 +1,15 @@
1
+ import { WorkflowJob } from './workflow-job';
2
+ /** Represents the root structure of a GitHub Actions workflow file. */
3
+ export interface WorkflowStructure {
4
+ /** Map of job IDs to job configurations. */
5
+ jobs?: Record<string, WorkflowJob>
6
+
7
+ /** Allow additional properties for workflow configuration. */
8
+ [key: string]: unknown
9
+
10
+ /** Display name for the workflow. */
11
+ name?: string
12
+
13
+ /** Events that trigger the workflow (push, pull_request, etc.). */
14
+ on?: unknown
15
+ }
package/license.md ADDED
@@ -0,0 +1,20 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright 2025 Azat S. <to@azat.io>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/package.json CHANGED
@@ -1,4 +1,55 @@
1
1
  {
2
2
  "name": "actions-up",
3
- "version": "0.0.1"
3
+ "version": "1.0.0",
4
+ "description": "Interactive CLI tool to update GitHub Actions to latest versions with SHA pinning",
5
+ "keywords": [
6
+ "github-actions",
7
+ "actions",
8
+ "updater",
9
+ "cli",
10
+ "workflow",
11
+ "ci-cd",
12
+ "security",
13
+ "sha",
14
+ "dependencies",
15
+ "update"
16
+ ],
17
+ "homepage": "https://github.com/azat-io/actions-up",
18
+ "repository": "azat-io/actions-up",
19
+ "license": "MIT",
20
+ "author": "Azat S. <to@azat.io>",
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/core/index.d.ts",
25
+ "default": "./dist/core/index.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "main": "./dist/core/index.js",
30
+ "types": "./dist/core/index.d.ts",
31
+ "bin": {
32
+ "actions-up": "./bin/actions-up.js"
33
+ },
34
+ "files": [
35
+ "./bin",
36
+ "./dist"
37
+ ],
38
+ "dependencies": {
39
+ "@octokit/graphql": "^9.0.1",
40
+ "cac": "^6.7.14",
41
+ "enquirer": "^2.4.1",
42
+ "nanospinner": "^1.2.2",
43
+ "picocolors": "^1.1.1",
44
+ "semver": "^7.7.2",
45
+ "yaml": "^2.8.1"
46
+ },
47
+ "engines": {
48
+ "node": "^18.0.0 || >=20.0.0"
49
+ },
50
+ "pnpm": {
51
+ "overrides": {
52
+ "vite": "npm:rolldown-vite@latest"
53
+ }
54
+ }
4
55
  }
package/readme.md ADDED
@@ -0,0 +1,175 @@
1
+ # Actions Up!
2
+
3
+ <img
4
+ src="https://raw.githubusercontent.com/azat-io/actions-up/main/assets/logo.svg"
5
+ alt="Actions Up! logo"
6
+ width="160"
7
+ height="160"
8
+ align="right"
9
+ />
10
+
11
+ [![Version](https://img.shields.io/npm/v/actions-up.svg?color=fff&labelColor=4493f8)](https://npmjs.com/package/actions-up)
12
+ [![Code Coverage](https://img.shields.io/codecov/c/github/azat-io/actions-up.svg?color=fff&labelColor=4493f8)](https://codecov.io/gh/azat-io/actions-up)
13
+ [![GitHub License](https://img.shields.io/badge/license-MIT-232428.svg?color=fff&labelColor=4493f8)](https://github.com/azat-io/actions-up/blob/main/license.md)
14
+
15
+ Actions Up scans your workflows and composite actions to discover every referenced GitHub Action, then checks for newer releases.
16
+
17
+ Interactively upgrade and pin actions to exact commit SHAs for secure, reproducible CI and low‑friction maintenance.
18
+
19
+ ## Features
20
+
21
+ - **Auto-discovery**: Scans all workflows (`.github/workflows/*.yml`) and composite actions (`.github/actions/*/action.yml`)
22
+ - **SHA Pinning**: Updates actions to use commit SHA instead of tags for better security
23
+ - **Batch Updates**: Update multiple actions at once
24
+ - **Interactive Selection**: Choose which actions to update
25
+ - **Breaking Changes Detection**: Warns about major version updates
26
+ - **Fast & Efficient**: Parallel processing with optimized API calls
27
+
28
+ ###
29
+
30
+ <br>
31
+
32
+ <picture>
33
+ <source
34
+ srcset="https://raw.githubusercontent.com/azat-io/actions-up/main/assets/example-light.webp"
35
+ media="(prefers-color-scheme: light)"
36
+ />
37
+ <source
38
+ srcset="https://raw.githubusercontent.com/azat-io/actions-up/main/assets/example-dark.webp"
39
+ media="(prefers-color-scheme: dark)"
40
+ />
41
+ <img
42
+ src="https://raw.githubusercontent.com/azat-io/actions-up/main/assets/example-light.webp"
43
+ alt="Actions Up interactive example"
44
+ width="820"
45
+ />
46
+ </picture>
47
+
48
+ ## Why
49
+
50
+ ### The Problem
51
+
52
+ Keeping GitHub Actions updated is a critical but tedious task:
53
+
54
+ - **Security Risk**: Using outdated actions with known vulnerabilities
55
+ - **Manual Hell**: Checking dozens of actions across multiple workflows by hand
56
+ - **Version Tags Are Mutable**: v1 or v2 tags can change without notice, breaking reproducibility
57
+ - **Time Sink**: Hours spent on maintenance that could be used for actual development
58
+
59
+ ### The Solution
60
+
61
+ Actions Up transforms a painful manual process into a delightful experience:
62
+
63
+ | Without Actions Up | With Actions Up |
64
+ | :----------------------------- | :------------------------------- |
65
+ | Check each action manually | Scan all workflows in seconds |
66
+ | Risk using vulnerable versions | SHA pinning for maximum security |
67
+ | 30+ minutes per repository | Under 1 minute total |
68
+
69
+ ## GitHub Token Required
70
+
71
+ > **Important**: GitHub API has strict rate limits (60 requests/hour without token vs 5000 with token).
72
+ > A GitHub token is **practically required** for using Actions Up.
73
+
74
+ ### Quick Token Setup
75
+
76
+ [Create a GitHub Personal Access Token](https://github.com/settings/tokens/new?scopes=public_repo&description=actions-up).
77
+
78
+ - For public repositories: Select `public_repo` scope
79
+ - For private repositories: Select `repo` scope
80
+
81
+ ## Installation
82
+
83
+ Quick use (no installation)
84
+
85
+ ```bash
86
+ npx actions-up
87
+ ```
88
+
89
+ Global installation
90
+
91
+ ```bash
92
+ npm install -g actions-up
93
+ ```
94
+
95
+ Per-project
96
+
97
+ ```bash
98
+ npm install --save-dev actions-up
99
+ ```
100
+
101
+ ## Usage
102
+
103
+ ### Interactive Mode (Default)
104
+
105
+ Run in your repository root with GitHub token:
106
+
107
+ ```bash
108
+ GITHUB_TOKEN=ghp_xxxx npx actions-up
109
+ ```
110
+
111
+ This will:
112
+
113
+ 1. Scan all `.github/workflows/*.yml` and `.github/actions/*/action.yml` files
114
+ 2. Check for available updates
115
+ 3. Show an interactive list to select updates
116
+ 4. Apply selected updates with SHA pinning
117
+
118
+ ### Auto-Update Mode
119
+
120
+ Skip all prompts and update everything:
121
+
122
+ ```bash
123
+ GITHUB_TOKEN=ghp_xxxx npx actions-up --yes
124
+ # or
125
+ GITHUB_TOKEN=ghp_xxxx npx actions-up -y
126
+ ```
127
+
128
+ ## Pro Tips
129
+
130
+ ### Shell Aliases
131
+
132
+ Add to your `.zshrc`, `.bashrc` or `.config/fish/config.fish`:
133
+
134
+ ```bash
135
+ # Basic alias with token from environment
136
+ export GITHUB_TOKEN=ghp_xxxx # Add this once to your shell config
137
+ alias actions-up='GITHUB_TOKEN=$GITHUB_TOKEN npx actions-up'
138
+
139
+ # With token from file
140
+ alias actions-up='GITHUB_TOKEN=$(cat ~/.github-token) npx actions-up'
141
+
142
+ # With 1Password CLI
143
+ alias actions-up='GITHUB_TOKEN=$(op read "op://Personal/GitHub/token") npx actions-up'
144
+
145
+ # With macOS Keychain
146
+ alias actions-up='GITHUB_TOKEN=$(security find-generic-password -w -s "github-token") npx actions-up'
147
+ ```
148
+
149
+ ## Example
150
+
151
+ ```yaml
152
+ # Before
153
+ - uses: actions/checkout@v3
154
+ - uses: actions/setup-node@v3
155
+
156
+ # After running actions-up
157
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
158
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
159
+ ```
160
+
161
+ ## Security
162
+
163
+ Actions Up promotes security best practices:
164
+
165
+ - **SHA Pinning**: Uses commit SHA instead of mutable tags
166
+ - **Version Comments**: Adds version as comment for readability
167
+ - **No Auto-Updates**: Full control over what gets updated
168
+
169
+ ## Contributing
170
+
171
+ See [Contributing Guide](https://github.com/azat-io/actions-up/blob/main/contributing.md).
172
+
173
+ ## License
174
+
175
+ MIT &copy; [Azat S.](https://azat.io)