Skip to content

Deploy

Deploy #21

Workflow file for this run

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