diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 421728bf..24feca48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,18 +77,25 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - docker: - name: Docker image - runs-on: ubuntu-latest + # Each architecture builds on a native runner: emulating arm64 under QEMU (compiling pycairo, npm, uv) was the slow part + docker-build: + name: Docker build (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} needs: [lint, test] permissions: contents: read packages: write + strategy: + fail-fast: true + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 # Only authenticate when we are actually going to push (never for PRs, which may come from forks) @@ -100,6 +107,60 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - id: build + name: Build (and push by digest on master) + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + build-args: GIT_SHA=${{ github.sha }} + outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=${{ github.event_name == 'push' }} + cache-from: type=gha,scope=${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=${{ matrix.platform }} + + - name: Export digest + if: github.event_name == 'push' + env: + DIGEST: ${{ steps.build.outputs.digest }} + run: | + mkdir -p "$RUNNER_TEMP/digests" + touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}" + + - name: Upload digest + if: github.event_name == 'push' + uses: actions/upload-artifact@v4 + with: + name: digests-${{ strategy.job-index }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + docker: + name: Docker image + runs-on: ubuntu-latest + needs: [docker-build] + if: github.event_name == 'push' + outputs: + tag: ${{ steps.sha-tag.outputs.tag }} + permissions: + contents: read + packages: write + steps: + - uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta uses: docker/metadata-action@v5 with: @@ -108,13 +169,40 @@ jobs: type=raw,value=latest type=sha - - name: Build (and push on master) - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name == 'push' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + - id: sha-tag + name: Pick the commit tag + env: + TAGS: ${{ steps.meta.outputs.tags }} + run: | + ref="$(grep -m1 ':sha-' <<< "$TAGS")" + test -n "$ref" + echo "tag=${ref##*:}" >> "$GITHUB_OUTPUT" + + - name: Create multi-arch manifest + working-directory: ${{ runner.temp }}/digests + env: + IMAGE: ghcr.io/${{ github.repository }} + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf "$IMAGE@sha256:%s " *) + + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: [docker] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + environment: production + concurrency: deploy-production + steps: + - name: Deploy the image built for this commit + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} + TAG: ${{ needs.docker.outputs.tag }} + run: | + install -m 700 -d ~/.ssh + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts + ssh deploy@rigs.nottinghamtec.co.uk "$TAG" diff --git a/Dockerfile b/Dockerfile index afcae5cb..c6a824c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,6 +97,10 @@ COPY --from=assets --chown=app:app /app/pipeline/built_assets /app/pipeline/buil # DEBUG must never be on in a built image; enable it explicitly via the environment if needed ENV DEBUG=false +# Declared last so a new commit SHA does not invalidate any of the layers above +ARG GIT_SHA=unknown +ENV GIT_SHA=$GIT_SHA + USER app EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "PyRIGS.wsgi"] diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py index 03bb4fbf..0f414f39 100644 --- a/PyRIGS/settings.py +++ b/PyRIGS/settings.py @@ -24,6 +24,8 @@ SECRET_KEY = env("SECRET_KEY", default="gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0 DEBUG = env("DEBUG", cast=bool, default=True) STAGING = env("STAGING", cast=bool, default=False) CI = env("CI", cast=bool, default=False) +# Commit the running image was built from (baked in at build time) +GIT_SHA = env("GIT_SHA", default="unknown") ALLOWED_HOSTS = [ host.strip() for host in env("DJANGO_ALLOWED_HOSTS", default="rigs.nottinghamtec.co.uk").split(",") if host.strip() @@ -253,6 +255,7 @@ TEMPLATES = [ "django.template.context_processors.request", "django.contrib.messages.context_processors.messages", "PyRIGS.views.ajax_context", + "PyRIGS.views.version_context", ], "debug": DEBUG, }, diff --git a/PyRIGS/views.py b/PyRIGS/views.py index f0cce9c0..8ac07196 100644 --- a/PyRIGS/views.py +++ b/PyRIGS/views.py @@ -38,6 +38,12 @@ def ajax_context(request): return {"is_ajax": is_ajax(request)} +def version_context(request): + """Template context processor exposing the running commit to every template.""" + sha = settings.GIT_SHA + return {"git_sha": sha, "git_sha_short": sha[:7]} + + def get_related( form, context ): # Get some other objects to include in the form. Used when there are errors but also nice and quick. diff --git a/templates/base.html b/templates/base.html index 9773f8fc..4298a842 100644 --- a/templates/base.html +++ b/templates/base.html @@ -82,6 +82,16 @@ {% block content %}{% endblock %} +{% if not is_ajax %} + +{% endif %} +