overview.md now requires a language on every code fence and a blank
line around every list and after every heading. It broke these rules
itself, and AGENTS.md broke the list and heading ones. Label the
directory-tree fences as text, separate headings and lead-in lines
from the lists under them, and wrap the one bare URL.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The routed documents cover processes; nothing told an agent how to
reason about a failure or what may leave the session. Agents tie a
red run to whatever changed last, trust remembered flags and
versions, run cluster-touching suites against whatever kubeconfig
context is current, and leak private cluster or tool names into
public text. State each rule once at the top level, where every
session reads it.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The overview.md route only triggered on structural phrasing such as
"where should I put X", so Go, Helm chart and build/test tasks reached
no documentation at all -- even though overview.md already covers them
under Go Code, Helm Charts, Values schema generation, Build and
Development Commands and Testing. Widen the trigger phrases to the work
agents actually get asked to do, and name the sections to land in.
Condense the seven activation rules to three. Rules 1 and 6 stated the
same scope constraint positively and negatively, and rules 3, 4 and 5
each said "follow the documentation exactly" in different words. No
instruction is dropped.
Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
A negated assertion is the natural way to spell "this must not
appear", and an absence is what nothing else in a test covers, so the
errexit exemption lands where it is least likely to be noticed and
invisible in a green run. Switching runners does not lift it: the ERR
trap the bats binary reports through carries the same exemption, so
neither runner sees a negated assertion that has any statement below
it.
Add a suite that scans every hack/**/*.bats, subdirectories included,
and fails on a statement whose first token is `!` -- naming the file,
the line the statement opens on, and the form to use instead.
Statement boundaries are the POSIX separators, each measured against
the runner before being admitted: `;` `&` `&&` `||` `{` `(`, the `)`
closing a case pattern, and the words `then` `do` `else`. A keyword may
legally be preceded only by whitespace or by `;` -- every other
separator in front of one is a shell syntax error, checked with `sh -n`
rather than recalled -- so the scan admits exactly those two and that
part of the class is closed rather than patched a spelling at a time.
Both spellings are vacuous under the runner, so missing either is a
silent miss, the one direction this scan must not err in. For the same
reason the scan takes a `!` with no space before its subshell,
`!(cmd)`, which is vacuous on dash and bash alike, and reads through a
`time` or `time -p` prefix, which is vacuous under bats and under
cozytest where /bin/sh is bash. A scan that wants `!` and then
whitespace as the first token misses both. On a dash /bin/sh `time` is
not a keyword and `time ! cmd` fails, so there it is an over-count.
Five over-counts remain, all erring loud, each named in the header:
they are enumerated rather than removed, because a removed error
leaves no trace while a named one tells the next reader what they are
looking at.
Inverted conditions are untouched because errexit is not what decides
a condition. `[ ! -f x ]` is untouched for a different reason and
stays a normal failing assertion: the `!` there is an operator of
`test`, not negation of a command, so nothing exempts its status.
Each claim the suite makes is pinned against removing the thing it
claims, and the separator class is measured branch by branch rather than
only at its newest entries. The recursion is exercised through the audit
function on a fixture in a subdirectory rather than through a second
copy of its find expression, because a copy passes while the function
loses its recursion -- and an audit that returns empty reads as a clean
tree. The condition-strip carries fixtures that put the negation after
an `&&` inside the condition, where only the strip can save it. The
continuation fold is pinned by a fixture whose negation sits on the
continued line, so folded and unfolded name different lines. Both
keyword spellings, the `time` prefix and `!(cmd)` have fixtures of
their own. The subshell over-count carries a fixture for both of its
halves -- that the scan reports the shape, and that the shape really
does fail.
Both halves of the premise are pinned, not just the half about the
banned form. A test asserts the runner still passes a bare negated
assertion; a second asserts the prescribed `if cmd; then ...; false; fi`
does fail, under both runners, and fails AT the form rather than
somewhere before it. Every converted absence rests on that second
half, and its failure mode is asymmetric: were `false` in a `then`
body to stop propagating, the first test would still pass while every
conversion went silently green.
The rule joins the E2E conventions doc as its own section, and the
inline list in AGENTS.md that an agent may read instead of the doc --
it belongs beside the EXIT-trap ban for the same reason, both leaving
a green suite that checks nothing.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Pinning the frozen source to a digest written into the migration picks one
release for every cluster, and v1.6.0 is not even the last one that ships
http-cache: v1.6.1 through v1.6.3 and v1.7.0-alpha.1 all do, and 1.6 patch
releases may keep doing so. A cluster on any of those would be rolled back
to an older chart by the upgrade that was meant to leave its instances
alone, and an air-gapped mirror resolves the constant only if someone
mirrored that exact manifest.
The hook runs before the upgrade re-points cozystack-packages, so the live
object still names the release the instances render from, in whatever
registry the operator serves it from and with whatever pull secret and
verification they configured. Reading the digest off it makes the freeze a
no-op by construction and needs no knowledge of which tag was last.
spec.ref.digest is used when the installer pinned one; a tag or semver ref
goes through status.artifact.revision, whose digest half is the manifest
digest. status.artifact.digest is the checksum of the downloaded tarball
and cannot stand in. A source with neither has nothing to freeze on and the
migration stops there. The copy also carries the no-delete label the
platform chart puts on the original, because it is the only chart source
those instances have left.
The tenant grant travels with the freeze as well. cozy:tenant:admin:base
enumerates the apps.cozystack.io resources a tenant owner may write, and an
application that has left the catalog has no business in that list: the
next sweep over it takes the entry as dead config, and the failure is
silent and remote, a Forbidden for the one person who still needs to remove
what they have. The chart drops httpcaches, and a cluster that freezes gets
a ClusterRole under the tenant admin aggregation label instead, applied
before the upgrade drops the rule so the access never lapses and carrying
the verb set the chart had so nothing changes for the owner. A cluster with
no instance is granted nothing.
The Package delete on the no-instance path has to block. The operator
reconciles off an informer cache, so a reconcile in flight against the
pre-delete entry re-creates the release it owns, and with it the
ApplicationDefinition and the Helm storage the following lines remove.
Returning immediately made the ordering a property of the command log
rather than of the cluster.
The preset check's SIGPIPE note named http-cache-rd as its example of a
large enum, and that chart goes away here; kubernetes-rd carries the same
enums.
Numbered 58 rather than 57: main already carries a migration 57, the
SeaweedFS volume-size-limit one, and its targetVersion is already 58.
The whole shape of this retirement was worked out here rather than
anywhere reusable, so docs/deprecating-applications.md writes it down as a
procedure: how to pick between migrating onto a successor and freezing,
what the prune takes with it, and what the migration owes a cluster on
each branch. The next application to go should not have to rediscover it.
Assisted-by: LLM
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
## What this PR does
Adds the `triage/needs-proposal` label to `.github/labels.yml` for
feature requests that need a design proposal in
[cozystack/community](https://github.com/cozystack/community) before any
work starts here. Until now that routing rule lived only in chat, so
such requests kept landing in this repository and were re-filed by hand
with nothing on the issue saying why. AGENTS.md now spells out the
handling next to the rest of the triage guidance: the request is
re-filed in cozystack/community, closed here with the label and a link
to the proposal, and work in this repository starts from a new issue
once the proposal is accepted.
### Screenshots
Not applicable, no UI change.
### Downstream repositories
- [x] 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:
The diff touches `.github/labels.yml` and `AGENTS.md` only; neither file
appears in the downstream trigger map in `docs/agents/contributing.md`.
### Release note
```release-note
chore(labels): add triage/needs-proposal for feature requests that need a design proposal in cozystack/community before work starts here
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Chores**
- Added a new triage label for issues that require a design proposal
before implementation.
- Documented the process for submitting, reviewing, and accepting
feature proposals before work begins.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Feature requests that need a design decision are routed through
cozystack/community first, as an issue or a design-proposal PR, and
work in this repository starts only once that proposal is accepted.
That rule lived only in chat, so such requests kept being filed here
and were moved by hand, with nothing on the issue recording why.
A dedicated triage state makes the decision visible on the issue
itself: the request is re-filed in cozystack/community and closed
here carrying the label and a link to the proposal. Re-filing rather
than transferring keeps the label on the closed issue, since a
transfer only preserves labels the target repository defines. While
the issue is still open, the daily triage sweep re-applies
triage/needs-triage to any open issue without a triage/* label, so
the routing has to be recorded by replacing the label rather than
removing it. AGENTS.md documents the rule next to the rest of the
triage guidance.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The attribution trailer exists for disclosure, not to advertise a
model. A model name in the trailer tells a reviewer nothing they can
act on, and it goes stale with every tool change, so the accepted form
is a single `Assisted-by: LLM` line however many tools took part.
A link to an assistant session is worse still: it rots, and it carries
nothing about the change itself, so no session trailer and no
transcript URL belong in a commit, a PR description, or a comment.
The commit body explains why a change is made; the diff already shows
what changed.
The hygiene rules for messages, trailers and in-code comments are
written down as review blockers for the same reason: a NOT LGTM on one
of them is then a stated rule, not a reviewer's taste.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
This reverts commit 2f87cdcdfa.
AGENTS.md sits at the repository root, where no scoped CODEOWNERS
rule matches it, so it falls through to the `*` catch-all owned by
@kvaps and @lllamnyp alone. Every other path this PR touches
(/.github/, /docs/, /hack/) lists @lexfrei, whose review already
satisfies them. That single file is the only reason this PR still
gates on an architect review, which is exactly the deadlock
hack/codeowners-invariant.bats was written to describe.
No substance is lost. docs/agents/image-refs.md carries the full
consumer table and the paragraph explaining why
overlay-main-images.sh and promote-packages-artifact.sh both stand
outside the shared enumeration, and it stays in this PR. What goes
away is the one-line routing summary in AGENTS.md that points at
that file. It leaves AGENTS.md briefly describing
overlay-main-images.sh as "a fourth consumer" while the detail file
lists five rows; the detail file is the authoritative one, and the
summary is re-landed on its own branch where a catch-all review
blocks nothing else.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The routing bullet called hack/overlay-main-images.sh "a fourth
consumer" from a time when three scripts sourced the enumeration.
Four do now, so the ordinal names the wrong thing in the file every
agent reads first.
State the distinction instead of a count: overlay is the exception
because it reads refs and walks the tree itself, so a newly declared
file never reaches it, while hack/promote-packages-artifact.sh
sources nothing because it publishes the whole tree and consumes no
refs. One clause each, with the existing pointer to
docs/agents/image-refs.md carrying the rest.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Issues created via the API or gh bypass the issue templates and arrive
without any triage/* label; 252 of the 396 open issues carry none.
Label issues on arrival (opened/reopened) and run a daily sweep as a
backstop: issues already prioritised, assigned, or marked as epics get
triage/accepted, the rest triage/needs-triage.
A signal has to be on stale.yaml's exempt-issue-labels, because
triage/accepted is itself exempt and a signal that is not would grant a
permanent stale reprieve as a side effect of being labeled. That is
necessary and not sufficient: lifecycle/frozen is exempt and still not
a signal, because a lifecycle/* label says what the stale bot may do
with an issue and nothing about whether anybody reviewed it.
The sweep paces its writes and caps how many it makes per run, so a
pass over a large backlog stays inside GitHub's secondary rate limits
instead of failing partway through. Each run writes a job summary with
the counts, the issue numbers per label, and both remainders it can
leave behind, because a backlog pass logs a line per issue and nobody
reads 252 of those to find out what happened.
The manual dispatch defaults to dry-run, so the one entry point a human
can reach writes nothing until somebody unchecks the box. The schedule
is the one event that writes without being asked.
Deleting a triage label does not undo a decision: an issue left with
none is relabeled by the next sweep, so AGENTS.md's labeling section
says to change a decision by replacing the label instead.
No PR lane can exercise this workflow, so the first real run is a sweep
over every open issue. A contract test pins the executable lines that
bound that: the pull-request skip, the already-triaged skip, the pacing
and the cap, the dry-run default, the pinned action, the token scopes,
that no signal comes from the lifecycle/* namespace, and that the
summary is written with the await that makes it flush.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The ban on EXIT-trap cleanup in hack/*.bats was enforced by two lists
inside hack/cozyreport.bats: one naming the files known to be clean, one
freezing an exact trap count for each file that was not. Both had to be
edited from whatever change moved them, and that is where the guard kept
failing. A change adding a trap to its own file had to edit a string in
a suite it otherwise never touches, so two changes sharing no line still
invalidated each other: each stayed green against its own base, git
merged both cleanly, and the guard went red only once the second landed.
A file arriving with traps hit the same wall from the other side -- the
inventory did notice it, since the string it compared was built by
scanning the directory, but absorbing it meant an edit in a file nothing
in the author's diff pointed at.
Replace both lists with a declaration each file makes about itself: one
"# EXIT-TRAP DEBT: N" comment, exact rather than a ceiling. A file
carrying none must install no EXIT trap, which covers the converted
files, the files that never had one, and every file added later.
Growing or shedding a trap now fails in the file the change already
edits, so two changes that disagree about a count collide textually
instead of silently.
The declaration is read from the leading comment block, not from
anywhere in the file. A .bats file is shell that writes shell, so the
same line turns up inside a heredoc, a fixture writer or an
expected-output string, where it is data belonging to one test. Reading
it there as a statement about the whole file would let an unrelated
fixture excuse a real trap, and would do it silently, since nothing in
that test's own diff looks like a declaration. A comment block is the
region with no interior: stopping instead at the first @test would still
read a line out of a helper's heredoc.
Not every counted handler is debt. A trap inside an explicit subshell
does not replace the bats binary's own, so a test failing inside
`( ... )` still prints its `not ok`. hack/e2e-test-openapi.bats kills a
backgrounded kubectl proxy that way, and moving the kill to the end of
the body would leak a process holding a fixed port. Its declaration
records that rather than scheduling a conversion, and because the
ratchet is exact, removing the trap fails too -- the count protects the
construct. What the count cannot do is tell the two apart: substituting
a test-level trap for the subshell one keeps the total at 1 and stays
green, which the header states rather than leaves to be found.
Counting bounds the keyword and the signal the same way, at any
character that cannot be part of an identifier, and matches the signal
in either case. Whitespace on the right missed `trap ... EXIT; cd
"$tmp"`; whitespace on the left missed `tmp=$(mktemp -d);trap ... EXIT`
and `(trap ... EXIT; true)`; upper case missed `trap ... exit`, which
bash and dash both install. All are real handlers that scored zero, and
the left boundary matters most, since the inventory being replaced had
none and caught the semicolon form. A bare word boundary is not enough
either way: `bootstrap ` ends in `trap `, and it must keep scoring
nothing. A quoted signal counts for the same reason as the rest.
Two handlers sharing one line are reported rather than counted, since
the count is a count of lines and the second would otherwise arrive
without moving the total; splitting the line properly needs a shell
parser, a semicolon inside a handler's own action not being a
separator, and guessing wrong undercounts -- the one direction a ratchet
cannot afford.
The scan recurses, so hack/e2e-apps/*.bats is covered rather than
sitting one directory below the guard that claims the tree. It reads
.bats and nothing else, so a handler arriving through a sourced .sh
stays outside it -- hack/e2e-chainsaw/_lib/run-kubernetes.sh installs
two, each benign for its own reason rather than by design: one sits in
a function declared with `(` and so runs in a subshell, the other in a
brace function no @test calls. That boundary is stated in the header
rather than papered over.
The guard moves to hack/bats-no-exit-trap.bats: its subject is every
unit suite under hack/, not the report collector it grew up in. Its
fixture helpers assemble the trap keyword and the signal from separate
arguments, because the guard scans its own source and a fixture written
as a literal would be counted as a real trap in it.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Review of the previous commit found the postcondition itself could abort
a release. It grepped the bare "X.Y.Z-rc.N" substring across the tree, and
three live files carry such strings without being image refs:
kubevirt-csi-driver's go.sum pins github.com/golang/protobuf v1.4.0-rc.2
-- a version cozystack actually cut, so promoting v1.4.0 would have
failed on a Go checksum line -- metallb_test.yaml names v1.5.0-rc.2 in a
comment, and the console pnpm-lock.yaml carries rolldown@1.0.0-rc.15. The
remediation the error message gave would have made things worse: declaring
go.sum as a ref source would sed a checksum file.
Match the version only in image-reference position now: after an
image/repository/tag key with no intervening '#', or immediately before an
@sha256 digest. Narrowing it to "${RC}@sha256:" alone would have been
wrong -- cilium keeps `tag: v1.5.0` and `digest:` under separate keys, so
the version is not always adjacent to the digest. Verified against four
real rc versions that previously aborted.
The suite also only tested the fail-closed direction, which is why that
defect survived, and the collector had no completeness guard at all: a
first-party ref added at an unenumerated path on a `latest@` tag was
mirrored by nothing and passed all eighteen tests. Since the mirror's
failure mode is the worst of the three -- the published nightly points at
the private build registry -- that gap mattered most where coverage was
thinnest.
Added: a fail-open test using the three real rc versions above; a
collector completeness diff that discovers refs by content and fails on
any the enumeration misses; coverage for the OCI-artifact shape, whose
deletion previously left every suite green; and an unreadable-file test,
since a read error was being treated as "no match" and silently skipped.
The component-versioned test was vacuous -- its rc version shared no
substring with the kamaji tag it claimed to protect, so it passed by
construction. It now uses an rc version sharing kamaji's 0.19.0 prefix,
which a loosely-anchored implementation would corrupt.
Round-trip fixture discovery matched a combined 'ghcr.io/cozystack/
cozystack/' prefix, which skips keycloak-operator (host in a sibling
registry: key) and kubeovn (host in global.registry.address). It now
matches 'cozystack/cozystack' and stamps split `tag:` keys too.
Also documented two pre-existing gaps rather than half-fixing them: the
nightly host rewrite is a contiguous-substring replace and cannot reach a
split host, and a ref inside a gzip is invisible to every tool including
the postcondition, which skips binaries. Both are host-dimension defects;
the version dimension this change owns is unaffected, because the version
always lives in a tag key regardless of where the host sits.
Assisted-By: Claude <noreply@anthropic.com>
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Promotion rewrote the rc version substring across the depth-2 package
values.yaml plus packages/apps/kubernetes/images/*.tag alone, on the
premise that the kubernetes app was the only one whose .tag files carry
the cozystack version. Nine other .tag files do, and system/multus seds
its ref straight into a vendored upstream manifest. v1.6.0 was therefore
staged with 33 of 55 first-party refs still reading v1.6.0-rc.4.
promote-retag.sh and nightly-mirror.sh had the same blind spot from the
other direction: both scanned only the depth-2 values.yaml, so those
images were never retagged to the stable version (30 refs selected
before, 42 after) and never mirrored to the public registry for a
nightly. A retag miss merely leaves an image short of a tag, since the
digest still resolves inside one registry; a mirror miss is worse,
because the host rewrite walks the same file list and a ref that is
neither mirrored nor rewritten leaves the published tree pointing at the
private build registry.
The three call sites each carried their own idea of where a ref can
live, which is how they drifted apart. Move that knowledge into
hack/lib/image-refs.sh as the single enumeration -- image_ref_files()
for the files, collect_image_refs() for the refs inside them -- and have
all three source it, so a new storage location is declared once and
reaches every consumer.
The rewrite itself moves out of promote-rc.yaml into
hack/promote-rewrite-tags.sh. Being workflow-inline is why this was only
observable by cutting a release: there was nothing to unit test. It now
also asserts its own postcondition, scanning wider than it rewrites and
failing the promotion when any ref survives in a location the
enumeration does not know about, rather than shipping it quietly.
Component-versioned images (kamaji's v0.19.0-cozystack.N) and
third-party pass-through images do not ride the cozystack version line
and are deliberately left alone; a test pins that so a future fix here
cannot over-reach. capi-providers-cpprovider is a documented known gap:
its ref is also embedded in a gzipped copy that the chart actually
ships, and rewriting only the readable copy would diverge the two.
docs/agents/image-refs.md records the contract -- three tag classes,
three storage shapes, the invariants, and which consumer does what.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Markdown renderers collapse a soft line break into a space, so a
paragraph hardwrapped at ~80 columns renders exactly like the same
paragraph on one line -- while breaking narrow viewports, mangling
nested list and table rendering, and turning a one-word edit into a
diff that reflows the entire block.
The rule covers prose this repository owns, in markdown files and in
every body published to GitHub: PR bodies, issue bodies, review
comments, release notes. It governs new and changed paragraphs; the
existing tree is wrapped in many places and is not a backlog of defects
to go and fix. Vendored upstream chart docs stay verbatim, published
changelogs stay as published, and commit message bodies keep the
~72-column wrap that git log and its tooling expect.
Structure that needs its line breaks is unaffected: code blocks, lists,
tables, headings, blockquotes, front matter, badge stacks, and explicit
hard breaks, which render as <br> and cannot be reflowed without
changing the output.
AGENTS.md opened with a wrapped paragraph of its own; reflow it.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Capture the e2e stabilization conventions established over the last few
months as a routed agent doc: no retries on deterministic CI steps,
event-driven backstops over fixed sleeps, no EXIT/RETURN traps,
fail-fast HelmRelease-readiness gating, the status.history remediation
guard, first-class failure diagnostics, and a reviewer checklist. Also
documents Test-Impact Analysis (TIA) and its real scope — it trims only
the per-app loop, runs after install, and most PRs escalate to the full
suite. Wire it into AGENTS.md so every agent reads it before e2e/CI work.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from gemini-code-assist on AGENTS.md:33:
expand bare labels.yml and pr-labeler.yaml to .github/labels.yml and
.github/workflows/pr-labeler.yaml for consistency with surrounding refs.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Add .github/labels.yml as the canonical label set, synced into the
repository by .github/workflows/labels.yaml using EndBug/label-sync.
Conventions follow the Kubernetes scheme:
https://github.com/kubernetes/test-infra/blob/master/label_sync/labels.md
Six namespaced groups: kind/, priority/, triage/, lifecycle/, area/,
do-not-merge/. Cozystack-specific labels preserved (epic, community,
security/*, size:*).
Migration via aliases keeps references on existing issues and PRs:
- bug -> kind/bug
- enhancement -> kind/feature
- documentation -> kind/documentation
- question -> kind/support
- frozen -> lifecycle/frozen
- stale -> lifecycle/stale
- do-not-merge -> do-not-merge/work-in-progress
delete-other-labels is false on the initial rollout; redundant labels
("do not merge", duplicate, invalid, wontfix) stay until a follow-up
PR removes them after stabilisation.
The labels workflow has a validate job (python3 schema check) that
runs on PR. Sync runs only on push to main, weekly cron, and manual
dispatch. Schema invariants:
- description <= 100 chars (GitHub REST API limit)
- color is 6-char hex without leading #
- unique top-level names
- aliases do not collide with top-level names
PR auto-labeling (.github/workflows/pr-labeler.yaml):
- Parses PR title as Conventional Commits header (type, scope, !).
- type -> kind/* (feat -> kind/feature, fix -> kind/bug, docs ->
kind/documentation, chore/refactor -> kind/cleanup; style, perf,
test, build, ci, revert -> no kind label).
- scope -> area/* via embedded mapping; composite scopes split on
comma. Bracket-style fallback ([scope] description) maps area/*
but cannot infer kind/*.
- '[Backport release-1.x]' prefix is stripped; area/release and
backport labels are added.
- '!' after type or 'BREAKING CHANGE:' footer in body adds
kind/breaking-change.
- Unmapped scope or non-conventional title adds area/uncategorized
to flag for human review.
- Additive only — never removes existing labels.
Hardcoded label references updated:
- .github/ISSUE_TEMPLATE/bug_report.md (bug -> kind/bug)
- .github/workflows/tags.yaml (documentation -> kind/documentation)
AGENTS.md gains an Activation entry pointing agents to labels.yml
as the source of truth and to contributing.md for the title
auto-labeling table.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Refocus docs/agents/contributing.md on project-side conventions only
and drop personal agent-behavior guidance.
Switch commit format from [component] prefix to Conventional Commits
(type(scope): description) in contributing.md, overview.md, AGENTS.md,
.gemini/styleguide.md and the PR template.
Reference .github/PULL_REQUEST_TEMPLATE.md instead of duplicating the
PR body template in contributing.md.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>