diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 4073a8b..ca8997b 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -26,155 +26,15 @@ jobs: if: ${{ secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login docker.io -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin + - name: Log in to Gitea registry + run: echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY}" -u "${REGISTRY_USERNAME}" --password-stdin + - name: Build all-in-one image - run: docker build -t sancho41/condado-newsletter:latest -f Dockerfile.allinone . - - - name: Deploy stack via Portainer API - env: - STACK_NAME: codado-newsletter-stack - PORTAINER_URL: http://portainer.lab/ - PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }} - PORTAINER_ENDPOINT_ID: ${{ secrets.PORTAINER_ENDPOINT_ID }} run: | - set -u - set +e + docker build -t "${REGISTRY}/${IMAGE_NAME}:latest" -f Dockerfile.allinone . + docker tag "${REGISTRY}/${IMAGE_NAME}:latest" "${REGISTRY}/${IMAGE_NAME}:${{ gitea.sha }}" - PORTAINER_BASE_URL=$(printf '%s' "${PORTAINER_URL}" | sed -E 's/[[:space:]]+$//; s#/*$##') - - echo "Portainer deploy debug" - echo "PORTAINER_URL=${PORTAINER_URL}" - echo "PORTAINER_BASE_URL=${PORTAINER_BASE_URL}" - echo "STACK_NAME=${STACK_NAME}" - echo "PORTAINER_ENDPOINT_ID=${PORTAINER_ENDPOINT_ID}" - echo "HTTP_PROXY=${HTTP_PROXY:-}" - echo "HTTPS_PROXY=${HTTPS_PROXY:-}" - echo "NO_PROXY=${NO_PROXY:-}" - - echo "Current runner network info:" - if command -v ip >/dev/null 2>&1; then - ip -4 addr show || true - ip route || true - else - hostname -I || true - fi - - PORTAINER_HOST=$(printf '%s' "${PORTAINER_BASE_URL}" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##; s/:.*$//') - echo "Resolved host target: ${PORTAINER_HOST}" - - PORTAINER_IP="" - ACTIVE_PORTAINER_BASE_URL="${PORTAINER_BASE_URL}" - - if command -v getent >/dev/null 2>&1; then - echo "Host lookup (getent):" - getent hosts "${PORTAINER_HOST}" || true - PORTAINER_IP=$(getent hosts "${PORTAINER_HOST}" | awk 'NR==1{print $1}') - if [ -n "${PORTAINER_IP}" ]; then - PORTAINER_IP_BASE_URL="${PORTAINER_BASE_URL/${PORTAINER_HOST}/${PORTAINER_IP}}" - echo "Portainer IP fallback URL: ${PORTAINER_IP_BASE_URL}" - fi - fi - - STACKS_BODY=$(mktemp) - STACKS_ERR=$(mktemp) - - STACKS_HTTP_CODE=$(curl -sS \ - --noproxy "*" \ - -o "${STACKS_BODY}" \ - -w "%{http_code}" \ - "${ACTIVE_PORTAINER_BASE_URL}/api/stacks" \ - -H "X-API-Key: ${PORTAINER_API_KEY}" \ - 2>"${STACKS_ERR}") - STACKS_CURL_EXIT=$? - - if [ "${STACKS_CURL_EXIT}" -eq 6 ] && [ -n "${PORTAINER_IP:-}" ]; then - echo "Retrying GET /api/stacks with IP fallback due to DNS failure" - STACKS_HTTP_CODE=$(curl -sS \ - --noproxy "*" \ - -o "${STACKS_BODY}" \ - -w "%{http_code}" \ - "${PORTAINER_IP_BASE_URL}/api/stacks" \ - -H "X-API-Key: ${PORTAINER_API_KEY}" \ - 2>"${STACKS_ERR}") - STACKS_CURL_EXIT=$? - if [ "${STACKS_CURL_EXIT}" -eq 0 ]; then - ACTIVE_PORTAINER_BASE_URL="${PORTAINER_IP_BASE_URL}" - fi - fi - - echo "GET /api/stacks curl exit: ${STACKS_CURL_EXIT}" - echo "GET /api/stacks http code: ${STACKS_HTTP_CODE}" - echo "GET /api/stacks stderr:" - cat "${STACKS_ERR}" || true - echo "GET /api/stacks response (sanitized):" - jq -r '.[] | "Id=\(.Id) Name=\(.Name) EndpointId=\(.EndpointId)"' "${STACKS_BODY}" || true - - if [ "${STACKS_CURL_EXIT}" -ne 0 ]; then - echo "Failed to reach Portainer API while listing stacks." - exit "${STACKS_CURL_EXIT}" - fi - - if [ "${STACKS_HTTP_CODE}" -lt 200 ] || [ "${STACKS_HTTP_CODE}" -ge 300 ]; then - echo "Portainer returned a non-success status for stack listing." - exit 1 - fi - - STACK_ID=$(jq -r --arg stack_name "${STACK_NAME}" '.[] | select(.Name == $stack_name) | .Id' "${STACKS_BODY}" | head -n 1) - - APPLY_BODY=$(mktemp) - APPLY_ERR=$(mktemp) - - if [ -n "${STACK_ID}" ]; then - echo "Existing stack found with id=${STACK_ID}; sending update request" - - PAYLOAD=$(jq -n \ - --rawfile stack_file docker-compose.prod.yml \ - '{StackFileContent: $stack_file, Env: [], Prune: false, PullImage: false}') - - APPLY_HTTP_CODE=$(curl -sS -X PUT \ - --noproxy "*" \ - -o "${APPLY_BODY}" \ - -w "%{http_code}" \ - "${ACTIVE_PORTAINER_BASE_URL}/api/stacks/${STACK_ID}?endpointId=${PORTAINER_ENDPOINT_ID}" \ - -H "X-API-Key: ${PORTAINER_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "${PAYLOAD}" \ - 2>"${APPLY_ERR}") - APPLY_CURL_EXIT=$? - else - echo "Stack not found; sending create request" - - PAYLOAD=$(jq -n \ - --arg name "${STACK_NAME}" \ - --rawfile stack_file docker-compose.prod.yml \ - '{Name: $name, StackFileContent: $stack_file, Env: [], FromAppTemplate: false}') - - APPLY_HTTP_CODE=$(curl -sS -X POST \ - --noproxy "*" \ - -o "${APPLY_BODY}" \ - -w "%{http_code}" \ - "${ACTIVE_PORTAINER_BASE_URL}/api/stacks/create/standalone/string?endpointId=${PORTAINER_ENDPOINT_ID}" \ - -H "X-API-Key: ${PORTAINER_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "${PAYLOAD}" \ - 2>"${APPLY_ERR}") - APPLY_CURL_EXIT=$? - fi - - echo "Apply curl exit: ${APPLY_CURL_EXIT}" - echo "Apply http code: ${APPLY_HTTP_CODE}" - echo "Apply stderr:" - cat "${APPLY_ERR}" || true - echo "Apply response body:" - cat "${APPLY_BODY}" || true - - if [ "${APPLY_CURL_EXIT}" -ne 0 ]; then - echo "Failed to reach Portainer API while applying stack changes." - exit "${APPLY_CURL_EXIT}" - fi - - if [ "${APPLY_HTTP_CODE}" -lt 200 ] || [ "${APPLY_HTTP_CODE}" -ge 300 ]; then - echo "Portainer returned a non-success status while applying stack changes." - exit 1 - fi - - echo "Portainer deploy step completed successfully" + - name: Push image tags + run: | + docker push "${REGISTRY}/${IMAGE_NAME}:latest" + docker push "${REGISTRY}/${IMAGE_NAME}:${{ gitea.sha }}" diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..61099d1 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,149 @@ +name: Deploy Production Stack + +on: + workflow_run: + workflows: ["Build And Publish Production Image"] + types: [completed] + workflow_dispatch: + +jobs: + deploy: + name: Deploy Stack Via Portainer + if: ${{ gitea.event_name == 'workflow_dispatch' || gitea.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + env: + STACK_NAME: condado-newsletter-stack + PORTAINER_URL: http://portainer.lab/ + PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }} + PORTAINER_ENDPOINT_ID: ${{ secrets.PORTAINER_ENDPOINT_ID }} + ENV_VARS: ${{ secrets.ENV_VARS }} + steps: + - uses: actions/checkout@v4 + with: + github-server-url: http://gitea.lab + + - name: Validate ENV_VARS secret + run: | + set -eu + if [ -z "${ENV_VARS}" ]; then + echo "ENV_VARS secret is empty." + exit 1 + fi + + - name: Deploy stack via Portainer API + run: | + set -u + set +e + + PORTAINER_BASE_URL=$(printf '%s' "${PORTAINER_URL}" | sed -E 's/[[:space:]]+$//; s#/*$##') + + ENV_JSON=$(printf '%s\n' "${ENV_VARS}" | jq -R -s ' + split("\n") + | map(gsub("\r$"; "")) + | map(select(length > 0)) + | map(select(startswith("#") | not)) + | map(select(test("^[A-Za-z_][A-Za-z0-9_]*=.*$"))) + | map(capture("^(?[A-Za-z_][A-Za-z0-9_]*)=(?.*)$")) + | map({name: .name, value: .value}) + ') + + echo "Loaded $(printf '%s' "${ENV_JSON}" | jq 'length') env entries from ENV_VARS" + + PORTAINER_HOST=$(printf '%s' "${PORTAINER_BASE_URL}" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##; s/:.*$//') + PORTAINER_IP="" + ACTIVE_PORTAINER_BASE_URL="${PORTAINER_BASE_URL}" + + if command -v getent >/dev/null 2>&1; then + PORTAINER_IP=$(getent hosts "${PORTAINER_HOST}" | awk 'NR==1{print $1}') + if [ -n "${PORTAINER_IP}" ]; then + PORTAINER_IP_BASE_URL="${PORTAINER_BASE_URL/${PORTAINER_HOST}/${PORTAINER_IP}}" + fi + fi + + STACKS_BODY=$(mktemp) + STACKS_ERR=$(mktemp) + + STACKS_HTTP_CODE=$(curl -sS \ + --noproxy "*" \ + -o "${STACKS_BODY}" \ + -w "%{http_code}" \ + "${ACTIVE_PORTAINER_BASE_URL}/api/stacks" \ + -H "X-API-Key: ${PORTAINER_API_KEY}" \ + 2>"${STACKS_ERR}") + STACKS_CURL_EXIT=$? + + if [ "${STACKS_CURL_EXIT}" -eq 6 ] && [ -n "${PORTAINER_IP:-}" ]; then + STACKS_HTTP_CODE=$(curl -sS \ + --noproxy "*" \ + -o "${STACKS_BODY}" \ + -w "%{http_code}" \ + "${PORTAINER_IP_BASE_URL}/api/stacks" \ + -H "X-API-Key: ${PORTAINER_API_KEY}" \ + 2>"${STACKS_ERR}") + STACKS_CURL_EXIT=$? + if [ "${STACKS_CURL_EXIT}" -eq 0 ]; then + ACTIVE_PORTAINER_BASE_URL="${PORTAINER_IP_BASE_URL}" + fi + fi + + if [ "${STACKS_CURL_EXIT}" -ne 0 ]; then + cat "${STACKS_ERR}" || true + exit "${STACKS_CURL_EXIT}" + fi + + if [ "${STACKS_HTTP_CODE}" -lt 200 ] || [ "${STACKS_HTTP_CODE}" -ge 300 ]; then + cat "${STACKS_BODY}" || true + exit 1 + fi + + STACK_ID=$(jq -r --arg stack_name "${STACK_NAME}" '.[] | select(.Name == $stack_name) | .Id' "${STACKS_BODY}" | head -n 1) + + APPLY_BODY=$(mktemp) + APPLY_ERR=$(mktemp) + + if [ -n "${STACK_ID}" ]; then + PAYLOAD=$(jq -n \ + --rawfile stack_file docker-compose.prod.yml \ + --argjson env_vars "${ENV_JSON}" \ + '{StackFileContent: $stack_file, Env: $env_vars, Prune: false, PullImage: true}') + + APPLY_HTTP_CODE=$(curl -sS -X PUT \ + --noproxy "*" \ + -o "${APPLY_BODY}" \ + -w "%{http_code}" \ + "${ACTIVE_PORTAINER_BASE_URL}/api/stacks/${STACK_ID}?endpointId=${PORTAINER_ENDPOINT_ID}" \ + -H "X-API-Key: ${PORTAINER_API_KEY}" \ + -H "Content-Type: application/json" \ + -d "${PAYLOAD}" \ + 2>"${APPLY_ERR}") + APPLY_CURL_EXIT=$? + else + PAYLOAD=$(jq -n \ + --arg name "${STACK_NAME}" \ + --rawfile stack_file docker-compose.prod.yml \ + --argjson env_vars "${ENV_JSON}" \ + '{Name: $name, StackFileContent: $stack_file, Env: $env_vars, FromAppTemplate: false}') + + APPLY_HTTP_CODE=$(curl -sS -X POST \ + --noproxy "*" \ + -o "${APPLY_BODY}" \ + -w "%{http_code}" \ + "${ACTIVE_PORTAINER_BASE_URL}/api/stacks/create/standalone/string?endpointId=${PORTAINER_ENDPOINT_ID}" \ + -H "X-API-Key: ${PORTAINER_API_KEY}" \ + -H "Content-Type: application/json" \ + -d "${PAYLOAD}" \ + 2>"${APPLY_ERR}") + APPLY_CURL_EXIT=$? + fi + + if [ "${APPLY_CURL_EXIT}" -ne 0 ]; then + cat "${APPLY_ERR}" || true + exit "${APPLY_CURL_EXIT}" + fi + + if [ "${APPLY_HTTP_CODE}" -lt 200 ] || [ "${APPLY_HTTP_CODE}" -ge 300 ]; then + cat "${APPLY_BODY}" || true + exit 1 + fi + + echo "Portainer deploy completed successfully" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 4251f34..13a04a6 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -59,7 +59,7 @@ services: - "homepage.group=Hyperlink" - "homepage.name=Condado Newsletter" - "homepage.description=Automated newsletter generator using AI" - - "homepage.logo=claude-dark.png" + - "homepage.logo=claude-ai.png" - "homepage.href=http://condado-newsletter.lab" volumes: