Skip to content
Merged
23 changes: 10 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Comment thread
bradjin8 marked this conversation as resolved.

### 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
Expand All @@ -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**:
Expand Down
29 changes: 27 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
bradjin8 marked this conversation as resolved.
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.
Comment thread
bradjin8 marked this conversation as resolved.
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

Expand Down
31 changes: 31 additions & 0 deletions RELEASE_NOTES_v0.1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# localci v0.1.0

<!-- Maintainer: merge the release PR to develop, tag v0.1.0 on that commit, confirm
CHANGELOG.md at the tag shows ## [0.1.0] - 2026-07-31 (not TBD) and CI is green,
then publish with: gh release create v0.1.0 --notes-file RELEASE_NOTES_v0.1.0.md -->

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
Comment thread
bradjin8 marked this conversation as resolved.
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
Comment thread
bradjin8 marked this conversation as resolved.
12 changes: 6 additions & 6 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 2 additions & 7 deletions cli/localci/cli/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import click

from localci.core.workflow import (
PLATFORM_CLI_MAP,
CompilerFamily,
Platform,
WorkflowAnalyzer,
Expand All @@ -29,12 +30,6 @@
# Helpers
# =====================================================================

_PLATFORM_MAP = {
"linux": Platform.LINUX,
"windows": Platform.WINDOWS,
"macos": Platform.MACOS,
}

_COMPILER_MAP = {
"gcc": CompilerFamily.GCC,
"clang": CompilerFamily.CLANG,
Expand Down Expand Up @@ -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]

Expand Down
18 changes: 5 additions & 13 deletions cli/localci/cli/run/run_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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
Expand Down
12 changes: 0 additions & 12 deletions cli/localci/core/patch_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
7 changes: 7 additions & 0 deletions cli/localci/core/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
8 changes: 0 additions & 8 deletions cli/localci/utils/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
28 changes: 0 additions & 28 deletions cli/localci/utils/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading