• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

OneBusAway / wayfinder / 22210548225

20 Feb 2026 03:51AM UTC coverage: 79.59% (+1.1%) from 78.449%
22210548225

Pull #346

github

web-flow
Merge 067a2fce8 into b5e7a242f
Pull Request #346: Prep 2026.2 Release

1685 of 1861 branches covered (90.54%)

Branch coverage included in aggregate %.

1106 of 1442 new or added lines in 33 files covered. (76.7%)

3 existing lines in 3 files now uncovered.

10739 of 13749 relevant lines covered (78.11%)

4.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/scripts/validate-env.js
NEW
1
import { readFileSync } from 'node:fs';
×
NEW
2
import { resolve, dirname } from 'node:path';
×
NEW
3
import { fileURLToPath } from 'node:url';
×
NEW
4
import dotenv from 'dotenv';
×
NEW
5

×
NEW
6
const __dirname = dirname(fileURLToPath(import.meta.url));
×
NEW
7
const root = resolve(__dirname, '..');
×
NEW
8

×
NEW
9
const envPath = process.argv[2] ? resolve(process.argv[2]) : resolve(root, '.env');
×
NEW
10
const schemaPath = resolve(root, 'env-schema.json');
×
NEW
11

×
NEW
12
// Load schema
×
NEW
13
let schema;
×
NEW
14
try {
×
NEW
15
        schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
×
NEW
16
} catch (err) {
×
NEW
17
        console.error(`Failed to load schema from ${schemaPath}: ${err.message}`);
×
NEW
18
        process.exit(1);
×
NEW
19
}
×
NEW
20

×
NEW
21
// Load .env file
×
NEW
22
let envContent;
×
NEW
23
try {
×
NEW
24
        envContent = readFileSync(envPath, 'utf-8');
×
NEW
25
} catch (err) {
×
NEW
26
        console.error(`Failed to read env file at ${envPath}: ${err.message}`);
×
NEW
27
        process.exit(1);
×
NEW
28
}
×
NEW
29

×
NEW
30
const parsed = dotenv.parse(envContent);
×
NEW
31

×
NEW
32
// Build a map of raw lines so we can detect values eaten by dotenv's # comment parsing
×
NEW
33
const rawLines = new Map();
×
NEW
34
for (const line of envContent.split('\n')) {
×
NEW
35
        const match = line.match(/^\s*([A-Za-z_]\w*)\s*=/);
×
NEW
36
        if (match) rawLines.set(match[1], line);
×
NEW
37
}
×
NEW
38

×
NEW
39
const errors = [];
×
NEW
40
const warnings = [];
×
NEW
41

×
NEW
42
// Validate each schema entry
×
NEW
43
for (const [name, rule] of Object.entries(schema)) {
×
NEW
44
        const value = parsed[name];
×
NEW
45
        const isPresent = name in parsed;
×
NEW
46

×
NEW
47
        // Check required
×
NEW
48
        if (rule.required && !isPresent) {
×
NEW
49
                errors.push(`${name}: missing (required)`);
×
NEW
50
                continue;
×
NEW
51
        }
×
NEW
52

×
NEW
53
        if (!isPresent) continue;
×
NEW
54

×
NEW
55
        // Check empty values
×
NEW
56
        if (value === '') {
×
NEW
57
                if (rule.allowEmpty) continue;
×
NEW
58

×
NEW
59
                // Detect unquoted hex colors that dotenv treated as comments
×
NEW
60
                const raw = rawLines.get(name) || '';
×
NEW
61
                if (rule.type === 'color' && raw.includes('#')) {
×
NEW
62
                        errors.push(`${name}: hex color values must be quoted (e.g., "#78aa36")`);
×
NEW
63
                } else if (rule.required) {
×
NEW
64
                        errors.push(`${name}: empty value (required)`);
×
NEW
65
                } else {
×
NEW
66
                        warnings.push(`${name}: present but empty (set allowEmpty or remove)`);
×
NEW
67
                }
×
NEW
68
                continue;
×
NEW
69
        }
×
NEW
70

×
NEW
71
        // Type-specific validation
×
NEW
72
        switch (rule.type) {
×
NEW
73
                case 'url':
×
NEW
74
                        try {
×
NEW
75
                                new URL(value);
×
NEW
76
                        } catch {
×
NEW
77
                                errors.push(`${name}: invalid URL "${value}"`);
×
NEW
78
                        }
×
NEW
79
                        break;
×
NEW
80

×
NEW
81
                case 'number': {
×
NEW
82
                        const num = Number(value);
×
NEW
83
                        if (isNaN(num)) {
×
NEW
84
                                errors.push(`${name}: not a valid number "${value}"`);
×
NEW
85
                        } else {
×
NEW
86
                                if (rule.min !== undefined && num < rule.min) {
×
NEW
87
                                        errors.push(`${name}: ${num} is below minimum ${rule.min}`);
×
NEW
88
                                }
×
NEW
89
                                if (rule.max !== undefined && num > rule.max) {
×
NEW
90
                                        errors.push(`${name}: ${num} is above maximum ${rule.max}`);
×
NEW
91
                                }
×
NEW
92
                        }
×
NEW
93
                        break;
×
NEW
94
                }
×
NEW
95

×
NEW
96
                case 'boolean':
×
NEW
97
                        if (value !== 'true' && value !== 'false') {
×
NEW
98
                                errors.push(`${name}: expected "true" or "false", got "${value}"`);
×
NEW
99
                        }
×
NEW
100
                        break;
×
NEW
101

×
NEW
102
                case 'enum':
×
NEW
103
                        if (rule.enum && !rule.enum.includes(value)) {
×
NEW
104
                                errors.push(`${name}: "${value}" is not one of [${rule.enum.join(', ')}]`);
×
NEW
105
                        }
×
NEW
106
                        break;
×
NEW
107

×
NEW
108
                case 'color':
×
NEW
109
                        if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value)) {
×
NEW
110
                                errors.push(`${name}: invalid hex color "${value}" (expected #RGB, #RRGGBB, or #RRGGBBAA)`);
×
NEW
111
                        }
