From 8a1825799463fd50e16554837a2c6e1cfa090349 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 10 Jul 2026 15:02:30 +0200 Subject: [PATCH] [CI] Flip pulse-frontend deploys to Woodpecker Switch push.yml from shadow tags to production tags (sha-<7> + latest) with per-branch registry cache_to, and add deploy.yml covering Nomad deploy (main + staging) and the main-only CDN scripts publish step (SRI hash + Exoscale S3 upload + Bunny purge for js.ciphera.net). Remove the GitHub Actions build-and-push workflow it replaces. --- .github/workflows/build-and-push.yml | 206 --------------------------- .woodpecker/deploy.yml | 130 +++++++++++++++++ .woodpecker/push.yml | 8 +- 3 files changed, 136 insertions(+), 208 deletions(-) delete mode 100644 .github/workflows/build-and-push.yml create mode 100644 .woodpecker/deploy.yml diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml deleted file mode 100644 index c34f82bdc..000000000 --- a/.github/workflows/build-and-push.yml +++ /dev/null @@ -1,206 +0,0 @@ -name: Build and push to Ciphera registry - -on: - push: - branches: - - main - - staging - workflow_dispatch: - -env: - REGISTRY: registry.ops.ciphera.net - -jobs: - build-push-deploy: - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Determine target - id: target - run: | - if [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "image_name=ciphera-net/pulse-frontend" >> "$GITHUB_OUTPUT" - echo "nomad_job_id=pulse-frontend" >> "$GITHUB_OUTPUT" - echo "vault_kv_path=kv/data/pulse-frontend/config" >> "$GITHUB_OUTPUT" - else - echo "image_name=ciphera-net/pulse-frontend-staging" >> "$GITHUB_OUTPUT" - echo "nomad_job_id=pulse-frontend-staging" >> "$GITHUB_OUTPUT" - echo "vault_kv_path=kv/data/pulse-frontend-staging/config" >> "$GITHUB_OUTPUT" - fi - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Install Teleport - uses: teleport-actions/setup@v1 - with: - version: "18.7.2" - proxy: teleport.ciphera.net:443 - - - name: Teleport tunnel to Vault - id: vault-tunnel - uses: teleport-actions/application-tunnel@v1 - with: - proxy: teleport.ciphera.net:443 - token: ci-deploy - app: vault - listen: "tcp://127.0.0.1:8200" - diag-port: 57001 - - - name: Read build config from Vault - id: vault-config - run: | - VAULT_ADDR=http://127.0.0.1:8200 - TOKEN=$(curl -sf -X POST "${VAULT_ADDR}/v1/auth/approle/login" \ - -d "{\"role_id\":\"${{ secrets.VAULT_CI_ROLE_ID }}\",\"secret_id\":\"${{ secrets.VAULT_CI_SECRET_ID }}\"}" \ - | jq -r '.auth.client_token') - CONFIG=$(curl -sf -H "X-Vault-Token: ${TOKEN}" \ - "${VAULT_ADDR}/v1/${{ steps.target.outputs.vault_kv_path }}" \ - | jq -r '.data.data') - NPM_TOKEN=$(curl -sf -H "X-Vault-Token: ${TOKEN}" \ - "${VAULT_ADDR}/v1/kv/data/shared/github-npm-token" \ - | jq -r '.data.data.value') - { - echo "NEXT_PUBLIC_API_URL=$(echo "$CONFIG" | jq -r '.next_public_api_url')" - echo "NEXT_PUBLIC_APP_URL=$(echo "$CONFIG" | jq -r '.next_public_app_url')" - echo "NEXT_PUBLIC_ID_URL=$(echo "$CONFIG" | jq -r '.next_public_id_url // "https://id.ciphera.net"')" - echo "NEXT_PUBLIC_ID_API_URL=$(echo "$CONFIG" | jq -r '.next_public_id_api_url // "https://api.id.ciphera.net"')" - echo "NEXT_PUBLIC_CAPTCHA_API_URL=$(echo "$CONFIG" | jq -r '.next_public_captcha_api_url // "https://captcha.ciphera.net/api/v1"')" - echo "NEXT_PUBLIC_CHARGEBEE_SITE=$(echo "$CONFIG" | jq -r '.next_public_chargebee_site')" - echo "NEXT_PUBLIC_CHARGEBEE_PUBLISHABLE_KEY=$(echo "$CONFIG" | jq -r '.next_public_chargebee_publishable_key')" - echo "NEXT_PUBLIC_CDN_URL=$(echo "$CONFIG" | jq -r '.next_public_cdn_url')" - echo "NPM_AUTH_TOKEN=${NPM_TOKEN}" - } >> "$GITHUB_OUTPUT" - - - name: Write .npmrc from Vault token - run: | - { - echo "@ciphera-net:registry=https://npm.pkg.github.com/" - echo "//npm.pkg.github.com/:_authToken=${{ steps.vault-config.outputs.NPM_AUTH_TOKEN }}" - echo "legacy-peer-deps=true" - } > /tmp/npmrc - - - name: Log in to Ciphera registry - uses: docker/login-action@v4 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.CIPHERA_REGISTRY_USER }} - password: ${{ secrets.CIPHERA_REGISTRY_PASSWORD }} - - - name: Extract metadata for tags - id: meta - uses: docker/metadata-action@v6 - with: - images: ${{ env.REGISTRY }}/${{ steps.target.outputs.image_name }} - tags: | - type=sha,prefix=sha-,format=short - type=raw,value=latest - - - name: Build and push - id: build - uses: docker/build-push-action@v7 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ steps.target.outputs.image_name }}:buildcache - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ steps.target.outputs.image_name }}:buildcache,mode=max - secret-files: | - npmrc=/tmp/npmrc - build-args: | - NEXT_PUBLIC_API_URL=${{ steps.vault-config.outputs.NEXT_PUBLIC_API_URL }} - NEXT_PUBLIC_APP_URL=${{ steps.vault-config.outputs.NEXT_PUBLIC_APP_URL }} - NEXT_PUBLIC_ID_URL=${{ steps.vault-config.outputs.NEXT_PUBLIC_ID_URL }} - NEXT_PUBLIC_ID_API_URL=${{ steps.vault-config.outputs.NEXT_PUBLIC_ID_API_URL }} - NEXT_PUBLIC_CAPTCHA_API_URL=${{ steps.vault-config.outputs.NEXT_PUBLIC_CAPTCHA_API_URL }} - NEXT_PUBLIC_CHARGEBEE_SITE=${{ steps.vault-config.outputs.NEXT_PUBLIC_CHARGEBEE_SITE }} - NEXT_PUBLIC_CHARGEBEE_PUBLISHABLE_KEY=${{ steps.vault-config.outputs.NEXT_PUBLIC_CHARGEBEE_PUBLISHABLE_KEY }} - NEXT_PUBLIC_CDN_URL=${{ steps.vault-config.outputs.NEXT_PUBLIC_CDN_URL }} - - - name: Generate SRI hashes for tracking scripts - if: github.ref == 'refs/heads/main' - run: | - echo -n '{' > public/script-sri.json - first=true - for file in script.js script.frustration.js script.interactions.js; do - [ -f "public/$file" ] || continue - hash=$(openssl dgst -sha384 -binary "public/$file" | openssl base64 -A) - $first || echo -n ',' >> public/script-sri.json - echo -n "\"$file\":\"sha384-${hash}\"" >> public/script-sri.json - first=false - done - echo '}' >> public/script-sri.json - echo "Generated SRI hashes:" - cat public/script-sri.json - - - name: Upload tracking scripts to CDN - if: github.ref == 'refs/heads/main' - env: - AWS_ACCESS_KEY_ID: ${{ secrets.EXOSCALE_CDN_SCRIPTS_KEY }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.EXOSCALE_CDN_SCRIPTS_SECRET }} - AWS_DEFAULT_REGION: ch-dk-2 - run: | - for file in script.js script.frustration.js script.interactions.js script-sri.json; do - [ -f "public/$file" ] || continue - ct="application/javascript" - [ "$file" = "script-sri.json" ] && ct="application/json" - aws s3 cp "public/$file" "s3://ciphera-scripts/$file" \ - --endpoint-url https://sos-ch-dk-2.exo.io \ - --content-type "$ct" \ - --cache-control "public, max-age=7200" \ - --acl public-read \ - --quiet - done - echo "Scripts uploaded to CDN" - - - name: Purge CDN cache for tracking scripts - if: github.ref == 'refs/heads/main' - run: | - for path in script.js script.frustration.js script.interactions.js script-sri.json; do - curl -fsS -X POST \ - "https://api.bunny.net/purge?url=https://js.ciphera.net/$path&async=false" \ - -H "AccessKey: ${{ secrets.BUNNY_API_KEY }}" - done - echo "CDN cache purged" - - - name: Stop Vault tunnel - run: | - kill ${{ steps.vault-tunnel.outputs.tbot-pid }} 2>/dev/null || true - sleep 1 - - - name: Teleport tunnel to Nomad - id: tunnel - uses: teleport-actions/application-tunnel@v1 - with: - proxy: teleport.ciphera.net:443 - token: ci-deploy - app: nomad - listen: "tcp://127.0.0.1:4646" - diag-port: 57002 - - - name: Deploy to Nomad - env: - NOMAD_TOKEN: ${{ secrets.NOMAD_CI_TOKEN }} - run: | - sleep 3 - DEPLOY_SHA="${GITHUB_SHA::7}" - JOB_ID="${{ steps.target.outputs.nomad_job_id }}" - JOB=$(curl -sf -H "X-Nomad-Token: $NOMAD_TOKEN" \ - http://127.0.0.1:4646/v1/job/$JOB_ID) - echo "$JOB" | jq " - .Meta.deploy_sha = \"$DEPLOY_SHA\" | - .TaskGroups[0].Tasks[0].Config.image = \"${{ env.REGISTRY }}/${{ steps.target.outputs.image_name }}@${{ steps.build.outputs.digest }}\" | - .TaskGroups[0].Tasks[0].Config.force_pull = true - " > /tmp/job.json - curl -fsS -X POST \ - -H "X-Nomad-Token: $NOMAD_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{\"Job\": $(cat /tmp/job.json)}" \ - http://127.0.0.1:4646/v1/jobs - echo "Deployed $JOB_ID (sha: $DEPLOY_SHA)" diff --git a/.woodpecker/deploy.yml b/.woodpecker/deploy.yml new file mode 100644 index 000000000..aa5c0c38c --- /dev/null +++ b/.woodpecker/deploy.yml @@ -0,0 +1,130 @@ +depends_on: + - push + +when: + - event: push + branch: main + - event: push + branch: staging + +steps: + deploy: + image: alpine:3.20 + when: + - event: push + branch: main + volumes: + - /opt/woodpecker/secrets/nomad-token:/run/ci/nomad-token:ro + - /opt/vault-agent/tls/ca.pem:/run/ci/nomad-ca.pem:ro + environment: + REGISTRY_USERNAME: + from_secret: registry_username + REGISTRY_PASSWORD: + from_secret: registry_password + commands: + - apk add --no-cache curl jq >/dev/null + - export NOMAD_TOKEN=$(cat /run/ci/nomad-token) + - export DEPLOY_SHA=${CI_COMMIT_SHA:0:7} + - | + DIGEST=$(curl -sfI -u "$REGISTRY_USERNAME:$REGISTRY_PASSWORD" \ + -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json" \ + "https://registry.ops.ciphera.net/v2/ciphera-net/pulse-frontend/manifests/sha-$DEPLOY_SHA" \ + | tr -d '\r' | awk 'tolower($1)=="docker-content-digest:"{print $2}') + test -n "$DIGEST" + - | + curl -sf --cacert /run/ci/nomad-ca.pem -H "X-Nomad-Token: $NOMAD_TOKEN" \ + https://10.10.0.110:4646/v1/job/pulse-frontend | jq \ + ".Meta.deploy_sha = \"$DEPLOY_SHA\" | + .TaskGroups[0].Tasks[0].Config.image = \"registry.ops.ciphera.net/ciphera-net/pulse-frontend@$DIGEST\" | + .TaskGroups[0].Tasks[0].Config.force_pull = true" > /tmp/job.json + - | + curl -fsS --cacert /run/ci/nomad-ca.pem -X POST -H "X-Nomad-Token: $NOMAD_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"Job\": $(cat /tmp/job.json)}" \ + https://10.10.0.110:4646/v1/jobs + - echo "Deployed pulse-frontend ($DEPLOY_SHA)" + + deploy-staging: + image: alpine:3.20 + when: + - event: push + branch: staging + volumes: + - /opt/woodpecker/secrets/nomad-token:/run/ci/nomad-token:ro + - /opt/vault-agent/tls/ca.pem:/run/ci/nomad-ca.pem:ro + environment: + REGISTRY_USERNAME: + from_secret: registry_username + REGISTRY_PASSWORD: + from_secret: registry_password + commands: + - apk add --no-cache curl jq >/dev/null + - export NOMAD_TOKEN=$(cat /run/ci/nomad-token) + - export DEPLOY_SHA=${CI_COMMIT_SHA:0:7} + - | + DIGEST=$(curl -sfI -u "$REGISTRY_USERNAME:$REGISTRY_PASSWORD" \ + -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json" \ + "https://registry.ops.ciphera.net/v2/ciphera-net/pulse-frontend-staging/manifests/sha-$DEPLOY_SHA" \ + | tr -d '\r' | awk 'tolower($1)=="docker-content-digest:"{print $2}') + test -n "$DIGEST" + - | + curl -sf --cacert /run/ci/nomad-ca.pem -H "X-Nomad-Token: $NOMAD_TOKEN" \ + https://10.10.0.110:4646/v1/job/pulse-frontend-staging | jq \ + ".Meta.deploy_sha = \"$DEPLOY_SHA\" | + .TaskGroups[0].Tasks[0].Config.image = \"registry.ops.ciphera.net/ciphera-net/pulse-frontend-staging@$DIGEST\" | + .TaskGroups[0].Tasks[0].Config.force_pull = true" > /tmp/job.json + - | + curl -fsS --cacert /run/ci/nomad-ca.pem -X POST -H "X-Nomad-Token: $NOMAD_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"Job\": $(cat /tmp/job.json)}" \ + https://10.10.0.110:4646/v1/jobs + - echo "Deployed pulse-frontend-staging ($DEPLOY_SHA)" + + cdn-scripts: + image: alpine:3.20 + when: + - event: push + branch: main + failure: ignore + environment: + AWS_ACCESS_KEY_ID: + from_secret: exoscale_cdn_scripts_key + AWS_SECRET_ACCESS_KEY: + from_secret: exoscale_cdn_scripts_secret + AWS_DEFAULT_REGION: ch-dk-2 + BUNNY_API_KEY: + from_secret: bunny_api_key + commands: + - apk add --no-cache aws-cli curl openssl >/dev/null + - | + echo -n '{' > public/script-sri.json + first=true + for file in script.js script.frustration.js script.interactions.js; do + [ -f "public/$file" ] || continue + hash=$(openssl dgst -sha384 -binary "public/$file" | openssl base64 -A) + $first || echo -n ',' >> public/script-sri.json + echo -n "\"$file\":\"sha384-${hash}\"" >> public/script-sri.json + first=false + done + echo '}' >> public/script-sri.json + cat public/script-sri.json + - | + for file in script.js script.frustration.js script.interactions.js script-sri.json; do + [ -f "public/$file" ] || continue + ct="application/javascript" + [ "$file" = "script-sri.json" ] && ct="application/json" + aws s3 cp "public/$file" "s3://ciphera-scripts/$file" \ + --endpoint-url https://sos-ch-dk-2.exo.io \ + --content-type "$ct" \ + --cache-control "public, max-age=7200" \ + --acl public-read \ + --quiet + done + echo "Scripts uploaded to CDN" + - | + for path in script.js script.frustration.js script.interactions.js script-sri.json; do + curl -fsS -X POST \ + "https://api.bunny.net/purge?url=https://js.ciphera.net/$path&async=false" \ + -H "AccessKey: $BUNNY_API_KEY" + done + echo "CDN cache purged" diff --git a/.woodpecker/push.yml b/.woodpecker/push.yml index 29338abee..5dfc6749b 100644 --- a/.woodpecker/push.yml +++ b/.woodpecker/push.yml @@ -25,13 +25,15 @@ steps: registry: registry.ops.ciphera.net repo: registry.ops.ciphera.net/ciphera-net/pulse-frontend tags: - - shadow-${CI_COMMIT_SHA:0:7} + - sha-${CI_COMMIT_SHA:0:7} + - latest username: from_secret: registry_username password: from_secret: registry_password cache_from: - type=registry\,ref=registry.ops.ciphera.net/ciphera-net/pulse-frontend:buildcache + cache_to: type=registry,ref=registry.ops.ciphera.net/ciphera-net/pulse-frontend:buildcache,mode=max secrets: - id=npmrc\,src=.npmrc-ci build_args: @@ -61,13 +63,15 @@ steps: registry: registry.ops.ciphera.net repo: registry.ops.ciphera.net/ciphera-net/pulse-frontend-staging tags: - - shadow-staging-${CI_COMMIT_SHA:0:7} + - sha-${CI_COMMIT_SHA:0:7} + - latest username: from_secret: registry_username password: from_secret: registry_password cache_from: - type=registry\,ref=registry.ops.ciphera.net/ciphera-net/pulse-frontend-staging:buildcache + cache_to: type=registry,ref=registry.ops.ciphera.net/ciphera-net/pulse-frontend-staging:buildcache,mode=max secrets: - id=npmrc\,src=.npmrc-ci build_args: