Skip to content

Commit 178c0ff

Browse files
authored
feat: add secret deletion (#47)
1 parent dd05465 commit 178c0ff

10 files changed

Lines changed: 117 additions & 12 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ The number of allowed concurrent calls to the set secret endpoint. Lower this nu
3838

3939
Run everything except for secret create and update functionality.
4040

41+
### `delete`
42+
43+
When set to `true`, the action will find and delete the selected secrets from repositories. Defaults to `false`.
44+
4145
## Usage
4246

4347
```yaml

__tests__/config.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe("getConfig", () => {
3232
const DRY_RUN = false;
3333
const RETRIES = 3;
3434
const CONCURRENCY = 50;
35+
const RUN_DELETE = false;
3536

3637
const inputs = {
3738
INPUT_GITHUB_TOKEN: GITHUB_TOKEN,
@@ -40,7 +41,8 @@ describe("getConfig", () => {
4041
INPUT_REPOSITORIES_LIST_REGEX: String(REPOSITORIES_LIST_REGEX),
4142
INPUT_DRY_RUN: String(DRY_RUN),
4243
INPUT_RETRIES: String(RETRIES),
43-
INPUT_CONCURRENCY: String(CONCURRENCY)
44+
INPUT_CONCURRENCY: String(CONCURRENCY),
45+
INPUT_RUN_DELETE: String(RUN_DELETE)
4446
};
4547

4648
beforeEach(() => {
@@ -65,7 +67,8 @@ describe("getConfig", () => {
6567
REPOSITORIES_LIST_REGEX,
6668
DRY_RUN,
6769
RETRIES,
68-
CONCURRENCY
70+
CONCURRENCY,
71+
RUN_DELETE
6972
});
7073
});
7174

@@ -85,7 +88,7 @@ describe("getConfig", () => {
8588
["", false]
8689
];
8790

88-
for (let [value, expected] of cases) {
91+
for (const [value, expected] of cases) {
8992
process.env["INPUT_DRY_RUN"] = value;
9093
const actual = getConfig().DRY_RUN;
9194
expect(`${value}=${actual}`).toEqual(`${value}=${expected}`);

__tests__/github.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
filterReposByPatterns,
2222
listAllMatchingRepos,
2323
publicKeyCache,
24-
setSecretForRepo
24+
setSecretForRepo,
25+
deleteSecretForRepo
2526
} from "../src/github";
2627

2728
// @ts-ignore-next-line
@@ -137,3 +138,29 @@ describe("setSecretForRepo", () => {
137138
expect(nock.isDone()).toBeTruthy();
138139
});
139140
});
141+
142+
describe("deleteSecretForRepo", () => {
143+
const repo = fixture[0].response;
144+
145+
jest.setTimeout(30000);
146+
147+
const secrets = { FOO: "BAR" };
148+
let deleteSecretMock: nock.Scope;
149+
150+
beforeEach(() => {
151+
nock.cleanAll();
152+
deleteSecretMock = nock("https://api.github.com")
153+
.delete(`/repos/${repo.full_name}/actions/secrets/FOO`)
154+
.reply(200);
155+
});
156+
157+
test("deleteSecretForRepo should not delete secret with dry run", async () => {
158+
await deleteSecretForRepo(octokit, "FOO", secrets.FOO, repo, true);
159+
expect(deleteSecretMock.isDone()).toBeFalsy();
160+
});
161+
162+
test("deleteSecretForRepo should call set secret endpoint", async () => {
163+
await deleteSecretForRepo(octokit, "FOO", secrets.FOO, repo, false);
164+
expect(nock.isDone()).toBeTruthy();
165+
});
166+
});

__tests__/main.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,23 @@ test("run should succeed with a repo and secret with repository_list_regex as fa
8484

8585
expect(process.exitCode).toBe(undefined);
8686
});
87+
88+
test("run should succeed with delete enabled, a repo and secret with repository_list_regex as false", async () => {
89+
(github.deleteSecretForRepo as jest.Mock) = jest
90+
.fn()
91+
.mockImplementation(async () => null);
92+
93+
(config.getConfig as jest.Mock) = jest.fn().mockReturnValue({
94+
GITHUB_TOKEN: "token",
95+
SECRETS: ["BAZ"],
96+
REPOSITORIES: [fixture[0].response.full_name],
97+
REPOSITORIES_LIST_REGEX: false,
98+
DRY_RUN: false,
99+
RUN_DELETE: true,
100+
CONCURRENCY: 1
101+
});
102+
await run();
103+
104+
expect(github.deleteSecretForRepo as jest.Mock).toBeCalledTimes(1);
105+
expect(process.exitCode).toBe(undefined);
106+
});

__tests__/secrets.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import * as core from "@actions/core";
1818

1919
import { getSecrets } from "../src/secrets";
2020

21-
let setSecretMock: jest.Mock = jest.fn();
21+
const setSecretMock: jest.Mock = jest.fn();
2222

2323
beforeAll(() => {
2424
// @ts-ignore-next-line

action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ inputs:
4545
number to avoid abuse limits.
4646
default: "10"
4747
required: false
48+
delete:
49+
description: |
50+
When set to `true`, the action will find and delete the selected secrets from repositories. Defaults to `false`.
51+
default: false
52+
required: false
4853
runs:
4954
using: 'node12'
5055
main: 'dist/index.js'

dist/index.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2300,7 +2300,10 @@ function run() {
23002300
const calls = [];
23012301
for (const repo of repos) {
23022302
for (const k of Object.keys(secrets)) {
2303-
calls.push(limit(() => github_1.setSecretForRepo(octokit, k, secrets[k], repo, config.DRY_RUN)));
2303+
const action = config.RUN_DELETE
2304+
? github_1.deleteSecretForRepo
2305+
: github_1.setSecretForRepo;
2306+
calls.push(limit(() => action(octokit, k, secrets[k], repo, config.DRY_RUN)));
23042307
}
23052308
}
23062309
yield Promise.all(calls);
@@ -5728,7 +5731,8 @@ function getConfig() {
57285731
REPOSITORIES_LIST_REGEX: ["1", "true"].includes(core
57295732
.getInput("REPOSITORIES_LIST_REGEX", { required: false })
57305733
.toLowerCase()),
5731-
DRY_RUN: ["1", "true"].includes(core.getInput("DRY_RUN", { required: false }).toLowerCase())
5734+
DRY_RUN: ["1", "true"].includes(core.getInput("DRY_RUN", { required: false }).toLowerCase()),
5735+
RUN_DELETE: ["1", "true"].includes(core.getInput("DELETE", { required: false }).toLowerCase())
57325736
};
57335737
if (config.DRY_RUN) {
57345738
core.info("[DRY_RUN='true'] No changes will be written to secrets");
@@ -7744,6 +7748,22 @@ function setSecretForRepo(octokit, name, secret, repo, dry_run) {
77447748
});
77457749
}
77467750
exports.setSecretForRepo = setSecretForRepo;
7751+
function deleteSecretForRepo(octokit, name, secret, repo, dry_run) {
7752+
return __awaiter(this, void 0, void 0, function* () {
7753+
core.info(`Remove ${name} from ${repo.full_name}`);
7754+
try {
7755+
if (!dry_run) {
7756+
const action = "DELETE";
7757+
const request = `/repos/${repo.full_name}/actions/secrets/${name}`;
7758+
return octokit.request(`${action} ${request}`);
7759+
}
7760+
}
7761+
catch (HttpError) {
7762+
//If secret is not found in target repo, silently continue
7763+
}
7764+
});
7765+
}
7766+
exports.deleteSecretForRepo = deleteSecretForRepo;
77477767

77487768

77497769
/***/ }),

src/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface Config {
2424
DRY_RUN: boolean;
2525
RETRIES: number;
2626
CONCURRENCY: number;
27+
RUN_DELETE: boolean;
2728
}
2829

2930
export function getConfig(): Config {
@@ -40,6 +41,9 @@ export function getConfig(): Config {
4041
),
4142
DRY_RUN: ["1", "true"].includes(
4243
core.getInput("DRY_RUN", { required: false }).toLowerCase()
44+
),
45+
RUN_DELETE: ["1", "true"].includes(
46+
core.getInput("DELETE", { required: false }).toLowerCase()
4347
)
4448
};
4549

src/github.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,23 @@ export async function setSecretForRepo(
184184
});
185185
}
186186
}
187+
188+
export async function deleteSecretForRepo(
189+
octokit: any,
190+
name: string,
191+
secret: string,
192+
repo: Repository,
193+
dry_run: boolean
194+
): Promise<void> {
195+
core.info(`Remove ${name} from ${repo.full_name}`);
196+
197+
try {
198+
if (!dry_run) {
199+
const action = "DELETE";
200+
const request = `/repos/${repo.full_name}/actions/secrets/${name}`;
201+
return octokit.request(`${action} ${request}`);
202+
}
203+
} catch (HttpError) {
204+
//If secret is not found in target repo, silently continue
205+
}
206+
}

src/main.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
DefaultOctokit,
2121
Repository,
2222
listAllMatchingRepos,
23-
setSecretForRepo
23+
setSecretForRepo,
24+
deleteSecretForRepo
2425
} from "./github";
2526

2627
import { getConfig } from "./config";
@@ -84,13 +85,14 @@ export async function run(): Promise<void> {
8485

8586
const limit = pLimit(config.CONCURRENCY);
8687
const calls: Promise<void>[] = [];
87-
8888
for (const repo of repos) {
8989
for (const k of Object.keys(secrets)) {
90+
const action = config.RUN_DELETE
91+
? deleteSecretForRepo
92+
: setSecretForRepo;
93+
9094
calls.push(
91-
limit(() =>
92-
setSecretForRepo(octokit, k, secrets[k], repo, config.DRY_RUN)
93-
)
95+
limit(() => action(octokit, k, secrets[k], repo, config.DRY_RUN))
9496
);
9597
}
9698
}

0 commit comments

Comments
 (0)