diff options
author | 2022-02-13 11:12:00 -0800 | |
---|---|---|
committer | 2022-02-13 11:12:00 -0800 | |
commit | e526aa82373ed20d30d52a99537cedce37fa0518 (patch) | |
tree | c4d41f9d94a10ffe8fcd280c63a2cf6b81a5fad8 | |
parent | Merge #132 - fix entrypoint and support Python args (diff) | |
parent | CI: remove unnecessary shell option from script (diff) |
Merge #133 - CI refactor
-rw-r--r-- | .coveragerc | 1 | ||||
-rw-r--r-- | .github/workflows/build.yaml | 96 | ||||
-rw-r--r-- | .github/workflows/deploy.yaml | 96 | ||||
-rw-r--r-- | .github/workflows/lint-test-build-push.yaml | 242 | ||||
-rw-r--r-- | .github/workflows/lint.yaml | 73 | ||||
-rw-r--r-- | .github/workflows/main.yaml | 26 | ||||
-rw-r--r-- | .github/workflows/test.yaml | 97 |
7 files changed, 389 insertions, 242 deletions
diff --git a/.coveragerc b/.coveragerc index cc2a148..ce475d7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,6 @@ [run] branch = true +data_file = ${COVERAGE_DATAFILE-.coverage} include = snekbox/* omit = snekbox/api/app.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..28e5b69 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,96 @@ +on: + workflow_call: + outputs: + artifact: + description: The name of the uploaded image aretfact. + value: ${{ jobs.build.outputs.artifact }} + tag: + description: The tag used for the built image. + value: ${{ jobs.build.outputs.tag }} + +jobs: + build: + name: Build snekbox-venv image + runs-on: ubuntu-latest + outputs: + artifact: ${{ env.artifact }} + tag: ${{ steps.sha_tag.outputs.tag }} + env: + artifact: image_artifact_snekbox-venv + + steps: + # Create a short SHA with which 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 + + # The current version (v2) of Docker's build-push action uses buildx, + # which comes with BuildKit. It has cache features which can speed up + # the builds. See https://github.com/docker/build-push-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # The image built for PRs may start to deviate from the "latest" image + # currently in GHCR. Configure the subsequent build step to cache the + # layers in GitHub Actions for PRs. + # See https://github.com/moby/buildkit#github-actions-cache-experimental + # + # Because the cache is scoped to the branch, it will not be available + # on the main branch when the PR is merged. Furthermore, using this cache + # on main is redundant since the previous build's images are already + # cached on GHCR. Thus, this step is only used for PRs. + - name: Configure cache + id: cache_config + run: | + set -eu + if [ "$GITHUB_EVENT_NAME" = 'pull_request' ]; then + cache_from="type=gha,scope=buildkit-${GITHUB_REF}" + cache_to="${cache_from},mode=max" + fi + echo "::set-output name=cache_from::${cache_from:-}" + echo "::set-output name=cache_to::${cache_to:-}" + + # Build the "DEV" version of the image, which targets the `venv` stage + # and includes development dependencies. + # + # Include an inline cache manifest in the image to support caching from + # GHCR. This enables subsequent builds to pull reusable layers from GHCR. + # If configured by the cache_config step, also cache the layers in + # GitHub Actions. + - name: Build image for linting and testing + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + push: false + target: venv + build-args: DEV=1 + outputs: type=docker,dest=${{ env.artifact }}.tar + cache-from: | + ${{ steps.cache_config.outputs.cache_from }} + ghcr.io/python-discord/snekbox-base:latest + ghcr.io/python-discord/snekbox-venv:latest + cache-to: ${{ steps.cache_config.outputs.cache_to }} + tags: ghcr.io/python-discord/snekbox-venv:${{ steps.sha_tag.outputs.tag }} + + # Make the image available as an artifact so other jobs will be able to + # download it. + - name: Upload image archive as an artifact + uses: actions/upload-artifact@v2 + with: + name: ${{ env.artifact }} + path: ${{ env.artifact }}.tar + retention-days: 1 # It's only needed for the duration of the workflow. + if-no-files-found: error diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..c4020b9 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,96 @@ +on: + workflow_call: + inputs: + artifact: + required: true + type: string + tag: + required: true + type: string + +jobs: + deploy: + name: Build, push, & deploy + runs-on: ubuntu-latest + + steps: + - name: Download image artifact + uses: actions/download-artifact@v2 + with: + name: ${{ inputs.artifact }} + + # Load the image to make use of common layers during the final build. + - name: Load image from archive + run: docker load -i ${{ inputs.artifact }}.tar + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # The Dockerfile will be needed. + - name: Checkout code + uses: actions/checkout@v2 + + # Build the final production image and push it to GHCR. + # Tag it with both the short commit SHA and 'latest'. + - name: Build final image + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + push: true + cache-from: | + ghcr.io/python-discord/snekbox-base:latest + ghcr.io/python-discord/snekbox-venv:latest + ghcr.io/python-discord/snekbox:latest + cache-to: type=inline + tags: | + ghcr.io/python-discord/snekbox:latest + ghcr.io/python-discord/snekbox:${{ inputs.tag }} + build-args: git_sha=${{ github.sha }} + + # Deploy to Kubernetes. + - name: Authenticate with Kubernetes + uses: azure/k8s-set-context@v1 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBECONFIG }} + + - name: Deploy to Kubernetes + uses: Azure/k8s-deploy@v1 + with: + manifests: deployment.yaml + images: 'ghcr.io/python-discord/snekbox:${{ inputs.tag }}' + kubectl-version: 'latest' + + # Push the base image to GHCR, with an inline cache manifest. + - name: Push base image + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + target: base + push: true + cache-from: 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. + - name: Push venv image + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + target: venv + push: true + cache-from: | + 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 diff --git a/.github/workflows/lint-test-build-push.yaml b/.github/workflows/lint-test-build-push.yaml deleted file mode 100644 index 0df0b32..0000000 --- a/.github/workflows/lint-test-build-push.yaml +++ /dev/null @@ -1,242 +0,0 @@ -name: Lint, Test, Build, Push - -on: - push: - branches: - - main - pull_request: - - -jobs: - lint-test-build-push: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-20.04, self-hosted] - include: - - os: ubuntu-20.04 - full: true - - os: self-hosted - full: false # Only run tests. - - 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/main' }} - - 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 - - # The current version (v2) of Docker's build-push action uses - # buildx, which comes with BuildKit features that help us speed - # up our builds using additional cache features. Buildx also - # has a lot of other features that are not as relevant to us. - # - # See https://github.com/docker/build-push-action - - 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.GITHUB_TOKEN }} - - # Create a local cache directory for PR builds, as the image - # we build for PRs may start to deviate from the "latest" image - # currently registered in the GHCR. For main, the best we can - # do is use the previous main build, which can be cached from - # the GHCR. - - name: Cache Image Layers - if: github.event_name == 'pull_request' - 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 }}- - - # Build the image we need for linting and testing using the - # `venv` target stage within our Dockerfile. We load the image - # into the runner's Docker image collection so we can run it - # later. - # - # The image includes an inline cache manifest to support caching - # from the GHCR, which means that a build can pull the layers it - # can reuse instead of building them from scratch. - - name: Build image for linting and testing - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - push: false - load: true - target: venv - build-args: DEV=1 - 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: | - export IMAGE_SUFFIX='-venv:${{ steps.sha_tag.outputs.tag }}' - docker-compose up --no-build -d - - # Required by pre-commit. - - name: Install git - if: matrix.full - run: >- - docker exec snekbox_dev /bin/bash -c - 'apt-get -y update && apt-get install -y git=1:2.20.*' - - # pre-commit's venv doesn't work with user installs. - # Skip the flake8 hook because the following step will run it. - - name: Run pre-commit hooks - id: run-pre-commit-hooks - if: matrix.full - run: >- - docker exec snekbox_dev /bin/bash -c - 'PIP_USER=0 SKIP=flake8 pre-commit run --all-files' - - - name: Show pre-commit logs - if: matrix.full && always() && steps.run-pre-commit-hooks.outcome != 'success' - run: >- - docker exec snekbox_dev /bin/bash -c - 'cat /root/.cache/pre-commit/pre-commit.log' - - # 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 - if: matrix.full - run: >- - docker exec snekbox_dev /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 - id: run_tests - run: | - echo '::set-output name=started::true' - docker exec snekbox_dev /bin/bash -c 'coverage run -m unittest' - - - name: Generate coverage report - if: always() && steps.run_tests.outputs.started == 'true' - run: docker exec snekbox_dev /bin/bash -c 'coverage report -m' - - # 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: matrix.os != 'self-hosted' && always() && steps.run_tests.outputs.started == 'true' - id: python - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - # 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 - # in order to use the local build cache generated by buildx while - # building the `venv` image in the lint/test phase. - - # 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 local build cache of the current run. - - name: Build final image - if: matrix.full && env.production_build == 'true' - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - push: true - cache-from: | - ghcr.io/python-discord/snekbox-base:latest - ghcr.io/python-discord/snekbox-venv:latest - ghcr.io/python-discord/snekbox:latest - cache-to: type=inline - tags: | - ghcr.io/python-discord/snekbox:latest - ghcr.io/python-discord/snekbox:${{ steps.sha_tag.outputs.tag }} - build-args: | - git_sha=${{ github.sha }} - - # Deploy to Kubernetes - - name: Authenticate with Kubernetes - if: matrix.full && env.production_build == 'true' - uses: azure/k8s-set-context@v1 - with: - method: kubeconfig - kubeconfig: ${{ secrets.KUBECONFIG }} - - - name: Deploy to Kubernetes - if: matrix.full && env.production_build == 'true' - uses: Azure/k8s-deploy@v1 - with: - manifests: | - deployment.yaml - images: 'ghcr.io/python-discord/snekbox:${{ steps.sha_tag.outputs.tag }}' - kubectl-version: 'latest' - - # Push the base image to GHCR, with an inline cache manifest - - name: Push base image - if: matrix.full && env.production_build == 'true' - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - target: base - push: true - cache-from: | - 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 - - name: Push venv image - if: matrix.full && env.production_build == 'true' - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - target: venv - push: true - cache-from: | - 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 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..8f530ab --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,73 @@ +on: + workflow_call: + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + PIP_NO_CACHE_DIR: false + PIP_USER: 1 # Make dependencies install into PYTHONUSERBASE. + + PIPENV_DONT_USE_PYENV: 1 + PIPENV_HIDE_EMOJIS: 1 + PIPENV_NOSPIN: 1 + + PYTHONUSERBASE: ${{ github.workspace }}/.cache/py-user-base + PRE_COMMIT_HOME: ${{ github.workspace }}/.cache/pre-commit-cache + + steps: + - name: Add custom PYTHONUSERBASE to PATH + run: echo '${{ env.PYTHONUSERBASE }}/bin/' >> $GITHUB_PATH + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + id: python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + + - name: Python dependency cache + uses: actions/cache@v2 + id: python_cache + with: + path: ${{ env.PYTHONUSERBASE }} + key: "python-0-${{ runner.os }}-${{ env.PYTHONUSERBASE }}-\ + ${{ steps.python.outputs.python-version }}-\ + ${{ hashFiles('./Pipfile', './Pipfile.lock') }}" + + # Install dependencies if there was a cache miss. + - name: Install Python dependencies + if: steps.python_cache.outputs.cache-hit != 'true' + run: >- + pip install pipenv==2021.11.23 + && pipenv install --deploy --system --dev + + - name: Pre-commit environment cache + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: "precommit-0-${{ runner.os }}-${{ env.PRE_COMMIT_HOME }}-\ + ${{ steps.python.outputs.python-version }}-\ + ${{ hashFiles('./.pre-commit-config.yaml') }}" + + # pre-commit's venv doesn't work with user installs. + # Skip the flake8 hook because the following step will run it. + - name: Run pre-commit hooks + id: run-pre-commit-hooks + run: PIP_USER=0 SKIP=flake8 pre-commit run --all-files + + # Show the log to debug failures. + - name: Show pre-commit log + if: always() && steps.run-pre-commit-hooks.outcome == 'failure' + run: cat "${PRE_COMMIT_HOME}/pre-commit.log" + + # Output linting errors in the format GitHub Actions recognises for + # annotations. + - name: Run flake8 + run: >- + flake8 --format "::error + file=%(path)s,line=%(row)d,col=%(col)d::[flake8] %(code)s: %(text)s" diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..516a1d6 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,26 @@ +name: main + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + uses: ./.github/workflows/build.yaml + lint: + uses: ./.github/workflows/lint.yaml + test: + uses: ./.github/workflows/test.yaml + needs: build + with: + artifact: ${{ needs.build.outputs.artifact }} + tag: ${{ needs.build.outputs.tag }} + deploy: + uses: ./.github/workflows/deploy.yaml + if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} + needs: [build, lint, test] + with: + artifact: ${{ needs.build.outputs.artifact }} + tag: ${{ needs.build.outputs.tag }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..77ef1fc --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,97 @@ +on: + workflow_call: + inputs: + artifact: + required: true + type: string + tag: + required: true + type: string + +jobs: + test: + name: Test with coverage + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-20.04, self-hosted] + + steps: + - name: Download image artifact + uses: actions/download-artifact@v2 + with: + name: ${{ inputs.artifact }} + + - name: Load image from archive + run: docker load -i ${{ inputs.artifact }}.tar + + # Needed for the Docker Compose file. + - name: Checkout code + uses: actions/checkout@v2 + + # Memory limit tests would fail if this isn't disabled. + - name: Disable swap memory + run: sudo swapoff -a + + # Run tests with coverage within the container. + # Suffix the generated coverage datafile with the name of the runner's OS. + - name: Run tests + id: run_tests + run: | + export IMAGE_SUFFIX='-venv:${{ inputs.tag }}' + docker-compose run \ + --rm -T -e COVERAGE_DATAFILE=.coverage.${{ matrix.os }} \ + snekbox \ + coverage run -m unittest + + # Upload it so the coverage from all matrix jobs can be combined later. + - name: Upload coverage data + uses: actions/upload-artifact@v2 + with: + name: coverage + path: .coverage.* + retention-days: 1 + + # Self-hosted runner needs containers, images, networks, volumes, etc. + # removed because they persist across runs and may interfere. + - name: Docker cleanup + if: matrix.os == 'self-hosted' && always() + run: | + export IMAGE_SUFFIX='-venv:${{ inputs.tag }}' + docker-compose down --rmi all --remove-orphans -v -t 0 + + report: + name: Report coverage + runs-on: ubuntu-20.04 + needs: test + + steps: + # Needed for the .coveragerc file. + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + + - name: Install dependencies + run: pip install coverage~=6.0 coveralls~=3.3 + + - name: Download coverage data + uses: actions/download-artifact@v2 + with: + name: coverage + + - name: Combine coverage data + run: coverage combine .coverage.* + + - name: Display coverage report + run: coverage report -m + + # Comment on the PR with the coverage results and register a GitHub check + # which links to the coveralls.io job. + - name: Publish coverage report to coveralls.io + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: coveralls --service=github |