diff --git a/.devcontainer/prebuild/.devcontainer/Dockerfile b/.devcontainer/prebuild/.devcontainer/Dockerfile new file mode 100644 index 00000000..78975159 --- /dev/null +++ b/.devcontainer/prebuild/.devcontainer/Dockerfile @@ -0,0 +1,54 @@ +ARG PYTHON_VERSION=3.12 + +######################################################################################## +# Dev image is used for development and cicd. +######################################################################################## + +FROM python:${PYTHON_VERSION} as dev + +RUN apt-get update && apt-get install -y --no-install-recommends \ +# To install Python applications. + pipx \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# Config pipx +ENV PIPX_HOME=/usr/local/pipx +ENV PIPX_BIN_DIR=/usr/local/bin + +# Install pdm +RUN pipx install pdm + +######################################################################################## +# Build image is an intermediate image used for building the project. +######################################################################################## + +FROM dev as build + +# Copy necessary files for build. +COPY pyproject.toml pdm.lock README.md /workspace/ +COPY src/ /workspace/src +COPY .git/ /workspace/.git + +# Install dependencies and project into the local packages directory. +WORKDIR /workspace +RUN mkdir __pypackages__ && pdm sync --prod --no-editable + +######################################################################################## +# Prod image is used for deployment and distribution. +######################################################################################## + +FROM python:${PYTHON_VERSION}-slim as prod + +# NOTE: python docker image has env `PYTHON_VERSION` but with patch version. +# ARG is used here for temporary override without changing the original env. +ARG PYTHON_VERSION=3.12 + +# Retrieve packages from build stage. +ENV PYTHONPATH=/workspace/pkgs +COPY --from=build /workspace/__pypackages__/${PYTHON_VERSION}/lib /workspace/pkgs + +# Retrieve executables from build stage. +COPY --from=build /workspace/__pypackages__/${PYTHON_VERSION}/bin/* /usr/local/bin/ + +# Set command to run the cli by default. +CMD ["ss-python-cli"] diff --git a/.devcontainer/prebuild/.devcontainer/devcontainer.json b/.devcontainer/prebuild/.devcontainer/devcontainer.json new file mode 100644 index 00000000..3c600e7b --- /dev/null +++ b/.devcontainer/prebuild/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "build": { + "args": { + "PYTHON_VERSION": "${localEnv:PYTHON_VERSION}" + }, + "cacheFrom": "ghcr.io/serious-scaffold/ss-python:dev-py${localEnv:PYTHON_VERSION}", + "context": "../../..", + "dockerfile": "Dockerfile", + "target": "dev" + } +} diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml new file mode 100644 index 00000000..1458c8da --- /dev/null +++ b/.github/workflows/devcontainer.yml @@ -0,0 +1,41 @@ +name: DevContainer Prebuild +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + devcontainer_prebuild: + permissions: + contents: read + packages: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: docker/login-action@v3 + with: + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.actor }} + - env: + PYTHON_VERSION: ${{ matrix.python-version }} + uses: devcontainers/ci@v0.3 + with: + imageName: ghcr.io/${{ github.repository }} + imageTag: dev-py${{ matrix.python-version }} + push: always + subFolder: .devcontainer/prebuild + strategy: + matrix: + python-version: + - '3.8' + - '3.9' + - '3.10' + - '3.11' + - '3.12' +on: + push: + branches: + - main + paths: + - .devcontainer/prebuild/** diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8813d434..fca30561 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ stages: + - build - ci - release default: - image: python:3.12 + image: ${CI_REGISTRY_IMAGE}:dev-py3.12 include: .gitlab/ci/**.yml diff --git a/.gitlab/ci/ci.yml b/.gitlab/ci/ci.yml index 9db4227d..b41ff8ad 100644 --- a/.gitlab/ci/ci.yml +++ b/.gitlab/ci/ci.yml @@ -5,7 +5,7 @@ ci: coverage_format: cobertura path: coverage.xml coverage: /(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/ - image: python:$PYTHON_VERSION + image: ${CI_REGISTRY_IMAGE}:dev-py${PYTHON_VERSION} interruptible: true only: - main diff --git a/.gitlab/ci/devcontainer.yml b/.gitlab/ci/devcontainer.yml new file mode 100644 index 00000000..fd4e1e43 --- /dev/null +++ b/.gitlab/ci/devcontainer.yml @@ -0,0 +1,28 @@ +devcontainer-prebuild: + image: docker:latest + interruptible: true + only: + - main + parallel: + matrix: + - PYTHON_VERSION: + - '3.8' + - '3.9' + - '3.10' + - '3.11' + - '3.12' + script: + - apk add --update nodejs npm python3 make g++ + - npm install -g @devcontainers/cli + - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - | + devcontainer build \ + --image-name ${CI_REGISTRY_IMAGE}:dev-py${PYTHON_VERSION} \ + --push true \ + --workspace-folder .devcontainer/prebuild/ + services: + - docker:dind + stage: build + variables: + DOCKER_TLS_CERTDIR: /certs + PYTHON_VERSION: ${PYTHON_VERSION} diff --git a/.vscode/settings.json b/.vscode/settings.json index edbfc016..52554fc4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,6 +22,8 @@ "cobertura", "deepclean", "deflist", + "devcontainer", + "devcontainers", "elif", "endmacro", "epub", @@ -36,6 +38,7 @@ "pathjoin", "pipenv", "pipx", + "prebuild", "pycache", "pydantic", "pyproject", diff --git a/template/.devcontainer/prebuild/.devcontainer/Dockerfile.jinja b/template/.devcontainer/prebuild/.devcontainer/Dockerfile.jinja new file mode 100644 index 00000000..bf76d4ef --- /dev/null +++ b/template/.devcontainer/prebuild/.devcontainer/Dockerfile.jinja @@ -0,0 +1,54 @@ +ARG PYTHON_VERSION={{ default_py }} + +######################################################################################## +# Dev image is used for development and cicd. +######################################################################################## + +FROM python:${PYTHON_VERSION} as dev + +RUN apt-get update && apt-get install -y --no-install-recommends \ +# To install Python applications. + pipx \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# Config pipx +ENV PIPX_HOME=/usr/local/pipx +ENV PIPX_BIN_DIR=/usr/local/bin + +# Install pdm +RUN pipx install pdm + +######################################################################################## +# Build image is an intermediate image used for building the project. +######################################################################################## + +FROM dev as build + +# Copy necessary files for build. +COPY pyproject.toml pdm.lock README.md /workspace/ +COPY src/ /workspace/src +COPY .git/ /workspace/.git + +# Install dependencies and project into the local packages directory. +WORKDIR /workspace +RUN mkdir __pypackages__ && pdm sync --prod --no-editable + +######################################################################################## +# Prod image is used for deployment and distribution. +######################################################################################## + +FROM python:${PYTHON_VERSION}-slim as prod + +# NOTE: python docker image has env `PYTHON_VERSION` but with patch version. +# ARG is used here for temporary override without changing the original env. +ARG PYTHON_VERSION={{ default_py }} + +# Retrieve packages from build stage. +ENV PYTHONPATH=/workspace/pkgs +COPY --from=build /workspace/__pypackages__/${PYTHON_VERSION}/lib /workspace/pkgs + +# Retrieve executables from build stage. +COPY --from=build /workspace/__pypackages__/${PYTHON_VERSION}/bin/* /usr/local/bin/ + +# Set command to run the cli by default. +CMD ["ss-python-cli"] diff --git a/template/.devcontainer/prebuild/.devcontainer/devcontainer.json b/template/.devcontainer/prebuild/.devcontainer/devcontainer.json new file mode 100644 index 00000000..3c600e7b --- /dev/null +++ b/template/.devcontainer/prebuild/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "build": { + "args": { + "PYTHON_VERSION": "${localEnv:PYTHON_VERSION}" + }, + "cacheFrom": "ghcr.io/serious-scaffold/ss-python:dev-py${localEnv:PYTHON_VERSION}", + "context": "../../..", + "dockerfile": "Dockerfile", + "target": "dev" + } +} diff --git a/template/.vscode/settings.json b/template/.vscode/settings.json index edbfc016..52554fc4 100644 --- a/template/.vscode/settings.json +++ b/template/.vscode/settings.json @@ -22,6 +22,8 @@ "cobertura", "deepclean", "deflist", + "devcontainer", + "devcontainers", "elif", "endmacro", "epub", @@ -36,6 +38,7 @@ "pathjoin", "pipenv", "pipx", + "prebuild", "pycache", "pydantic", "pyproject", diff --git a/template/[% if repo_host_type == 'github.com' %].github[% endif %]/workflows/devcontainer.yml.jinja b/template/[% if repo_host_type == 'github.com' %].github[% endif %]/workflows/devcontainer.yml.jinja new file mode 100644 index 00000000..67ac12ba --- /dev/null +++ b/template/[% if repo_host_type == 'github.com' %].github[% endif %]/workflows/devcontainer.yml.jinja @@ -0,0 +1,52 @@ +[% from pathjoin("includes", "version_compare.jinja") import version_between -%] +name: DevContainer Prebuild +concurrency: + cancel-in-progress: true + group: {{ '${{ github.workflow }}-${{ github.ref }}' }} +jobs: + devcontainer_prebuild: + permissions: + contents: read + packages: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: docker/login-action@v3 + with: + password: {{ '${{ secrets.GITHUB_TOKEN }}' }} + registry: ghcr.io + username: {{ '${{ github.actor }}' }} + - env: + PYTHON_VERSION: {{ '${{ matrix.python-version }}' }} + uses: devcontainers/ci@v0.3 + with: + imageName: ghcr.io/{{ '${{ github.repository }}' }} + imageTag: dev-py{{ '${{ matrix.python-version }}' }} + push: always + subFolder: .devcontainer/prebuild + strategy: + matrix: + python-version: +[%- if version_between("3.8", min_py, max_py) %] + - '3.8' +[%- endif %] +[%- if version_between("3.9", min_py, max_py) %] + - '3.9' +[%- endif %] +[%- if version_between("3.10", min_py, max_py) %] + - '3.10' +[%- endif %] +[%- if version_between("3.11", min_py, max_py) %] + - '3.11' +[%- endif %] +[%- if version_between("3.12", min_py, max_py) %] + - '3.12' +[%- endif %] +on: + push: + branches: + - main + paths: + - .devcontainer/prebuild/** diff --git a/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab-ci.yml[% endif %].jinja b/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab-ci.yml[% endif %].jinja index 1b0c8473..6ddd0c1d 100644 --- a/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab-ci.yml[% endif %].jinja +++ b/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab-ci.yml[% endif %].jinja @@ -1,6 +1,7 @@ stages: + - build - ci - release default: - image: python:{{ default_py }} + image: ${CI_REGISTRY_IMAGE}:dev-py{{ default_py }} include: .gitlab/ci/**.yml diff --git a/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab[% endif %]/ci/ci.yml.jinja b/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab[% endif %]/ci/ci.yml.jinja index f5d13c3f..8f6d23eb 100644 --- a/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab[% endif %]/ci/ci.yml.jinja +++ b/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab[% endif %]/ci/ci.yml.jinja @@ -6,7 +6,7 @@ ci: coverage_format: cobertura path: coverage.xml coverage: /(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/ - image: python:$PYTHON_VERSION + image: ${CI_REGISTRY_IMAGE}:dev-py${PYTHON_VERSION} interruptible: true only: - main diff --git a/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab[% endif %]/ci/devcontainer.yml b/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab[% endif %]/ci/devcontainer.yml new file mode 100644 index 00000000..fd4e1e43 --- /dev/null +++ b/template/[% if repo_host_type == 'gitlab.com' or repo_host_type == 'gitlab-self-managed' %].gitlab[% endif %]/ci/devcontainer.yml @@ -0,0 +1,28 @@ +devcontainer-prebuild: + image: docker:latest + interruptible: true + only: + - main + parallel: + matrix: + - PYTHON_VERSION: + - '3.8' + - '3.9' + - '3.10' + - '3.11' + - '3.12' + script: + - apk add --update nodejs npm python3 make g++ + - npm install -g @devcontainers/cli + - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} + - | + devcontainer build \ + --image-name ${CI_REGISTRY_IMAGE}:dev-py${PYTHON_VERSION} \ + --push true \ + --workspace-folder .devcontainer/prebuild/ + services: + - docker:dind + stage: build + variables: + DOCKER_TLS_CERTDIR: /certs + PYTHON_VERSION: ${PYTHON_VERSION}