From 5f08d275d3a336acd90cb8b3ed27cdac2e627979 Mon Sep 17 00:00:00 2001 From: James Henderson Date: Fri, 29 May 2026 16:42:02 +0100 Subject: [PATCH 1/3] chore: update Claude Code section of docs explaining how to invoke the skill --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d934c3..ee4edb5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Supports **npm**, **pnpm**, and **yarn**. ``` Or symlink / marketplace when listed. 2. Open your **app** folder in your editor of choice (not this repo). -3. Invoke **`@godaddy-nodejs-hosting`** and ask it to prepare the project for Node.js Hosting (build or adapt). +3. Invoke **`/godaddy-nodejs-hosting`** (Claude) or **`@godaddy-nodejs-hosting`** (Cursor) and ask it to prepare the project for Node.js Hosting (build or adapt). 4. Validate before upload (optional; requires Node.js): ```bash npm run validate -- /path/to/your-app From 5d09aba1c04c6d9fb0339b9970abbc8730b189f9 Mon Sep 17 00:00:00 2001 From: James Henderson Date: Fri, 29 May 2026 16:43:31 +0100 Subject: [PATCH 2/3] refactor: enhance skill's knowledge of Node.js Hosting ecosystem (managed DB integration, code sources, ports --- AGENTS.md | 12 ++- docs/CONTRIBUTING-SKILL.md | 2 +- skills/godaddy-nodejs-hosting/SKILL.md | 32 ++++++-- skills/godaddy-nodejs-hosting/contract.md | 8 +- skills/godaddy-nodejs-hosting/examples.md | 35 ++++++++ .../scripts/validate-paas.mjs | 79 ++++++++++++++++++- .../godaddy-nodejs-hosting/troubleshooting.md | 2 +- tests/drift-check.test.mjs | 2 +- .../bad-mysql-missing-env/package.json | 13 +++ .../fixtures/bad-mysql-missing-env/server.js | 20 +++++ tests/fixtures/mysql-good/package.json | 13 +++ tests/fixtures/mysql-good/server.js | 20 +++++ tests/validate-paas.test.mjs | 24 +++++- 13 files changed, 244 insertions(+), 18 deletions(-) create mode 100644 tests/fixtures/bad-mysql-missing-env/package.json create mode 100644 tests/fixtures/bad-mysql-missing-env/server.js create mode 100644 tests/fixtures/mysql-good/package.json create mode 100644 tests/fixtures/mysql-good/server.js diff --git a/AGENTS.md b/AGENTS.md index 3be195a..cf50219 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,15 +4,23 @@ This project is built to deploy on Node.js Hosting, a managed Node.js hosting pl ## Platform Overview -Node.js Hosting is a managed Node.js PaaS that supports Node.js applications and static sites. Customers upload their project folder through the GoDaddy interface — no Docker, no CI/CD pipelines, no infrastructure config needed. The platform handles SSL, CDN, and server-side compute automatically. +Node.js Hosting is a managed Node.js PaaS that supports Node.js applications and static sites. Customers deploy by **uploading a zip** or **connecting a Git repository** in the GoDaddy interface — no Docker, no CI/CD pipelines, no infrastructure config needed. The platform handles SSL, CDN, and server-side compute automatically. + +A local `.git` folder does **not** by itself mean the customer uses Git deploy (zip exports often include `.git`). To avoid confusing the customer, we shouldn't make any references to uploading a zip or pushing to a remote Git in messages to the customer unless the customer explicitly asks about that. ## Deployment Flow -1. Customer uploads their project folder via the Node.js Hosting UI +1. Customer provides the app via **zip upload** or **Git sync** in the Node.js Hosting UI 2. The platform installs dependencies and builds the app 3. The app is deployed to a private preview environment (requires GoDaddy auth to view) 4. Once ready, the customer can publish to production and connect a custom domain +**Zip upload:** zip the project root, exclude irrelevant files (`node_modules`, build caches, and `.git`), and include a lockfile when possible. + +**Git sync:** connect the remote repository, commit the lockfile, do not commit `node_modules` or `.env`. + +When guiding a customer to publish changes, describe **both** Git sync and zip upload unless they’ve confirmed which they use. + ## Requirements Audit the project against each item below. Apply the **minimum** changes needed; do not remove existing functionality. diff --git a/docs/CONTRIBUTING-SKILL.md b/docs/CONTRIBUTING-SKILL.md index 767eaf8..a3bb3e8 100644 --- a/docs/CONTRIBUTING-SKILL.md +++ b/docs/CONTRIBUTING-SKILL.md @@ -25,7 +25,7 @@ These files must stay aligned: **One PR** should update all affected files when changing deploy rules. -Bump `contractVersion` in `contract.md` for breaking contract changes (current: **2** — requires `main`, always requires `build`). +Bump `contractVersion` in `contract.md` for breaking contract changes (current: **3** — requires `main`, always requires `build`, MySQL E009/E010). ## Adding a framework diff --git a/skills/godaddy-nodejs-hosting/SKILL.md b/skills/godaddy-nodejs-hosting/SKILL.md index 0fb76d9..97fca2e 100644 --- a/skills/godaddy-nodejs-hosting/SKILL.md +++ b/skills/godaddy-nodejs-hosting/SKILL.md @@ -21,6 +21,7 @@ Rules: [contract.md](contract.md). Recipes: [examples.md](examples.md). Errors: | New app / scaffold / greenfield | [New app workflow](#new-app-workflow) | | Existing repo / deploy failed / make this work on hosting | [Adapting an existing app](#adapting-an-existing-app) | | AI export (Lovable, Replit, Bolt, etc.) | [Adapting an existing app](#adapting-an-existing-app) + [AI export quick fixes](#ai-export-quick-fixes) | +| Uses MySQL / database on platform | [Managed MySQL](#managed-mysql) | ## Audience routing @@ -29,7 +30,7 @@ Rules: [contract.md](contract.md). Recipes: [examples.md](examples.md). Errors: 1. Output the [pre-upload checklist](#pre-upload-checklist) first in plain language. 2. Follow [Adapting an existing app](#adapting-an-existing-app) (existing zip or export). 3. Prefer the [static SPA + Express](examples.md#vite-react-vue-spa) or [static only](examples.md#static-only) recipe when there is no server. -4. One small change at a time; explain what to zip and upload. +4. One small change at a time; explain the next hosting step in plain language (Git sync or zip upload in the UI). 5. Run the validator before saying the app is ready. **Technical user - existing app**: follow [Adapting an existing app](#adapting-an-existing-app); use the matching framework recipe for `start`/`build` only; do not re-explaining basics. @@ -46,6 +47,21 @@ Rules: [contract.md](contract.md). Recipes: [examples.md](examples.md). Errors: - One app per upload; lockfile when possible - Outbound HTTP/HTTPS only; platform MySQL via `DB_*` env + `mysql2` if using a database ([contract.md](contract.md)) +## Publishing updates (customer communication) + +The platform supports **Git repository sync** and **zip upload** in the Node.js Hosting UI. See [AGENTS.md](../../AGENTS.md) Deployment Flow for agent context. + +- **Do not infer** deploy method from `.git`, lockfiles, or export origin (a zip export may include `.git` from local development). +- **Default when telling the user how to publish:** use neutral wording — e.g. *“When you’re ready, update the app in Node.js Hosting using Git sync or a zip upload in the hosting UI.”* +- **Only go path-specific** (zip steps vs connect/push Git) if the user **states or asks** which they use. +- **Avoid** prescriptive one-path phrases like “upload a new zip” or “push to your remote” without that confirmation. + +## Managed MySQL + +Optional platform MySQL injects `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`. The app must read each from `process.env` (not hard-coded). Use **`mysql2`** in `dependencies` and parameterized queries. + +Recipe: [examples.md#managed-mysql](examples.md#managed-mysql). Validator: **E009**, **E010** when a DB client or Prisma MySQL provider is detected. + ## Adapting an existing app Use this path when the user already has a project (local repo, zip export, or app that failed on Node.js Hosting). Goal: **hosting compatibility only** — not new features, refactors, or framework changes. @@ -93,7 +109,9 @@ Symptom detail: [troubleshooting.md](troubleshooting.md). | E008 | Add `main` pointing at an existing entry file | | W001 | Remove `.env` from upload; use hosting UI for env vars | | W002 | Optional: add `engines.node` | -| W004 | Remove `node_modules` from upload zip | +| W004 | Exclude `node_modules` from deployment (zip or Git) | +| E009 | Add `mysql2` to `dependencies` | +| E010 | Read each `DB_*` from `process.env` (see [managed-mysql](examples.md#managed-mysql)) | **Special cases** @@ -103,7 +121,7 @@ Symptom detail: [troubleshooting.md](troubleshooting.md). For Replit, Lovable, and Bolt patterns, see [AI export quick fixes](#ai-export-quick-fixes) after applying this workflow. -Adaptation is complete under the same [done criteria](#done-criteria) as a new app (validator `0`, checklist, zip the project root). +Adaptation is complete under the same [done criteria](#done-criteria) as a new app (validator `0`, checklist, ready to update in Node.js Hosting). ## New app workflow @@ -150,11 +168,11 @@ For Lovable/Bolt static hosting, use the [vite-react-vue-spa](examples.md#vite-r ## Pre-upload checklist - [ ] `package.json`: `name`, `version`, `main`, `build`, `start` -- [ ] Production deps in `dependencies`; no `node_modules` in zip +- [ ] Production deps in `dependencies`; do not deploy `node_modules` (exclude from zip or Git) - [ ] `process.env.PORT` for listening; secrets via `process.env` -- [ ] Zip under 100 MB +- [ ] Deployment artifact under 100 MB (zip upload) - [ ] Runs locally: install → build → start -- [ ] Lockfile in zip; validator exit 0; no `.env` +- [ ] Lockfile present (committed or included in upload); validator exit 0; no `.env` - [ ] HTTP/HTTPS outbound only; DB uses `DB_*` + `mysql2` if applicable ## Done criteria @@ -163,4 +181,4 @@ Task is complete only when: 1. `validate-paas.mjs` exits `0` on the project directory. 2. Pre-upload checklist is satisfied. -3. User knows to zip the **project root** (single app folder) for upload. +3. User knows they can publish/update the app in Node.js Hosting (Git sync or zip upload in the UI). Mention only the path they use if they’ve already said so. diff --git a/skills/godaddy-nodejs-hosting/contract.md b/skills/godaddy-nodejs-hosting/contract.md index 87903c5..e2d4650 100644 --- a/skills/godaddy-nodejs-hosting/contract.md +++ b/skills/godaddy-nodejs-hosting/contract.md @@ -1,6 +1,6 @@ # Node.js Hosting deploy contract -contractVersion: 2 +contractVersion: 3 Testable rules for apps deployed on GoDaddy Node.js Hosting (PaaS). Enforced by [scripts/validate-paas.mjs](scripts/validate-paas.mjs) where noted. @@ -27,7 +27,7 @@ Testable rules for apps deployed on GoDaddy Node.js Hosting (PaaS). Enforced by | C9 | Vite: platform updates allowed-hosts; dev/preview must respect `PORT`. | — | | C10 | Upload zip under 100 MB; exclude `node_modules`, caches, large artifacts. | W004 | | C11 | Outbound: HTTP (80), HTTPS (443), and GoDaddy managed MySQL only. | — | -| C12 | If using DB: read `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` from env; use `mysql2` and parameterized queries. External DBs on non-80/443 are not reachable. | — | +| C12 | If using DB: read `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` from `process.env`; use `mysql2` in `dependencies` and parameterized queries. External DBs on non-80/443 are not reachable. | E009, E010 | ## Framework build hints @@ -63,4 +63,6 @@ These must not appear only in `devDependencies` if required at runtime: | W001 | `.env` file present in project | | W002 | Missing `engines.node` | | W003 | Could not verify start script target | -| W004 | `node_modules` present (exclude from zip) | +| W004 | `node_modules` present (exclude from deployment) | +| E009 | Database use without `mysql2` in `dependencies` | +| E010 | Missing `process.env` reference for a required `DB_*` variable | diff --git a/skills/godaddy-nodejs-hosting/examples.md b/skills/godaddy-nodejs-hosting/examples.md index 4385c0f..e8a744d 100644 --- a/skills/godaddy-nodejs-hosting/examples.md +++ b/skills/godaddy-nodejs-hosting/examples.md @@ -264,3 +264,38 @@ app.get('*', (req, res) => { }); app.listen(port); ``` + +--- + +## managed-mysql + +**Detection:** `mysql2`, `sequelize`, `knex`, `typeorm`, or `mariadb` in dependencies; or Prisma `provider = "mysql"` in `prisma/schema.prisma`. + +**Platform:** Since each Node.js Hosting app gets its own MySQL capacity automatically, the platform sets `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, and `DB_PASSWORD`. Read **each** from `process.env` in application code (typically one config module). + +**package.json:** + +```json +{ + "dependencies": { + "express": "^4.18.0", + "mysql2": "^3.11.0" + } +} +``` + +**db.js (all five vars from env):** + +```javascript +const mysql = require('mysql2/promise'); + +module.exports = mysql.createPool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, +}); +``` + +Use **parameterized** queries (`pool.query('SELECT * FROM users WHERE id = ?', [id])`). Do not interpolate user input into SQL strings. diff --git a/skills/godaddy-nodejs-hosting/scripts/validate-paas.mjs b/skills/godaddy-nodejs-hosting/scripts/validate-paas.mjs index b6493e5..90ef22a 100644 --- a/skills/godaddy-nodejs-hosting/scripts/validate-paas.mjs +++ b/skills/godaddy-nodejs-hosting/scripts/validate-paas.mjs @@ -62,6 +62,16 @@ const SKIP_DIRS = new Set([ 'coverage', ]); +const DB_ENV_VARS = ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD']; + +const MYSQL_CLIENT_PACKAGES = [ + 'mysql2', + 'sequelize', + 'knex', + 'typeorm', + 'mariadb', +]; + const errors = []; const warnings = []; @@ -267,7 +277,73 @@ function checkPackageMetadata(pkg, projectRoot) { function checkUploadHygiene(projectRoot) { const nm = pathUnderProjectRoot(projectRoot, 'node_modules'); if (nm && existsSync(nm)) { - warn('W004', 'node_modules present — exclude from upload zip (platform runs install)'); + warn( + 'W004', + 'node_modules present — exclude from deployment (zip or Git); platform runs install', + ); + } +} + +export function envVarReferencedInContent(content, varName) { + const patterns = [ + new RegExp(`process\\.env\\.${varName}\\b`), + new RegExp(`process\\.env\\?\\.${varName}\\b`), + new RegExp(`process\\.env\\[\\s*['"]${varName}['"]\\s*\\]`), + ]; + if (patterns.some((re) => re.test(content))) return true; + return new RegExp(`\\b${varName}\\b[^=]*=\\s*process\\.env`).test(content); +} + +function envVarReferencedInFiles(files, varName) { + for (const file of files) { + let content; + try { + content = readFileSync(file, 'utf8'); + } catch { + continue; + } + if (envVarReferencedInContent(content, varName)) return true; + } + return false; +} + +function usesMysqlFromPrisma(projectRoot) { + const schemaPath = pathUnderProjectRoot(projectRoot, 'prisma', 'schema.prisma'); + if (!schemaPath || !existsSync(schemaPath)) return false; + try { + const schema = readFileSync(schemaPath, 'utf8'); + return /provider\s*=\s*["']mysql["']/i.test(schema); + } catch { + return false; + } +} + +export function usesDatabase(deps, projectRoot) { + if (MYSQL_CLIENT_PACKAGES.some((name) => deps[name])) return true; + return usesMysqlFromPrisma(projectRoot); +} + +function checkDatabaseConfig(projectRoot, pkg) { + const deps = allDeps(pkg); + if (!usesDatabase(deps, projectRoot)) return; + + const runtimeDeps = pkg.dependencies || {}; + if (!runtimeDeps.mysql2) { + if (deps.mysql2) { + err('E009', 'mysql2 is only in devDependencies — move to dependencies for production'); + } else { + err('E009', 'Database client detected — add mysql2 to dependencies for platform MySQL'); + } + } + + const files = walkSourceFiles(projectRoot); + for (const varName of DB_ENV_VARS) { + if (!envVarReferencedInFiles(files, varName)) { + err( + 'E010', + `Managed MySQL requires process.env.${varName} (set by platform when DB is enabled)`, + ); + } } } @@ -353,6 +429,7 @@ function validateProject(projectRoot) { checkBuildScript(pkg, deps); checkRuntimeDeps(pkg); checkHardcodedPorts(projectRoot); + checkDatabaseConfig(projectRoot, pkg); const envPath = pathUnderProjectRoot(projectRoot, '.env'); if (envPath && existsSync(envPath)) { diff --git a/skills/godaddy-nodejs-hosting/troubleshooting.md b/skills/godaddy-nodejs-hosting/troubleshooting.md index d017cf5..f47dbad 100644 --- a/skills/godaddy-nodejs-hosting/troubleshooting.md +++ b/skills/godaddy-nodejs-hosting/troubleshooting.md @@ -14,7 +14,7 @@ Map symptoms to [contract.md](contract.md) rules and validator IDs. | Secrets leaked | C8 | W001 | Remove `.env` from upload; use hosting UI for env vars | | Monorepo upload fails | C1 | — | Upload single package folder only | | External API unreachable | C11 | — | Use HTTP/HTTPS (80/443) only | -| Database connection fails | C12 | — | Use platform MySQL `DB_*` env vars and `mysql2`; not remote :3306 | +| Database connection fails | C12 | E009, E010 | Use platform MySQL: all five `DB_*` from `process.env`, `mysql2` in `dependencies`; not remote :3306 | ## Validator exit codes diff --git a/tests/drift-check.test.mjs b/tests/drift-check.test.mjs index 3e68ac6..a538330 100644 --- a/tests/drift-check.test.mjs +++ b/tests/drift-check.test.mjs @@ -23,7 +23,7 @@ const REQUIRED_IN_AGENTS = [ ]; const REQUIRED_IN_CONTRACT = [ - 'contractVersion: 2', + 'contractVersion: 3', 'process.env.PORT', 'scripts.start', 'scripts.build', diff --git a/tests/fixtures/bad-mysql-missing-env/package.json b/tests/fixtures/bad-mysql-missing-env/package.json new file mode 100644 index 0000000..70244d2 --- /dev/null +++ b/tests/fixtures/bad-mysql-missing-env/package.json @@ -0,0 +1,13 @@ +{ + "name": "bad-mysql-missing-env", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "build": "echo build", + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.0", + "mysql2": "^3.11.0" + } +} diff --git a/tests/fixtures/bad-mysql-missing-env/server.js b/tests/fixtures/bad-mysql-missing-env/server.js new file mode 100644 index 0000000..e62a836 --- /dev/null +++ b/tests/fixtures/bad-mysql-missing-env/server.js @@ -0,0 +1,20 @@ +const express = require('express'); +const mysql = require('mysql2/promise'); + +const app = express(); +const port = process.env.PORT || 3000; + +const pool = mysql.createPool({ + host: process.env.DB_HOST, + port: 3306, + database: 'app', + user: 'root', + password: 'secret', +}); + +app.get('/', async (req, res) => { + const [rows] = await pool.query('SELECT 1 AS ok'); + res.json(rows[0]); +}); + +app.listen(port, () => console.log(`Listening on ${port}`)); diff --git a/tests/fixtures/mysql-good/package.json b/tests/fixtures/mysql-good/package.json new file mode 100644 index 0000000..8013f64 --- /dev/null +++ b/tests/fixtures/mysql-good/package.json @@ -0,0 +1,13 @@ +{ + "name": "mysql-good", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "build": "echo build", + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.0", + "mysql2": "^3.11.0" + } +} diff --git a/tests/fixtures/mysql-good/server.js b/tests/fixtures/mysql-good/server.js new file mode 100644 index 0000000..0799c04 --- /dev/null +++ b/tests/fixtures/mysql-good/server.js @@ -0,0 +1,20 @@ +const express = require('express'); +const mysql = require('mysql2/promise'); + +const app = express(); +const port = process.env.PORT || 3000; + +const pool = mysql.createPool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, +}); + +app.get('/', async (req, res) => { + const [rows] = await pool.query('SELECT 1 AS ok'); + res.json(rows[0]); +}); + +app.listen(port, () => console.log(`Listening on ${port}`)); diff --git a/tests/validate-paas.test.mjs b/tests/validate-paas.test.mjs index 4815346..03d807a 100644 --- a/tests/validate-paas.test.mjs +++ b/tests/validate-paas.test.mjs @@ -3,6 +3,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { parseNodeStart, + envVarReferencedInContent, runValidation, } from '../skills/godaddy-nodejs-hosting/scripts/validate-paas.mjs'; @@ -44,8 +45,9 @@ test('express-node-require fixture warns W003 instead of false E003', () => { }); test('next fixture passes', () => { - const { exitCode } = runValidation([fixtureDir('next')]); - assert.equal(exitCode, 0); + const { exitCode, errors } = runValidation([fixtureDir('next')]); + assert.equal(errors.length, 0); + assert.ok(exitCode === 0 || exitCode === 2, `exit ${exitCode}`); }); test('static-vite fixture passes', () => { @@ -104,6 +106,24 @@ test('missing directory exits 1', () => { assert.equal(exitCode, 1); }); +test('envVarReferencedInContent detects process.env.DB_HOST', () => { + assert.ok(envVarReferencedInContent('const h = process.env.DB_HOST;', 'DB_HOST')); + assert.ok(envVarReferencedInContent("const h = process.env['DB_HOST'];", 'DB_HOST')); + assert.equal(envVarReferencedInContent('const h = "localhost";', 'DB_HOST'), false); +}); + +test('mysql-good fixture passes', () => { + const { exitCode, errors } = runValidation([fixtureDir('mysql-good')]); + assert.equal(errors.length, 0); + assert.ok(exitCode === 0 || exitCode === 2); +}); + +test('bad-mysql-missing-env fails with E010', () => { + const { exitCode, errors } = runValidation([fixtureDir('bad-mysql-missing-env')]); + assert.equal(exitCode, 1); + assert.ok(errors.some((e) => e.id === 'E010')); +}); + test('runValidation returns independent error snapshots per call', () => { const first = runValidation([fixtureDir('bad-hardcoded-port')]); const second = runValidation([fixtureDir('express')]); From 37e802d21c55381bd73a721d72f072aafc0cf74a Mon Sep 17 00:00:00 2001 From: James Henderson Date: Fri, 29 May 2026 16:45:57 +0100 Subject: [PATCH 3/3] docs: update AGENTS.md title --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index cf50219..8d3e451 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# CLAUDE.md — Node.js Hosting +# AGENTS.md — Node.js Hosting This project is built to deploy on Node.js Hosting, a managed Node.js hosting platform. Use this file as context when helping build, debug, or prepare this app for deployment.