Skip to content

feat(platform): NocoDB and n8n composable deployment migration#15

Open
jestyr27 wants to merge 16 commits into
mainfrom
feat/nocodb-n8n-composable
Open

feat(platform): NocoDB and n8n composable deployment migration#15
jestyr27 wants to merge 16 commits into
mainfrom
feat/nocodb-n8n-composable

Conversation

@jestyr27
Copy link
Copy Markdown
Member

@jestyr27 jestyr27 commented May 7, 2026

Summary

  • Migrate NocoDB and n8n from legacy deploy scripts to the composable deployment pattern (OpenBao → Ansible → Jinja2 → deploy.sh container-only)
  • Add Jinja2 env templates for both services
  • Refactor deploy.sh to container-lifecycle-only (remove secret generation and OpenBao calls)
  • Add 4-phase composable playbooks matching the NetBox pattern
  • Add clean-deploy playbooks and Semaphore templates for both services
  • 32 BATS tests passing, zero security leaks

Breaking Change Mitigation

  • Secret names use prefixed format (nocodb_pg_password, n8n_admin_password) matching existing secrets/ files on VMs
  • NOCODB_ADMIN_PASSWORD and N8N_OWNER_PASSWORD separated from database credentials
  • Prerequisite: Run sync-secrets-to-openbao.yml for both services before first composable deploy

Credential Flow (new)

OpenBao → Ansible manage-secrets.yml → Jinja2 template → config/*.env → deploy.sh (grep)

deploy.sh no longer reads from secrets/ or talks to OpenBao directly.

Test plan

  • CI passes (ruff, shellcheck, ansible-lint, yamllint, pytest, BATS, trufflehog)
  • Run sync-secrets-to-openbao.yml for nocodb_svc and n8n_svc
  • Branch-test NocoDB via Semaphore (service_branch: feat/nocodb-n8n-composable)
  • Branch-test n8n via Semaphore (service_branch: feat/nocodb-n8n-composable)
  • Verify NocoDB UI accessible and data intact
  • Verify n8n UI accessible and workflows intact
  • Verify API tokens still work

Automation Layer

Playbooks & Tasks:

  • Added deploy-nocodb.yml and deploy-n8n.yml as self-contained 4-phase composable playbooks (replacing thin wrapper imports) with phases for: repo cloning/secrets initialization, container lifecycle bootstrap, OpenBao credential persistence via AppRole login, and health verification
  • Added clean-deploy-nocodb.yml and clean-deploy-n8n.yml playbooks that run cleanup tasks before redeploying
  • Added check-vm-secrets.yml playbook for pre-deploy validation of monorepo clones and filesystem state on target VMs

Deploy Scripts:

  • Refactored deploy.sh for both services from secrets-generation/OpenBao-persistence workflows to container-lifecycle-only: scripts now read runtime values from generated config/*.env via grep and no longer invoke OpenBao or generate/store secrets locally
  • NocoDB deploy.sh: Bootstraps via admin password from config/nocodb.env, creates API token via REST endpoint, outputs NOCODB_API_TOKEN=...
  • n8n deploy.sh: Bootstraps via N8N_OWNER_PASSWORD from config/n8n.env, creates API key via REST endpoint with PostgreSQL fallback, outputs N8N_API_KEY=...

Environment Templates:

  • Added nocodb.env.j2 Jinja2 template with PostgreSQL connection params, NC_DB URI, JWT secret, invite-only signup flag, telemetry disable, and admin password
  • Added n8n.env.j2 Jinja2 template with PostgreSQL non-root credentials, encryption key, and owner password

Semaphore Templates:

  • Added Clean Deploy NocoDB and Clean Deploy n8n task templates with service_branch survey variable

Tests:

  • Added test_nocodb_templates.bats (9 test cases validating template existence, required secrets.* placeholders, NC_DB URI, invite/telemetry flags, and absence of hardcoded values)
  • Added test_n8n_templates.bats (5 test cases validating template structure, secrets placeholders, Postgres non-root variables, and encryption key format)

Platform Layer

Docker Compose Files:

  • NocoDB: Renamed nocodb-postgres service to postgres, changed port mapping from ${NOCODB_LISTEN:-127.0.0.1}:8181:8080 to 8080:8080, switched to named volumes (nocodb_nc_data, nocodb_db_data)
  • n8n: Renamed services n8n-postgres/n8n-redis to postgres/redis, replaced inline env values with shared YAML anchors, updated to use variable defaults (e.g., ${POSTGRES_DB}), added local-files bind mount for n8n storage, changed port exposure to 5678:5678

Documentation Layer

CLAUDE.md:

  • Updated OpenBao secrets layout documentation with secret/services/nocodb and secret/services/n8n entries
  • Expanded "Independent Workflows" section with standalone "Deploy NocoDB", "Deploy n8n", and "Clean Deploy ..." workflow rows

Security Impact

This PR changes the secrets credential flow from direct deploy.sh ↔ OpenBao integration to a managed pattern where OpenBao stores extracted runtime API keys via Ansible AppRole authentication, with deploy.sh reading from Jinja2-templated config files only—reducing attack surface by removing OpenBao client invocations from deployed containers and enforcing centralized secret initialization through Ansible.

jestyr27 added 9 commits May 6, 2026 20:18
NocoDB template: postgres_password, jwt_secret placeholders
n8n template: admin_password, user_password, encryption_key placeholders
BATS tests verify template structure and no hardcoded secrets
Remove secret generation (step_generate_secrets) and OpenBao storage
(step_store_in_openbao). Ansible manage-secrets.yml now handles both.
Bootstrap step reads admin password from config/nocodb.env instead of
secrets/ directory. API token printed to stdout for Ansible to capture.
Remove secret generation (step_generate_secrets) and OpenBao storage
(step_store_in_openbao). Ansible manage-secrets.yml now handles both.
Bootstrap step reads owner password from config/n8n.env instead of
secrets/ directory. API key printed to stdout for Ansible to capture.
Replace thin wrapper with full composable deployment:
Phase 1: Clone repo, OpenBao secrets, Jinja2 template env files
Phase 2: deploy.sh (container lifecycle + bootstrap)
Phase 3: Store runtime API token in OpenBao
Phase 4: Health check verification
Replace thin wrapper with full composable deployment:
Phase 1: Clone repo, OpenBao secrets, Jinja2 template env files
Phase 2: deploy.sh (container lifecycle + bootstrap)
Phase 3: Store runtime API key in OpenBao
Phase 4: Health check verification
…or NocoDB and n8n

Clean deploy playbooks use tasks/clean-service.yml to destroy all data
then run the full composable deployment. Semaphore templates added for
both services with branch override support.
Update OpenBao secrets layout with full secret sets for both services.
Add NocoDB and n8n to completed deployments and workflows table.
Add clean-deploy playbooks to workflows table.
Use prefixed secret names (nocodb_pg_password, n8n_admin_password, etc.)
to match existing secrets on VMs and in OpenBao. Unprefixed names would
cause manage-secrets.yml to generate new passwords, breaking running
database connections.

Also separate N8N_OWNER_PASSWORD from POSTGRES_NON_ROOT_PASSWORD in the
n8n template — these are independent secrets for different auth domains.
deploy.sh was reading POSTGRES_PASSWORD as the NocoDB web admin password.
These are separate credentials — add NOCODB_ADMIN_PASSWORD to the env
template so deploy.sh uses the correct secret for signup/signin.
Same pattern as the n8n N8N_OWNER_PASSWORD fix.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Warning

Rate limit exceeded

@jestyr27 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 36 minutes and 32 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0c701a4a-447b-4a96-b958-da44d13b572a

📥 Commits

Reviewing files that changed from the base of the PR and between 5e28db6 and 0fa216f.

📒 Files selected for processing (10)
  • platform/playbooks/deploy-n8n.yml
  • platform/playbooks/deploy-nocodb.yml
  • platform/playbooks/seed-secrets-from-env.yml
  • platform/services/n8n/deployment/compose.yml
  • platform/services/n8n/deployment/deploy.sh
  • platform/services/nocodb/deployment/compose.yml
  • platform/services/nocodb/deployment/deploy.sh
  • platform/services/nocodb/deployment/templates/nocodb.env.j2
  • platform/tests/test_nocodb_templates.bats
  • platform/tests/test_secret_file_format.bats
📝 Walkthrough

Walkthrough

Converted NocoDB and n8n deploys from thin wrappers into explicit multi-phase Ansible playbooks that clone the monorepo, render env templates from Ansible secrets.*, run container bootstrap scripts that emit runtime tokens to stdout, persist tokens to OpenBao via AppRole (delegated to localhost), and perform health checks. Added clean-deploy playbooks, a VM inspection playbook, Semaphore templates, compose and deployment script changes, templates, and Bats tests.

Changes

NocoDB & n8n deployment and orchestration

Layer / File(s) Summary
Data Shape
platform/services/nocodb/deployment/templates/nocodb.env.j2, platform/services/n8n/deployment/templates/n8n.env.j2
New Jinja2 env templates define runtime secrets (Postgres credentials, JWT/encryption keys, admin/owner/user passwords) sourced from secrets.*.
Compose / Runtime Defaults
platform/services/nocodb/deployment/compose.yml, platform/services/n8n/deployment/compose.yml
Compose files standardized service names (postgres, redis), simplified port mappings, switched to ${VAR:-default} envs and shared anchors, renamed volumes to named volumes, and added n8n bind mount for local files.
Deployment Script Core Logic
platform/services/nocodb/deployment/deploy.sh, platform/services/n8n/deployment/deploy.sh
Scripts reworked to focus on container lifecycle and bootstrap: start services, wait for health, signup/signin or owner setup, create API tokens via REST (emit NOCODB_API_TOKEN=... / N8N_API_KEY=... to stdout). n8n retains a DB-insert fallback for API key. Removed local secret persistence and direct OpenBao calls from scripts.
Multi-Phase Playbooks
platform/playbooks/deploy-nocodb.yml, platform/playbooks/deploy-n8n.yml
Expanded from wrapper imports into self-contained multi-phase playbooks: (1) clone/update repo and render env via tasks/manage-secrets.yml; (2) run deploy.sh and capture stdout token; (3) AppRole authenticate (delegated to localhost, no_log: true, run_once) and write secret/data/services/{nocodb,n8n}; (4) HTTP health checks with retries.
Clean Deploys
platform/playbooks/clean-deploy-nocodb.yml, platform/playbooks/clean-deploy-n8n.yml
New playbooks: run tasks/clean-service.yml on target host (destructive), then import corresponding deploy playbook for fresh redeploy.
Inspection Playbook
platform/playbooks/check-vm-secrets.yml
New read-only playbook that verifies monorepo/deploy directories, lists secrets/config files with sizes, queries running containers matched to service name, and emits a debug report.
CI / Orchestration Templates
platform/semaphore/templates.yml
Added Semaphore templates “Clean Deploy NocoDB” and “Clean Deploy n8n” wired to their clean-deploy playbooks with service_branch survey var (default "main").
Tests & Documentation
platform/tests/test_nocodb_templates.bats, platform/tests/test_n8n_templates.bats, CLAUDE.md
New Bats tests ensure templates exist, contain required secrets.* placeholders, disallow hardcoded secret-like values, and verify specific env flags. CLAUDE.md updated for OpenBao secret layout and independent workflow rows.

Sequence Diagram

sequenceDiagram
    actor Operator
    participant Semaphore
    participant Agent
    participant OpenBao
    participant Service

    Operator->>Semaphore: trigger deploy/clean-deploy
    Semaphore->>Agent: clone/update agent-cloud repo
    Agent->>OpenBao: (via manage-secrets.yml) fetch secrets
    Agent->>Agent: render config/*.env templates
    Agent->>Agent: execute deploy.sh
    Agent->>Service: start containers (docker-compose)
    Service-->>Agent: become healthy (/healthz)
    Agent->>Service: bootstrap credentials (signup/signin or owner setup)
    Service-->>Agent: return token on stdout (NOCODB_API_TOKEN / N8N_API_KEY)
    Agent->>Semaphore: capture deploy stdout
    Semaphore->>OpenBao: AppRole login (delegate to localhost)
    Semaphore->>OpenBao: write secret/data/services/{nocodb,n8n} (token + service_url)
    OpenBao-->>Semaphore: 200 OK
    Semaphore->>Service: final health check
    Service-->>Semaphore: 200 OK
    Semaphore-->>Operator: deployment complete
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested Labels

platform, playbooks, semaphore, security, breaking

Poem

🔑 Secrets now flow to vault’s gate,
Playbooks clone, then orchestrate,
Scripts emit the tokens clear,
Deploys rebuilt, inspections near—
Fresh NocoDB and n8n state.

🚥 Pre-merge checks | ✅ 10
✅ Passed checks (10 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(platform): NocoDB and n8n composable deployment migration' directly and accurately summarizes the main change—migrating both services to a composable deployment pattern aligned with existing infrastructure conventions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Pr Title Check ✅ Passed PR title conforms to conventional commit format: feat(platform): NocoDB and n8n composable deployment migration. Type, scope, format, and character count all meet requirements.
No Leaked Secrets Or Ips ✅ Passed No leaked secrets or IPs. All credentials use Jinja2 {{ secrets.* }} templates. OpenBao auth protected with no_log. config/*.env gitignored. No hardcoded tokens, AWS keys, or SSH keys found.
Deploy.Sh Does Not Manage Secrets ✅ Passed Both deploy.sh scripts respect boundary: containers only, zero vault/bao code, no OpenBao writes. Secret management delegated to Ansible.
No Direct-To-Main Commits ✅ Passed PR originates from feat/nocodb-n8n-composable branch, which follows the required naming convention (feat/*). Source branch is not main; changes properly route through feature branch.
Semaphore Config Is Code Managed ✅ Passed All Semaphore configuration changes are code-managed via platform/semaphore/templates.yml and deployed through setup-templates.yml. No manual UI or ad-hoc API calls detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/nocodb-n8n-composable
  • 🛠️ Enforce Secrets Boundary: Commit on current branch
  • 🛠️ Enforce Secrets Boundary: Create PR
  • 🛠️ Validate Composable Pattern: Commit on current branch
  • 🛠️ Validate Composable Pattern: Create PR
  • 🛠️ Check deploy script conventions: Commit on current branch
  • 🛠️ Check deploy script conventions: Create PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
platform/services/n8n/deployment/deploy.sh (2)

58-72: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

trap set inside a function is global — clobbers any pre-existing trap.

Bash traps are process-global, so installing trap 'rm -f "$cookie_jar"' EXIT INT TERM on line 60 silently overwrites whatever trap common.sh or earlier code may have registered. Low risk today since nothing else is registered, but it's a footgun if anyone later adds cleanup in main.

♻️ Suggested fix — clean up locally with `&&`/explicit rm
   local cookie_jar login_payload
   cookie_jar=$(mktemp)
-  trap 'rm -f "$cookie_jar"' EXIT INT TERM
+  # Local cleanup; avoid overriding any global trap installed by common.sh
+  _cleanup_cookie() { rm -f "$cookie_jar"; }

…and call _cleanup_cookie on each return path, or use a subshell. Skippable if you're sure no other trap will ever land here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/n8n/deployment/deploy.sh` around lines 58 - 72, The trap
call in the login routine installs a global EXIT/INT/TERM handler and can
clobber other traps; remove the trap 'rm -f "$cookie_jar"' usage and instead
ensure the temp file is removed locally by either wrapping the login sequence in
a subshell or implementing a local cleanup function (e.g., _cleanup_cookie) that
calls rm -f "$cookie_jar" and invoking that function on every return path
(success and failure) after the curl block; update references around cookie_jar,
login_payload and the curl POST to call the local cleanup before returning so no
global trap is required.

90-111: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Guideline violation: deploy.sh is generating a secret in the DB-insert fallback.

openssl rand -hex 20 on line 93 generates a runtime credential, and the script then inserts it directly into Postgres. Two coding guidelines call this out explicitly:

  • "deploy.sh must NOT generate secrets, interact with OpenBao, or call generate-secrets.sh."
  • "deploy.sh must be pure container operations."

This also breaks the stated OpenBao-as-source-of-truth model: a key that exists only in the n8n DB (because the API endpoint was unavailable at deploy time) is now out of band from Phase 3's OpenBao write — except that since you echo it back, Phase 3 will store it, but only if the regex match still works on stdout. The bigger problem is that this path bypasses any policy/audit OpenBao would apply.

Two reasonable options:

  1. Move the fallback to Ansible: have Phase 2 ask OpenBao for n8n_api_key and pass it to deploy.sh via env when present, so the script only ever consumes a key.
  2. If keeping the fallback here for operational simplicity, update the coding guideline + CLAUDE.md to carve out "runtime bootstrap credentials" so this doesn't trip future reviews.

Either way, the current state contradicts the rules in **/deploy.sh and platform/services/*/deployment/deploy.sh.

As per coding guidelines: "deploy.sh must be pure container operations. It must NOT generate secrets, interact with OpenBao, or call generate-secrets.sh."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/n8n/deployment/deploy.sh` around lines 90 - 111, The script
currently generates a runtime secret with openssl rand -hex 20, inserts it into
Postgres, and echoes it (api_key / insert_result / echo "N8N_API_KEY..."), which
violates the "deploy.sh must NOT generate secrets" rule; change the fallback so
deploy.sh only consumes a provided secret: remove the openssl rand invocation
and the DB-insert echo, read N8N_API_KEY from the environment (or fail fast with
a clear warn and exit if unset), and use that value for the psql INSERT (keep
the existing insert_result logic), or alternatively move this fallback into
Ansible/Phase‑2 so OpenBao supplies the key—do not generate or print secrets in
deploy.sh.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform/playbooks/deploy-n8n.yml`:
- Around line 74-91: The debug task "Deploy output" is leaking secrets because
_deploy.stdout_lines includes the printed N8N_API_KEY; remove or disable that
debug output and mark any tasks that handle the secret as no_log: true
(specifically the "Run deploy.sh" shell task that registers _deploy and the
"Extract API key from deploy output" set_fact that writes _n8n_api_key) so the
API key is never logged; keep the regex extraction into _n8n_api_key as-is but
ensure no_log: true is set on the set_fact (and the shell task) so the secret is
suppressed in controller logs.
- Around line 105-135: Replace the two uri-based tasks ("Authenticate to
OpenBao" and "Store API key in OpenBao") with a single
community.hashi_vault.vault_write call that uses auth_method: approle (providing
role_id and secret_id) and writes the KV data for services/n8n (n8n_api_key and
url); this uses community.hashi_vault.vault_write to handle AppRole auth, token
lifecycle, and the KV v2 path semantics, remove the manual _bao_auth register
and the separate POST, keep no_log for sensitive inputs, and ensure the playbook
requires collection version >=6.0.0 so the module is available.
- Around line 148-156: The "Health check" ansible.builtin.uri task currently
relies on retries/delay without an explicit condition; register the task result
(e.g., register: health_check) and add an explicit until condition such as
until: health_check.status == 200 so the task will actually retry the health
endpoint until it returns 200 (keep retries/delay as-is and retain the when:
service_url is defined guard); ensure you reference the uri task named "Health
check" and the registered variable health_check when adding the until clause.

In `@platform/playbooks/deploy-nocodb.yml`:
- Around line 34-61: Replace the inline git clone and symlink tasks with the
composable task include tasks/clone-and-deploy.yml: remove the
ansible.builtin.git block and the ansible.builtin.file (symlink) block and
instead add ansible.builtin.include_tasks: tasks/clone-and-deploy.yml, ensuring
you pass through the same variables used by the inlined tasks (monorepo_repo,
_monorepo_dir, _branch, _deploy_dir, service_name, and any register/failure
semantics expected) so behavior and error handling remain identical; keep the
subsequent include of tasks/manage-secrets.yml unchanged.
- Around line 73-91: The deploy playbook currently leaks NOCODB_API_TOKEN via
the "Run deploy.sh" stdout and the "Deploy output" debug task and also writes
the secret into _nocodb_api_token without no_log; to fix, add no_log: true to
the "Run deploy.sh" task (the ansible.builtin.shell that registers _deploy) so
stdout isn't stored in cleartext, remove or change the "Deploy output" debug
task so it does not print _deploy.stdout_lines (replace with a non-secret
summary message if needed), and add no_log: true to the "Extract API token from
deploy output" set_fact that sets _nocodb_api_token so the extracted token is
not logged.
- Around line 147-155: The "Health check" ansible task is missing a
register/until pair so retries are never used; update the task to register the
response (e.g., register: _health) and add until: _health.status == 200 so the
retries/delay loop actually re-attempts the URI call (keep validate_certs,
timeout, status_code as-is and retain the when: service_url is defined
condition).

In `@platform/services/n8n/deployment/templates/n8n.env.j2`:
- Around line 3-7: The template currently hardcodes usernames
POSTGRES_USER=n8n_admin and POSTGRES_NON_ROOT_USER=n8n_user; change these to
template variables (e.g., POSTGRES_USER={{ postgres_admin_user }} and
POSTGRES_NON_ROOT_USER={{ postgres_non_root_user }}) and wire those variables
into your inventory/secret inputs so they are provided at deploy time instead of
committed; update the corresponding inventory/secret source to define
postgres_admin_user and postgres_non_root_user (or map them from existing secret
names) and ensure any code referencing POSTGRES_USER/POSTGRES_NON_ROOT_USER
still receives the values from the template variables.
- Around line 4-10: Secret templating uses {{ secrets.* }} but must follow the
repo convention; update the secret placeholders in the n8n env template by
replacing each {{ secrets.* }} usage with the vault_* (or _secret_*) form.
Specifically change POSTGRES_PASSWORD, POSTGRES_NON_ROOT_PASSWORD,
DB_POSTGRESDB_PASSWORD, N8N_ENCRYPTION_KEY and N8N_OWNER_PASSWORD to use
vault_n8n_admin_password / vault_n8n_user_password / vault_n8n_user_password /
vault_n8n_encryption_key / vault_n8n_owner_password (or the equivalent _secret_*
names) while leaving non-secret entries like POSTGRES_DB and
POSTGRES_NON_ROOT_USER unchanged.

In `@platform/services/nocodb/deployment/deploy.sh`:
- Around line 12-13: The deploy.sh uses a relative traversal to build LIB_DIR;
change it to prefer the repo root via CLONE_DIR by setting LIB_DIR to use
"${CLONE_DIR:-...}/platform/lib" (i.e. use CLONE_DIR if set, otherwise fall back
to the current dirname traversal), then source "${LIB_DIR}/common.sh" as before;
update the LIB_DIR assignment in deploy.sh (the symbol LIB_DIR and its use when
sourcing common.sh) so the script honors the CLONE_DIR contract.
- Around line 34-37: The bootstrap currently greps CONFIG_DIR/nocodb.env into
admin_pass and, if empty, calls warn and returns success; change this to fail
fast: before or immediately after the admin_pass assignment (the line that reads
admin_pass=$(grep '^NOCODB_ADMIN_PASSWORD=' "${CONFIG_DIR}/nocodb.env" ...)),
detect a missing or unreadable "${CONFIG_DIR}/nocodb.env" or an empty admin_pass
and replace the warn/return 0 behavior with a non-zero exit (e.g., log an error
with the script's error/fatal function or echo to stderr) and exit 1 so the
script aborts when the .env is absent or the password is unset.
- Line 90: The deploy workflow is leaking the NOCODB API token because the shell
task "Run deploy.sh", the debug task "Deploy output", and the set_fact task
"Extract API token from deploy output" all log stdout; update the playbook tasks
named exactly "Run deploy.sh", "Deploy output", and "Extract API token from
deploy output" to include no_log: true so the token echoed by deploy.sh (the
line echo "NOCODB_API_TOKEN=${api_token}" in
platform/services/nocodb/deployment/deploy.sh) is not recorded in Ansible logs;
ensure each task's YAML contains no_log: true (leave the echo in deploy.sh
unchanged).

In `@platform/services/nocodb/deployment/templates/nocodb.env.j2`:
- Around line 4-8: The template uses {{ secrets.nocodb_pg_password }}, {{
secrets.nocodb_jwt_secret }}, and {{ secrets.nocodb_admin_password }} but must
use the mandated secret variable namespace; update the template lines that set
POSTGRES_PASSWORD, NC_DB (the pg URL), NC_AUTH_JWT_SECRET, and
NOCODB_ADMIN_PASSWORD to reference the approved variables (e.g., {{
vault_nocodb_pg_password }} or {{ _secret_nocodb_pg_password }} etc.) and adjust
the corresponding render/vars that supply these values so they match the new
names.
- Around line 3-5: Replace the hardcoded POSTGRES_USER value with a template
variable so credentials/usernames aren't committed: change the literal "nocodb"
to a Jinja variable (e.g., {{ secrets.nocodb_pg_user }} or a similarly named
inventory variable) in nocodb.env.j2, update any inventory/secrets source to
provide that key, and adjust deployment/config docs or templates that reference
POSTGRES_USER to use the new variable (search for POSTGRES_USER and nocodb
references to update callers like your deployment/playbook that loads
nocodb.env.j2).

In `@platform/tests/test_n8n_templates.bats`:
- Around line 11-19: Tests currently assert presence of '{{ secrets.* }}'
placeholders (see grep patterns for '{{ secrets.n8n_admin_password }}', '{{
secrets.n8n_user_password }}', '{{ secrets.n8n_encryption_key }}' in the
n8n.env.j2 tests) but our templates must use the vault_*/_secret_* convention;
update each assertion in the "@test" blocks that grep those secrets to instead
validate the allowed conventions (accept '{{ vault_n8n_* }}' or '{{
_secret_n8n_* }}' placeholders for admin password, user password and encryption
key), and make the same change for the other related assertions referenced in
the comment (the tests around lines 37–43).

In `@platform/tests/test_nocodb_templates.bats`:
- Around line 11-15: The tests are asserting the old secrets.* placeholders;
update the grep expectations in the test cases that check
"$TEMPLATE_DIR/nocodb.env.j2" (both the PG password and JWT secret checks and
the similar checks at lines ~28-34) to look for the mandated service-template
secret names using the vault/_secret convention (e.g., replace checks for '{{
secrets.nocodb_pg_password }}' and '{{ secrets.nocodb_jwt_secret }}' with the
allowed pattern such as '{{ vault_nocodb_pg_password }}' or '{{
_secret_nocodb_pg_password }}' and similarly for the JWT secret) so CI enforces
the vault_*/_secret_* variable family.

---

Outside diff comments:
In `@platform/services/n8n/deployment/deploy.sh`:
- Around line 58-72: The trap call in the login routine installs a global
EXIT/INT/TERM handler and can clobber other traps; remove the trap 'rm -f
"$cookie_jar"' usage and instead ensure the temp file is removed locally by
either wrapping the login sequence in a subshell or implementing a local cleanup
function (e.g., _cleanup_cookie) that calls rm -f "$cookie_jar" and invoking
that function on every return path (success and failure) after the curl block;
update references around cookie_jar, login_payload and the curl POST to call the
local cleanup before returning so no global trap is required.
- Around line 90-111: The script currently generates a runtime secret with
openssl rand -hex 20, inserts it into Postgres, and echoes it (api_key /
insert_result / echo "N8N_API_KEY..."), which violates the "deploy.sh must NOT
generate secrets" rule; change the fallback so deploy.sh only consumes a
provided secret: remove the openssl rand invocation and the DB-insert echo, read
N8N_API_KEY from the environment (or fail fast with a clear warn and exit if
unset), and use that value for the psql INSERT (keep the existing insert_result
logic), or alternatively move this fallback into Ansible/Phase‑2 so OpenBao
supplies the key—do not generate or print secrets in deploy.sh.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e1fabdce-b249-403f-8296-b00855032427

📥 Commits

Reviewing files that changed from the base of the PR and between 129a068 and 4fd2516.

📒 Files selected for processing (12)
  • CLAUDE.md
  • platform/playbooks/clean-deploy-n8n.yml
  • platform/playbooks/clean-deploy-nocodb.yml
  • platform/playbooks/deploy-n8n.yml
  • platform/playbooks/deploy-nocodb.yml
  • platform/semaphore/templates.yml
  • platform/services/n8n/deployment/deploy.sh
  • platform/services/n8n/deployment/templates/n8n.env.j2
  • platform/services/nocodb/deployment/deploy.sh
  • platform/services/nocodb/deployment/templates/nocodb.env.j2
  • platform/tests/test_n8n_templates.bats
  • platform/tests/test_nocodb_templates.bats

Comment thread platform/playbooks/deploy-n8n.yml
Comment on lines +105 to +135
- name: "Authenticate to OpenBao"
ansible.builtin.uri:
url: "{{ _bao_url }}/v1/auth/approle/login"
method: POST
body_format: json
body:
role_id: "{{ _bao_role_id }}"
secret_id: "{{ _bao_secret_id }}"
status_code: [200]
register: _bao_auth
delegate_to: localhost
run_once: true
no_log: true
when: _n8n_api_key | default('') | length > 0

- name: "Store API key in OpenBao"
ansible.builtin.uri:
url: "{{ _bao_url }}/v1/secret/data/services/n8n"
method: POST
headers:
X-Vault-Token: "{{ _bao_auth.json.auth.client_token }}"
body_format: json
body:
data:
n8n_api_key: "{{ _n8n_api_key }}"
url: "{{ service_url | default('http://localhost:5678') }}"
status_code: [200]
delegate_to: localhost
run_once: true
no_log: true
when: _n8n_api_key | default('') | length > 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

🧩 Analysis chain

🌐 Web query:

community.hashi_vault vault_write module AppRole authentication parameters

💡 Result:

For the Ansible collection community.hashi_vault.vault_write module, AppRole authentication is done via these module parameters: 1) auth_method - Set auth_method to "approle" to use AppRole authentication. [1][2] 2) role_id - role_id is the Vault AppRole role ID (or role name) used for approle auth. [1][2] 3) secret_id - secret_id is the AppRole SecretID used for approle authentication. [1][2] These parameters are used together with the module’s url and path to perform the write. For example, generating a SecretID is a write to auth/approle/role//secret-id, and the module can be used to retrieve/generate RoleID and SecretID as part of a workflow. [1][2] Separately (if you’re looking at Vault/Terraform-native AppRole login rather than Ansible), Vault’s AppRole login uses the auth/approle/role//login endpoint and the role can be configured with parameters such as token_type, secret_id_ttl, token_ttl, token_max_ttl, and secret_id_num_uses (among others). [3] If you meant “community.hashi_vault.vault_write module (Ansible) authentication parameters” specifically: the AppRole-relevant ones are exactly auth_method, role_id, and secret_id as documented above. [1][2]

Citations:


🏁 Script executed:

# First, verify the file exists and check the exact code
head -150 platform/playbooks/deploy-n8n.yml | tail -50

Repository: uhstray-io/agent-cloud

Length of output: 1709


🏁 Script executed:

# Search for any existing community.hashi_vault usage in the codebase
rg "community\.hashi_vault|vault_write|vault_read" --type yml -B 2 -A 5

Repository: uhstray-io/agent-cloud

Length of output: 94


🏁 Script executed:

# Check if there's any AppRole-related task library usage
find platform -name "*.yml" -type f | xargs grep -l "manage-approle\|approle" | head -10

Repository: uhstray-io/agent-cloud

Length of output: 463


🏁 Script executed:

# Check for manage-approle.yml task mentioned in guidelines
find platform -name "manage-approle.yml" -o -name "*approle*"

Repository: uhstray-io/agent-cloud

Length of output: 110


🏁 Script executed:

