Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update GitLab CI/CD to use the Dev Container CLI #206

Merged
merged 5 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: template

Expand All @@ -34,16 +34,14 @@ jobs:
cruft create --no-input --extra-context '{"package_name": "My Package", "python_version": "3.8", "docker_image":"radixai/python-gpu:$PYTHON_VERSION-cuda11.8", "with_fastapi_api": "1", "with_typer_cli": "1"}' ./template/

- name: Set up Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 21

- name: Install @devcontainers/cli
run: npm install --location=global @devcontainers/cli@0.41.0
run: npm install --location=global @devcontainers/cli@0.55.0

- name: Start Dev Container with Python ${{ matrix.python-version }}
env:
DOCKER_BUILDKIT: 1
run: |
git config --global init.defaultBranch main
git init
Expand All @@ -58,14 +56,8 @@ jobs:
- name: Test package
run: devcontainer exec --workspace-folder my-package poe test

- name: Build ci Docker image
uses: docker/build-push-action@v3
with:
context: ./my-package/
target: ci

- name: Build app Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v5
with:
build-args: |
SOURCE_BRANCH=${{ env.GITHUB_REF }}
Expand Down
6 changes: 5 additions & 1 deletion {{ cookiecutter.__package_name_kebab_case }}/.dockerignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
.git
# Caches
.*_cache/

# Git
.git/
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Log in to the Docker registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: {% raw %}${{ env.DOCKER_REGISTRY }}{% endraw %}
username: {% raw %}${{ github.actor }}{% endraw %}
Expand All @@ -43,7 +43,7 @@ jobs:
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV

- name: Build and push Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v5
with:
context: .
push: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,17 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 21

- name: Install @devcontainers/cli
run: npm install --location=global @devcontainers/cli@0.41.0
run: npm install --location=global @devcontainers/cli@0.55.0

- name: Start Dev Container
env:
DOCKER_BUILDKIT: 1
run: |
git config --global init.defaultBranch main
PYTHON_VERSION={% raw %}${{{% endraw %} matrix.python-version }} devcontainer up --workspace-folder .
Expand Down
191 changes: 98 additions & 93 deletions {{ cookiecutter.__package_name_kebab_case }}/.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,103 +1,81 @@
stages:
- prebuild
- build
- test
- {% if cookiecutter.with_fastapi_api|int or cookiecutter.with_streamlit_app|int %}deploy{% else %}publish{% endif %}

Compute CI image hash:
stage: prebuild
script:
- export DOCKER_IMAGE_SHA="$(sha1sum Dockerfile poetry.lock pyproject.toml | sha1sum | cut -c 1-12)"
- echo "CI_IMAGE_SHA=$DOCKER_IMAGE_SHA" >> .env
artifacts:
reports:
dotenv: .env
variables:
DOCKER_TLS_CERTDIR: "/certs"

.python_matrix:
parallel:
matrix:
- PYTHON_VERSION: ["{{ cookiecutter.python_version }}"]

# Base Docker build script.
.docker:
image: docker:stable
.install_devcontainers_cli:
cache:
paths:
- .apk_cache
- .npm_cache
before_script:
- mkdir -p .apk_cache && apk add --cache-dir .apk_cache npm
sinopeus marked this conversation as resolved.
Show resolved Hide resolved
- npm install --cache .npm_cache --global --prefer-offline @devcontainers/[email protected]

# Build the Dev Container.
lsorber marked this conversation as resolved.
Show resolved Hide resolved
Build:
extends:
- .python_matrix
- .install_devcontainers_cli
stage: build
image: docker:latest
services:
- docker:stable-dind
variables:
DOCKER_REGISTRY: $CI_REGISTRY
DOCKER_REGISTRY_USER: $CI_REGISTRY_USER
DOCKER_REGISTRY_PASSWORD: $CI_REGISTRY_PASSWORD
- docker:dind
script:
- |
echo "$DOCKER_REGISTRY_PASSWORD" | docker login --username "$DOCKER_REGISTRY_USER" --password-stdin "$DOCKER_REGISTRY"
DOCKER_PUSH=${DOCKER_PUSH:-$(timeout 2s docker pull "$DOCKER_IMAGE":"$DOCKER_IMAGE_SHA" >/dev/null 2>&1 && echo $? || echo $?)}
if [ "$DOCKER_PUSH" -ne 1 ]; then
echo "$DOCKER_IMAGE:$DOCKER_IMAGE_SHA exists, skipping this job..."
# Log in to the Docker registry.
echo "$CI_REGISTRY_PASSWORD" | docker login --username "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"

