.github/workflows/test.yaml
name: CI/CD Pipeline
# Continuous Integration / Continuous Delivery
# Triggers on all Branches and v* Tags
### Stress-Testing, with Multi-Factor Job Matrix, on: ###
# - tags v*
# - the 'stress-test' branch (GITHUB_REF_NAME == 'stress-test')
### Production PyPI Publish, pypi.org, on: ###
# - v* tags on 'master' branch only
### Staging/Test PyPI Publish, test.pypi.org, on: ###
## Test PyPI publish on: ##
# - v*-rc 'pre-release' tags on 'release' branch
### Dockerhub publish on ###
# - all branches and tags
on:
push:
branches:
- "*"
tags:
- v*
env:
### STRESS TEST Job MATRIX ###
FULL_MATRIX_STRATEGY: "{\"platform\": [\"ubuntu-latest\", \"macos-latest\", \"windows-latest\"], \"python-version\": [\"3.7\", \"3.8\", \"3.9\", \"3.10\", \"3.11\"]}"
# Python 3.7 has reached End of Life (EOL) on June 27th, 2023
# Python 3.12 is in bugfix mode, same as 3.11 -> can start supporting 3.12 it
UBUNTU_PY310_STRATEGY: "{\"platform\":[\"ubuntu-latest\"], \"python-version\":[\"3.10\"]}"
TEST_STRATEGY: "{\"platform\":[\"ubuntu-latest\", \"macos-latest\", \"windows-latest\"], \"python-version\":[\"3.9\"]}"
##### JOB ON/OFF SWITCHES #####
RUN_UNIT_TESTS: "true"
RUN_LINT_CHECKS: "true"
DOCKER_JOB_ON: "true"
PUBLISH_ON_PYPI: "true"
DOCS_ON: "true"
DRAW_DEPENDENCIES: "true"
###############################
### DOCKER Job Policy ####
# Override Docker Policy-dependent decision-making and
# Accept any ALL (branch/build) to Publish to Dockerhub
# if true, it will push image and ignore DOCKER_JOB_POLICY
ALWAYS_BUILD_N_PUBLSIH_DOCKER: "false"
DOCKER_JOB_POLICY: "CDeployment"
# - CDeployment : Builds and Publishes only if Tests ran and passed
# - CDelivery : Builds and Publishes if Tests Passed or if Tests were Skipped
############################
#### STATIC CODE ANALYSIS Job ####
ALWAYS_LINT: "false"
LINT_JOB_POLICY: '2' # {2, 3}
## Python Runtime version to set the Job runner with ##
STATIC_ANALYSIS_PY: "3.10"
## Pylint Minimum Acceptance Rating/Score ##
PYLINT_SCORE_THRESHOLD: "8.2"
#### DOCS Build/Test ####
ALWAYS_DOCS: "false"
DOCS_JOB_POLICY: '2' # {2, 3}
DOCS_BUILDER_RUNTIME: "3.10"
#### CODE VISUALIZATION Job ####
ALWAYS_CODE_VIZ: "false"
CODE_VIZ_POLICY: '2' # {2, 3}
##########################
jobs:
# we use the below to read the workflow env vars and be able to use in "- if:" Job conditionals
# now we can do -> if: ${{ needs.set_github_outputs.outputs.TESTS_ENABLED == 'true' }}
# github does not have a way to simply do "- if: ${{ env.RUN_UNIT_TESTS == 'true' }} " !!
set_github_outputs:
name: Read Workflow Env Section Vars and set Github Outputs
runs-on: ubuntu-latest
steps:
- name: Pass 'env' section variables to GITHUB_OUTPUT
id: pass-env-to-output
run: |
# set the matrix strategy to Full Matrix Stress Test if on master/main or stress-test branch or any tag
BRANCH_NAME=${GITHUB_REF_NAME}
if [[ $BRANCH_NAME == "stress-test" || $GITHUB_REF == refs/tags/* ]]; then
echo "matrix=$FULL_MATRIX_STRATEGY" >> $GITHUB_OUTPUT
else
echo "matrix=$UBUNTU_PY310_STRATEGY" >> $GITHUB_OUTPUT
fi
echo "TESTS_ENABLED=$RUN_UNIT_TESTS" >> $GITHUB_OUTPUT
echo "PUBLISH_ON_PYPI=$PUBLISH_ON_PYPI" >> $GITHUB_OUTPUT
## Docker - Pipeline Settings ##
- id: derive_docker_policy
run: echo "POL=${{ (env.DOCKER_JOB_ON != 'true' && '0') || (env.ALWAYS_BUILD_N_PUBLSIH_DOCKER == 'true' && '1') || (env.DOCKER_JOB_POLICY == 'CDeployment' && '2') || (env.DOCKER_JOB_POLICY == 'CDelivery' && '3') }}" >> $GITHUB_OUTPUT
## Static Code Analysis - Pipeline Settings ##
- id: derive_sqa_policy
run: echo "POL=${{ (env.RUN_LINT_CHECKS != 'true' && '0') || (env.ALWAYS_LINT == 'true' && '1') || env.LINT_JOB_POLICY }}" >> $GITHUB_OUTPUT
- id: read_sqa_py
run: echo SQA_PY=${{ env.STATIC_ANALYSIS_PY }} >> $GITHUB_OUTPUT
- id: read_pylint_baseline_score
run: echo PYLINT_BASELINE_SCORE=${{ env.PYLINT_SCORE_THRESHOLD }} >> $GITHUB_OUTPUT
## Docs Build/Test - Pipeline Settings ##
- id: derive_docs_policy
run: echo "POL=${{ (env.DOCS_ON != 'true' && '0') || (env.ALWAYS_DOCS == 'true' && '1') || env.DOCS_JOB_POLICY }}" >> $GITHUB_OUTPUT
- id: read_docs_py
run: echo DOCS_PY=${{ env.DOCS_BUILDER_RUNTIME }} >> $GITHUB_OUTPUT
## Code Visualization - Pipeline Settings ##
- id: derive_code_viz_policy
run: echo "POL=${{ (env.DRAW_DEPENDENCIES != 'true' && '0') || (env.ALWAYS_CODE_VIZ == 'true' && '1') || env.CODE_VIZ_POLICY }}" >> $GITHUB_OUTPUT
outputs:
matrix: ${{ steps.pass-env-to-output.outputs.matrix }}
TESTS_ENABLED: ${{ steps.pass-env-to-output.outputs.TESTS_ENABLED }}
PUBLISH_ON_PYPI: ${{ steps.pass-env-to-output.outputs.PUBLISH_ON_PYPI }}
## Docker - Pipeline Settings ##
PIPE_DOCKER_POLICY: ${{ steps.derive_docker_policy.outputs.POL }}
## Static Code Analysis - Pipeline Settings ##
PIPE_SQA_POLICY: ${{ steps.derive_sqa_policy.outputs.POL }}
PIPE_SQA_PY: ${{ steps.read_sqa_py.outputs.SQA_PY }}
PIPE_SQA_PYLINT_PASS_SCORE: ${{ steps.read_pylint_baseline_score.outputs.PYLINT_BASELINE_SCORE }}
## Docs Build/Test - Pipeline Settings ##
PIPE_DOCS_POLICY: ${{ steps.derive_docs_policy.outputs.POL }}
PIPE_DOCS_PY: ${{ steps.read_docs_py.outputs.DOCS_PY }}
## Code Visualization - Pipeline Settings ##
PIPE_CODE_VIZ_POLICY: ${{ steps.derive_code_viz_policy.outputs.POL }}
# RUN TEST SUITE ON ALL PLATFORMS
test_suite:
runs-on: ${{ matrix.platform }}
needs: set_github_outputs
if: ${{ needs.set_github_outputs.outputs.TESTS_ENABLED == 'true' }}
strategy:
matrix: ${{fromJSON(needs.set_github_outputs.outputs.matrix)}}
env:
WHEELS_PIP_DIR: "wheels-pip"
steps:
- run: echo "Platform -> ${{ matrix.platform }} , Python -> ${{ matrix.python-version }}"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: python -m pip install --upgrade pip && python -m pip install tox==3.28 tox-gh-actions
- uses: actions/checkout@v4
- name: Pin 'Static Type Checking' Dependencies
run: tox -vv -s false -e pin-deps -- -E typing
- name: Do Type Checking
run: tox -e type -vv -s false
###### TEST SUITE RUN against Edit and Sdist ######
- name: Build and Test 'Edit' and 'Sdist' & Build Wheel(s)
run: |
set -o pipefail
tox -vv -s false | tee test_output.log
env:
PLATFORM: ${{ matrix.platform }}
BUILD_DEST: ${{ env.WHEELS_PIP_DIR }}
- name: List Wheel(s) created for Distro and its Requirements
run: ls -l ${{ env.WHEELS_PIP_DIR }}
### PARSE WHEEL File NAME ###
- name: Extract Wheel Name and Size
id: extract_wheel_info
shell: bash
run: |
WHEEL_INFO=$(grep -E "Created wheel for" test_output.log | sed -E "s/.*filename=([^ ]+) size=([^ ]+) .*/\1 \2/")
# extract file name
WHEEL_NAME=$(echo $WHEEL_INFO | cut -d ' ' -f 1)
echo "WHEEL_NAME=$WHEEL_NAME" >> $GITHUB_ENV
# extract file size
WHEEL_SIZE=$(echo $WHEEL_INFO | cut -d ' ' -f 2)
echo "WHEEL_SIZE=$WHEEL_SIZE" >> $GITHUB_ENV
# extract '1.12.5.dev0' from 'biskotaki-1.12.5.dev0-py3-none-any.whl'
PEP_VERSION=$(echo $WHEEL_NAME | sed -E "s/biskotaki-([^ ]+)-py3-none-any.whl/\1/")
echo "PEP_VERSION=${PEP_VERSION}" >> $GITHUB_OUTPUT
- run: 'echo "WHEEL_NAME: $WHEEL_NAME SIZE: $WHEEL_SIZE"'
- run: 'echo "PEP_VERSION: $PEP_VERSION"'
# Crash Workflow if PEP_VERSION is not set
- if: ${{ steps.extract_wheel_info.outputs.PEP_VERSION == '' }}
run: exit 1
###### TEST SUITE RUN against Wheel ######
- name: Run Test Suite Against Wheel
run: tox -e wheel-test -s false
env:
PLATFORM: ${{ matrix.platform }}
BUILD_DEST: ${{ env.WHEELS_PIP_DIR }}
WHEEL: ${{ env.WHEEL_NAME }}
## Combine Coverage Data from Tests' Results ##
- name: "Combine Coverage (dev, sdist, wheel) & make Reports"
run: tox -e coverage --sitepackages -vv -s false
- name: Rename Coverage Files
shell: bash
run: |
mv ./.tox/coverage.xml ./coverage-${{ matrix.platform }}-${{ matrix.python-version }}.xml
## UPLOAD CI ARTIFACTS - Coverage Reports ##
- name: "Upload Test Coverage as Artifacts"
uses: actions/upload-artifact@v3
with:
name: all_coverage_raw
path: coverage-${{ matrix.platform }}-${{ matrix.python-version }}.xml
if-no-files-found: error
- name: Check for compliance with Python Best Practices
shell: bash
env:
PKG_VERSION: ${{ steps.extract_wheel_info.outputs.PEP_VERSION }}
WHEEL_PATH: ${{ env.WHEELS_PIP_DIR }}/${{ env.WHEEL_NAME }}
run: |
DIST_DIR=dist
echo "DIST_DIR=${DIST_DIR}" >> $GITHUB_ENV
mkdir ${DIST_DIR}
mv ".tox/${DIST_DIR}/biskotaki-${PKG_VERSION}.tar.gz" "${DIST_DIR}"
mv "${{ env.WHEEL_PATH }}" "${DIST_DIR}"
tox -e check -vv -s false
## UPLOAD CI ARTIFACTS - Distro Build(s), 1 sdist and 1 or more wheels ##
- name: Upload Source & Wheel distributions as Artefacts
uses: actions/upload-artifact@v3
with:
name: dist-${{ matrix.platform }}-${{ matrix.python-version }}
path: ${{ env.DIST_DIR }}
if-no-files-found: error
outputs:
PEP_VERSION: ${{ steps.extract_wheel_info.outputs.PEP_VERSION }}
codecov_coverage_host:
runs-on: ubuntu-latest
needs: test_suite
steps:
- uses: actions/checkout@v3
- name: Get Codecov binary
run: |
curl -Os https://uploader.codecov.io/latest/linux/codecov
chmod +x codecov
- name: Download Raw Coverage Data Artefacts
uses: actions/download-artifact@v3
with:
name: all_coverage_raw
- name: Upload Coverage Reports to Codecov
run: |
for file in coverage*.xml; do
OS_NAME=$(echo $file | sed -E "s/coverage-(\w\+)-/\1/")
PY_VERSION=$(echo $file | sed -E "s/coverage-\w\+-(\d\.)\+/\1/")
./codecov -f $file -e "OS=$OS_NAME,PYTHON=$PY_VERSION" --flags unittests --verbose
echo "Sent to Codecov: $file !"
done
## DOCKER BUILD and PUBLISH ON DOCKERHUB ##
# Docs Ref Page: https://automated-workflows.readthedocs.io/en/main/ref_docker/
docker_build:
needs: [set_github_outputs, test_suite]
uses: boromir674/automated-workflows/.github/workflows/docker.yml@v1.1.0
if: always()
with:
acceptance_policy: ${{ needs.set_github_outputs.outputs.PIPE_DOCKER_POLICY }}
image_slug: "biskotaki"
# target_stage: "some_stage_alias" # no stage, means no `--target` flag, on build
tests_pass: ${{ needs.test_suite.result == 'success' }}
tests_run: ${{ !contains(fromJSON('["skipped", "cancelled"]'), needs.test_suite.result) }}
DOCKER_USER: ${{ vars.DOCKER_USER }}
secrets:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
# PUBLISH DISTRIBUTIONS ON PYPI
# we consider publishing on tags starting with "v"
check_which_git_branch_we_are_on:
runs-on: ubuntu-latest
needs: set_github_outputs
if: startsWith(github.event.ref, 'refs/tags/v') && needs.set_github_outputs.outputs.PUBLISH_ON_PYPI == 'true'
outputs:
ENVIRONMENT_NAME: ${{ steps.set_environment_name.outputs.ENVIRONMENT_NAME }}
AUTOMATED_DEPLOY: ${{ steps.set_environment_name.outputs.AUTOMATED_DEPLOY }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: rickstaa/action-contains-tag@v1
id: main_contains_tag
with:
reference: "master"
tag: ${{ github.ref }}
- run: echo "ON_MAIN_BRANCH=${{ steps.main_contains_tag.outputs.retval }}" >> $GITHUB_OUTPUT
- uses: rickstaa/action-contains-tag@v1
id: release_contains_tag
with:
reference: "release"
tag: ${{ github.ref }}
- run: echo "ON_RELEASE_BRANCH=${{ steps.release_contains_tag.outputs.retval }}" >> $GITHUB_OUTPUT
- name: Pick Production or Test Environment, if tag on master or release branch respectively
id: set_environment_name
run: |
DEPLOY=true
if [[ "${{ steps.main_contains_tag.outputs.retval }}" == "true" ]]; then
echo "ENVIRONMENT_NAME=PROD_DEPLOYMENT" >> $GITHUB_OUTPUT
elif [[ "${{ steps.release_contains_tag.outputs.retval }}" == "true" ]]; then
echo "ENVIRONMENT_NAME=TEST_DEPLOYMENT" >> $GITHUB_OUTPUT
else
echo "A tag was pushed but not on master or release branch. No deployment will be done."
DEPLOY=false
fi
echo "AUTOMATED_DEPLOY=$DEPLOY" >> $GITHUB_OUTPUT
pypi_publish:
needs: [test_suite, check_which_git_branch_we_are_on]
runs-on: ubuntu-latest
# if we are on tag starting with "v" and if we are on master or dev branch
if: startsWith(github.event.ref, 'refs/tags/v') && ${{ needs.check_which_git_branch_we_are_on.outputs.AUTOMATED_DEPLOY == 'true' }}
environment:
name: ${{ needs.check_which_git_branch_we_are_on.outputs.ENVIRONMENT_NAME }}
env:
DIST_DIR: dist
PACKAGE_DIST_VERSION: ${{ needs.test_suite.outputs.PEP_VERSION }}
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
PYPI_SERVER: ${{ vars.PYPI_SERVER }}
steps:
- uses: actions/checkout@v3
- name: Download Source & Wheel distributions
uses: actions/download-artifact@v3
with:
path: downloaded-artifacts
- name: Get Publishable files from the Artifacts
run: |
TAG="${GITHUB_REF_NAME}"
SEM_VER="${TAG:1}" # remove the first character (v)
PARSER="./scripts/parse_version.py"
PACKAGE_VERSION=$(python "${PARSER}")
if [[ "${PACKAGE_VERSION}" != "${SEM_VER}" ]]; then
echo "ERROR: Sem Ver, from Python Code (${PACKAGE_VERSION}), does not match tag version (${SEM_VER})"
exit 1
fi
- run: mkdir ${DIST_DIR}
- run: |
# Get Source Distribution (tar.gz of source code)
source_distributions=$(find downloaded-artifacts -type f -name biskotaki*.tar.gz)
source_distributions_array=($source_distributions)
source_distribution=${source_distributions_array[0]} # a *.tar.gz file path
# Extract the base name (without the path)
source_distribution_name=$(basename "$source_distribution")
# Check if all source distribution (.tar.gz) files have the same name
for file in "${source_distributions_array[@]}"; do
if [ "$source_distribution_name" != "$(basename "$file")" ]; then
echo "Error: Not all Source Distribution .tar.gz files have the same name!"
exit 1
fi
done
echo "source_distribution=$source_distribution" >> $GITHUB_ENV
- run: cp "$source_distribution" ${DIST_DIR}
- run: |
# Get all built Wheels and copy to dist folder
for f in `find downloaded-artifacts -type f -name biskotaki*.whl`; do
echo "F: $f";
# TODO check for duplicates, which means that our build matrix produces the same wheel (need a different compiler that python such as pypy, cython, etc)
cp $f ${DIST_DIR}
done
- name: Install Dependencies
run: pip install tox==3.28
- run: echo "Publishing $PACKAGE_DIST_VERSION to $PYPI_SERVER PyPI"
- name: Publish to PyPI
run: tox -vv -s false -e deploy -- upload --non-interactive --skip-existing
- run: echo "Published :\)"
### STATIC CODE ANALYSIS & LINTING ###
lint:
name: Static Code Analysis
needs: set_github_outputs
uses: ./.github/workflows/policy_lint.yml
with:
run_policy: ${{ needs.set_github_outputs.outputs.PIPE_SQA_POLICY }}
dedicated_branches: 'main, master, dev'
source_code_targets: 'src,tests,scripts'
python_version: ${{ needs.set_github_outputs.outputs.PIPE_SQA_PY }}
pylint_threshold: ${{ needs.set_github_outputs.outputs.PIPE_SQA_PYLINT_PASS_SCORE }}
### DOCS BUILD/TEST - DOCUMENTATION SITE ###
docs:
name: Build Documentation
needs: set_github_outputs
uses: boromir674/automated-workflows/.github/workflows/policy_docs.yml@v1.3.0
with:
run_policy: ${{ needs.set_github_outputs.outputs.PIPE_DOCS_POLICY }}
python_version: ${{ needs.set_github_outputs.outputs.PIPE_DOCS_PY }}
command: 'tox -s false -e pin-deps -- -E docs && tox -e docs --sitepackages -vv -s false'
### DRAW PYTHON DEPENDENCY GRAPHS ###
code_visualization:
needs: set_github_outputs
name: Code Visualization of Python Imports as Graphs, in .svg
uses: boromir674/automated-workflows/.github/workflows/python_imports.yml@8a441fa8fa008f1902ba892f8e8332fc77284597
with:
run_policy: '${{ needs.set_github_outputs.outputs.PIPE_CODE_VIZ_POLICY }}'
branches: 'main, master, dev'
source_code_targets: 'src'
python_version: '3.10'
artifacts_dir: 'dependency-graphs'