diff --git a/CHANGELOG.md b/CHANGELOG.md index 1668306..2b3c296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.0] - 2026-07-31 + +First public pre-release. The CLI package version is `0.1.0` (`cli/pyproject.toml`). + ### Added +- `localci` CLI — analyze, list, run, status, logs, images, cache, and config commands +- Configurable workflow patch pipeline for local act execution +- Docker image management and caching (ccache, Boost, CMake, APT) +- CI: ruff lint/format, mypy strict typecheck, unit tests, and act/Docker integration tests +- Runnable validation example under `examples/` +- README yq disambiguation and per-OS install matrix for [mikefarah/yq](https://github.com/mikefarah/yq) v4+ - `CONTRIBUTING.md` — developer onboarding (setup, tests, lint, pre-commit, PR expectations) - `CHANGELOG.md` — this file - `.github/CODEOWNERS` — maintainer routing for reviews @@ -29,19 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `PriorityJobQueue`: jobs in `WAITING_DEPS` return to `QUEUED` when their dependencies finish. Same-priority `needs` chains were leaving dependents stuck. - `PriorityJobQueue.cancel_all` drops cancelled keys from `_running_keys`. A `READY` job from `next_ready()` no longer counts as running after cancel. -## [0.1.0] - TBD - -Pre-release baseline. The CLI package version is `0.1.0` (`cli/pyproject.toml`). - -### Added - -- `localci` CLI — analyze, list, run, status, logs, images, cache, and config commands -- Configurable workflow patch pipeline for local act execution -- Docker image management and caching (ccache, Boost, CMake, APT) -- CI: ruff lint/format, mypy strict typecheck, unit tests, and act/Docker integration tests -- Runnable validation example under `examples/` -- README yq disambiguation and per-OS install matrix for [mikefarah/yq](https://github.com/mikefarah/yq) v4+ - ### Pre-release stability (before 1.0) This project is in **0.x** pre-release. Until **1.0.0**: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54eeb2a..88b616b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,10 +93,35 @@ pre-commit run -a ## Pull requests -1. Branch from `develop` (or `main` for release fixes). +1. Branch from `develop`. 2. Keep changes focused; link related issues in the PR description. -3. Ensure CI is green — lint, typecheck, unit tests, and integration tests must pass. +3. Ensure CI is green: lint, typecheck, unit tests, and integration tests must pass. 4. Request review from at least one maintainer. PRs need **at least one approving review** before merge. +5. Update `CHANGELOG.md` for user-visible changes under `## [Unreleased]`. + +## Branching and releases + +- `develop` is the default branch where day-to-day work lands and releases are cut from. +- To cut release `X.Y.Z` when `cli/pyproject.toml` already declares that version: + 1. Move the `## [Unreleased]` entries in [CHANGELOG.md](CHANGELOG.md) under a new + `## [X.Y.Z] - YYYY-MM-DD` heading, leave `## [Unreleased]` empty, and update + the link references at the bottom. + 2. Create `RELEASE_NOTES_vX.Y.Z.md` from the new `## [X.Y.Z]` section (see + [RELEASE_NOTES_v0.1.0.md](RELEASE_NOTES_v0.1.0.md) for the format). + 3. Merge the changelog and release-notes update to `develop` via PR; wait for CI + to be green. + 4. Create an annotated tag `vX.Y.Z` on the merged `develop` commit and push it + (`git tag -a vX.Y.Z` then `git push origin vX.Y.Z`). If `vX.Y.Z` already + exists, stop and verify `git rev-parse vX.Y.Z^{commit}` matches the merged + release commit (`git rev-parse origin/develop`); do not publish from a tag + that points elsewhere. + 5. Validate the tag: confirm `git show vX.Y.Z:CHANGELOG.md` has + `## [X.Y.Z] - YYYY-MM-DD` (not `TBD`) and CI is green on the tagged commit. + 6. Publish the GitHub Release + (`gh release create vX.Y.Z --notes-file RELEASE_NOTES_vX.Y.Z.md`) only after + steps 4–5 confirm the tag points to the merged release commit. + 7. Confirm the `[Unreleased]` and `[X.Y.Z]` compare links at the bottom of + [CHANGELOG.md](CHANGELOG.md) resolve after the new tag is created. ## Maintainers diff --git a/RELEASE_NOTES_v0.1.0.md b/RELEASE_NOTES_v0.1.0.md new file mode 100644 index 0000000..c75aa15 --- /dev/null +++ b/RELEASE_NOTES_v0.1.0.md @@ -0,0 +1,31 @@ +# localci v0.1.0 + + + +First public pre-release of Local CI. Run GitHub Actions workflows locally in Docker via [act](https://github.com/nektos/act). + +## Highlights + +- `localci` CLI: analyze, list, run, status, logs, images, cache, and config +- Generic patch profile by default; set `profile: capy` for Boost.Capy/B2 workflow patches +- Docker image management and caching (ccache, Boost, CMake, APT) +- Priority-ordered parallel execution with resource limits +- Windows and macOS matrix jobs fail with an explicit Linux-only message instead of running act without an image + +## Install + +```bash +git clone --branch v0.1.0 https://github.com/cppalliance/local-ci-test-system.git +cd local-ci-test-system/cli +pip install . +``` + +See [README.md](README.md) for prerequisites (Docker, act, mikefarah/yq v4+). + +## Pre-1.0 stability + +0.x pre-release: CLI flags, `.localci.yml`, and patch steps may change between minors. Pin a version and read [CHANGELOG.md](CHANGELOG.md) before upgrading. + +Full changelog: https://github.com/cppalliance/local-ci-test-system/blob/v0.1.0/CHANGELOG.md diff --git a/SECURITY.md b/SECURITY.md index d9bdd57..b83632c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,19 +2,19 @@ ## Supported Versions -Local CI is in pre-release development. There are no tagged stable releases yet -(`pyproject.toml` declares `0.1.0`, not yet tagged). +| Version | Supported | +| ------- | ------------------ | +| 0.1.x | :white_check_mark: | -Security fixes are produced only against the latest commit on the +Security fixes land only on the latest commit on [`develop`](https://github.com/cppalliance/local-ci-test-system/tree/develop) -branch. Once a `v0.1.0` tag is cut, this section will be reformatted as a -semver-style support table. +for the current 0.1.x line. ## Reporting a Vulnerability **Please do not open a public GitHub issue for security vulnerabilities.** -Report security issues through **[GitHub private vulnerability reporting](https://github.com/cppalliance/local-ci-test-system/security/advisories/new)** (preferred), or email **will@cppalliance.org** if you do not have a GitHub account. +Report security issues through **[GitHub private vulnerability reporting](https://github.com/cppalliance/local-ci-test-system/security/advisories/new)** or email **will@cppalliance.org**. If you need an encrypted channel, mention this in your initial email and a maintainer will reply with a current PGP key fingerprint or arrange another diff --git a/cli/localci/cli/list.py b/cli/localci/cli/list.py index 795e30a..ee3df86 100644 --- a/cli/localci/cli/list.py +++ b/cli/localci/cli/list.py @@ -12,6 +12,7 @@ import click from localci.core.workflow import ( + PLATFORM_CLI_MAP, CompilerFamily, Platform, WorkflowAnalyzer, @@ -29,12 +30,6 @@ # Helpers # ===================================================================== -_PLATFORM_MAP = { - "linux": Platform.LINUX, - "windows": Platform.WINDOWS, - "macos": Platform.MACOS, -} - _COMPILER_MAP = { "gcc": CompilerFamily.GCC, "clang": CompilerFamily.CLANG, @@ -165,7 +160,7 @@ def list_cmd( entries = wf.all_matrix_entries() if platform != "all": - target = _PLATFORM_MAP.get(platform) + target = PLATFORM_CLI_MAP.get(platform) if target: entries = [e for e in entries if e.platform == target] diff --git a/cli/localci/cli/run/run_flow.py b/cli/localci/cli/run/run_flow.py index 46e4cea..96aa4ff 100644 --- a/cli/localci/cli/run/run_flow.py +++ b/cli/localci/cli/run/run_flow.py @@ -13,15 +13,10 @@ from localci.core.github_token import resolve_github_token, warn_sentinel_github_token from localci.core.models import JobEvent, JobEventType from localci.core.results import ExecutionSummary -from localci.core.workflow import MatrixEntry, Platform, Workflow +from localci.core.workflow import PLATFORM_CLI_MAP, MatrixEntry, Workflow from localci.errors import ActNotFoundError, DockerNotAvailableError, WorkflowError from localci.utils.output import print_error, print_info, print_warning - -_PLATFORM_MAP = { - "linux": Platform.LINUX, - "windows": Platform.WINDOWS, - "macos": Platform.MACOS, -} +from localci.utils.paths import REGISTRY_FILENAME, find_file_upward def execute_run( @@ -73,13 +68,10 @@ def execute_run( return 0 priority_config = container.priority_config_factory(cfg) - registry_candidate = project_dir / "image-registry.yml" - registry_path: Path | None = ( - registry_candidate if registry_candidate.exists() else None - ) + registry_path = find_file_upward(REGISTRY_FILENAME, project_dir) matrix_include, matrix_exclude = _resolve_matrix_filters(options, cfg) - plat_filter = _PLATFORM_MAP.get(options.platform) if options.platform else None + plat_filter = PLATFORM_CLI_MAP.get(options.platform) if options.platform else None compiler_filter = options.compiler.lower() if options.compiler else None selected_set = {(jid, e.index) for jid, e in selected} job_filter_list = list({jid for jid, _ in selected}) @@ -197,7 +189,7 @@ def _filter_matrix_pairs( ) -> list[tuple[str, MatrixEntry]]: selected = list(all_pairs) if platform: - target_plat = _PLATFORM_MAP.get(platform) + target_plat = PLATFORM_CLI_MAP.get(platform) selected = [(jid, e) for jid, e in selected if e.platform == target_plat] compiler_filter = compiler.lower() if compiler else None diff --git a/cli/localci/core/patch_registry.py b/cli/localci/core/patch_registry.py index b3d541e..3581ca5 100644 --- a/cli/localci/core/patch_registry.py +++ b/cli/localci/core/patch_registry.py @@ -14,8 +14,6 @@ from importlib.metadata import entry_points from typing import TYPE_CHECKING -from localci.core.config import PATCH_STEP_NAMES - logger = logging.getLogger(__name__) if TYPE_CHECKING: @@ -38,13 +36,3 @@ def get_patch_step_registry() -> dict[str, type[PatchStep]]: "Failed to load localci.patch_steps entry point %r", ep.name ) return registry - - -def clear_patch_step_registry_cache() -> None: - """Clear the registry cache (for tests that register plugins at runtime).""" - get_patch_step_registry.cache_clear() - - -def is_builtin_patch_step(name: str) -> bool: - """Return True when *name* is a built-in (non-plugin) patch step.""" - return name in PATCH_STEP_NAMES diff --git a/cli/localci/core/workflow.py b/cli/localci/core/workflow.py index 5810da8..d2dfb28 100644 --- a/cli/localci/core/workflow.py +++ b/cli/localci/core/workflow.py @@ -41,6 +41,13 @@ class Platform(Enum): UNKNOWN = "unknown" +PLATFORM_CLI_MAP: dict[str, Platform] = { + "linux": Platform.LINUX, + "windows": Platform.WINDOWS, + "macos": Platform.MACOS, +} + + class CompilerFamily(Enum): """Compiler family classification.""" diff --git a/cli/localci/utils/output.py b/cli/localci/utils/output.py index 8d3cf04..bb1b6ca 100644 --- a/cli/localci/utils/output.py +++ b/cli/localci/utils/output.py @@ -105,11 +105,3 @@ def make_table(*columns: str, title: str | None = None) -> Table: for col in columns: table.add_column(col) return table - - -def print_not_implemented(command: str) -> None: - """Placeholder for commands whose backend is not yet built.""" - print_warning( - f"[muted]{command}[/muted] is not yet implemented. " - "It will be available once the corresponding module is built." - ) diff --git a/cli/localci/utils/paths.py b/cli/localci/utils/paths.py index aca1029..b2fe28b 100644 --- a/cli/localci/utils/paths.py +++ b/cli/localci/utils/paths.py @@ -9,34 +9,6 @@ from pathlib import Path -def resolve_workflow( - path: str | Path | None, config_workflow: Path | None = None -) -> Path: - """Resolve a workflow file path. - - Priority: - 1. Explicit *path* argument from the CLI - 2. ``workflow`` value from the loaded config - 3. Default ``.github/workflows/ci.yml`` - - Raises - ------ - FileNotFoundError - If the resolved path does not exist. - """ - if path is not None: - resolved = Path(path).resolve() - elif config_workflow is not None: - resolved = Path(config_workflow).resolve() - else: - resolved = Path(".github/workflows/ci.yml").resolve() - - if not resolved.is_file(): - raise FileNotFoundError(f"Workflow file not found: {resolved}") - - return resolved - - def ensure_directory(path: Path) -> Path: """Create directory (and parents) if it does not exist, then return it.""" path.mkdir(parents=True, exist_ok=True)