Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ updates:
update-types:
- minor
- patch
commit-message:
prefix: "ci"
prefix-development: "ci"
include: "scope"

- package-ecosystem: gomod
directory: /controller
Expand All @@ -25,6 +29,10 @@ updates:
update-types:
- minor
- patch
commit-message:
prefix: "chore"
prefix-development: "chore"
include: "scope"

- package-ecosystem: docker
directory: /controller
Expand All @@ -38,6 +46,10 @@ updates:
update-types:
- minor
- patch
commit-message:
prefix: "chore"
prefix-development: "chore"
include: "scope"

- package-ecosystem: docker
directory: /runner
Expand All @@ -51,3 +63,7 @@ updates:
update-types:
- minor
- patch
commit-message:
prefix: "chore"
prefix-development: "chore"
include: "scope"
97 changes: 96 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ name: Validate ci-fleet prototype

on:
pull_request:
Comment thread
Nickfost marked this conversation as resolved.
# `edited` revalidates the title when a contributor retitles the PR: the
# required check would otherwise stay green for the same head SHA.
types: [opened, synchronize, reopened, edited]
push:
branches: [main]
tags: ['**']
Comment thread
Nickfost marked this conversation as resolved.
Comment thread
Nickfost marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate release metadata before publishing the tag

For an invalid or incorrectly placed version tag, this workflow starts only after the tag ref has already been created, and the workflow has read-only permissions with no step that removes a rejected tag. A malformed or mis-versioned tag therefore remains publicly fetchable even when this check fails, so the stated tag-based release gate does not prevent publication; restrict tag creation to a protected release path that runs these checks before pushing the ref.

Useful? React with 👍 / 👎.

Comment thread
Nickfost marked this conversation as resolved.
workflow_dispatch:

permissions:
Expand All @@ -14,8 +18,96 @@ concurrency:
cancel-in-progress: true

jobs:
commit-convention:
name: Enforce conventional commits and pull-request title
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
fetch-depth: 0

- name: Validate pull-request title
if: ${{ github.event_name == 'pull_request' }}
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
# The convention checks must run trusted code: a PR can edit its own
# copy of the validator, so extract it from the base revision when it
Comment on lines +38 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the required workflow definition outside the proposed ref

When a pull request modifies .github/workflows/validate.yml, the pull_request run uses the proposed workflow definition, so the contributor can replace these extraction and validation commands with a no-op while retaining the same job and required-check name. Extracting the Python validator from the base revision therefore does not make the convention gate trusted; enforce it through a protected workflow or ruleset whose definition cannot be changed by the pull request being checked.

AGENTS.md reference: AGENTS.md:L40-L40

Useful? React with 👍 / 👎.

# exists there. This first PR bootstraps the gate before the script
# exists on main; until then the checkout copy is the only available
# implementation, and this same commit lands it so the next merge
# base contains it and later PRs are fully trusted.
validator="$RUNNER_TEMP/trusted-validator.py"
BASE_SHA="${{ github.event.pull_request.base.sha }}"
if git cat-file -e "$BASE_SHA:scripts/validate_commits.py" 2>/dev/null; then
git show "$BASE_SHA:scripts/validate_commits.py" >"$validator"
else
cp scripts/validate_commits.py "$validator"
fi
python3 "$validator" --pr-title "$PR_TITLE"

- name: Validate proposed commit messages
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'push' && github.event.before || '' }}
Comment thread
Nickfost marked this conversation as resolved.
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
# Validate only commits proposed on this ref (base..head). Commits
# already on the base branch are never re-checked. The validator is
# shallow-clone safe: an empty base or empty range falls back to the
# head commit alone. For workflow_dispatch, validate only HEAD.
#
# Like the secret scanner below, the validator itself is extracted
# from the trusted base revision when available, so a PR cannot pass
# these gates by editing its own copy of the script. Until the script
# exists on the base branch (bootstrap PR), the checkout copy runs.
validator="$RUNNER_TEMP/trusted-validator.py"
if [[ "$EVENT_NAME" == pull_request ]] && git cat-file -e "$BASE_SHA:scripts/validate_commits.py" 2>/dev/null; then
git show "$BASE_SHA:scripts/validate_commits.py" >"$validator"
else
cp scripts/validate_commits.py "$validator"
Comment thread
Nickfost marked this conversation as resolved.
Outdated
fi
# A newly created tag reports an all-zero `before`, which makes
# `git rev-list <zero>..<tag>` fail and silently reduce validation
# to the tagged commit alone. Derive the real range start from the
# merge base with origin/main so every commit behind a branch-local
# (prerelease) tag is validated too. workflow_dispatch keeps its
# intentional HEAD-only behavior.
if [[ "$EVENT_NAME" == push && ( -z "$BASE_SHA" || "$BASE_SHA" =~ ^0+$ ) ]]; then
BASE_SHA="$(git merge-base "$HEAD_SHA" origin/main)"
fi
python3 "$validator" --base "$BASE_SHA" --head "$HEAD_SHA"

- name: Validate release tags are SemVer 2.0.0
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
TAG_NAME: ${{ github.ref_name }}
TAG_COMMIT: ${{ github.sha }}
run: |
# Like the commit gates above, tag policy must run trusted code: a
# branch-local tagged commit could otherwise weaken its own tag
# validation by editing scripts/validate_commits.py. Extract the
# validator from the merge base with origin/main; fall back to the
# checkout copy only while the bootstrap PR has no main copy yet.
validator="$RUNNER_TEMP/trusted-validator.py"
TRUSTED_SHA="$(git merge-base "$GITHUB_SHA" origin/main)"
if git cat-file -e "$TRUSTED_SHA:scripts/validate_commits.py" 2>/dev/null; then
git show "$TRUSTED_SHA:scripts/validate_commits.py" >"$validator"
else
cp scripts/validate_commits.py "$validator"
fi
# Stable tags must point into the main line (fetch-depth 0 gives us
# origin/main); prerelease tags must NOT point into it (they are
# branch-local per docs/CONTRIBUTING.md).
python3 "$validator" --version "$TAG_NAME" --tag-commit "$TAG_COMMIT"
Comment thread
Nickfost marked this conversation as resolved.
Outdated

validate:
name: Build without registering a runner
needs: commit-convention
Comment thread
Nickfost marked this conversation as resolved.
Comment thread
Nickfost marked this conversation as resolved.
if: ${{ always() }}
Comment thread
Nickfost marked this conversation as resolved.
Outdated
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand All @@ -32,7 +124,10 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
scanner=scripts/scan_committed_secrets.py
if [[ "$EVENT_NAME" == pull_request ]] && git cat-file -e "$BASE_SHA:$scanner" 2>/dev/null; then
if [[ "$EVENT_NAME" == push && ( -z "$BASE_SHA" || "$BASE_SHA" =~ ^0+$ ) ]]; then
BASE_SHA="$(git merge-base "$HEAD_SHA" origin/main)"
fi
if [[ -n "$BASE_SHA" ]] && git cat-file -e "$BASE_SHA:$scanner" 2>/dev/null; then
git show "$BASE_SHA:$scanner" >"$RUNNER_TEMP/trusted-secret-scanner.py"
Comment on lines +134 to 138

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use a main-derived scanner for updated tags

When an existing branch-local tag is moved forward, github.event.before is nonzero, so this condition does not replace BASE_SHA with the merge base and the scanner is extracted from the old tagged commit rather than trusted main history. A tag commit can harmlessly introduce a scanner that ignores selected files, pass its initial run because the main-derived scanner is used then, and later move the same tag to a descendant containing a credential; the update run executes the weakened old scanner and can report green. Fresh current-head evidence beyond the prior new-tag fix is that trusted-base normalization is still limited to empty/all-zero before values; derive scanner trust from origin/main for every tag push.

AGENTS.md reference: AGENTS.md:L28-L28

Useful? React with 👍 / 👎.

scanner="$RUNNER_TEMP/trusted-secret-scanner.py"
fi
Expand Down
142 changes: 142 additions & 0 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Contributing to ci-fleet

This document defines the contributor contract that governs every commit and
pull request in `RandomDevelopment/ci-fleet`. It is mandatory for all authors
and agents editing this repository.

## Release model

