name: CI/CD Pipeline on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: inputs: deploy: description: "Recreate the davidaragon-portfolio stack on Portainer (deletes + creates with latest image)" type: boolean default: false required: false # Serialise runs on the shared act_runner: prevents two simultaneous runs from # cancelling each other's in-progress steps (observed in older portfolio runs). # See python-project-template-internal/.gitea/workflows/ci.yaml for the same # pattern with full context (runs 794 and 799). concurrency: group: ci-${{ github.ref }} cancel-in-progress: false jobs: build: name: Build & Deploy runs-on: ubuntu-latest env: # IMAGE_NAME used by the redeploy step + the deploy compose file. IMAGE_NAME: gitlab.impresion3d.pro/root/davidaragon-portfolio:0.0.1 steps: - uses: actions/checkout@v4 - name: Build and push Docker image run: | # The build always tags and pushes both :latest (for ad-hoc inspection) # and :0.0.1 (pinned version that the stack redeploy step uses). # The runner's `docker buildx` is set up by the QNAP self-hosted runner; # no QEMU emulation required for the linux/amd64 build target here. docker login gitlab.impresion3d.pro -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKER_PASSWORD }}" docker build \ -t gitlab.impresion3d.pro/root/davidaragon-portfolio:latest \ -t gitlab.impresion3d.pro/root/davidaragon-portfolio:0.0.1 \ . docker push gitlab.impresion3d.pro/root/davidaragon-portfolio:latest docker push gitlab.impresion3d.pro/root/davidaragon-portfolio:0.0.1 # ---------------------------------------------------------------------- # Optional redeploy step (gated by workflow_dispatch.deploy=true). # # Approach: delete the existing stack on Portainer (idempotent), then # create a new one from the docker-compose.prod.yml at the repo root. # # We DELETE + CREATE rather than PUT-edit because: # * Put-edit requires the same stack ID + PRUNE; the simpler recreate # works for our size (1 service, no inter-service references). # * Earlier the stack was "unhealthy"; this gives a clean slate. # # Required secrets: # PORTAINER_URL e.g. http://192.168.1.30:9000 # PORTAINER_TOKEN Access token from a Portainer user (scope: admin) # PORTAINER_ENDPOINT_ID Numeric endpoint ID (usually 1) # PORTAINER_STACK_ID Numeric stack ID to delete before recreating # ---------------------------------------------------------------------- - name: Redeploy stack on Portainer via SSH if: github.event_name == 'workflow_dispatch' && inputs.deploy == true env: 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: | # 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 "--- 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 "--- 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 "--- Done ---"