From 55704ff98fff53a1c36fa79497efff4a062c7d52 Mon Sep 17 00:00:00 2001 From: Blas Date: Fri, 26 Jun 2026 06:10:03 -0300 Subject: [PATCH 1/7] feat: add upgrade integration test workflow (manual, matrix-based) --- .../workflows/upgrade-integration-test.yml | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 .github/workflows/upgrade-integration-test.yml diff --git a/.github/workflows/upgrade-integration-test.yml b/.github/workflows/upgrade-integration-test.yml new file mode 100644 index 0000000000..05d789c692 --- /dev/null +++ b/.github/workflows/upgrade-integration-test.yml @@ -0,0 +1,418 @@ +# Upgrade Integration Test +# +# Tests that Dokploy can upgrade from version A to version B while keeping +# user projects (Postgres, MongoDB, two web apps, one static site) alive. +# +# Two modes, combined into a single matrix: +# • Auto: every stable tag >= floor_version → latest stable tag (N jobs) +# • Curated: hand-picked pairs (default: latest v0.25.x → latest v0.26.x) +# +# Only triggered manually to avoid burning Actions minutes. + +name: Upgrade Integration Test + +on: + workflow_dispatch: + inputs: + floor_version: + description: 'Oldest version tag to include in auto pairs (e.g. v0.20.0)' + required: false + default: 'v0.20.0' + extra_pairs: + description: 'Extra pairs as JSON, e.g. [{"from":"v0.25.2","to":"v0.26.0"}]' + required: false + default: '' + +env: + DOKPLOY_IMAGE: dokploy/dokploy + DOKPLOY_SERVICE: dokploy + DOKPLOY_PORT: 3000 + +# ────────────────────────────────────────────────────────────────────────────── +jobs: + + # ── 1. Build the matrix ───────────────────────────────────────────────────── + build-matrix: + name: Build upgrade pair matrix + runs-on: ubuntu-latest + outputs: + pairs: ${{ steps.pairs.outputs.pairs }} + + steps: + - name: Generate pairs + id: pairs + env: + FLOOR: ${{ inputs.floor_version }} + EXTRA: ${{ inputs.extra_pairs }} + run: | + set -euo pipefail + + # Fetch all semver tags from Docker Hub (dokploy/dokploy) + ALL_TAGS=$(curl -fsSL \ + "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags?page_size=100" | \ + jq -r '.results[].name' | \ + grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \ + sort -V) + + LATEST=$(echo "$ALL_TAGS" | tail -n1) + echo "Latest tag on Docker Hub: $LATEST" + + # Floor semver components + FLOOR_CLEAN="${FLOOR#v}" + IFS='.' read -r F_MAJ F_MIN F_PAT <<< "$FLOOR_CLEAN" + + # Build "each version >= FLOOR, != LATEST → LATEST" pairs + AUTO_PAIRS=$(echo "$ALL_TAGS" | while read -r TAG; do + [ "$TAG" = "$LATEST" ] && continue + TAG_CLEAN="${TAG#v}" + IFS='.' read -r T_MAJ T_MIN T_PAT <<< "$TAG_CLEAN" + if [ "$T_MAJ" -gt "$F_MAJ" ] || \ + { [ "$T_MAJ" -eq "$F_MAJ" ] && [ "$T_MIN" -gt "$F_MIN" ]; } || \ + { [ "$T_MAJ" -eq "$F_MAJ" ] && [ "$T_MIN" -eq "$F_MIN" ] && [ "$T_PAT" -ge "$F_PAT" ]; }; then + printf '{"from":"%s","to":"%s"}\n' "$TAG" "$LATEST" + fi + done | jq -sc '.') + + # Curated pairs (latest v0.25.x → latest v0.26.x) + # Find the highest v0.25.x and v0.26.x tags from Docker Hub + V25=$(echo "$ALL_TAGS" | grep '^v0\.25\.' | tail -n1) + V26=$(echo "$ALL_TAGS" | grep '^v0\.26\.' | tail -n1) + CURATED='[]' + if [ -n "$V25" ] && [ -n "$V26" ] && [ "$V25" != "$V26" ]; then + CURATED=$(jq -cn --arg f "$V25" --arg t "$V26" '[{"from":$f,"to":$t}]') + fi + + EXTRA_JSON="${EXTRA:-[]}" + if [ -z "$EXTRA_JSON" ] || [ "$EXTRA_JSON" = "" ]; then + EXTRA_JSON="[]" + fi + + # Merge all sources, deduplicate by "from+to" + ALL=$(jq -cn \ + --argjson auto "$AUTO_PAIRS" \ + --argjson curated "$CURATED" \ + --argjson extra "$EXTRA_JSON" \ + '$auto + $curated + $extra | unique_by(.from + "-" + .to)') + + COUNT=$(echo "$ALL" | jq 'length') + echo "Total pairs: $COUNT" + echo "$ALL" | jq -r '.[] | " \(.from) → \(.to)"' + + echo "pairs=$ALL" >> "$GITHUB_OUTPUT" + + + # ── 2. Run one upgrade test per pair ──────────────────────────────────────── + upgrade-test: + name: "${{ matrix.pair.from }} → ${{ matrix.pair.to }}" + needs: build-matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + pair: ${{ fromJSON(needs.build-matrix.outputs.pairs) }} + + steps: + + # ── Environment setup ────────────────────────────────────────────────── + - name: Free disk space + run: | + sudo rm -rf \ + /usr/share/dotnet /opt/ghc /usr/local/share/boost \ + "$AGENT_TOOLSDIRECTORY" /usr/local/lib/android \ + /usr/local/share/chromium /opt/hostedtoolcache + docker system prune -af --volumes + df -h + + - name: Pre-init Docker Swarm (so install.sh skips its own swarm init) + run: | + docker swarm init --advertise-addr 127.0.0.1 + docker network create --driver overlay --attachable dokploy-network || true + + # ── Install Dokploy infrastructure ───────────────────────────────────── + - name: Run Dokploy install.sh (sets up Traefik, Redis, Postgres, service) + run: curl -sSL https://dokploy.com/install.sh | bash + + # ── Pin to version A before any data is written ──────────────────────── + - name: Pin Dokploy to VERSION A (${{ matrix.pair.from }}) + run: | + docker service update \ + --image "${{ env.DOKPLOY_IMAGE }}:${{ matrix.pair.from }}" \ + --force \ + "${{ env.DOKPLOY_SERVICE }}" + + echo "Waiting for service to converge on ${{ matrix.pair.from }}..." + timeout 180 bash -c ' + until ! docker service inspect dokploy \ + --format "{{.UpdateStatus.State}}" 2>/dev/null \ + | grep -q "^updating$"; do + sleep 4 + done + until docker service ls --filter name=dokploy \ + --format "{{.Name}} {{.Replicas}}" \ + | grep "^dokploy " | grep -q " 1/1"; do + sleep 4 + done + ' + echo "✅ Service running on ${{ matrix.pair.from }}" + + - name: Wait for Dokploy API to accept requests + run: | + timeout 120 bash -c ' + until curl -sf -o /dev/null \ + "http://localhost:${{ env.DOKPLOY_PORT }}"; do + sleep 3 + done + ' + echo "✅ Dokploy API is up" + + # ── Bootstrap admin user ─────────────────────────────────────────────── + - name: Register first admin user + id: auth + run: | + COOKIE_JAR=$(mktemp /tmp/dokploy-cookies-XXXXXX) + echo "cookie_jar=$COOKIE_JAR" >> "$GITHUB_OUTPUT" + + # better-auth: sign-up/email (allowed only before any owner exists) + RESP=$(curl -sf -X POST \ + "http://localhost:${{ env.DOKPLOY_PORT }}/api/auth/sign-up/email" \ + -H "Content-Type: application/json" \ + -c "$COOKIE_JAR" -b "$COOKIE_JAR" \ + -d '{"name":"CI Admin","email":"ci@dokploy.test","password":"CiTest1234!"}') + + echo "Sign-up response: $RESP" + # Session cookie is stored in the jar; also capture token if present + TOKEN=$(echo "$RESP" | jq -r '.token // empty' 2>/dev/null || true) + if [ -n "$TOKEN" ]; then + echo "::add-mask::$TOKEN" + # Inject token as a cookie for older versions that don't use cookie jar + echo "dokploy.com FALSE / FALSE 0 auth_token $TOKEN" >> "$COOKIE_JAR" + fi + + # ── Create test resources ────────────────────────────────────────────── + - name: Create project, databases and applications + id: create + env: + BASE: "http://localhost:${{ env.DOKPLOY_PORT }}" + COOKIE: ${{ steps.auth.outputs.cookie_jar }} + run: | + # --- tRPC helpers --- + # Mutation (POST) + trpc_mut() { + curl -sf -X POST "$BASE/api/trpc/$1" \ + -H "Content-Type: application/json" \ + -b "$COOKIE" \ + -d "{\"json\":$2}" + } + + # --- Project --- + PROJECT=$(trpc_mut project.create \ + '{"name":"ci-upgrade-test","description":"CI upgrade integration test"}') + echo "project.create → $PROJECT" + + PROJECT_ID=$(echo "$PROJECT" | jq -r '.result.data.json.project.projectId') + ENV_ID=$(echo "$PROJECT" | jq -r '.result.data.json.environment.environmentId') + echo "project_id=$PROJECT_ID" >> "$GITHUB_OUTPUT" + echo "env_id=$ENV_ID" >> "$GITHUB_OUTPUT" + + # --- PostgreSQL 15 --- + PG=$(trpc_mut postgres.create \ + "{\"name\":\"ci-pg\",\"appName\":\"ci-pg-db\",\ + \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ + \"databasePassword\":\"CiPg1!\",\ + \"dockerImage\":\"postgres:15\",\"environmentId\":\"$ENV_ID\"}") + PG_ID=$(echo "$PG" | jq -r '.result.data.json.postgresId') + trpc_mut postgres.start "{\"postgresId\":\"$PG_ID\"}" > /dev/null + echo "pg_id=$PG_ID" >> "$GITHUB_OUTPUT" + + # --- MongoDB 7.0 --- + MG=$(trpc_mut mongo.create \ + "{\"name\":\"ci-mongo\",\"appName\":\"ci-mongo-db\",\ + \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ + \"databasePassword\":\"CiMg1!\",\ + \"dockerImage\":\"mongo:7.0\",\"environmentId\":\"$ENV_ID\"}") + MG_ID=$(echo "$MG" | jq -r '.result.data.json.mongoId') + trpc_mut mongo.start "{\"mongoId\":\"$MG_ID\"}" > /dev/null + echo "mg_id=$MG_ID" >> "$GITHUB_OUTPUT" + + # --- Docker-image application helper --- + # Creates an application, sets Docker image provider, triggers deploy. + make_app() { + local DISP_NAME=$1 APP_NAME=$2 IMAGE=$3 + APP=$(trpc_mut application.create \ + "{\"name\":\"$DISP_NAME\",\"appName\":\"$APP_NAME\",\ + \"environmentId\":\"$ENV_ID\"}") + APP_ID=$(echo "$APP" | jq -r '.result.data.json.applicationId') + trpc_mut application.saveDockerProvider \ + "{\"applicationId\":\"$APP_ID\",\"dockerImage\":\"$IMAGE\",\ + \"username\":\"\",\"password\":\"\",\"registryUrl\":\"\"}" \ + > /dev/null + trpc_mut application.deploy "{\"applicationId\":\"$APP_ID\"}" \ + > /dev/null + echo "$APP_ID" + } + + # Static site (nginx) + APP_STATIC=$(make_app "ci-static" "ci-static-app" "nginx:alpine") + echo "app_static_id=$APP_STATIC" >> "$GITHUB_OUTPUT" + + # Node.js hello-world (echo server image — small, no args needed) + APP_NODE=$(make_app "ci-node" "ci-node-app" "ealen/echo-server:latest") + echo "app_node_id=$APP_NODE" >> "$GITHUB_OUTPUT" + + # Go HTTP server (traefik/whoami is a tiny Go binary, port 80) + APP_GO=$(make_app "ci-go" "ci-go-app" "traefik/whoami:latest") + echo "app_go_id=$APP_GO" >> "$GITHUB_OUTPUT" + + echo "✅ All resources created" + + # ── Pre-upgrade health check ─────────────────────────────────────────── + - name: Wait for all services → 'done' (pre-upgrade) + env: + BASE: "http://localhost:${{ env.DOKPLOY_PORT }}" + COOKIE: ${{ steps.auth.outputs.cookie_jar }} + PG_ID: ${{ steps.create.outputs.pg_id }} + MG_ID: ${{ steps.create.outputs.mg_id }} + APP_STATIC_ID: ${{ steps.create.outputs.app_static_id }} + APP_NODE_ID: ${{ steps.create.outputs.app_node_id }} + APP_GO_ID: ${{ steps.create.outputs.app_go_id }} + run: | + # Poll a tRPC query endpoint until the service status field == "done" + wait_done() { + local NAME=$1 ENDPOINT=$2 ID_KEY=$3 ID=$4 STATUS_KEY=$5 + echo "Waiting for $NAME to reach 'done'..." + timeout 300 bash -c " + until [ \"\$(curl -sf -G '$BASE/api/trpc/$ENDPOINT' \ + --data-urlencode 'input={\"json\":{\"$ID_KEY\":\"$ID\"}}' \ + -b '$COOKIE' | \ + jq -r '.result.data.json.$STATUS_KEY // \"unknown\"')\" \ + = 'done' ]; do + sleep 5 + done + " + echo "✅ $NAME is done" + } + + wait_done postgres postgres.one postgresId "$PG_ID" applicationStatus + wait_done mongo mongo.one mongoId "$MG_ID" applicationStatus + wait_done static application.one applicationId "$APP_STATIC_ID" applicationStatus + wait_done node-app application.one applicationId "$APP_NODE_ID" applicationStatus + wait_done go-app application.one applicationId "$APP_GO_ID" applicationStatus + + - name: Assert Docker Swarm services healthy (pre-upgrade) + run: | + echo "=== docker service ls ===" + docker service ls + + # User services should all be 1/1; check none are 0/1 + FAIL=$(docker service ls --format '{{.Name}} {{.Replicas}}' | \ + grep -E '^ci-' | grep -v ' 1/1' || true) + if [ -n "$FAIL" ]; then + echo "❌ User services not healthy before upgrade:" + echo "$FAIL" + exit 1 + fi + echo "✅ All user services healthy before upgrade" + + # ── Upgrade ──────────────────────────────────────────────────────────── + - name: Upgrade Dokploy to VERSION B (${{ matrix.pair.to }}) + run: | + docker service update \ + --image "${{ env.DOKPLOY_IMAGE }}:${{ matrix.pair.to }}" \ + --force \ + "${{ env.DOKPLOY_SERVICE }}" + + echo "Waiting for service to converge on ${{ matrix.pair.to }}..." + timeout 180 bash -c ' + until ! docker service inspect dokploy \ + --format "{{.UpdateStatus.State}}" 2>/dev/null \ + | grep -q "^updating$"; do + sleep 4 + done + until docker service ls --filter name=dokploy \ + --format "{{.Name}} {{.Replicas}}" \ + | grep "^dokploy " | grep -q " 1/1"; do + sleep 4 + done + ' + echo "✅ Service running on ${{ matrix.pair.to }}" + + - name: Wait for Dokploy API to respond post-upgrade + run: | + timeout 120 bash -c ' + until curl -sf -o /dev/null \ + "http://localhost:${{ env.DOKPLOY_PORT }}"; do + sleep 3 + done + ' + echo "✅ Dokploy API is up after upgrade" + + # ── Post-upgrade health check ────────────────────────────────────────── + - name: Verify all services still healthy (post-upgrade) + env: + BASE: "http://localhost:${{ env.DOKPLOY_PORT }}" + COOKIE: ${{ steps.auth.outputs.cookie_jar }} + PG_ID: ${{ steps.create.outputs.pg_id }} + MG_ID: ${{ steps.create.outputs.mg_id }} + APP_STATIC_ID: ${{ steps.create.outputs.app_static_id }} + APP_NODE_ID: ${{ steps.create.outputs.app_node_id }} + APP_GO_ID: ${{ steps.create.outputs.app_go_id }} + run: | + check_status() { + local NAME=$1 ENDPOINT=$2 ID_KEY=$3 ID=$4 STATUS_KEY=$5 + STATUS=$(curl -sf -G "$BASE/api/trpc/$ENDPOINT" \ + --data-urlencode "input={\"json\":{\"$ID_KEY\":\"$ID\"}}" \ + -b "$COOKIE" | \ + jq -r ".result.data.json.$STATUS_KEY // \"unknown\"") + if [ "$STATUS" != "done" ]; then + echo "❌ $NAME status after upgrade: $STATUS" + return 1 + fi + echo "✅ $NAME: $STATUS" + } + + check_status postgres postgres.one postgresId "$PG_ID" applicationStatus + check_status mongo mongo.one mongoId "$MG_ID" applicationStatus + check_status static application.one applicationId "$APP_STATIC_ID" applicationStatus + check_status node-app application.one applicationId "$APP_NODE_ID" applicationStatus + check_status go-app application.one applicationId "$APP_GO_ID" applicationStatus + + echo "=== docker service ls (post-upgrade) ===" + docker service ls + + FAIL=$(docker service ls --format '{{.Name}} {{.Replicas}}' | \ + grep -E '^ci-' | grep -v ' 1/1' || true) + if [ -n "$FAIL" ]; then + echo "❌ User services not healthy after upgrade:" + echo "$FAIL" + exit 1 + fi + echo "✅ All services healthy after upgrade to ${{ matrix.pair.to }}" + + # ── Diagnostics on failure ───────────────────────────────────────────── + - name: Dump state on failure + if: failure() + run: | + echo "=== docker service ls ===" && docker service ls || true + echo "=== dokploy service tasks ===" && \ + docker service ps "${{ env.DOKPLOY_SERVICE }}" --no-trunc || true + echo "=== dokploy logs (last 100) ===" && \ + docker service logs "${{ env.DOKPLOY_SERVICE }}" --tail 100 2>&1 || true + echo "=== disk usage ===" && df -h + + # ── Job summary ──────────────────────────────────────────────────────── + - name: Write job summary + if: always() + run: | + STATUS="${{ job.status }}" + ICON="✅"; [ "$STATUS" != "success" ] && ICON="❌" + cat >> "$GITHUB_STEP_SUMMARY" < Date: Fri, 26 Jun 2026 09:32:52 +0000 Subject: [PATCH 2/7] fix(ci): run dokploy install.sh as root, install version A directly via DOKPLOY_VERSION Previous run failed with 'This script must be run as root'. install.sh also respects DOKPLOY_VERSION and ADVERTISE_ADDR env vars, so we can install version A directly instead of installing latest and downgrading (which would risk B's migrations running on A's expected schema). --- .../workflows/upgrade-integration-test.yml | 96 ++++++++++--------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/.github/workflows/upgrade-integration-test.yml b/.github/workflows/upgrade-integration-test.yml index 05d789c692..4f1b1d3b2a 100644 --- a/.github/workflows/upgrade-integration-test.yml +++ b/.github/workflows/upgrade-integration-test.yml @@ -123,41 +123,39 @@ jobs: docker system prune -af --volumes df -h - - name: Pre-init Docker Swarm (so install.sh skips its own swarm init) + # ── Install Dokploy directly at VERSION A ────────────────────────────── + # install.sh: + # • requires root (run via sudo bash) + # • respects DOKPLOY_VERSION → installs that tag directly + # (so we don't need a separate "downgrade" step that would risk + # running B's migrations on A's expected schema) + # • respects ADVERTISE_ADDR → skip the external IP lookup + # • initializes Docker Swarm + dokploy-network itself + - name: Install Dokploy at VERSION A (${{ matrix.pair.from }}) run: | - docker swarm init --advertise-addr 127.0.0.1 - docker network create --driver overlay --attachable dokploy-network || true - - # ── Install Dokploy infrastructure ───────────────────────────────────── - - name: Run Dokploy install.sh (sets up Traefik, Redis, Postgres, service) - run: curl -sSL https://dokploy.com/install.sh | bash - - # ── Pin to version A before any data is written ──────────────────────── - - name: Pin Dokploy to VERSION A (${{ matrix.pair.from }}) + curl -fsSL https://dokploy.com/install.sh -o /tmp/install.sh + chmod +x /tmp/install.sh + sudo -E env \ + DOKPLOY_VERSION="${{ matrix.pair.from }}" \ + ADVERTISE_ADDR="127.0.0.1" \ + bash /tmp/install.sh + + - name: Wait for dokploy service to converge on ${{ matrix.pair.from }} run: | - docker service update \ - --image "${{ env.DOKPLOY_IMAGE }}:${{ matrix.pair.from }}" \ - --force \ - "${{ env.DOKPLOY_SERVICE }}" - - echo "Waiting for service to converge on ${{ matrix.pair.from }}..." - timeout 180 bash -c ' - until ! docker service inspect dokploy \ - --format "{{.UpdateStatus.State}}" 2>/dev/null \ - | grep -q "^updating$"; do - sleep 4 - done + echo "Waiting for 'dokploy' Swarm service to reach 1/1..." + timeout 240 bash -c ' until docker service ls --filter name=dokploy \ --format "{{.Name}} {{.Replicas}}" \ | grep "^dokploy " | grep -q " 1/1"; do sleep 4 done ' + docker service ls echo "✅ Service running on ${{ matrix.pair.from }}" - name: Wait for Dokploy API to accept requests run: | - timeout 120 bash -c ' + timeout 180 bash -c ' until curl -sf -o /dev/null \ "http://localhost:${{ env.DOKPLOY_PORT }}"; do sleep 3 @@ -169,23 +167,25 @@ jobs: - name: Register first admin user id: auth run: | - COOKIE_JAR=$(mktemp /tmp/dokploy-cookies-XXXXXX) + COOKIE_JAR="$RUNNER_TEMP/dokploy-cookies.txt" + : > "$COOKIE_JAR" + chmod 600 "$COOKIE_JAR" echo "cookie_jar=$COOKIE_JAR" >> "$GITHUB_OUTPUT" # better-auth: sign-up/email (allowed only before any owner exists) - RESP=$(curl -sf -X POST \ + set -x + curl -sS -i -X POST \ "http://localhost:${{ env.DOKPLOY_PORT }}/api/auth/sign-up/email" \ -H "Content-Type: application/json" \ -c "$COOKIE_JAR" -b "$COOKIE_JAR" \ - -d '{"name":"CI Admin","email":"ci@dokploy.test","password":"CiTest1234!"}') - - echo "Sign-up response: $RESP" - # Session cookie is stored in the jar; also capture token if present - TOKEN=$(echo "$RESP" | jq -r '.token // empty' 2>/dev/null || true) - if [ -n "$TOKEN" ]; then - echo "::add-mask::$TOKEN" - # Inject token as a cookie for older versions that don't use cookie jar - echo "dokploy.com FALSE / FALSE 0 auth_token $TOKEN" >> "$COOKIE_JAR" + -d '{"name":"CI Admin","email":"ci@dokploy.test","password":"CiTest1234!"}' \ + | tee /tmp/signup.out + set +x + + # Verify we got a session cookie + if ! grep -qE '(better-auth|auth)\.session' "$COOKIE_JAR"; then + echo "⚠️ No session cookie matched expected name; jar contents:" + cat "$COOKIE_JAR" fi # ── Create test resources ────────────────────────────────────────────── @@ -195,12 +195,13 @@ jobs: BASE: "http://localhost:${{ env.DOKPLOY_PORT }}" COOKIE: ${{ steps.auth.outputs.cookie_jar }} run: | - # --- tRPC helpers --- - # Mutation (POST) + set -euo pipefail + + # --- tRPC POST helper --- trpc_mut() { curl -sf -X POST "$BASE/api/trpc/$1" \ -H "Content-Type: application/json" \ - -b "$COOKIE" \ + -b "$COOKIE" -c "$COOKIE" \ -d "{\"json\":$2}" } @@ -218,7 +219,7 @@ jobs: PG=$(trpc_mut postgres.create \ "{\"name\":\"ci-pg\",\"appName\":\"ci-pg-db\",\ \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ - \"databasePassword\":\"CiPg1!\",\ + \"databasePassword\":\"CiPg1pass\",\ \"dockerImage\":\"postgres:15\",\"environmentId\":\"$ENV_ID\"}") PG_ID=$(echo "$PG" | jq -r '.result.data.json.postgresId') trpc_mut postgres.start "{\"postgresId\":\"$PG_ID\"}" > /dev/null @@ -228,14 +229,13 @@ jobs: MG=$(trpc_mut mongo.create \ "{\"name\":\"ci-mongo\",\"appName\":\"ci-mongo-db\",\ \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ - \"databasePassword\":\"CiMg1!\",\ + \"databasePassword\":\"CiMg1pass\",\ \"dockerImage\":\"mongo:7.0\",\"environmentId\":\"$ENV_ID\"}") MG_ID=$(echo "$MG" | jq -r '.result.data.json.mongoId') trpc_mut mongo.start "{\"mongoId\":\"$MG_ID\"}" > /dev/null echo "mg_id=$MG_ID" >> "$GITHUB_OUTPUT" # --- Docker-image application helper --- - # Creates an application, sets Docker image provider, triggers deploy. make_app() { local DISP_NAME=$1 APP_NAME=$2 IMAGE=$3 APP=$(trpc_mut application.create \ @@ -276,11 +276,10 @@ jobs: APP_NODE_ID: ${{ steps.create.outputs.app_node_id }} APP_GO_ID: ${{ steps.create.outputs.app_go_id }} run: | - # Poll a tRPC query endpoint until the service status field == "done" wait_done() { local NAME=$1 ENDPOINT=$2 ID_KEY=$3 ID=$4 STATUS_KEY=$5 echo "Waiting for $NAME to reach 'done'..." - timeout 300 bash -c " + timeout 360 bash -c " until [ \"\$(curl -sf -G '$BASE/api/trpc/$ENDPOINT' \ --data-urlencode 'input={\"json\":{\"$ID_KEY\":\"$ID\"}}' \ -b '$COOKIE' | \ @@ -303,7 +302,6 @@ jobs: echo "=== docker service ls ===" docker service ls - # User services should all be 1/1; check none are 0/1 FAIL=$(docker service ls --format '{{.Name}} {{.Replicas}}' | \ grep -E '^ci-' | grep -v ' 1/1' || true) if [ -n "$FAIL" ]; then @@ -322,7 +320,7 @@ jobs: "${{ env.DOKPLOY_SERVICE }}" echo "Waiting for service to converge on ${{ matrix.pair.to }}..." - timeout 180 bash -c ' + timeout 240 bash -c ' until ! docker service inspect dokploy \ --format "{{.UpdateStatus.State}}" 2>/dev/null \ | grep -q "^updating$"; do @@ -338,7 +336,7 @@ jobs: - name: Wait for Dokploy API to respond post-upgrade run: | - timeout 120 bash -c ' + timeout 180 bash -c ' until curl -sf -o /dev/null \ "http://localhost:${{ env.DOKPLOY_PORT }}"; do sleep 3 @@ -395,8 +393,12 @@ jobs: echo "=== docker service ls ===" && docker service ls || true echo "=== dokploy service tasks ===" && \ docker service ps "${{ env.DOKPLOY_SERVICE }}" --no-trunc || true - echo "=== dokploy logs (last 100) ===" && \ - docker service logs "${{ env.DOKPLOY_SERVICE }}" --tail 100 2>&1 || true + echo "=== dokploy logs (last 200) ===" && \ + docker service logs "${{ env.DOKPLOY_SERVICE }}" --tail 200 2>&1 || true + echo "=== install.sh tail ===" && \ + tail -100 /tmp/install.sh 2>&1 || true + echo "=== signup response ===" && \ + cat /tmp/signup.out 2>&1 || true echo "=== disk usage ===" && df -h # ── Job summary ──────────────────────────────────────────────────────── From f29a1a1976db0526de13bf39ef7b457d8dc87fde Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 13:43:30 +0000 Subject: [PATCH 3/7] fix(ci): handle both old (flat) and new (nested) project.create response formats In Dokploy <= ~v0.24, project.create returns a flat object with just projectId at the top level; no environment concept exists yet, so resources (postgres, mongo, apps) are created with projectId. In Dokploy >= ~v0.25, project.create returns nested {project, environment} objects and resources require environmentId. Previous workflow always used .project.projectId and .environment.environmentId, both of which evaluated to null on old versions, causing curl to exit with code 22 (HTTP error) on every subsequent resource-create call. Fix: extract PROJECT_ID with a // fallback, check ENV_ID nullness, and build a SCOPE fragment (either 'environmentId' or 'projectId') used in all resource-creation tRPC calls. --- .../workflows/upgrade-integration-test.yml | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/upgrade-integration-test.yml b/.github/workflows/upgrade-integration-test.yml index 4f1b1d3b2a..d0e3531e42 100644 --- a/.github/workflows/upgrade-integration-test.yml +++ b/.github/workflows/upgrade-integration-test.yml @@ -210,17 +210,34 @@ jobs: '{"name":"ci-upgrade-test","description":"CI upgrade integration test"}') echo "project.create → $PROJECT" - PROJECT_ID=$(echo "$PROJECT" | jq -r '.result.data.json.project.projectId') - ENV_ID=$(echo "$PROJECT" | jq -r '.result.data.json.environment.environmentId') + # Support both API eras: + # Old (pre-environments, ≤ ~v0.24): project.create returns a flat + # object → .result.data.json.projectId + # New (environments feature, ≥ ~v0.25): returns nested objects + # → .result.data.json.project.projectId + .environment.environmentId + PROJECT_ID=$(echo "$PROJECT" | jq -r \ + '.result.data.json.project.projectId // .result.data.json.projectId') + ENV_ID=$(echo "$PROJECT" | jq -r \ + '.result.data.json.environment.environmentId // empty') echo "project_id=$PROJECT_ID" >> "$GITHUB_OUTPUT" - echo "env_id=$ENV_ID" >> "$GITHUB_OUTPUT" + echo "env_id=${ENV_ID:-none}" >> "$GITHUB_OUTPUT" + echo "Detected PROJECT_ID=$PROJECT_ID ENV_ID=${ENV_ID:-}" + + # Build the JSON scope fragment used in every resource-create call. + # Old API uses projectId; new API uses environmentId. + if [ -n "$ENV_ID" ] && [ "$ENV_ID" != "null" ]; then + SCOPE="\"environmentId\":\"$ENV_ID\"" + else + SCOPE="\"projectId\":\"$PROJECT_ID\"" + fi + echo "Resource scope: $SCOPE" # --- PostgreSQL 15 --- PG=$(trpc_mut postgres.create \ "{\"name\":\"ci-pg\",\"appName\":\"ci-pg-db\",\ \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ \"databasePassword\":\"CiPg1pass\",\ - \"dockerImage\":\"postgres:15\",\"environmentId\":\"$ENV_ID\"}") + \"dockerImage\":\"postgres:15\",$SCOPE}") PG_ID=$(echo "$PG" | jq -r '.result.data.json.postgresId') trpc_mut postgres.start "{\"postgresId\":\"$PG_ID\"}" > /dev/null echo "pg_id=$PG_ID" >> "$GITHUB_OUTPUT" @@ -230,7 +247,7 @@ jobs: "{\"name\":\"ci-mongo\",\"appName\":\"ci-mongo-db\",\ \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ \"databasePassword\":\"CiMg1pass\",\ - \"dockerImage\":\"mongo:7.0\",\"environmentId\":\"$ENV_ID\"}") + \"dockerImage\":\"mongo:7.0\",$SCOPE}") MG_ID=$(echo "$MG" | jq -r '.result.data.json.mongoId') trpc_mut mongo.start "{\"mongoId\":\"$MG_ID\"}" > /dev/null echo "mg_id=$MG_ID" >> "$GITHUB_OUTPUT" @@ -239,8 +256,7 @@ jobs: make_app() { local DISP_NAME=$1 APP_NAME=$2 IMAGE=$3 APP=$(trpc_mut application.create \ - "{\"name\":\"$DISP_NAME\",\"appName\":\"$APP_NAME\",\ - \"environmentId\":\"$ENV_ID\"}") + "{\"name\":\"$DISP_NAME\",\"appName\":\"$APP_NAME\",$SCOPE}") APP_ID=$(echo "$APP" | jq -r '.result.data.json.applicationId') trpc_mut application.saveDockerProvider \ "{\"applicationId\":\"$APP_ID\",\"dockerImage\":\"$IMAGE\",\ From f59f11474b6a40892020742d7c8cf6f6854057fe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 14:15:13 +0000 Subject: [PATCH 4/7] fix(ci): raise floor to v0.29.4, require environmentId from project.create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.20.x used a completely different postgres/mongo/app API schema — projectId fallback returned boolean false, not the expected resource object. Since the goal is testing v0.29.4+, drop the old-API compatibility shim and fail fast with a clear message if environmentId is missing. Also: default floor_version changed from v0.20.0 to v0.29.4 to reduce the number of matrix jobs and focus on the stable modern API surface. --- .../workflows/upgrade-integration-test.yml | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/upgrade-integration-test.yml b/.github/workflows/upgrade-integration-test.yml index d0e3531e42..875496c42c 100644 --- a/.github/workflows/upgrade-integration-test.yml +++ b/.github/workflows/upgrade-integration-test.yml @@ -15,9 +15,9 @@ on: workflow_dispatch: inputs: floor_version: - description: 'Oldest version tag to include in auto pairs (e.g. v0.20.0)' + description: 'Oldest version tag to include in auto pairs (e.g. v0.29.4)' required: false - default: 'v0.20.0' + default: 'v0.29.4' extra_pairs: description: 'Extra pairs as JSON, e.g. [{"from":"v0.25.2","to":"v0.26.0"}]' required: false @@ -210,34 +210,34 @@ jobs: '{"name":"ci-upgrade-test","description":"CI upgrade integration test"}') echo "project.create → $PROJECT" - # Support both API eras: - # Old (pre-environments, ≤ ~v0.24): project.create returns a flat - # object → .result.data.json.projectId - # New (environments feature, ≥ ~v0.25): returns nested objects - # → .result.data.json.project.projectId + .environment.environmentId + # project.create in v0.29+ returns nested {project:{projectId}, environment:{environmentId}} PROJECT_ID=$(echo "$PROJECT" | jq -r \ - '.result.data.json.project.projectId // .result.data.json.projectId') + '.result.data.json.project.projectId // .result.data.json.projectId // empty') ENV_ID=$(echo "$PROJECT" | jq -r \ '.result.data.json.environment.environmentId // empty') - echo "project_id=$PROJECT_ID" >> "$GITHUB_OUTPUT" - echo "env_id=${ENV_ID:-none}" >> "$GITHUB_OUTPUT" - echo "Detected PROJECT_ID=$PROJECT_ID ENV_ID=${ENV_ID:-}" - - # Build the JSON scope fragment used in every resource-create call. - # Old API uses projectId; new API uses environmentId. - if [ -n "$ENV_ID" ] && [ "$ENV_ID" != "null" ]; then - SCOPE="\"environmentId\":\"$ENV_ID\"" - else - SCOPE="\"projectId\":\"$PROJECT_ID\"" + + if [ -z "$PROJECT_ID" ] || [ "$PROJECT_ID" = "null" ]; then + echo "❌ Could not extract projectId from project.create response:" + echo "$PROJECT" | jq . + exit 1 fi - echo "Resource scope: $SCOPE" + if [ -z "$ENV_ID" ] || [ "$ENV_ID" = "null" ]; then + echo "❌ Could not extract environmentId — this version may not support environments." + echo " project.create response: $PROJECT" + exit 1 + fi + + echo "project_id=$PROJECT_ID" >> "$GITHUB_OUTPUT" + echo "env_id=$ENV_ID" >> "$GITHUB_OUTPUT" + echo "Detected PROJECT_ID=$PROJECT_ID ENV_ID=$ENV_ID" # --- PostgreSQL 15 --- PG=$(trpc_mut postgres.create \ "{\"name\":\"ci-pg\",\"appName\":\"ci-pg-db\",\ \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ \"databasePassword\":\"CiPg1pass\",\ - \"dockerImage\":\"postgres:15\",$SCOPE}") + \"dockerImage\":\"postgres:15\",\"environmentId\":\"$ENV_ID\"}") + echo "postgres.create → $PG" PG_ID=$(echo "$PG" | jq -r '.result.data.json.postgresId') trpc_mut postgres.start "{\"postgresId\":\"$PG_ID\"}" > /dev/null echo "pg_id=$PG_ID" >> "$GITHUB_OUTPUT" @@ -247,7 +247,8 @@ jobs: "{\"name\":\"ci-mongo\",\"appName\":\"ci-mongo-db\",\ \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ \"databasePassword\":\"CiMg1pass\",\ - \"dockerImage\":\"mongo:7.0\",$SCOPE}") + \"dockerImage\":\"mongo:7.0\",\"environmentId\":\"$ENV_ID\"}") + echo "mongo.create → $MG" MG_ID=$(echo "$MG" | jq -r '.result.data.json.mongoId') trpc_mut mongo.start "{\"mongoId\":\"$MG_ID\"}" > /dev/null echo "mg_id=$MG_ID" >> "$GITHUB_OUTPUT" @@ -256,7 +257,7 @@ jobs: make_app() { local DISP_NAME=$1 APP_NAME=$2 IMAGE=$3 APP=$(trpc_mut application.create \ - "{\"name\":\"$DISP_NAME\",\"appName\":\"$APP_NAME\",$SCOPE}") + "{\"name\":\"$DISP_NAME\",\"appName\":\"$APP_NAME\",\"environmentId\":\"$ENV_ID\"}") APP_ID=$(echo "$APP" | jq -r '.result.data.json.applicationId') trpc_mut application.saveDockerProvider \ "{\"applicationId\":\"$APP_ID\",\"dockerImage\":\"$IMAGE\",\ From 2643ae0a86a7cfc801a3536c140f98eb9925ecdf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 14:27:36 +0000 Subject: [PATCH 5/7] fix(ci): deploy postgres/mongo instead of start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The create step was calling postgres.start / mongo.start immediately after create. In Dokploy, .start only scales an already-deployed Swarm service; on a freshly created resource the service doesn't exist yet, so the server ran 'docker service scale ci-pg-db-xxx=1' against a missing service and returned a 500 (curl -sf → exit 22 → step failure). .deploy is the mutation that actually builds and creates the Swarm service. Applications already used .deploy correctly; this aligns the databases. --- .github/workflows/upgrade-integration-test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/upgrade-integration-test.yml b/.github/workflows/upgrade-integration-test.yml index 875496c42c..8491e45bbc 100644 --- a/.github/workflows/upgrade-integration-test.yml +++ b/.github/workflows/upgrade-integration-test.yml @@ -239,7 +239,9 @@ jobs: \"dockerImage\":\"postgres:15\",\"environmentId\":\"$ENV_ID\"}") echo "postgres.create → $PG" PG_ID=$(echo "$PG" | jq -r '.result.data.json.postgresId') - trpc_mut postgres.start "{\"postgresId\":\"$PG_ID\"}" > /dev/null + # deploy (not start) — deploy creates the Swarm service; start only + # scales an already-deployed service and 500s on a fresh resource. + trpc_mut postgres.deploy "{\"postgresId\":\"$PG_ID\"}" > /dev/null echo "pg_id=$PG_ID" >> "$GITHUB_OUTPUT" # --- MongoDB 7.0 --- @@ -250,7 +252,7 @@ jobs: \"dockerImage\":\"mongo:7.0\",\"environmentId\":\"$ENV_ID\"}") echo "mongo.create → $MG" MG_ID=$(echo "$MG" | jq -r '.result.data.json.mongoId') - trpc_mut mongo.start "{\"mongoId\":\"$MG_ID\"}" > /dev/null + trpc_mut mongo.deploy "{\"mongoId\":\"$MG_ID\"}" > /dev/null echo "mg_id=$MG_ID" >> "$GITHUB_OUTPUT" # --- Docker-image application helper --- From eaec68023ede4fd9f37ef9212d204c83175b3b39 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 15:20:10 +0000 Subject: [PATCH 6/7] feat: consecutive upgrade pairs, drop curated pair and extra_pairs input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace "every version → latest" with consecutive pairs (v[i] → v[i+1]) - Remove curated v0.25→v0.26 pair logic - Remove extra_pairs workflow_dispatch input --- .../workflows/upgrade-integration-test.yml | 63 ++++++++----------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/.github/workflows/upgrade-integration-test.yml b/.github/workflows/upgrade-integration-test.yml index 8491e45bbc..ecb7e0b598 100644 --- a/.github/workflows/upgrade-integration-test.yml +++ b/.github/workflows/upgrade-integration-test.yml @@ -3,9 +3,7 @@ # Tests that Dokploy can upgrade from version A to version B while keeping # user projects (Postgres, MongoDB, two web apps, one static site) alive. # -# Two modes, combined into a single matrix: -# • Auto: every stable tag >= floor_version → latest stable tag (N jobs) -# • Curated: hand-picked pairs (default: latest v0.25.x → latest v0.26.x) +# Generates consecutive upgrade pairs: each stable tag >= floor_version → next tag. # # Only triggered manually to avoid burning Actions minutes. @@ -15,13 +13,9 @@ on: workflow_dispatch: inputs: floor_version: - description: 'Oldest version tag to include in auto pairs (e.g. v0.29.4)' + description: 'Oldest version tag to include in pairs (e.g. v0.29.4)' required: false default: 'v0.29.4' - extra_pairs: - description: 'Extra pairs as JSON, e.g. [{"from":"v0.25.2","to":"v0.26.0"}]' - required: false - default: '' env: DOKPLOY_IMAGE: dokploy/dokploy @@ -43,7 +37,6 @@ jobs: id: pairs env: FLOOR: ${{ inputs.floor_version }} - EXTRA: ${{ inputs.extra_pairs }} run: | set -euo pipefail @@ -54,51 +47,45 @@ jobs: grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \ sort -V) - LATEST=$(echo "$ALL_TAGS" | tail -n1) - echo "Latest tag on Docker Hub: $LATEST" + echo "All tags found:" + echo "$ALL_TAGS" # Floor semver components FLOOR_CLEAN="${FLOOR#v}" IFS='.' read -r F_MAJ F_MIN F_PAT <<< "$FLOOR_CLEAN" - # Build "each version >= FLOOR, != LATEST → LATEST" pairs - AUTO_PAIRS=$(echo "$ALL_TAGS" | while read -r TAG; do - [ "$TAG" = "$LATEST" ] && continue + # Filter tags >= FLOOR + FILTERED=$(echo "$ALL_TAGS" | while read -r TAG; do TAG_CLEAN="${TAG#v}" IFS='.' read -r T_MAJ T_MIN T_PAT <<< "$TAG_CLEAN" if [ "$T_MAJ" -gt "$F_MAJ" ] || \ { [ "$T_MAJ" -eq "$F_MAJ" ] && [ "$T_MIN" -gt "$F_MIN" ]; } || \ { [ "$T_MAJ" -eq "$F_MAJ" ] && [ "$T_MIN" -eq "$F_MIN" ] && [ "$T_PAT" -ge "$F_PAT" ]; }; then - printf '{"from":"%s","to":"%s"}\n' "$TAG" "$LATEST" + echo "$TAG" fi - done | jq -sc '.') - - # Curated pairs (latest v0.25.x → latest v0.26.x) - # Find the highest v0.25.x and v0.26.x tags from Docker Hub - V25=$(echo "$ALL_TAGS" | grep '^v0\.25\.' | tail -n1) - V26=$(echo "$ALL_TAGS" | grep '^v0\.26\.' | tail -n1) - CURATED='[]' - if [ -n "$V25" ] && [ -n "$V26" ] && [ "$V25" != "$V26" ]; then - CURATED=$(jq -cn --arg f "$V25" --arg t "$V26" '[{"from":$f,"to":$t}]') - fi + done) - EXTRA_JSON="${EXTRA:-[]}" - if [ -z "$EXTRA_JSON" ] || [ "$EXTRA_JSON" = "" ]; then - EXTRA_JSON="[]" - fi + echo "Tags >= $FLOOR:" + echo "$FILTERED" + + # Build consecutive pairs: tag[i] → tag[i+1] + TAGS_ARR=() + while IFS= read -r t; do TAGS_ARR+=("$t"); done <<< "$FILTERED" - # Merge all sources, deduplicate by "from+to" - ALL=$(jq -cn \ - --argjson auto "$AUTO_PAIRS" \ - --argjson curated "$CURATED" \ - --argjson extra "$EXTRA_JSON" \ - '$auto + $curated + $extra | unique_by(.from + "-" + .to)') + PAIRS='[]' + for (( i=0; i<${#TAGS_ARR[@]}-1; i++ )); do + FROM="${TAGS_ARR[$i]}" + TO="${TAGS_ARR[$i+1]}" + PAIRS=$(echo "$PAIRS" | jq -c \ + --arg f "$FROM" --arg t "$TO" \ + '. + [{"from":$f,"to":$t}]') + done - COUNT=$(echo "$ALL" | jq 'length') + COUNT=$(echo "$PAIRS" | jq 'length') echo "Total pairs: $COUNT" - echo "$ALL" | jq -r '.[] | " \(.from) → \(.to)"' + echo "$PAIRS" | jq -r '.[] | " \(.from) → \(.to)"' - echo "pairs=$ALL" >> "$GITHUB_OUTPUT" + echo "pairs=$PAIRS" >> "$GITHUB_OUTPUT" # ── 2. Run one upgrade test per pair ──────────────────────────────────────── From a504449f1dfedcd78acab809f489bb346d01e189 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 15:20:10 +0000 Subject: [PATCH 7/7] feat: add target_version input, replace extra_pairs field - Replace second form field (extra_pairs) with target_version - target_version sets version B ("to"); empty = highest available tag - Pair every tag in [floor_version, target_version) with target_version --- .../workflows/upgrade-integration-test.yml | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/.github/workflows/upgrade-integration-test.yml b/.github/workflows/upgrade-integration-test.yml index ecb7e0b598..f73b7276e6 100644 --- a/.github/workflows/upgrade-integration-test.yml +++ b/.github/workflows/upgrade-integration-test.yml @@ -3,7 +3,9 @@ # Tests that Dokploy can upgrade from version A to version B while keeping # user projects (Postgres, MongoDB, two web apps, one static site) alive. # -# Generates consecutive upgrade pairs: each stable tag >= floor_version → next tag. +# Generates upgrade pairs: each stable tag in [floor_version, target_version) +# is paired with target_version. If target_version is empty, the highest tag +# available on Docker Hub is used as the target. # # Only triggered manually to avoid burning Actions minutes. @@ -16,6 +18,10 @@ on: description: 'Oldest version tag to include in pairs (e.g. v0.29.4)' required: false default: 'v0.29.4' + target_version: + description: 'Target version to upgrade to (version B). Leave empty to use the highest available.' + required: false + default: '' env: DOKPLOY_IMAGE: dokploy/dokploy @@ -36,10 +42,16 @@ jobs: - name: Generate pairs id: pairs env: - FLOOR: ${{ inputs.floor_version }} + FLOOR: ${{ inputs.floor_version }} + TARGET: ${{ inputs.target_version }} run: | set -euo pipefail + # semver "a >= b" comparison helper (vX.Y.Z, strips leading v) + ge() { + [ "$(printf '%s\n%s\n' "${1#v}" "${2#v}" | sort -V | tail -n1)" = "${1#v}" ] + } + # Fetch all semver tags from Docker Hub (dokploy/dokploy) ALL_TAGS=$(curl -fsSL \ "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags?page_size=100" | \ @@ -50,36 +62,35 @@ jobs: echo "All tags found:" echo "$ALL_TAGS" + # Target version (version B): explicit input, else highest available. + if [ -n "$TARGET" ]; then + TO="$TARGET" + echo "Using user-supplied target version: $TO" + else + TO=$(echo "$ALL_TAGS" | tail -n1) + echo "No target supplied; using highest available: $TO" + fi + # Floor semver components FLOOR_CLEAN="${FLOOR#v}" IFS='.' read -r F_MAJ F_MIN F_PAT <<< "$FLOOR_CLEAN" - # Filter tags >= FLOOR - FILTERED=$(echo "$ALL_TAGS" | while read -r TAG; do + # Each tag in [FLOOR, TO) → TO + PAIRS='[]' + while read -r TAG; do + [ -z "$TAG" ] && continue + # skip tags above-or-equal to the target (incl. the target itself) + ge "$TAG" "$TO" && continue TAG_CLEAN="${TAG#v}" IFS='.' read -r T_MAJ T_MIN T_PAT <<< "$TAG_CLEAN" if [ "$T_MAJ" -gt "$F_MAJ" ] || \ { [ "$T_MAJ" -eq "$F_MAJ" ] && [ "$T_MIN" -gt "$F_MIN" ]; } || \ { [ "$T_MAJ" -eq "$F_MAJ" ] && [ "$T_MIN" -eq "$F_MIN" ] && [ "$T_PAT" -ge "$F_PAT" ]; }; then - echo "$TAG" + PAIRS=$(echo "$PAIRS" | jq -c \ + --arg f "$TAG" --arg t "$TO" \ + '. + [{"from":$f,"to":$t}]') fi - done) - - echo "Tags >= $FLOOR:" - echo "$FILTERED" - - # Build consecutive pairs: tag[i] → tag[i+1] - TAGS_ARR=() - while IFS= read -r t; do TAGS_ARR+=("$t"); done <<< "$FILTERED" - - PAIRS='[]' - for (( i=0; i<${#TAGS_ARR[@]}-1; i++ )); do - FROM="${TAGS_ARR[$i]}" - TO="${TAGS_ARR[$i+1]}" - PAIRS=$(echo "$PAIRS" | jq -c \ - --arg f "$FROM" --arg t "$TO" \ - '. + [{"from":$f,"to":$t}]') - done + done <<< "$ALL_TAGS" COUNT=$(echo "$PAIRS" | jq 'length') echo "Total pairs: $COUNT"