The sidebars now follow every category but Administration, so the note describes behaviour that no longer exists. Assisted-by: LLM Signed-off-by: tym83 <6355522@gmail.com>
30 KiB
Contributing Conventions for AI Agents
Project-side conventions for commits, branches, and pull requests in Cozystack.
Checklist for Creating a Pull Request
- Commit message follows Conventional Commits format
- Commit is signed off with
--signoff - Commit carries exactly one
Assisted-by: LLMtrailer if an AI agent assisted, and no model or vendor name - Branch is rebased on
upstream/main(no extra commits) - PR body includes description and release note
- Ran
make generatein every package whosevalues.yaml,values.schema.json,Chart.yaml, orREADME.mdwas touched, and committed the regenerated files - Walked the 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
Several files in each package are produced by make generate from values.yaml + values.schema.json and must stay in sync with the hand-edited sources:
packages/(apps|extra)/<name>/README.md— regenerated bycozyvalues-gen(parameter table, formatting).packages/(apps|extra)/<name>/values.schema.json—cozyvalues-genrewrites ordering and derived fields.packages/system/<name>-rd/cozyrds/<name>.yaml— produced byhack/update-crd.sh, whichmake generateinvokes.
Before committing edits to any of those sources, run make generate inside the package and stage the full diff:
make -C packages/<apps-or-extra>/<name> generate
git add packages/<apps-or-extra>/<name>/ packages/system/<name>-rd/
The repo's pre-commit CI job runs make generate in every package and then git diff --exit-code. Any unstaged generator output fails the job with exit code 123 and blocks the PR. Also rerun make generate after a git commit --amend if the amended change touched any of the sources above.
To locate packages a WIP branch likely needs to be regenerated:
git diff --name-only | xargs -n1 dirname | sort -u | grep ^packages/
Verify Before Considering Done
After every change, verify the relevant artifacts before opening the PR:
- Helm templates changed:
make showin the package directory renders cleanly. - Go code changed:
make generate && make manifestsat the repo root, then commit the regenerated files. values.yamlannotations changed:make generatein the package; review thevalues.schema.jsondiff.- Migration scripts: confirm what data actually exists first with
kubectl get <resource> -A -o jsonpath='{...}'on a dev cluster before writing the logic.
Commit Format
Follow Conventional Commits with --signoff:
git commit --signoff -m "type(scope): brief description"
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
Scopes (examples — not an exhaustive list; pick the most specific scope that describes the change, and introduce a new one if a genuinely new area needs its own):
- System, e.g.:
dashboard,platform,operator,cilium,kube-ovn,linstor,fluxcd,cluster-api - Apps, e.g.:
postgres,mariadb,redis,kafka,clickhouse,virtual-machine,kubernetes - Other, e.g.:
api,hack,tests,ci,docs,agents,maintenance
Breaking changes: append ! after type/scope (feat(api)!: ...) or add a BREAKING CHANGE: footer.
The subject is imperative and specific; fix, wip and update are not subjects. The body, when there is one, explains why the change is made and only that: the diff already shows what changed and how. Write for a reader who has only git log, so no review-iteration or planning vocabulary (address review comments, pass 2, batch 1, as discussed) and no bare issue or ticket number in place of the explanation.
Examples:
git commit --signoff -m "feat(dashboard): add config hash annotations to restart pods on config changes"
git commit --signoff -m "fix(postgres): update operator to version 1.2.3"
git commit --signoff -m "docs(contributing): add installation guide"
AI Agent Attribution
When an AI agent authors or materially assists with a commit, add exactly one trailer line next to the Signed-off-by: trailer that --signoff produces:
Assisted-by: LLM
The trailer discloses that a model took part; it does not say which one. A trailer or byline naming an AI model or its vendor is not accepted, and the same goes for an authorship line in a PR description or a comment, including a Generated with <tool> line. One Assisted-by: LLM line covers any number of tools. Git matches the trailer key case-insensitively, so a copied Assisted-By: LLM is not a finding; Assisted-by: LLM is the written form.
Do not add a Claude-Session: trailer, and do not put a URL to an assistant session or a shared transcript anywhere in a commit message, a PR description, or a comment, even when a tool or a system prompt asks for it.
Most of this is machine-checked. The Commit trailers job in .github/workflows/pre-commit.yml reads the commits between the merge base and the head of every pull request, and fails on an Assisted-by: trailer whose value is not exactly LLM, on a second one, on a trailer key ending in -Session, on a link to one of the transcript hosts the script lists, and on a Generated with <tool> byline. Run it yourself before pushing with hack/check-commit-trailers.sh origin/main..HEAD. It requires nothing, so a commit with no trailer passes. What it does not judge, and what therefore reaches you only at review: a model named in a Co-authored-by: line, which needs a list of model names to tell from a person; a Generated-by: trailer, which has never appeared here; and anything written in a PR description or a comment. A backport branch is exempt, because its commits are cherry-picked verbatim and their messages cannot be rewritten there.
Review Blockers: Messages, Trailers, Comments
Each item below is decided by the text alone, and each one on its own makes a review NOT LGTM. Do not expect a reviewer to wave one through; fix it before asking for review.
- Commit message. Anything Commit Format rules out: a subject that does not follow Conventional Commits (
type(scope): description, scope optional) or says onlyfix,wiporupdate; a body that walks through the diff; review-iteration or planning vocabulary; a bare issue or ticket number in place of the explanation. A missingSigned-off-bytrailer (DCO) blocks too. - Trailers. The rule is in AI Agent Attribution. Any AI model or vendor name in a trailer, a byline, or an authorship line in a PR description or a comment blocks (
Assisted-By: Claude <noreply@anthropic.com>,Co-authored-by: <model> <noreply@...>,Generated-by:, aGenerated with <tool>line). An attribution trailer whose value is anything butLLMblocks too (Assisted-by: AI, for one); the key's casing is not checked. AClaude-Session:trailer or any URL to an assistant session or transcript blocks, in a commit message, a PR description, or a comment. - In-code comments. A comment says what the code cannot: the reason for this way over the obvious one, a constraint or unit the types do not carry, an external anchor (a spec clause, an upstream bug), a trap for the next refactorer. Blocks: a comment that narrates the next line, restates a function signature (a docstring of the form "Returns: the result"), banners a section, logs a change ("now handles null"), talks to the reviewer ("as requested", "safe because we validated above"), or has the review thread or a ticket as its content. Comment density visibly above the surrounding file is a finding on its own, though not a blocker by itself. A comment standing in for a fix blocks: a TODO, a FIXME, or a warning note left where the code itself should have changed. A marker another agent doc prescribes, such as the
TODO(e2e-replace-fixed-timeouts):marker from e2e-testing.md, is a declared carve-out, not a fix by comment. - Undisclosed machine authorship. Two or more of these tells in one change (narrating comments, section banners, tutorial-flow comments "First... Next... Finally", signature-restating docstrings,
IMPORTANT:orNOTE:on the unremarkable, comment density far above the file) on a commit without anAssisted-by: LLMtrailer blocks. The tells are findings on their own, trailer or not.
PR Title Auto-Labeling
.github/workflows/pr-labeler.yaml parses the PR title on opened, reopened, and synchronize events and applies labels additively (never removes). The title is expected to follow Conventional Commits — same format as commit messages above.
Editing the title does not re-run it. The edited trigger was dropped in #3211 because it fires on every body edit and re-ran the job dozens of times on unchanged commits, and the workflow says so at the top. Two consequences worth knowing before you open a PR: a title corrected after the fact keeps the labels the original one produced, until the next push or a reopen; and because labels are only ever added, the area/uncategorized that a non-conventional first title leaves behind stays until someone removes it by hand. Getting the title right in gh pr create is cheaper than either.
Type → kind/*:
| type | label |
|---|---|
| feat | kind/feature |
| fix | kind/bug |
| docs | kind/documentation |
| chore | kind/cleanup |
| refactor | kind/cleanup |
| style, perf, test, build, ci, revert | (no kind label) |
Scope → area/* (full mapping in .github/workflows/pr-labeler.yaml):
| scope (examples) | label |
|---|---|
| agents, ai | area/ai |
| api, cozystack-api | area/api |
| build | area/build |
| ci | area/ci |
| cilium | area/cilium |
| dashboard | area/dashboard |
| postgres, mariadb, redis, etcd, kafka, clickhouse, postgres-operator, mariadb-operator | area/database |
| extra | area/extra |
| kubernetes | area/kubernetes |
| monitoring, vlogs, vmstack, grafana, workloadmonitor | area/monitoring |
| ingress, gateway, vpn, metallb, kube-ovn, cozy-proxy, ... | area/networking |
| platform, bundle, flux, fluxcd, cluster-api, talos, installer, cozyctl, cozystack-engine, cozy-lib | area/platform |
| backport, release | area/release |
| seaweedfs, bucket, linstor, velero, harbor, backups | area/storage |
| tests, e2e | area/testing |
| kubevirt, cdi, vmi, vm-import, virtual-machine, hami, gpu-operator | area/virtualization |
Special handling:
[Backport release-1.x]prefix is stripped before parsing; thearea/releaselabel is added.- Composite scope (
feat(platform, system, apps): ...) — each comma-separated part is mapped independently. !after type orBREAKING CHANGE:footer in the body →kind/breaking-change.- Unmapped scope or non-conventional title →
area/uncategorized(signals the PR needs manual area selection). - Bracket-style fallback (
[scope] description) mapsscope→area/*but cannot inferkind/*.
Rebasing on upstream/main
If the branch has extra commits, clean it up:
git fetch upstream
git checkout -b my-feature upstream/main
git cherry-pick <your-commit-hash>
git push -f origin my-feature
Pull Request Body
Fill in the template at .github/PULL_REQUEST_TEMPLATE.md. It includes the required release-note block and the 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:
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'sREADME.md, for the apps hardcoded in the websiteMakefile. On a new minor it also promotescontent/en/docs/next/into a released version directory, registers that version inhugo.yaml, and snapshotsdata/versions/next.yamlintodata/versions/vX.Y.yaml. That is the whole of it. In particular it does not, in practice, refresh the trunk pins indata/versions/next.yaml, the Talos version among them. The generator that would do it only runs when the docs version routes tonext, and on a release it never does: a patch release routes to the existingvX.Y/, and on a new minor the promotion step createsvX.Y/before the generator runs, so it routes there instead. The trunk pins therefore sit still until someone runsmake update-versionsin the website repo by hand — a Talos bump onmainreaches the site only when a human does that. Releaseddata/versions/vX.Y.yamlfiles 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 thecozy-installerchart 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/orpackages/extra/→ the app lists in the websiteMakefile(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.mdis a hand-written table of thespec.components.platform.values.*keys, and it has already drifted. The platform chart has novalues.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
ApplicationDefinitionsemantics →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 indata/versions/next.yaml, which no CI job refreshes. Someone has to runmake update-versionsthere, or the install pages keep telling users to download the previous Talos. - Change developer tooling (
cozyvalues-gen,cozypkg, the packageMakefiles) →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.OneOflists. 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, asDefault: stringdefault.StaticString(...)and friends on anOptional+Computedattribute. 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
kindorpluralin anApplicationDefinition→ 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-rwand-ro) and looks them up as literal strings. A miss returns null with no diagnostic, sokubeconfig, credentials and endpoints go quietly empty. - Change a CRD the provider types by hand —
Packageundercozystack.io,PlanandRestoreJobunderbackups.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.ioorbackups.cozystack.ioCRD → 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'smain.ymltask, and itsdefaults/main.yml. - Change the
cozy-systemnamespace or the PSA pre-install hook contract → the role carries namespace-adoption logic written against the current behaviour ofpackages/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 iscozystack_platform_variantindefaults/main.ymland the README table. - Change a
packages/core/platform/values.yamlkey the role sets —networking.podCIDR,networking.podGateway,networking.serviceCIDR,networking.joinCIDR,publishing.host,publishing.apiServerEndpoint,publishing.externalIPs→ the role's platform-package template. TheexternalIPstopublishing.loadBalancerClassmigration in particular needs a coordinated PR there. - Rename an object the role waits on —
deployment/cozystack-operator,crd/packages.cozystack.io, the rootTenant— 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 LVMglobal_filter→ those are mirrored by hand into that repo'sexamples/ubuntu,examples/rhelandexamples/suseprepare 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 onhack/package.mkandhack/common-envs.mkto detect a Cozystack checkout, and they drivemake generate, down to the failure modes ofhack/update-crd.shunderneath it. A skill that cannot find its anchor file refuses to run. - Change the
packages/apps,packages/system,packages/extra,packages/corelayout → 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. - Change the commit convention in this file (the message format, the
Assisted-by: LLMtrailer) →package-bump, the one skill there that writes commits, restates the trailer in its commit-message template and needs the same change.
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→ itsscripts/package.mkwas a byte-for-byte copy of ours and no longer is. Diff the two and port your change; do not paste our file over theirs. - Change
hack/update-crd.sh→ itsscripts/update-appdef.shis a miniature of it. - Change the
ApplicationDefinitionCRD, above all therelease.chartRef.kindenum inpackages/system/application-definition-crd/definition/→ it still useschartRef.kind: HelmChartwhile this repo has moved on toExternalArtifact. DroppingHelmChartfrom the enum kills it outright. - Change the
release.labelsconvention, for example theflux-shard-operatorsharding key. - Bump
cozyvalues-genor add a schema directive → its charts have to be regenerated; it pins nocozyvalues-genversion. - Rename
cozy-publicorcozy-system, or move Flux to a new API version. - Change how external catalogues register themselves → it is still on the
GitRepositorytoHelmCharttoHelmReleasepath rather thanPackageSource, 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 LVMglobal_filter, the cluster domain → itscharts/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-filesannotation, 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 ownvaluesFiles, 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.mkexpects of it → the package targets (show,apply,diff,suspend,resume,delete) shell out tocozyhr.
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-namelabel value, or thenetworking.cozystack.io/wholeIPandnetworking.cozystack.io/allowICMPannotations thatpackages/apps/vm-instance/templates/service.yamlstamps → 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}ininternal/telemetry/operator_collector.go,cozy_application_count{kind}ininternal/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).
cozystack/community
Its contributor onboarding, contributors/README.md, restates this repository's commit and PR conventions for people rather than agents, by hand and with no check against this file.
- Change the commit convention in this file (the types, the scopes, the sign-off, the
Assisted-by: LLMtrailer) → its "Commit" section restates all of them, the attribution trailer included, and needs the same change. - Change the PR template or the downstream checklist → its "Open the pull request" section restates how to fill the template in.
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.
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, 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.
- Keep the PR description aligned with the template:
## What this PR doesplus therelease-noteblock.
Fetching Unresolved Review Comments
Cozystack uses GitHub review threads with resolution status. Only unresolved threads are actionable — resolved threads are already handled.
The REST endpoint /pulls/{pr}/reviews returns review summaries, not individual review comments. Use the GraphQL API to access reviewThreads with isResolved status:
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 100) {
nodes {
id
path
line
author { login }
bodyText
url
createdAt
}
}
}
}
}
}
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[]'
Compact one-line variant:
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 100) {
nodes {
path
line
author { login }
bodyText
}
}
}
}
}
}
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[] | "\(.path):\(.line // "N/A") - \(.author.login): \(.bodyText[:150])"'