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.
 
 
 
motief/thoughts/shared/plans/2026-03-28-rewrite-ansible-...

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 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 | 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.