envilder 0.1.5 → 0.2.1
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.
- package/.github/dependabot.yml +40 -0
- package/.github/pull_request_template.md +20 -0
- package/.github/workflows/codeql-analysis.yml +49 -0
- package/.github/workflows/coverage-report.yml +73 -0
- package/.github/workflows/unit-tests.yml +48 -0
- package/README.md +27 -1
- package/lib/cli/cliRunner.js +1 -1
- package/lib/cli/cliRunner.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +59 -17
- package/lib/index.js.map +1 -1
- package/package.json +9 -4
- package/src/cli/cliRunner.ts +1 -1
- package/src/index.ts +69 -17
- package/tests/cli/cliRunner.test.ts +0 -13
- package/tests/index.test.ts +74 -2
- package/tests/sample/param_map.json +1 -3
- package/vite.config.ts +6 -2
- package/vitest.config.js +12 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: npm
|
|
9
|
+
directory: "/"
|
|
10
|
+
pull-request-branch-name:
|
|
11
|
+
separator: "/"
|
|
12
|
+
schedule:
|
|
13
|
+
interval: monthly
|
|
14
|
+
day: monday
|
|
15
|
+
time: "10:00"
|
|
16
|
+
timezone: Europe/Madrid
|
|
17
|
+
labels:
|
|
18
|
+
- "npm"
|
|
19
|
+
- "dependencies"
|
|
20
|
+
reviewers:
|
|
21
|
+
- macalbert
|
|
22
|
+
assignees:
|
|
23
|
+
- macalbert
|
|
24
|
+
|
|
25
|
+
- package-ecosystem: github-actions
|
|
26
|
+
directory: "/"
|
|
27
|
+
pull-request-branch-name:
|
|
28
|
+
separator: "/"
|
|
29
|
+
schedule:
|
|
30
|
+
interval: monthly
|
|
31
|
+
day: monday
|
|
32
|
+
time: "10:00"
|
|
33
|
+
timezone: Europe/Madrid
|
|
34
|
+
labels:
|
|
35
|
+
- "github-actions"
|
|
36
|
+
- "dependencies"
|
|
37
|
+
reviewers:
|
|
38
|
+
- macalbert
|
|
39
|
+
assignees:
|
|
40
|
+
- macalbert
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Description
|
|
2
|
+
|
|
3
|
+
_Describe the problem or feature in addition to a link to the issues._
|
|
4
|
+
|
|
5
|
+
## Approach
|
|
6
|
+
|
|
7
|
+
_How does this change address the problem?_
|
|
8
|
+
|
|
9
|
+
## Open Questions and Pre-Merge TODOs
|
|
10
|
+
|
|
11
|
+
- [ ] Use github checklists. When solved, check the box and explain the answer.
|
|
12
|
+
|
|
13
|
+
## Learning
|
|
14
|
+
|
|
15
|
+
_Describe the research stage_
|
|
16
|
+
_Links to blog posts, patterns, libraries or addons used to solve this problem_
|
|
17
|
+
|
|
18
|
+
## Blog Posts
|
|
19
|
+
|
|
20
|
+
- [How to Pull Request](https://github.com/flexyford/pull-request) Github Repo with Learning focused Pull Request Template.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
paths:
|
|
9
|
+
- ".github/workflows/codeql-analysis.yml"
|
|
10
|
+
|
|
11
|
+
push:
|
|
12
|
+
branches: [main]
|
|
13
|
+
paths:
|
|
14
|
+
- ".github/workflows/codeql-analysis.yml"
|
|
15
|
+
- "src/**"
|
|
16
|
+
- "test/**"
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
analyze:
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false
|
|
26
|
+
matrix:
|
|
27
|
+
language: ["javascript"]
|
|
28
|
+
|
|
29
|
+
permissions:
|
|
30
|
+
security-events: write
|
|
31
|
+
|
|
32
|
+
runs-on: ubuntu-22.04
|
|
33
|
+
|
|
34
|
+
steps:
|
|
35
|
+
- name: Checkout Repository
|
|
36
|
+
uses: actions/checkout@v4
|
|
37
|
+
with:
|
|
38
|
+
lfs: true
|
|
39
|
+
|
|
40
|
+
- name: Initialize CodeQL
|
|
41
|
+
uses: github/codeql-action/init@v3
|
|
42
|
+
with:
|
|
43
|
+
languages: ${{ matrix.language }}
|
|
44
|
+
|
|
45
|
+
- name: Autobuild
|
|
46
|
+
uses: github/codeql-action/autobuild@v3
|
|
47
|
+
|
|
48
|
+
- name: Perform CodeQL Analysis
|
|
49
|
+
uses: github/codeql-action/analyze@v3
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: 🔦 code-coverage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
pull_request:
|
|
7
|
+
paths:
|
|
8
|
+
- ".github/workflows/coverage-report.yml"
|
|
9
|
+
|
|
10
|
+
push:
|
|
11
|
+
branches:
|
|
12
|
+
- "main"
|
|
13
|
+
paths:
|
|
14
|
+
- ".github/workflows/coverage-report.yml"
|
|
15
|
+
- "src/**"
|
|
16
|
+
- "test/**"
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: "pages"
|
|
20
|
+
cancel-in-progress: false
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
pages: write
|
|
25
|
+
id-token: write
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
build-coverage:
|
|
29
|
+
environment:
|
|
30
|
+
name: github-pages
|
|
31
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
if: ${{ !github.event.pull_request.draft }}
|
|
34
|
+
steps:
|
|
35
|
+
- name: Check if PR is created by Dependabot
|
|
36
|
+
id: dependabot-check
|
|
37
|
+
run: |
|
|
38
|
+
if [[ "${{ github.actor }}" == *dependabot* ]]; then
|
|
39
|
+
echo "is_dependabot=true" >> "$GITHUB_OUTPUT"
|
|
40
|
+
else
|
|
41
|
+
echo "is_dependabot=false" >> "$GITHUB_OUTPUT"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
- name: Check if the workflow should run
|
|
45
|
+
if: ${{ steps.dependabot-check.outputs.is_dependabot == 'false' }}
|
|
46
|
+
run: echo "The workflow is allowed to run for this PR"
|
|
47
|
+
|
|
48
|
+
- name: 🧲 Checkout
|
|
49
|
+
uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
- name: 🛠️ Setup Node.js with Cache
|
|
52
|
+
uses: actions/setup-node@v4
|
|
53
|
+
with:
|
|
54
|
+
node-version: "20.x"
|
|
55
|
+
cache: "yarn"
|
|
56
|
+
|
|
57
|
+
- name: 📦 Install dependencies
|
|
58
|
+
run: yarn install
|
|
59
|
+
|
|
60
|
+
- name: 🔥 Run tests and collect coverage
|
|
61
|
+
run: yarn test
|
|
62
|
+
|
|
63
|
+
- name: Setup Pages
|
|
64
|
+
uses: actions/configure-pages@v5
|
|
65
|
+
|
|
66
|
+
- name: Upload artifact
|
|
67
|
+
uses: actions/upload-pages-artifact@v3
|
|
68
|
+
with:
|
|
69
|
+
path: "./coverage"
|
|
70
|
+
|
|
71
|
+
- name: Deploy to GitHub Pages
|
|
72
|
+
id: deployment
|
|
73
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: 🌱 unit-tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch: {}
|
|
5
|
+
|
|
6
|
+
pull_request:
|
|
7
|
+
branches:
|
|
8
|
+
- "*"
|
|
9
|
+
types:
|
|
10
|
+
- opened
|
|
11
|
+
- reopened
|
|
12
|
+
- synchronize
|
|
13
|
+
- ready_for_review
|
|
14
|
+
paths:
|
|
15
|
+
- ".github/workflows/unit-tests.yml"
|
|
16
|
+
- "src/**"
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
envilder-test:
|
|
24
|
+
runs-on: ubuntu-24.04
|
|
25
|
+
if: ${{ !github.event.pull_request.draft }}
|
|
26
|
+
timeout-minutes: 30
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- name: 🚀 ♂️ Checkout
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
|
|
32
|
+
- name: 🛠️ Setup Node.js with Cache
|
|
33
|
+
uses: actions/setup-node@v4
|
|
34
|
+
with:
|
|
35
|
+
node-version: '20.x'
|
|
36
|
+
cache: 'yarn'
|
|
37
|
+
|
|
38
|
+
- name: 📦 Install packages
|
|
39
|
+
run: yarn install
|
|
40
|
+
|
|
41
|
+
- name: 🔍 Run code quality checker
|
|
42
|
+
run: yarn lint
|
|
43
|
+
|
|
44
|
+
- name: 🚧 Build
|
|
45
|
+
run: yarn build
|
|
46
|
+
|
|
47
|
+
- name: 🚴♀️ Run unit tests
|
|
48
|
+
run: yarn test
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🌱 Envilder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`Envilder` is a CLI tool to manage AWS SSM Parameter Store parameters and automatically generate the required `.env` file. This tool simplifies environment variable management for projects, avoiding manual updates and ensuring consistency across environments.
|
|
4
4
|
|
|
5
5
|
## ✨ Features
|
|
6
6
|
|
|
@@ -9,6 +9,32 @@
|
|
|
9
9
|
- 🛡️ Handles both encrypted and unencrypted SSM parameters.
|
|
10
10
|
- 🪶 Lightweight and simple to use.
|
|
11
11
|
|
|
12
|
+
## Prerequisites
|
|
13
|
+
Before using `Envilder`, ensure that you have the AWS CLI installed and properly configured on your local machine. This configuration is required for `Envilder` to access and manage parameters in AWS SSM.
|
|
14
|
+
|
|
15
|
+
### AWS CLI Installation & Configuration
|
|
16
|
+
1. Install the AWS CLI by following the instructions [here](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
|
|
17
|
+
2. After installation, configure the AWS CLI using the following command:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
aws configure
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You'll be prompted to provide:
|
|
24
|
+
- AWS Access Key ID
|
|
25
|
+
- AWS Secret Access Key
|
|
26
|
+
- Default region name (e.g., `us-east-1`)
|
|
27
|
+
- Default output format (e.g., `json`)
|
|
28
|
+
|
|
29
|
+
Make sure that the AWS credentials you're using have the appropriate permissions to access the SSM Parameter Store in your AWS account.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
You can install `Envilder` globally using yarn. This will allow you to use the `envilder` command from any directory on your system.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yarn global add envilder
|
|
36
|
+
```
|
|
37
|
+
|
|
12
38
|
## 📦 Installation
|
|
13
39
|
|
|
14
40
|
You can install **envilder** globally or locally using npm:
|
package/lib/cli/cliRunner.js
CHANGED
package/lib/cli/cliRunner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cliRunner.js","sourceRoot":"","sources":["../../src/cli/cliRunner.ts"],"names":[],"mappings":";;;;;;;;;;AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,UAAgB,SAAS;;QAC7B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;QAE9B,OAAO;aACJ,IAAI,CAAC,UAAU,CAAC;aAChB,WAAW,CAAC,2DAA2D,CAAC;aACxE,OAAO,CAAC,OAAO,CAAC;aAChB,cAAc,CAAC,cAAc,EAAE,yDAAyD,CAAC;aACzF,cAAc,CAAC,kBAAkB,EAAE,uCAAuC,CAAC,CAAC;QAE/E,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAE/B,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,CAAC;QAED,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;CAAA;AAED,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IAC1B,OAAO,CAAC,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"cliRunner.js","sourceRoot":"","sources":["../../src/cli/cliRunner.ts"],"names":[],"mappings":";;;;;;;;;;AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,UAAgB,SAAS;;QAC7B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;QAE9B,OAAO;aACJ,IAAI,CAAC,UAAU,CAAC;aAChB,WAAW,CAAC,2DAA2D,CAAC;aACxE,OAAO,CAAC,OAAO,CAAC;aAChB,cAAc,CAAC,cAAc,EAAE,yDAAyD,CAAC;aACzF,cAAc,CAAC,kBAAkB,EAAE,uCAAuC,CAAC,CAAC;QAE/E,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAE/B,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,CAAC;QAED,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;CAAA;AAED,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IAC1B,OAAO,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAC;AAC7E,CAAC,CAAC,CAAC"}
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,wBAAsB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBAQ7D"}
|
package/lib/index.js
CHANGED
|
@@ -9,36 +9,78 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import { GetParameterCommand, SSM } from '@aws-sdk/client-ssm';
|
|
12
|
+
import * as dotenv from 'dotenv';
|
|
12
13
|
const ssm = new SSM({});
|
|
13
14
|
export function run(mapPath, envFilePath) {
|
|
14
15
|
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
16
|
+
const paramMap = loadParamMap(mapPath);
|
|
17
|
+
const existingEnvVariables = loadExistingEnvVariables(envFilePath);
|
|
18
|
+
const updatedEnvVariables = yield fetchAndUpdateEnvVariables(paramMap, existingEnvVariables);
|
|
19
|
+
writeEnvFile(envFilePath, updatedEnvVariables);
|
|
20
|
+
console.log(`Environment File generated at '${envFilePath}'`);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function loadParamMap(mapPath) {
|
|
24
|
+
const content = fs.readFileSync(mapPath, 'utf-8');
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(content);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error(`Error parsing JSON from ${mapPath}`);
|
|
30
|
+
throw new Error(`Invalid JSON in parameter map file: ${mapPath}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function loadExistingEnvVariables(envFilePath) {
|
|
34
|
+
const envVariables = {};
|
|
35
|
+
if (!fs.existsSync(envFilePath))
|
|
36
|
+
return envVariables;
|
|
37
|
+
const existingEnvContent = fs.readFileSync(envFilePath, 'utf-8');
|
|
38
|
+
const parsedEnv = dotenv.parse(existingEnvContent);
|
|
39
|
+
Object.assign(envVariables, parsedEnv);
|
|
40
|
+
return envVariables;
|
|
41
|
+
}
|
|
42
|
+
function fetchAndUpdateEnvVariables(paramMap, existingEnvVariables) {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
const errors = [];
|
|
19
45
|
for (const [envVar, ssmName] of Object.entries(paramMap)) {
|
|
20
46
|
try {
|
|
21
|
-
const
|
|
22
|
-
Name: ssmName,
|
|
23
|
-
WithDecryption: true,
|
|
24
|
-
});
|
|
25
|
-
const { Parameter } = yield ssm.send(command);
|
|
26
|
-
const value = Parameter === null || Parameter === void 0 ? void 0 : Parameter.Value;
|
|
47
|
+
const value = yield fetchSSMParameter(ssmName);
|
|
27
48
|
if (value) {
|
|
28
|
-
|
|
29
|
-
console.log(`${envVar}=${value}`);
|
|
49
|
+
existingEnvVariables[envVar] = value;
|
|
50
|
+
console.log(`${envVar}=${value.length > 3 ? '*'.repeat(value.length - 3) + value.slice(-3) : '*'.repeat(value.length)}`);
|
|
30
51
|
}
|
|
31
52
|
else {
|
|
32
|
-
console.error(`Warning: No value found for ${ssmName}`);
|
|
53
|
+
console.error(`Warning: No value found for: '${ssmName}'`);
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
56
|
catch (error) {
|
|
36
|
-
console.error(`Error fetching parameter ${ssmName}
|
|
37
|
-
|
|
57
|
+
console.error(`Error fetching parameter: '${ssmName}'`);
|
|
58
|
+
errors.push(`ParameterNotFound: ${ssmName}`);
|
|
38
59
|
}
|
|
39
60
|
}
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
if (errors.length > 0) {
|
|
62
|
+
throw new Error(`Some parameters could not be fetched:\n${errors.join('\n')}`);
|
|
63
|
+
}
|
|
64
|
+
return existingEnvVariables;
|
|
42
65
|
});
|
|
43
66
|
}
|
|
67
|
+
function fetchSSMParameter(ssmName) {
|
|
68
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
const command = new GetParameterCommand({
|
|
70
|
+
Name: ssmName,
|
|
71
|
+
WithDecryption: true,
|
|
72
|
+
});
|
|
73
|
+
const { Parameter } = yield ssm.send(command);
|
|
74
|
+
return Parameter === null || Parameter === void 0 ? void 0 : Parameter.Value;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function writeEnvFile(envFilePath, envVariables) {
|
|
78
|
+
const envContent = Object.entries(envVariables)
|
|
79
|
+
.map(([key, value]) => {
|
|
80
|
+
const escapedValue = value.replace(/(\n|\r|\n\r)/g, '\\n').replace(/"/g, '\\"');
|
|
81
|
+
return `${key}=${escapedValue}`;
|
|
82
|
+
})
|
|
83
|
+
.join('\n');
|
|
84
|
+
fs.writeFileSync(envFilePath, envContent);
|
|
85
|
+
}
|
|
44
86
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envilder",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A CLI tool to generate .env files from AWS SSM parameters",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test-run": "npm run build && node lib/cli/cliRunner.js --map=tests/sample/param_map.json --envfile=.env",
|
|
16
16
|
"build": "npm run clean && tsc -p tsconfig.build.json --sourceMap --declaration",
|
|
17
17
|
"lint": "biome lint --write && biome format --write && biome check --write && tsc --noEmit && secretlint **/*",
|
|
18
|
-
"test": "
|
|
18
|
+
"test": "vitest run --reporter verbose --coverage",
|
|
19
19
|
"cli-run": "npm run build && node --trace-warnings lib",
|
|
20
20
|
"npm-publish": "npm run lint && npm run build && npm publish",
|
|
21
21
|
"npm-release-patch": "npm version patch && npm run npm-publish",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@secretlint/secretlint-rule-preset-recommend": "^8.2.4",
|
|
43
43
|
"@types/node": "^22.5.5",
|
|
44
44
|
"commander": "^12.1.0",
|
|
45
|
+
"dotenv": "^16.4.5",
|
|
45
46
|
"picocolors": "^1.1.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
@@ -51,10 +52,14 @@
|
|
|
51
52
|
"secretlint": "^8.2.4",
|
|
52
53
|
"ts-node": "^10.9.2",
|
|
53
54
|
"typescript": "^5.6.2",
|
|
54
|
-
"vitest": "^2.1.
|
|
55
|
+
"vitest": "^2.1.2"
|
|
56
|
+
},
|
|
57
|
+
"resolutions": {
|
|
58
|
+
"string-width": "4.2.3",
|
|
59
|
+
"strip-ansi": "6.0.1"
|
|
55
60
|
},
|
|
56
61
|
"engines": {
|
|
57
|
-
"node": ">=
|
|
62
|
+
"node": ">=20.0.0",
|
|
58
63
|
"yarn": ">=1.22"
|
|
59
64
|
}
|
|
60
65
|
}
|
package/src/cli/cliRunner.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,36 +1,88 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import { GetParameterCommand, SSM } from '@aws-sdk/client-ssm';
|
|
3
|
+
import * as dotenv from 'dotenv';
|
|
3
4
|
|
|
4
5
|
const ssm = new SSM({});
|
|
5
6
|
|
|
6
7
|
export async function run(mapPath: string, envFilePath: string) {
|
|
8
|
+
const paramMap = loadParamMap(mapPath);
|
|
9
|
+
const existingEnvVariables = loadExistingEnvVariables(envFilePath);
|
|
10
|
+
|
|
11
|
+
const updatedEnvVariables = await fetchAndUpdateEnvVariables(paramMap, existingEnvVariables);
|
|
12
|
+
|
|
13
|
+
writeEnvFile(envFilePath, updatedEnvVariables);
|
|
14
|
+
console.log(`Environment File generated at '${envFilePath}'`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadParamMap(mapPath: string): Record<string, string> {
|
|
7
18
|
const content = fs.readFileSync(mapPath, 'utf-8');
|
|
8
|
-
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(content);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(`Error parsing JSON from ${mapPath}`);
|
|
23
|
+
throw new Error(`Invalid JSON in parameter map file: ${mapPath}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadExistingEnvVariables(envFilePath: string): Record<string, string> {
|
|
28
|
+
const envVariables: Record<string, string> = {};
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(envFilePath)) return envVariables;
|
|
31
|
+
|
|
32
|
+
const existingEnvContent = fs.readFileSync(envFilePath, 'utf-8');
|
|
33
|
+
const parsedEnv = dotenv.parse(existingEnvContent);
|
|
34
|
+
Object.assign(envVariables, parsedEnv);
|
|
35
|
+
|
|
36
|
+
return envVariables;
|
|
37
|
+
}
|
|
9
38
|
|
|
10
|
-
|
|
39
|
+
async function fetchAndUpdateEnvVariables(
|
|
40
|
+
paramMap: Record<string, string>,
|
|
41
|
+
existingEnvVariables: Record<string, string>,
|
|
42
|
+
): Promise<Record<string, string>> {
|
|
43
|
+
const errors: string[] = [];
|
|
11
44
|
|
|
12
|
-
console.log('Fetching parameters...');
|
|
13
45
|
for (const [envVar, ssmName] of Object.entries(paramMap)) {
|
|
14
46
|
try {
|
|
15
|
-
const
|
|
16
|
-
Name: ssmName,
|
|
17
|
-
WithDecryption: true,
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const { Parameter } = await ssm.send(command);
|
|
21
|
-
const value = Parameter?.Value;
|
|
47
|
+
const value = await fetchSSMParameter(ssmName);
|
|
22
48
|
if (value) {
|
|
23
|
-
|
|
24
|
-
console.log(
|
|
49
|
+
existingEnvVariables[envVar] = value;
|
|
50
|
+
console.log(
|
|
51
|
+
`${envVar}=${value.length > 3 ? '*'.repeat(value.length - 3) + value.slice(-3) : '*'.repeat(value.length)}`,
|
|
52
|
+
);
|
|
25
53
|
} else {
|
|
26
|
-
console.error(`Warning: No value found for ${ssmName}`);
|
|
54
|
+
console.error(`Warning: No value found for: '${ssmName}'`);
|
|
27
55
|
}
|
|
28
56
|
} catch (error) {
|
|
29
|
-
console.error(`Error fetching parameter ${ssmName}
|
|
30
|
-
|
|
57
|
+
console.error(`Error fetching parameter: '${ssmName}'`);
|
|
58
|
+
errors.push(`ParameterNotFound: ${ssmName}`);
|
|
31
59
|
}
|
|
32
60
|
}
|
|
33
61
|
|
|
34
|
-
|
|
35
|
-
|
|
62
|
+
if (errors.length > 0) {
|
|
63
|
+
throw new Error(`Some parameters could not be fetched:\n${errors.join('\n')}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return existingEnvVariables;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fetchSSMParameter(ssmName: string): Promise<string | undefined> {
|
|
70
|
+
const command = new GetParameterCommand({
|
|
71
|
+
Name: ssmName,
|
|
72
|
+
WithDecryption: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const { Parameter } = await ssm.send(command);
|
|
76
|
+
return Parameter?.Value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeEnvFile(envFilePath: string, envVariables: Record<string, string>): void {
|
|
80
|
+
const envContent = Object.entries(envVariables)
|
|
81
|
+
.map(([key, value]) => {
|
|
82
|
+
const escapedValue = value.replace(/(\n|\r|\n\r)/g, '\\n').replace(/"/g, '\\"');
|
|
83
|
+
return `${key}=${escapedValue}`;
|
|
84
|
+
})
|
|
85
|
+
.join('\n');
|
|
86
|
+
|
|
87
|
+
fs.writeFileSync(envFilePath, envContent);
|
|
36
88
|
}
|
|
@@ -42,17 +42,4 @@ describe('cliRunner', () => {
|
|
|
42
42
|
// Assert
|
|
43
43
|
await expect(action).rejects.toThrow('process.exit called');
|
|
44
44
|
});
|
|
45
|
-
|
|
46
|
-
it('Should_DisplayHelp_When_NoArgumentsAreProvided', async () => {
|
|
47
|
-
// Arrange
|
|
48
|
-
vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
49
|
-
throw new Error('process.exit called with code 0');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Act
|
|
53
|
-
const action = cliRunner();
|
|
54
|
-
|
|
55
|
-
// Assert
|
|
56
|
-
await expect(action).rejects.toThrow('process.exit called with code 0');
|
|
57
|
-
});
|
|
58
45
|
});
|
package/tests/index.test.ts
CHANGED
|
@@ -11,12 +11,17 @@ vi.mock('@aws-sdk/client-ssm', () => {
|
|
|
11
11
|
Parameter: { Value: 'mockedEmail@example.com' },
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
|
+
|
|
14
15
|
if (command.input.Name === '/path/to/ssm/password') {
|
|
15
16
|
return Promise.resolve({
|
|
16
17
|
Parameter: { Value: 'mockedPassword' },
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
if (command.input.Name === '/path/to/ssm/password_no_value') {
|
|
22
|
+
return Promise.resolve({ Parameter: { Value: '' } });
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
return Promise.reject(new Error(`ParameterNotFound: ${command.input.Name}`));
|
|
21
26
|
}),
|
|
22
27
|
})),
|
|
@@ -57,7 +62,7 @@ describe('Envilder CLI', () => {
|
|
|
57
62
|
const mockMapPath = './tests/param_map.json';
|
|
58
63
|
const mockEnvFilePath = './tests/.env.test';
|
|
59
64
|
const paramMapContent = {
|
|
60
|
-
NEXT_PUBLIC_CREDENTIAL_EMAIL: '
|
|
65
|
+
NEXT_PUBLIC_CREDENTIAL_EMAIL: 'non-existent parameter',
|
|
61
66
|
};
|
|
62
67
|
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
|
|
63
68
|
|
|
@@ -65,7 +70,74 @@ describe('Envilder CLI', () => {
|
|
|
65
70
|
const action = run(mockMapPath, mockEnvFilePath);
|
|
66
71
|
|
|
67
72
|
// Assert
|
|
68
|
-
await expect(action).rejects.toThrow('ParameterNotFound:
|
|
73
|
+
await expect(action).rejects.toThrow('ParameterNotFound: non-existent parameter');
|
|
74
|
+
fs.unlinkSync(mockMapPath);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('Should_AppendNewSSMParameters_When_EnvFileContainsExistingVariables', async () => {
|
|
78
|
+
// Arrange
|
|
79
|
+
const mockMapPath = './tests/param_map.json';
|
|
80
|
+
const mockEnvFilePath = './tests/.env.test';
|
|
81
|
+
|
|
82
|
+
const existingEnvContent = 'EXISTING_VAR=existingValue';
|
|
83
|
+
fs.writeFileSync(mockEnvFilePath, existingEnvContent);
|
|
84
|
+
const paramMapContent = {
|
|
85
|
+
NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/email',
|
|
86
|
+
NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password',
|
|
87
|
+
};
|
|
88
|
+
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
|
|
89
|
+
|
|
90
|
+
// Act
|
|
91
|
+
await run(mockMapPath, mockEnvFilePath);
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
const updatedEnvFileContent = fs.readFileSync(mockEnvFilePath, 'utf-8');
|
|
95
|
+
expect(updatedEnvFileContent).toContain('EXISTING_VAR=existingValue');
|
|
96
|
+
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_EMAIL=mockedEmail@example.com');
|
|
97
|
+
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword');
|
|
98
|
+
fs.unlinkSync(mockEnvFilePath);
|
|
99
|
+
fs.unlinkSync(mockMapPath);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('Should_OverwriteSSMParameters_When_EnvFileContainsSameVariables', async () => {
|
|
103
|
+
// Arrange
|
|
104
|
+
const mockMapPath = './tests/param_map.json';
|
|
105
|
+
const mockEnvFilePath = './tests/.env.test';
|
|
106
|
+
const existingEnvContent = 'NEXT_PUBLIC_CREDENTIAL_EMAIL=oldEmail@example.com';
|
|
107
|
+
fs.writeFileSync(mockEnvFilePath, existingEnvContent);
|
|
108
|
+
const paramMapContent = {
|
|
109
|
+
NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/email',
|
|
110
|
+
NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password',
|
|
111
|
+
};
|
|
112
|
+
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
|
|
113
|
+
|
|
114
|
+
// Act
|
|
115
|
+
await run(mockMapPath, mockEnvFilePath);
|
|
116
|
+
|
|
117
|
+
// Assert
|
|
118
|
+
const updatedEnvFileContent = fs.readFileSync(mockEnvFilePath, 'utf-8');
|
|
119
|
+
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_EMAIL=mockedEmail@example.com');
|
|
120
|
+
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword');
|
|
121
|
+
fs.unlinkSync(mockEnvFilePath);
|
|
122
|
+
fs.unlinkSync(mockMapPath);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('Should_LogWarning_When_SSMParameterHasNoValue', async () => {
|
|
126
|
+
// Arrange
|
|
127
|
+
const mockMapPath = './tests/param_map.json';
|
|
128
|
+
const mockEnvFilePath = './tests/.env.test';
|
|
129
|
+
const paramMapContent = {
|
|
130
|
+
NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password_no_value',
|
|
131
|
+
};
|
|
132
|
+
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
|
|
133
|
+
const consoleSpy = vi.spyOn(console, 'error');
|
|
134
|
+
|
|
135
|
+
// Act
|
|
136
|
+
await run(mockMapPath, mockEnvFilePath);
|
|
137
|
+
|
|
138
|
+
// Assert
|
|
139
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Warning: No value found for'));
|
|
140
|
+
fs.unlinkSync(mockEnvFilePath);
|
|
69
141
|
fs.unlinkSync(mockMapPath);
|
|
70
142
|
});
|
|
71
143
|
});
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"NEXT_PUBLIC_CREDENTIAL_PASSWORD": "/M47.Claims.Apps.Minimal.Api/Production/Auth/CredentialPassword",
|
|
4
|
-
"NEXT_PUBLIC_JWT_SECRET": "/M47.Claims.Apps.Minimal.Api/Production/Auth/JwtSecret"
|
|
2
|
+
"TOKEN_SECRET": "/Test/Token"
|
|
5
3
|
}
|
package/vite.config.ts
CHANGED
|
@@ -6,8 +6,12 @@ export default defineConfig({
|
|
|
6
6
|
environment: 'node',
|
|
7
7
|
include: ['tests/**/*.test.ts'],
|
|
8
8
|
coverage: {
|
|
9
|
-
|
|
10
|
-
reporter: ['text', '
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
reporter: ['text', 'html', 'json'],
|
|
11
|
+
reportsDirectory: './coverage',
|
|
12
|
+
all: true,
|
|
13
|
+
include: ['src/**/*.ts'],
|
|
14
|
+
exclude: ['node_modules', 'test', 'coverage', 'dist'],
|
|
11
15
|
},
|
|
12
16
|
},
|
|
13
17
|
});
|
package/vitest.config.js
ADDED