×
NEW
112
                        break;
×
NEW
113

×
NEW
114
                case 'json':
×
NEW
115
                        try {
×
NEW
116
                                JSON.parse(value);
×
NEW
117
                        } catch {
×
NEW
118
                                errors.push(`${name}: invalid JSON — ${value}`);
×
NEW
119
                        }
×
NEW
120
                        break;
×
NEW
121

×
NEW
122
                case 'string':
×
NEW
123
                        break;
×
NEW
124

×
NEW
125
                default:
×
NEW
126
                        warnings.push(`${name}: unknown type "${rule.type}" in schema`);
×
NEW
127
                        break;
×
NEW
128
        }
×
NEW
129
}
×
NEW
130

×
NEW
131
// Warn on unknown variables
×
NEW
132
const schemaKeys = new Set(Object.keys(schema));
×
NEW
133
for (const name of Object.keys(parsed)) {
×
NEW
134
        if (!schemaKeys.has(name)) {
×
NEW
135
                warnings.push(`${name}: not defined in env-schema.json`);
×
NEW
136
        }
×
NEW
137
}
×
NEW
138

×
NEW
139
// Print results
×
NEW
140
if (errors.length === 0 && warnings.length === 0) {
×
NEW
141
        console.log(`\u2714 ${envPath}: all ${Object.keys(schema).length} variables validated`);
×
NEW
142
        process.exit(0);
×
NEW
143
}
×
NEW
144

×
NEW
145
if (warnings.length > 0) {
×
NEW
146
        console.log(`\nWarnings (${warnings.length}):`);
×
NEW
147
        for (const w of warnings) {
×
NEW
148
                console.log(`  \u26A0 ${w}`);
×
NEW
149
        }
×
NEW
150
}
×
NEW
151

×
NEW
152
if (errors.length > 0) {
×
NEW
153
        console.log(`\nErrors (${errors.length}):`);
×
NEW
154
        for (const e of errors) {
×
NEW
155
                console.log(`  \u2716 ${e}`);
×
NEW
156
        }
×
NEW
157

×
NEW
158
        const requiredCount = Object.values(schema).filter((r) => r.required).length;
×
NEW
159
        const missingCount = errors.filter((e) => e.endsWith('(required)')).length;
×
NEW
160
        if (missingCount >= requiredCount / 2) {
×
NEW
161
                console.log('\nHint: run `cp .env.example .env` and fill in your values.');
×
NEW
162
        }
×
NEW
163

×
NEW
164
        console.log('');
×
NEW
165
        process.exit(1);
×
NEW
166
}
×
NEW
167

×
NEW
168
console.log('');
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc