Skip to content

Commit 9e94737

Browse files
committed
feat(dev): add --tasks-preview=N flag to nix run .#dev
Allows connecting to an opencouncil-tasks preview deployment for a given PR number. Starts an ngrok tunnel so the remote tasks server can POST callbacks back to localhost, and overrides NEXTAUTH_URL to the ngrok URL for correct URL construction.
1 parent 4798fc7 commit 9e94737

File tree

2 files changed

+151
-20
lines changed

2 files changed

+151
-20
lines changed

flake.lock

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 133 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
# We pin via flake.lock, but choose a channel that contains Prisma 5.22.x
77
# so NixOS can use nixpkgs-provided Prisma engines without version mismatches.
88
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
9+
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
910
};
1011

11-
outputs = { self, nixpkgs }:
12+
outputs = { self, nixpkgs, nixpkgs-unstable }:
1213
let
1314
systems = [
1415
"x86_64-linux"
@@ -18,7 +19,13 @@
1819
];
1920

2021
forAllSystems =
21-
f: nixpkgs.lib.genAttrs systems (system: f system (import nixpkgs { inherit system; }));
22+
f: nixpkgs.lib.genAttrs systems (system: f system
23+
(import nixpkgs { inherit system; })
24+
(import nixpkgs-unstable {
25+
inherit system;
26+
config.allowUnfreePredicate = pkg:
27+
builtins.elem (nixpkgs-unstable.lib.getName pkg) [ "ngrok" ];
28+
}));
2229

