chore(ci): bump actions/github-script from 8 to 9 (#2172) #228
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Develop | |
| # Required secrets: | |
| # DEPLOY_SSH_KEY - Private SSH key (Ed25519 or RSA 4096+) for the deploy user on the Droplet | |
| # DROPLET_IP - IP address of the DigitalOcean Droplet (e.g. 143.198.xxx.xxx) | |
| # | |
| # The deploy user is created by deploy/demo/provision.sh with docker group membership | |
| # and ownership of /opt/meridian-develop/. Run provision.sh once as root to set up the server. | |
| on: | |
| push: | |
| branches: [develop] | |
| workflow_dispatch: | |
| concurrency: | |
| group: deploy-develop | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| build-and-push: | |
| name: Build and Push Docker Image | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write # Required for cosign keyless signing via OIDC | |
| outputs: | |
| image-digest: ${{ steps.build.outputs.digest }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.26.2' | |
| cache: true | |
| - name: Set up buf | |
| uses: bufbuild/buf-action@v1 | |
| with: | |
| setup_only: true | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate protobuf files | |
| run: buf generate | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=develop | |
| type=sha,prefix=develop-,format=short | |
| - name: Build and push Docker image | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./cmd/meridian/Dockerfile | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-args: | | |
| VERSION=${{ github.ref_name }} | |
| COMMIT=${{ github.sha }} | |
| BUILD_DATE=${{ github.event.head_commit.timestamp || github.event.repository.updated_at }} | |
| - name: Install Cosign | |
| uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 | |
| - name: Sign container image | |
| run: | | |
| cosign sign --yes \ | |
| ghcr.io/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} | |
| build-frontend: | |
| name: Build Frontend | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Set up buf | |
| uses: bufbuild/buf-action@v1 | |
| with: | |
| setup_only: true | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Install dependencies | |
| working-directory: frontend | |
| run: npm ci | |
| - name: Generate protobuf TypeScript clients | |
| working-directory: frontend | |
| run: | | |
| export PATH="$PWD/node_modules/.bin:$PATH" | |
| buf generate --template buf.gen.yaml ../api/proto | |
| - name: Build frontend | |
| working-directory: frontend | |
| env: | |
| VITE_API_BASE_URL: https://develop.meridianhub.cloud | |
| VITE_BASE_DOMAIN: develop.meridianhub.cloud | |
| VITE_BUILD_VERSION: ${{ github.ref_name }} | |
| VITE_BUILD_COMMIT: ${{ github.sha }} | |
| run: npx vite build | |
| - name: Upload frontend artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: frontend-dist-develop | |
| path: frontend/dist/ | |
| retention-days: 1 | |
| deploy: | |
| name: Deploy to Develop Environment | |
| needs: [build-and-push, build-frontend] | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: develop | |
| url: https://develop.meridianhub.cloud | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Install Cosign | |
| uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 | |
| - name: Verify container image signature | |
| run: | | |
| cosign verify \ | |
| --certificate-identity="https://github.com/${{ github.repository }}/.github/workflows/deploy-develop.yml@refs/heads/develop" \ | |
| --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ | |
| ghcr.io/${{ env.IMAGE_NAME }}@${{ needs.build-and-push.outputs.image-digest }} | |
| - name: Download frontend artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: frontend-dist-develop | |
| path: frontend-dist/ | |
| - name: Deploy frontend via SCP | |
| uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 | |
| with: | |
| host: ${{ secrets.DROPLET_IP }} | |
| username: deploy | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| source: "frontend-dist/*" | |
| target: /opt/meridian-develop/frontend/ | |
| strip_components: 1 | |
| overwrite: true | |
| - name: Deploy config files via SCP | |
| uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 | |
| with: | |
| host: ${{ secrets.DROPLET_IP }} | |
| username: deploy | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| source: "deploy/develop/docker-compose.develop.yml,deploy/develop/init-databases-develop.sql" | |
| target: /opt/meridian-develop/ | |
| strip_components: 2 | |
| overwrite: true | |
| - name: Deploy Caddyfile to demo stack | |
| uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 | |
| with: | |
| host: ${{ secrets.DROPLET_IP }} | |
| username: deploy | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| source: "deploy/demo/Caddyfile" | |
| target: /opt/meridian/ | |
| strip_components: 2 | |
| overwrite: true | |
| - name: Deploy via SSH | |
| uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 | |
| env: | |
| IMAGE_DIGEST: ${{ needs.build-and-push.outputs.image-digest }} | |
| with: | |
| host: ${{ secrets.DROPLET_IP }} | |
| username: deploy | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| envs: IMAGE_DIGEST | |
| script: | | |
| cd /opt/meridian-develop | |
| # Pull the exact verified digest to prevent TOCTOU between verify and deploy | |
| docker pull ghcr.io/meridianhub/meridian@${IMAGE_DIGEST} | |
| docker tag ghcr.io/meridianhub/meridian@${IMAGE_DIGEST} ghcr.io/meridianhub/meridian:develop | |
| docker compose -f docker-compose.develop.yml up -d --remove-orphans | |
| # Verify all services came up (see deploy-demo.yml for rationale) | |
| sleep 5 | |
| failed=$(docker compose -f docker-compose.develop.yml ps --status exited --format '{{.Service}}') | |
| if [ -n "$failed" ]; then | |
| echo "Services failed to start: $failed - retrying" | |
| docker compose -f docker-compose.develop.yml up -d $failed | |
| sleep 3 | |
| still_failed=$(docker compose -f docker-compose.develop.yml ps --status exited --format '{{.Service}}') | |
| if [ -n "$still_failed" ]; then | |
| echo "Services still down after retry: $still_failed" | |
| exit 1 | |
| fi | |
| fi | |
| echo "All services running" | |
| # Reload Caddy from the demo stack (Caddy runs there, serves both environments) | |
| docker compose -f /opt/meridian/docker-compose.yml exec -T caddy caddy reload --config /etc/caddy/Caddyfile | |
| docker image prune -f | |
| - name: Ensure databases exist | |
| uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 | |
| with: | |
| host: ${{ secrets.DROPLET_IP }} | |
| username: deploy | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| script: | | |
| # Docker init scripts only run on first volume creation. When new | |
| # databases are added to init-databases-develop.sql, existing volumes | |
| # won't have them. Create any missing databases so the app can start. | |
| cd /opt/meridian-develop | |
| created=0 | |
| for db in $(grep -oP 'CREATE DATABASE \K\w+' init-databases-develop.sql); do | |
| exists=$(docker exec postgres-develop psql -U meridian -tAc \ | |
| "SELECT 1 FROM pg_database WHERE datname = '${db}'") | |
| if [ "$exists" != "1" ]; then | |
| docker exec postgres-develop createdb -U meridian "${db}" | |
| echo "Created missing database: ${db}" | |
| created=1 | |
| fi | |
| done | |
| if [ "$created" = "1" ]; then | |
| echo "New databases created - restarting app to pick up connections" | |
| docker compose -f docker-compose.develop.yml restart meridian-develop | |
| fi | |
| # Wait for the container to be running (not restarting) before migrations. | |
| # The app image is distroless - no shell utilities - so use docker inspect. | |
| for i in $(seq 1 30); do | |
| status=$(docker inspect --format='{{.State.Status}}' meridian-develop 2>/dev/null || echo "missing") | |
| if [ "$status" = "running" ]; then | |
| echo "Container is running" | |
| exit 0 | |
| fi | |
| echo "Waiting for container (status: $status)... ($i/30)" | |
| sleep 2 | |
| done | |
| echo "Container failed to start after 60s" | |
| exit 1 | |
| - name: Run migrations | |
| uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 | |
| with: | |
| host: ${{ secrets.DROPLET_IP }} | |
| username: deploy | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| script: | | |
| docker exec meridian-develop /meridian --migrate | |
| - name: Restart meridian-develop post-migration | |
| uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 | |
| with: | |
| host: ${{ secrets.DROPLET_IP }} | |
| username: deploy | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| script: | | |
| cd /opt/meridian-develop | |
| # Reset tenant provisioning state and drop tenant schemas so every | |
| # deploy re-provisions from scratch. Three states must be reconciled: | |
| # | |
| # 1. tenant.status - the provisioning worker polls this via | |
| # ListByStatus(StatusProvisioningPending) (provisioning_worker.go). | |
| # Without resetting, the worker never claims the tenant. | |
| # | |
| # 2. tenant_provisioning.service_schemas - the provisioner checks | |
| # this JSONB per-service and short-circuits via "service already | |
| # provisioned, skipping" (postgres_provisioner.go). Clearing to | |
| # '[]' forces a full re-run of the loop. | |
| # | |
| # 3. Physical org_<tenant> schemas in each service database - if | |
| # left in place they may contain partial/stale tables from a | |
| # previous broken run, causing migrations to fail with "relation | |
| # does not exist" when they reference objects that got renamed | |
| # or dropped earlier. DROP SCHEMA CASCADE ensures migrations | |
| # run against completely empty schemas, matching the E2E path | |
| # which is known to pass. | |
| # | |
| # Stop meridian-develop BEFORE resetting state. The worker polls | |
| # tenant.status every 10s, so if the app is still running when we | |
| # flip tenants to provisioning_pending it will race with this | |
| # script: start provisioning against half-cleaned databases and | |
| # then get interrupted when we issue the restart. Stopping first | |
| # eliminates the race, and starting (not restarting) afterwards | |
| # lets the worker observe the fully-reset state on boot. | |
| # | |
| # Must run AFTER migrations so the provisioner uses up-to-date DDL. | |
| docker compose -f docker-compose.develop.yml stop meridian-develop | |
| SERVICE_DBS="meridian_party meridian_current_account meridian_position_keeping meridian_financial_accounting meridian_payment_order meridian_market_information meridian_reference_data meridian_internal_account meridian_reconciliation meridian_identity meridian_control_plane" | |
| for DB in $SERVICE_DBS; do | |
| docker exec postgres-develop psql -U meridian -d $DB -tA -c " | |
| SELECT format('DROP SCHEMA IF EXISTS %I CASCADE', nspname) | |
| FROM pg_namespace WHERE nspname LIKE 'org\_%' ESCAPE '\\' | |
| " | while read stmt; do | |
| [ -z "$stmt" ] && continue | |
| docker exec postgres-develop psql -U meridian -d $DB -c "$stmt" | |
| done | |
| done | |
| docker exec postgres-develop psql -U meridian -d meridian_platform -c " | |
| UPDATE tenant SET status = 'provisioning_pending', updated_at = NOW() WHERE status != 'deprovisioned'; | |
| UPDATE tenant_provisioning SET state = 'pending', service_schemas = '[]'::jsonb, error_message = '' WHERE state != 'deprovisioned'; | |
| " | |
| # Start the app back up with the reset state in place. The | |
| # provisioning worker will claim the pending tenants on its first | |
| # poll and re-run all migrations against the newly empty schemas. | |
| docker compose -f docker-compose.develop.yml start meridian-develop | |
| - name: Seed develop data | |
| uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 | |
| with: | |
| host: ${{ secrets.DROPLET_IP }} | |
| username: deploy | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| script: | | |
| # seed-dev has its own gateway health polling (60s timeout, 2s interval) | |
| # so no extra wait needed between restart and seed. | |
| docker exec meridian-develop /seed-dev \ | |
| --gateway-url=http://localhost:8090 \ | |
| --grpc-addr=localhost:50051 \ | |
| --tenant-id=volterra_energy \ | |
| --tenant-slug=volterra-energy \ | |
| --display-name='Volterra Energy' \ | |
| --subdomain=volterra-energy.develop.meridianhub.cloud \ | |
| --manifest=/app/examples/manifests/energy.json \ | |
| --with-fixtures | |
| - name: Verify deployment health | |
| uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 | |
| with: | |
| host: ${{ secrets.DROPLET_IP }} | |
| username: deploy | |
| key: ${{ secrets.DEPLOY_SSH_KEY }} | |
| script: | | |
| max_attempts=30 | |
| attempt=0 | |
| until [ $attempt -ge $max_attempts ]; do | |
| attempt=$((attempt+1)) | |
| echo "Health check attempt $attempt/$max_attempts" | |
| if curl -sf http://localhost:80/healthz -H 'Host: develop.meridianhub.cloud' > /dev/null 2>&1; then | |
| echo "Service is healthy" | |
| exit 0 | |
| fi | |
| sleep 5 | |
| done | |
| echo "Health check failed after $max_attempts attempts" | |
| cd /opt/meridian-develop && docker compose -f docker-compose.develop.yml logs --tail=50 | |
| exit 1 | |
| notify-failure: | |
| name: Notify Deployment Failure | |
| needs: [build-and-push, build-frontend, deploy] | |
| if: failure() | |
| runs-on: ubuntu-latest | |
| permissions: | |
| statuses: write | |
| steps: | |
| - name: Report deployment failure | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| await github.rest.repos.createCommitStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: context.sha, | |
| state: 'failure', | |
| context: 'deploy/develop', | |
| description: 'Develop environment deployment failed', | |
| target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` | |
| }); |