Skip to content

Merge pull request #1588 from freedomofpress/develop #868

Merge pull request #1588 from freedomofpress/develop

Merge pull request #1588 from freedomofpress/develop #868

Workflow file for this run

name: CI
on:
pull_request:
types: ['opened', 'synchronize']
push:
branches: ['develop', 'prod']
merge_group:
env:
CONTAINER_ENGINE: docker
VERSION_DOCKER: v29.6.2
COMPOSE_FILE_DEV: docker-compose.yaml
COMPOSE_FILE_PROD: prod-docker-compose.yaml
permissions:
contents: read
actions: read
jobs:
lint-actions:
name: Lint:Actions
uses: freedomofpress/actionslib/.github/workflows/lint-actions.yaml@main
build-dev:
name: Build:Dev
uses: freedomofpress/actionslib/.github/workflows/oci-build.yaml@main
with:
context: '.'
containerfile: ci/containers/Containerfile
target: dev
# All three build jobs share one Containerfile, so hadolint only needs to
# run in one of them. Honours .hadolint.yaml at the repo root.
lint: true
build-args: USERID=1001
registry: localhost/securedroporg-django
build-node:
name: Build:Node
uses: freedomofpress/actionslib/.github/workflows/oci-build.yaml@main
with:
context: '.'
containerfile: ci/containers/Containerfile
target: node-dev
build-args: USERID=1001
registry: localhost/securedroporg-node
build-prod:
name: Build:Prod
uses: freedomofpress/actionslib/.github/workflows/oci-build.yaml@main
with:
context: '.'
containerfile: ci/containers/Containerfile
target: prod
# No USERID override: the Containerfile defaults to 1000, matching both
# publish.yaml and the k8s securityContext, so the image tested here is
# the same artifact that gets deployed.
registry: localhost/securedroporg
requires-deep-history: true
lint-lockfiles:
# Standalone, with no `needs`, so it runs alongside the image builds rather
# than waiting on them: it uses a throwaway pip-tools container, not the dev
# image. The dev image installs the checked-in dev-requirements.txt, so
# nothing else in CI would notice the lockfiles drifting from the .in files.
name: Lint:Lockfiles
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Install just
uses: taiki-e/install-action@6cd13508893c0e7eab5f273c2575d3859bd7229a # v2.86.6
with:
tool: just
- name: Recompile lockfiles and fail on drift
run: just pip-check
test:
# This job name is also the branch-protection check name, so it is decoupled
# from the just recipe: `name` pins the published check where a recipe has
# since been renamed, defaulting to the recipe name otherwise. Renaming a
# pinned check means updating the repo's required checks in the same breath.
name: Test:${{ matrix.name || matrix.target }}
runs-on: ubuntu-latest
needs:
- build-dev
- build-node
strategy:
# A failing linter shouldn't cancel the Django suite's feedback.
fail-fast: false
matrix:
include:
# These run in a one-shot container via `compose run --no-deps`, so
# they need no service stack and the compose-up below is skipped.
- target: check-migrations
stack: false
- target: bandit
stack: false
- target: ruff
stack: false
# The Django suite needs live postgres and the built asset bundles.
- target: test
name: dev-tests
stack: true
steps:
- name: Install Docker
uses: docker/setup-docker-action@v5.4.0
with:
version: ${{ env.VERSION_DOCKER }}
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Download dev image
uses: actions/download-artifact@v8
with:
name: ${{ needs.build-dev.outputs.artifact-name }}
path: ${{ runner.temp }}/images
- name: Download node image
uses: actions/download-artifact@v8
with:
name: ${{ needs.build-node.outputs.artifact-name }}
path: ${{ runner.temp }}/images
- name: Restore dev images
env:
LOAD_DEV_FILE: ${{ runner.temp }}/images/${{ needs.build-dev.outputs.artifact-image }}
IMAGE_DEV_URL: ${{ needs.build-dev.outputs.image-url }}
LOAD_NODE_FILE: ${{ runner.temp }}/images/${{ needs.build-node.outputs.artifact-image }}
IMAGE_NODE_URL: ${{ needs.build-node.outputs.image-url }}
run: |
docker image load --input "$LOAD_DEV_FILE"
docker image load --input "$LOAD_NODE_FILE"
docker image tag "$IMAGE_DEV_URL" localhost/securedroporg-django
docker image tag "$IMAGE_NODE_URL" localhost/securedroporg-node
- name: Install just
uses: taiki-e/install-action@6cd13508893c0e7eab5f273c2575d3859bd7229a # v2.86.6
with:
tool: just
- name: Create env config
run: just dev-init
# --no-build asserts the images restored above are the ones under test:
# compose errors out if a tag is missing rather than silently rebuilding
# from source, which would mask a broken artifact hand-off.
- name: Start
if: ${{ matrix.stack }}
run: docker compose --file="$COMPOSE_FILE_DEV" up --wait --wait-timeout=180 --detach --pull=missing --no-build
- name: Run target '${{ matrix.target }}'
run: just ${{ matrix.target }}
- name: Collect logs on failure
if: ${{ failure() && matrix.stack }}
run: |
docker compose --file="$COMPOSE_FILE_DEV" logs django
docker compose --file="$COMPOSE_FILE_DEV" logs node
- name: Save HTML coverage
uses: actions/upload-artifact@v7
if: ${{ matrix.target == 'test' }}
with:
name: test-${{ matrix.target }}-${{ github.run_id }}-htmlcov
path: htmlcov
if-no-files-found: error
overwrite: true
retention-days: 5
test-prod:
name: Test:production
runs-on: ubuntu-latest
needs:
- build-prod
steps:
- name: Install Docker
uses: docker/setup-docker-action@v5.4.0
with:
version: ${{ env.VERSION_DOCKER }}
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Download prod image
uses: actions/download-artifact@v8
with:
name: ${{ needs.build-prod.outputs.artifact-name }}
path: ${{ runner.temp }}/images
- name: Restore prod image
env:
LOAD_FILE: ${{ runner.temp }}/images/${{ needs.build-prod.outputs.artifact-image }}
IMAGE_URL: ${{ needs.build-prod.outputs.image-url }}
run: |
docker image load --input "$LOAD_FILE"
# This tag needs to match whatever is in the prod compose file
docker image tag "$IMAGE_URL" ghcr.io/freedomofpress/securedrop-org
- name: Start
run: docker compose --file="$COMPOSE_FILE_PROD" up --wait --wait-timeout=180 --detach --pull=missing --no-build
- name: Verify database
run: |
docker compose --file="$COMPOSE_FILE_PROD" exec django \
/bin/bash -c "./manage.py createdevdata"
- name: Collect logs on failure
if: ${{ failure() && matrix.stack }}
run: |
docker compose --file="$COMPOSE_FILE_DEV" logs django
docker compose --file="$COMPOSE_FILE_DEV" logs node
cleanup:
name: Cleanup
runs-on: ubuntu-latest
needs:
- build-dev
- build-node
- build-prod
- test
- test-prod
permissions:
contents: read
actions: write
if: ${{ always() }}
steps:
- name: Delete dev image
if: ${{ needs.build-dev.result == 'success' && always() }}
uses: freedomofpress/actionslib/act/delete-artifact@main
with:
artifact: ${{ needs.build-dev.outputs.artifact-name }}
error-if-absent: 'false'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Delete node image
if: ${{ needs.build-node.result == 'success' && always() }}
uses: freedomofpress/actionslib/act/delete-artifact@main
with:
artifact: ${{ needs.build-node.outputs.artifact-name }}
error-if-absent: 'false'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Delete prod image
if: ${{ needs.build-prod.result == 'success' && always() }}
uses: freedomofpress/actionslib/act/delete-artifact@main
with:
artifact: ${{ needs.build-prod.outputs.artifact-name }}
error-if-absent: 'false'
github-token: ${{ secrets.GITHUB_TOKEN }}