* Lint an integrations env yaml file directly (without a .deepnote notebook). * Validates the file structure, integration schemas, and env var references.
(filePath: string)
| 248 | * Validates the file structure, integration schemas, and env var references. |
| 249 | */ |
| 250 | async function lintIntegrationsFile(filePath: string): Promise<LintFileResult> { |
| 251 | const absolutePath = resolve(process.cwd(), filePath) |
| 252 | |
| 253 | // Verify the file exists |
| 254 | try { |
| 255 | await fs.stat(absolutePath) |
| 256 | } catch (error) { |
| 257 | if (isErrnoENOENT(error)) { |
| 258 | throw new FileResolutionError(`File not found: ${absolutePath}`) |
| 259 | } |
| 260 | throw error |
| 261 | } |
| 262 | |
| 263 | const fileDir = dirname(absolutePath) |
| 264 | |
| 265 | // Loading `.env` mutates `process.env`; restore it afterwards so a lint run is idempotent and |
| 266 | // doesn't clobber caller-provided env values (see withRestoredEnv). |
| 267 | return withRestoredEnv(async () => { |
| 268 | // Load .env file so env: references in integrations can be resolved |
| 269 | dotenv.config({ path: join(fileDir, DEFAULT_ENV_FILE), quiet: true }) |
| 270 | |
| 271 | debug(`Linting integrations file: ${absolutePath}`) |
| 272 | const parsedIntegrations = await parseIntegrationsFile(absolutePath) |
| 273 | |
| 274 | debug(`Parsed ${parsedIntegrations.integrations.length} integrations, ${parsedIntegrations.issues.length} issues`) |
| 275 | |
| 276 | // Also run env var generation so schema-valid-but-impossible to generate integrations (e.g. invalid |
| 277 | // BigQuery/Spanner service_account JSON) are caught here too, not just on the .deepnote path. |
| 278 | const integrationsIssues = [...parsedIntegrations.issues] |
| 279 | if (parsedIntegrations.integrations.length > 0) { |
| 280 | const { issues } = generateIntegrationEnvVars(parsedIntegrations.integrations, fileDir) |
| 281 | integrationsIssues.push(...issues) |
| 282 | } |
| 283 | |
| 284 | const hasErrors = integrationsIssues.length > 0 |
| 285 | |
| 286 | // Integration/config issues are hard errors (no severity field), so report them as errors in |
| 287 | // `issueCount` (there are no notebook issues on this path) to keep it consistent with `success` |
| 288 | // and `integrationsFile.issues`. |
| 289 | return { |
| 290 | path: absolutePath, |
| 291 | success: !hasErrors, |
| 292 | issueCount: { errors: integrationsIssues.length, warnings: 0, total: integrationsIssues.length }, |
| 293 | issues: [], |
| 294 | // Emit empty integrations/inputs so the JSON shape matches the .deepnote path. There is no |
| 295 | // notebook on this path, so both are always empty — but keeping the keys present saves machine |
| 296 | // consumers from having to special-case the direct-YAML output. |
| 297 | integrations: { configured: [], missing: [] }, |
| 298 | inputs: { total: 0, withValues: 0, needingValues: [] }, |
| 299 | integrationsFile: { |
| 300 | path: absolutePath, |
| 301 | integrationCount: parsedIntegrations.integrations.length, |
| 302 | issues: integrationsIssues, |
| 303 | }, |
| 304 | } |
| 305 | }) |
| 306 | } |
| 307 |
no test coverage detected