# Compute a hash for the Dev Container image.
export CI_IMAGE_SHA="$(sha1sum Dockerfile poetry.lock pyproject.toml | sha1sum | cut -c 1-8)"
echo "CI_IMAGE_SHA=$CI_IMAGE_SHA" >> .env

# Build and push the Dev Container image, unless it already exists.
IMAGE_NAME="$CI_REGISTRY_IMAGE/devcontainer:$PYTHON_VERSION-$CI_IMAGE_SHA"
IMAGE_EXISTS=${IMAGE_EXISTS:-$(timeout 2s docker pull "$IMAGE_NAME" >/dev/null 2>&1 && echo $? || echo $?)}
if [ "$IMAGE_EXISTS" -ne 1 ]; then
echo "$IMAGE_NAME exists, skipping this job..."
else
# Compile a list of image tags consisting of a hash of its contents, the latest tag if this
# pipeline is running for the default branch, and the Git tag if this commit is tagged.
DOCKER_TAGS="$DOCKER_IMAGE_SHA"
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then DOCKER_TAGS="$DOCKER_TAGS latest"; fi
if [ -n "$CI_COMMIT_TAG" ]; then DOCKER_TAGS="$DOCKER_TAGS $CI_COMMIT_TAG"; fi
if [ -n "$CI_ENVIRONMENT_NAME" ]; then DOCKER_TAGS="$DOCKER_TAGS $CI_ENVIRONMENT_NAME"; fi
DOCKER_TAGS_JOINED=""
for DOCKER_TAG in $DOCKER_TAGS; do
DOCKER_TAGS_JOINED="$DOCKER_TAGS_JOINED --tag $DOCKER_IMAGE:$DOCKER_TAG"
done

# Build the Docker image with all of the selected tags.
{%- if cookiecutter.private_package_repository_name %}
echo "[http-basic.{{ cookiecutter.private_package_repository_name|slugify }}]" >> auth.toml
echo "username = \"gitlab-ci-token\"" >> auth.toml
echo "password = \"$CI_JOB_TOKEN\"" >> auth.toml
export $POETRY_AUTH_TOML_PATH=$(pwd)/auth.toml
{%- endif %}
DOCKER_BUILDKIT=1 docker build \
--cache-from "$CI_REGISTRY_IMAGE/ci:$CI_IMAGE_SHA" \
{%- if cookiecutter.private_package_repository_name %}
--secret id=poetry_auth,src=auth.toml \
{%- endif %}
--target "$DOCKER_TARGET" \
--pull \
$DOCKER_TAGS_JOINED \
.

# Push all the tagged images.
for DOCKER_TAG in $DOCKER_TAGS; do
docker push "$DOCKER_IMAGE:$DOCKER_TAG"
done
devcontainer build --image-name "$IMAGE_NAME" --workspace-folder .
docker push "$IMAGE_NAME"
fi
rules:
- if: $CI_PIPELINE_SOURCE != "merge_request_event"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- Dockerfile
- poetry.lock
- pyproject.toml

# Build CI Docker image.
Build CI image:
extends:
- .docker
stage: build
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE/ci
DOCKER_IMAGE_SHA: $CI_IMAGE_SHA
DOCKER_TARGET: ci
artifacts:
reports:
dotenv: .env

# Lint and test the package.
Test:
extends:
- .python_matrix
- .install_devcontainers_cli
stage: test
image: $CI_REGISTRY_IMAGE/ci:$CI_IMAGE_SHA
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .mypy_cache/
- .pytest_cache/
image: docker:latest
services:
- docker:dind
script:
{%- if cookiecutter.private_package_repository_name %}
- poetry config http-basic.{{ cookiecutter.private_package_repository_name|slugify }} "gitlab-ci-token" "$CI_JOB_TOKEN"
{%- endif %}
- poe lint
- poe test
- |
devcontainer up --cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/devcontainer:$PYTHON_VERSION-$CI_IMAGE_SHA" --workspace-folder .
devcontainer exec --workspace-folder . git config --global --add safe.directory /workspaces/{{ cookiecutter.__package_name_kebab_case }}
devcontainer exec --workspace-folder . poe lint
devcontainer exec --workspace-folder . poe test
coverage: '/^TOTAL.*\s+(\d+(?:\.\d+)?)%/'
artifacts:
reports:
coverage_report:
coverage_report:
coverage_format: cobertura
path: reports/coverage.xml
junit:
Expand All @@ -107,10 +85,10 @@ Test:
when: always

{% if not cookiecutter.with_fastapi_api|int and not cookiecutter.with_streamlit_app|int -%}
# Publish this package version to a (private) package repository.
# Publish this package version to {% if cookiecutter.private_package_repository_name %}a private package repository{% else %}PyPI{% endif %}.
Publish:
stage: publish
image: $CI_REGISTRY_IMAGE/ci:$CI_IMAGE_SHA
image: $CI_REGISTRY_IMAGE/devcontainer:{{ cookiecutter.python_version }}-$CI_IMAGE_SHA
script:
{%- if cookiecutter.private_package_repository_name %}
- poetry config repositories.private "{{ cookiecutter.private_package_repository_url.replace('simple/', '').replace('simple', '') }}"
Expand All @@ -123,21 +101,48 @@ Publish:
only:
- tags
{%- else -%}
# Build the application as a Docker image and push it to the GitLab registry.
.deploy:
extends:
- .docker
# Deploy the application to the Docker registry.
Deploy:
stage: deploy
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE
DOCKER_IMAGE_SHA: ${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}
DOCKER_PUSH: 1
DOCKER_TARGET: app
image: docker:latest
services:
- docker:dind
script:
- |
# Log in to the Docker registry.
echo "$CI_REGISTRY_PASSWORD" | docker login --username "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"

# Compile a list of tags for the image.
DOCKER_TAGS=""
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then DOCKER_TAGS="$DOCKER_TAGS latest"; fi
if [ -n "$CI_COMMIT_TAG" ]; then DOCKER_TAGS="$DOCKER_TAGS $CI_COMMIT_TAG"; fi
if [ -n "$CI_ENVIRONMENT_NAME" ]; then DOCKER_TAGS="$DOCKER_TAGS $CI_ENVIRONMENT_NAME"; fi
DOCKER_TAGS_JOINED=""
for DOCKER_TAG in $DOCKER_TAGS; do
DOCKER_TAGS_JOINED="$DOCKER_TAGS_JOINED --tag $CI_REGISTRY_IMAGE:$DOCKER_TAG"
done

# Build the app image.
{%- if cookiecutter.private_package_repository_name %}
echo "[http-basic.{{ cookiecutter.private_package_repository_name|slugify }}]" >> auth.toml
echo "username = \"gitlab-ci-token\"" >> auth.toml
echo "password = \"$CI_JOB_TOKEN\"" >> auth.toml
{%- endif %}
docker build \
--cache-from "type=registry,ref=$CI_REGISTRY_IMAGE/devcontainer:{{ cookiecutter.python_version }}-$CI_IMAGE_SHA" \
--pull \
{%- if cookiecutter.private_package_repository_name %}
--secret id=poetry-auth,src=auth.toml \
{%- endif %}
--target app \
$DOCKER_TAGS_JOINED \
.

# Push the tags to the Docker registry.
for DOCKER_TAG in $DOCKER_TAGS; do
docker push "$CI_REGISTRY_IMAGE:$DOCKER_TAG"
done
only:
- tags
when: manual
{% for environment in ["feature", "development", "test", "acceptance", "production"] %}
Deploy ({{ environment }}):
extends:
- .deploy
environment: {{ environment }}
{% endfor %}
{%- endif %}
20 changes: 0 additions & 20 deletions {{ cookiecutter.__package_name_kebab_case }}/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,6 @@ RUN --mount=type=cache,uid=$UID,gid=$GID,target=/home/user/.cache/pypoetry/ \



FROM poetry as ci
lsorber marked this conversation as resolved.
Show resolved Hide resolved

# Allow CI to run as root.
USER root

# Install git so we can run pre-commit.
RUN --mount=type=cache,target=/var/cache/apt/ \
--mount=type=cache,target=/var/lib/apt/ \
apt-get update && \
apt-get install --no-install-recommends --yes git

# Install the CI/CD Python dependencies in the virtual environment.
RUN --mount=type=cache,target=/root/.cache/pypoetry/ \
{%- if cookiecutter.private_package_repository_name %}
--mount=type=secret,id=poetry-auth,target=/root/.config/pypoetry/auth.toml \
{%- endif %}
poetry install --only main,test --no-interaction



FROM poetry as dev

# Install development tools: curl, git, gpg, ssh, starship, sudo, vim, and zsh.
Expand Down
1 change: 0 additions & 1 deletion {{ cookiecutter.__package_name_kebab_case }}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ _Python application_: to serve this {% if cookiecutter.with_fastapi_api|int %}RE
1. [Install Docker Desktop](https://www.docker.com/get-started).
- Enable _Use Docker Compose V2_ in Docker Desktop's preferences window.
- _Linux only_:
- [Configure Docker to use the BuildKit build system](https://docs.docker.com/build/buildkit/#getting-started). On macOS and Windows, BuildKit is enabled by default in Docker Desktop.
- Export your user's user id and group id so that [files created in the Dev Container are owned by your user](https://github.com/moby/moby/issues/3206):
```sh
cat << EOF >> ~/.bashrc
Expand Down