# Search for any Vault/Bao interaction patterns in the codebase
rg "vault_write|vault_read|hashi_vault|OpenBao|secret/data" --type yaml -B 2 -A 3 | head -80

Repository: uhstray-io/agent-cloud

Length of output: 5252


🏁 Script executed:

# Check the actual vault_write module documentation to understand if it handles AppRole auth
# Let's look at what the module expects
rg "auth_method.*approle|role_id.*secret_id" --type yaml

Repository: uhstray-io/agent-cloud

Length of output: 2076


🏁 Script executed:

# Look at how vault_read is actually used in distribute-ssh-keys.yml
sed -n '1,100p' platform/playbooks/distribute-ssh-keys.yml | grep -A 10 -B 5 "vault_read"

Repository: uhstray-io/agent-cloud

Length of output: 48


🏁 Script executed:

# Get broader context of vault_read usage
rg "vault_read|vault_write" --type yaml -B 3 -A 8 | head -120

Repository: uhstray-io/agent-cloud

Length of output: 48


🏁 Script executed:

# Check the manage-approle.yml task
cat platform/playbooks/tasks/manage-approle.yml

Repository: uhstray-io/agent-cloud

Length of output: 3541


🏁 Script executed:

# Look at the exact context where vault_read is used in distribute-ssh-keys.yml
sed -n '1,150p' platform/playbooks/distribute-ssh-keys.yml

Repository: uhstray-io/agent-cloud

Length of output: 4603


🏁 Script executed:

# Search for all instances of vault_read to see the exact pattern
rg "vault_read" --type yaml -B 5 -A 2

Repository: uhstray-io/agent-cloud

Length of output: 48


🏁 Script executed:

# Search for any use of vault_write or vault_read as modules (not lookups)
rg "community.hashi_vault.vault_write|community.hashi_vault.vault_read" --type yaml

Repository: uhstray-io/agent-cloud

Length of output: 48


🏁 Script executed:

# Check what version of community.hashi_vault is required
cat platform/services/semaphore/deployment/collections/requirements.yml

Repository: uhstray-io/agent-cloud

Length of output: 138


🏁 Script executed:

# Look at the coding guidelines section again to understand the exact requirement
cat pyproject.toml | grep -A 20 "hashi_vault\|OpenBao" || echo "not in pyproject"

Repository: uhstray-io/agent-cloud

Length of output: 83


🏁 Script executed:

# Check if there's any documentation about vault_write usage expectations
find platform -name "*.md" -o -name "*.txt" | xargs grep -l "vault_write\|hashi_vault" 2>/dev/null | head -5

Repository: uhstray-io/agent-cloud

Length of output: 95


🏁 Script executed:

# Check the playbooks README for guidance on vault usage
cat platform/playbooks/README.md

Repository: uhstray-io/agent-cloud

Length of output: 5417


🏁 Script executed:

# Check if there are any examples of vault_write being used anywhere in the repo
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.md" \) | xargs grep -l "vault_write" 2>/dev/null

Repository: uhstray-io/agent-cloud

Length of output: 50


Migrate to community.hashi_vault.vault_write for secret storage.

The guideline states "Use community.hashi_vault for OpenBao access," but this playbook uses raw uri calls for AppRole auth and secret writes. The vault_write module (available in the required >=6.0.0 collection) natively handles AppRole authentication, token management, and cleanup, eliminating the manual shuffle of client_token between tasks and reducing token exposure in register dicts.

Not blocking — uri functions — but brings consistency with the stated guideline and removes a couple of potential failure modes.

🧰 Tools
🪛 Checkov (3.2.526)

[medium] 105-121: Ensure that HTTPS url is used with uri

(CKV2_ANSIBLE_1)

🪛 GitHub Check: Static Analysis

[failure] 120-120: run-once[task]
Using run_once may behave differently if strategy is set to free.


[failure] 105-105: run-once[task]
Using run_once may behave differently if strategy is set to free.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/playbooks/deploy-n8n.yml` around lines 105 - 135, Replace the two
uri-based tasks ("Authenticate to OpenBao" and "Store API key in OpenBao") with
a single community.hashi_vault.vault_write call that uses auth_method: approle
(providing role_id and secret_id) and writes the KV data for services/n8n
(n8n_api_key and url); this uses community.hashi_vault.vault_write to handle
AppRole auth, token lifecycle, and the KV v2 path semantics, remove the manual
_bao_auth register and the separate POST, keep no_log for sensitive inputs, and
ensure the playbook requires collection version >=6.0.0 so the module is
available.

Comment thread platform/playbooks/deploy-n8n.yml
Comment on lines +34 to +61
tasks:
- name: "Install git if needed"
ansible.builtin.apt:
name: git
state: present
become: true

- name: "Clone or update agent-cloud monorepo"
ansible.builtin.git:
repo: "{{ monorepo_repo }}"
dest: "{{ _monorepo_dir }}"
version: "{{ _branch }}"
force: true

- name: "Create convenience symlink"
ansible.builtin.file:
src: "{{ _deploy_dir }}"
dest: "/home/{{ ansible_user }}/{{ service_name }}"
state: link
force: true
register: _symlink_result
failed_when:
- _symlink_result is failed
- _symlink_result.msg is defined
- "'permission denied' in (_symlink_result.msg | lower)"

- name: "Manage secrets and template env files"
ansible.builtin.include_tasks: tasks/manage-secrets.yml
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Reuse tasks/clone-and-deploy.yml instead of inlining clone + symlink (same as deploy-n8n.yml).

Phase 1 duplicates the clone/symlink logic that already lives in the composable task library. Consolidating both NocoDB and n8n on clone-and-deploy.yml removes drift risk and keeps the deployment surface idiomatic per the composability guideline.

As per coding guidelines: "Use composable Ansible tasks from the task library (manage-secrets.yml, manage-diode-credentials.yml, manage-approle.yml, deploy-orb-agent.yml, clean-service.yml, clone-and-deploy.yml) as building blocks for deployment playbooks."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/playbooks/deploy-nocodb.yml` around lines 34 - 61, Replace the
inline git clone and symlink tasks with the composable task include
tasks/clone-and-deploy.yml: remove the ansible.builtin.git block and the
ansible.builtin.file (symlink) block and instead add
ansible.builtin.include_tasks: tasks/clone-and-deploy.yml, ensuring you pass
through the same variables used by the inlined tasks (monorepo_repo,
_monorepo_dir, _branch, _deploy_dir, service_name, and any register/failure
semantics expected) so behavior and error handling remain identical; keep the
subsequent include of tasks/manage-secrets.yml unchanged.

Comment thread platform/playbooks/deploy-nocodb.yml
Comment thread platform/services/nocodb/deployment/deploy.sh
Comment on lines +3 to +5
POSTGRES_USER=nocodb
POSTGRES_PASSWORD={{ secrets.nocodb_pg_password }}
POSTGRES_DB=nocodb
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid hardcoded usernames in committed env templates.

POSTGRES_USER=nocodb is a committed username literal. Move it to a template variable sourced from inventory/secrets to keep committed files free of credentials/usernames.

As per coding guidelines, **/* must not commit credentials, IPs, or usernames directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/nocodb/deployment/templates/nocodb.env.j2` around lines 3 -
5, Replace the hardcoded POSTGRES_USER value with a template variable so
credentials/usernames aren't committed: change the literal "nocodb" to a Jinja
variable (e.g., {{ secrets.nocodb_pg_user }} or a similarly named inventory
variable) in nocodb.env.j2, update any inventory/secrets source to provide that
key, and adjust deployment/config docs or templates that reference POSTGRES_USER
to use the new variable (search for POSTGRES_USER and nocodb references to
update callers like your deployment/playbook that loads nocodb.env.j2).

Comment on lines +4 to +8
POSTGRES_PASSWORD={{ secrets.nocodb_pg_password }}
POSTGRES_DB=nocodb
NC_DB=pg://workflow-nocodb-postgres:5432?u=nocodb&p={{ secrets.nocodb_pg_password }}&d=nocodb
NC_AUTH_JWT_SECRET={{ secrets.nocodb_jwt_secret }}
NOCODB_ADMIN_PASSWORD={{ secrets.nocodb_admin_password }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the mandated secret variable namespace in service templates.

This template uses {{ secrets.* }} for secret values, but service deployment templates are required to use {{ vault_* }} or {{ _secret_* }} variables. Please align this file (and corresponding render vars) to that contract.

As per coding guidelines, platform/services/*/deployment/templates/*.j2 must use {{ vault_* }} or {{ _secret_* }} for secret values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/nocodb/deployment/templates/nocodb.env.j2` around lines 4 -
8, The template uses {{ secrets.nocodb_pg_password }}, {{
secrets.nocodb_jwt_secret }}, and {{ secrets.nocodb_admin_password }} but must
use the mandated secret variable namespace; update the template lines that set
POSTGRES_PASSWORD, NC_DB (the pg URL), NC_AUTH_JWT_SECRET, and
NOCODB_ADMIN_PASSWORD to reference the approved variables (e.g., {{
vault_nocodb_pg_password }} or {{ _secret_nocodb_pg_password }} etc.) and adjust
the corresponding render/vars that supply these values so they match the new
names.

Comment thread platform/tests/test_n8n_templates.bats Outdated
Comment on lines +11 to +19
grep -q '{{ secrets.n8n_admin_password }}' "$TEMPLATE_DIR/n8n.env.j2"
}

@test "n8n.env.j2 contains non-root user password placeholder" {
grep -q '{{ secrets.n8n_user_password }}' "$TEMPLATE_DIR/n8n.env.j2"
}

@test "n8n.env.j2 contains N8N_ENCRYPTION_KEY placeholder" {
grep -q '{{ secrets.n8n_encryption_key }}' "$TEMPLATE_DIR/n8n.env.j2"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

n8n template tests enforce the wrong secret variable convention.

The test suite requires secrets.* placeholders, but service templates are expected to use vault_*/_secret_*. Please update assertions so CI validates the intended convention.

As per coding guidelines, platform/services/*/deployment/templates/*.j2 secret values must use {{ vault_* }} or {{ _secret_* }} variables.

Also applies to: 37-43

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/tests/test_n8n_templates.bats` around lines 11 - 19, Tests currently
assert presence of '{{ secrets.* }}' placeholders (see grep patterns for '{{
secrets.n8n_admin_password }}', '{{ secrets.n8n_user_password }}', '{{
secrets.n8n_encryption_key }}' in the n8n.env.j2 tests) but our templates must
use the vault_*/_secret_* convention; update each assertion in the "@test"
blocks that grep those secrets to instead validate the allowed conventions
(accept '{{ vault_n8n_* }}' or '{{ _secret_n8n_* }}' placeholders for admin
password, user password and encryption key), and make the same change for the
other related assertions referenced in the comment (the tests around lines
37–43).

