Deploy mirror: f450e27c1b47efb4048c011333770bad69905769 #4
Workflow file for this run
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 Mirror | |
| run-name: "Deploy mirror: ${{ github.sha }}" | |
| concurrency: | |
| group: deploy-mirror | |
| cancel-in-progress: true | |
| on: | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| jobs: | |
| deploy: | |
| name: Deploy Mirror | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Install and typecheck | |
| run: | | |
| npm ci | |
| npx astro sync | |
| npm run typecheck | |
| - name: Build and push | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: production | |
| push: true | |
| provenance: false | |
| sbom: false | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| tags: | | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:mirror-${{ github.sha }} | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:mirror-latest | |
| labels: ${{ steps.meta.outputs.labels }} | |
| - name: Start SSH agent | |
| uses: webfactory/ssh-agent@v0.9.0 | |
| with: | |
| ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| - name: Add known hosts | |
| run: | | |
| mkdir -p ~/.ssh | |
| ssh-keyscan -H "${{ secrets.SSH_HOST_DEV }}" >> ~/.ssh/known_hosts | |
| - name: Deploy over SSH | |
| env: | |
| SSH_USER: ${{ secrets.SSH_USER }} | |
| SSH_HOST: ${{ secrets.SSH_HOST_DEV }} | |
| REPO: ${{ github.repository }} | |
| BRANCH: ${{ github.ref_name }} | |
| GHCR_USER: ${{ secrets.GHCR_USER }} | |
| GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} | |
| IMAGE_TAG: mirror-${{ github.sha }} | |
| run: | | |
| ssh "${SSH_USER}@${SSH_HOST}" \ | |
| "env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE_TAG='${IMAGE_TAG}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS' | |
| set -euo pipefail | |
| REPO="${1:?missing repo}" | |
| BRANCH="${2:-main}" | |
| : "${IMAGE_TAG:?missing IMAGE_TAG}" | |
| : "${GHCR_USER:?missing GHCR_USER}" | |
| : "${GHCR_TOKEN:?missing GHCR_TOKEN}" | |
| STACK_NAME="underlay-mirror" | |
| REPO_NAME="${REPO##*/}" | |
| APP_DIR="/srv/${REPO_NAME}-mirror" | |
| PROD_DIR="/srv/${REPO_NAME}" | |
| REPO_SSH="git@github.com:${REPO}.git" | |
| ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null | |
| chmod 600 ~/.ssh/known_hosts | |
| if [[ ! -d "${APP_DIR}/.git" ]]; then | |
| sudo mkdir -p "${APP_DIR}" | |
| sudo chown -R "$USER:$USER" "${APP_DIR}" | |
| git clone --branch "${BRANCH}" "${REPO_SSH}" "${APP_DIR}" | |
| fi | |
| cd "${APP_DIR}" | |
| git fetch --prune --tags origin | |
| git checkout "${BRANCH}" | |
| git pull origin "${BRANCH}" | |
| # Load secrets from prod's .env (already decrypted on the server) | |
| # This gives us S3_*, UNDERLAY_UPSTREAM_API_KEY, etc. | |
| if [[ -f "${PROD_DIR}/.env" ]]; then | |
| set -a | |
| source <(grep -v '^#' "${PROD_DIR}/.env" | grep -v '^$') | |
| set +a | |
| else | |
| echo "ERROR: ${PROD_DIR}/.env not found — run a prod deploy first" | |
| exit 1 | |
| fi | |
| # Init swarm if not already active | |
| if ! sudo docker info --format '{{.Swarm.LocalNodeState}}' | grep -qx active; then | |
| sudo docker swarm init | |
| fi | |
| echo "$GHCR_TOKEN" | sudo docker login ghcr.io -u "$GHCR_USER" --password-stdin | |
| sudo docker pull "ghcr.io/${REPO}:${IMAGE_TAG}" | |
| # Deploy using docker-compose.mirror.yml — Postgres is self-contained. | |
| # S3 creds and API key are sourced from prod .env above. | |
| sudo env \ | |
| IMAGE="ghcr.io/${REPO}" IMAGE_TAG="$IMAGE_TAG" \ | |
| S3_ENDPOINT="${S3_ENDPOINT:-}" S3_REGION="${S3_REGION:-auto}" \ | |
| S3_ACCESS_KEY="${S3_ACCESS_KEY:-}" S3_SECRET_KEY="${S3_SECRET_KEY:-}" \ | |
| S3_BUCKET="${S3_BUCKET:-underlay}" \ | |
| UNDERLAY_UPSTREAM_API_KEY="${UNDERLAY_UPSTREAM_API_KEY:-}" \ | |
| SESSION_SECRET="$(openssl rand -hex 32)" \ | |
| docker stack deploy -c docker-compose.mirror.yml \ | |
| --with-registry-auth --resolve-image always --prune "${STACK_NAME}" | |
| sudo docker stack services "${STACK_NAME}" | |
| echo "Mirror deployed as ${STACK_NAME} (image: ${IMAGE_TAG})" | |
| EOS |