Skip to content

Commit 718ef76

Browse files
committed
Add org invite and actions artifact cleanup scripts
Taken from https://github.com/pierluigi/gha-cleanup and https://github.com/pierluigi/org-invite
1 parent fd0d095 commit 718ef76

File tree

12 files changed

+2038
-0
lines changed

12 files changed

+2038
-0
lines changed

api/javascript/gha-cleanup/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
yarn.lock
3+
.env

api/javascript/gha-cleanup/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# gha-cleanup - Clean up GitHub Actions artifacts
2+
3+
List and delete artifacts created by GitHub Actions in your repository.
4+
Requires a Personal Access Token with full repo permissions.
5+
6+
![Screenshot](screenshot.png?raw=true "Script in action")
7+
8+
# Instructions
9+
10+
```
11+
yarn install
12+
npm link // Optional step. Call ./cli.js instead
13+
14+
// Options can be supplied interactively or via flags
15+
16+
$ gha-cleanup --help
17+
Usage: gha-cleanup [options]
18+
19+
Options:
20+
-t, --token <PAT> Your GitHub PAT
21+
-u, --user <username> Your GitHub username
22+
-r, --repo <repository> Repository name
23+
-h, --help output usage information
24+
25+
```
26+
27+
# Configuration
28+
29+
You can pass the PAT and username directly from the prompt. To avoid repeating yourself all the time, create a .env file in the root (don't worry, it will be ignored by git) and set:
30+
31+
```
32+
$GH_PAT=<Your-GitHub-Personal-Access-Token>
33+
$GH_USER=<Your-GitHub-Username>
34+
```
35+
36+
Then you can simply invoke `gha-cleanup` and confirm the prefilled values.
37+
38+
39+

api/javascript/gha-cleanup/cli.js

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#!/usr/bin/env node
2+
3+
const program = require("commander");
4+
const prettyBytes = require("pretty-bytes");
5+
const chalk = require("chalk");
6+
const _ = require("lodash");
7+
const moment = require("moment");
8+
var inquirer = require("inquirer");
9+
const Octokit = require("@octokit/rest");
10+
11+
const dotenv = require("dotenv");
12+
13+
dotenv.config();
14+
15+
program.option(
16+
"-t, --token <PAT>",
17+
"Your GitHub PAT (leave blank for prompt or set $GH_PAT)"
18+
);
19+
program.option(
20+
"-u, --user <username>",
21+
"Your GitHub username (leave blank for prompt or set $GH_USER)"
22+
);
23+
program.option("-r, --repo <repository>", "Repository name");
24+
25+
program.parse(process.argv);
26+
const showArtifacts = async ({ owner, repo, PAT }) => {
27+
var loader = ["/ Loading", "| Loading", "\\ Loading", "- Loading"];
28+
var i = 4;
29+
var ui = new inquirer.ui.BottomBar({ bottomBar: loader[i % 4] });
30+
31+
const loadingInterval = setInterval(() => {
32+
ui.updateBottomBar(loader[i++ % 4]);
33+
}, 200);
34+
35+
const octokit = new Octokit({
36+
auth: PAT
37+
});
38+
39+
const prefs = { owner, repo };
40+
ui.log.write(`${chalk.dim("[1/3]")} 🔍 Getting list of workflows...`);
41+
42+
const {
43+
data: { workflows }
44+
} = await octokit.actions.listRepoWorkflows({ ...prefs });
45+
46+
let everything = {};
47+
48+
ui.log.write(`${chalk.dim("[2/3]")} 🏃‍♀️ Getting list of workflow runs...`);
49+
50+
let runs = await workflows.reduce(async (promisedRuns, w) => {
51+
const memo = await promisedRuns;
52+
53+
const {
54+
data: { workflow_runs }
55+
} = await octokit.actions.listWorkflowRuns({ ...prefs, workflow_id: w.id });
56+
57+
everything[w.id] = {
58+
name: w.name,
59+
id: w.id,
60+
updated_at: w.updated_at,
61+
state: w.updated_at,
62+
runs: workflow_runs.reduce(
63+
(r, { id, run_number, status, conclusion, html_url }) => {
64+
return {
65+
...r,
66+
[id]: {
67+
id,
68+
workflow_id: w.id,
69+
run_number,
70+
status,
71+
conclusion,
72+
html_url,
73+
artifacts: []
74+
}
75+
};
76+
},
77+
{}
78+
)
79+
};
80+
81+
if (!workflow_runs.length) return memo;
82+
return [...memo, ...workflow_runs];
83+
}, []);
84+
85+
ui.log.write(
86+
`${chalk.dim(
87+
"[3/3]"
88+
)} 📦 Getting list of artifacts for each run... (this may take a while)`
89+
);
90+
91+
let all_artifacts = await runs.reduce(async (promisedArtifact, r) => {
92+
const memo = await promisedArtifact;
93+
94+
const {
95+
data: { artifacts }
96+
} = await octokit.actions.listWorkflowRunArtifacts({
97+
...prefs,
98+
run_id: r.id
99+
});
100+
101+
if (!artifacts.length) return memo;
102+
103+
const run_wf = _.find(everything, wf => wf.runs[r.id] != undefined);
104+
if (run_wf && everything[run_wf.id]) {
105+
everything[run_wf.id].runs[r.id].artifacts = artifacts;
106+
}
107+
108+
return [...memo, ...artifacts];
109+
}, []);
110+
111+
let output = [];
112+
_.each(everything, wf => {
113+
_.each(wf.runs, ({ run_number, artifacts }) => {
114+
_.each(artifacts, ({ id, name, size_in_bytes, created_at }) => {
115+
output.push({
116+
name,
117+
artifact_id: id,
118+
size: prettyBytes(size_in_bytes),
119+
size_in_bytes,
120+
created: moment(created_at).format("dddd, MMMM Do YYYY, h:mm:ss a"),
121+
created_at,
122+
run_number,
123+
workflow: wf.name
124+
});
125+
});
126+
});
127+
});
128+
129+
const out = _.orderBy(output, ["size_in_bytes"], ["desc"]);
130+
clearInterval(loadingInterval);
131+
132+
inquirer
133+
.prompt([
134+
{
135+
type: "checkbox",
136+
name: "artifact_ids",
137+
message: "Select the artifacts you want to delete",
138+
choices: output.map((row, k) => ({
139+
name: `${row.workflow} - ${row.name}, ${row.size} (${row.created}, ID: ${row.artifact_id}, Run #: ${row.run_number})`,
140+
value: row.artifact_id
141+
}))
142+
}
143+
])
144+
.then(answers => {
145+
if (answers.artifact_ids.length == 0) {
146+
process.exit();
147+
}
148+
149+
inquirer
150+
.prompt([
151+
{
152+
type: "confirm",
153+
name: "delete",
154+
message: `You are about to delete ${answers.artifact_ids.length} artifacts permanently. Are you sure?`
155+
}
156+
])
157+
.then(confirm => {
158+
if (!confirm.delete) process.exit();
159+
160+
answers.artifact_ids.map(aid => {
161+
octokit.actions
162+
.deleteArtifact({ ...prefs, artifact_id: aid })
163+
.then(r => {
164+
console.log(
165+
r.status === 204
166+
? `${chalk.green("[OK]")} Artifact with ID ${chalk.dim(
167+
aid
168+
)} deleted`
169+
: `${chalk.red("[ERR]")} Artifact with ID ${chalk.dim(
170+
aid
171+
)} could not be deleted.`
172+
);
173+
})
174+
.catch(e => {
175+
console.error(e.status, e.message);
176+
});
177+
});
178+
});
179+
});
180+
};
181+
182+
inquirer
183+
.prompt([
184+
{
185+
type: "password",
186+
name: "PAT",
187+
message: "What's your GitHub PAT?",
188+
default: function() {
189+
return program.token || process.env.GH_PAT;
190+
}
191+
},
192+
{
193+
type: "input",
194+
name: "owner",
195+
message: "Your username?",
196+
default: function() {
197+
return program.user || process.env.GH_USER;
198+
}
199+
},
200+
{
201+
type: "input",
202+
name: "repo",
203+
message: "Which repository?",
204+
default: function() {
205+
return program.repo;
206+
}
207+
}
208+
])
209+
.then(answers => {
210+
showArtifacts({ ...answers });
211+
});

0 commit comments

Comments
 (0)