From 8f4af5e3223d50d8f082a1b3f1c2a30d3687ead7 Mon Sep 17 00:00:00 2001 From: hitanshu310 Date: Tue, 7 Jul 2026 02:51:58 +0530 Subject: [PATCH 1/4] Hithomelabs/CFTunnels#99: Deploy portainer-automation service - Add Dockerfile for portainer-automation (multi-stage build) - Add standalone docker-compose.yaml for PA service - Add application-prod.properties (prod Portainer :9443) - Add application-ci.properties (CI test endpoints) - Add .env.example with documented env vars for PA - Add PORTAINER_STACK.md with deployment guide, env var tables, architecture diagram - Add CI workflow (portainer_automation_build_push.yml) for image build/push - Update root docker-compose.yaml to include PA service --- .../portainer_automation_build_push.yml | 66 ++++++ docker-compose.yaml | 14 +- portainer-automation/.env.example | 38 +++ portainer-automation/Dockerfile | 12 + portainer-automation/PORTAINER_STACK.md | 224 ++++++++++++++++++ portainer-automation/docker-compose.yaml | 13 + .../main/resources/application-ci.properties | 6 + .../resources/application-prod.properties | 6 + 8 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/portainer_automation_build_push.yml create mode 100644 portainer-automation/.env.example create mode 100644 portainer-automation/Dockerfile create mode 100644 portainer-automation/PORTAINER_STACK.md create mode 100644 portainer-automation/docker-compose.yaml create mode 100644 portainer-automation/src/main/resources/application-ci.properties create mode 100644 portainer-automation/src/main/resources/application-prod.properties diff --git a/.gitea/workflows/portainer_automation_build_push.yml b/.gitea/workflows/portainer_automation_build_push.yml new file mode 100644 index 0000000..0a68187 --- /dev/null +++ b/.gitea/workflows/portainer_automation_build_push.yml @@ -0,0 +1,66 @@ +name: portainer-automation build and push +run-name: PA Build started by ${{ gitea.actor }} +on: + push: + branches: [test] +jobs: + tag: + runs-on: ubuntu-latest + outputs: + new_version: ${{ steps.new_version.outputs.new_version }} + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get new version + id: new_version + run: | + VERSION=$(git describe --tags --abbrev=0) + echo "Current version: ${VERSION}" + MAJOR=$(echo ${VERSION} | cut -d "." -f 1) + MINOR=$(echo ${VERSION} | cut -d "." -f 2) + PATCH=$(echo ${VERSION} | cut -d "." -f 3) + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + echo "New version: ${NEW_VERSION}" + echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT + build_tag_push: + runs-on: ubuntu-latest + needs: tag + container: + image: 192.168.0.100:8928/hithomelabs/ci-runner:1.0.0 + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install JDK (Alpine package) + run: | + apk add --no-cache openjdk17-jdk + echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk" >> "$GITHUB_ENV" + - name: Validate Gradle Wrapper (offline checksum) + run: | + sha256sum --check gradle/wrapper/gradle-wrapper.jar.sha256 + - name: Create and push tag + run: | + echo "New version: ${{ needs.tag.outputs.new_version }}" + git config --global user.name "${{ gitea.actor }}" + git config --global user.email "${{ gitea.actor }}@users.noreply.github.com" + git tag -a "pa-${{ needs.tag.outputs.new_version }}" -m "Pushing PA version ${{ needs.tag.outputs.new_version }}" + git push origin "pa-${{ needs.tag.outputs.new_version }}" + - name: Log in to Gitea Docker Registry + uses: docker/login-action@v3 + with: + registry: 'http://192.168.0.100:8928' + username: hitanshu + password: ${{ secrets.TOKEN }} + - name: Gradle build image + run: ./gradlew :portainer-automation:bootBuildImage --imageName=192.168.0.100:8928/hithomelabs/portainer-automation:${{ needs.tag.outputs.new_version }} + - name: Tag image as test + run: docker tag 192.168.0.100:8928/hithomelabs/portainer-automation:${{ needs.tag.outputs.new_version }} 192.168.0.100:8928/hithomelabs/portainer-automation:test + - name: Push to Gitea Registry + run: | + docker push 192.168.0.100:8928/hithomelabs/portainer-automation:test + docker push 192.168.0.100:8928/hithomelabs/portainer-automation:${{ needs.tag.outputs.new_version }} diff --git a/docker-compose.yaml b/docker-compose.yaml index 1983e88..07f9090 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -29,4 +29,16 @@ services: ports: - "${DB_PORT}:5432" volumes: - - ${DB_PATH}:/var/lib/postgresql/data \ No newline at end of file + - ${DB_PATH}:/var/lib/postgresql/data + portainer-automation: + image: gitea.hithomelabs.com/hithomelabs/portainer-automation:${ENV} + container_name: portainer-automation_${ENV} + ports: + - "${PORTAINER_AUTOMATION_PORT:-8081}:8081" + environment: + - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-local} + - PORTAINER_API_KEY=${PORTAINER_API_KEY} + - PORTAINER_SERVICE_API_KEY=${PORTAINER_SERVICE_API_KEY} + env_file: + - stack.env + restart: unless-stopped \ No newline at end of file diff --git a/portainer-automation/.env.example b/portainer-automation/.env.example new file mode 100644 index 0000000..1d6593c --- /dev/null +++ b/portainer-automation/.env.example @@ -0,0 +1,38 @@ +# ============================================ +# Portainer Automation Service — Environment Variables +# ============================================ +# Copy this file to .env and fill in your values. +# This service is NOT exposed via Cloudflare Tunnel (internal-only). + +# --- Portainer Connection --- + +# Portainer API Key for the target Portainer instance +# Create via Portainer UI: Settings → Authentication → Access tokens +# Dev: https://devdocker.hithomelabs.com → ptk_xxxx_dev_xxxx +# Prod: https://192.168.0.100:9443 → ptk_xxxx_prod_xxxx +PORTAINER_API_KEY=ptr_your_portainer_api_key_here + +# Portainer endpoint ID (1 = primary Docker endpoint, 2 = remote) +PORTAINER_ENDPOINT_ID=2 + +# --- Service Auth --- + +# API key that CI workflows send in the X-API-Key header +# to authenticate against this service's /api/deploy endpoint +PORTAINER_SERVICE_API_KEY=change-me + +# --- Spring Profile --- + +# Active Spring profile (local | ci | prod) +# local — uses devdocker.hithomelabs.com via Cloudflare Tunnel (default) +# ci — uses test/stub endpoints +# prod — uses production Portainer on internal network +SPRING_PROFILES_ACTIVE=local + +# --- Deployment --- + +# Deployment environment label (dev | test | prod) +ENV=dev + +# Host port to map to container port 8081 +HOST_PORT=8081 diff --git a/portainer-automation/Dockerfile b/portainer-automation/Dockerfile new file mode 100644 index 0000000..93fb0b1 --- /dev/null +++ b/portainer-automation/Dockerfile @@ -0,0 +1,12 @@ +FROM openjdk:17-jdk as build +WORKDIR /app +COPY gradlew settings.gradle build.gradle ./ +COPY gradle ./gradle +COPY common ./common +COPY cftunnels-service ./cftunnels-service +COPY portainer-automation ./portainer-automation +RUN ./gradlew :portainer-automation:bootJar + +FROM openjdk:17-jdk-slim +COPY --from=build /app/portainer-automation/build/libs/*.jar app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/portainer-automation/PORTAINER_STACK.md b/portainer-automation/PORTAINER_STACK.md new file mode 100644 index 0000000..95a2aba --- /dev/null +++ b/portainer-automation/PORTAINER_STACK.md @@ -0,0 +1,224 @@ +# Portainer Automation Service — Portainer Stack Guide + +## Overview + +The Portainer Automation service exposes a single endpoint (`POST /api/deploy/{stackId}`) +that triggers a redeploy of a Portainer stack. CI workflows call this endpoint after +pushing new container images. + +The service is deployed as a **separate Portainer stack** (Option B) — independent from +the CFTunnels stacks — so it can be reused by other projects. + +--- + +## Architecture Diagram + +``` +┌─────────────────┐ push tag ┌──────────────────┐ +│ Gitea Runner │ ──────────────► │ Docker Registry │ +│ (CI workflow) │ │ :8928 │ +└────────┬────────┘ └──────────────────┘ + │ + │ POST /api/deploy/{stackId} + │ X-API-Key: + ▼ +┌─────────────────────────────────────┐ +│ Portainer Automation Service │ +│ (portainer-automation_${ENV}) │ +│ port: 8081 │ +└──────────┬──────────────────────────┘ + │ PUT /api/stacks/{id}/git/redeploy + │ X-API-Key: + ▼ +┌─────────────────────────────────────┐ +│ Portainer CE │ +│ Dev: :9442 / Prod: :9443 │ +└──────────┬──────────────────────────┘ + │ git pull + stack redeploy + ▼ +┌─────────────────────────────────────┐ +│ Target Stack (e.g. CFTunnels) │ +│ Docker Compose from Git │ +└─────────────────────────────────────┘ +``` + +--- + +## Portainer Stack Configuration + +### Create a New Stack in Portainer + +1. **Navigate**: Portainer UI → Stacks → **Add stack** +2. **Name**: `portainer-automation-{env}` (e.g. `portainer-automation-dev`) +3. **Build method**: **Git repository** +4. **Repository URL**: `https://gitea.hithomelabs.com/Hithomelabs/CFTunnels.git` +5. **Repository reference**: `refs/heads/test` (dev) or `refs/heads/main` (prod) +6. **Compose path**: `portainer-automation/docker-compose.yaml` +7. **Environment variables**: See table below + +### Environment Variables — Dev + +| Variable | Value | Description | +|----------|-------|-------------| +| `ENV` | `dev` | Environment label | +| `HOST_PORT` | `8081` | Host port mapping | +| `SPRING_PROFILES_ACTIVE` | `local` | Spring profile (uses `devdocker.hithomelabs.com`) | +| `PORTAINER_API_KEY` | `ptk_xxxx_dev_xxxx` | Portainer Dev API access token | +| `PORTAINER_SERVICE_API_KEY` | *(generate a random key)* | CI → Service auth key | + +Portainer Dev endpoint: `https://devdocker.hithomelabs.com` (port 9442 behind Cloudflare Tunnel) + +### Environment Variables — Prod + +| Variable | Value | Description | +|----------|-------|-------------| +| `ENV` | `prod` | Environment label | +| `HOST_PORT` | `8082` | Host port mapping | +| `SPRING_PROFILES_ACTIVE` | `prod` | Spring profile (uses `192.168.0.100:9443`) | +| `PORTAINER_API_KEY` | `ptk_xxxx_prod_xxxx` | Portainer Prod API access token | +| `PORTAINER_SERVICE_API_KEY` | *(generate a different random key)* | CI → Service auth key | + +Portainer Prod endpoint: `https://192.168.0.100:9443` (internal network) + +--- + +## Deployment Steps + +### Prerequisites + +1. Portainer Automation Docker image is built and pushed (see CI workflow) +2. Portainer API access tokens created (Portainer UI → My access tokens) +3. Gitea secrets configured: + - `PORTAINER_SERVICE_API_KEY` — shared secret for dev + - `PORTAINER_SERVICE_API_KEY_PROD` — shared secret for prod + +### Step 1: Build and Push Image + +The CI workflow `.gitea/workflows/portainer_automation_build_push.yml` handles this +automatically on push to the `test` branch. + +To build manually: +```bash +./gradlew :portainer-automation:bootBuildImage \ + --imageName=192.168.0.100:8928/hithomelabs/portainer-automation: +``` + +### Step 2: Create Stack in Portainer (Dev) + +1. Log in to Portainer Dev (`https://devdocker.hithomelabs.com`) +2. Stacks → Add stack +3. Name: `portainer-automation-dev` +4. Build method: Git repository +5. Repository: `https://gitea.hithomelabs.com/Hithomelabs/CFTunnels.git` +6. Reference: `refs/heads/test` +7. Compose path: `portainer-automation/docker-compose.yaml` +8. Add env vars from the **Dev** table above +9. Click **Deploy the stack** + +### Step 3: Verify Dev Deployment + +```bash +# Redeploy a stack via the automation service +curl -X POST \ + -H "X-API-Key: " \ + http://portainer-automation-dev:8081/api/deploy/ + +# Expected: "Deployment initiated for stack " +``` + +### Step 4: Create Stack in Portainer (Prod) + +1. Log in to Portainer Prod (`https://192.168.0.100:9443`) +2. Follow the same steps as Step 2, with: + - Name: `portainer-automation-prod` + - Reference: `refs/heads/main` + - Env vars from the **Prod** table above + +### Step 5: Wire CI Triggers + +Ensure CI workflows call the automation endpoint: + +**Dev** (in `test_image_build_push.yml`): +```yaml +- name: Trigger Portainer Redeploy (Dev) + if: success() + env: + PORTAINER_AUTOMATION_URL: "http://portainer-automation-dev:8081" + STACK_ID: ${{ vars.PORTAINER_AUTOMATION_DEV_STACK_ID }} + API_KEY: ${{ secrets.PORTAINER_SERVICE_API_KEY }} + run: | + wget -q --no-check-certificate --timeout=30 \ + --header="X-API-Key: $API_KEY" \ + -O - \ + "$PORTAINER_AUTOMATION_URL/api/deploy/$STACK_ID" 2>&1 \ + || echo "Deploy trigger failed (non-fatal)" +``` + +**Prod** (in `prod_image_tag_promote.yaml`): +```yaml +- name: Trigger Portainer Redeploy (Prod) + if: success() + env: + PORTAINER_AUTOMATION_URL: "http://portainer-automation-prod:8082" + STACK_ID: ${{ vars.PORTAINER_AUTOMATION_PROD_STACK_ID }} + API_KEY: ${{ secrets.PORTAINER_SERVICE_API_KEY_PROD }} + run: | + wget -q --no-check-certificate --timeout=30 \ + --header="X-API-Key: $API_KEY" \ + -O - \ + "$PORTAINER_AUTOMATION_URL/api/deploy/$STACK_ID" 2>&1 \ + || echo "Deploy trigger failed (non-fatal)" +``` + +--- + +## Configuration Reference + +### Profile Resolution Order + +The service resolves configuration in this order (later overrides earlier): + +1. `application.properties` — default: `https://192.168.0.100:9442` +2. `application-{profile}.properties` — profile-specific overrides +3. Environment variables — highest priority + +### Profile Purposes + +| Profile | File | Portainer Target | Use Case | +|---------|------|-----------------|----------| +| default | `application.properties` | `192.168.0.100:9442` (Dev) | CI/CD, Docker compose | +| `local` | `application-local.properties` | `devdocker.hithomelabs.com` | Local dev via Cloudflare Tunnel | +| `ci` | `application-ci.properties` | `portainer-test:9000` | CI pipeline testing | +| `prod` | `application-prod.properties` | `192.168.0.100:9443` (Prod) | Production deployment | + +### Environment Variable Reference + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `SPRING_PROFILES_ACTIVE` | No | *(none)* | Active Spring profile | +| `PORTAINER_API_KEY` | Yes | — | Portainer API access token | +| `PORTAINER_SERVICE_API_KEY` | Yes | — | Shared secret for CI → service auth | +| `ENV` | No | `test` | Deployment environment label | +| `HOST_PORT` | No | `8081` | External port mapping | + +--- + +## Health Checks + +The service exposes no dedicated health endpoint yet. Verify availability by: + +```bash +# Check that the service responds (expects 401 Unauthorized without API key) +curl -X POST http://portainer-automation-dev:8081/api/deploy/1 +# Expected: {"timestamp":"...","status":401,"error":"Unauthorized",...} +# A 401 response means the service is running. +``` + +## Troubleshooting + +| Symptom | Likely Cause | Fix | +|---------|-------------|-----| +| `Connection refused` | Service not running | Check stack status in Portainer | +| `Invalid API key` | Wrong `PORTAINER_SERVICE_API_KEY` | Verify env var matches CI secret | +| `Portainer error: Unauthorized` | Wrong `PORTAINER_API_KEY` | Regenerate token in Portainer UI | +| `Stack not found` | Wrong stack ID | Verify stack ID in Portainer URL | diff --git a/portainer-automation/docker-compose.yaml b/portainer-automation/docker-compose.yaml new file mode 100644 index 0000000..3dad479 --- /dev/null +++ b/portainer-automation/docker-compose.yaml @@ -0,0 +1,13 @@ +services: + portainer-automation: + image: gitea.hithomelabs.com/hithomelabs/portainer-automation:${ENV:-test} + container_name: portainer-automation_${ENV:-test} + ports: + - "${HOST_PORT:-8081}:8081" + environment: + - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-local} + - PORTAINER_API_KEY=${PORTAINER_API_KEY} + - PORTAINER_SERVICE_API_KEY=${PORTAINER_SERVICE_API_KEY} + env_file: + - stack.env + restart: unless-stopped diff --git a/portainer-automation/src/main/resources/application-ci.properties b/portainer-automation/src/main/resources/application-ci.properties new file mode 100644 index 0000000..e6f56bc --- /dev/null +++ b/portainer-automation/src/main/resources/application-ci.properties @@ -0,0 +1,6 @@ +# CI-specific Portainer Configuration +# Uses test/stub endpoints during CI pipeline testing +portainer.base-url=http://portainer-test:9000 +portainer.api-key=${PORTAINER_API_KEY:ci-test-key} +portainer.endpoint-id=1 +portainer.service.api-key=${PORTAINER_SERVICE_API_KEY:ci-test-key} diff --git a/portainer-automation/src/main/resources/application-prod.properties b/portainer-automation/src/main/resources/application-prod.properties new file mode 100644 index 0000000..54b367b --- /dev/null +++ b/portainer-automation/src/main/resources/application-prod.properties @@ -0,0 +1,6 @@ +# Production Portainer Configuration +# Targets the production Portainer instance on the internal network (port 9443) +portainer.base-url=https://192.168.0.100:9443 +portainer.api-key=${PORTAINER_API_KEY} +portainer.endpoint-id=2 +portainer.service.api-key=${PORTAINER_SERVICE_API_KEY} -- 2.45.2 From bfed3d220b35705914fd1a08d843f60a14d9dbde Mon Sep 17 00:00:00 2001 From: hitanshu310 Date: Tue, 7 Jul 2026 03:48:53 +0530 Subject: [PATCH 2/4] Hithomelabs/CFTunnels#99: address PR review - specific exceptions, SLF4J logging, health endpoint --- .../controller/DeployController.java | 40 +++++++++++++++++-- .../portainer/service/DeployService.java | 23 +++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/portainer-automation/src/main/java/com/hithomelabs/portainer/controller/DeployController.java b/portainer-automation/src/main/java/com/hithomelabs/portainer/controller/DeployController.java index afd1e34..a5d2a4f 100644 --- a/portainer-automation/src/main/java/com/hithomelabs/portainer/controller/DeployController.java +++ b/portainer-automation/src/main/java/com/hithomelabs/portainer/controller/DeployController.java @@ -1,20 +1,29 @@ package com.hithomelabs.portainer.controller; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.hithomelabs.common.portainer.client.exception.PortainerAuthenticationException; +import com.hithomelabs.common.portainer.client.exception.PortainerConnectionException; +import com.hithomelabs.common.portainer.client.exception.PortainerDeploymentException; +import com.hithomelabs.common.portainer.client.exception.PortainerResourceNotFoundException; import com.hithomelabs.portainer.config.PortainerAutomationProperties; import com.hithomelabs.portainer.service.DeployService; @RestController -@RequestMapping("/api/deploy") +@RequestMapping("/api") public class DeployController { + private static final Logger log = LoggerFactory.getLogger(DeployController.class); + private final DeployService deployService; private final PortainerAutomationProperties props; @@ -23,17 +32,42 @@ public class DeployController { this.props = props; } - @PostMapping("/{stackId}") + @GetMapping("/health") + public ResponseEntity health() { + return ResponseEntity.ok("{\"status\":\"UP\"}"); + } + + @PostMapping("/deploy/{stackId}") public ResponseEntity deploy(@PathVariable Long stackId, @RequestHeader(value = "X-API-Key", required = false) String apiKey) { String expectedApiKey = props.getService().getApiKey(); if (expectedApiKey == null || !expectedApiKey.equals(apiKey)) { + log.warn("Deploy auth failed for stack {}: invalid API key", stackId); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid API key"); } + log.info("Deployment requested for stack {}", stackId); try { deployService.redeploy(stackId); + log.info("Deployment initiated for stack {}", stackId); return ResponseEntity.ok("Deployment initiated for stack " + stackId); - } catch (Exception e) { + } catch (PortainerConnectionException e) { + log.error("Portainer connection error for stack {}: {}", stackId, e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_GATEWAY) + .body("Portainer connection failed: " + e.getMessage()); + } catch (PortainerResourceNotFoundException e) { + log.error("Stack {} not found: {}", stackId, e.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body("Stack not found: " + e.getMessage()); + } catch (PortainerAuthenticationException e) { + log.error("Portainer auth error for stack {}: {}", stackId, e.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body("Portainer authentication failed: " + e.getMessage()); + } catch (PortainerDeploymentException e) { + log.error("Portainer deployment error for stack {}: {}", stackId, e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_GATEWAY) + .body("Deployment failed: " + e.getMessage()); + } catch (RuntimeException e) { + log.error("Unexpected error deploying stack {}: {}", stackId, e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("Deployment failed: " + e.getMessage()); } diff --git a/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java b/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java index 33bde32..989299b 100644 --- a/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java +++ b/portainer-automation/src/main/java/com/hithomelabs/portainer/service/DeployService.java @@ -1,8 +1,13 @@ package com.hithomelabs.portainer.service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import com.hithomelabs.common.portainer.client.PortainerApiClient; +import com.hithomelabs.common.portainer.client.exception.PortainerConnectionException; +import com.hithomelabs.common.portainer.client.exception.PortainerDeploymentException; +import com.hithomelabs.common.portainer.client.exception.PortainerResourceNotFoundException; import com.hithomelabs.common.portainer.model.EnvVariable; import com.hithomelabs.common.portainer.model.PortainerStack; import com.hithomelabs.portainer.config.PortainerAutomationProperties; @@ -12,6 +17,8 @@ import java.util.List; @Service public class DeployService { + private static final Logger log = LoggerFactory.getLogger(DeployService.class); + private final PortainerApiClient portainerApiClient; private final PortainerAutomationProperties props; @@ -22,8 +29,18 @@ public class DeployService { } public void redeploy(Long stackId) { - PortainerStack stack = portainerApiClient.getStack(stackId); - List env = stack.getEnv() != null ? stack.getEnv() : List.of(); - portainerApiClient.redeployGitStack(stackId, props.getEndpointId(), true, env); + log.info("Redeploying stack {}", stackId); + try { + PortainerStack stack = portainerApiClient.getStack(stackId); + List env = stack.getEnv() != null ? stack.getEnv() : List.of(); + portainerApiClient.redeployGitStack(stackId, props.getEndpointId(), true, env); + log.info("Stack {} redeployed successfully", stackId); + } catch (PortainerResourceNotFoundException | PortainerConnectionException | PortainerDeploymentException e) { + log.error("Deploy failed for stack {}: {}", stackId, e.getMessage()); + throw e; + } catch (RuntimeException e) { + log.error("Unexpected error during deploy of stack {}: {}", stackId, e.getMessage(), e); + throw e; + } } } -- 2.45.2 From dbfb49f5a9f13a7f38db31747a2ffff15c046eee Mon Sep 17 00:00:00 2001 From: hitanshu310 Date: Thu, 9 Jul 2026 00:31:34 +0530 Subject: [PATCH 3/4] Hithomelabs/CFTunnels#99: Fix stack.env path in portainer-automation docker-compose --- portainer-automation/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portainer-automation/docker-compose.yaml b/portainer-automation/docker-compose.yaml index 3dad479..c4709d5 100644 --- a/portainer-automation/docker-compose.yaml +++ b/portainer-automation/docker-compose.yaml @@ -9,5 +9,5 @@ services: - PORTAINER_API_KEY=${PORTAINER_API_KEY} - PORTAINER_SERVICE_API_KEY=${PORTAINER_SERVICE_API_KEY} env_file: - - stack.env + - ../stack.env restart: unless-stopped -- 2.45.2 From 634794e7d3afb7ddbbd161ffc696dd22e438c1c9 Mon Sep 17 00:00:00 2001 From: Dave the Dev Date: Wed, 8 Jul 2026 19:53:17 +0000 Subject: [PATCH 4/4] Hithomelabs/CFTunnels#139: Parameterize portainer-automation build workflow for multi-environment reuse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace single-branch push trigger with test + main + workflow_dispatch - Add environment determination (test → PATCH bump, main/prod → MINOR bump) - Add semver calculation with conditional patch/minor logic - Add env validation (fails early on invalid env) - Remove pa- prefix from git tags (use plain semver) - Test builds fresh image; main/prod promotes test image to prod - Match final architecture spec from Archie --- .../portainer_automation_build_push.yml | 113 +++++++++++++----- 1 file changed, 85 insertions(+), 28 deletions(-) diff --git a/.gitea/workflows/portainer_automation_build_push.yml b/.gitea/workflows/portainer_automation_build_push.yml index 0a68187..8ce4c0f 100644 --- a/.gitea/workflows/portainer_automation_build_push.yml +++ b/.gitea/workflows/portainer_automation_build_push.yml @@ -1,66 +1,123 @@ -name: portainer-automation build and push +name: Build & Push Portainer Automation run-name: PA Build started by ${{ gitea.actor }} + on: push: - branches: [test] + branches: + - test # → PATCH bump, build & tag as test + - main # → MINOR bump, promote test→prod + workflow_dispatch: + inputs: + environment: + description: 'Target environment (test → PATCH bump, prod → MINOR bump)' + required: true + default: 'test' + type: choice + options: + - test + - prod + jobs: - tag: + version: runs-on: ubuntu-latest outputs: - new_version: ${{ steps.new_version.outputs.new_version }} + new_version: ${{ steps.semver.outputs.new_version }} + target_env: ${{ steps.env.outputs.target_env }} steps: - - name: Check out repository code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Get new version - id: new_version + - name: Determine environment + id: env run: | - VERSION=$(git describe --tags --abbrev=0) - echo "Current version: ${VERSION}" + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + ENV="${{ inputs.environment }}" + else + # Push events: test branch → test env, main branch → prod env + [[ "${{ github.ref_name }}" == "main" ]] && ENV="prod" || ENV="${{ github.ref_name }}" + fi + # Validate + if [[ "$ENV" != "test" && "$ENV" != "prod" ]]; then + echo "❌ Invalid environment: $ENV. Must be 'test' or 'prod'." + exit 1 + fi + echo "target_env=$ENV" >> $GITHUB_OUTPUT + + - name: Calculate semver bump + id: semver + run: | + VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0") + echo "Latest tag: ${VERSION}" MAJOR=$(echo ${VERSION} | cut -d "." -f 1) MINOR=$(echo ${VERSION} | cut -d "." -f 2) PATCH=$(echo ${VERSION} | cut -d "." -f 3) - NEW_PATCH=$((PATCH + 1)) - NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" - echo "New version: ${NEW_VERSION}" + + if [[ "${{ steps.env.outputs.target_env }}" == "prod" ]]; then + # MINOR bump, reset PATCH + NEW_MINOR=$((MINOR + 1)) + NEW_VERSION="${MAJOR}.${NEW_MINOR}.0" + echo "Production → minor bump: ${NEW_VERSION}" + else + # test → PATCH bump + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + echo "Test → patch bump: ${NEW_VERSION}" + fi echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT - build_tag_push: + + build-and-push: runs-on: ubuntu-latest - needs: tag + needs: version container: image: 192.168.0.100:8928/hithomelabs/ci-runner:1.0.0 steps: - - name: Check out repository code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Install JDK (Alpine package) run: | apk add --no-cache openjdk17-jdk echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk" >> "$GITHUB_ENV" + - name: Validate Gradle Wrapper (offline checksum) run: | sha256sum --check gradle/wrapper/gradle-wrapper.jar.sha256 - - name: Create and push tag + + - name: Create and push git tag run: | - echo "New version: ${{ needs.tag.outputs.new_version }}" + echo "New version: ${{ needs.version.outputs.new_version }}" git config --global user.name "${{ gitea.actor }}" git config --global user.email "${{ gitea.actor }}@users.noreply.github.com" - git tag -a "pa-${{ needs.tag.outputs.new_version }}" -m "Pushing PA version ${{ needs.tag.outputs.new_version }}" - git push origin "pa-${{ needs.tag.outputs.new_version }}" + git tag -a "${{ needs.version.outputs.new_version }}" \ + -m "Push PA version ${{ needs.version.outputs.new_version }}" + git push origin "${{ needs.version.outputs.new_version }}" + - name: Log in to Gitea Docker Registry uses: docker/login-action@v3 with: registry: 'http://192.168.0.100:8928' username: hitanshu password: ${{ secrets.TOKEN }} - - name: Gradle build image - run: ./gradlew :portainer-automation:bootBuildImage --imageName=192.168.0.100:8928/hithomelabs/portainer-automation:${{ needs.tag.outputs.new_version }} - - name: Tag image as test - run: docker tag 192.168.0.100:8928/hithomelabs/portainer-automation:${{ needs.tag.outputs.new_version }} 192.168.0.100:8928/hithomelabs/portainer-automation:test - - name: Push to Gitea Registry + + - name: Build image (test) or promote (prod) run: | - docker push 192.168.0.100:8928/hithomelabs/portainer-automation:test - docker push 192.168.0.100:8928/hithomelabs/portainer-automation:${{ needs.tag.outputs.new_version }} + ENV="${{ needs.version.outputs.target_env }}" + VER="${{ needs.version.outputs.new_version }}" + REG="192.168.0.100:8928/hithomelabs/portainer-automation" + + if [[ "$ENV" == "test" ]]; then + echo "🔨 Building fresh image for test" + ./gradlew :portainer-automation:bootBuildImage --imageName="${REG}:${VER}" + docker tag "${REG}:${VER}" "${REG}:test" + docker push "${REG}:test" + docker push "${REG}:${VER}" + else + echo "♻️ Promoting test image to prod" + docker pull "${REG}:test" + docker tag "${REG}:test" "${REG}:${VER}" + docker tag "${REG}:${VER}" "${REG}:prod" + docker push "${REG}:prod" + docker push "${REG}:${VER}" + fi -- 2.45.2