CI/CD Pipeline

This document describes the continuous integration and release pipeline for the Parsl Ephemeral Provider.

The authoritative definitions are the workflow files themselves — .github/workflows/ci.yml and .github/workflows/release.yml. This page explains why each job exists and how to run the same checks locally; it deliberately does not inline the YAML, because the copy that used to live here drifted out of sync with the workflow it documented.

Two workflows

Workflow

Trigger

Purpose

ci.yml

push to main, pull requests, manual dispatch

lint, type-check, tests, build, docs

release.yml

push of a v* tag

version check, tests, build, GitHub release, PyPI

ci.yml replaced a near-duplicate ci.yml + ci-cd.yml pair that between them ran the same unit suite five times, over Python versions the package no longer supports. Everything now runs through uvuv sync --locked installs from the committed uv.lock, so CI resolves the same dependency set a developer’s checkout does.

ci.yml jobs

lint

ruff check and ruff format --check, plus a bandit security scan.

Two details worth knowing:

  • Scope is ., the whole repository. It was once narrowed to parsl_ephemeral_provider tests, because tools/ carried bare excepts and unused imports in one-off debug scripts. #93 and #165 pruned those in v0.8.0, so the narrowing no longer protects anything and a .-scoped check passes.

  • ruff-format, not black. black and isort are no longer dependencies. The repo formats with ruff-format at its default 88-character line length; the old [tool.black] line-length = 100 disagreed with how every file was actually formatted, so running black reformatted ~80 unrelated lines. The ruff-pre-commit hook is pinned to the same ruff version uv.lock resolves, so a local pre-commit pass and the CI check cannot demand opposite output.

type-check

mypy parsl_ephemeral_provider, reported but not gated (continue-on-error: true) while the pre-existing error count is worked down under #81 and #82. Remove the continue-on-error once it reaches zero.

unit-tests

Runs tests/unit and tests/security on Python 3.10, 3.11, and 3.12. requires-python is >=3.10 — Parsl 2026.x dropped 3.9, so the old 3.8/3.9 matrix entries could not have installed the package at all.

tests/security is pure-mock and marked unit, so it belongs to this job rather than a separate one.

Selection is by path, not -m unit. That distinction caused a long-running divergence: the Makefile selected -m unit and collected only a fraction of the suite, passing, while CI ran the unmarked set and failed. Selecting by path means a newly-added file without a marker still runs — and the two now agree, because every file under tests/unit and tests/security carries the marker (1,211 collected either way).

This job carries the project’s real coverage gate, --cov-fail-under=65. The --cov-fail-under in pyproject.toml is a lower smoke floor because addopts applies to every invocation, including narrow ones — pytest tests/integration alone measures 34%.

integration-tests

Runs tests/integration against a pinned substrate service container (ghcr.io/scttfrdmn/substrate), plus a gated tests/test_substrate_emulation.py conformance step. Substrate replaced LocalStack in #125: LocalStack OSS is end-of-life and its latest community tag now resolves to the Pro build, which exits 55 without a license token – before any step runs, and outside what continue-on-error covers.

The integration suite gates as of #192. It carried continue-on-error: true for as long as 46 mode constructions across 9 files omitted the network IDs #69 made required, since gating before that was fixed would have turned every PR red on known debt. #92 closed in v0.8.0 and the suite is green, so the exemption could only have hidden a regression. Only the Codecov upload step remains non-gating.

Note that a pytest marker only selects tests — it never skips them. Each emulator-backed file pairs its markers with a skipif(not is_substrate_available()) guard; without one, a plain pytest tests/integration errors instead of skipping.

aws-e2e-tests

The real-AWS E2E suite (tests/aws), manual dispatch only — it bills money and needs a pre-provisioned VPC, subnet, and security group. #60 closed with tests here that no workflow referenced; there are 87 now, across ten files.

Credentials come from OIDC via aws-actions/configure-aws-credentials, so no long-lived access keys live in secrets. Configure:

  • Secret AWS_E2E_ROLE_ARN — the role the workflow assumes.

  • Variables AWS_TEST_REGION, AWS_TEST_VPC_ID, AWS_TEST_SUBNET_ID, AWS_TEST_SG_ID.

The job is gated on vars.AWS_TEST_REGION != '', so a dispatch on a repository without these configured skips rather than failing. The gate is what makes that true, not tests/aws/conftest.py: conftest does skip when the three IDs are unset, but pytest is never reached — configure-aws-credentials fails first on the empty region, so before #161 every dispatch was red.

Once it does run, conftest validates the IDs against AWS_TEST_REGION up front — IDs from another region otherwise surface minutes in, from deep inside RunInstances, after instances have been billed. Pick a subnet in an AZ that offers your instance type (us-east-1e does not offer t3.micro).

A final always() step runs parsl-ephemeral-cleanup --dry-run to report orphans, since a failed test is exactly when instances are most likely to be left running. It reports without deleting, so CI never mutates a shared account. The script takes credentials from the boto3 chain — the OIDC credentials the earlier step exported — rather than a named profile; it previously defaulted to the local aws profile and died here on “The config profile (aws) could not be found”.

test-bats

Runs bats tests/bats/ for the shell scripts under scripts/.

build

uv build, then twine check. Also asserts the CloudFormation templates are present in the built wheel: before #112, get_cf_template() resolved templates by filesystem path, so a wheel that omitted them failed only at runtime, on a real AWS call.

docs

Builds the Sphinx documentation and uploads it as an artifact.

release.yml

Triggered by pushing a v* tag. Before anything else it verifies the tag matches parsl_ephemeral_provider.__version__bump-my-version has silently missed __init__.py before (its [tool.bumpversion] search string drifted out of sync), and v0.6.0 shipped with __version__ == "0.1.0". Catching that here matters because a PyPI version can never be reused. make version-verify runs the same check locally, and the version-bump-* targets call it automatically.

PyPI publishing uses trusted publishing via OIDC, so no PYPI_API_TOKEN secret is needed. The GitHub release is created with gh release create; actions/create-release and actions/upload-release-asset are both archived and unmaintained.

The old ci-cd.yml carried a second build-and-publish job on release: published, so a tag push followed by a published release ran two independent PyPI uploads. This is now the only publish path.

Running the same checks locally

The Makefile runs exactly what CI runs, through uv:

make lint-python      # ruff check + ruff format --check
make type-check       # mypy
make test-unit        # tests/unit + tests/security, with the 65% gate
make test-integration # starts substrate, then tests/integration
make test-aws         # tests/aws against real AWS (prompts first; costs money)
make build            # uv build
make version-verify   # pyproject and __init__ versions agree

Never invoke pytest, ruff, or mypy bare — they resolve against whatever is on PATH rather than .venv. Use uv run (or the Makefile, which does).

Badges

[![CI](https://github.com/scttfrdmn/parsl-ephemeral-provider/actions/workflows/ci.yml/badge.svg)](https://github.com/scttfrdmn/parsl-ephemeral-provider/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/scttfrdmn/parsl-ephemeral-provider/branch/main/graph/badge.svg)](https://codecov.io/gh/scttfrdmn/parsl-ephemeral-provider)
[![PyPI version](https://badge.fury.io/py/parsl-ephemeral-provider.svg)](https://badge.fury.io/py/parsl-ephemeral-provider)

SPDX-License-Identifier: Apache-2.0 SPDX-FileCopyrightText: 2025-2026 Scott Friedman and Project Contributors