mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 05:25:14 +09:00
docs(agents): add downstream repository checklist to the PR template (#3298)
## What this PR does Cozystack is upstream for repositories that nothing keeps in sync with it, and no CI job compares the two sides, so a change here can break them in silence. It has already happened: `packages/apps/opensearch` ships but has no reference page on the website, the Terraform provider still offers a Kubernetes version this repo removed, and the plugin repo still sends operators to a `hack/` script that was deleted. This adds a **Downstream Repositories** trigger map to `docs/agents/contributing.md` — per repository, which change here forces a change over there and which file to touch — and a checklist in the PR template that links to it. The template addresses AI agents directly, since they author a large share of PRs, and tells them to open the follow-up rather than tick a box that claims work nobody did. A unit test (`hack/downstream-trigger-map.bats`) keeps the map from rotting: it pins every in-repo path the map cites to a file that exists, pins the map and the checklist to the same repository list, and pins the CI carve-out that keeps the test running when the map is edited alone. Its header is explicit about what it cannot check — whether a coupling is real, and whether it still holds in the other repo, stays a human's job. ### Downstream repositories - [x] No downstream repository is affected by this change ### Release note ```release-note NONE ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Expanded contributor guidance for downstream repository changes and follow-up links. * Clarified how to preserve PR template checklists when creating pull requests. * Added detailed downstream repository trigger-map instructions. * **CI** * Documentation changes affecting the trigger map now run unit and controller checks. * **Tests** * Added validation to ensure trigger-map paths, workflow wiring, and repository lists remain consistent. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
4 files changed
+370
-3
No files matched your search
@@ -19,6 +19,33 @@
|
||||
<!-- REQUIRED for UI changes: attach screenshots or screen recordings demonstrating
|
||||
the visual impact of your changes. PRs with UI changes without screenshots will not be merged. -->
|
||||
|
||||
### Downstream repositories
|
||||
|
||||
<!--
|
||||
Cozystack is upstream for repositories that are NOT kept in sync with it automatically, and nothing in CI compares the two sides, so a change here can break them silently.
|
||||
|
||||
Which change forces what, and which file to touch, is the trigger map in docs/agents/contributing.md under "Downstream Repositories". Walk it before you tick anything.
|
||||
|
||||
IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM TO FILL IN:
|
||||
|
||||
1. Walk the trigger map against the actual diff of this PR, file by file. Do not decide from the PR title or from your memory of what you changed.
|
||||
2. For every repository you tick, open the follow-up PR there and link it on the same line. A ticked box with no link claims work that does not exist, which is worse than an empty box. Search that repository for an open PR or issue covering it first, and link that instead of filing a duplicate.
|
||||
3. When the follow-up is out of scope, or needs a decision that is not yours to make, open an issue in that repository instead, link that, and say so in the PR body. Do not push a speculative PR into another repository just to fill a line here.
|
||||
4. Do not tick "No downstream repository is affected" to make the checklist go away. If you are unsure, leave every box empty and say so in the PR body, so a human decides.
|
||||
-->
|
||||
|
||||
- [ ] No downstream repository is affected by this change
|
||||
- [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up:
|
||||
- [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up:
|
||||
- [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up:
|
||||
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
|
||||
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
|
||||
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up:
|
||||
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up:
|
||||
- [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up:
|
||||
- [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up:
|
||||
- [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up:
|
||||
|
||||
### Release note
|
||||
|
||||
<!-- Write a release note:
|
||||
@@ -29,4 +56,4 @@ the visual impact of your changes. PRs with UI changes without screenshots will
|
||||
|
||||
```release-note
|
||||
|
||||
```
|
||||
```
|
||||
@@ -29,6 +29,7 @@ jobs:
|
||||
contents: read
|
||||
outputs:
|
||||
code: ${{ steps.p.outputs.code }}
|
||||
trigger_map: ${{ steps.p.outputs.trigger_map }}
|
||||
matrix: ${{ steps.p.outputs.matrix }}
|
||||
any: ${{ steps.p.outputs.any }}
|
||||
touched: ${{ steps.p.outputs.touched }}
|
||||
@@ -48,6 +49,23 @@ jobs:
|
||||
else
|
||||
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
# docs/agents/contributing.md carries the downstream trigger map, which
|
||||
# hack/downstream-trigger-map.bats asserts against: every path the map
|
||||
# cites still exists, and its repository list still matches the PR
|
||||
# template. Editing the map alone is a docs-only PR, which would skip the
|
||||
# unit tests and exempt the map from its own guard. This flag lets the
|
||||
# unit-test job opt back in, and only that job: it deliberately does not
|
||||
# feed `code`, so a docs PR still builds nothing.
|
||||
#
|
||||
# Detect the map by its old path too: a rename is reported as the new path
|
||||
# alone, so a PR that moves the map would skip the very test that pins the
|
||||
# literal below to it, and break for whoever pushes next.
|
||||
git diff --name-only --no-renames "origin/${BASE_REF}...HEAD" > /tmp/changed_norenames.txt
|
||||
if grep -qxF 'docs/agents/contributing.md' /tmp/changed_norenames.txt; then
|
||||
echo "trigger_map=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "trigger_map=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
matrix="$(hack/build-matrix.sh /tmp/changed.txt)"
|
||||
echo "Build matrix: $matrix"
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
@@ -75,8 +93,11 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: ["plan"]
|
||||
# trigger_map opts a docs-only PR back in when it edits the downstream trigger
|
||||
# map, so hack/downstream-trigger-map.bats still guards it. No other job reads
|
||||
# that flag: a docs PR runs the unit tests and nothing heavier.
|
||||
if: |
|
||||
needs.plan.outputs.code == 'true'
|
||||
(needs.plan.outputs.code == 'true' || needs.plan.outputs.trigger_map == 'true')
|
||||
&& !contains(github.event.pull_request.labels.*.name, 'release')
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
+123
-1
@@ -9,6 +9,7 @@ Project-side conventions for commits, branches, and pull requests in Cozystack.
|
||||
- [ ] Branch is rebased on `upstream/main` (no extra commits)
|
||||
- [ ] PR body includes description and release note
|
||||
- [ ] Ran `make generate` in every package whose `values.yaml`, `values.schema.json`, `Chart.yaml`, or `README.md` was touched, and committed the regenerated files
|
||||
- [ ] Walked the [Downstream Repositories](#downstream-repositories) trigger map against the diff, and linked a follow-up (a PR, or an issue when the PR is out of scope) in every repository the change reaches
|
||||
|
||||
## Regenerate Artifacts Before Committing
|
||||
|
||||
@@ -135,10 +136,131 @@ git push -f origin my-feature
|
||||
|
||||
## Pull Request Body
|
||||
|
||||
Fill in the template at [`.github/PULL_REQUEST_TEMPLATE.md`](../../.github/PULL_REQUEST_TEMPLATE.md). It includes the required `release-note` block.
|
||||
Fill in the template at [`.github/PULL_REQUEST_TEMPLATE.md`](../../.github/PULL_REQUEST_TEMPLATE.md). It includes the required `release-note` block and the [Downstream Repositories](#downstream-repositories) checklist.
|
||||
|
||||
Create the PR with `gh pr create --title "type(scope): brief description" --body-file <file>`.
|
||||
|
||||
`--body` and `--body-file` replace the body wholesale, so the template is not applied and its checklists are silently dropped. Start the body file from the template instead:
|
||||
|
||||
```bash
|
||||
cp .github/PULL_REQUEST_TEMPLATE.md /tmp/pr-body.md
|
||||
# fill in /tmp/pr-body.md, then:
|
||||
gh pr create --draft --title "type(scope): brief description" --body-file /tmp/pr-body.md
|
||||
```
|
||||
|
||||
## Downstream Repositories
|
||||
|
||||
Cozystack is upstream for repositories that are **not** kept in sync with it automatically. A change here can break them silently, because nothing in CI compares the two sides.
|
||||
|
||||
The failure is structural, not hypothetical. The website's docs generator works from an app list hardcoded in its own `Makefile`, and a package that nobody adds to that list is simply skipped, in silence. `packages/apps/opensearch` ships today with a `README.md` and a `values.schema.json` and has no reference page on the site at all; `packages/apps/bucket` has only a hand-written page, which no generator keeps in step with its values. Nobody broke either of them — the coupling just had no owner.
|
||||
|
||||
Walk the trigger map below against the **diff**, not against the PR title, and tick the matching boxes in the PR template. Tick a repository only after opening the follow-up PR there and linking it: a ticked box with no link claims work that does not exist. Search the target repository for an open PR or issue covering it first, and link that rather than filing a duplicate. When the follow-up is genuinely out of scope, or when it needs a decision that is not yours to make, open an issue there instead, link that, and say so in the PR body.
|
||||
|
||||
### What is already automated
|
||||
|
||||
Two narrow paths, and neither of them runs on your PR:
|
||||
|
||||
- **`website`** — on a stable tag a bot opens a docs PR that regenerates the managed-apps reference pages from each package's `README.md`, for the apps hardcoded in the website `Makefile`. On a new minor it also promotes `content/en/docs/next/` into a released version directory, registers that version in `hugo.yaml`, and snapshots `data/versions/next.yaml` into `data/versions/vX.Y.yaml`. That is the whole of it. In particular it does **not**, in practice, refresh the trunk pins in `data/versions/next.yaml`, the Talos version among them. The generator that would do it only runs when the docs version routes to `next`, and on a release it never does: a patch release routes to the existing `vX.Y/`, and on a new minor the promotion step creates `vX.Y/` before the generator runs, so it routes there instead. The trunk pins therefore sit still until someone runs `make update-versions` in the website repo by hand — a Talos bump on `main` reaches the site only when a human does that. Released `data/versions/vX.Y.yaml` files are frozen at cut time and a patch release does not revisit them.
|
||||
- **`ansible-cozystack`** — Renovate, on its own schedule rather than on a release, bumps the `cozy-installer` chart version and the k3s version. It carries nothing structural: no values key, no task, no playbook.
|
||||
|
||||
Everything else is manual and unguarded, bar the odd version pin Renovate carries for a satellite. The website keeps one copy of each page per documentation version, so a docs follow-up normally lands in `content/en/docs/next/` and is backported only if it matters for a released version.
|
||||
|
||||
### cozystack/website
|
||||
|
||||
- Add, rename or remove a package under `packages/apps/` or `packages/extra/` → the app lists in the website `Makefile` (`APPS`, `VMS`, `NETWORKING`, `K8S`, `SERVICES`) are hardcoded, and a package missing from them gets no reference page at all, with nothing reporting the gap.
|
||||
- Change `packages/core/platform/values.yaml` → `content/en/docs/next/operations/configuration/platform-package.md` is a hand-written table of the `spec.components.platform.values.*` keys, and it has already drifted. The platform chart has no `values.schema.json`, so nothing on this path is generated.
|
||||
- Add, rename or recompose a platform variant or bundle → `content/en/docs/next/operations/configuration/variants.md`, `install/cozystack/kubernetes-distribution.md`, `install/ansible.md`.
|
||||
- Add or remove a platform component → `content/en/docs/next/guides/platform-stack/_index.md`, `operations/configuration/licenses.md`.
|
||||
- Rename a release asset in `hack/upload-assets.sh` → the install pages hardcode asset filenames: `content/en/docs/next/install/kubernetes/generic.md`, `install/talos/iso.md`, `install/providers/*.md`.
|
||||
- Change `ApplicationDefinition` semantics → `content/en/docs/next/cozystack-api/application-definitions.md`.
|
||||
- Bump the Talos version in `packages/core/talos/images/talos/profiles/installer.yaml` → the website's trunk pins in `data/versions/next.yaml`, which no CI job refreshes. Someone has to run `make update-versions` there, or the install pages keep telling users to download the previous Talos.
|
||||
- Change developer tooling (`cozyvalues-gen`, `cozypkg`, the package `Makefile`s) → `content/en/docs/next/development.md`.
|
||||
- Make a documented workaround obsolete → grep the docs for it and delete it, because users keep following stale workarounds.
|
||||
|
||||
### cozystack/terraform-provider-cozystack
|
||||
|
||||
The provider is hand-written: every Kind carries a hand-maintained Terraform schema and its own expand/flatten pair, with no codegen from `values.schema.json`. A test guard there catches new top-level spec fields, but only once someone bumps its `api/apps/v1alpha1` pin, and it sees neither nested fields nor enums nor defaults.
|
||||
|
||||
- Add an app under `packages/apps/` → a new resource and data source, written from scratch.
|
||||
- Add, remove or rename a field in an app's `values.schema.json` → the schema, the model, and the expand/flatten pair.
|
||||
- Change a version enum (postgres, kubernetes, mariadb, mongodb, redis, rabbitmq, opensearch) → the provider's `stringvalidator.OneOf` lists. Drift means Terraform rejects a version the API accepts, or accepts one it will refuse.
|
||||
- Change a default in an app's `values.yaml` → the provider restates the defaults of every field it models, as `Default: stringdefault.StaticString(...)` and friends on an `Optional`+`Computed` attribute. That means the provider *sends* its own default rather than letting the API apply yours, so a stale one does not merely show up as a diff: it silently wins, and users keep getting the old value.
|
||||
- Change `kind` or `plural` in an `ApplicationDefinition` → the provider hardcodes both in the client descriptor it builds each GVR from. This one at least fails loudly: a write errors out, and a read stops matching and drops the resource from state.
|
||||
- Change `release.prefix`, or rename an output Secret or Service → the provider derives those names by concatenating the prefix (`"postgres-" + name`, then `-rw` and `-ro`) and looks them up as literal strings. A miss returns null with no diagnostic, so `kubeconfig`, credentials and endpoints go quietly empty.
|
||||
- Change a CRD the provider types by hand — `Package` under `cozystack.io`, `Plan` and `RestoreJob` under `backups.cozystack.io` → those are schema and model changes over there. Its tests also carry hand-vendored copies of those field names, because importing this module is too heavy, so the copies have to move too.
|
||||
- Change any other `cozystack.io` or `backups.cozystack.io` CRD → their specs travel as opaque JSON, so the provider keeps working but silently offers no typed access to whatever you added.
|
||||
|
||||
### cozystack/ansible-cozystack
|
||||
|
||||
- Add a required key to `packages/core/installer/values.yaml`, or rename one the role passes (`cozystackOperator.variant`, `cozystack.apiServerHost`, `cozystack.apiServerPort`) → the chart values block in the role's `main.yml` task, and its `defaults/main.yml`.
|
||||
- Change the `cozy-system` namespace or the PSA pre-install hook contract → the role carries namespace-adoption logic written against the current behaviour of `packages/core/installer/templates/`.
|
||||
- Add or rename a platform variant → the variant list is hardcoded in Go, in `cmd/cozystack-operator/main.go`; over there it is `cozystack_platform_variant` in `defaults/main.yml` and the README table.
|
||||
- Change a `packages/core/platform/values.yaml` key the role sets — `networking.podCIDR`, `networking.podGateway`, `networking.serviceCIDR`, `networking.joinCIDR`, `publishing.host`, `publishing.apiServerEndpoint`, `publishing.externalIPs` → the role's platform-package template. The `externalIPs` to `exposureClass` migration in particular needs a coordinated PR there.
|
||||
- Rename an object the role waits on — `deployment/cozystack-operator`, `crd/packages.cozystack.io`, the root `Tenant` — or change the `<packagesource>.<package>` naming scheme of the Package it applies → the role's tasks and its own CI assertions.
|
||||
- Change node prerequisites in `hack/e2e-prepare-cluster.bats` — the kernel modules a node must load, the containerd settings, the LVM `global_filter` → those are mirrored by hand into that repo's `examples/ubuntu`, `examples/rhel` and `examples/suse` prepare playbooks. Its CI tests some of them, but never against this repo, so a kernel module added here and not there surfaces as a broken storage layer on a generic cluster.
|
||||
- Change what Cozystack requires from the Kubernetes distribution (CNI, kube-proxy, cluster domain, max pods) → the k3s flags, which live only in that repo's inventories.
|
||||
|
||||
### cozystack/ccp
|
||||
|
||||
Its CI validates internal consistency only, never against this repo.
|
||||
|
||||
- Move or rename anything under `hack/`, or change what a make target does → its skills gate on `hack/package.mk` and `hack/common-envs.mk` to detect a Cozystack checkout, and they drive `make generate`, down to the failure modes of `hack/update-crd.sh` underneath it. A skill that cannot find its anchor file refuses to run.
|
||||
- Change the `packages/apps`, `packages/system`, `packages/extra`, `packages/core` layout → package resolution in its skills is hardcoded to that split.
|
||||
- Rename `packages/system/<name>-rd/cozyrds/<name>.yaml` → it is the declared source of truth for dependency contracts in its external-app skill.
|
||||
- Rename a namespace: `cozy-system`, `cozy-installer`, `tenant-root`.
|
||||
- Change variant or bundle names → its variant-picker reference tables.
|
||||
- Change release-prep behaviour in `.github/workflows/tags.yaml` → its contributor-versus-maintainer guidance rests on it.
|
||||
|
||||
### cozystack/external-apps-example
|
||||
|
||||
It has no CI at all, it vendors copies of this repo's tooling, and it is the reference every third-party catalogue is built from, so it breaks quietly and takes them with it.
|
||||
|
||||
- Change `hack/package.mk` → its `scripts/package.mk` is a byte-for-byte copy.
|
||||
- Change `hack/update-crd.sh` → its `scripts/update-appdef.sh` is a miniature of it.
|
||||
- Change the `ApplicationDefinition` CRD, above all the `release.chartRef.kind` enum in `packages/system/application-definition-crd/definition/` → it still uses `chartRef.kind: HelmChart` while this repo has moved on to `ExternalArtifact`. Dropping `HelmChart` from the enum kills it outright.
|
||||
- Change the `release.labels` convention, for example the `flux-shard-operator` sharding key.
|
||||
- Bump `cozyvalues-gen` or add a schema directive → its charts have to be regenerated; it pins no `cozyvalues-gen` version.
|
||||
- Rename `cozy-public` or `cozy-system`, or move Flux to a new API version.
|
||||
- Change how external catalogues register themselves → it is still on the `GitRepository` to `HelmChart` to `HelmRelease` path rather than `PackageSource`, so moving that contract rewrites the example instead of patching it.
|
||||
|
||||
### cozystack/talm
|
||||
|
||||
It ships `charts/cozystack/`, the preset the documented Talos bootstrap applies, and that preset restates this repo's node contract.
|
||||
|
||||
- Change node prerequisites in `hack/e2e-prepare-cluster.bats` — the kernel modules, the LVM `global_filter`, the cluster domain → its `charts/cozystack/` restates them. This is the same contract the Ansible collection mirrors, so a change here usually needs a PR in both. Its copy overlaps ours but is not identical: it loads the vfio modules for PCI passthrough on top of ours, and templates the cluster domain instead of hardcoding it. Diff the two and port your change; do not paste our block over theirs.
|
||||
|
||||
### cozystack/cozyhr
|
||||
|
||||
- Rename the `cozyhr.cozystack.io/values-files` annotation, or change what the operator writes into it (`internal/operator/package_reconciler.go`) → it reads that annotation to decide which values files to merge. Its fallback is the HelmRelease's own `valuesFiles`, which is empty for an ExternalArtifact-backed release, so a rename does not fail: it silently merges the wrong values.
|
||||
- Add or change a chart source kind (`HelmChart`, `ExternalArtifact`, `OCIRepository`) → it switches on the kind to resolve the chart, so a new one is simply unsupported until it learns about it.
|
||||
- Change what `hack/package.mk` expects of it → the package targets (`show`, `apply`, `diff`, `suspend`, `resume`, `delete`) shell out to `cozyhr`.
|
||||
|
||||
### cozystack/cozy-proxy
|
||||
|
||||
Its chart is vendored here — `packages/system/cozy-proxy/Makefile` pulls it from the latest upstream tag — so a chart fix belongs in that repository and must be re-vendored: an edit to the vendored copy is wiped by the next `make update`. The runtime contract, though, is produced here and consumed there.
|
||||
|
||||
- Rename the `service.kubernetes.io/service-proxy-name` label value, or the `networking.cozystack.io/wholeIP` and `networking.cozystack.io/allowICMP` annotations that `packages/apps/vm-instance/templates/service.yaml` stamps → it matches all three as literal strings, and gates every service it manages on the first. Rename one and it quietly stops handling external VM services: no error, no crash.
|
||||
|
||||
### cozystack/cozystack-telemetry-server
|
||||
|
||||
- Rename a telemetry metric or one of its labels — `cozy_cluster_info{cozystack_version,kubernetes_version}` in `internal/telemetry/operator_collector.go`, `cozy_application_count{kind}` in `internal/telemetry/collector.go` → it queries those names in its Grafana dashboard and its overview. A rename does not error anywhere: the panels just go empty.
|
||||
|
||||
### cozystack/examples
|
||||
|
||||
Not coupled to the API: it holds Terraform that provisions bare nodes, with no Cozystack manifests in it. The only thing that reaches it is a change to the minimum node requirements (count, CPU, RAM, disk) or to the network requirements (VLAN, underlay).
|
||||
|
||||
### Not a downstream repository
|
||||
|
||||
`cozystack-ui` is archived. The console was vendored into `packages/system/dashboard/images/console`, so a UI change belongs in this repo, in the same PR. Note that the console still hardcodes the marketplace category list, so an `ApplicationDefinition` with a brand-new `spec.dashboard.category` renders in the marketplace but gets no sidebar entry.
|
||||
|
||||
`boot-to-talos` converts a running OS to Talos. It pins the Talos image this repo publishes, but Renovate carries that tag for it, so nothing you change here forces a change there. Renaming the image path would, and nothing would catch it.
|
||||
|
||||
`homebrew-tap` builds `cmd/cozypkg` out of this repo, but its formula is deprecated in favour of homebrew-core and pinned to a tag, so only a `--HEAD` install tracks `main`. Moving that command would break it, quietly and for almost nobody.
|
||||
|
||||
The rest of the organisation's repositories do not restate anything this one owns: they are either consumed by it or independent of it, so a change here does not reach them.
|
||||
|
||||
The release process keeps a **separate** list of repositories, in [`changelog.md`](./changelog.md), checked for tags cut during a release window. That list exists to assemble release notes; this one exists to catch code coupling. They overlap but are not interchangeable, and neither replaces the other.
|
||||
|
||||
## Review Expectations
|
||||
|
||||
- A bug-fix PR should include a behavioural regression test, not just a single field assertion — prove the bug can no longer recur.
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env bats
|
||||
# Guards the "Downstream Repositories" trigger map in docs/agents/contributing.md.
|
||||
#
|
||||
# The map tells a contributor which change in this repo forces a follow-up in a
|
||||
# satellite repository. Each trigger names a file on this side: hack/update-crd.sh,
|
||||
# packages/core/platform/values.yaml, and so on. Rename one of those and the map
|
||||
# keeps confidently pointing at a path that no longer exists, from inside the PR
|
||||
# template, to every reader. The repository list is deliberately not restated here
|
||||
# — it would be one more copy to drift; the map and the PR template are the two
|
||||
# sources, and the tests below hold them to each other.
|
||||
#
|
||||
# The three tests pin, in order: every in-repo path the map cites to a file that
|
||||
# exists; the workflow carve-out that keeps this suite running on a docs-only PR
|
||||
# to the map's own location; and the map and the PR-template checklist to the same
|
||||
# repository list, so a repository cannot be listed in one and forgotten in the
|
||||
# other.
|
||||
#
|
||||
# WHAT THIS DOES NOT CHECK, so nobody mistakes a green tick for a correct map:
|
||||
#
|
||||
# * Whether a trigger is TRUE. "Change X here and Y breaks over there" is a
|
||||
# claim about a satellite repo. Verifying it needs that repo checked out at
|
||||
# a known ref, which this suite deliberately does not do: a unit test must
|
||||
# not depend on the network or on six sibling clones. A trigger can cite a
|
||||
# path that exists here and still describe a coupling that is nonsense.
|
||||
# * Anything on the satellite side. A file named there (its Makefile, its
|
||||
# scripts/, its playbooks) is not reachable from this repo and is not
|
||||
# validated. Those names are exactly where the map is most likely to rot.
|
||||
# * Whether the map is COMPLETE. Deleting a whole trigger, or a whole block of
|
||||
# them, passes: the repository-list test compares section headings, not bodies.
|
||||
# * A path written without backticks. Extraction keys off `code spans`, which
|
||||
# is the section's convention; a lone unquoted path slips through. Stripping
|
||||
# the backticks wholesale does fail, via the anti-vacuum guard below.
|
||||
#
|
||||
# In short: this catches a rename on THIS side, which is the cheap half. The
|
||||
# expensive half — is the coupling real, and is it still real over there — stays
|
||||
# a human's job, and reviewers should not assume CI did it for them.
|
||||
#
|
||||
# A docs-only PR normally skips the unit tests, which would have exempted the map
|
||||
# from its own guard; the plan step in .github/workflows/pull-requests.yaml carves
|
||||
# out docs/agents/contributing.md so that editing the map alone still runs these.
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
|
||||
MAP_FILE="$REPO_ROOT/docs/agents/contributing.md"
|
||||
TEMPLATE_FILE="$REPO_ROOT/.github/PULL_REQUEST_TEMPLATE.md"
|
||||
|
||||
# Body of the "## Downstream Repositories" section, up to the next h2.
|
||||
map_section() {
|
||||
awk '/^## Downstream Repositories$/ { inside = 1; next }
|
||||
/^## / { inside = 0 }
|
||||
inside' "$MAP_FILE"
|
||||
}
|
||||
|
||||
@test "every in-repo path cited by the downstream trigger map exists" {
|
||||
[ -f "$MAP_FILE" ] || { echo "missing $MAP_FILE" >&2; exit 1; }
|
||||
|
||||
section="$(map_section)"
|
||||
[ -n "$section" ] || { echo "Could not locate the '## Downstream Repositories' section in $MAP_FILE" >&2; exit 1; }
|
||||
|
||||
# Backticked tokens rooted at a top-level directory of THIS repo. Satellite
|
||||
# paths cited by the map (scripts/package.mk, content/en/docs/...) do not match
|
||||
# these roots and so fall out. Note this is a heuristic, not a guarantee: a
|
||||
# satellite repo has its own packages/ tree, so a backticked path from over
|
||||
# there that happens to share a root would be checked against this tree.
|
||||
# A trailing slash is prose, not a distinct path: `packages/apps/` and
|
||||
# `packages/apps` are the same anchor and would otherwise both inflate the count.
|
||||
paths="$(printf '%s\n' "$section" \
|
||||
| grep -o '`[^`]*`' \
|
||||
| tr -d '`' \
|
||||
| grep -E '^(hack|packages|cmd|api|internal|pkg|\.github)/' \
|
||||
| sed 's#/$##' \
|
||||
| sort -u)"
|
||||
|
||||
# Guard against a vacuous pass: if the extraction breaks, or the section is
|
||||
# reworded so it no longer cites paths, this test must fail loudly rather than
|
||||
# silently verify nothing. The floor sits well under the real count (28 as of
|
||||
# writing) because the map legitimately shrinks too, and a red build should not
|
||||
# send the reader hunting for a bug that is really a deleted section.
|
||||
count="$(printf '%s\n' "$paths" | grep -c . || true)"
|
||||
[ "$count" -ge 20 ] || {
|
||||
echo "Extracted only $count in-repo path(s) from the trigger map; expected at least 20." >&2
|
||||
echo "Either the extraction broke and this test is verifying nothing, or the map genuinely" >&2
|
||||
echo "shrank - if a downstream repository went away, lower this floor deliberately." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
missing=""
|
||||
for path in $paths; do
|
||||
# A placeholder segment (<name>) stands for any package, so match it as a glob.
|
||||
pattern="$(printf '%s' "$path" | sed 's/<[^>]*>/*/g')"
|
||||
# Unquoted on purpose: the shell must expand the glob. No match => ls fails.
|
||||
if ! (cd "$REPO_ROOT" && ls -d $pattern) >/dev/null 2>&1; then
|
||||
missing="$missing $path"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$missing" ]; then
|
||||
echo "The downstream trigger map in docs/agents/contributing.md cites paths that do not exist:" >&2
|
||||
for path in $missing; do echo " - $path" >&2; done
|
||||
echo "Fix: update the map to the new path, or drop the trigger if it no longer applies." >&2
|
||||
echo "The map is read by contributors from inside the PR template; a stale path there sends them to a dead end." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All $count in-repo path(s) cited by the trigger map exist"
|
||||
}
|
||||
|
||||
@test "the workflow opts the trigger map back into the unit tests" {
|
||||
WORKFLOW="$REPO_ROOT/.github/workflows/pull-requests.yaml"
|
||||
[ -f "$WORKFLOW" ] || { echo "missing $WORKFLOW" >&2; exit 1; }
|
||||
|
||||
# Every check below reads the workflow with comments stripped. A commented-out
|
||||
# line still contains its own text, so grepping the raw file would accept a
|
||||
# carve-out that had been disabled and left behind as a TODO — the most likely
|
||||
# way this actually rots.
|
||||
code_only="$(sed 's/#.*//' "$WORKFLOW")"
|
||||
|
||||
# The block of the job that runs the unit tests, from its header to the next
|
||||
# job's. The condition has to sit on THAT job: pinning it file-wide would accept
|
||||
# the exact expression pasted into some other job while the unit-test job quietly
|
||||
# loses it.
|
||||
unit_job="$(printf '%s\n' "$code_only" | awk '
|
||||
/^ [a-zA-Z0-9_-]+:[[:space:]]*$/ { inside = ($0 == " checks:") }
|
||||
inside')"
|
||||
printf '%s\n' "$unit_job" | grep -q 'make unit-tests' || {
|
||||
echo "The 'checks' job in .github/workflows/pull-requests.yaml no longer runs 'make unit-tests'." >&2
|
||||
echo "This suite pins the trigger-map carve-out to that job. If the unit tests moved, point" >&2
|
||||
echo "the checks below at their new job." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Three links carry the carve-out: the plan step names the map, the plan job
|
||||
# exports the flag, and the unit-test job gates on it. Cutting any one leaves the
|
||||
# other two looking perfectly wired, and none of them fails loudly — an unset or
|
||||
# unexported output dereferences to an empty string, so the condition is merely
|
||||
# false and the suite is skipped in silence, on exactly the PRs it guards, with
|
||||
# every test in this file still green. Pin all three.
|
||||
rel="${MAP_FILE#"$REPO_ROOT"/}"
|
||||
printf '%s\n' "$code_only" | grep -qF "'$rel'" || {
|
||||
echo "The plan step in .github/workflows/pull-requests.yaml does not name '$rel'." >&2
|
||||
echo "Without it, a PR that only edits the trigger map is treated as docs-only, the unit" >&2
|
||||
echo "tests are skipped, and this file never runs — exactly when it is needed most." >&2
|
||||
echo "Fix: point the trigger_map detection at the map's new path." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
printf '%s\n' "$code_only" | grep -qF 'trigger_map: ${{ steps.p.outputs.trigger_map }}' || {
|
||||
echo "The plan job in .github/workflows/pull-requests.yaml does not export trigger_map." >&2
|
||||
echo "The step still computes it and the unit-test job still reads it, so this looks wired" >&2
|
||||
echo "up, but an unexported output dereferences to an empty string: the job is skipped in" >&2
|
||||
echo "silence on exactly the PRs this suite guards." >&2
|
||||
echo "Fix: restore 'trigger_map: \${{ steps.p.outputs.trigger_map }}' to the plan job's outputs." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
condition="(needs.plan.outputs.code == 'true' || needs.plan.outputs.trigger_map == 'true')"
|
||||
printf '%s\n' "$unit_job" | grep -qF "$condition" || {
|
||||
echo "The job that runs 'make unit-tests' is not gated on:" >&2
|
||||
echo " $condition" >&2
|
||||
echo "Mentioning trigger_map in another job, or in a comment, does not count. Without that" >&2
|
||||
echo "exact condition on that job, a PR which only edits the trigger map stays docs-only," >&2
|
||||
echo "skips the unit tests, and never runs this file — while every test here still passes." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "The workflow names '$rel', exports the flag, and gates the unit tests on it"
|
||||
}
|
||||
|
||||
@test "the trigger map and the PR-template checklist list the same repositories" {
|
||||
[ -f "$TEMPLATE_FILE" ] || { echo "missing $TEMPLATE_FILE" >&2; exit 1; }
|
||||
|
||||
# Checklist lines look like: - [ ] [cozystack/website](https://...) - follow-up:
|
||||
template_repos="$(grep -oE '^- \[ \] \[cozystack/[a-z0-9.-]+\]' "$TEMPLATE_FILE" \
|
||||
| grep -oE 'cozystack/[a-z0-9.-]+' \
|
||||
| sort -u)"
|
||||
|
||||
# Map headings look like: ### cozystack/website
|
||||
map_repos="$(map_section \
|
||||
| grep -oE '^### cozystack/[a-z0-9.-]+' \
|
||||
| grep -oE 'cozystack/[a-z0-9.-]+' \
|
||||
| sort -u)"
|
||||
|
||||
template_count="$(printf '%s\n' "$template_repos" | grep -c . || true)"
|
||||
[ "$template_count" -ge 1 ] || { echo "Found no cozystack/* checklist entries in $TEMPLATE_FILE - extraction is broken" >&2; exit 1; }
|
||||
|
||||
if [ "$template_repos" != "$map_repos" ]; then
|
||||
echo "The PR-template checklist and the trigger map disagree on which repositories are downstream." >&2
|
||||
echo "In the checklist (.github/PULL_REQUEST_TEMPLATE.md):" >&2
|
||||
printf '%s\n' "$template_repos" | sed 's/^/ /' >&2
|
||||
echo "In the map (docs/agents/contributing.md, '### cozystack/<repo>' headings):" >&2
|
||||
printf '%s\n' "$map_repos" | sed 's/^/ /' >&2
|
||||
echo "Fix: add the repository to both, or remove it from both." >&2
|
||||
echo "A repository listed in only one of them gets ticked with no guidance, or documented with no box to tick." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checklist and trigger map agree on $template_count repositories"
|
||||
}
|
||||
Reference in new issue
Block a user