You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
269 lines
16 KiB
269 lines
16 KiB
# Rewrite @ansible package for npm publish — Implementation Plan
|
|
|
|
**Goal:** Implement a publishable npm-scoped example package at packages/@ansible/example, add guarded GitHub Actions publish and deploy workflows (publish on v* tags or manual_dispatch; deploy to motief.sgeboers.nl via user webapps), CI pack-tests, and documentation for embedding and deployment secrets.
|
|
|
|
**Design:** see thoughts/shared/designs/2026-03-28-rewrite-ansible-package-design.md
|
|
|
|
Author: Sven Geboers
|
|
|
|
---
|
|
|
|
## Dependency Graph
|
|
|
|
```
|
|
Batch 1 (parallel): 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7
|
|
Batch 2 (parallel): 2.1, 2.2, 2.3, 2.4 [depends on Batch 1]
|
|
```
|
|
|
|
---
|
|
|
|
## Batch 1: Foundation (parallel - independent files)
|
|
|
|
All tasks in this batch have NO dependencies and can be created in parallel.
|
|
|
|
### Task 1.1: package.json for package
|
|
**File:** `packages/@ansible/example/package.json`
|
|
**Summary:** New package.json implementing an npm-scoped package @ansible/example. Exact choices made:
|
|
- name: "@ansible/example"
|
|
- version: "0.1.0"
|
|
- author: "Sven Geboers"
|
|
- publishConfig.access: "public"
|
|
- files: ["src/**", "README.md", "package.json"] (controls published files, avoids .npmignore)
|
|
- scripts:
|
|
- test: `node tests/run.js`
|
|
- pack-inspect: `node tests/test_pack_inspect.js`
|
|
- prepublish:verify: `npm run pack-inspect` (keeps a named script for CI)
|
|
- pack: `npm pack`
|
|
**Tests to add:** none in this file; tests below will exercise package.json
|
|
**Verify:**
|
|
- Local: cd packages/@ansible/example && node -e "console.log(require('./package.json').name)" # should print @ansible/example
|
|
- Run package tests: cd packages/@ansible/example && npm test
|
|
**Effort:** 0.5h
|
|
|
|
### Task 1.2: README.md
|
|
**File:** `packages/@ansible/example/README.md`
|
|
**Summary:** Short README with package purpose, usage example, and author attribution line "Author: Sven Geboers". Include publish and deploy notes and reference to docs/*.md for deployment details.
|
|
**Tests to add:** none
|
|
**Verify:** open file or run: sed -n '1,40p' packages/@ansible/example/README.md
|
|
**Effort:** 0.5h
|
|
|
|
### Task 1.3: package entrypoint
|
|
**File:** `packages/@ansible/example/src/index.js`
|
|
**Summary:** Minimal, well-documented CommonJS module that exports a function used by tests. Keep runtime trivial (e.g., function hello(name){ return `hello ${name}` }). No dependencies.
|
|
**Tests to add:** used by unit test below
|
|
**Verify:** node -e "console.log(require('./packages/@ansible/example/src/index.js')('world'))"
|
|
**Effort:** 0.5h
|
|
|
|
### Task 1.4: unit test — package.json fields
|
|
**File:** `packages/@ansible/example/tests/test_package_json.js`
|
|
**Summary:** Node test that loads package.json and asserts required fields are present: name === "@ansible/example", version present, author === "Sven Geboers", publishConfig.access === "public", files array exists.
|
|
**Tests to add:** this is the test file
|
|
**Verify:** cd packages/@ansible/example && node tests/test_package_json.js (exit 0 on success)
|
|
**Effort:** 0.5h
|
|
|
|
### Task 1.5: pack-inspect test
|
|
**File:** `packages/@ansible/example/tests/test_pack_inspect.js`
|
|
**Summary:** Test that runs `npm pack` (in package dir) programmatically, captures the produced tarball name, asserts the tarball exists and contains package/package.json. Implementation notes: uses child_process.execSync and `tar -xOzf` to read package/package.json from the tarball and assert name/version match. This test requires `tar` on the runner (Linux/macOS). If `tar` not available, the test fails with clear message.
|
|
**Tests to add:** this is the test
|
|
**Verify:** cd packages/@ansible/example && node tests/test_pack_inspect.js
|
|
**Effort:** 1.0h
|
|
|
|
### Task 1.6: tiny package test runner
|
|
**File:** `packages/@ansible/example/tests/run.js`
|
|
**Summary:** Small node test harness that runs both test_package_json.js and test_pack_inspect.js, prints nice output, and returns non-zero on failure. Used by package.json test script to keep CI independent of external test runners.
|
|
**Tests to add:** none (this is the runner used by `npm test`)
|
|
**Verify:** cd packages/@ansible/example && node tests/run.js
|
|
**Effort:** 0.5h
|
|
|
|
### Task 1.7: pack-inspect helper (optional small script)
|
|
**File:** `packages/@ansible/example/tests/_pack_helpers.js`
|
|
**Summary:** Small utility used by test_pack_inspect.js to encapsulate npm pack and tar inspection logic. Keeps main test readable. (Kept internal/private to tests.)
|
|
**Tests to add:** none
|
|
**Verify:** run the tests which import it
|
|
**Effort:** 0.25h
|
|
|
|
---
|
|
|
|
## Batch 2: CI + Docs (parallel — depend on Batch 1)
|
|
|
|
All tasks in this batch depend on the package files being present (Batch 1).
|
|
|
|
### Task 2.1: GitHub Actions publish workflow
|
|
**File:** `.github/workflows/publish-ansible-example.yml`
|
|
**Summary:** New Actions workflow that performs build/test/pack/publish for packages/@ansible/example.
|
|
**Key behavior:**
|
|
- Triggers: push tags matching `v*` and workflow_dispatch (manual).
|
|
- Jobs:
|
|
- verify: runs on all triggers: checks out repo, sets up Node 18 (LTS) using actions/setup-node, installs root-level dependencies if any (skipped if none), then runs `cd packages/@ansible/example && npm ci || true` (guard), then `npm test`. Then runs `npm pack` and verifies produced tarball (reuses test script). The verify job always runs and must pass before publish.
|
|
- publish: runs only when `github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')` OR when manually triggered AND `secrets.NPM_TOKEN` exists. publish job is gated by `if: ${{ secrets.NPM_TOKEN != '' && ( github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') ) }}`.
|
|
- On publish: write ephemeral ~/.npmrc with `//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}` (use `echo` and file permissions 0600), run `npm publish --access public` from package directory, then securely delete ~/.npmrc (shred if available or overwrite and rm). Use Actions mask to avoid logging secrets.
|
|
|
|
**Secrets required:** NPM_TOKEN (string)
|
|
**Tests to add:** none (workflow file)
|
|
**Verify:**
|
|
- Locally simulate: cd packages/@ansible/example && npm test && npm pack
|
|
- In Actions: push a non-production tag on a test repository with a safe NPM_TOKEN that points to a test registry OR run workflow_dispatch with a dry-run branch (see dry-run below)
|
|
**Effort:** 1.5h
|
|
|
|
### Task 2.2: GitHub Actions deploy workflow
|
|
**File:** `.github/workflows/deploy-to-vps.yml`
|
|
**Summary:** Workflow to prepare and run deployment to external VPS motief.sgeboers.nl using user `webapps`. Triggers: push to main and workflow_dispatch. The job is non-destructive by default and will not run remote commands unless DEPLOY_SSH_KEY or DEPLOY_PASSWORD secrets are present. It includes a dry-run/verify job that checks SSH connectivity without executing commands that change state.
|
|
**Key behavior:**
|
|
- Inputs/secrets used (recommended names): DEPLOY_HOST (default motief.sgeboers.nl), DEPLOY_USER (default webapps), DEPLOY_SSH_PORT (default 22), DEPLOY_SSH_KEY (private key, optional), DEPLOY_PASSWORD (optional fallback), DEPLOY_PATH (path to deploy, optional)
|
|
- Jobs:
|
|
- dry-run-connect: attempts to verify connectivity. If DEPLOY_SSH_KEY present, create an ephemeral key file (0600), run `ssh -o BatchMode=yes -i $KEY -p $PORT $USER@$HOST 'echo connected'` and return success if the host echoes. This step is strictly a connection check (no file writes). If only DEPLOY_PASSWORD provided, the job will print instructions explaining the need for SSH key or use of a runner with sshpass (not recommended) and skip.
|
|
- deploy: guarded `if: steps.check_secrets.outputs.has_creds == 'true'` — only runs when credentials are present. Steps: checkout, optionally build artifacts, create ephemeral SSH key file, rsync or scp artifact to $USER@$HOST:$DEPLOY_PATH, run non-destructive remote commands (e.g., systemctl --user status <unit> or echo) only if DEPLOY_CONFIRM=true input is set. By default the job performs no destructive operations; it's expected operator sets inputs when ready.
|
|
|
|
**Secrets required (for actual deploy):** DEPLOY_SSH_KEY (preferred), OR DEPLOY_PASSWORD (less secure, not recommended)
|
|
**Secrets recommended / env:** DEPLOY_HOST (default motief.sgeboers.nl), DEPLOY_USER=webapps, DEPLOY_SSH_PORT=22, DEPLOY_PATH=/home/webapps/motief (example)
|
|
**Tests to add:** none (workflow file)
|
|
**Verify:**
|
|
- Run dry-run via workflow_dispatch on Actions to verify connectivity (without running deploy). Ensure secrets are set in repository settings.
|
|
**Effort:** 2.0h
|
|
|
|
### Task 2.3: docs — deployment and Drone note
|
|
**File:** `docs/deployment/ansible-package-deploy.md`
|
|
**Summary:** Documentation describing: default host motief.sgeboers.nl, recommended DEPLOY_USER webapps, how to add GitHub repo secrets (DEPLOY_SSH_KEY, DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_PORT), sample systemd unit name recommendation (e.g., service name `motief`), and instructions for users preferring Drone: how to set equivalent secrets in Drone and a note that .drone.yml is left untouched.
|
|
**Tests to add:** none
|
|
**Verify:** open file or grep keywords DEPLOY_SSH_KEY, motief.sgeboers.nl
|
|
**Effort:** 0.75h
|
|
|
|
### Task 2.4: docs — embeddings and environment variables
|
|
**File:** `docs/embeddings.md`
|
|
**Summary:** Document that the project uses QWEN embeddings (qwen/qwen3-embedding-4b via OpenRouter), recommend setting OPENROUTER_API_KEY in repo/host secrets, and mention OPENAI_API_KEY as optional fallback. Include example env var usage and security guidance.
|
|
**Tests to add:** none
|
|
**Verify:** open file and confirm environment variables documented
|
|
**Effort:** 0.5h
|
|
|
|
---
|
|
|
|
## CI workflow outlines (YAML-level steps in prose)
|
|
|
|
Publish workflow (.github/workflows/publish-ansible-example.yml):
|
|
- on:
|
|
- push: tags: ['v*']
|
|
- workflow_dispatch
|
|
- jobs:
|
|
- verify:
|
|
- runs-on: ubuntu-latest
|
|
- steps:
|
|
1. actions/checkout@v4
|
|
2. actions/setup-node@v4 (node-version: '18') with cache: 'npm'
|
|
3. echo current package info (for debugging) but do NOT print secrets
|
|
4. cd packages/@ansible/example && npm ci --no-audit --prefer-offline || true
|
|
5. cd packages/@ansible/example && npm test (calls tests/run.js)
|
|
6. cd packages/@ansible/example && npm run pack-inspect
|
|
- artifacts: upload pack artifact for inspection (optional)
|
|
|
|
- publish:
|
|
- needs: verify
|
|
- runs-on: ubuntu-latest
|
|
- if: (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')) && secrets.NPM_TOKEN != ''
|
|
- steps:
|
|
1. checkout
|
|
2. setup-node
|
|
3. create ephemeral ~/.npmrc with content `//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}` using `run: printf` and set 0600
|
|
4. cd packages/@ansible/example && npm publish --access public
|
|
5. securely remove ~/.npmrc (overwrite if shred available then rm)
|
|
- secrets required: NPM_TOKEN
|
|
- guard rails: job will not run if NPM_TOKEN is empty; Actions expressions used to gate execution
|
|
|
|
Deploy workflow (.github/workflows/deploy-to-vps.yml):
|
|
- on:
|
|
- push: branches: [ main ]
|
|
- workflow_dispatch
|
|
- inputs (for manual dispatch): target_branch, confirm_deploy (boolean)
|
|
- jobs:
|
|
- dry-run-connect:
|
|
- runs-on: ubuntu-latest
|
|
- steps:
|
|
1. checkout
|
|
2. set env from secrets (DEPLOY_HOST=motief.sgeboers.nl default)
|
|
3. if secrets.DEPLOY_SSH_KEY set: write ephemeral key file with 0600 and run ssh -o BatchMode=yes -p $DEPLOY_SSH_PORT $DEPLOY_USER@$DEPLOY_HOST 'echo connected' (short timeout)
|
|
4. if no key present but DEPLOY_PASSWORD present: skip and print instructions to set key
|
|
- outcome: outputs.has_creds true/false for next job
|
|
|
|
- deploy:
|
|
- needs: dry-run-connect
|
|
- if: needs.dry-run-connect.outputs.has_creds == 'true' && github.event.inputs.confirm_deploy == 'true'
|
|
- steps:
|
|
1. checkout
|
|
2. build artifacts (if required)
|
|
3. create ephemeral key file and copy artifacts via rsync/scp to $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH
|
|
4. optionally run remote non-destructive checks (e.g., tail logs, systemctl --user status motief)
|
|
- secrets required for actual deploy: DEPLOY_SSH_KEY or DEPLOY_PASSWORD
|
|
|
|
Security considerations for workflows:
|
|
- Never echo secrets. Use Actions' built-in mask and avoid printing environment variables that contain secrets.
|
|
- Create ephemeral ~/.npmrc and remove it immediately. Use file permissions 0600.
|
|
- Create ephemeral SSH key files with 0600 and remove them at the end of the job.
|
|
|
|
---
|
|
|
|
## Rollback / Undo plan for risky changes
|
|
|
|
- Publish workflow added: to rollback, remove or disable `.github/workflows/publish-ansible-example.yml` and push a commit. If a package was accidentally published, use npm unpublish only if within npm policy window (careful: unpublish can harm consumers) — prefer deprecating the published version via `npm deprecate @ansible/example@x.y.z "do not use"`.
|
|
- Deploy workflow added: to rollback, remove/disable `.github/workflows/deploy-to-vps.yml`. If deployment ran, have a documented process on the VPS to rollback the app (e.g., systemd service `motief` reversion, keep previous release tarball and symlink rollback). Document in deploy docs how to revert symlink/munpack and restart systemd.
|
|
- Docs changed: simply revert doc files and commit.
|
|
|
|
Notes on npm unpublish: prefer `npm deprecate` over unpublish in most cases. Unpublish should be used only with caution and awareness of npm registry policy.
|
|
|
|
---
|
|
|
|
## Final operator checklist (manual steps before running publish/deploy)
|
|
|
|
1. Add repository secrets in GitHub Settings > Secrets:
|
|
- NPM_TOKEN — a token with publish access to the @ansible scope (required for publish job)
|
|
- DEPLOY_SSH_KEY — (recommended) private SSH key for `webapps` user on motief.sgeboers.nl; set as secret (do NOT include passphrase unless CI knows how to handle it)
|
|
- Optionally DEPLOY_PASSWORD — password fallback (not recommended)
|
|
- DEPLOY_HOST — default: motief.sgeboers.nl (recommended to set explicitly)
|
|
- DEPLOY_USER — default: webapps
|
|
- DEPLOY_SSH_PORT — default: 22
|
|
- OPENROUTER_API_KEY — recommended for embeddings
|
|
- OPENAI_API_KEY — optional fallback
|
|
|
|
2. Ensure target VPS `motief.sgeboers.nl` has user `webapps` configured and an authorized public key corresponding to the DEPLOY_SSH_KEY private key.
|
|
|
|
3. Ensure the VPS has a systemd unit name prepared (recommendation: `motief.service`) and that deployment user `webapps` may write to the deploy path (e.g., /home/webapps/motief) and manage its own files. Documented in `docs/deployment/ansible-package-deploy.md`.
|
|
|
|
4. (Optional) If you prefer Drone CI: set Drone secrets for NPM_TOKEN and DEPLOY_* equivalently; .drone.yml remains untouched and you can run publish steps in Drone if you adapt the template.
|
|
|
|
5. Verify local tests: from repo root run:
|
|
- cd packages/@ansible/example && npm test
|
|
- cd packages/@ansible/example && npm pack && tar -tzf <produced-tgz> | head -n 20
|
|
|
|
6. For publish: create a tag `git tag v0.1.0` (or desired semver) and push the tag. The publish workflow will run and attempt publish if NPM_TOKEN secret is present.
|
|
|
|
7. For deploy: run the deploy workflow_dispatch after setting DEPLOY_SSH_KEY and testing dry-run connectivity via the dry-run job.
|
|
|
|
---
|
|
|
|
## Decisions & Assumptions
|
|
|
|
- Package name chosen: `@ansible/example` per design doc. Changeable later by editing package.json and tag.
|
|
- Default version set to `0.1.0` to indicate initial publish candidate.
|
|
- Testing uses a zero-dependency Node test harness (simple node scripts) to avoid introducing a test framework and to keep CI minimal.
|
|
- SSH key auth chosen as default for deployment because it's more secure and scriptable in CI than password auth. SSH keys avoid exposing passwords in secrets logs and work with `ssh -o BatchMode=yes` checks. We recommend DEPLOY_SSH_KEY; DEPLOY_PASSWORD is supported as a fallback but not recommended.
|
|
- Workflows are deliberately gated: publish runs only on tags `v*` or manual dispatch, and publish job requires NPM_TOKEN secret. Deploy will not execute destructive commands unless manual confirm is provided in workflow_dispatch inputs.
|
|
- We will not modify existing `.drone.yml` as requested; docs will describe how to set Drone secrets if the operator prefers Drone.
|
|
|
|
---
|
|
|
|
## Staged execution order
|
|
|
|
1. Batch 1 (create package files & tests): tasks 1.1 — 1.7 (all parallel if multiple implementers available)
|
|
2. Batch 2 (CI & docs): tasks 2.1 — 2.4 (parallel after Batch 1 complete)
|
|
|
|
---
|
|
|
|
## Estimates summary
|
|
|
|
- Batch 1 total: ~3.75h
|
|
- Batch 2 total: ~4.75h
|
|
- Grand total: ~8.5h (approx)
|
|
|
|
---
|
|
|
|
Write this plan to: `thoughts/shared/plans/2026-03-28-rewrite-ansible-package.md`
|
|
|
|
Done.
|
|
|