diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 7f3688b4..8dd02581 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -46,24 +46,11 @@ jobs: id: nbgv uses: dotnet/nbgv@master - # Skip create on an existing tag (no-op republish). A re-dispatch refreshes it. - - name: Check for existing release step - id: release-exists - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.nbgv.outputs.SemVer2 }} - run: | - set -euo pipefail - if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - + # Create-or-refresh: every trigger here is a dispatch, so an existing tag is refreshed, never skipped + # (the exists-gate belongs to the multi-trigger reusable form, where a scheduled re-run must no-op). # target_commitish pins the tag to the exact built commit (GitCommitId), not the default branch. The release is # the tag plus GitHub's auto source archive, README, and LICENSE - no build assets (source-only). - name: Create GitHub release step - if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: generate_release_notes: true diff --git a/AGENTS.md b/AGENTS.md index 9fb49bfb..b8eeb92c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,6 +71,14 @@ The template uses a **two-phase model by default**: PRs build fast, publishing i - **Issue-closing keywords (`Closes #N`, `Fixes #N`) go in the `develop -> main` promotion PR, not the feature -> develop PR.** GitHub auto-closes an issue only when the closing keyword merges into the **default branch** (`main`); a feature/develop PR merges into `develop`, so the keyword never fires there. Reference the issue in the develop PR body if useful, but put the actual closing keyword on the promotion PR. - **Wrapper repos that track an upstream release.** A repo wrapping an upstream release uses [`check-upstream-version-task.yml`](./catalog/snippets/workflows/check-upstream-version-task.yml): a resolver command prints the upstream version(s) as a **JSON object of `name -> version`**, written to a committed state file at the **repo root beside `version.json`** (default `upstream-version.json` - it is a build-input version source, not GitHub-platform config, so it does not belong under `.github/`), and opens a rolling App-signed bump PR per branch that the merge-bot auto-merges (`merge-upstream-version`). The object carries one key for the common single-version case (`{"version":"X"}`) or N keys for a wrapper that pins several upstream components (e.g. an image plus a companion tool), and the build reads each component by key; the bump PR's title/body name only the keys that actually moved. Call it from a scheduled entry-point workflow and matrix only the branches that ship the version (a CI-only version uses `["develop"]`). A merged bump ships on the **next publish**, not immediately - the two-phase latency tradeoff. +## Repository Onboarding and Conformance + +Every fleet repo is a standard-style repo the hub audits **downward** against its declared type - the model the fleet uses because managing downstream divergence is too costly. Three obligations follow, and they are not optional: + +- **Definition of done is the audit.** Onboarding or materially changing a repo is complete only when it passes [`AUDIT.md`](./AUDIT.md) for its type, **or** carries a committed `reports//audit.md` plus a tracking issue for the residual deltas. Do not leave a repo partially stood up and unrecorded - that is itself a defect. Stand a repo (or a new type) up with [`STANDUP.md`](./STANDUP.md); verify it with [`AUDIT.md`](./AUDIT.md); the two share the same manifests, so a repo stood up correctly passes the audit by construction. +- **The registry is ground truth about reality, not intent.** After any conformance change, reconcile the repo's [`registry/repos.json`](./registry/repos.json) entry - `status`, `types`, `releaseTrigger`, `workflowModel`, `driftNotes` - to what the repo actually is. [`spec/validate.py`](./spec/validate.py) proves the catalog is self-consistent; it does **not** prove the catalog matches the live repo. A `validate.py`-clean entry can still be false, and a stale entry is a finding. +- **Prove the docs suffice.** The onboarding docs are sufficient only when a context-free agent stands each supported repo shape - project type(s) plus workflow model - up from them alone; the cold-start self-test in [`STANDUP.md`](./STANDUP.md) and the [conformance matrix](./reports/conformance-matrix.md) track that. A shape that cannot be stood up cold is a documentation defect, fixed in the hub, not worked around per repo. + ## Pull Request Title and Commit Message Conventions ### Format diff --git a/AUDIT.md b/AUDIT.md index be68626f..52752a18 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -5,6 +5,13 @@ How an agent audits a repository against the fleet ground truth in this repo and The verdict vocabulary is [`WORKFLOW.md`][workflow]'s: **operational / not operational**, **N/A**, **defect**, and the applicable/absent rule. Do not invent a parallel scheme. +## 0. When to Run and What "Done" Means + +This audit is not occasional. Run it whenever you **create, adopt, or materially change** a fleet repo, and on demand for any known repo: + +- **Onboarding a repo is complete only when it either passes this audit** (operational - every applicable check) **or carries a committed `reports//audit.md` plus a tracking issue** enumerating every residual delta. A repo that is partially set up but never audited is itself a **defect** - the exact state this process prevents. The create-to-conformance counterpart is [`STANDUP.md`][standup]; because both read the same manifests, a repo stood up by that file passes this audit by construction. +- **Touching a repo** (any conformance-affecting change) ends by re-running the applicable checks and **reconciling the registry entry to reality** - `status`, `types`, `releaseTrigger`, `workflowModel`, `driftNotes`. The registry records reality, not intent; [`spec/validate.py`][validate] proves the catalog is self-consistent, not that it matches the live repo - closing that gap is this audit's job. + ## 1. Scope and Ground-Truth Branch Audit one repository at a time. Read the target's **`main` branch** as ground truth: `main` is the released, authoritative state. Read `develop` only to detect divergence - a stale or diverged `develop` (behind `main`, or diverged) is reported as a **drift finding**, never audited as the truth. Do not treat a `develop`-only file as present if it is absent on `main`. @@ -117,5 +124,7 @@ The convergence model: the hub audits and the agent **applies** the fixes via ta [repos]: ./registry/repos.json [secrets]: ./spec/secrets.json [spec]: ./spec/ +[standup]: ./STANDUP.md [template]: ./reports/_template.md +[validate]: ./spec/validate.py [workflow]: ./WORKFLOW.md diff --git a/STANDUP.md b/STANDUP.md new file mode 100644 index 00000000..3b47392d --- /dev/null +++ b/STANDUP.md @@ -0,0 +1,63 @@ +# STANDUP.md + +How an agent takes a repository from nothing (or a partial state) to **operational** against the fleet ground truth. This is the create-to-conformance procedure; [`AUDIT.md`][audit] is its read-only verifier and owns the definition of done. Both read the same ground truth - [`registry/repos.json`][repos], the [`spec/`][spec] manifests, [`repo-config/`][repo-config], and the prose authorities ([`AGENTS.md`][agents], [`CODESTYLE.md`][codestyle], [`WORKFLOW.md`][workflow]) - so a repo stood up by this file passes the audit by construction. + +Standing up a repo is **applying the manifests until the audit passes**, nothing more invented. If a repo needs a construct no manifest covers, that is a spec gap: raise it ([`AUDIT.md`][audit] section 9), never improvise a per-repo answer. This is the downward-audit model - standard-style repos the hub audits against their declared type - which the fleet uses because managing downstream divergence is too costly. + +## 1. Classify and Catalog + +Resolve the repo's type(s) with the [`AUDIT.md`][audit] section 2 detection rules, then write or repair its [`registry/repos.json`][repos] entry: `status`, `types[]`, `groundTruthBranch`, `hasDevelop`, `publish[]`, `requiredSecrets[]`, `consumerModel`, `releaseTrigger`, `workflowModel` (omit to take the `release` default), `configLayout`, and `driftNotes` that describe what the repo **actually is**. Run [`spec/validate.py`][validate] to confirm it classifies cleanly. The registry is ground truth about reality, not intent - a `validate.py`-clean entry is still false if it disagrees with the live repo. + +## 2. Carry the Baseline Files + +Copy every [`spec/files.json`][files] entry that applies to the repo's types - the `appliesTo: "*"` baseline plus the per-type additions - **adapted, not cloned**. The prose files (`CODESTYLE.md`, `README.md`, and the like) describe the repo's own toolchain, so adapt them to reality rather than propagating template specifics verbatim (see the "Adapt before propagating" callout in [`CODESTYLE.md`][codestyle]; a verbatim copy that misdescribes the repo is rejected in review). The baseline covers `WORKFLOW.md`, `version.json`, `repo-config/develop.json` + `main.json`, `.github/dependabot.yml`, `.editorconfig`, `.gitattributes`, the linter configs, and the per-type files (`.vscode/tasks.json` from the language's snippet, `codecov.yml`, `.dockerignore`, `Docker/README.md`). + +## 3. Stand Up the Workflows + +Implement the Actions that satisfy [`WORKFLOW.md`][workflow] for the repo's type (its section 6 per-type walkthrough): the source-only subset for a source-only repo, the file-target leaf(s) for a publishing repo, the two-workflow shape for an operational config repo. Reuse [`catalog/snippets/workflows/`][workflows] as the reference implementation - satisfy the contract by outcome, not byte for byte. + +## 4. Apply Settings, Rulesets, and Secrets + +Run `repo-config/configure.sh [owner/repo] [release|operational]` (the repo defaults to the current one, the model to the registry lookup) to apply the fleet settings and the two rulesets idempotently (import the JSON, never hand-build - see [`repo-config/README.md`][repo-config-readme]). Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s) - Actions, and Dependabot where the mechanism needs it - and confirm no forbidden secret is present. The required check binds by name (`Check pull request workflow status job`) and turns green only after the PR workflow has run once. + +## 5. Verify - Run the Audit + +Run [`AUDIT.md`][audit] end to end. The repo is stood up only when it is **operational** (every applicable check passes) or its residual deltas are tracked in `reports//audit.md` plus an issue. Converge any drift through a Copilot-reviewed target PR ([`AUDIT.md`][audit] section 10); the maintainer merges. A repo left partially set up and unrecorded is the exact failure this procedure exists to prevent. + +## Onboarding a New Repo Type + +When a repo matches no existing type, the work is onboarding a **type**, not just a repo: + +1. Add the type to [`spec/project-types.json`][project-types] (`detect[]`, plus `checks` with verdict tiers and intent refs) and any per-type files to [`spec/files.json`][files]; add its publish mechanism to [`spec/secrets.json`][secrets] if new. +2. Add the reference workflow leaf to [`catalog/snippets/workflows/`][workflows] and document the type's [`WORKFLOW.md`][workflow] walkthrough. +3. Add the type to the [conformance matrix][matrix] and run the cold-start self-test until a context-free agent stands it up to operational. + +## Self-Test - Cold-Start Conformance + +The onboarding docs are sufficient only if a **context-free agent stands up each supported repo shape from them alone** - a shape being the project type(s) plus the workflow model (`operational` is a `workflowModel` overlay, not a `spec/project-types.json` type). Run this whenever the onboarding docs or manifests change, and periodically as a fleet health check: + +- For each shape in the [conformance matrix][matrix], task a fresh agent (no prior context) with "Using only this repo's docs, stand up a `` repo," pointing it at this file. +- Run [`AUDIT.md`][audit] against the result. Record pass or fail, and the first doc gap that tripped the agent, in the [conformance matrix][matrix]. +- Iterate the **docs and tooling** (not the agent's memory) until every supported shape stands up cold to operational. A shape that cannot be stood up cold is a documentation defect, tracked like any other. + +The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo; its report lists deviations and repo-specific deltas. The self-test and the fleet audit are one procedure, pointed at a new repo or an existing one. + + + +[workflows]: ./catalog/snippets/workflows/ + + + +[agents]: ./AGENTS.md +[audit]: ./AUDIT.md +[codestyle]: ./CODESTYLE.md +[files]: ./spec/files.json +[matrix]: ./reports/conformance-matrix.md +[project-types]: ./spec/project-types.json +[repo-config]: ./repo-config/ +[repo-config-readme]: ./repo-config/README.md +[repos]: ./registry/repos.json +[secrets]: ./spec/secrets.json +[spec]: ./spec/ +[validate]: ./spec/validate.py +[workflow]: ./WORKFLOW.md diff --git a/WORKFLOW.md b/WORKFLOW.md index 9e6156e3..37d97ce1 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -218,7 +218,7 @@ Read the workflow files plus `version.json` and assert the structural fact behin - **D1:** a `changes` paths-filter job exists, covers each of the repo's targets, and **excludes** `.github/workflows/**`; the PR entry workflow's smoke call sets `github/nuget/dockerhub: false` on the release task; the leaf receives `smoke: true` and a derived `push` (false on smoke); every build-task `upload-artifact` (and any aggregation job) is gated `!smoke`; the aggregator `needs:` the `changes` and validation jobs, blocks on `failure`/`cancelled`, passes on `skipped`; a validation job runs unconditionally. - **D2:** an entry validation job/step exists per complex-input workflow; the release gate checks both directions, strips `+buildmetadata`, and skips on smoke; the publisher rejects a dispatch from a ref other than `main` or `develop`. - **D3:** each run builds one branch, so NBGV classifies `github.ref` directly (no `IGNORE_GITHUB_REF`); the default-branch literal in the gate (`== 'main'`), the `prerelease` expression (`!= 'main'`), and `version.json`'s `publicReleaseRefSpec` all name the repo's actual default branch. -- **D4:** `target_commitish` is the NBGV commit id; `prerelease` equals `branch != default`; the release-create step is gated `exists == false || workflow_dispatch`; the asset-delete step is gated identically. +- **D4:** `target_commitish` is the NBGV commit id; `prerelease` equals `branch != default`; the release-create step is gated `exists == 'false' || github.event_name == 'workflow_dispatch'` (the step output is the string `'false'`, not a boolean); the asset-delete step is gated identically. A dispatch-only publisher (`releaseTrigger: dispatch-only`) may omit the gate and the exists-check entirely - every run is a dispatch, so the skip leg can never fire and create-or-refresh is unconditional; record the gate N/A there, not missing. - **D5:** each cross-job transfer artifact has a delete step at its consumer, gated to the consumer's condition, `continue-on-error: true`, looping all ids; **every** upload sets `retention-days: 1`; **no** `.artifacts[].id` blanket delete exists anywhere. - **D6:** the release download uses `pattern:`/`merge-multiple:` (no `artifact-ids:`); branch-derived config reads `inputs.branch` (a `github.ref_name` in such config is a finding); artifact names are branch-suffixed; the target set is consistent across the release task and the paths-filter. - **D7:** the publisher concurrency group is ref-independent with `cancel-in-progress: false`; reusable jobs declare permissions; boolean `if:` uses both forms. diff --git a/repo-config/README.md b/repo-config/README.md index b1f98fe4..a6421bc9 100644 --- a/repo-config/README.md +++ b/repo-config/README.md @@ -2,10 +2,18 @@ Repository and branch configuration held as committed files, kept out of `.github/` (which is reserved for GitHub-Actions-owned content). This mirrors the layout the fleet repos use. -- `main.json`, `develop.json` - the branch rulesets as the writable API subset (`name`, `target`, `enforcement`, `bypass_actors`, `conditions`, `rules`). These are the canonical expected payload the audit ([AUDIT.md][audit]) diffs each repo's live rulesets against. -- `operational/develop.json` - the `develop` ruleset for **operational** repos (registry `workflowModel: operational`): direct signed pushes, no PR gate. `main.json` is shared by both models. See "Rulesets" below. +- `main.json` plus one `develop` variant - the branch rulesets as the writable API subset (`name`, `target`, `enforcement`, `bypass_actors`, `conditions`, `rules`). The `develop` payload is `develop.json` (`release` repos) or `operational/develop.json` (`operational` repos); the hub keeps both, a carried copy only its own model's (see "Downstream Carry"). These are the canonical expected payload the hub's audit (`AUDIT.md`, hub-only) diffs each repo's live rulesets against. +- `operational/develop.json` - the `develop` ruleset for **operational** repos (registry `workflowModel: operational`): direct signed pushes, no PR gate. Present at the hub and in operational carries only - a carried `release` repo does not have it. See "Rulesets" below. - `configure.sh` - applies the rulesets to a repository via the GitHub API (create or full-payload update, idempotent). Run `repo-config/configure.sh [owner/repo] [release|operational]`; the model defaults to the registry `workflowModel` lookup. +## Downstream Carry + +Every fleet repo carries this directory; the hub keeps the canonical copy. Rules for the carried copy: + +- **Carry only your model's `develop` variant.** A `release` repo carries `develop.json`; an `operational` repo carries `operational/develop.json` instead. `main.json` and `settings.json` are shared by both models. `configure.sh` aborts when the payload its model needs is missing rather than applying a partial configuration. +- **Hub-only references stay plain text.** The hub is a private repo: never URL-link it from a downstream repo - the link 404s for anyone without hub access. Files that exist only at the hub (`AUDIT.md`, `spec/`) are mentioned by name, not linked; links into files every repo carries (`AGENTS.md`) resolve everywhere and are fine. +- **The regen snippet targets the current repo**, so it works unchanged in a carried copy. + ## Rulesets Two workflow models share `main.json` but differ on `develop` (registry `workflowModel`, default `release`): @@ -17,20 +25,24 @@ Two workflow models share `main.json` but differ on `develop` (registry `workflo **Configure by importing these JSON files, never by hand-building the rules** (hand reconstruction has gone wrong on past setups). The result must be **exactly two rulesets named `develop` and `main`** - the names are load-bearing (`AGENTS.md` and the workflows reference them); only the `develop` *content* varies by model. First remove all legacy classic branch-protection rules and any stray rulesets, then run `configure.sh` (which picks the `develop` payload from the repo's `workflowModel`), or `gh api -X POST repos///rulesets --input repo-config/.json` per file (operational repos use `operational/develop.json` for `develop`). `gh ruleset` is read-only; creation goes through `gh api`. The required check binds by name and only turns green after the repo's PR workflow runs once. To edit a ruleset, GET it, change the field, and PUT the whole writable subset back (a partial PUT `422`s). -To change the canonical rulesets, edit the live rulesets here, then regenerate the committed files: +To change the canonical rulesets, edit the live rulesets (fleet-wide changes happen at the hub), then regenerate the committed files from the current repo: ```sh +repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" for name in develop main; do - id=$(gh api repos/ptr727/ProjectTemplate/rulesets --jq ".[] | select(.name==\"$name\") | .id") - gh api "repos/ptr727/ProjectTemplate/rulesets/$id" \ + out="repo-config/$name.json" + # An operational carry keeps its develop payload at operational/develop.json (develop.json is absent). + [ "$name" = "develop" ] && [ ! -e "$out" ] && out="repo-config/operational/develop.json" + id=$(gh api "repos/$repo/rulesets" --jq ".[] | select(.name==\"$name\") | .id") + gh api "repos/$repo/rulesets/$id" \ --jq '{name, target, enforcement, bypass_actors, conditions, rules}' \ - | jq -S --indent 4 '.' > "repo-config/$name.json" + | jq -S --indent 4 '.' > "$out" done ``` ## Secrets -Publish credentials required per mechanism are enumerated in [spec/secrets.json][secrets]. NuGet and PyPI use keyless OIDC Trusted Publishing (no stored key; the publish job needs `id-token: write`, and PyPI additionally an `environment: pypi` gate). Docker Hub has no OIDC equivalent and uses a stored `DOCKER_HUB_USERNAME` + `DOCKER_HUB_ACCESS_TOKEN` in both the Actions and Dependabot secret stores. Codegen and merge-bot repos add a GitHub App (`CODEGEN_APP_CLIENT_ID` + `CODEGEN_APP_PRIVATE_KEY` in both stores; the app must be installed, not just created). App-token call sites use `client-id`, never the deprecated `app-id`. +Publish credentials required per mechanism are enumerated in the hub's `spec/secrets.json` (hub-only). A repo needs only the mechanisms its own publish targets use - a source-only repo needs none of the publish credentials below. NuGet and PyPI use keyless OIDC Trusted Publishing (no stored key; the publish job needs `id-token: write`, and PyPI additionally an `environment: pypi` gate). Docker Hub has no OIDC equivalent and uses a stored `DOCKER_HUB_USERNAME` + `DOCKER_HUB_ACCESS_TOKEN` in both the Actions and Dependabot secret stores. Codegen and merge-bot repos add a GitHub App (`CODEGEN_APP_CLIENT_ID` + `CODEGEN_APP_PRIVATE_KEY` in both stores; the app must be installed, not just created). App-token call sites use `client-id`, never the deprecated `app-id`. ## Repo Settings @@ -49,8 +61,6 @@ The fleet-standard general settings live in [`settings.json`][settings-json] and -[settings-json]: ./settings.json [agents-branching-model]: ../AGENTS.md#branching-model [agents-git-and-commit-rules]: ../AGENTS.md#git-and-commit-rules -[audit]: ../AUDIT.md -[secrets]: ../spec/secrets.json +[settings-json]: ./settings.json diff --git a/repo-config/configure.sh b/repo-config/configure.sh index 3e21767c..6e2b228c 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -62,11 +62,18 @@ if [ -e "$settings_file" ]; then fi # ----- Branch rulesets ----- -# main.json is shared; the develop ruleset was selected by workflow model above. +# main.json is shared; the develop ruleset was selected by workflow model above. A missing or nameless +# payload aborts - silently skipping it would report success on a partially-applied configuration. for file in "$develop_ruleset" "$script_dir/main.json"; do - [ -e "$file" ] || continue + if [ ! -e "$file" ]; then + echo "Ruleset payload $file not found; aborting to avoid a partially-applied configuration." >&2 + exit 1 + fi ruleset_name="$(jq -r '.name // empty' "$file")" - [ -n "$ruleset_name" ] || continue + if [ -z "$ruleset_name" ]; then + echo "Ruleset payload $file has no name; aborting to avoid a partially-applied configuration." >&2 + exit 1 + fi # Paginate so a name match on a later page is never missed (which would create a duplicate ruleset), and # fail loudly if the API call itself fails (auth/404/network) rather than treating it as "not found". if ! ids="$(gh api --paginate "repos/$repo/rulesets" --jq ".[] | select(.name==\"$ruleset_name\") | .id")"; then diff --git a/reports/conformance-matrix.md b/reports/conformance-matrix.md new file mode 100644 index 00000000..0e1804db --- /dev/null +++ b/reports/conformance-matrix.md @@ -0,0 +1,40 @@ +# Conformance Matrix + +Tracks, per supported repo **shape** - the project type(s) plus the workflow model (`operational` is a `workflowModel` overlay, not a `spec/project-types.json` type) - whether a **context-free agent stands it up cold** from the hub docs to an audit-passing state ([`STANDUP.md`][standup] "Self-Test"), and the date (`YYYY-MM-DD`; `-` = not yet audited) of the shape's most recent audit ([`AUDIT.md`][audit]). A shape that cannot be stood up cold is a documentation defect, not an agent failure - iterate the docs until it can. + +`Cold-standup` values: `passing` (a fresh agent reaches operational), `gaps` (reaches partial; the note records the first doc gap), `not-tested` (self-test not yet run for this shape). + +The primary shapes are stood up as whole repos; the **composable targets** (`nuget`, `pypi`, `docker`) layer a publish leaf onto a base repo and are exercised as part of a base shape's standup, not alone. + +## Primary Shapes + +| Shape | Reference repo | Cold-standup | Last audited | First gap / notes | +|---|---|---|---|---| +| `python` + `source-only` | Financial-Modeling | not-tested | - | Reference for the source-release (dispatch-only) profile; the downstream standup issue is open. | +| `csharp` + `console` | - | not-tested | - | | +| `csharp` + `docker` | - | not-tested | - | | +| `homeassistant` | - | not-tested | - | Standalone-config conventions (home-assistant/core); scored by the `ha.*` checks. | +| `eda` | - | not-tested | - | Data-zip release, pull consumer. | +| `upstream-wrapper` | - | not-tested | - | Tag from a committed state file, not SemVer2. | +| `codegen` | - | not-tested | - | Deterministic matrix over both branches. | +| `docs` | ProjectTemplate | not-tested | - | Governance hub; CI is lint-only. | +| `operational` config | - | not-tested | - | `workflowModel: operational`; direct signed commits to `develop`, promotion-PR gate. | + +## Composable Targets + +| Target | Exercised via | Cold-standup | Notes | +|---|---|---|---| +| `nuget` | a `csharp` library base | not-tested | OIDC Trusted Publishing; no stored key. | +| `pypi` | a `python` library base | not-tested | OIDC; `environment: pypi`, `skip-existing: true`. | +| `docker` | any base with a Dockerfile | not-tested | Registry layer cache; always re-push. | + +## Updating a Row + +1. Run the [`STANDUP.md`][standup] self-test for the shape (fresh agent, docs only). +2. Run [`AUDIT.md`][audit] against the result; set `Cold-standup` and `Last audited`. +3. If the result is not `passing`, record the first doc gap and fix it in the hub (docs or manifests), then re-run. + + + +[audit]: ../AUDIT.md +[standup]: ../STANDUP.md