name: Forgejo Docker Build on: push: branches: [main] workflow_dispatch: inputs: push_image: description: Push image to Forgejo container registry required: false default: 'true' deploy: description: Deploy the built image to the host after pushing required: false default: 'false' jobs: root-test: name: Root app tests runs-on: forgejo-local steps: - uses: actions/checkout@v4 - uses: https://github.com/actions/setup-node@v4 with: node-version: '24' cache: npm cache-dependency-path: package-lock.json - run: npm ci - run: npm test build: needs: root-test name: Build Docker image runs-on: forgejo-local steps: - name: Checkout uses: actions/checkout@v4 - name: Prepare compose env files run: | touch .env - name: Validate Compose config run: docker compose -f docker-compose.yml config >/tmp/ped-ai-compose.yml # The same script a person runs locally, so a CI image and a hand-built # one cannot drift. It validates the revision and bakes it into the image, # which is what makes /api/build able to say what is running. - name: Build compose service run: ./scripts/build-image.sh - name: Tag image by revision run: | IMAGE="git.danvics.com/danvics/pediatric-ai-scribe-v3" REVISION=$(git rev-parse HEAD) # The full revision is the immutable name; :latest is only a pointer. docker tag ped-ai-local:latest "$IMAGE:$REVISION" docker tag ped-ai-local:latest "$IMAGE:latest" - name: Push image to Forgejo registry if: ${{ github.event.inputs.push_image != 'false' }} env: FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} run: | IMAGE="git.danvics.com/danvics/pediatric-ai-scribe-v3" REVISION=$(git rev-parse HEAD) echo "$FORGEJO_TOKEN" | docker login git.danvics.com -u danvics --password-stdin docker push "$IMAGE:$REVISION" docker push "$IMAGE:latest" # ── Deploy ────────────────────────────────────────────────────────────── # Opt-in, because the deploy directory is also a working tree: this refuses to # run if it has uncommitted changes rather than resetting over someone's work. # # It moves that checkout to the built revision first, so the Compose file and # the entrypoint that ship with the image are the ones used to run it, then # hands over to scripts/deploy.sh, which pins the image, waits for health, # asks /api/build what is actually running and rolls back if it disagrees. # Schema migrations are applied by the container's own entrypoint. deploy: needs: build name: Deploy to the host runs-on: forgejo-local if: ${{ github.event.inputs.deploy == 'true' }} env: DEPLOY_DIR: ${{ vars.DEPLOY_DIR || '/home/danvics/docker/ped-ai' }} steps: - name: Refuse to deploy over uncommitted work run: | if [ -n "$(git -C "$DEPLOY_DIR" status --porcelain)" ]; then echo "$DEPLOY_DIR has uncommitted changes; commit or stash them first." >&2 git -C "$DEPLOY_DIR" status --short >&2 exit 1 fi # Detaches HEAD at the deployed revision, which is what you want a # deployed tree to be. If DEPLOY_DIR is also where you write code, point # it at a checkout of its own instead — vars.DEPLOY_DIR. - name: Move the deploy checkout to this revision run: | REVISION="${{ github.sha }}" git -C "$DEPLOY_DIR" fetch --quiet --all git -C "$DEPLOY_DIR" checkout --quiet --detach "$REVISION" - name: Deploy and verify run: | IMAGE="git.danvics.com/danvics/pediatric-ai-scribe-v3" REVISION="${{ github.sha }}" "$DEPLOY_DIR/scripts/deploy.sh" "$IMAGE:$REVISION" "$REVISION"