2330
# Shared PostGIS 3.3.5 builder - used by dev packages and preview module
2431
# This ensures both use the same locked version matching production
@@ -51,7 +58,7 @@
5158
in {
5259
# Export shared builders for use by nixosModules
5360
lib = { inherit mkPostgis335 mkPostgresCompat mkPrismaEnv mkOpenSslEnv; };
54-
devShells = forAllSystems (_system: pkgs: {
61+
devShells = forAllSystems (_system: pkgs: _pkgs-unstable: {
5562
default = pkgs.mkShell {
5663
buildInputs =
5764
(with pkgs; [
@@ -131,7 +138,7 @@
131138
};
132139
});
133140

134-
packages = forAllSystems (_system: pkgs:
141+
packages = forAllSystems (_system: pkgs: pkgs-unstable:
135142
let
136143
# Default postgres - uses nixpkgs PostGIS (fast, pre-built from binary cache)
137144
postgres = pkgs.postgresql_16.withPackages (ps: [ ps.postgis ]);
@@ -485,6 +492,8 @@ USAGE
485492
pkgs.nodejs
486493
pkgs.nodePackages.npm
487494
pkgs.curl
495+
pkgs-unstable.ngrok
496+
pkgs.jq
488497
oc-dev-db-nix
489498
oc-dev-db-nix-locked
490499
oc-dev-db-docker
@@ -502,7 +511,7 @@ USAGE
502511
usage() {
503512
cat <<'USAGE'
504513
Usage:
505-
nix run .#dev -- [--db=remote|external|nix|docker] [--db-url URL] [--direct-url URL] [--migrate] [--no-studio] [--locked] [--no-lan]
514+
nix run .#dev -- [--db=remote|external|nix|docker] [--db-url URL] [--direct-url URL] [--migrate] [--no-studio] [--locked] [--no-lan] [--tasks-preview=N]
506515
507516
DB modes:
508517
--db=nix Start Postgres+PostGIS via Nix + app (process-compose TUI) (default)
@@ -511,10 +520,11 @@ DB modes:
511520
--db=docker Start Docker PostGIS + app (requires Docker)
512521
513522
Flags:
514-
--migrate Run migrations (npm run db:deploy) before starting the app (remote/external only)
515-
--no-studio Disable Prisma Studio process (enabled by default for local DB modes)
516-
--fast Use pre-built PostGIS from binary cache (faster first build, but may not match production)
517-
--no-lan Bind dev server to localhost only (default: binds to 0.0.0.0 for mobile preview)
523+
--migrate Run migrations (npm run db:deploy) before starting the app (remote/external only)
524+
--no-studio Disable Prisma Studio process (enabled by default for local DB modes)
525+
--fast Use pre-built PostGIS from binary cache (faster first build, but may not match production)
526+
--no-lan Bind dev server to localhost only (default: binds to 0.0.0.0 for mobile preview)
527+
--tasks-preview=N Connect to opencouncil-tasks preview for PR #N (starts ngrok tunnel for callbacks)
518528
USAGE
519529
}
520530
@@ -528,6 +538,7 @@ USAGE
528538
studio_enabled=""
529539
postgis_locked="''${OC_POSTGIS_LOCKED:-1}"
530540
lan_enabled="''${OC_LAN:-1}"
541+
tasks_preview_pr="''${OC_TASKS_PREVIEW:-}"
531542
532543
for arg in "$@"; do
533544
case "$arg" in
@@ -538,6 +549,7 @@ USAGE
538549
--no-studio) studio_override="0" ;;
539550
--fast) postgis_locked="0" ;;
540551
--no-lan) lan_enabled="0" ;;
552+
--tasks-preview=*) tasks_preview_pr="''${arg#--tasks-preview=}" ;;
541553
--help|-h) usage; exit 0 ;;
542554
*)
543555
echo "Unknown argument: $arg" >&2
@@ -603,16 +615,96 @@ USAGE
603615
studio_port="$(find_available_port 5555)"
604616
fi
605617
606-
# Check if TASK_API_URL is configured and reachable (non-blocking warning)
607-
if [ -n "''${TASK_API_URL:-}" ]; then
608-
echo "🔗 Task API configured: $TASK_API_URL"
609-
if command -v curl >/dev/null 2>&1; then
610-
if curl -sf --connect-timeout 2 "$TASK_API_URL/health" >/dev/null 2>&1; then
611-
echo " ✓ Task server is reachable"
618+
# Check task API health and optionally validate API key.
619+
# Usage: check_task_api <url> <strict>
620+
# strict=1: exit on failure (for --tasks-preview)
621+
# strict=0: warn on failure (for .env TASK_API_URL)
622+
check_task_api() {
623+
local url="$1" strict="$2"
624+
local health_json auth_ok
625+
626+
# Health check (with auth header if TASK_API_KEY is set)
627+
health_json=$(curl -sf --connect-timeout 5 \
628+
''${TASK_API_KEY:+-H "Authorization: Bearer $TASK_API_KEY"} \
629+
"$url/health" 2>/dev/null) || {
630+
if [ "$strict" = "1" ]; then
631+
echo " ✗ Not reachable at $url" >&2
632+
exit 1
612633
else
613634
echo " ⚠ Task server not reachable (start it separately for E2E testing)"
635+
return 1
636+
fi
637+
}
638+
echo " ✓ Task server is reachable"
639+
640+
# Validate API key if one was sent
641+
if [ -n "''${TASK_API_KEY:-}" ]; then
642+
auth_ok=$(echo "$health_json" | jq -r '.authenticated' 2>/dev/null || true)
643+
if [ "$auth_ok" = "true" ]; then
644+
echo " ✓ API key accepted"
645+
elif [ "$auth_ok" = "false" ]; then
646+
if [ "$strict" = "1" ]; then
647+
echo " ✗ TASK_API_KEY is not valid for this server." >&2
648+
echo " Check the token in your .env matches the server." >&2
649+
exit 1
650+
else
651+
echo " ⚠ TASK_API_KEY is not valid for this server"
652+
fi
614653
fi
615654
fi
655+
}
656+
657+
# --tasks-preview: connect to a remote tasks preview and start ngrok for callbacks
658+
ngrok_pid=""
659+
if [ -n "$tasks_preview_pr" ]; then
660+
tasks_preview_url="https://pr-''${tasks_preview_pr}.tasks.opencouncil.gr"
661+
export TASK_API_URL="$tasks_preview_url"
662+
if [ -z "''${TASK_API_KEY:-}" ]; then
663+
echo " ✗ TASK_API_KEY is not set. Set it in .env or export it." >&2
664+
echo " The tasks preview server requires a valid API token." >&2
665+
exit 1
666+
fi
667+
668+
echo "🔗 Connecting to tasks preview PR #$tasks_preview_pr..."
669+
check_task_api "$tasks_preview_url" 1
670+
671+
# Start ngrok tunnel so the remote tasks server can POST callbacks to localhost
672+
echo " Starting ngrok tunnel for localhost:$app_port..."
673+
ngrok http "$app_port" --log=stdout > "$logs_dir/ngrok.log" 2>&1 &
674+
ngrok_pid=$!
675+
676+
# Wait for ngrok to assign a public URL
677+
ngrok_url=""
678+
for _i in $(seq 1 30); do
679+
ngrok_url=$(curl -sf http://localhost:4040/api/tunnels 2>/dev/null \
680+
| jq -r '.tunnels[] | select(.proto == "https") | .public_url' 2>/dev/null || true)
681+
if [ -n "$ngrok_url" ] && [ "$ngrok_url" != "null" ]; then
682+
break
683+
fi
684+
sleep 0.5
685+
done
686+
687+
if [ -z "$ngrok_url" ] || [ "$ngrok_url" = "null" ]; then
688+
echo " ✗ Failed to start ngrok tunnel." >&2
689+
echo " Check $logs_dir/ngrok.log" >&2
690+
echo " If first run: ngrok config add-authtoken <TOKEN>" >&2
691+
kill "$ngrok_pid" 2>/dev/null || true
692+
exit 1
693+
fi
694+
695+
export NEXTAUTH_URL="$ngrok_url"
696+
echo " ✓ Tunnel active: $ngrok_url → localhost:$app_port"
697+
echo ""
698+
echo " Tasks API: $tasks_preview_url"
699+
echo " Callbacks: $ngrok_url"
700+
echo " Ngrok logs: $logs_dir/ngrok.log"
701+
echo ""
702+
fi
703+
704+
# Check if TASK_API_URL is configured and reachable (non-blocking warning)
705+
if [ -z "$tasks_preview_pr" ] && [ -n "''${TASK_API_URL:-}" ]; then
706+
echo "🔗 Task API configured: $TASK_API_URL"
707+
check_task_api "$TASK_API_URL" 0 || true
616708
echo ""
617709
fi
618710
@@ -735,8 +827,13 @@ EOF
735827
736828
pc_port="$(find_available_port 8080)"
737829
738-
# On Linux with --lan, open the firewall port automatically and
739-
# clean it up when the dev runner exits.
830+
# Determine if we need cleanup on exit (firewall rule and/or ngrok).
831+
# When cleanup is needed we run process-compose without exec so the
832+
# trap fires after it exits. Otherwise we exec for a cleaner process tree.
833+
needs_cleanup=false
834+
cleanup_cmds=""
835+
836+
# On Linux with --lan, open the firewall port automatically.
740837
if [ "$lan_enabled" = "1" ] && command -v iptables >/dev/null 2>&1 \
741838
&& ! sudo -n iptables -C INPUT -p tcp --dport "$app_port" -j ACCEPT 2>/dev/null; then
742839
echo ""
@@ -745,7 +842,24 @@ EOF
745842
echo ""
746843
sudo iptables -I INPUT -p tcp --dport "$app_port" -j ACCEPT
747844
echo "Opened firewall port $app_port for LAN access"
748-
trap 'sudo iptables -D INPUT -p tcp --dport "$app_port" -j ACCEPT 2>/dev/null; echo "Closed firewall port $app_port"' EXIT
845+
needs_cleanup=true
846+
cleanup_cmds="sudo iptables -D INPUT -p tcp --dport \"$app_port\" -j ACCEPT 2>/dev/null; echo \"Closed firewall port $app_port\";"
847+
fi
848+
849+
# If ngrok is running, ensure it gets cleaned up on exit.
850+
if [ -n "$ngrok_pid" ]; then
851+
needs_cleanup=true
852+
cleanup_cmds="''${cleanup_cmds}kill $ngrok_pid 2>/dev/null || true; echo \"Stopped ngrok tunnel\";"
853+
fi
854+
855+
# Brief pause so startup messages are readable before TUI takes over
856+
echo "Starting process-compose..."
857+
sleep 5
858+
859+
if [ "$needs_cleanup" = "true" ]; then
860+
cleanup_cmds="''${cleanup_cmds}rm -rf \"$tmp_dir\";"
861+
# shellcheck disable=SC2064 # Intentional: expand commands now, not at signal time
862+
trap "$cleanup_cmds" EXIT
749863
process-compose -f "$pc_file" up --port "$pc_port"
750864
else
751865
exec process-compose -f "$pc_file" up --port "$pc_port"
@@ -1604,7 +1718,7 @@ CADDYEOF
16041718
};
16051719
};
16061720

1607-
apps = forAllSystems (system: pkgs: {
1721+
apps = forAllSystems (system: pkgs: _pkgs-unstable: {
16081722
dev = {
16091723
type = "app";
16101724
program = "${self.packages.${system}.oc-dev}/bin/oc-dev";

0 commit comments

Comments
 (0)