- **Versioning**: [Semantic Versioning 2.0.0](https://semver.org/). Tags may use
an optional leading `v` (e.g. `v1.2.3`), but the version payload is always a
valid SemVer string.
- **Commit format**: [Conventional Commits 1.0.0](https://www.conventionalcommits.org/).
The project squash-merges to `main`, so the squash title is the canonical
release entry and the PR title must itself be a conventional subject.
- **Pre-1.0 / `0.y.z`**: the project is in initial development. A `0.y.z`
release has an unstable public API: anything may change at any time without
notice. PATCH is still permitted for pure internal fixes, but MINOR and
MAJOR carry no stability guarantee until a `1.0.0` is tagged. Do not invent
or publish a release merely to satisfy versioning rules; releases are gated
by `docs/CONTRIBUTING.md` and the operator review window below.
- **No force-push** of published history and no rebased rewrites of shared
branches. Use `git revert` for corrections.

## Conventional Commits 1.0.0

```
<type>[optional scope][!]: <description>
```

- The `<type>` MUST be one of: `build`, `chore`, `ci`, `docs`, `feat`, `fix`,
`perf`, `refactor`, `revert`, `style`, `test`.
- The `<scope>` is optional and nested in parentheses, e.g. `feat(runner):`.
- Append `!` before the colon to mark a breaking change.
- The `<description>` is a single line; the complete subject (type, scope,
marker, separator, and description) is limited to <=100 characters and the
description begins with a lowercase letter (lowercase ASCII type + scope is
the convention; the subject itself may contain capitals for identifiers).
- Separate the subject from the body with exactly one blank line.
- Footers use `Token: value` form. A breaking change MAY also be declared with
a `BREAKING CHANGE:` footer (uppercase, per spec).

### SemVer mapping

| Commit | Bump |
| --- | --- |
| `fix:` or `perf:`, `refactor:`, `chore:`, `ci:`, `build:`, `style:`, `test:`, `docs:` (no `!`) | PATCH |
| `feat:` (no `!`) | MINOR |
| `feat!:`, `fix!:`, any `!`, or `BREAKING CHANGE:` footer | MAJOR |

### Examples

```
feat: add capacity telemetry endpoint
fix(runner): close leak on job cancellation
docs: record five-minute CI shard contract
ci: enforce conventional commits and semantic versioning
perf: cache host capability lookup
refactor: de-duplicate reconcile drift detection
feat!: replace the legacy controller entrypoint
```

```
fix(ci): stop recommending mutable image tags

The previous guidance used `:latest`, which violates pinning requirements.

Closes #42
Reviewed-by: An Operator <an-operator@example.org>
```

```
feat(controller): drop the legacy reconcile command

BREAKING CHANGE: `install-worker-controller.sh --reconcile` is removed.
Operators must use `--upgrade` instead.
```

## Public API / compatibility contract

The versioned public API of ci-fleet consists of the following stable
interfaces. A breaking change to any entry increments the MAJOR version.

1. **Configuration schema** — `templates/config-repository/fleet.schema.json`,
`schema_version: 3`. Managed projects submit Git-authored desired state
validated against this schema.
2. **Task-plan schema** — `examples/project/scripts/ci/plan.schema.json`,
`schema_version: 1`. The matrix-expansion contract consumed by project
workflows.
3. **Status-report evidence format** — `schemas/status-report-v1.json`,
`schema_version: 1`. The format emitted by `scripts/status_receiver.py` and
consumed by health/monitoring tooling.
4. **Engine rollout evidence format** —
`templates/config-repository/engine-rollout-evidence.json`,
`schema_version: 1`.
5. **Installer command contract** —
`scripts/install-worker-controller.sh` with `--install`, `--adopt`,
`--check`, `--upgrade`, `--rollback`, `--uninstall` and the
`--config-repo`, `--ref`, `--controller` arguments.
6. **Host-role command contracts** — the systemd unit command lines under
`host/systemd/*`, including the cleanup, health, drift, and reconcile
timer/entry-point contracts.
7. **Generated task matrix** — the `include` output produced by
`.github/actions/plan/plan.py`, consumed by project workflow matrices.

Non-API commits (docs, tests, CI, chore, style) never bump the public version
for API purposes; CI enforces PATCH-level change at minimum.

## Release gate

A version is released (tagged on `main`) only when:

- the tagged commit passes all CI checks;
- the change set is reviewed and the SemVer bump matches the Conventional
Commits classification;
- an operator has confirmed the live pilot evidence for any engine rollout
evidence schema change.

Do not tag a release to force a version number. This repository is pre-1.0;
avoid `1.0.0` until the controlled migration and compliance checklist
(`docs/COMPLIANCE-CHECKLIST.md`) are complete.

## Prerelease and build metadata

- Prerelease identifiers are supported by the validator but are not used for
`main`-sourced tags. Use `.0` patch sequences or branch-local tags only.
- Build metadata (`+build.<source>`) is permitted on tags but MUST NOT affect
SemVer precedence ordering.

## Validation

`scripts/validate_commits.py` enforces the Conventional Commits grammar, the
SemVer validator, and a `--suggest-bump` helper. `scripts/test_validate_commits.py`
is the regression suite. Both run in CI (see the `commit-convention` job in
`.github/workflows/validate.yml`).

Run locally:

```bash
python3 scripts/test_validate_commits.py
python3 scripts/validate_commits.py --message - <<< 'feat: local example'
python3 scripts/validate_commits.py --version 0.1.0
```
Loading