safelaunch 1.0.3 → 1.0.5
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/README.md +20 -42
- package/package.json +1 -1
- package/src/validate.js +122 -43
package/README.md
CHANGED
|
@@ -5,46 +5,26 @@
|
|
|
5
5
|
safelaunch validates your local environment against a required environment contract before you push to production. No more missing variables. No more runtime mismatches. No more production surprises.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
npm install -g safelaunch
|
|
10
|
-
```
|
|
11
10
|
|
|
12
11
|
## Quick Start
|
|
13
12
|
|
|
14
|
-
**Step 1:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"DATABASE_URL": {
|
|
25
|
-
"required": true,
|
|
26
|
-
"description": "PostgreSQL connection string"
|
|
27
|
-
},
|
|
28
|
-
"REDIS_URL": {
|
|
29
|
-
"required": true,
|
|
30
|
-
"description": "Redis connection string"
|
|
31
|
-
},
|
|
32
|
-
"API_KEY": {
|
|
33
|
-
"required": true,
|
|
34
|
-
"description": "External API key"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
**Step 2: Run the validator**
|
|
41
|
-
```bash
|
|
13
|
+
**Step 1: Generate your environment manifest automatically**
|
|
14
|
+
|
|
15
|
+
Run this in your project folder:
|
|
16
|
+
|
|
17
|
+
safelaunch init
|
|
18
|
+
|
|
19
|
+
safelaunch scans your entire codebase, finds every environment variable your app uses, and generates env.manifest.json automatically.
|
|
20
|
+
|
|
21
|
+
**Step 2: Validate before you push**
|
|
22
|
+
|
|
42
23
|
safelaunch validate
|
|
43
|
-
```
|
|
44
24
|
|
|
45
|
-
**Step 3: See exactly what
|
|
46
|
-
|
|
47
|
-
Running safelaunch
|
|
25
|
+
**Step 3: See exactly what will break**
|
|
26
|
+
|
|
27
|
+
Running safelaunch validate gives you:
|
|
48
28
|
|
|
49
29
|
❌ MISSING VARIABLES (2 found)
|
|
50
30
|
|
|
@@ -57,23 +37,21 @@ Running safelaunch...
|
|
|
57
37
|
|
|
58
38
|
Your environment is not ready for production.
|
|
59
39
|
Fix the issues above and run safelaunch again.
|
|
60
|
-
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
safelaunch init scan project and generate env.manifest.json automatically
|
|
44
|
+
safelaunch validate validate your .env against the manifest
|
|
61
45
|
|
|
62
46
|
## CI Integration
|
|
63
47
|
|
|
64
48
|
Add this to your GitHub Actions workflow to block deployments automatically:
|
|
65
|
-
|
|
49
|
+
|
|
66
50
|
- name: Install safelaunch
|
|
67
51
|
run: npm install -g safelaunch
|
|
68
52
|
|
|
69
53
|
- name: Validate environment
|
|
70
54
|
run: safelaunch validate
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## What it checks
|
|
74
|
-
|
|
75
|
-
- Missing required environment variables
|
|
76
|
-
- Runtime version mismatches between local and manifest
|
|
77
55
|
|
|
78
56
|
## Built by Orches
|
|
79
57
|
|
package/package.json
CHANGED
package/src/validate.js
CHANGED
|
@@ -4,7 +4,7 @@ const path = require('path');
|
|
|
4
4
|
function readManifest() {
|
|
5
5
|
const manifestPath = path.join(process.cwd(), 'env.manifest.json');
|
|
6
6
|
if (!fs.existsSync(manifestPath)) {
|
|
7
|
-
console.log('
|
|
7
|
+
console.log('\nNo env.manifest.json found. Run safelaunch init first.\n');
|
|
8
8
|
process.exit(1);
|
|
9
9
|
}
|
|
10
10
|
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
@@ -13,83 +13,162 @@ function readManifest() {
|
|
|
13
13
|
function readEnv() {
|
|
14
14
|
const envPath = path.join(process.cwd(), '.env');
|
|
15
15
|
if (!fs.existsSync(envPath)) {
|
|
16
|
-
|
|
16
|
+
console.log('\nNo .env file found.\n');
|
|
17
|
+
process.exit(1);
|
|
17
18
|
}
|
|
19
|
+
const envVars = {};
|
|
20
|
+
const duplicates = [];
|
|
21
|
+
const seen = new Set();
|
|
18
22
|
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
25
|
+
if (match) {
|
|
26
|
+
const key = match[1].trim();
|
|
27
|
+
if (seen.has(key)) duplicates.push(key);
|
|
28
|
+
seen.add(key);
|
|
29
|
+
envVars[key] = match[2].trim();
|
|
24
30
|
}
|
|
25
|
-
}
|
|
26
|
-
return
|
|
31
|
+
}
|
|
32
|
+
return { envVars, duplicates };
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
function readEnvExample() {
|
|
36
|
+
const envExamplePath = path.join(process.cwd(), '.env.example');
|
|
37
|
+
if (!fs.existsSync(envExamplePath)) return null;
|
|
38
|
+
const envVars = {};
|
|
39
|
+
const lines = fs.readFileSync(envExamplePath, 'utf8').split('\n');
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
42
|
+
if (match) envVars[match[1].trim()] = match[2].trim();
|
|
32
43
|
}
|
|
44
|
+
return envVars;
|
|
45
|
+
}
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
function checkRuntime(manifest) {
|
|
48
|
+
if (!manifest.runtime || !manifest.runtime.node) return null;
|
|
49
|
+
const required = String(manifest.runtime.node);
|
|
50
|
+
const actual = process.version.replace('v', '').split('.')[0];
|
|
51
|
+
if (required !== actual) return { required, actual };
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
function checkEnvExample(envVars) {
|
|
56
|
+
const example = readEnvExample();
|
|
57
|
+
if (!example) return [];
|
|
58
|
+
const missing = [];
|
|
59
|
+
for (const key of Object.keys(example)) {
|
|
60
|
+
if (!envVars[key]) missing.push(key);
|
|
42
61
|
}
|
|
62
|
+
return missing;
|
|
63
|
+
}
|
|
43
64
|
|
|
44
|
-
|
|
65
|
+
function checkDependencyDrift() {
|
|
66
|
+
const packagePath = path.join(process.cwd(), 'package.json');
|
|
67
|
+
const modulesPath = path.join(process.cwd(), 'node_modules');
|
|
68
|
+
if (!fs.existsSync(packagePath)) return null;
|
|
69
|
+
if (!fs.existsSync(modulesPath)) return { notInstalled: true, missing: [] };
|
|
70
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
71
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
72
|
+
const missing = [];
|
|
73
|
+
for (const dep of deps) {
|
|
74
|
+
const depPath = path.join(modulesPath, dep);
|
|
75
|
+
if (!fs.existsSync(depPath)) missing.push(dep);
|
|
76
|
+
}
|
|
77
|
+
return { notInstalled: false, missing };
|
|
45
78
|
}
|
|
46
79
|
|
|
47
80
|
function validate() {
|
|
48
|
-
console.log('\nRunning
|
|
81
|
+
console.log('\nRunning safelaunch...\n');
|
|
49
82
|
|
|
50
83
|
const manifest = readManifest();
|
|
51
|
-
const
|
|
52
|
-
const variables = manifest.variables;
|
|
84
|
+
const { envVars, duplicates } = readEnv();
|
|
53
85
|
|
|
54
86
|
const missing = [];
|
|
87
|
+
const empty = [];
|
|
55
88
|
const passing = [];
|
|
56
|
-
const runtimeMismatch = checkRuntime(manifest);
|
|
57
89
|
|
|
58
|
-
Object.
|
|
59
|
-
if (
|
|
90
|
+
for (const [key, val] of Object.entries(manifest.variables || {})) {
|
|
91
|
+
if (val.required && key in envVars && envVars[key] === '') {
|
|
92
|
+
empty.push(key);
|
|
93
|
+
} else if (val.required && !(key in envVars)) {
|
|
60
94
|
missing.push(key);
|
|
61
95
|
} else {
|
|
62
96
|
passing.push(key);
|
|
63
97
|
}
|
|
64
|
-
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const runtimeMismatch = checkRuntime(manifest);
|
|
101
|
+
const exampleMissing = checkEnvExample(envVars);
|
|
102
|
+
const drift = checkDependencyDrift();
|
|
65
103
|
|
|
66
104
|
if (runtimeMismatch) {
|
|
67
|
-
console.log('
|
|
68
|
-
console.log(
|
|
69
|
-
console.log(
|
|
105
|
+
console.log('⚠️ RUNTIME MISMATCH\n');
|
|
106
|
+
console.log(' Node required: ' + runtimeMismatch.required);
|
|
107
|
+
console.log(' Node actual: ' + runtimeMismatch.actual);
|
|
108
|
+
console.log('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (duplicates.length > 0) {
|
|
112
|
+
console.log('⚠️ DUPLICATE VARIABLES (' + duplicates.length + ' found)\n');
|
|
113
|
+
for (const key of duplicates) {
|
|
114
|
+
console.log(' ' + key + ' defined more than once in .env');
|
|
115
|
+
}
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (drift) {
|
|
120
|
+
if (drift.notInstalled) {
|
|
121
|
+
console.log('⚠️ DEPENDENCIES NOT INSTALLED\n');
|
|
122
|
+
console.log(' node_modules not found. Run npm install.\n');
|
|
123
|
+
} else if (drift.missing.length > 0) {
|
|
124
|
+
console.log('⚠️ DEPENDENCY DRIFT (' + drift.missing.length + ' found)\n');
|
|
125
|
+
for (const dep of drift.missing) {
|
|
126
|
+
console.log(' ' + dep + ' in package.json but not installed');
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (exampleMissing.length > 0) {
|
|
133
|
+
console.log('⚠️ MISSING FROM .env.example (' + exampleMissing.length + ' found)\n');
|
|
134
|
+
for (const key of exampleMissing) {
|
|
135
|
+
console.log(' ' + key + ' in .env.example but missing from .env');
|
|
136
|
+
}
|
|
137
|
+
console.log('');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (empty.length > 0) {
|
|
141
|
+
console.log('❌ EMPTY VARIABLES (' + empty.length + ' found)\n');
|
|
142
|
+
for (const key of empty) {
|
|
143
|
+
console.log(' ' + key + ' required but empty in .env');
|
|
144
|
+
}
|
|
145
|
+
console.log('');
|
|
70
146
|
}
|
|
71
147
|
|
|
72
148
|
if (missing.length > 0) {
|
|
73
|
-
console.log(
|
|
74
|
-
|
|
75
|
-
console.log(
|
|
76
|
-
}
|
|
149
|
+
console.log('❌ MISSING VARIABLES (' + missing.length + ' found)\n');
|
|
150
|
+
for (const key of missing) {
|
|
151
|
+
console.log(' ' + key + ' required but missing from .env');
|
|
152
|
+
}
|
|
153
|
+
console.log('');
|
|
77
154
|
}
|
|
78
155
|
|
|
79
156
|
if (passing.length > 0) {
|
|
80
|
-
console.log(
|
|
81
|
-
|
|
82
|
-
console.log(
|
|
83
|
-
}
|
|
157
|
+
console.log('✅ PASSING (' + passing.length + ')\n');
|
|
158
|
+
for (const key of passing) {
|
|
159
|
+
console.log(' ' + key + ' present');
|
|
160
|
+
}
|
|
161
|
+
console.log('');
|
|
84
162
|
}
|
|
85
163
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
164
|
+
const hasFailed = runtimeMismatch || missing.length > 0 || empty.length > 0 || duplicates.length > 0 || exampleMissing.length > 0 || (drift && (drift.notInstalled || drift.missing.length > 0));
|
|
165
|
+
|
|
166
|
+
if (hasFailed) {
|
|
167
|
+
console.log('Your environment is not ready for production.\n');
|
|
89
168
|
process.exit(1);
|
|
90
169
|
} else {
|
|
91
|
-
console.log('
|
|
170
|
+
console.log('Your environment is ready for production.\n');
|
|
92
171
|
}
|
|
93
172
|
}
|
|
94
173
|
|
|
95
|
-
validate();
|
|
174
|
+
validate();
|