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 packTests to add: none in this file; tests below will exercise package.json Verify:
- test:
- 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), thennpm test. Then runsnpm packand 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 ANDsecrets.NPM_TOKENexists. publish job is gated byif: ${{ 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 }}(useechoand file permissions 0600), runnpm publish --access publicfrom package directory, then securely delete ~/.npmrc (shred if available or overwrite and rm). Use Actions mask to avoid logging secrets.
- On publish: write ephemeral ~/.npmrc with
- 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
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 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.
- dry-run-connect: attempts to verify connectivity. If DEPLOY_SSH_KEY present, create an ephemeral key file (0600), run
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:
- actions/checkout@v4
- actions/setup-node@v4 (node-version: '18') with cache: 'npm'
- echo current package info (for debugging) but do NOT print secrets
- cd packages/@ansible/example && npm ci --no-audit --prefer-offline || true
- cd packages/@ansible/example && npm test (calls tests/run.js)
- 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:
- checkout
- setup-node
- create ephemeral ~/.npmrc with content
//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}usingrun: printfand set 0600 - cd packages/@ansible/example && npm publish --access public
- 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:
- checkout
- set env from secrets (DEPLOY_HOST=motief.sgeboers.nl default)
- 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)
- 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:
- checkout
- build artifacts (if required)
- create ephemeral key file and copy artifacts via rsync/scp to $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH
- 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.ymland 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 vianpm 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 servicemotiefreversion, 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)
-
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
webappsuser 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
-
Ensure target VPS
motief.sgeboers.nlhas userwebappsconfigured and an authorized public key corresponding to the DEPLOY_SSH_KEY private key. -
Ensure the VPS has a systemd unit name prepared (recommendation:
motief.service) and that deployment userwebappsmay write to the deploy path (e.g., /home/webapps/motief) and manage its own files. Documented indocs/deployment/ansible-package-deploy.md. -
(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.
-
Verify local tests: from repo root run:
- cd packages/@ansible/example && npm test
- cd packages/@ansible/example && npm pack && tar -tzf | head -n 20
-
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. -
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/exampleper design doc. Changeable later by editing package.json and tag. - Default version set to
0.1.0to 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=yeschecks. 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.ymlas requested; docs will describe how to set Drone secrets if the operator prefers Drone.
Staged execution order
- Batch 1 (create package files & tests): tasks 1.1 — 1.7 (all parallel if multiple implementers available)
- 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.