trying new approach with static steps and local 'config'

This commit is contained in:
sommersoft 2022-03-23 09:32:13 -05:00
parent 5612367f3f
commit c791725fda
3 changed files with 369 additions and 0 deletions

View File

@ -0,0 +1,62 @@
name: downstream_compose
on:
push:
branches:
- downstream_compose
jobs:
downstream-ubuntu:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- name: "pytest-django"
repo: "pytest-dev/pytest-django"
docker_profile: "postgres"
jobs: "test"
workflow_name: "main.yml"
matrix_exclude: |
linting,docs py39-dj40-mysql_innodb-coverage py36-dj22-sqlite-xdist-coverage py37-dj22-sqlite-xdist-coverage
py38-dj32-sqlite-xdist-coverage py38-dj40-sqlite-xdist-coverage py39-djmain-sqlite-coverage
py35-dj22-postgres-pytest54-coverage py35-dj22-sqlite_file-coverage py36-dj32-mysql_myisam-coverage
- name: "pytest-django"
repo: "pytest-dev/pytest-django"
docker_profile: "mysql"
jobs: "test"
workflow_name: "main.yml"
matrix_exclude: |
linting,docs py310-dj40-postgres-xdist-coverage py310-dj32-postgres-xdist-coverage
py39-dj32-postgres-xdist-coverage py35-dj22-postgres-pytest54-coverage pypy3-dj22-postgres
py36-dj22-sqlite-xdist-coverage py37-dj22-sqlite-xdist-coverage py38-dj32-sqlite-xdist-coverage
py38-dj40-sqlite-xdist-coverage py39-djmain-sqlite-coverage py35-dj22-sqlite_file-coverage
- name: "pytest-django"
repo: "pytest-dev/pytest-django"
docker_profile: "nodb"
jobs: "test"
workflow_name: "main.yml"
matrix_exclude: |
linting,docs py39-dj40-mysql_innodb-coverage py35-dj22-postgres-pytest54-coverage
py39-dj40-mysql_innodb-coverage py35-dj22-postgres-pytest54-coverage py36-dj32-mysql_myisam-coverage
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Checkout ${{ matrix.name }}
uses: actions/checkout@v2
with:
fetch-depth: 0
repository: ${{ matrix.repo }}
path: ${{ matrix.name }}
- name: Run Downstream Tests
run: docker-compose -f ./testing/downstream_testing/docker-compose.yml --profile ${{ matrix.docker_profile }} up --exit-code-from base_${{ matrix.docker_profile }}
env:
DS_NAME: ${{ matrix.name }}
DS_YAML: ./${{ matrix.name }}/.github/workflows/${{ matrix.workflow_name }}
DS_JOBS: ${{ matrix.jobs }}
DS_MATRIX_EXCLUDE: ${{ matrix.matrix_exclude }}
DOCKER_BUILDKIT: 1

View File

@ -0,0 +1,18 @@
{
"pytest-django": {
"matrix": [
"matrix",
"include"
],
"tox_cmd_build": {
"base": "name",
"prefix": "py",
"sub":
{
"pattern": "\\-coverage$",
"replace": ""
}
},
"python_version": "python"
}
}

View File

