Skip to content

Commit

Permalink
feat: allow syncing of environment secrets (#55)
Browse files Browse the repository at this point in the history
* Allow syncing secrets into repository environment

This commit allows one to set an input called "environment", which when
enabled will sync the secrets as environment secrets to the same
repositories instead of putting the secrets as repository secrets.

The action will fail if the targeted repositories does not have the
specified environment created prior to running the action.

* Update README.md with environment secrets
  • Loading branch information
Natanande authored Dec 16, 2021
1 parent 072a97d commit 1aba261
Show file tree
Hide file tree
Showing 13 changed files with 4,435 additions and 6,108 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Run everything except for secret create and update functionality.

When set to `true`, the action will find and delete the selected secrets from repositories. Defaults to `false`.

### `environment`

If this value is set to the name of a valid environment in the target repositories, the action will not set repository secrets but instead only set environment secrets for the specified environment. When not set, will set repository secrets only.

## Usage

```yaml
Expand Down
28 changes: 22 additions & 6 deletions __tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { getConfig } from "../src/config";

function clearInputs() {
Object.keys(process.env)
.filter(k => k.match(/INPUT_.*/))
.forEach(k => {
.filter((k) => k.match(/INPUT_.*/))
.forEach((k) => {
process.env[k] = "";
});
}
Expand All @@ -35,8 +35,22 @@ describe("getConfig", () => {
const RETRIES = 3;
const CONCURRENCY = 50;
const RUN_DELETE = false;
const ENVIRONMENT = "production";

const inputs = {
// Must implement because operands for delete must be optional in typescript >= 4.0
interface Inputs {
INPUT_GITHUB_API_URL?: string;
INPUT_GITHUB_TOKEN: string;
INPUT_SECRETS: string;
INPUT_REPOSITORIES: string;
INPUT_REPOSITORIES_LIST_REGEX: string;
INPUT_DRY_RUN: string;
INPUT_RETRIES: string;
INPUT_CONCURRENCY: string;
INPUT_RUN_DELETE: string;
INPUT_ENVIRONMENT: string;
}
const inputs: Inputs = {
INPUT_GITHUB_API_URL: String(GITHUB_API_URL),
INPUT_GITHUB_TOKEN: GITHUB_TOKEN,
INPUT_SECRETS: SECRETS.join("\n"),
Expand All @@ -45,7 +59,8 @@ describe("getConfig", () => {
INPUT_DRY_RUN: String(DRY_RUN),
INPUT_RETRIES: String(RETRIES),
INPUT_CONCURRENCY: String(CONCURRENCY),
INPUT_RUN_DELETE: String(RUN_DELETE)
INPUT_RUN_DELETE: String(RUN_DELETE),
INPUT_ENVIRONMENT: String(ENVIRONMENT),
};

beforeEach(() => {
Expand All @@ -72,7 +87,8 @@ describe("getConfig", () => {
DRY_RUN,
RETRIES,
CONCURRENCY,
RUN_DELETE
RUN_DELETE,
ENVIRONMENT,
});
});

Expand Down Expand Up @@ -104,7 +120,7 @@ describe("getConfig", () => {
["False", false],
["FALSE", false],
["foo", false],
["", false]
["", false],
];

for (const [value, expected] of cases) {
Expand Down
178 changes: 164 additions & 14 deletions __tests__/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import {
DefaultOctokit,
filterReposByPatterns,
listAllMatchingRepos,
getRepos,
publicKeyCache,
setSecretForRepo,
deleteSecretForRepo
deleteSecretForRepo,
} from "../src/github";

// @ts-ignore-next-line
Expand All @@ -39,11 +40,11 @@ beforeAll(() => {
REPOSITORIES: [".*"],
REPOSITORIES_LIST_REGEX: true,
DRY_RUN: false,
RETRIES: 3
RETRIES: 3,
});

octokit = DefaultOctokit({
auth: ""
auth: "",
});
});

Expand All @@ -59,7 +60,7 @@ describe("listing repos from github", () => {
.reply(200, [
fixture[0].response,
fixture[0].response,
{ archived: true, full_name: "foo/bar" }
{ archived: true, full_name: "foo/bar" },
]);

nock("https://api.github.com")
Expand All @@ -71,7 +72,7 @@ describe("listing repos from github", () => {
const repos = await listAllMatchingRepos({
patterns: [".*"],
octokit,
pageSize
pageSize,
});

expect(repos.length).toEqual(3);
Expand All @@ -81,7 +82,33 @@ describe("listing repos from github", () => {
const repos = await listAllMatchingRepos({
patterns: ["octokit.*"],
octokit,
pageSize
pageSize,
});

expect(repos.length).toEqual(3);
});
});

describe("getting single repos from github", () => {
nock.cleanAll();

const repo = fixture[0].response;

beforeEach(() => {
nock("https://api.github.com")
.persist()
.get(`/repos/${repo.full_name}`)
.reply(fixture[0].status, fixture[0].response);
});

test("getRepos returns from multiple pages", async () => {
const repos = await getRepos({
patterns: [
fixture[0].response.full_name,
fixture[0].response.full_name,
fixture[0].response.full_name,
],
octokit,
});

expect(repos.length).toEqual(3);
Expand All @@ -97,13 +124,15 @@ describe("setSecretForRepo", () => {
const repo = fixture[0].response;
const publicKey = {
key_id: "1234",
key: "HRkzRZD1+duhfvNvY8eiCPb+ihIjbvkvRyiehJCs8Vc="
key: "HRkzRZD1+duhfvNvY8eiCPb+ihIjbvkvRyiehJCs8Vc=",
};

jest.setTimeout(30000);

const secrets = { FOO: "BAR" };

const repoEnvironment = "production";

let publicKeyMock: nock.Scope;
let setSecretMock: nock.Scope;

Expand All @@ -113,8 +142,9 @@ describe("setSecretForRepo", () => {
publicKeyMock = nock("https://api.github.com")
.get(`/repos/${repo.full_name}/actions/secrets/public-key`)
.reply(200, publicKey);

setSecretMock = nock("https://api.github.com")
.put(`/repos/${repo.full_name}/actions/secrets/FOO`, body => {
.put(`/repos/${repo.full_name}/actions/secrets/FOO`, (body) => {
expect(body.encrypted_value).toBeTruthy();
expect(body.key_id).toEqual(publicKey.key_id);
return body;
Expand All @@ -123,18 +153,94 @@ describe("setSecretForRepo", () => {
});

test("setSecretForRepo should retrieve public key", async () => {
await setSecretForRepo(octokit, "FOO", secrets.FOO, repo, true);
await setSecretForRepo(octokit, "FOO", secrets.FOO, repo, "", true);
expect(publicKeyMock.isDone()).toBeTruthy();
});

test("setSecretForRepo should not set secret with dry run", async () => {
await setSecretForRepo(octokit, "FOO", secrets.FOO, repo, true);
await setSecretForRepo(octokit, "FOO", secrets.FOO, repo, "", true);
expect(publicKeyMock.isDone()).toBeTruthy();
expect(setSecretMock.isDone()).toBeFalsy();
});

test("setSecretForRepo should should call set secret endpoint", async () => {
await setSecretForRepo(octokit, "FOO", secrets.FOO, repo, false);
test("setSecretForRepo should call set secret endpoint", async () => {
await setSecretForRepo(octokit, "FOO", secrets.FOO, repo, "", false);
expect(nock.isDone()).toBeTruthy();
});
});

describe("setSecretForRepo with environment", () => {
const repo = fixture[0].response;
const publicKey = {
key_id: "1234",
key: "HRkzRZD1+duhfvNvY8eiCPb+ihIjbvkvRyiehJCs8Vc=",
};

jest.setTimeout(30000);

const secrets = { FOO: "BAR" };

const repoEnvironment = "production";

let environmentPublicKeyMock: nock.Scope;
let setEnvironmentSecretMock: nock.Scope;

beforeEach(() => {
nock.cleanAll();
publicKeyCache.clear();

environmentPublicKeyMock = nock("https://api.github.com")
.get(
`/repositories/${repo.id}/environments/${repoEnvironment}/secrets/public-key`
)
.reply(200, publicKey);

setEnvironmentSecretMock = nock("https://api.github.com")
.put(
`/repositories/${repo.id}/environments/${repoEnvironment}/secrets/FOO`,
(body) => {
expect(body.encrypted_value).toBeTruthy();
expect(body.key_id).toEqual(publicKey.key_id);
return body;
}
)
.reply(200);
});

test("setSecretForRepo should retrieve public key", async () => {
await setSecretForRepo(
octokit,
"FOO",
secrets.FOO,
repo,
repoEnvironment,
true
);
expect(environmentPublicKeyMock.isDone()).toBeTruthy();
});

test("setSecretForRepo should not set secret with dry run", async () => {
await setSecretForRepo(
octokit,
"FOO",
secrets.FOO,
repo,
repoEnvironment,
true
);
expect(environmentPublicKeyMock.isDone()).toBeTruthy();
expect(setEnvironmentSecretMock.isDone()).toBeFalsy();
});

test("setSecretForRepo should call set secret endpoint", async () => {
await setSecretForRepo(
octokit,
"FOO",
secrets.FOO,
repo,
repoEnvironment,
false
);
expect(nock.isDone()).toBeTruthy();
});
});
Expand All @@ -155,12 +261,56 @@ describe("deleteSecretForRepo", () => {
});

test("deleteSecretForRepo should not delete secret with dry run", async () => {
await deleteSecretForRepo(octokit, "FOO", secrets.FOO, repo, true);
await deleteSecretForRepo(octokit, "FOO", secrets.FOO, repo, "", true);
expect(deleteSecretMock.isDone()).toBeFalsy();
});

test("deleteSecretForRepo should call set secret endpoint", async () => {
await deleteSecretForRepo(octokit, "FOO", secrets.FOO, repo, false);
await deleteSecretForRepo(octokit, "FOO", secrets.FOO, repo, "", false);
expect(nock.isDone()).toBeTruthy();
});
});

describe("deleteSecretForRepo with environment", () => {
const repo = fixture[0].response;

const repoEnvironment = "production";

jest.setTimeout(30000);

const secrets = { FOO: "BAR" };
let deleteSecretMock: nock.Scope;

beforeEach(() => {
nock.cleanAll();
deleteSecretMock = nock("https://api.github.com")
.delete(
`/repositories/${repo.id}/environments/${repoEnvironment}/secrets/FOO`
)
.reply(200);
});

test("deleteSecretForRepo should not delete secret with dry run", async () => {
await deleteSecretForRepo(
octokit,
"FOO",
secrets.FOO,
repo,
repoEnvironment,
true
);
expect(deleteSecretMock.isDone()).toBeFalsy();
});

test("deleteSecretForRepo should call set secret endpoint", async () => {
await deleteSecretForRepo(
octokit,
"FOO",
secrets.FOO,
repo,
repoEnvironment,
false
);
expect(nock.isDone()).toBeTruthy();
});
});
Loading

0 comments on commit 1aba261

Please sign in to comment.