Files
cozystack/docs/agents/contributing.md
tym83 a3b802406c docs(agents): drop the note on hardcoded sidebar categories
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>
2026-10-04 14:21:08 +05:00

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: LLM trailer 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 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 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 by cozyvalues-gen (parameter table, formatting).
  • packages/(apps|extra)/<name>/values.schema.json — cozyvalues-gen rewrites ordering and derived fields.
  • packages/system/<name>-rd/cozyrds/<name>.yaml — produced by hack/update-crd.sh, which make generate invokes.

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 show in the package directory renders cleanly.
  • Go code changed: make generate && make manifests at the repo root, then commit the regenerated files.
  • values.yaml annotations changed: make generate in the package; review the values.schema.json diff.
  • 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 only fix, wip or update; a body that walks through the diff; review-iteration or planning vocabulary; a bare issue or ticket number in place of the explanation. A missing Signed-off-by trailer (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:, a Generated with <tool> line). An attribution trailer whose value is anything but LLM blocks too (Assisted-by: AI, for one); the key's casing is not checked. A Claude-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: or NOTE: on the unremarkable, comment density far above the file) on a commit without an Assisted-by: LLM trailer 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; the area/release label is added.
  • Composite scope (feat(platform, system, apps): ...) — each comma-separated part is mapped independently.
  • ! after type or BREAKING 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) maps scope → area/* but cannot infer kind/*.

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'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 Makefiles) → 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 publishing.loadBalancerClass 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.
  • Change the commit convention in this file (the message format, the Assisted-by: LLM trailer) → 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 → its scripts/package.mk was 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 → 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).

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: LLM trailer) → 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 does plus the release-note block.

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])"'