-
Notifications
You must be signed in to change notification settings - Fork 0
docs: map complete operator workflows #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| # Operator workflows | ||
|
|
||
| Use this page to choose an outcome, confirm the responsible role, and either follow the current runbook or stop at an explicit product gate. It does not replace the linked runbooks. | ||
|
|
||
| ## Status legend | ||
|
|
||
| - **Current** — implemented on the default branch and documented for the stated experimental scope. | ||
| - **Prototype only** — useful for isolated evaluation, not a managed or production path. | ||
| - **In review** — not available from the default branch; do not install it as current behavior. | ||
| - **Planned** — no supported command exists yet. | ||
| - **Not production-ready** — the repository does not authorize production use. | ||
|
|
||
| ## Choose an outcome | ||
|
|
||
| | Outcome | Status | Runs in | Changes state? | Current path or stopping condition | | ||
| | --- | --- | --- | --- | --- | | ||
| | Install or converge an ordinary-CI worker/controller | Current, experimental | Prepared Linux Docker host, using reviewed private configuration | Yes | Follow the [worker quickstart](QUICKSTART.md) and [desired-state lifecycle](DESIRED-STATE.md). | | ||
| | Add another worker host or location | Current, experimental | Private configuration, then the new Linux host | Yes | Follow [adding a host](ADDING-A-HOST.md); the application repositories do not change. | | ||
| | Authorize and onboard an application project | Current, experimental | Application repository, private configuration, and GitHub runner-group policy | Yes; no host mutation | Follow [adding a project](ADDING-A-PROJECT.md), then the [migration procedure](MIGRATING-EXISTING-CI.md). | | ||
| | Create controller GitHub credentials and runner-group prerequisites | Current manual procedure | GitHub web UI and the target host | Yes | Follow [GitHub App setup](GITHUB-APP-SETUP.md) and the [runner-group procedure](LIVE-PILOT.md#2-create-the-organization-runner-group). Automated target-host bootstrap remains [planned](https://github.com/RandomDevelopment/ci-fleet/issues/27). | | ||
| | Run an isolated first-job proof | Prototype only | GitHub Actions and one isolated worker host | Yes, transiently | Follow the [live pilot](LIVE-PILOT.md). Stop if the matching job queue is not proven empty. | | ||
| | Install a persistent test/staging environment host | Planned | Separate test host | N/A | No supported command exists. Stop at [issue #23](https://github.com/RandomDevelopment/ci-fleet/issues/23); do not adapt the worker installer. | | ||
| | Install a deployment host | In review | Separate deployment host | N/A on the default branch | [Issue #22](https://github.com/RandomDevelopment/ci-fleet/issues/22) and [PR #69](https://github.com/RandomDevelopment/ci-fleet/pull/69) track the installer and its required real-host evidence. Do not install it from the default branch yet. | | ||
| | Deploy to production | Not production-ready | Separate production boundary | N/A | Stop. The project status and production evidence do not authorize this workflow. | | ||
| | Publish or update from the standalone configuration template | Planned release path | Management workstation and a private configuration repository | Yes | Use the template vendored in the exact reviewed engine commit today. A standalone immutable release/compatibility signal remains [open](https://github.com/RandomDevelopment/ci-fleet-config-template/issues/12). | | ||
|
|
||
| ## Roles must remain separate | ||
|
|
||
| | Role | Purpose | May hold | Must never hold or do | | ||
| | --- | --- | --- | --- | | ||
| | CI worker/controller | Run short ordinary-CI jobs in disposable runner containers | Narrow runner-registration identity and generic host state | Deployment credentials, production authority, project runtimes on the host, or untrusted public-PR jobs | | ||
| | Test/staging environment host | Keep an isolated deployed candidate available for integration or browser testing | Test-only credentials and disposable test data | Production credentials, production data, ordinary runner jobs, or production promotion | | ||
| | Deployment host | Apply an explicitly approved immutable artifact through application-owned deployment logic | Environment-specific deployment identity | Ordinary runner jobs, source builds for promotion, or another environment's credentials | | ||
| | Application project | Define its test image, task plan, and thin Actions workflow | Project code and project-scoped CI secrets | Fleet controller credentials, host inventory, or host-specific capacity | | ||
| | Private configuration repository | Declare reviewed pools, controllers, capacity, and required secret names | Secret-free logical desired state | Secret values, host addresses, project runtime code, or unreviewed engine refs | | ||
|
|
||
| A Linux machine must not combine the worker, tester, and deployer roles. Runner containers share a Docker daemon security boundary with their host; container separation does not make credentials on that host safe from a host-privileged job. | ||
|
|
||
| ## Where each step happens | ||
|
|
||
| | Surface | Operator action | Mutates state? | | ||
| | --- | --- | --- | | ||
| | GitHub web UI | Create/install the GitHub App, create the restricted runner group, review configuration changes, authorize a repository, dispatch the proof job | Yes | | ||
| | Management workstation | Prepare and validate secret-free desired state, open configuration/application pull requests, record immutable commit IDs | Yes when committed or pushed | | ||
| | Private configuration repository | Review runner pools, logical controllers, capacity, repository allowlists, lifecycle, and engine pins | Yes when merged | | ||
| | Application repository | Add the project-owned test image, task plan, CI entrypoint, and thin workflow | Yes when merged | | ||
| | Linux worker host | Place host-local identity files, run the installer, and verify cleanup/health | Yes for lifecycle operations; `--check` is read-only | | ||
| | Hypervisor or hosting control plane | Provision the isolated Linux machine, network, storage, and recovery boundary | Yes, but outside this repository; stop and hand off to its authorized operator | | ||
|
|
||
| Never put credentials, private host details, or real infrastructure inventory in this public repository or in secret-free desired state. | ||
|
|
||
| ## Current one-command worker lifecycle | ||
|
|
||
| Prerequisites and credential placement are deliberate stopping points, not work hidden inside the installer. Complete the [quickstart](QUICKSTART.md) through reviewed configuration and host-local identity setup first. | ||
|
|
||
| Runs on: the prepared Linux worker host. Mutates state: **yes** (`--install`). | ||
|
|
||
| ```bash | ||
| sudo ./scripts/install-worker-controller.sh --install --config-repo example-org/example-fleet-config --ref 1111111111111111111111111111111111111111 --controller example-ci-01 | ||
| ``` | ||
|
|
||
| Runs on: the Linux worker host. Mutates state: **no** (`--check`). | ||
|
|
||
| ```bash | ||
| sudo ./scripts/install-worker-controller.sh --check --config-repo example-org/example-fleet-config --ref 1111111111111111111111111111111111111111 --controller example-ci-01 | ||
| ``` | ||
|
|
||
| The same script owns `--adopt`, `--upgrade`, `--rollback`, and `--uninstall`. Read the mode-specific prerequisites, stop conditions, expected report, and rollback behavior in [desired-state lifecycle](DESIRED-STATE.md) before using a mutating mode. | ||
|
|
||
| ## End-to-end worker checklist | ||
|
|
||
| ### Before mutation | ||
|
|
||
| - [ ] The role is an ordinary-CI worker only; no tester, deployer, production, or unrelated workload shares the host. | ||
| - [ ] The engine ref is a reviewed full commit reachable from the public default branch. | ||
| - [ ] The desired-state ref is a reviewed merge commit from the private configuration repository. | ||
| - [ ] The configuration validates strictly and contains fictional/public-safe data only where examples are involved. | ||
| - [ ] Host-local credentials exist with the ownership and mode required by the runbook; no credential value appears in Git or command arguments. | ||
| - [ ] GitHub App installation, runner-group restriction, and the controlled-first-job queue condition are proven. | ||
| - [ ] The operator has an explicit host recovery or replacement boundary. | ||
|
|
||
| ### After mutation | ||
|
|
||
| - [ ] Installer result is successful and records the intended controller, engine ref, and lifecycle state. | ||
| - [ ] `--check` reports `CHECK_OK` against the same immutable inputs. | ||
| - [ ] The controlled proof job uses read-only permissions and the expected shared routing label. | ||
| - [ ] The job is attributed to the intended scale set/controller. | ||
| - [ ] No ephemeral runner container or job-owned Docker resource remains. | ||
| - [ ] A fresh health evaluation succeeds and the controller returns to its configured idle state. | ||
| - [ ] Existing project CI remains enabled until parallel validation and rollback gates are complete. | ||
|
|
||
| If any item cannot be proven, stop without broadening permissions or improvising another role's installer. | ||
|
|
||
| ## Final report template | ||
|
|
||
| Store this report in the operator's approved private record, never in a public issue when it would reveal infrastructure or repository inventory. | ||
|
|
||
| ```text | ||
| Operation: | ||
| Role: | ||
| Result: SUCCESS | STOPPED | FAILED | ||
| Engine commit: | ||
| Configuration commit: | ||
| Controller or logical target: | ||
| Installer mode: | ||
| Validation performed: | ||
| GitHub-side evidence: | ||
| Cleanup and health evidence: | ||
| Rollback or replacement point: | ||
| Warnings: | ||
| Next safe action: | ||
| ``` | ||
|
|
||
| The report records identifiers and outcomes, not credential values, tokens, private keys, addresses, or internal host details. | ||
|
|
||
| ## Troubleshooting stops | ||
|
|
||
| | Symptom | Check | Safe next action | | ||
| | --- | --- | --- | | ||
| | Job remains queued | Complete `runs-on` expression, runner-group authorization, controller health, and scale-set identity | Correct reviewed policy or labels; do not raise capacity blindly. | | ||
| | Installer cannot read private configuration | Credential visibility for the root-run installer and scheduled drift service | Provide the documented narrow read path; do not copy a repository-writing token onto the worker. | | ||
| | Health service looks inactive | It is a timer-driven oneshot, so stale `systemctl status` is not a fresh evaluation | Start the health service explicitly, then run installer `--check` as documented in the quickstart. | | ||
| | Host is not clean after a job | Fleet ownership labels scoped to the runner/job, not global Docker state | Stop onboarding, preserve evidence, and use scoped cleanup only; never run global prune. | | ||
| | A guide asks for an unavailable role | Status table above | Stop at the linked issue or review gate; do not combine roles or invent a production path. | | ||
|
|
||
| For detailed errors and recovery commands, use the runbook linked from the selected outcome rather than reconstructing the workflow from this summary. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| #!/usr/bin/env python3 | ||
| """Fail when a tracked Markdown file links to a missing local path or heading.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
| from urllib.parse import unquote, urlsplit | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[1] | ||
| LINK = re.compile(r"(?<!!)\[[^\]]+\]\(([^)]+)\)") | ||
| HEADING = re.compile(r"^#{1,6}\s+(.+?)\s*#*\s*$") | ||
| FENCE = re.compile(r"^\s{0,3}([`~]{3,})") | ||
| EXTERNAL_SCHEMES = {"http", "https", "mailto"} | ||
|
|
||
|
|
||
| def tracked_markdown() -> list[Path]: | ||
| result = subprocess.run( | ||
| ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "*.md"], | ||
| cwd=ROOT, | ||
| check=True, | ||
| capture_output=True, | ||
| ) | ||
| return [ROOT / item.decode() for item in result.stdout.split(b"\0") if item] | ||
|
|
||
|
|
||
| def anchor(text: str) -> str: | ||
| text = re.sub(r"<[^>]+>", "", text) | ||
| text = re.sub(r"[`*_~]", "", text).strip().lower() | ||
| text = re.sub(r"[^\w\- ]", "", text) | ||
| return re.sub(r" +", "-", text) | ||
|
|
||
|
|
||
| def prose_lines(text: str): | ||
| fence_character = "" | ||
| fence_length = 0 | ||
| for line_number, line in enumerate(text.splitlines(), 1): | ||
| match = FENCE.match(line) | ||
| if match: | ||
| marker = match.group(1) | ||
| if not fence_character: | ||
| fence_character, fence_length = marker[0], len(marker) | ||
| elif marker[0] == fence_character and len(marker) >= fence_length: | ||
| fence_character, fence_length = "", 0 | ||
| continue | ||
| if not fence_character: | ||
| yield line_number, line | ||
|
|
||
|
|
||
| def anchors(path: Path) -> set[str]: | ||
| found: set[str] = set() | ||
| counts: dict[str, int] = {} | ||
| for _, line in prose_lines(path.read_text(encoding="utf-8")): | ||
| match = HEADING.match(line) | ||
| if not match: | ||
| continue | ||
| heading = match.group(1) | ||
| base = anchor(heading) | ||
| count = counts.get(base, 0) | ||
| found.add(base if count == 0 else f"{base}-{count}") | ||
| counts[base] = count + 1 | ||
| return found | ||
|
|
||
|
|
||
| def destination(raw: str) -> str: | ||
| raw = raw.strip() | ||
| if raw.startswith("<") and raw.endswith(">"): | ||
| return raw[1:-1] | ||
| # Markdown permits an optional quoted title after a whitespace separator. | ||
| return raw.split(maxsplit=1)[0] | ||
|
|
||
|
|
||
| def main() -> int: | ||
| failures: list[str] = [] | ||
| markdown = tracked_markdown() | ||
| known_anchors: dict[Path, set[str]] = {} | ||
|
|
||
| for source in markdown: | ||
| text = source.read_text(encoding="utf-8") | ||
| for line_number, line in prose_lines(text): | ||
| for match in LINK.finditer(line): | ||
| raw = destination(match.group(1)) | ||
| parsed = urlsplit(raw) | ||
| if parsed.scheme in EXTERNAL_SCHEMES or parsed.netloc: | ||
| continue | ||
|
|
||
| relative = unquote(parsed.path) | ||
| target = source if not relative else (source.parent / relative).resolve() | ||
| try: | ||
| target.relative_to(ROOT) | ||
| except ValueError: | ||
| failures.append(f"{source.relative_to(ROOT)}:{line_number}: link escapes repository: {raw}") | ||
| continue | ||
| if target.is_dir(): | ||
| target /= "README.md" | ||
| if not target.exists(): | ||
| failures.append(f"{source.relative_to(ROOT)}:{line_number}: missing target: {raw}") | ||
| continue | ||
| if parsed.fragment and target.suffix.lower() == ".md": | ||
| expected = unquote(parsed.fragment).lower() | ||
| available = known_anchors.setdefault(target, anchors(target)) | ||
| if expected not in available: | ||
| failures.append(f"{source.relative_to(ROOT)}:{line_number}: missing heading: {raw}") | ||
|
|
||
| if failures: | ||
| print("Markdown link validation failed:", file=sys.stderr) | ||
| print("\n".join(f"- {failure}" for failure in failures), file=sys.stderr) | ||
| return 1 | ||
| print(f"OK: {len(markdown)} tracked Markdown files have valid local links") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.