Comment on lines +11 to +15
grep -q '{{ secrets.nocodb_pg_password }}' "$TEMPLATE_DIR/nocodb.env.j2"
}

@test "nocodb.env.j2 contains NC_AUTH_JWT_SECRET placeholder" {
grep -q '{{ secrets.nocodb_jwt_secret }}' "$TEMPLATE_DIR/nocodb.env.j2"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Template tests currently enforce a non-compliant placeholder namespace.

These assertions hardcode secrets.*, which conflicts with the required vault_*/_secret_* service-template convention. Update test expectations to the mandated variable family so CI enforces the correct contract.

As per coding guidelines, service env templates must use {{ vault_* }} or {{ _secret_* }} variables for secret values.

Also applies to: 28-34

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/tests/test_nocodb_templates.bats` around lines 11 - 15, The tests
are asserting the old secrets.* placeholders; update the grep expectations in
the test cases that check "$TEMPLATE_DIR/nocodb.env.j2" (both the PG password
and JWT secret checks and the similar checks at lines ~28-34) to look for the
mandated service-template secret names using the vault/_secret convention (e.g.,
replace checks for '{{ secrets.nocodb_pg_password }}' and '{{
secrets.nocodb_jwt_secret }}' with the allowed pattern such as '{{
vault_nocodb_pg_password }}' or '{{ _secret_nocodb_pg_password }}' and similarly
for the JWT secret) so CI enforces the vault_*/_secret_* variable family.

jestyr27 added 4 commits May 6, 2026 21:48
NocoDB:
- Container names: nocodb, postgres (matching running VMs)
- Volume names: nocodb_nc_data, nocodb_db_data (reuse existing data)
- Port 8080 (matching VM deployment)
- NC_DB connection string points to postgres service
- Add NC_INVITE_ONLY_SIGNUP and NC_DISABLE_TELE from pre-monorepo config

n8n:
- Service names: postgres, redis (podman-compose produces n8n_postgres_1)
- Volume names: n8n_db_storage, n8n_redis_storage, n8n_n8n_storage (reuse)
- ENCRYPTION_KEY in env file, compose maps to N8N_ENCRYPTION_KEY
- Add local-files mount for n8n file storage
- Dynamic postgres container detection in DB fallback

Both VMs use Podman 4.9.3 with podman-compose 1.0.6.
Validates that secret files in site-config have no trailing newlines,
carriage returns, non-printable characters, or empty files. These cause
JSON parsing failures when passed to OpenBao or Semaphore APIs.
Tests skip gracefully when site-config is not available (CI).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

♻️ Duplicate comments (5)
platform/services/nocodb/deployment/deploy.sh (1)

33-37: ⚠️ Potential issue | 🟠 Major

This still needs to fail fast when config/nocodb.env or NOCODB_ADMIN_PASSWORD is missing.

Returning success here lets the deploy continue without the bootstrap/token phase ever running, which masks a broken Ansible templating phase on first deploy.

As per coding guidelines, platform/services/*/deployment/deploy.sh should verify required .env files exist and fail early if Ansible hasn’t run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/nocodb/deployment/deploy.sh` around lines 33 - 37, The
script currently returns success when the config or NOCODB_ADMIN_PASSWORD is
missing; instead, verify the file exists and fail fast: check for existence of
"${CONFIG_DIR}/nocodb.env" before reading it and if missing call an error log
and exit 1, then read admin_pass with admin_pass=$(grep ...) and if [ -z
"$admin_pass" ]; then call an error logging function (or stderr) and exit 1 —
replace the current warn + return 0 path with a non-zero exit so deploy.sh fails
early when CONFIG_DIR/nocodb.env or NOCODB_ADMIN_PASSWORD is absent.
platform/tests/test_nocodb_templates.bats (1)

10-40: ⚠️ Potential issue | 🟠 Major

These assertions are still locking CI to secrets.*.

The template contract has moved to vault_* / _secret_*, but this suite still requires {{ secrets.* }} and rejects any compliant replacement. That will block the template fix rather than protect it.

As per coding guidelines, platform/services/*/deployment/templates/*.j2 secret values must use {{ vault_* }} or {{ _secret_* }} variables.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/tests/test_nocodb_templates.bats` around lines 10 - 40, The tests in
test_nocodb_templates.bats still require Jinja placeholders to be secrets.*
which is outdated; update the placeholder checks (tests like "nocodb.env.j2
contains POSTGRES_PASSWORD placeholder", "nocodb.env.j2 contains
NC_AUTH_JWT_SECRET placeholder", and the final "uses only secrets.* Jinja2
placeholders" block) to accept the new template contract by matching either
secrets.*, vault_* or _secret_* prefixes; change the grep/regex patterns that
look for '\{\{[^}]+\}\}' and specific 'secrets.' occurrences to a single regex
allowing '{{ (secrets\.|vault_[^}\s]+|_secret_[^}\s]+)[^}]* }}' (or equivalent)
so valid vault_*/_secret_* placeholders pass while still rejecting hardcoded
secrets.
platform/services/nocodb/deployment/templates/nocodb.env.j2 (1)

3-10: ⚠️ Potential issue | 🟠 Major

Still unresolved: this template is using deprecated placeholders and committed usernames.

POSTGRES_USER / the u=nocodb fragment in NC_DB are still hardcoded, and the secret-backed fields still use {{ secrets.* }} instead of the required vault_* / _secret_* names.

As per coding guidelines, platform/services/*/deployment/templates/*.j2 secret values must use {{ vault_* }} or {{ _secret_* }} variables, and **/*.j2 must not commit usernames or credentials directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/nocodb/deployment/templates/nocodb.env.j2` around lines 3 -
10, Replace hardcoded credentials and deprecated {{ secrets.* }} placeholders in
this template: remove the committed username "nocodb" and the literal u=nocodb
fragment in NC_DB and instead source POSTGRES_USER, POSTGRES_PASSWORD,
POSTGRES_DB, NC_DB credentials from vault-backed variables (e.g., use {{
vault_postgres_user }} or {{ _secret_postgres_user }} for POSTGRES_USER and the
equivalent for POSTGRES_PASSWORD, POSTGRES_DB, NC_AUTH_JWT_SECRET,
NC_INVITE_ONLY_SIGNUP, and NOCODB_ADMIN_PASSWORD), and rebuild NC_DB to
interpolate those vault-backed user/password/db values rather than a hardcoded
user fragment; ensure no usernames or credentials remain literal in
POSTGRES_USER, NC_DB, or NOCODB_ADMIN_PASSWORD.
platform/tests/test_n8n_templates.bats (1)

10-38: ⚠️ Potential issue | 🟠 Major

These tests are still enforcing the deprecated secrets.* placeholder contract.

Once the template is corrected, this suite will fail because it both requires {{ secrets.* }} and rejects any other Jinja placeholder family. CI should be validating the mandated vault_* / _secret_* convention instead.

As per coding guidelines, platform/services/*/deployment/templates/*.j2 secret values must use {{ vault_* }} or {{ _secret_* }} variables.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/tests/test_n8n_templates.bats` around lines 10 - 38, The tests are
enforcing the deprecated {{ secrets.* }} placeholders; update them to require
the mandated vault/_secret convention instead: change assertions that look for
'{{ secrets.n8n_admin_password }}', '{{ secrets.n8n_user_password }}', '{{
secrets.n8n_encryption_key }}', and '{{ secrets.n8n_owner_password }}' to accept
either '{{ vault_n8n_admin_password }}' or '{{ _secret_n8n_admin_password }}'
(and likewise for user_password, n8n_encryption_key, n8n_owner_password) by
updating the grep patterns in the corresponding test blocks, and replace the
"n8n.env.j2 uses only secrets.* Jinja2 placeholders" test (and its
bad_placeholders extraction) to allow only placeholders matching 'vault_' or
'_secret_' while rejecting 'secrets.'; keep the hardcoded-secrets negative regex
and other checks intact.
platform/services/n8n/deployment/templates/n8n.env.j2 (1)

3-9: ⚠️ Potential issue | 🟠 Major

Still unresolved: this template does not follow the repo’s variable contract.

POSTGRES_USER / POSTGRES_NON_ROOT_USER are still committed literals, and the secret fields still use {{ secrets.* }} instead of the required vault_* / _secret_* namespace. That keeps the deployment on the deprecated template interface.

As per coding guidelines, platform/services/*/deployment/templates/*.j2 secret values must use {{ vault_* }} or {{ _secret_* }} variables, and **/*.j2 must not commit usernames or credentials directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/n8n/deployment/templates/n8n.env.j2` around lines 3 - 9,
The template commits hard-coded usernames and uses the wrong secret namespace;
replace literal POSTGRES_USER and POSTGRES_NON_ROOT_USER values with variable
references and switch all secret placeholders to the repo contract (use vault_*
or _secret_*), e.g. update POSTGRES_USER, POSTGRES_NON_ROOT_USER,
POSTGRES_PASSWORD, POSTGRES_NON_ROOT_PASSWORD, ENCRYPTION_KEY and
N8N_OWNER_PASSWORD to reference the appropriate vault/_secret variables instead
of literal names or {{ secrets.* }} so the file conforms to
platform/services/*/deployment/templates/*.j2 conventions and avoids committing
credentials.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform/playbooks/check-vm-secrets.yml`:
- Around line 48-53: The "Check running containers" task defaults
container_engine to 'docker', which can misreport on Podman hosts; change the
logic in that task to default to 'podman' instead of 'docker' or implement the
same runtime detection used by the deploy scripts (use the same detection helper
or playbook variable) so the shell command uses the correct engine
(container_engine) when checking {{ service_name }} and registering _containers;
ensure changed_when remains false.
- Around line 30-37: Remove the task named "List secrets directory" that uses
ansible.builtin.find to inspect "{{ _deploy_dir }}/secrets" and the registered
variable _secret_files; this check must be deleted so the playbook no longer
looks for an on-VM secrets/ directory. Also remove or update any downstream
references/conditions that use the _secret_files variable (or its when
conditions) so they instead rely on the generated config/*.env flow (Jinja2
templating of env files) and do not create or expect intermediary secrets files
on the VM.

In `@platform/playbooks/deploy-nocodb.yml`:
- Around line 147-155: The "Health check" task using ansible.builtin.uri
currently skips when service_url is undefined; remove the "when: service_url is
defined" clause so the task always runs (the task named "Health check" that hits
"{{ service_url | default('http://localhost:8080') }}/api/v1/health"); apply the
same change to the equivalent "Health check" tasks in deploy-netbox.yml,
deploy-n8n.yml and sync-secrets-to-openbao.yml so Phase 4 (verify-health)
executes against the default URL when service_url is not provided.
- Around line 104-133: The "Store API token in OpenBao" task currently does a
POST via ansible.builtin.uri which overwrites the KV v2 secret (losing keys
written earlier by Phase 1); replace this task with the
community.hashi_vault.vault_kv2_write module (or implement an explicit
read-merge-write using community.hashi_vault.vault_kv2_read followed by
vault_kv2_write) to merge the new nocodb_api_token and url into the existing
secret at services/nocodb, or alternatively perform a PATCH with Content-Type:
application/merge-patch+json if you must use HTTP; also change this and related
secret operations (e.g., the "Authenticate to OpenBao" flow that provides the
token) to use community.hashi_vault modules per coding guidelines instead of
ansible.builtin.uri so existing keys (pg_password, jwt_secret, admin_password)
are preserved.

In `@platform/services/n8n/deployment/compose.yml`:
- Around line 42-59: Update the n8n and n8n-worker service blocks: replace
restart: always with restart: unless-stopped, change image:
docker.n8n.io/n8nio/n8n:latest to the pinned form using the VERSION variable
(e.g., image: docker.n8n.io/n8nio/n8n:${VERSION:-tag}), and add a healthcheck
block for each service that probes the service readiness (for example an HTTP
check against the n8n port/health endpoint or a TCP check) with sensible
interval/retries/start-period settings so Compose can distinguish started vs
ready; apply these exact changes to the n8n and n8n-worker service definitions
referenced in the compose file.
- Around line 42-50: Remove the service-level YAML merge for n8n (the "<<:
*n8n-env" entry) so only the explicit environment: block defines variables;
change restart: always to restart: unless-stopped for both n8n and n8n-worker
services; replace image: docker.n8n.io/n8nio/n8n:latest with a pinned tag using
a variable like docker.n8n.io/n8nio/n8n:${VERSION:-1.0.0} for both the n8n and
n8n-worker service definitions; and add healthcheck blocks to both n8n and
n8n-worker services (matching the existing postgres/redis healthcheck pattern)
immediately after their environment sections to ensure health monitoring.

In `@platform/services/n8n/deployment/deploy.sh`:
- Around line 33-37: The script currently silently skips bootstrap when
N8N_OWNER_PASSWORD is missing; change it to fail fast by first verifying the
config file exists and then aborting with an error if the owner password is
absent: add a check for the file (test -f "${CONFIG_DIR}/n8n.env") and if it is
missing call the error handler (or echo to stderr) and exit non‑zero, and
replace the warn + return 0 branch that handles an empty owner_pass with an
error log and non‑zero exit (reference owner_pass, CONFIG_DIR, n8n.env,
warn/return 0 in deploy.sh).
- Around line 99-110: The fallback DB insert uses a fragile container picker and
a hardcoded owner id; update the logic so pg_container is resolved by compose
project/service (e.g. prefer containers whose name includes "-postgres-" or
filter ps by the compose label "com.docker.compose.project" / service name
instead of grep -E 'postgres' | head -1), and replace the literal user_id '1' in
the INSERT SQL with a subquery that looks up the real owner id by role (e.g.
SELECT id FROM "user" WHERE role = 'global:owner' OR WHERE settings->>'role' =
'global:owner'), validate that a single owner id was found and abort the
fallback if not, and keep the existing guard that suppresses the fallback or
remove the fallback entirely if you prefer to force manual API-key creation;
update references to pg_container, CONTAINER_ENGINE, and insert_result
accordingly and ensure a clear warn/error is emitted when owner lookup or
container resolution fails.

In `@platform/services/nocodb/deployment/compose.yml`:
- Around line 25-26: The compose file is publishing Postgres to the host via the
ports mapping ("5432:5432") which is unnecessary for inter-service
communication; remove the "5432:5432" entry from the postgres service's ports
block (the ports key under the postgres service) so nocodb communicates over the
compose network, or if host-only access is truly needed change it to bind to
localhost ("127.0.0.1:5432:5432") instead; update the postgres service's ports
setting and keep nocodb unchanged.

In `@platform/services/nocodb/deployment/templates/nocodb.env.j2`:
- Line 6: The NC_DB environment line embeds the raw secret
(secrets.nocodb_pg_password) into a URI and can break if the password contains
special characters; update the template in nocodb.env.j2 to URL-encode the
password before interpolation (e.g. use the Jinja2 urlencode filter: replace {{
secrets.nocodb_pg_password }} with {{ secrets.nocodb_pg_password | urlencode
}}), or alternatively switch NC_DB to a connection form that supplies
user/password separately rather than embedding them in the URI.

---

Duplicate comments:
In `@platform/services/n8n/deployment/templates/n8n.env.j2`:
- Around line 3-9: The template commits hard-coded usernames and uses the wrong
secret namespace; replace literal POSTGRES_USER and POSTGRES_NON_ROOT_USER
values with variable references and switch all secret placeholders to the repo
contract (use vault_* or _secret_*), e.g. update POSTGRES_USER,
POSTGRES_NON_ROOT_USER, POSTGRES_PASSWORD, POSTGRES_NON_ROOT_PASSWORD,
ENCRYPTION_KEY and N8N_OWNER_PASSWORD to reference the appropriate vault/_secret
variables instead of literal names or {{ secrets.* }} so the file conforms to
platform/services/*/deployment/templates/*.j2 conventions and avoids committing
credentials.

In `@platform/services/nocodb/deployment/deploy.sh`:
- Around line 33-37: The script currently returns success when the config or
NOCODB_ADMIN_PASSWORD is missing; instead, verify the file exists and fail fast:
check for existence of "${CONFIG_DIR}/nocodb.env" before reading it and if
missing call an error log and exit 1, then read admin_pass with
admin_pass=$(grep ...) and if [ -z "$admin_pass" ]; then call an error logging
function (or stderr) and exit 1 — replace the current warn + return 0 path with
a non-zero exit so deploy.sh fails early when CONFIG_DIR/nocodb.env or
NOCODB_ADMIN_PASSWORD is absent.

In `@platform/services/nocodb/deployment/templates/nocodb.env.j2`:
- Around line 3-10: Replace hardcoded credentials and deprecated {{ secrets.* }}
placeholders in this template: remove the committed username "nocodb" and the
literal u=nocodb fragment in NC_DB and instead source POSTGRES_USER,
POSTGRES_PASSWORD, POSTGRES_DB, NC_DB credentials from vault-backed variables
(e.g., use {{ vault_postgres_user }} or {{ _secret_postgres_user }} for
POSTGRES_USER and the equivalent for POSTGRES_PASSWORD, POSTGRES_DB,
NC_AUTH_JWT_SECRET, NC_INVITE_ONLY_SIGNUP, and NOCODB_ADMIN_PASSWORD), and
rebuild NC_DB to interpolate those vault-backed user/password/db values rather
than a hardcoded user fragment; ensure no usernames or credentials remain
literal in POSTGRES_USER, NC_DB, or NOCODB_ADMIN_PASSWORD.

In `@platform/tests/test_n8n_templates.bats`:
- Around line 10-38: The tests are enforcing the deprecated {{ secrets.* }}
placeholders; update them to require the mandated vault/_secret convention
instead: change assertions that look for '{{ secrets.n8n_admin_password }}', '{{
secrets.n8n_user_password }}', '{{ secrets.n8n_encryption_key }}', and '{{
secrets.n8n_owner_password }}' to accept either '{{ vault_n8n_admin_password }}'
or '{{ _secret_n8n_admin_password }}' (and likewise for user_password,
n8n_encryption_key, n8n_owner_password) by updating the grep patterns in the
corresponding test blocks, and replace the "n8n.env.j2 uses only secrets.*
Jinja2 placeholders" test (and its bad_placeholders extraction) to allow only
placeholders matching 'vault_' or '_secret_' while rejecting 'secrets.'; keep
the hardcoded-secrets negative regex and other checks intact.

In `@platform/tests/test_nocodb_templates.bats`:
- Around line 10-40: The tests in test_nocodb_templates.bats still require Jinja
placeholders to be secrets.* which is outdated; update the placeholder checks
(tests like "nocodb.env.j2 contains POSTGRES_PASSWORD placeholder",
"nocodb.env.j2 contains NC_AUTH_JWT_SECRET placeholder", and the final "uses
only secrets.* Jinja2 placeholders" block) to accept the new template contract
by matching either secrets.*, vault_* or _secret_* prefixes; change the
grep/regex patterns that look for '\{\{[^}]+\}\}' and specific 'secrets.'
occurrences to a single regex allowing '{{
(secrets\.|vault_[^}\s]+|_secret_[^}\s]+)[^}]* }}' (or equivalent) so valid
vault_*/_secret_* placeholders pass while still rejecting hardcoded secrets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 48c0fe1f-c52a-463a-8db0-4407fdec631a

📥 Commits

Reviewing files that changed from the base of the PR and between 4fd2516 and 5e28db6.

📒 Files selected for processing (11)
  • platform/playbooks/check-vm-secrets.yml
  • platform/playbooks/deploy-nocodb.yml
  • platform/services/n8n/deployment/compose.yml
  • platform/services/n8n/deployment/deploy.sh
  • platform/services/n8n/deployment/local-files/.gitkeep
  • platform/services/n8n/deployment/templates/n8n.env.j2
  • platform/services/nocodb/deployment/compose.yml
  • platform/services/nocodb/deployment/deploy.sh
  • platform/services/nocodb/deployment/templates/nocodb.env.j2
  • platform/tests/test_n8n_templates.bats
  • platform/tests/test_nocodb_templates.bats

Comment on lines +30 to +37
- name: "List secrets directory"
ansible.builtin.find:
paths: "{{ _deploy_dir }}/secrets"
patterns: "*"
file_type: file
register: _secret_files
when: _deploy_stat.stat.exists | default(false)
ignore_errors: true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Stop checking for a secrets/ directory in the composable path.

This playbook is normalizing an artifact the new flow explicitly avoids. For NocoDB/n8n, runtime secrets are supposed to exist only as generated config/*.env, not as on-VM secrets/ files.

Based on learnings, “Secrets flow: OpenBao → Ansible memory → Jinja2 templates → .env files (compose-readable, gitignored). Never create intermediary secret files or secrets/ directory on VMs.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/playbooks/check-vm-secrets.yml` around lines 30 - 37, Remove the
task named "List secrets directory" that uses ansible.builtin.find to inspect
"{{ _deploy_dir }}/secrets" and the registered variable _secret_files; this
check must be deleted so the playbook no longer looks for an on-VM secrets/
directory. Also remove or update any downstream references/conditions that use
the _secret_files variable (or its when conditions) so they instead rely on the
generated config/*.env flow (Jinja2 templating of env files) and do not create
or expect intermediary secrets files on the VM.

Comment on lines +48 to +53
- name: "Check running containers"
ansible.builtin.shell: |
set -o pipefail
{{ container_engine | default('docker') }} ps --format '{{ '{{' }}.Names{{ '}}' }} {{ '{{' }}.Status{{ '}}' }}' 2>/dev/null | grep -i '{{ service_name }}' || echo "No {{ service_name }} containers"
register: _containers
changed_when: false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Defaulting container inspection to Docker will misreport these hosts.

If container_engine is unset, this task will happily print “No … containers” on a Podman VM even when the service is running. Default to Podman here, or detect the runtime the same way the deploy scripts do.

Based on learnings, “Use Podman for all other services (rootless, security-focused).”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/playbooks/check-vm-secrets.yml` around lines 48 - 53, The "Check
running containers" task defaults container_engine to 'docker', which can
misreport on Podman hosts; change the logic in that task to default to 'podman'
instead of 'docker' or implement the same runtime detection used by the deploy
scripts (use the same detection helper or playbook variable) so the shell
command uses the correct engine (container_engine) when checking {{ service_name
}} and registering _containers; ensure changed_when remains false.

Comment on lines +104 to +133
- name: "Authenticate to OpenBao"
ansible.builtin.uri:
url: "{{ _bao_url }}/v1/auth/approle/login"
method: POST
body_format: json
body:
role_id: "{{ _bao_role_id }}"
secret_id: "{{ _bao_secret_id }}"
status_code: [200]
register: _bao_auth
delegate_to: localhost
run_once: true
no_log: true
when: _nocodb_api_token | default('') | length > 0

- name: "Store API token in OpenBao"
ansible.builtin.uri:
url: "{{ _bao_url }}/v1/secret/data/services/nocodb"
method: POST
headers:
X-Vault-Token: "{{ _bao_auth.json.auth.client_token }}"
body_format: json
body:
data:
nocodb_api_token: "{{ _nocodb_api_token }}"
url: "{{ service_url | default('http://localhost:8080') }}"
status_code: [200]
delegate_to: localhost
run_once: true
no_log: true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

# Check file exists and get line count
wc -l platform/playbooks/deploy-nocodb.yml

Repository: uhstray-io/agent-cloud

Length of output: 107


🏁 Script executed:

# Read lines 90-140 to see context around lines 104-133
sed -n '90,140p' platform/playbooks/deploy-nocodb.yml

Repository: uhstray-io/agent-cloud

Length of output: 1830


🏁 Script executed:

# Search for Phase 1 and initial secret setup in the file
grep -n "Phase" platform/playbooks/deploy-nocodb.yml

Repository: uhstray-io/agent-cloud

Length of output: 876


🏁 Script executed:

# Check if community.hashi_vault is used elsewhere in the playbook
grep -n "community.hashi_vault" platform/playbooks/deploy-nocodb.yml

Repository: uhstray-io/agent-cloud

Length of output: 48


🏁 Script executed:

# Look at the full playbook to understand structure
head -150 platform/playbooks/deploy-nocodb.yml

Repository: uhstray-io/agent-cloud

Length of output: 5357


🏁 Script executed:

# Find manage-secrets.yml
find . -name "manage-secrets.yml" -type f

Repository: uhstray-io/agent-cloud

Length of output: 112


🏁 Script executed:

# Check if manage-secrets.yml exists and read it
cat -n platform/playbooks/tasks/manage-secrets.yml

Repository: uhstray-io/agent-cloud

Length of output: 4316


🏁 Script executed:

# Also check the directory structure of tasks
ls -la platform/playbooks/tasks/

Repository: uhstray-io/agent-cloud

Length of output: 824


🌐 Web query:

OpenBao KV v2 secret engine POST partial data overwrites entire secret version

💡 Result:

Yes, in OpenBao's KV v2 secrets engine, sending a POST request to the /data/:path endpoint with partial data will overwrite the entire secret version, replacing all existing data with only the provided keys [1][2][3]. The POST method performs a full replacement of the 'data' map, discarding any unspecified fields [3]. To perform partial updates without overwriting the entire secret: - Use HTTP PATCH on the same /:mount/data/:path endpoint with Content-Type: application/merge-patch+json. This applies a JSON merge patch to the existing data, preserving unspecified fields, and requires the 'patch' ACL capability [3][4]. - Example curl: curl --header "X-Vault-Token:..." --header "Content-Type: application/merge-patch+json" --request PATCH --data '{"newkey": "value"}' https://127.0.0.1:8200/v1/secret/data/my-secret [3]. - Alternatively, use the CLI 'bao kv patch' command, which prefers PATCH but falls back to read-then-write (requiring 'read' and 'update' capabilities) if 'patch' is unavailable [1][5]. This behavior matches HashiCorp Vault KV v2, from which OpenBao is forked [6][7].

Citations:


🌐 Web query:

community.hashi_vault Ansible collection KV v2 PUT write support

💡 Result:

community.hashi_vault DOES support KV v2 PUT/write via the module community.hashi_vault.vault_kv2_write. It is a “Perform a write operation against a KVv2 secret in HashiCorp Vault” module, and its parameters include path (KVv2 path to write to) and data (KVv2 secret data to write) [1][2]. Example usage (write/create a secret): community.hashi_vault.vault_kv2_write: url: https://vault:8200 path: hello data: foo: bar [1] Notes: - The module supports CAS via a cas parameter, including “cas: 0” to require that the secret must not exist, and CAS values to update an existing secret version [2]. - If you instead use the more generic module community.hashi_vault.vault_write, it may not automatically handle KV v2 path/data formats; the community discussion states kv1 and kv2 API paths differ and you must include the correct /data/ formatting for kv2 when using vault_write (because it’s not kv-specific) [3]. Conclusion: For KV v2 PUT/write support in the community.hashi_vault Ansible collection, use community.hashi_vault.vault_kv2_write (not vault_write) [1][3].

Citations:


🏁 Script executed:

# Check if there are any examples of community.hashi_vault usage in the repo
rg "community\.hashi_vault" --type yml --type yaml

Repository: uhstray-io/agent-cloud

Length of output: 94


🏁 Script executed:

# Search for community.hashi_vault usage without file type filter
rg "community\.hashi_vault"

Repository: uhstray-io/agent-cloud

Length of output: 5042


Use community.hashi_vault.vault_kv2_write in Phase 3 to preserve full secret payload.

Lines 119-129 POST only nocodb_api_token and url to /v1/secret/data/services/nocodb, which replaces the entire KV v2 secret version. Phase 1 writes pg_password, jwt_secret, and admin_password to the same path; these are lost on the Phase 3 POST.

Replace the "Store API token in OpenBao" task with community.hashi_vault.vault_kv2_write using a read-merge-write pattern, or use PATCH with Content-Type: application/merge-patch+json to preserve existing keys. Additionally, both manage-secrets.yml and Phase 3 should use community.hashi_vault modules instead of ansible.builtin.uri per the coding guidelines.

🧰 Tools
🪛 Checkov (3.2.526)

[medium] 104-120: Ensure that HTTPS url is used with uri

(CKV2_ANSIBLE_1)

🪛 GitHub Check: Static Analysis

[failure] 119-119: run-once[task]
Using run_once may behave differently if strategy is set to free.


[failure] 104-104: run-once[task]
Using run_once may behave differently if strategy is set to free.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/playbooks/deploy-nocodb.yml` around lines 104 - 133, The "Store API
token in OpenBao" task currently does a POST via ansible.builtin.uri which
overwrites the KV v2 secret (losing keys written earlier by Phase 1); replace
this task with the community.hashi_vault.vault_kv2_write module (or implement an
explicit read-merge-write using community.hashi_vault.vault_kv2_read followed by
vault_kv2_write) to merge the new nocodb_api_token and url into the existing
secret at services/nocodb, or alternatively perform a PATCH with Content-Type:
application/merge-patch+json if you must use HTTP; also change this and related
secret operations (e.g., the "Authenticate to OpenBao" flow that provides the
token) to use community.hashi_vault modules per coding guidelines instead of
ansible.builtin.uri so existing keys (pg_password, jwt_secret, admin_password)
are preserved.

Comment on lines +147 to +155
- name: "Health check"
ansible.builtin.uri:
url: "{{ service_url | default('http://localhost:8080') }}/api/v1/health"
status_code: [200]
validate_certs: false
timeout: 30
retries: 5
delay: 10
when: service_url is defined
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and read the relevant section
cat -n platform/playbooks/deploy-nocodb.yml | sed -n '140,160p'

Repository: uhstray-io/agent-cloud

Length of output: 796


🏁 Script executed:

# Also check the overall structure of the playbook to understand the 4-phase pattern
head -100 platform/playbooks/deploy-nocodb.yml | tail -50

Repository: uhstray-io/agent-cloud

Length of output: 1795


🏁 Script executed:

# Search for similar health check patterns in other deploy playbooks
fd -e yml platform/playbooks/deploy-*.yml -x grep -l "Health check"

Repository: uhstray-io/agent-cloud

Length of output: 778


🏁 Script executed:

# Check other deploy playbooks for similar conditional health check patterns
rg "when: service_url is defined" platform/playbooks/

Repository: uhstray-io/agent-cloud

Length of output: 361


🏁 Script executed:

# Check if other deploy playbooks have health checks and how they handle defaults
rg -A 3 "Health check" platform/playbooks/deploy-*.yml

Repository: uhstray-io/agent-cloud

Length of output: 958


Remove the when: service_url is defined condition to enforce mandatory Phase 4 verification.

This playbook has a default URL fallback on line 149, but line 155 skips the entire health check when service_url is unset. The 4-phase composable pattern requires Phase 4 (verify-health) to always execute. Removing this condition ensures the readiness check runs against the default URL, preventing false-positive deployment success.

Suggested fix
     - name: "Health check"
       ansible.builtin.uri:
         url: "{{ service_url | default('http://localhost:8080') }}/api/v1/health"
         status_code: [200]
         validate_certs: false
         timeout: 30
       retries: 5
       delay: 10
-      when: service_url is defined

This same pattern occurs in deploy-netbox.yml, deploy-n8n.yml, and sync-secrets-to-openbao.yml — address all instances.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: "Health check"
ansible.builtin.uri:
url: "{{ service_url | default('http://localhost:8080') }}/api/v1/health"
status_code: [200]
validate_certs: false
timeout: 30
retries: 5
delay: 10
when: service_url is defined
- name: "Health check"
ansible.builtin.uri:
url: "{{ service_url | default('http://localhost:8080') }}/api/v1/health"
status_code: [200]
validate_certs: false
timeout: 30
retries: 5
delay: 10
🧰 Tools
🪛 Checkov (3.2.526)

[medium] 147-158: Ensure that certificate validation isn't disabled with uri

(CKV_ANSIBLE_1)


[medium] 147-158: Ensure that HTTPS url is used with uri

(CKV2_ANSIBLE_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/playbooks/deploy-nocodb.yml` around lines 147 - 155, The "Health
check" task using ansible.builtin.uri currently skips when service_url is
undefined; remove the "when: service_url is defined" clause so the task always
runs (the task named "Health check" that hits "{{ service_url |
default('http://localhost:8080') }}/api/v1/health"); apply the same change to
the equivalent "Health check" tasks in deploy-netbox.yml, deploy-n8n.yml and
sync-secrets-to-openbao.yml so Phase 4 (verify-health) executes against the
default URL when service_url is not provided.

Comment on lines 42 to 59
n8n:
container_name: workflow-n8n
<<: *n8n-env
image: docker.n8n.io/n8nio/n8n:latest
restart: always
ports:
- "${N8N_LISTEN:-127.0.0.1}:5678:5678"
- "5678:5678"
env_file: ./config/n8n.env
environment:
<<: *n8n-env
volumes:
- n8n_data:/home/node/.n8n
- n8n_storage:/home/node/.n8n
- ./local-files:/files
depends_on:
n8n-redis:
redis:
condition: service_healthy
n8n-postgres:
postgres:
condition: service_healthy

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat platform/services/n8n/deployment/compose.yml

Repository: uhstray-io/agent-cloud

Length of output: 2045


Add healthchecks to n8n and n8n-worker services; pin image versions and correct restart policies.

The n8n and n8n-worker services lack healthcheck blocks, preventing Compose from distinguishing started vs. ready states. Additionally, images use :latest instead of the required ${VERSION:-tag} syntax, and restart policies are always instead of unless-stopped.

Per coding guidelines:

  • "Services must define healthcheck blocks"
  • "Compose files should pin image versions using ${VERSION:-tag} syntax"
  • "Restart policy should be 'unless-stopped'"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/n8n/deployment/compose.yml` around lines 42 - 59, Update
the n8n and n8n-worker service blocks: replace restart: always with restart:
unless-stopped, change image: docker.n8n.io/n8nio/n8n:latest to the pinned form
using the VERSION variable (e.g., image:
docker.n8n.io/n8nio/n8n:${VERSION:-tag}), and add a healthcheck block for each
service that probes the service readiness (for example an HTTP check against the
n8n port/health endpoint or a TCP check) with sensible
interval/retries/start-period settings so Compose can distinguish started vs
ready; apply these exact changes to the n8n and n8n-worker service definitions
referenced in the compose file.

Comment thread platform/services/n8n/deployment/compose.yml
Comment on lines +33 to 37
local owner_pass
owner_pass=$(grep '^N8N_OWNER_PASSWORD=' "${CONFIG_DIR}/n8n.env" 2>/dev/null | cut -d= -f2-)
if [ -z "$owner_pass" ]; then
warn " No N8N_OWNER_PASSWORD found in config/n8n.env — skipping bootstrap."
return 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing config/n8n.env should abort this phase, not silently skip it.

A first deploy can report success here without ever creating the owner or API key if Ansible did not render N8N_OWNER_PASSWORD. That breaks the expected secrets → bootstrap flow.

As per coding guidelines, platform/services/*/deployment/deploy.sh should verify required .env files exist and fail early if Ansible hasn’t run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/n8n/deployment/deploy.sh` around lines 33 - 37, The script
currently silently skips bootstrap when N8N_OWNER_PASSWORD is missing; change it
to fail fast by first verifying the config file exists and then aborting with an
error if the owner password is absent: add a check for the file (test -f
"${CONFIG_DIR}/n8n.env") and if it is missing call the error handler (or echo to
stderr) and exit non‑zero, and replace the warn + return 0 branch that handles
an empty owner_pass with an error log and non‑zero exit (reference owner_pass,
CONFIG_DIR, n8n.env, warn/return 0 in deploy.sh).

Comment on lines +99 to 110
local pg_container
pg_container=$($CONTAINER_ENGINE ps --format '{{.Names}}' 2>/dev/null | grep -E 'postgres' | head -1)
if [ -z "$pg_container" ]; then
warn " No postgres container found — aborting DB insert."
return 0
fi
insert_result=$($CONTAINER_ENGINE exec "$pg_container" \
psql -U n8n_user -d n8n -t -A -c \
"INSERT INTO api_key (user_id, label, api_key, created_at, updated_at)
SELECT '1', 'nemoclaw-agent', '${api_key}', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM api_key WHERE label = 'nemoclaw-agent')
RETURNING api_key;" 2>/dev/null) || insert_result=""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For the n8n version used by this deployment, is direct SQL insertion into the api_keytable with columns(user_id, label, api_key, created_at, updated_at) supported, and is the owner account guaranteed to have id 1?

💡 Result:

Direct SQL insertion into a table named api_key with columns (user_id, label, api_key, created_at, updated_at) is not something the n8n docs or codebase generally guarantee. 1) Evidence about the database schema not matching your proposed table/column names - n8n’s database structure is managed by n8n migrations/TypeORM (not a stable public SQL contract) [1]. - n8n uses an ApiKey entity/table managed in code, and there is ongoing refactoring of API key persistence (“Move API keys into their own table”) [2]. This indicates the physical table/column layout (including exact column names like api_key, created_at, updated_at) can change across versions. Given that neither the n8n “database structure” documentation nor the referenced repo changes confirm an api_key table with exactly those column names, you should assume direct insertion with that exact column set is unsupported/fragile for the deployment’s specific n8n version. 2) Is the owner account guaranteed to have id 1? - n8n identifies the instance owner by role (global:owner), and will create a new default owner if no owner exists; the docs do not state that the owner’s database id is always 1 [3]. - Therefore, ownership by id = 1 is not guaranteed. Practical guidance (based on the cited sources) - Don’t hard-code id = 1 for “owner”; instead, query for the user with the global:owner role [3]. - Don’t rely on direct INSERT into a guessed api_key table/columns; use n8n’s API (“Settings > n8n API” / API key endpoints) or adapt to the exact table/columns created by the running n8n version’s migrations [1][4].

Citations:


🏁 Script executed:

# Check the file size and structure
wc -l platform/services/n8n/deployment/deploy.sh

# Look at the beginning of the file to understand its structure and n8n version
head -50 platform/services/n8n/deployment/deploy.sh

# Check for n8n version specification
grep -i "n8n" platform/services/n8n/deployment/deploy.sh | head -20

Repository: uhstray-io/agent-cloud

Length of output: 2801


🏁 Script executed:

# Look at the problematic section in context (lines 80-120)
sed -n '80,120p' platform/services/n8n/deployment/deploy.sh

Repository: uhstray-io/agent-cloud

Length of output: 1492


🏁 Script executed:

# Check if there's a way to get the n8n version from the deployment
find platform/services/n8n -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.env*" \) | head -20

Repository: uhstray-io/agent-cloud

Length of output: 165


🏁 Script executed:

# Check the compose file structure
head -80 platform/services/n8n/deployment/compose.yml

Repository: uhstray-io/agent-cloud

Length of output: 2045


🏁 Script executed:

# Check the n8n.env template to understand version and configuration
cat platform/services/n8n/deployment/templates/n8n.env.j2

Repository: uhstray-io/agent-cloud

Length of output: 468


Scoped Postgres container selection and role-based owner lookup required for DB fallback.

The fallback DB insertion has two critical issues:

  1. Container selectiongrep -E 'postgres' | head -1 can match the wrong container on multi-service hosts. Use the service name from compose (postgres → container name <project>-postgres-1) or filter by compose project.

  2. Hardcoded owner IDuser_id = '1' is unsafe. N8n owner is identified by role (global:owner), not by a fixed ID. On restored or migrated data, this will fail or create the key under the wrong user. Query the user with role global:owner instead.

Additionally, the web search results confirm that direct SQL insertion into the api_key table is unsupported across n8n versions — the table structure is managed by n8n's TypeORM migrations and can change. While the REST API approach (primary path) is correct, the fallback should either:

  • Query the correct owner by role and inspect the actual api_key table schema at runtime, or
  • Remove the fallback entirely and require manual API key creation if the endpoint is unavailable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/services/n8n/deployment/deploy.sh` around lines 99 - 110, The
