envilder 0.1.4 → 0.2.0
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 +39 -21
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +46 -12
- package/lib/index.js.map +1 -1
- package/package.json +8 -4
- package/src/index.ts +53 -12
- package/tests/cli/cliRunner.test.ts +0 -13
- package/tests/index.test.ts +81 -6
- 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,36 +1,56 @@
|
|
|
1
|
-
# Envilder
|
|
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
|
-
## Features
|
|
5
|
+
## ✨ Features
|
|
6
6
|
|
|
7
|
-
- Fetch parameters securely from AWS SSM Parameter Store.
|
|
8
|
-
- Automatically generates a `.env` file with specified parameters.
|
|
9
|
-
- Handles both encrypted and unencrypted SSM parameters.
|
|
10
|
-
- Lightweight and simple to use.
|
|
7
|
+
- 🔒 Fetch parameters securely from AWS SSM Parameter Store.
|
|
8
|
+
- ⚡ Automatically generates a `.env` file with specified parameters.
|
|
9
|
+
- 🛡️ Handles both encrypted and unencrypted SSM parameters.
|
|
10
|
+
- 🪶 Lightweight and simple to use.
|
|
11
11
|
|
|
12
|
-
##
|
|
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.
|
|
13
14
|
|
|
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.
|
|
15
33
|
|
|
16
34
|
```bash
|
|
17
|
-
|
|
35
|
+
yarn global add envilder
|
|
18
36
|
```
|
|
19
37
|
|
|
20
|
-
|
|
38
|
+
## 📦 Installation
|
|
39
|
+
|
|
40
|
+
You can install **envilder** globally or locally using npm:
|
|
21
41
|
|
|
22
42
|
```bash
|
|
23
|
-
npm install envilder
|
|
43
|
+
npm install -g envilder
|
|
24
44
|
```
|
|
25
45
|
|
|
26
|
-
## Usage
|
|
46
|
+
## 🚀 Usage
|
|
27
47
|
|
|
28
48
|
Envilder requires two arguments:
|
|
29
49
|
|
|
30
50
|
- `--map <path>`: Path to a JSON file mapping environment variable names to SSM parameters.
|
|
31
51
|
- `--envfile <path>`: Path where the generated .env file will be saved.
|
|
32
52
|
|
|
33
|
-
## Example
|
|
53
|
+
## 🔧 Example
|
|
34
54
|
|
|
35
55
|
1. Create a mapping file `param_map.json`:
|
|
36
56
|
|
|
@@ -44,19 +64,19 @@ Envilder requires two arguments:
|
|
|
44
64
|
2. Run envilder to generate your `.env` file:
|
|
45
65
|
|
|
46
66
|
```bash
|
|
47
|
-
envilder --map=
|
|
67
|
+
envilder --map=param_map.json --envfile=.env
|
|
48
68
|
```
|
|
49
69
|
|
|
50
70
|
3. The `.env` file will be generated in the specified location.
|
|
51
71
|
|
|
52
|
-
## Sample `.env` Output
|
|
72
|
+
## 📂 Sample `.env` Output
|
|
53
73
|
|
|
54
74
|
```makefile
|
|
55
75
|
NEXT_PUBLIC_CREDENTIAL_EMAIL=mockedEmail@example.com
|
|
56
76
|
NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword
|
|
57
77
|
```
|
|
58
78
|
|
|
59
|
-
## Running Tests
|
|
79
|
+
## 🧪 Running Tests
|
|
60
80
|
|
|
61
81
|
To run the tests with coverage:
|
|
62
82
|
|
|
@@ -64,12 +84,10 @@ To run the tests with coverage:
|
|
|
64
84
|
yarn test
|
|
65
85
|
```
|
|
66
86
|
|
|
67
|
-
## License
|
|
87
|
+
## 📝 License
|
|
68
88
|
|
|
69
89
|
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
|
|
70
90
|
|
|
71
|
-
## Contributing
|
|
91
|
+
## 🙌 Contributing
|
|
72
92
|
|
|
73
93
|
Contributions are welcome! Feel free to submit issues and pull requests.
|
|
74
|
-
|
|
75
|
-
Created by [Marçal Albert](https://github.com/macalbert).
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,wBAAsB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,wBAAsB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBAQ7D"}
|
package/lib/index.js
CHANGED
|
@@ -12,20 +12,39 @@ import { GetParameterCommand, SSM } from '@aws-sdk/client-ssm';
|
|
|
12
12
|
const ssm = new SSM({});
|
|
13
13
|
export function run(mapPath, envFilePath) {
|
|
14
14
|
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
15
|
+
const paramMap = loadParamMap(mapPath);
|
|
16
|
+
const existingEnvVariables = loadExistingEnvVariables(envFilePath);
|
|
17
|
+
const updatedEnvVariables = yield fetchAndUpdateEnvVariables(paramMap, existingEnvVariables);
|
|
18
|
+
writeEnvFile(envFilePath, updatedEnvVariables);
|
|
19
|
+
console.log(`.env file generated at ${envFilePath}`);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function loadParamMap(mapPath) {
|
|
23
|
+
const content = fs.readFileSync(mapPath, 'utf-8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
function loadExistingEnvVariables(envFilePath) {
|
|
27
|
+
const envVariables = {};
|
|
28
|
+
if (!fs.existsSync(envFilePath))
|
|
29
|
+
return envVariables;
|
|
30
|
+
const existingEnvContent = fs.readFileSync(envFilePath, 'utf-8');
|
|
31
|
+
const lines = existingEnvContent.split('\n');
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const [key, value] = line.split('=');
|
|
34
|
+
if (key && value) {
|
|
35
|
+
envVariables[key] = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return envVariables;
|
|
39
|
+
}
|
|
40
|
+
function fetchAndUpdateEnvVariables(paramMap, existingEnvVariables) {
|
|
41
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
18
42
|
console.log('Fetching parameters...');
|
|
19
43
|
for (const [envVar, ssmName] of Object.entries(paramMap)) {
|
|
20
44
|
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;
|
|
45
|
+
const value = yield fetchSSMParameter(ssmName);
|
|
27
46
|
if (value) {
|
|
28
|
-
|
|
47
|
+
existingEnvVariables[envVar] = value;
|
|
29
48
|
console.log(`${envVar}=${value}`);
|
|
30
49
|
}
|
|
31
50
|
else {
|
|
@@ -37,8 +56,23 @@ export function run(mapPath, envFilePath) {
|
|
|
37
56
|
throw new Error(`ParameterNotFound: ${ssmName}`);
|
|
38
57
|
}
|
|
39
58
|
}
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
return existingEnvVariables;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function fetchSSMParameter(ssmName) {
|
|
63
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
const command = new GetParameterCommand({
|
|
65
|
+
Name: ssmName,
|
|
66
|
+
WithDecryption: true,
|
|
67
|
+
});
|
|
68
|
+
const { Parameter } = yield ssm.send(command);
|
|
69
|
+
return Parameter === null || Parameter === void 0 ? void 0 : Parameter.Value;
|
|
42
70
|
});
|
|
43
71
|
}
|
|
72
|
+
function writeEnvFile(envFilePath, envVariables) {
|
|
73
|
+
const envContent = Object.entries(envVariables)
|
|
74
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
75
|
+
.join('\n');
|
|
76
|
+
fs.writeFileSync(envFilePath, envContent);
|
|
77
|
+
}
|
|
44
78
|
//# 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;AAE/D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC;AAExB,MAAM,UAAgB,GAAG,CAAC,OAAe,EAAE,WAAmB;;QAC5D,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,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.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -51,10 +51,14 @@
|
|
|
51
51
|
"secretlint": "^8.2.4",
|
|
52
52
|
"ts-node": "^10.9.2",
|
|
53
53
|
"typescript": "^5.6.2",
|
|
54
|
-
"vitest": "^2.1.
|
|
54
|
+
"vitest": "^2.1.2"
|
|
55
|
+
},
|
|
56
|
+
"resolutions": {
|
|
57
|
+
"string-width": "4.2.3",
|
|
58
|
+
"strip-ansi": "6.0.1"
|
|
55
59
|
},
|
|
56
60
|
"engines": {
|
|
57
|
-
"node": ">=
|
|
61
|
+
"node": ">=20.0.0",
|
|
58
62
|
"yarn": ">=1.22"
|
|
59
63
|
}
|
|
60
64
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,23 +4,47 @@ import { GetParameterCommand, SSM } from '@aws-sdk/client-ssm';
|
|
|
4
4
|
const ssm = new SSM({});
|
|
5
5
|
|
|
6
6
|
export async function run(mapPath: string, envFilePath: string) {
|
|
7
|
+
const paramMap = loadParamMap(mapPath);
|
|
8
|
+
const existingEnvVariables = loadExistingEnvVariables(envFilePath);
|
|
9
|
+
|
|
10
|
+
const updatedEnvVariables = await fetchAndUpdateEnvVariables(paramMap, existingEnvVariables);
|
|
11
|
+
|
|
12
|
+
writeEnvFile(envFilePath, updatedEnvVariables);
|
|
13
|
+
console.log(`.env file generated at ${envFilePath}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function loadParamMap(mapPath: string): Record<string, string> {
|
|
7
17
|
const content = fs.readFileSync(mapPath, 'utf-8');
|
|
8
|
-
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadExistingEnvVariables(envFilePath: string): Record<string, string> {
|
|
22
|
+
const envVariables: Record<string, string> = {};
|
|
23
|
+
if (!fs.existsSync(envFilePath)) return envVariables;
|
|
24
|
+
|
|
25
|
+
const existingEnvContent = fs.readFileSync(envFilePath, 'utf-8');
|
|
26
|
+
const lines = existingEnvContent.split('\n');
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const [key, value] = line.split('=');
|
|
29
|
+
if (key && value) {
|
|
30
|
+
envVariables[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
9
33
|
|
|
10
|
-
|
|
34
|
+
return envVariables;
|
|
35
|
+
}
|
|
11
36
|
|
|
37
|
+
async function fetchAndUpdateEnvVariables(
|
|
38
|
+
paramMap: Record<string, string>,
|
|
39
|
+
existingEnvVariables: Record<string, string>,
|
|
40
|
+
): Promise<Record<string, string>> {
|
|
12
41
|
console.log('Fetching parameters...');
|
|
42
|
+
|
|
13
43
|
for (const [envVar, ssmName] of Object.entries(paramMap)) {
|
|
14
44
|
try {
|
|
15
|
-
const
|
|
16
|
-
Name: ssmName,
|
|
17
|
-
WithDecryption: true,
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const { Parameter } = await ssm.send(command);
|
|
21
|
-
const value = Parameter?.Value;
|
|
45
|
+
const value = await fetchSSMParameter(ssmName);
|
|
22
46
|
if (value) {
|
|
23
|
-
|
|
47
|
+
existingEnvVariables[envVar] = value;
|
|
24
48
|
console.log(`${envVar}=${value}`);
|
|
25
49
|
} else {
|
|
26
50
|
console.error(`Warning: No value found for ${ssmName}`);
|
|
@@ -31,6 +55,23 @@ export async function run(mapPath: string, envFilePath: string) {
|
|
|
31
55
|
}
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
return existingEnvVariables;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function fetchSSMParameter(ssmName: string): Promise<string | undefined> {
|
|
62
|
+
const command = new GetParameterCommand({
|
|
63
|
+
Name: ssmName,
|
|
64
|
+
WithDecryption: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { Parameter } = await ssm.send(command);
|
|
68
|
+
return Parameter?.Value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function writeEnvFile(envFilePath: string, envVariables: Record<string, string>): void {
|
|
72
|
+
const envContent = Object.entries(envVariables)
|
|
73
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
74
|
+
.join('\n');
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(envFilePath, envContent);
|
|
36
77
|
}
|
|
@@ -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
|
})),
|
|
@@ -48,8 +53,6 @@ describe('Envilder CLI', () => {
|
|
|
48
53
|
const envFileContent = fs.readFileSync(mockEnvFilePath, 'utf-8');
|
|
49
54
|
expect(envFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_EMAIL=mockedEmail@example.com');
|
|
50
55
|
expect(envFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword');
|
|
51
|
-
|
|
52
|
-
// Cleanup
|
|
53
56
|
fs.unlinkSync(mockEnvFilePath);
|
|
54
57
|
fs.unlinkSync(mockMapPath);
|
|
55
58
|
});
|
|
@@ -59,14 +62,86 @@ describe('Envilder CLI', () => {
|
|
|
59
62
|
const mockMapPath = './tests/param_map.json';
|
|
60
63
|
const mockEnvFilePath = './tests/.env.test';
|
|
61
64
|
const paramMapContent = {
|
|
62
|
-
NEXT_PUBLIC_CREDENTIAL_EMAIL: '
|
|
65
|
+
NEXT_PUBLIC_CREDENTIAL_EMAIL: 'non-existent parameter',
|
|
66
|
+
};
|
|
67
|
+
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
const action = run(mockMapPath, mockEnvFilePath);
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
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 = `
|
|
83
|
+
EXISTING_VAR=existingValue
|
|
84
|
+
`;
|
|
85
|
+
fs.writeFileSync(mockEnvFilePath, existingEnvContent);
|
|
86
|
+
const paramMapContent = {
|
|
87
|
+
NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/email',
|
|
88
|
+
NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password',
|
|
89
|
+
};
|
|
90
|
+
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
|
|
91
|
+
|
|
92
|
+
// Act
|
|
93
|
+
await run(mockMapPath, mockEnvFilePath);
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
const updatedEnvFileContent = fs.readFileSync(mockEnvFilePath, 'utf-8');
|
|
97
|
+
expect(updatedEnvFileContent).toContain('EXISTING_VAR=existingValue');
|
|
98
|
+
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_EMAIL=mockedEmail@example.com');
|
|
99
|
+
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword');
|
|
100
|
+
fs.unlinkSync(mockEnvFilePath);
|
|
101
|
+
fs.unlinkSync(mockMapPath);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('Should_OverwriteSSMParameters_When_EnvFileContainsSameVariables', async () => {
|
|
105
|
+
// Arrange
|
|
106
|
+
const mockMapPath = './tests/param_map.json';
|
|
107
|
+
const mockEnvFilePath = './tests/.env.test';
|
|
108
|
+
const existingEnvContent = `
|
|
109
|
+
NEXT_PUBLIC_CREDENTIAL_EMAIL=oldEmail@example.com
|
|
110
|
+
`;
|
|
111
|
+
fs.writeFileSync(mockEnvFilePath, existingEnvContent);
|
|
112
|
+
const paramMapContent = {
|
|
113
|
+
NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/email',
|
|
114
|
+
NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password',
|
|
115
|
+
};
|
|
116
|
+
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
|
|
117
|
+
|
|
118
|
+
// Act
|
|
119
|
+
await run(mockMapPath, mockEnvFilePath);
|
|
120
|
+
|
|
121
|
+
// Assert
|
|
122
|
+
const updatedEnvFileContent = fs.readFileSync(mockEnvFilePath, 'utf-8');
|
|
123
|
+
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_EMAIL=mockedEmail@example.com');
|
|
124
|
+
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword');
|
|
125
|
+
fs.unlinkSync(mockEnvFilePath);
|
|
126
|
+
fs.unlinkSync(mockMapPath);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('Should_LogWarning_When_SSMParameterHasNoValue', async () => {
|
|
130
|
+
// Arrange
|
|
131
|
+
const mockMapPath = './tests/param_map.json';
|
|
132
|
+
const mockEnvFilePath = './tests/.env.test';
|
|
133
|
+
const paramMapContent = {
|
|
134
|
+
NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password_no_value',
|
|
63
135
|
};
|
|
64
136
|
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
|
|
137
|
+
const consoleSpy = vi.spyOn(console, 'error');
|
|
65
138
|
|
|
66
|
-
// Act
|
|
67
|
-
await
|
|
139
|
+
// Act
|
|
140
|
+
await run(mockMapPath, mockEnvFilePath);
|
|
68
141
|
|
|
69
|
-
//
|
|
142
|
+
// Assert
|
|
143
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Warning: No value found for'));
|
|
144
|
+
fs.unlinkSync(mockEnvFilePath);
|
|
70
145
|
fs.unlinkSync(mockMapPath);
|
|
71
146
|
});
|
|
72
147
|
});
|
|
@@ -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