#!/usr/bin/env bash # Phase 7a (AUD-115): import + publish the 6:00am workflow in n8n. # # n8n on .13 is a shared family service. Run this only after the change is # approved. It: # 1. imports one SSH credential built from the restricted trigger key, # 2. imports one workflow (schedule -> SSH -> failure check), # 3. publishes the workflow, # 4. restarts n8n once so the 06:00 schedule registers, # 5. verifies the workflow is active in the n8n log, not just in the database. # # It does NOT run the workflow. The first real run is the next 06:00 trigger, # or `docker exec n8n n8n execute --id=schoolMorningBrf` by hand (real send). # # The SSH credential holds the private half of the restricted key. It is built # into a mode-0600 temp file, copied into the container, imported, and removed. # It is never written into this repository. # # Why not an Execute Command node? n8n 2.x disables `n8n-nodes-base.executeCommand` # by default (`nodes.exclude`), so such a workflow cannot activate. The SSH node # is enabled, and the restricted key keeps it to one command. # # Usage: # deploy/import-n8n-morning-brief.sh # dry run, print the plan # deploy/import-n8n-morning-brief.sh --apply # deploy/import-n8n-morning-brief.sh --apply --no-restart # # Environment overrides: # CONTAINER n8n container name (default n8n) # WORKFLOW workflow json path (default /deploy/n8n-morning-brief.json) # KEY private key path (default ~/.ssh/id_ed25519_school_scan) # SSH_HOST host the brief runs on (default 192.168.20.13) # SSH_USER user on that host (default sam) set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CONTAINER="${CONTAINER:-n8n}" WORKFLOW="${WORKFLOW:-$REPO_ROOT/deploy/n8n-morning-brief.json}" KEY="${KEY:-$HOME/.ssh/id_ed25519_school_scan}" SSH_HOST="${SSH_HOST:-192.168.20.13}" SSH_USER="${SSH_USER:-sam}" WF_ID="schoolMorningBrf" WF_NAME="Compass / NHS — 6:00am morning brief" CRED_ID="schoolScanKey" CRED_NAME="school-scan restricted key" IN_CONTAINER_WF="/tmp/n8n-morning-brief.json" IN_CONTAINER_CRED="/tmp/n8n-school-scan-cred.json" APPLY=0 RESTART=1 for arg in "$@"; do case "$arg" in --apply) APPLY=1 ;; --no-restart) RESTART=0 ;; -h|--help) sed -n '2,30p' "$0"; exit 0 ;; *) echo "unknown argument: $arg" >&2; exit 2 ;; esac done [[ -f "$WORKFLOW" ]] || { echo "workflow not found: $WORKFLOW" >&2; exit 1; } n8n_cli() { docker exec "$CONTAINER" n8n "$@"; } echo "workflow file : $WORKFLOW" echo "n8n container : $CONTAINER" echo "key : $KEY" echo "ssh target : $SSH_USER@$SSH_HOST" echo "restart n8n : $([[ "$RESTART" -eq 1 ]] && echo yes || echo no)" if [[ "$APPLY" -eq 0 ]]; then echo echo "DRY RUN — no change to n8n. Re-run with --apply." echo "steps:" echo " 1. build the SSH credential from $KEY and import it (id $CRED_ID)" echo " 2. copy the workflow into the container" echo " 3. n8n import:workflow" echo " 4. n8n publish:workflow --id=$WF_ID" echo " 5. $([[ "$RESTART" -eq 1 ]] && echo 'docker restart n8n (≈10s)' || echo 'skip restart')" echo " 6. confirm the n8n log shows the workflow activated" exit 0 fi [[ -f "$KEY" ]] || { echo "private key not found: $KEY (run deploy/setup-n8n-scan-key.sh --apply)" >&2; exit 1; } CRED_TMP="$(mktemp)" AUTH_TMP="$(mktemp)" cleanup() { rm -f "$CRED_TMP" "$AUTH_TMP"; docker exec "$CONTAINER" rm -f "$IN_CONTAINER_CRED" "$IN_CONTAINER_WF" >/dev/null 2>&1 || true; } trap cleanup EXIT # 1. Build the credential JSON without ever printing the key. umask 077 python3 - "$KEY" "$CRED_ID" "$CRED_NAME" "$SSH_HOST" "$SSH_USER" > "$CRED_TMP" <<'PY' import json, sys, pathlib key_path, cred_id, cred_name, host, user = sys.argv[1:6] key = pathlib.Path(key_path).read_text() json.dump([{ "id": cred_id, "name": cred_name, "type": "sshPrivateKey", "data": {"host": host, "port": 22, "username": user, "privateKey": key, "passphrase": ""}, }], sys.stdout, indent=2) PY echo echo "== 1. import the SSH credential ==" docker exec -i "$CONTAINER" sh -c "cat > $IN_CONTAINER_CRED" < "$CRED_TMP" n8n_cli import:credentials --input="$IN_CONTAINER_CRED" echo "== 2. copy the workflow into the container ==" docker exec -i "$CONTAINER" sh -c "cat > $IN_CONTAINER_WF" < "$WORKFLOW" echo "== 3. import ==" n8n_cli import:workflow --input="$IN_CONTAINER_WF" echo "== 4. publish ==" n8n_cli publish:workflow --id="$WF_ID" echo "== 5. restart ==" if [[ "$RESTART" -eq 1 ]]; then docker restart "$CONTAINER" >/dev/null echo "n8n restarted; waiting for the API to answer…" for _ in $(seq 1 30); do if curl -s -o /dev/null --max-time 3 "http://127.0.0.1:5678/healthz"; then echo "n8n is up" break fi sleep 2 done sleep 3 fi echo "== 6. verify activation in the log ==" if docker logs "$CONTAINER" 2>&1 | grep -q "Activated workflow \"$WF_NAME\""; then echo "activated: $WF_NAME" else echo "the n8n log does not show an activation for '$WF_NAME' yet" >&2 docker logs "$CONTAINER" 2>&1 | grep -iE "schoolMorningBrf|$WF_NAME" | tail -5 >&2 || true echo "check: docker logs $CONTAINER | grep -i 'did fail'" >&2 exit 1 fi n8n_cli list:workflow --active=true | grep "$WF_ID" || true