Deploy #21
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 | |
| on: | |
| workflow_dispatch: | |
| concurrency: | |
| group: deploy-docs-tigrbl-com | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - run: npm ci | |
| - run: npm run check | |
| - run: npm run build | |
| - name: Validate Docker proxy target | |
| run: npm run proxy:target-check | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: docs-dist | |
| path: dist | |
| if-no-files-found: error | |
| deploy: | |
| needs: build | |
| runs-on: deployment | |
| env: | |
| NPM_BASE_URL: ${{ secrets.NPM_BASE_URL }} | |
| NPM_IDENTITY: ${{ secrets.NPM_IDENTITY }} | |
| NPM_SECRET: ${{ secrets.NPM_SECRET || secrets.NPM_PASSWORD || secrets.NGINX_PROXY_MANAGER_SECRET }} | |
| NPM_TIMEOUT_S: 30 | |
| NAMECHEAP_API_USER: ${{ secrets.NAMECHEAP_API_USER }} | |
| NAMECHEAP_API_KEY: ${{ secrets.NAMECHEAP_API_KEY }} | |
| NAMECHEAP_USERNAME: ${{ secrets.NAMECHEAP_USERNAME }} | |
| NAMECHEAP_CLIENT_IP: ${{ secrets.NAMECHEAP_CLIENT_IP }} | |
| SITE_SERVER_IP: ${{ secrets.SITE_SERVER_IP || secrets.SERVER_IP || secrets.DEPLOY_SERVER_IP }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: docs-dist | |
| path: dist | |
| - name: Install npmctl from PyPI | |
| run: python -m pip install --upgrade "npmctl>=0.3.10" "npmctl-namecheap>=0.3.10" | |
| - name: Validate npmctl orchestration secrets | |
| shell: bash | |
| run: | | |
| missing=0 | |
| for name in NPM_BASE_URL NPM_IDENTITY NPM_SECRET NAMECHEAP_API_USER NAMECHEAP_API_KEY NAMECHEAP_USERNAME NAMECHEAP_CLIENT_IP SITE_SERVER_IP; do | |
| if [ -z "${!name}" ]; then | |
| echo "Missing required secret-backed environment variable: ${name}" | |
| missing=1 | |
| fi | |
| done | |
| exit ${missing} | |
| - name: Validate Docker proxy target | |
| run: npm run proxy:target-check | |
| - name: Render proxy desired state | |
| shell: bash | |
| run: | | |
| mkdir -p .tmp/npmctl | |
| sed "s/__NPM_IDENTITY__/${NPM_IDENTITY}/g" desired-state/proxy.yaml > .tmp/npmctl/proxy.yaml | |
| - name: Validate npmctl proxy desired state | |
| run: npmctl validate .tmp/npmctl/proxy.yaml | |
| - name: Build site image | |
| run: docker-compose -f docker-compose.yaml build docs-tigrbl-com | |
| - name: Restart site container | |
| run: | | |
| docker-compose -f docker-compose.yaml stop docs-tigrbl-com || true | |
| docker-compose -f docker-compose.yaml rm -f docs-tigrbl-com || true | |
| docker rm -f docs-tigrbl-com || true | |
| docker-compose -f docker-compose.yaml up -d --no-build --force-recreate --no-deps docs-tigrbl-com | |
| - name: Render DNS desired state | |
| shell: bash | |
| run: | | |
| mkdir -p .tmp | |
| sed "s/__SITE_SERVER_IP__/${SITE_SERVER_IP}/g" desired-state/dns.yaml > .tmp/dns.yaml | |
| - name: Validate DNS desired state | |
| run: npmctl validate .tmp/dns.yaml | |
| - name: Validate Namecheap provider | |
| run: npmctl dns doctor --provider namecheap | |
| - name: Normalize Namecheap DNS records | |
| run: python scripts/namecheap-normalize-dns.py .tmp/dns.yaml | |
| - name: Apply Namecheap DNS with npmctl | |
| continue-on-error: true | |
| run: npmctl apply .tmp/dns.yaml --owner docs-tigrbl-com | |
| - name: Check Nginx Proxy Manager API schema | |
| continue-on-error: true | |
| run: npmctl schema check | |
| - name: Adopt matching unmanaged NPM resources | |
| continue-on-error: true | |
| run: npmctl adopt .tmp/npmctl/proxy.yaml --owner docs-tigrbl-com --force | |
| - name: Plan Nginx Proxy Manager host with npmctl | |
| run: npmctl plan .tmp/npmctl/proxy.yaml --owner docs-tigrbl-com | |
| - name: Apply Nginx Proxy Manager host with npmctl | |
| run: npmctl apply .tmp/npmctl/proxy.yaml --owner docs-tigrbl-com | |
| - name: Verify public HTTPS and HTTP redirect | |
| shell: bash | |
| run: | | |
| node <<'NODE' | |
| const fs = require('node:fs'); | |
| const { spawnSync } = require('node:child_process'); | |
| const manifest = JSON.parse(fs.readFileSync('site.manifest.json', 'utf8')); | |
| const primary = manifest.host || manifest.site; | |
| const hosts = [primary]; | |
| if (!primary.startsWith('docs.') && !primary.startsWith('www.')) { | |
| hosts.push(`www.${primary}`); | |
| } | |
| let failed = false; | |
| for (const host of hosts) { | |
| const https = spawnSync('curl', ['-I', '-sS', '--max-time', '25', '-o', '/dev/null', '-w', '%{http_code}|%{errormsg}', `https://${host}/`], { encoding: 'utf8' }); | |
| const httpsOut = `${https.stdout || ''}${https.stderr || ''}`.trim(); | |
| const httpsCode = Number((https.stdout || '').split('|')[0]); | |
| console.log(`${host} HTTPS ${httpsOut}`); | |
| if (https.status !== 0 || httpsCode !== 200 || httpsOut.includes('502') || /timed out|Could not resolve|Failed to connect/i.test(httpsOut)) { | |
| failed = true; | |
| } | |
| const http = spawnSync('curl', ['-I', '-sS', '--max-time', '25', '-o', '/dev/null', '-w', '%{http_code}|%{redirect_url}|%{errormsg}', `http://${host}/`], { encoding: 'utf8' }); | |
| const httpOut = `${http.stdout || ''}${http.stderr || ''}`.trim(); | |
| const httpCode = Number((http.stdout || '').split('|')[0]); | |
| console.log(`${host} HTTP ${httpOut}`); | |
| if (http.status !== 0 || httpCode < 300 || httpCode >= 400 || httpOut.includes('502') || /timed out|Could not resolve|Failed to connect/i.test(httpOut)) { | |
| failed = true; | |
| } | |
| } | |
| if (failed) process.exit(1); | |
| NODE |