fallback DB insert uses a fragile container picker and a hardcoded owner id;
update the logic so pg_container is resolved by compose project/service (e.g.
prefer containers whose name includes "-postgres-" or filter ps by the compose
label "com.docker.compose.project" / service name instead of grep -E 'postgres'
| head -1), and replace the literal user_id '1' in the INSERT SQL with a
subquery that looks up the real owner id by role (e.g. SELECT id FROM "user"
WHERE role = 'global:owner' OR WHERE settings->>'role' = 'global:owner'),
validate that a single owner id was found and abort the fallback if not, and
keep the existing guard that suppresses the fallback or remove the fallback
entirely if you prefer to force manual API-key creation; update references to
pg_container, CONTAINER_ENGINE, and insert_result accordingly and ensure a clear
warn/error is emitted when owner lookup or container resolution fails.

Comment thread platform/services/nocodb/deployment/compose.yml Outdated
Comment thread platform/services/nocodb/deployment/templates/nocodb.env.j2 Outdated
jestyr27 added 3 commits May 6, 2026 22:46
Special characters (!@#$%^&* etc.) are valid for password complexity.
Only reject null bytes, control characters (0x01-0x1F except newline),
and DEL — these break shell parsing and JSON encoding.
Security:
- Add no_log to deploy.sh execution tasks (prevents API token/key leak)
- Filter API credentials from Ansible stdout output
- URL-encode password in NocoDB NC_DB connection string
- Remove exposed Postgres port from NocoDB compose (internal-only)

Correctness:
- Add until condition to health check retries (required by Ansible)
- Fail fast if config/*.env missing instead of silently skipping
- Set compose project name (-p nocodb/-p n8n) for correct volume naming
- Remove invalid YAML anchor merge at n8n service level
- Create config/ directory before templating env files
@Caxha254
Copy link
Copy Markdown

Caxha254 commented May 7, 2026

Saw your post about feat(platform): NocoDB and n8n composable deployme.

I deploy custom AI assistants — local models, persistent memory, no subscriptions. Your data stays yours.

Price: $600, 48h. Free 15-min scope call first if you want.

— Lyn
lynchatta@gmail.com


Reply or email: lynchatta@gmail.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants