Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for prefixing all secrets on copy #10

Merged
merged 2 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "Build"
on:
on:
[push, pull_request]
permissions:
contents: read
Expand All @@ -12,8 +12,14 @@ jobs:
uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
with:
egress-policy: audit

- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: 16
- run: |
npm i
npm run all
- name: Upload coverage reports to Codecov
if: always()
uses: codecov/codecov-action@v3
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,7 @@ Thumbs.db

# Ignore built ts files
__tests__/runner/*
lib/**/*
lib/**/*

# Ignore IntelliJ files
.idea
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ If this value is set to the name of a valid environment in the target repositori

Target where secrets should be stored: `actions` (default), `codespaces` or `dependabot`.

### `new_secret_prefix`

If this value is set, the action will prefix the name of the secret with the provided value. This is useful when you want to use the same secret name in multiple repositories but want to avoid conflicts.

## Usage

```yaml
Expand Down
4 changes: 4 additions & 0 deletions __tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe("getConfig", () => {
const RUN_DELETE = false;
const ENVIRONMENT = "production";
const TARGET = "actions";
const NEW_SECRET_PREFIX = "PREFIX_";

// Must implement because operands for delete must be optional in typescript >= 4.0
interface Inputs {
Expand All @@ -51,6 +52,7 @@ describe("getConfig", () => {
INPUT_RUN_DELETE: string;
INPUT_ENVIRONMENT: string;
INPUT_TARGET: string;
INPUT_NEW_SECRET_PREFIX: string;
}
const inputs: Inputs = {
INPUT_GITHUB_API_URL: String(GITHUB_API_URL),
Expand All @@ -64,6 +66,7 @@ describe("getConfig", () => {
INPUT_RUN_DELETE: String(RUN_DELETE),
INPUT_ENVIRONMENT: String(ENVIRONMENT),
INPUT_TARGET: String(TARGET),
INPUT_NEW_SECRET_PREFIX: String(NEW_SECRET_PREFIX),
};

beforeEach(() => {
Expand Down Expand Up @@ -93,6 +96,7 @@ describe("getConfig", () => {
RUN_DELETE,
ENVIRONMENT,
TARGET,
NEW_SECRET_PREFIX,
});
});

Expand Down
23 changes: 21 additions & 2 deletions __tests__/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import * as config from "../src/config";

import {
DefaultOctokit,
deleteSecretForRepo,
filterReposByPatterns,
listAllMatchingRepos,
getRepos,
listAllMatchingRepos,
publicKeyCache,
setSecretForRepo,
deleteSecretForRepo,
} from "../src/github";

// @ts-ignore-next-line
Expand All @@ -41,6 +41,7 @@ beforeAll(() => {
REPOSITORIES_LIST_REGEX: true,
DRY_RUN: false,
RETRIES: 3,
NEW_SECRET_PREFIX: "",
});

octokit = DefaultOctokit({
Expand Down Expand Up @@ -189,6 +190,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"actions"
);
Expand All @@ -202,6 +204,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"dependabot"
);
Expand All @@ -215,6 +218,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"codespaces"
);
Expand All @@ -228,6 +232,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"actions"
);
Expand All @@ -242,6 +247,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"actions"
);
Expand All @@ -255,6 +261,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"dependabot"
);
Expand All @@ -268,6 +275,7 @@ describe("setSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"codespaces"
);
Expand Down Expand Up @@ -320,6 +328,7 @@ describe("setSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"actions"
);
Expand All @@ -333,6 +342,7 @@ describe("setSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"actions"
);
Expand All @@ -347,6 +357,7 @@ describe("setSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"dependabot"
);
Expand All @@ -361,6 +372,7 @@ describe("setSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
false,
"actions"
);
Expand Down Expand Up @@ -401,6 +413,7 @@ describe("deleteSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
true,
"actions"
);
Expand All @@ -414,6 +427,7 @@ describe("deleteSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"actions"
);
Expand All @@ -427,6 +441,7 @@ describe("deleteSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"dependabot"
);
Expand All @@ -440,6 +455,7 @@ describe("deleteSecretForRepo", () => {
secrets.FOO,
repo,
"",
"",
false,
"codespaces"
);
Expand Down Expand Up @@ -473,6 +489,7 @@ describe("deleteSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"actions"
);
Expand All @@ -486,6 +503,7 @@ describe("deleteSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
true,
"dependabot"
);
Expand All @@ -499,6 +517,7 @@ describe("deleteSecretForRepo with environment", () => {
secrets.FOO,
repo,
repoEnvironment,
"",
false,
"actions"
);
Expand Down
7 changes: 6 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# action.yml
name: "Secrets Sync Action"
branding:
icon: 'copy'
icon: 'copy'
color: 'red'
description: "Copies secrets from the action's environment to many other repos."
inputs:
Expand Down Expand Up @@ -66,6 +66,11 @@ inputs:
Target where secrets should be stored: `actions` (default), `codespaces` or `dependabot`.
default: "actions"
required: false
new_secret_prefix:
default: ""
description: |
If this value is set, the action will prefix the secret name with this value.
required: false
runs:
using: 'node20'
main: 'dist/index.js'
30 changes: 17 additions & 13 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function getConfig() {
RUN_DELETE: ["1", "true"].includes(core.getInput("DELETE", { required: false }).toLowerCase()),
ENVIRONMENT: core.getInput("ENVIRONMENT", { required: false }),
TARGET: core.getInput("TARGET", { required: false }),
NEW_SECRET_PREFIX: core.getInput("NEW_SECRET_PREFIX", { required: false }),
};
if (config.DRY_RUN) {
core.info("[DRY_RUN='true'] No changes will be written to secrets");
Expand Down Expand Up @@ -265,27 +266,28 @@ function getPublicKey(octokit, repo, environment, target) {
});
}
exports.getPublicKey = getPublicKey;
function setSecretForRepo(octokit, name, secret, repo, environment, dry_run, target) {
function setSecretForRepo(octokit, name, secret, repo, environment, new_secret_prefix, dry_run, target) {
return __awaiter(this, void 0, void 0, function* () {
const [repo_owner, repo_name] = repo.full_name.split("/");
const publicKey = yield getPublicKey(octokit, repo, environment, target);
const encrypted_value = (0, utils_1.encrypt)(secret, publicKey.key);
core.info(`Set \`${name} = ***\` on ${repo.full_name}`);
const final_name = new_secret_prefix ? new_secret_prefix + name : name;
core.info(`Set \`${final_name} = ***\` on ${repo.full_name}`);
if (!dry_run) {
switch (target) {
case "codespaces":
return octokit.codespaces.createOrUpdateRepoSecret({
owner: repo_owner,
repo: repo_name,
secret_name: name,
secret_name: final_name,
key_id: publicKey.key_id,
encrypted_value,
});
case "dependabot":
return octokit.dependabot.createOrUpdateRepoSecret({
owner: repo_owner,
repo: repo_name,
secret_name: name,
secret_name: final_name,
key_id: publicKey.key_id,
encrypted_value,
});
Expand All @@ -295,7 +297,7 @@ function setSecretForRepo(octokit, name, secret, repo, environment, dry_run, tar
return octokit.actions.createOrUpdateEnvironmentSecret({
repository_id: repo.id,
environment_name: environment,
secret_name: name,
secret_name: final_name,
key_id: publicKey.key_id,
encrypted_value,
});
Expand All @@ -304,7 +306,7 @@ function setSecretForRepo(octokit, name, secret, repo, environment, dry_run, tar
return octokit.actions.createOrUpdateRepoSecret({
owner: repo_owner,
repo: repo_name,
secret_name: name,
secret_name: final_name,
key_id: publicKey.key_id,
encrypted_value,
});
Expand All @@ -314,24 +316,25 @@ function setSecretForRepo(octokit, name, secret, repo, environment, dry_run, tar
});
}
exports.setSecretForRepo = setSecretForRepo;
function deleteSecretForRepo(octokit, name, secret, repo, environment, dry_run, target) {
function deleteSecretForRepo(octokit, name, secret, repo, environment, new_secret_prefix, dry_run, target) {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Remove ${name} from ${repo.full_name}`);
const final_name = new_secret_prefix ? new_secret_prefix + name : name;
core.info(`Remove ${final_name} from ${repo.full_name}`);
try {
if (!dry_run) {
const action = "DELETE";
switch (target) {
case "codespaces":
return octokit.request(`${action} /repos/${repo.full_name}/codespaces/secrets/${name}`);
return octokit.request(`${action} /repos/${repo.full_name}/codespaces/secrets/${final_name}`);
case "dependabot":
return octokit.request(`${action} /repos/${repo.full_name}/dependabot/secrets/${name}`);
return octokit.request(`${action} /repos/${repo.full_name}/dependabot/secrets/${final_name}`);
case "actions":
default:
if (environment) {
return octokit.request(`${action} /repositories/${repo.id}/environments/${environment}/secrets/${name}`);
return octokit.request(`${action} /repositories/${repo.id}/environments/${environment}/secrets/${final_name}`);
}
else {
return octokit.request(`${action} /repos/${repo.full_name}/actions/secrets/${name}`);
return octokit.request(`${action} /repos/${repo.full_name}/actions/secrets/${final_name}`);
}
}
}
Expand Down Expand Up @@ -513,6 +516,7 @@ function run() {
FOUND_SECRETS: Object.keys(secrets),
ENVIRONMENT: config.ENVIRONMENT,
TARGET: config.TARGET,
NEW_SECRET_PREFIX: config.NEW_SECRET_PREFIX,
}, null, 2));
const limit = (0, p_limit_1.default)(config.CONCURRENCY);
const calls = [];
Expand All @@ -521,7 +525,7 @@ function run() {
const action = config.RUN_DELETE
? github_1.deleteSecretForRepo
: github_1.setSecretForRepo;
calls.push(limit(() => action(octokit, k, secrets[k], repo, config.ENVIRONMENT, config.DRY_RUN, config.TARGET)));
calls.push(limit(() => action(octokit, k, secrets[k], repo, config.ENVIRONMENT, config.NEW_SECRET_PREFIX, config.DRY_RUN, config.TARGET)));
}
}
yield Promise.all(calls);
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Config {
RUN_DELETE: boolean;
ENVIRONMENT: string;
TARGET: string;
NEW_SECRET_PREFIX: string;
}

export function getConfig(): Config {
Expand All @@ -54,6 +55,7 @@ export function getConfig(): Config {
),
ENVIRONMENT: core.getInput("ENVIRONMENT", { required: false }),
TARGET: core.getInput("TARGET", { required: false }),
NEW_SECRET_PREFIX: core.getInput("NEW_SECRET_PREFIX", { required: false }),
};

if (config.DRY_RUN) {
Expand Down
Loading
Loading