name: Lint, Test, Build, Push on: push: branches: - sebastiaan/backend/cache-docker-images jobs: lint-test: runs-on: ubuntu-latest env: # Determine whether or not we should build the # final production image and push it to GHCR. production_build: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/sebastiaan/backend/cache-docker-images' }} steps: # Create a short SHA-tag to tag built images - name: Create SHA Container Tag id: sha_tag run: | tag=$(cut -c 1-7 <<< $GITHUB_SHA) echo "::set-output name=tag::$tag" - name: Checkout code uses: actions/checkout@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to Github Container Registry uses: docker/login-action@v1 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GHCR_TOKEN }} # Set up a caching directory for image layers. According to the docker # documentation, it's recommended to use a SHA-based key to get the # greatest change of finding the most relevant cached layer. We fall # down to more generic containers by then matching by GitHub branch, # to use cache generated earlier in the same branch, and finally to # the latest cache in general. The `v0` is purely a cache version # indicator that can be incremented manually if we want to invalidate # old caches completely. - name: Cache Image Layers uses: actions/cache@v2 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-v0-buildx-${{ github.ref }}-${{ github.sha }} restore-keys: | ${{ runner.os }}-v0-buildx-${{ github.ref }}- ${{ runner.os }}-v0-buildx- # Build the image we need for testing/linting the current codebase, # without pushing the image to the GHCR. Instead, we load it into # the runner's docker environment so we can run it later. The # target of this build is the `venv` stage of the Dockerfile, as we # don't want to include the final production entry point stage. # # This build caches to our GitHub Actions cache and uses that cache # during the build process as well. If no GitHub Actions cache was # available, it will use the latest intermediate images pushed to # the GHCR as a cache source. - name: Build image for linting and testing uses: docker/build-push-action@v2 with: context: . file: ./docker/Dockerfile push: false load: true target: venv cache-from: | type=local,src=/tmp/.buildx-cache ghcr.io/python-discord/snekbox-base:latest ghcr.io/python-discord/snekbox-venv:latest cache-to: type=local,dest=/tmp/.buildx-cache,mode=max tags: ghcr.io/python-discord/snekbox-venv:${{ steps.sha_tag.outputs.tag }} - name: Start Container run: "docker run \ --tty \ --detach \ --name snekbox_test \ --privileged \ --hostname pdsnk-dev \ -e PYTHONDONTWRITEBYTECODE=1 \ -e PIPENV_PIPFILE='/snekbox/Pipfile\' \ -e ENV=\"${PWD}/scripts/.profile\" \ --volume \"${PWD}\":\"${PWD}\" \ --workdir \"${PWD}\" \ --entrypoint /bin/bash \ ghcr.io/python-discord/snekbox-venv:${{ steps.sha_tag.outputs.tag }}" - name: Install dependencies run: "docker exec snekbox_test /bin/bash -c \ 'pipenv install --system --deploy --dev'" # This runs `flake8` in the container and asks `flake8` to output # linting errors in the format of the command for registering workflow # error messages/annotations. This means that Github Actions will pick # up on this output to generate nice annotations to indicate what went # wrong where. - name: Run linter run: "docker exec snekbox_test /bin/bash -c 'flake8 \ --format \"::error file=%(path)s,line=%(row)d,col=%(col)d::\ [flake8] %(code)s: %(text)s\"'" # Memory limit tests would fail if this isn't disabled. - name: Disable swap memory run: sudo swapoff -a # Run unittests and generate coverage report in the container - name: Run unit tests and generate coverage report id: run_tests run: | echo '::set-output name=started::true' cmd='coverage run -m unittest; coverage report -m' docker exec snekbox_test /bin/bash -c "${cmd}" # Set-up a Python version to process the coverage reports # Note: This step runs even if the test step failed to make # sure we process the coverage reports. - name: Setup python if: always() && steps.run_tests.outputs.started == 'true' id: python uses: actions/setup-python@v2 with: python-version: '3.9' # We'll only ever need a single dependency in this python # environment and we'll only use it in the CI, so let's # install it directly here and run it. # # This step will publish the coverage results to coveralls.io # print a job link in the output. It will also register a # step in the check suite visible in the PR with a link to # the job. - name: Publish coverage report to coveralls.io if: always() && steps.run_tests.outputs.started == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | pip install coveralls~=2.1 coveralls # Final build stage. This is run in the same job with conditions # to prevent us from having to reload the caching directory. We # already built a huge chunk of the image before this point in # the run, so it does not make sense to drop down to a completely # fresh build environment in a new worker/runner. # Build the final production image and push it to GHCR, tagging it # both with the short commit SHA and 'latest'. This step should use # the cache that was just generated when we built the test container. - name: Build final image if: env.production_build == 'true' uses: docker/build-push-action@v2 with: context: . file: ./docker/Dockerfile push: true cache-from: | type=local,src=/tmp/.buildx-cache ghcr.io/python-discord/snekbox-base:latest ghcr.io/python-discord/snekbox-venv:latest ghcr.io/python-discord/snekbox:latest cache-to: type=local,dest=/tmp/.buildx-cache tags: | ghcr.io/python-discord/snekbox:latest ghcr.io/python-discord/snekbox:${{ steps.sha_tag.outputs.tag }} # Push the base image to GHCR, *with* an inline cache manifest to # ensure we can use this image as a cache source if our GitHub Actions # "local" cache failed to be restored. GHCR does not support pushing a # separate cache manifest, meaning we have to use an "inline" manifest. - name: Push base image if: env.production_build == 'true' uses: docker/build-push-action@v2 with: context: . file: ./docker/Dockerfile target: base push: true cache-from: | type=local,src=/tmp/.buildx-cache ghcr.io/python-discord/snekbox-base:latest cache-to: type=inline tags: ghcr.io/python-discord/snekbox-base:latest # Push the venv image to GHCR *with* an inline cache manifest. See # the comment attached to the previous step for more information. - name: Push venv image if: env.production_build == 'true' uses: docker/build-push-action@v2 with: context: . file: ./docker/Dockerfile target: venv push: true cache-from: | type=local,src=/tmp/.buildx-cache ghcr.io/python-discord/snekbox-base:latest ghcr.io/python-discord/snekbox-venv:latest cache-to: type=inline tags: ghcr.io/python-discord/snekbox-venv:latest