diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml new file mode 100644 index 0000000..562c44a --- /dev/null +++ b/.gitea/workflows/ci-cd.yml @@ -0,0 +1,103 @@ +name: production-orders-sync CI/CD + +on: + push: + branches: + - main + workflow_dispatch: + +env: + DEFAULT_REGISTRY_URL: gitea.blyzer.com.br + DEFAULT_IMAGE_REPOSITORY: blyzer/production-order-tiny + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Run syntax checks + run: npm run check + + build-and-push: + name: Build and Push Image + runs-on: ubuntu-latest + needs: + - test + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve image names + env: + REGISTRY_URL: ${{ secrets.REGISTRY_URL }} + IMAGE_REPOSITORY: ${{ secrets.IMAGE_REPOSITORY }} + run: | + set -eu + REGISTRY_URL="${REGISTRY_URL:-$DEFAULT_REGISTRY_URL}" + IMAGE_REPOSITORY="${IMAGE_REPOSITORY:-$DEFAULT_IMAGE_REPOSITORY}" + SHORT_SHA="$(printf '%s' "$GITHUB_SHA" | cut -c1-12)" + IMAGE_REF="${REGISTRY_URL}/${IMAGE_REPOSITORY}" + + echo "REGISTRY_URL=${REGISTRY_URL}" >> "$GITHUB_ENV" + echo "IMAGE_REF=${IMAGE_REF}" >> "$GITHUB_ENV" + echo "IMAGE_TAG=${SHORT_SHA}" >> "$GITHUB_ENV" + echo "Building ${IMAGE_REF}:${SHORT_SHA} and ${IMAGE_REF}:latest" + + - name: Login to registry + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + set -eu + if [ -z "${REGISTRY_USERNAME:-}" ] || [ -z "${REGISTRY_PASSWORD:-}" ]; then + echo "REGISTRY_USERNAME and REGISTRY_PASSWORD secrets are required." + exit 1 + fi + printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" --username "$REGISTRY_USERNAME" --password-stdin + + - name: Build Docker image + run: | + set -eu + docker build --pull \ + --tag "${IMAGE_REF}:${IMAGE_TAG}" \ + --tag "${IMAGE_REF}:latest" \ + . + + - name: Push Docker image + run: | + set -eu + docker push "${IMAGE_REF}:${IMAGE_TAG}" + docker push "${IMAGE_REF}:latest" + + deploy-portainer: + name: Trigger Portainer + runs-on: ubuntu-latest + needs: + - build-and-push + steps: + - name: Trigger Portainer stack webhook + env: + PORTAINER_WEBHOOK_URL: ${{ secrets.PORTAINER_WEBHOOK_URL }} + run: | + set -eu + if [ -z "${PORTAINER_WEBHOOK_URL:-}" ]; then + echo "PORTAINER_WEBHOOK_URL secret is not set; skipping Portainer trigger." + exit 0 + fi + curl --fail --silent --show-error --request POST "$PORTAINER_WEBHOOK_URL" + echo "Portainer webhook triggered." diff --git a/README.md b/README.md index 03ac6b1..5afe6eb 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,22 @@ docker compose run --rm production-orders-sync The included `postgres` service is only a local example. In normal deployment, set `DATABASE_URL` to the existing Graphs Postgres database. +## Gitea Actions + Portainer + +CI/CD is defined in `.gitea/workflows/ci-cd.yml`: + +- run `npm test` +- run `npm run check` +- build and push the Docker image +- trigger the Portainer stack webhook when `PORTAINER_WEBHOOK_URL` is configured + +Portainer stack templates live in `deploy/`: + +- `deploy/portainer-one-shot.yml` for first dry-run checks and backfills +- `deploy/portainer-stack.yml` for the long-running recent polling stack + +See `deploy/README.md` for required Gitea secrets and Portainer environment variables. + ## Logs The worker logs: diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..3c238c4 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,62 @@ +# Portainer Deployment + +This directory contains Portainer stack templates for `production-orders-sync`. + +## Stack Templates + +- `portainer-one-shot.yml`: manual one-shot stack for first dry-run checks and historical backfills. +- `portainer-stack.yml`: long-running polling stack. It runs `npm run sync`, sleeps, then repeats. + +Keep the first production deployment as a one-day dry run: + +```sh +DRY_RUN=true +SYNC_MODE=backfill +SYNC_START_DATE=2026-07-02 +SYNC_END_DATE=2026-07-02 +TINY_REQUEST_DELAY_MS=5000 +``` + +After validating Tiny payload mapping and DB output, switch the polling stack to: + +```sh +DRY_RUN=false +SYNC_MODE=recent +RECENT_SYNC_DAYS=2 +POLL_INTERVAL_SECONDS=1800 +``` + +## Portainer Variables + +Set these in the Portainer stack environment: + +```sh +PRODUCTION_ORDERS_SYNC_IMAGE=gitea.blyzer.com.br/blyzer/production-order-tiny:latest +TINY_API_TOKEN=... +DATABASE_URL=postgres://... +DRY_RUN=true +SYNC_MODE=backfill +SYNC_START_DATE=2026-07-02 +SYNC_END_DATE=2026-07-02 +TINY_REQUEST_DELAY_MS=5000 +``` + +For polling, use `SYNC_MODE=recent` and leave `SYNC_START_DATE` / `SYNC_END_DATE` empty unless you intentionally want an explicit recent range. + +## Gitea Actions Secrets + +The workflow at `.gitea/workflows/ci-cd.yml` expects these repository secrets: + +```sh +REGISTRY_URL=gitea.blyzer.com.br +REGISTRY_USERNAME=... +REGISTRY_PASSWORD=... +IMAGE_REPOSITORY=blyzer/production-order-tiny +PORTAINER_WEBHOOK_URL=https://... # optional but recommended +``` + +`PORTAINER_WEBHOOK_URL` should be the webhook URL generated by Portainer for this stack. If it is missing, the workflow still tests and pushes the image, but it skips the redeploy trigger. + +## Architecture Boundary + +This stack is standalone. It does not call n8n and does not add production-order sync logic to `api-tiny-n8n`. diff --git a/deploy/portainer-one-shot.yml b/deploy/portainer-one-shot.yml new file mode 100644 index 0000000..a065abf --- /dev/null +++ b/deploy/portainer-one-shot.yml @@ -0,0 +1,25 @@ +services: + production-orders-sync: + image: ${PRODUCTION_ORDERS_SYNC_IMAGE:-gitea.blyzer.com.br/blyzer/production-order-tiny:latest} + environment: + NODE_ENV: production + TINY_API_TOKEN: ${TINY_API_TOKEN:?TINY_API_TOKEN is required} + DATABASE_URL: ${DATABASE_URL:-} + TINY_API_BASE_URL: ${TINY_API_BASE_URL:-https://api.tiny.com.br/api2} + TINY_LIST_ENDPOINT: ${TINY_LIST_ENDPOINT:-ordens.producao.pesquisa.php} + TINY_DETAIL_ENDPOINT: ${TINY_DETAIL_ENDPOINT:-ordem.producao.obter.php} + TINY_HTTP_METHOD: ${TINY_HTTP_METHOD:-POST} + TINY_START_DATE_PARAM: ${TINY_START_DATE_PARAM:-dataInicial} + TINY_END_DATE_PARAM: ${TINY_END_DATE_PARAM:-dataFinal} + TINY_DATE_FORMAT: ${TINY_DATE_FORMAT:-DD/MM/YYYY} + TINY_FETCH_DETAILS: ${TINY_FETCH_DETAILS:-false} + TINY_REQUEST_DELAY_MS: ${TINY_REQUEST_DELAY_MS:-5000} + TINY_BLOCK_RETRY_MS: ${TINY_BLOCK_RETRY_MS:-60000} + TINY_MAX_RETRIES: ${TINY_MAX_RETRIES:-3} + SYNC_MODE: ${SYNC_MODE:-backfill} + SYNC_TIME_ZONE: ${SYNC_TIME_ZONE:-America/Sao_Paulo} + RECENT_SYNC_DAYS: ${RECENT_SYNC_DAYS:-2} + SYNC_START_DATE: ${SYNC_START_DATE:?SYNC_START_DATE is required for backfill} + SYNC_END_DATE: ${SYNC_END_DATE:-} + DRY_RUN: ${DRY_RUN:-true} + restart: "no" diff --git a/deploy/portainer-stack.yml b/deploy/portainer-stack.yml new file mode 100644 index 0000000..c5b2749 --- /dev/null +++ b/deploy/portainer-stack.yml @@ -0,0 +1,38 @@ +services: + production-orders-sync: + image: ${PRODUCTION_ORDERS_SYNC_IMAGE:-gitea.blyzer.com.br/blyzer/production-order-tiny:latest} + environment: + NODE_ENV: production + TINY_API_TOKEN: ${TINY_API_TOKEN:?TINY_API_TOKEN is required} + DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required} + TINY_API_BASE_URL: ${TINY_API_BASE_URL:-https://api.tiny.com.br/api2} + TINY_LIST_ENDPOINT: ${TINY_LIST_ENDPOINT:-ordens.producao.pesquisa.php} + TINY_DETAIL_ENDPOINT: ${TINY_DETAIL_ENDPOINT:-ordem.producao.obter.php} + TINY_HTTP_METHOD: ${TINY_HTTP_METHOD:-POST} + TINY_START_DATE_PARAM: ${TINY_START_DATE_PARAM:-dataInicial} + TINY_END_DATE_PARAM: ${TINY_END_DATE_PARAM:-dataFinal} + TINY_DATE_FORMAT: ${TINY_DATE_FORMAT:-DD/MM/YYYY} + TINY_FETCH_DETAILS: ${TINY_FETCH_DETAILS:-false} + TINY_REQUEST_DELAY_MS: ${TINY_REQUEST_DELAY_MS:-5000} + TINY_BLOCK_RETRY_MS: ${TINY_BLOCK_RETRY_MS:-60000} + TINY_MAX_RETRIES: ${TINY_MAX_RETRIES:-3} + SYNC_MODE: ${SYNC_MODE:-recent} + SYNC_TIME_ZONE: ${SYNC_TIME_ZONE:-America/Sao_Paulo} + RECENT_SYNC_DAYS: ${RECENT_SYNC_DAYS:-2} + SYNC_START_DATE: ${SYNC_START_DATE:-} + SYNC_END_DATE: ${SYNC_END_DATE:-} + DRY_RUN: ${DRY_RUN:-true} + POLL_INTERVAL_SECONDS: ${POLL_INTERVAL_SECONDS:-1800} + command: + - sh + - -c + - | + while true; do + npm run sync + exit_code=$$? + if [ "$$exit_code" -ne 0 ]; then + echo "[Production Orders Sync] run failed with exit code $$exit_code; sleeping before retry" + fi + sleep "$${POLL_INTERVAL_SECONDS:-1800}" + done + restart: unless-stopped