@ -0,0 +1,289 @@
import argparse
from collections import namedtuple, UserDict
import configparser
import json
import logging
import os
import os.path
from pprint import pprint
import re
import shlex
import subprocess
import sh
from sh.contrib import git
import yaml
logging.basicConfig(format="%(levelname)s | %(module)s.%(funcName)s | %(message)s", level="INFO")
logger = logging.getLogger(__name__)
parser = argparse.ArgumentParser(
description="downstream Actions runner"
)
parser.add_argument(
"repo",
#nargs=1,
help="Name of the repo."
)
parser.add_argument(
"source",
nargs=1,
help="Path to source YAML file."
)
parser.add_argument(
"jobs",
nargs="+",
help="Job names to use."
)
parser.add_argument(
"--matrix-exclude",
nargs="*",
default=[],
help="Exclude these matrix names."
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Do not run parsed downstream action. Only display the generated command list."
)
def load_matrix_schema(repo):
""" Loads the matrix schema for `repo` """
schema = None
working_dir = os.getcwd()
schema_path = os.path.join(
working_dir,
"testing",
"downstream_testing",
"action_schemas.json"
)
logger.debug("loading schema: %s", schema_path)
if os.path.exists(schema_path):
with open(schema_path) as schema_file:
try:
schema = json.load(schema_file)
except json.JSONDecodeError:
logger.error("failed to read action_schemas.json")
else:
logger.warning("action_schemas.json not found.")
if schema is not None and repo in schema:
schema = schema[repo]
logger.debug("'%s' schema loaded: %s", repo, schema)
else:
schema = None
logger.warning("'%s' schema not found in actions_schema.json", repo)
return schema
class DownstreamRunner:
# TODO: actualize args vs **kwargs
def __init__(self, **kwargs):
self.yaml_source = kwargs.get("yaml_source", None)
if self.yaml_source == None:
raise SystemExit("No YAML source provided.")
self._yaml_tree = None
self.repo = kwargs.get("repo")
self.matrix_exclude = kwargs.get("matrix_exclude")
self.job_names = kwargs.get("jobs")
self._matrix = None
self._steps = None
self.dry_run = kwargs.get("dry_run", False)
self.matrix_schema = load_matrix_schema(self.repo)
@property
def yaml_tree(self):
""" The YAML tree built from the `yaml_source` file. """
with open(self.yaml_source) as f:
self._yaml_tree = yaml.safe_load(f.read())
if self._yaml_tree == None:
raise SystemExit("Supplied YAML source failed to parse.")
return self._yaml_tree
def apply_git_patches(self):
""" Apply any applicable git patches to the repo."""
if self.dry_run:
logger.info("Skipping all git patches (dry-run).")
return
working_dir = os.getcwd()
patches_dir = os.path.join(working_dir, "testing", "downstream_testing", "patches")
repo_dir = os.path.join(working_dir, self.repo)
for patch in os.listdir(patches_dir):
if patch.startswith(self.repo) and patch.endswith(".patch"):
patch_full_path = os.path.join(patches_dir, patch)
logger.info("Applying patch: %s", patch)
if not self.dry_run:
if not os.getcwd() == repo_dir:
os.chdir(repo_dir)
try:
git.config("user.email", "noreply@noreply.no")
git.config("user.name", "pytest-dev")
logger.info("%s", git.am(patch_full_path))
except sh.ErrorReturnCode as err:
logger.info(
"Failed to apply '%s' to '%s': %s",
patch,
self.repo,
str(err.stderr)
)
else:
logger.info("Skipping patch: %s", patch)
if not os.getcwd() == working_dir:
os.chdir(working_dir)
def inject_pytest_dep(self):
""" Ensure pytest is a dependency in tox.ini to allow us to use the 'local'
version of pytest.
"""
ini_path = self.repo + "/tox.ini"
pytest_dep = f"pytest @ file://{os.getcwd()}"
DEPS = {
"pytest": {
"src": f"pytest @ file://{os.getcwd()}",
"condition": lambda x: x.startswith("pytest") and not x.startswith("pytest-"),
"has_gen": lambda x: re.search(r"pytest\w*:", x)
},
"pytest-rerunfailures": {
"src": "pytest-rerunfailures @ git+https://github.com/pytest-dev/pytest-rerunfailures.git",
"condition": lambda x: x.startswith("pytest-rerunfailures"),
"has_gen": lambda x: re.search(r"pytest-rerunfailures\w*:", x)
}
}
tox_source = configparser.ConfigParser()
tox_source.read_file(open(ini_path))
#updated_deps = set()
found_pytest = False
for section in tox_source.sections():
updated_deps = set()
section_deps = tox_source.get(section, "deps", fallback=None)
if section_deps:
for dep in section_deps.split("\n"):
for check_dep in DEPS:
if DEPS[check_dep]["condition"](dep):
has_gen = DEPS[check_dep]["has_gen"](dep)
if has_gen is not None and not found_pytest:
found_pytest = True
updated_deps.add(f"!{has_gen.group()} {DEPS[check_dep]['src']}")
updated_deps = '\n'.join(updated_deps)
tox_source[section]["deps"] = f"{tox_source[section]['deps']}\n{updated_deps}"
with open(ini_path, "w") as f:
tox_source.write(f)
def __repr__(self):
return str(
"DownstreamRunner("
f"repo={self.repo}, "
f"yaml_source={self.yaml_source}, "
f"job_names={self.job_names}, "
f"matrix={self.matrix}, "
f"steps={self.steps}"
")"
)
@property
def matrix(self):
def parse_matrix(yaml_tree):
parsed_matrix = yaml_tree
for key in self.matrix_schema["matrix"]:
parsed_matrix = parsed_matrix[key]
#logger.debug("%s", parsed_matrix)
if parsed_matrix != yaml_tree:
tox_base = self.matrix_schema["tox_cmd_build"]["base"]
tox_prefix = self.matrix_schema["tox_cmd_build"]["prefix"]
skip_matrices = []
for item in parsed_matrix:
if (not item[tox_base].startswith(tox_prefix) or
item[tox_base] in self.matrix_exclude
):
skip_matrices.append(item)
continue
item["tox_cmd"] = re.sub(
self.matrix_schema["tox_cmd_build"]["sub"]["pattern"],
self.matrix_schema["tox_cmd_build"]["sub"]["replace"],
item[tox_base]
)
#logger.debug("re.sub: %s", item[tox_base])
for matrice in skip_matrices:
parsed_matrix.remove(matrice)
return parsed_matrix
if self._matrix is None:
matrix_items = {}
for job in self.job_names:
job_yaml = self.yaml_tree["jobs"][job]["strategy"]
parsed_matrix = parse_matrix(job_yaml)
matrix_items[job] = parsed_matrix
self._matrix = matrix_items
logger.debug("matrix: %s", self._matrix)
return self._matrix
@property
def steps(self):
if self._steps is None:
step_items = {}
for job in self.job_names:
if job not in step_items:
step_items[job] = []
for item in self.yaml_tree["jobs"][job]["steps"]:
if "run" in item:
step_items[job].append(item)
self._steps = step_items
return self._steps
def build_run(self):
run = {}
for job in self.job_names:
logger.debug("job_name: %s", job)
for matrix in self.matrix[job]:
logger.debug("matrix[job]: %s", matrix)
run[matrix["name"]] = [
"pip install tox",
f"tox -e {matrix['tox_cmd']}"
]
logger.debug("built run: %s", run)
return run
def run(self):
self.apply_git_patches()
self.inject_pytest_dep()
run_steps = self.build_run()
os.chdir(self.repo)
for matrix, steps in run_steps.items():
for step in steps:
cmd = shlex.split(step)
logger.info("--> running: '%s'", step)
if not self.dry_run:
subprocess.run(
cmd,
encoding="utf-8",
check=True
)
if __name__ == "__main__":
cli_args = parser.parse_args()
if cli_args.dry_run:
logger.setLevel("DEBUG")
runner_args = {
"repo": cli_args.repo,
"yaml_source": cli_args.source[0],
"jobs": cli_args.jobs,
"matrix_exclude": cli_args.matrix_exclude,
"dry_run": cli_args.dry_run,
}
runner = DownstreamRunner(**runner_args)
runner.run()