diff --git a/.gitea/workflows/ci-cd.yaml b/.gitea/workflows/ci-cd.yaml index 4306c83..c8157e1 100644 --- a/.gitea/workflows/ci-cd.yaml +++ b/.gitea/workflows/ci-cd.yaml @@ -62,73 +62,48 @@ jobs: # PORTAINER_ENDPOINT_ID Numeric endpoint ID (usually 1) # PORTAINER_STACK_ID Numeric stack ID to delete before recreating # ---------------------------------------------------------------------- - - name: Redeploy stack on Portainer + - name: Redeploy stack on Portainer via SSH if: github.event_name == 'workflow_dispatch' && inputs.deploy == true env: - PORTAINER_TOKEN: ${{ secrets.PORTAINER_TOKEN }} - PORTAINER_ENDPOINT_ID: ${{ secrets.PORTAINER_ENDPOINT_ID }} - PORTAINER_STACK_ID: ${{ secrets.PORTAINER_STACK_ID }} + NAS_SSH_KEY: ${{ secrets.NAS_SSH_KEY }} + NAS_HOST: ${{ secrets.NAS_HOST }} + NAS_USER: ${{ secrets.NAS_USER }} + STACK_NAME: davidaragon-portfolio + COMPOSE_FILE: docker-compose.prod.yml run: | - # Portainer, Gitea and the runners all share the `portainer_default` - # Docker network on the NAS, so the service name "portainer" resolves - # directly from the job container on port 9000. - PORTAINER_URL="http://portainer:9000" - echo "Using PORTAINER_URL=$PORTAINER_URL" - if ! curl -sS -o /dev/null -w '%{http_code}' --max-time 5 \ - "${PORTAINER_URL}/api/status" | grep -q '^200$'; then - echo "ERROR: Portainer not reachable at $PORTAINER_URL" >&2 + # The act_runner job container can't reach 'portainer' because it + # doesn't share the runner's network namespace in v0.6.1. We work + # around this by SSHing into the NAS (where Portainer is) and + # using the docker CLI directly to do `docker stack deploy`. + set -euo pipefail + + mkdir -p ~/.ssh + echo "$NAS_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + + echo "--- Pre-flight: ensure NAS is reachable ---" + ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \ + "$NAS_USER@$NAS_HOST" 'hostname && docker version --format "{{.Server.Version}}"' \ + | head + + echo "--- Reading docker-compose.prod.yml from the repo ---" + if [ ! -f "$COMPOSE_FILE" ]; then + echo "ERROR: $COMPOSE_FILE not found in repo root" >&2 exit 1 fi - echo "--- Step 1: pre-flight (delete existing stack if present) ---" - DELETE_HTTP_CODE=$(curl -sS -o /tmp/portainer-delete.json -w '%{http_code}' \ - -X DELETE \ - -H "X-API-Key: ${PORT...EN}" \ - "${PORTAINER_URL}/api/stacks/${PORTAINER_STACK_ID}?endpointId=${PORTAINER_ENDPOINT_ID}") - echo "DELETE HTTP ${DELETE_HTTP_CODE}" - if [ "${DELETE_HTTP_CODE}" != "204" ] && [ "${DELETE_HTTP_CODE}" != "404" ]; then - echo "ERROR: Portainer rejected DELETE on stack ${PORTAINER_STACK_ID}:" >&2 - cat /tmp/portainer-delete.json >&2 - exit 1 - fi + echo "--- Redeploying stack '$STACK_NAME' on the NAS ---" + # The NAS has Portainer and docker compose (v2) available. We stream + # the compose file over SSH and let docker compose recreate the + # project. We pin the project name to 'davidaragon-portfolio' so the + # volumes and networks of the existing stack are reused. + cat "$COMPOSE_FILE" | ssh -o StrictHostKeyChecking=no \ + "$NAS_USER@$NAS_HOST" \ + "export PATH=/share/CACHEDEV1_DATA/.qpkg/container-station/usr/bin:\$PATH; cd /tmp && docker compose -p '$STACK_NAME' -f - up -d" - echo "--- Step 2: read docker-compose.prod.yml ---" - if [ ! -f docker-compose.prod.yml ]; then - echo "ERROR: docker-compose.prod.yml is missing from the repo root" >&2 - exit 1 - fi - # Inline the compose file. Portainer expects `composeFileContent` as raw text. - COMPOSE_BODY=$(jq -Rs --arg compose "$(cat docker-compose.prod.yml)" \ - '{composeFileContent: $compose, env: []}' < /dev/null) + echo "--- Stack '$STACK_NAME' redeployed. Smoke-test: ---" + sleep 5 + curl -sS -o /dev/null -w 'http://localhost:3001/ -> HTTP=%{http_code}\n' \ + --max-time 5 http://localhost:3001/ || true - echo "--- Step 3: create fresh stack from docker-compose.prod.yml ---" - CREATE_HTTP_CODE=$(curl -sS -o /tmp/portainer-create.json -w '%{http_code}' \ - -X POST \ - -H "X-API-Key: ${PORTAINER_TOKEN}" \ - -H "Content-Type: application/json" \ - --data "${COMPOSE_BODY}" \ - "${PORTAINER_URL}/api/stacks?endpointId=${PORTAINER_ENDPOINT_ID}&type=2&method=string&name=davidaragon-portfolio") - echo "CREATE HTTP ${CREATE_HTTP_CODE}" - if [ "${CREATE_HTTP_CODE}" != 201 ]; then - echo "ERROR: Portainer rejected stack creation:" >&2 - cat /tmp/portainer-create.json >&2 - exit 1 - fi - - echo "--- Step 4: smoke-test the freshly deployed stack ---" - # Give the container a brief window to start before checking. - sleep 8 - # The Portainer host:9000 is reachable from the job container - # (we just proved that with the auto-discovery step above). The - # portfolio container itself is published on host port 3001, so - # smoke-test through the same host. We don't fail the job if the - # proxy upstream isn't reachable from the runner's network. - HEALTH=$(curl -sS -o /dev/null -w '%{http_code}' \ - --max-time 5 \ - "${PORTAINER_URL%:[0-9]*}:3001/" 2>/dev/null || true) - echo "Health check on ${PORTAINER_URL%:[0-9]*}:3001/ returned: ${HEALTH:-}" - # We log but don't fail the job if 3001 isn't reachable — the upstream - # proxy (Nginx Proxy Manager → davidaragon.impresion3d.pro) is a better - # place to wire a hard-fail check in a future iteration. - - echo "--- Stack recreated successfully. ---" \ No newline at end of file + echo "--- Done ---" \ No newline at end of file