Merge pull request #1588 from freedomofpress/develop #868
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: 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 }} |