From 15b699ce6decd4c55ec73676c9ebd2d75ed2efa3 Mon Sep 17 00:00:00 2001 From: Nulled Agent Date: Thu, 23 Jul 2026 21:57:04 +0000 Subject: [PATCH] ci: publish latest image for Watchtower (NUL-252) --- .github/workflows/deploy.yml | 147 ++++------------------------------- 1 file changed, 15 insertions(+), 132 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 48bcf0e..ded0366 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,65 +1,28 @@ name: Deploy IPAM -# Push-to-main CI/CD: build the image, push to ghcr.io/thenulldev/ipam, and -# trigger /srv/ipam/deploy.sh on this host over SSH. -# -# See docs/operations/deploy-workflow.md for the full ops write-up and the -# list of repo secrets the workflow reads. -# -# workflow_dispatch exists for two purposes: -# 1. Manual deploy (e.g. after a hotfix that bypassed CI). -# 2. Rollback — `inputs.pinned_tag` lets an operator rerun ONLY the deploy -# step against a known-good SHA without rebuilding the image. +# Build and publish the rolling image consumed by Watchtower on the host. +# Watchtower polls ghcr.io/thenulldev/ipam:latest and restarts the container +# when that tag resolves to a new digest. The SHA tag remains for forensics and +# operator-selected rollback; GitHub Actions does not connect to the host. on: push: branches: [main] workflow_dispatch: - inputs: - pinned_tag: - description: > - Override the tag passed to /srv/ipam/deploy.sh. Leave empty on a push - trigger (the SHA is auto-derived). Set to a previous SHA for rollback. - The image must already exist at ghcr.io/thenulldev/ipam:. - required: false - type: string - deploy_only: - description: > - If 'true', skip the build + push job and run only the deploy step - against the supplied pinned_tag (or github.sha if pinned_tag is - empty). Use this for rollback when the image is already in ghcr.io. - required: false - type: choice - default: 'false' - options: - - 'false' - - 'true' -# Never kill an in-flight deploy. Two pushes to main in quick succession -# MUST serialize — otherwise the second push could `docker compose up -d` -# while the first is still pulling, which would race on the named volume -# and produce a half-up container. concurrency: - group: ipam-deploy-${{ github.ref }} + group: ipam-image-publish-${{ github.ref }} cancel-in-progress: false env: - # The image registry. Override via the IPAM_DOCKER_REGISTRY var on the - # repo to retarget (e.g. a staging registry) without editing this file. - IMAGE_BASE: ${{ vars.IPAM_DOCKER_REGISTRY || 'ghcr.io' }}/${{ github.repository }} + IMAGE_NAME: ghcr.io/thenulldev/ipam jobs: build: - name: Build & push image - # Skip on a workflow_dispatch that explicitly opted into "deploy only". - if: ${{ github.event_name != 'workflow_dispatch' || inputs.deploy_only != 'true' }} + name: Build and push image runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read - # GITHUB_TOKEN-based ghcr.io auth: minimal secret surface. Falls back - # to IPAM_DOCKER_USERNAME/IPAM_DOCKER_PASSWORD (see "Alternate auth" - # note at the bottom of this file) if those secrets are set AND the - # founder prefers parity with the existing release.yml pipeline. packages: write steps: @@ -70,100 +33,20 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Log in to ghcr.io - # Default path: GitHub's automatic GITHUB_TOKEN with packages:write. - # `docker/login-action` accepts it directly — no PAT needed for - # push-to-same-org. If the founder prefers parity with release.yml, - # set secrets.IPAM_DOCKER_USERNAME + secrets.IPAM_DOCKER_PASSWORD - # and the conditional below picks them up automatically. uses: docker/login-action@v3 with: - registry: ${{ vars.IPAM_DOCKER_REGISTRY || 'ghcr.io' }} - username: ${{ secrets.IPAM_DOCKER_USERNAME || github.actor }} - password: ${{ secrets.IPAM_DOCKER_PASSWORD || secrets.GITHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.IMAGE_BASE }} - tags: | - type=sha,format=long - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push - uses: docker/build-push-action@v6 + - name: Build and push SHA and rolling tags + uses: docker/build-push-action@v5 with: context: . file: Dockerfile push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - # Cache by GH Actions cache backend (free, scoped to the repo). + tags: | + ${{ env.IMAGE_NAME }}:${{ github.sha }} + ${{ env.IMAGE_NAME }}:latest cache-from: type=gha cache-to: type=gha,mode=max - - deploy: - # Plain name — `secrets` is not allowed in job.name (only in if:/steps). - name: SSH deploy - needs: build - # The deploy step is safe to re-run on workflow_dispatch WITHOUT rebuild. - # `always()` so we still run on a skipped build (deploy_only=true path); - # the explicit result check guards against an actual build failure. - if: ${{ always() && (needs.build.result == 'success' || needs.build.result == 'skipped') }} - runs-on: ubuntu-latest - timeout-minutes: 10 - # The deploy job does not need any GitHub token scopes — it only uses - # an out-of-band SSH key (the deploy key scoped to paperclip@this-host). - permissions: - contents: read - - steps: - - name: Determine deploy target tag - id: target - # Priority: explicit workflow_dispatch input > auto-derived SHA. - # For a normal push trigger, github.sha IS the SHA tag we just pushed. - run: | - if [ -n "${{ inputs.pinned_tag }}" ]; then - echo "tag=${{ inputs.pinned_tag }}" >> "$GITHUB_OUTPUT" - echo "Deploying pinned tag: ${{ inputs.pinned_tag }}" - else - echo "tag=${{ github.sha }}" >> "$GITHUB_OUTPUT" - echo "Deploying SHA from trigger: ${{ github.sha }}" - fi - - - name: SSH deploy - # appleboy/ssh-action@v1 — pinned to major v1 for stability. - # We deliberately run ONE command (the authorized_keys-restricted - # `bash /srv/ipam/deploy.sh `) so the SSH key can be locked - # down on the host via `command="..."` in authorized_keys. NUL-225 - # (Relay) owns that authorized_keys setup. - uses: appleboy/ssh-action@v1 - with: - host: ${{ secrets.IPAM_DEPLOY_HOST }} - username: ${{ secrets.IPAM_DEPLOY_USER }} - key: ${{ secrets.IPAM_DEPLOY_SSH_KEY }} - command_timeout: 8m - script: | - set -euo pipefail - echo "::group::ipam deploy ${{ steps.target.outputs.tag }}" - bash /srv/ipam/deploy.sh ${{ steps.target.outputs.tag }} - echo "::endgroup::" - -# --------------------------------------------------------------------------- -# Alternate auth note (kept as a comment for the reviewer): -# -# release.yml uses IPAM_DOCKER_USERNAME/IPAM_DOCKER_PASSWORD explicitly and -# gates on their presence. To match that exactly, replace the "Log in to -# ghcr.io" step above with: -# -# - name: Log in to ghcr.io -# uses: docker/login-action@v3 -# with: -# registry: ${{ vars.IPAM_DOCKER_REGISTRY || 'ghcr.io' }} -# username: ${{ secrets.IPAM_DOCKER_USERNAME }} -# password: ${{ secrets.IPAM_DOCKER_PASSWORD }} -# -# and add `if: ${{ secrets.IPAM_DOCKER_USERNAME != '' }}` to gate the build -# job. Trade-off: two more repo secrets, but the workflow is identical in -# shape to release.yml. Founder picks — see docs/operations/deploy-workflow.md. -# ---------------------------------------------------------------------------