Skip to content

[Enhancement]: Watch unraid/api releases and schema changes, and open bug/enhancement issues automatically #136

Closed

Description

@ruaan-deysel

Affected area

CI and repository automation

Problem statement

This library tracks unraid/api by hand, so upstream changes reach it late. Releases 4.37.7–4.37.9 (23–28 Oct) shipped two changes that broke the library on live servers, and both were only found by a manual audit on 30 Oct (#135):

  • unraid/api#2123 made the docker and vms query roots nullable (docker: Docker! → docker: Docker). A failed Docker or VM service now raises AttributeError in the library instead of UnraidAPIError.
  • unraid/api#2095 and #2129 disabled schema introspection by default. Capability detection silently fell back to permissive, so consumers lost VM UUIDs. The same change breaks scripts/validate-schema.py, which introspects a live server.

Upstream also publishes the complete schema as api/generated-schema.graphql in every commit. That is enough to detect most of these changes in CI, without a server, the day they merge.

Proposed solution and architecture

Add an Upstream watch workflow (.github/workflows/upstream-watch.yml), modelled on ha-unraid's HA Next and HA dev blog watch (tinkerologie/ha-unraid#391): scheduled daily and workflow_dispatch, permissions: {} at the top level, SHA-pinned actions, Python 3.14.2 through uv. actions/setup-python has no 3.14.2 build for the Ubuntu 26.04 runner.

  1. script/upstream_watch.py (scan), read-only, with graphql-core (already a dependency):
    • Releases: list unraid/api releases newer than the last one recorded, using a marker such as <!-- upstream-release: v4.37.9 --> in this repository's issues, open or closed. For each new release, read its notes and the PRs it links.
    • Schema diff: parse api/generated-schema.graphql at the previous and the new release tag, and also at main for unreleased changes. Report removed types and fields, non-null → nullable and nullable → non-null changes, argument and input changes, enum value changes, new deprecations, and added types, fields, queries, mutations and subscriptions.
    • Impact on this library: validate every operation the library sends against the new schema with graphql.validate, reusing the operation extraction in scripts/validate-schema.py. Then, for each changed field, say whether the library selects it (file:line). A removed field the library selects, or a root or field the library reads that became nullable, is breaking.
    • Security and behaviour notes: flag release-note entries that change request handling (introspection, request limits, auth, rate limiting, error shapes), even without a schema change.
    • Write one JSON report.
  2. Report job (issues: write on that job only, actions/github-script), one issue per finding group, deduplicated by a hidden marker in the issue body (release tag, plus main for unreleased changes), never through the search API, which lags:
    • bug + upstream: breaking changes (a library operation no longer validates; a field or root the library reads became nullable or was removed), with the schema diff, the affected methods and the release notes.
    • enhancement + upstream: new types, fields, queries, mutations and subscriptions the library doesn't expose yet, plus non-breaking deprecations.
    • Labels are set in the create call (created first if missing). Issues opened with GITHUB_TOKEN never trigger issue-labeler.yml, and CodeRabbit only writes a coding plan for bug/enhancement.
    • The job summary lists every release and schema change checked, including those that need no action.
  3. Make scripts/validate-schema.py work on 4.37.8+: a --schema-file option (default: the upstream SDL for the server's API version) instead of introspection, which those servers refuse.

Acceptance criteria

  • script/upstream_watch.py scan produces a report for any release range; run against v4.37.6 → v4.37.9 it reports the nullable docker/vms roots as breaking (with the library methods that read them) and the introspection policy as a behaviour change
  • The workflow opens deduplicated bug/enhancement + upstream issues and records the last processed release; a re-run opens nothing new, and closed issues still prevent duplicates
  • Unreleased schema changes on upstream main are reported once, and not again when they ship in a release
  • scripts/validate-schema.py --schema-file validates every library operation without a live server
  • New functionality exposed with full test coverage
  • Linter and type checks pass locally
  • Documentation and CHANGELOG.md updated under [Unreleased]

Alternatives considered

  • Watching releases only: this would have caught 4.37.8, but not unreleased schema changes on main days earlier, and it can't say whether the library is affected.
  • Live-server contract tests in CI: they need credentials and a server, and introspection is now disabled.
  • Dependabot or Renovate: the library doesn't depend on unraid/api as a package.

Additional context

Found while auditing upstream 4.37.7–4.37.9 for #135. Upstream issues to keep watching that need no library change: unraid/api#2120 (Docker stats stream goes silent until an API restart), #2131 and #2007. The watcher could also list open upstream issues that mention a subscription or query the library uses, as an optional second stage.

Disclosure: this issue was written with the help of AI (Claude Code).

Pre-submission confirmations

  • I searched existing open and closed issues before opening this request.
  • I confirm this request targets the unraid-api Python package.
  • I provided sufficient technical context and acceptance criteria for plan generation.
  • I read and agree to follow repository contribution rules and guidelines.

Activity

  1. changed the title [-]Watch unraid/api releases and schema changes, and open bug/enhancement issues automatically[/-] [+][Enhancement]: Watch unraid/api releases and schema changes, and open bug/enhancement issues automatically[/+] on Oct 29, 2026
  2. coderabbitai commented on Oct 30, 2026

    @coderabbitai
    Contributor

    Coding Plan

    Summary

    • Use one tested script/schema_tools.py module for the watcher and validator. Replace the static extractor that corrupts dynamic operations with a hybrid inventory. Render capability-gated builders against capabilities derived from the target schema.
    • Use graphql-core for SDL parsing and graphql.validate. Use a custom diff with stable change IDs. Classify changes by whether the library selects the affected coordinate.
    • Deduplicate only through bot-authored hidden issue markers. Read issues with paginated state: all listing. Use group markers, per-change markers and a closed state issue. Prevent duplicates from re-runs, closed issues and changes moving from main to a release.
    • Separate the read-only scan from a thin issues: write report job. Add an offline SDL path to validate-schema.py. Keep introspection as an opt-in.
    Design Choices
    Design Choice 1: Provide graphql-core as a tooling dependency. Exploration confirms that it is absent from pyproject.toml and uv.lock, despite the ticket stating that it is already a dependency.

    Options Considered:

    1. Add it as a runtime dependency.
    2. Add it to the dev optional extra and regenerate uv.lock.

    Chosen Option: 2

    Rationale: Only tooling uses it, and the workflow already installs --all-extras. The library runtime does not change.

    Design Choice 2: Choose script/ from the ticket or the existing scripts/ directory for watcher code. scripts/ is excluded from Ruff. File names with a hyphen cannot be imported normally.

    Options Considered:

    1. Put upstream_watch.py in scripts/ next to the validator.
    2. Use script/ as the ticket specifies. Put shared schema tooling there. Let scripts/validate-schema.py import it.

    Chosen Option: 2

    Rationale: Ruff then lints the new code, the modules are importable by tests, and the ticket path is kept.

    Design Choice 3: Build an accurate library operation inventory. Static AST extraction corrupts interpolated operations and attributes them to builders instead of public methods.

    Options Considered:

    1. Reuse extract_queries_from_source unchanged.
    2. Capture operations at runtime by calling every client method with a stubbed transport.
    3. Use static extraction for literal operations. Add an explicit registry that renders dynamic builders with capability variants. Add a guard that fails if an interpolated operation is not in the registry.

    Chosen Option: 3

    Rationale: This is accurate for every current dynamic case. It does not need to invoke hundreds of methods with arguments. The guard catches new dynamic builders.

    Design Choice 4: Record the last processed release and deduplicate changes from main to a release. A release with no findings creates no issue. A main change appears again in the next release diff.

    Options Considered:

    1. Use markers on finding issues only, with one marker for each group.
    2. Use a closed Upstream watch state issue that holds the upstream-release and upstream-main markers. Add one upstream-change marker for each change in each finding issue.

    Chosen Option: 2

    Rationale: Releases without findings are recorded. Changes reported from main are not reported again when they ship in a release. Closed issues still count.

    Design Choice 5: Select the default schema source when --schema-file is absent and retain optional introspection.

    Options Considered:

    1. Remove introspection completely.
    2. By default, fetch upstream SDL for the server's API version. Keep introspection only behind an explicit --introspect flag. Fail clearly if the version is "unknown" or does not parse.

    Chosen Option: 2

    Rationale: The ticket asks for this default. Existing behaviour stays available for servers with the developer sandbox enabled.

    Design Choice 6: Choose the location for reporting logic.

    Options Considered:

    1. Put deduplication, rendering and labelling in actions/github-script JavaScript.
    2. Let the Python scan render complete issue payloads and the summary. Limit the github-script step to listing issues, checking markers, creating labels and issues, and writing the summary.

    Chosen Option: 2

    Rationale: The logic is in Python and pytest covers it. The job with write access stays small, and does not need a checkout.

    Design Choice 7: Meet the v4.37.6 → v4.37.9 acceptance criterion without confirmed public tags. The web search did not verify v4.37.6, v4.37.7 or v4.37.9, or the schema file at each tag.

    Options Considered:

    1. Depend on live GitHub in tests.
    2. Use trimmed fixture SDL files and release-note fixtures in tests. At runtime, handle a missing tag or schema file with a clear status.

    Chosen Option: 2

    Rationale: Tests are deterministic. A real scan --since v4.37.6 --until v4.37.9 run stays a manual check.

    💡 User Tips

    Regenerate the plan with different choices with @coderabbitai <feedback>.

    Implementation Steps

    Phase 1: Shared schema and operation tooling

    Create one tested module that loads SDL, builds the library operation inventory with provenance, validates operations, maps selected fields to file:line, and diffs two schemas. Use this module in the watcher and validator.

    Task 1: Dependency and tooling configuration

    Make script/ installable, linted, type-checked, tested and measured.

    • Update pyproject.toml: add graphql-core to the dev extra. Add script to Ruff src. Add pythonpath = ["script"] to pytest options. Add script to coverage source.
    • Regenerate uv.lock with uv lock.
    • Update .github/workflows/ci.yml and .pre-commit-config.yaml so mypy checks src and script. If the mypy hook uses an isolated environment, add graphql-core to its additional_dependencies.
    Task 2: SDL loading and operation inventory

    Replace the corrupting extractor with an accurate inventory. Keep one implementation.

    • Create script/schema_tools.py. Move ExtractedQuery and extract_queries_from_source from scripts/validate-schema.py. Extend them:
      • Skip docstrings. Require a keyword boundary after query/mutation/subscription.
      • Record f-strings with interpolations as "dynamic". Do not emit them as operations.
      • Scan src/unraid_api/client.py and src/unraid_api/capabilities.py.
    • Add a registry of dynamic sources: _build_containers_query, _build_containers_query_safe, the VM query in typed_get_vms, and build_introspection_query(). Render each source with two capability sets:
      • ServerCapabilities.permissive().
      • Capabilities derived from the target schema. Execute build_introspection_query() against the graphql-core schema with graphql_sync. Pass the result to ServerCapabilities.from_introspection_response.
    • Render builders by constructing an UnraidClient with dummy credentials and preset _capabilities. For typed_get_vms, patch query with a recording AsyncMock. Do not use network access.
    • If the registry does not cover an interpolated operation f-string, fail the inventory.
    • Build an intra-file call graph from self.<method>( calls. Attribute each operation to all transitive public callers, for example typed_get_docker_networks → get_docker_networks. Keep all provenance when deduplicating identical documents.
    • Exclude caller-supplied documents (query(), subscribe() arguments) and script-owned documents.
    Task 3: Validation, selected-field coordinates and schema diff

    Provide the analysis primitives in script/schema_tools.py.

    • Add an SDL loader that uses graphql.build_schema from a file path or text.
    • Validate each inventory operation with graphql.parse and graphql.validate. Return errors with the operation, its methods and file:line.
    • Collect selected coordinates (Type.field, including root fields such as Query.docker) for each operation with TypeInfo/TypeInfoVisitor. Map each coordinate to methods and file:line.
      • For literal operations, use the string start line plus the document line offset.
      • For dynamic sources, use the line of the field literal in the builder. Use the builder's start line as a fallback.
    • Add a schema diff walker that emits typed change records with stable IDs (kind + coordinate + before/after). Cover these kinds: type added or removed, field added or removed, output non-null → nullable and nullable → non-null, argument added, removed or type-changed, input field changes, enum value added or removed, new deprecation, and root field added on Query/Mutation/Subscription.
    Task 4: Tests for shared tooling
    • Create tests/test_schema_tools.py with Test* classes. Store fixture SDL files in tests/fixtures/upstream/.
    • Cover these cases:
      • Docstring exclusion and keyword boundaries.
      • Non-empty container selections in both capability variants.
      • VM uuid present only for detected support.
      • Capability introspection operation included in the inventory.
      • Registry guard failure for an unregistered interpolated operation.
      • Wrapper attribution.
      • Expected validation passes and failures.
      • Coordinate mapping.
      • Every diff kind.
    • Reach full coverage of the new module.
    🤖 Prompt for AI agents
    Implement shared schema analysis in `script/schema_tools.py` for the watcher and
    validator. Build an accurate operation inventory with provenance and
    schema-derived capability variants.
    
    - Configure `graphql-core` in the `dev` extra and regenerate `uv.lock` with `uv
    lock`. Include `script` in Ruff `src`, pytest `pythonpath`, coverage `source`
    and mypy checks. Update the CI workflow and pre-commit configuration. If the
    mypy hook is isolated, add `graphql-core` to its dependencies.
    - Move `ExtractedQuery` and `extract_queries_from_source` into the shared
    module. Scan `client.py` and `capabilities.py`. Skip docstrings, enforce
    operation keyword boundaries and record interpolated operations as dynamic
    rather than emitting incomplete documents.
    - Register the container builders, the `typed_get_vms` query and
    `build_introspection_query()`. Render permissive and target-schema capability
    variants. Derive capabilities with `graphql_sync` and
    `ServerCapabilities.from_introspection_response`. Use dummy client credentials,
    preset `_capabilities` and a recording `AsyncMock` for the VM query. Do not
    access the network.
    - Fail on unregistered interpolated operation f-strings. Build an intra-file
    `self.<method>(` call graph. Attribute operations to all transitive public
    callers. Preserve all provenance during document deduplication. Exclude
    caller-supplied and script-owned documents.
    - Load SDL with `graphql.build_schema`. Validate operations with `graphql.parse`
    and `graphql.validate`. Include operations, methods and `file:line` in errors.
    - Use `TypeInfo`/`TypeInfoVisitor` to map selected coordinates to methods and
    source locations. Use string start lines plus document offsets for literals. Use
    field literal lines for dynamic sources, with builder start lines as fallback.
    - Emit typed schema changes with stable IDs based on kind, coordinate and
    before/after values. Cover every diff kind listed in Task 1.3.
    - Add `Test*` classes and SDL fixtures. Cover extraction, both capability
    variants, VM `uuid`, introspection inventory, registry failures, caller
    attribution, validation, coordinates and all diff kinds. Reach full module
    coverage.
    

    Phase 2: Upstream watcher scan

    Implement the read-only scan command. Produce one JSON report with findings, pre-rendered issue payloads and a summary.

    Task 1: GitHub access and release range
    • Create script/upstream_watch.py with a scan subcommand. Add --since, --until, --output and --repo (this repository, for markers). Accept an optional token from GITHUB_TOKEN.
    • Add one HTTPS fetch helper with a host allowlist (api.github.com, raw.githubusercontent.com), a timeout and clear errors. Inject the helper so tests can replace it. Keep Ruff S310 handling local to the helper.
    • Before using a tag or commit in a URL, validate tags against ^v\d+\.\d+\.\d+$ and commits against a 40-hex SHA. Sort releases with packaging.version. Skip drafts and pre-releases.
    • List this repository's issues with REST pagination and state: all. Skip pull requests. Do not use the search API. Read markers only from issues authored by github-actions[bot].
    • Read the highest <!-- upstream-release: vX -->, <!-- upstream-main: <sha> -->, all <!-- upstream-change: <id> --> IDs, and all group markers.
    • Apply these range rules:
      • Explicit --since/--until override markers.
      • Otherwise, process releases after the highest recorded release.
      • If no marker exists, process only the latest release against its predecessor.
      • If an explicit tag does not exist, fail with a clear message.
    • Resolve main to its HEAD SHA.
    Task 2: Schema diff, impact and classification
    • Fetch api/generated-schema.graphql at each consecutive tag pair and at latest release → main@sha. If a ref has no file, record "schema unavailable" for that step and continue.
    • For each step, run the diff and validate the inventory against the new schema. Attach selecting methods and file:line to each change.
    • Classify findings:
      • breaking (bug): an operation fails validation; a selected field or root was removed; a selected field or root changed from non-null to nullable.
      • enhancement: added types, fields and root fields that the library does not select; new deprecations, with a flag when the library selects the field.
      • no action (summary only): nullable → non-null, removed fields that are not selected, and enum and argument changes that do not break validation.
    • Drop changes whose ID already exists in an issue marker. Record the issue number in the summary as "already reported".
    Task 3: Release notes, linked PRs and behaviour notes
    • For each new release, read the release body. Extract linked unraid/api PR numbers, up to a fixed limit. Fetch each PR title and body.
    • Flag entries that change request handling. Use keyword rules for introspection, __schema/__type, sandbox, request limits, complexity/depth, auth/API key, rate limiting, CORS/CSRF, WebSocket and error shape.
    • Put behaviour notes in the release's bug group, in a separate "Behaviour changes" section. If a release has only behaviour notes, create a bug issue.
    Task 4: Report and issue payloads
    • Write one JSON report with:
      • range: the processed range.
      • One entry for each release and for main: schema status, changes, classification, behaviour notes and PR links.
      • issues[]: group key, title, labels, body and change IDs.
      • state: the new release and main markers.
      • summary_markdown: every release and change checked, including no-action items.
    • Use group keys: <tag>:bug, <tag>:enhancement, main@<sha7>:bug, main@<sha7>:enhancement.
    • Include these items in each body: the group marker, the upstream-release marker (or main marker), one upstream-change marker for each change, the schema diff, affected methods with file:line, and links to release notes and PRs.
    • Sanitise upstream text. Remove HTML comments so upstream text cannot inject markers. Neutralise mentions. Truncate bodies below the GitHub body limit.
    Task 5: Tests for the watcher
    • Create tests/test_upstream_watch.py with a fake fetcher and fixtures in tests/fixtures/upstream/.
    • Add trimmed SDL for v4.37.6 and v4.37.9 (docker: Docker! → docker: Docker, and the same change for vms). Add release notes that link #2095, #2123 and #2129.
    • Test this acceptance scenario: scan --since v4.37.6 --until v4.37.9 reports Query.docker and Query.vms as breaking. The report lists typed_get_containers, typed_get_containers_safe, get_containers, typed_get_vms, get_vms, get_container_update_statuses, get_docker_port_conflicts, get_docker_networks, typed_get_docker_networks, get_container_logs, typed_get_container_logs, get_docker_organizer and typed_get_docker_organizer. It flags introspection as a behaviour change.
    • Cover these cases:
      • A re-run with existing markers produces no issues.
      • Markers in closed issues still deduplicate.
      • Markers from non-bot authors are ignored.
      • A change reported from main is not reported again in a later release.
      • Invalid tags and missing tags fail.
      • A missing schema file gives a "schema unavailable" status.
      • Pagination skips pull requests.
      • Marker injection is sanitised.
      • The CLI entry point works.
    🤖 Prompt for AI agents
    Implement the read-only `scan` command in `script/upstream_watch.py`. Produce a
    JSON report with classified findings, sanitised issue payloads and a complete
    summary.
    
    - Add `--since`, `--until`, `--output` and `--repo`. Accept an optional
    `GITHUB_TOKEN`. Use one injectable HTTPS helper with the specified host
    allowlist, timeout and clear errors. Keep `S310` handling local.
    - Validate tags against `^v\d+\.\d+\.\d+$` and commits as 40-hex SHAs before URL
    use. Sort releases with `packaging.version`. Skip drafts and pre-releases.
    Resolve `main` to its HEAD SHA.
    - Read issues through paginated REST listing with `state: all`. Skip pull
    requests. Do not use search. Trust markers only from `github-actions[bot]`. Read
    release, main, change and group markers. Apply explicit range overrides,
    recorded-release defaults and the latest-versus-predecessor fallback. Fail
    clearly for missing explicit tags.
    - Fetch schemas for consecutive tag pairs and latest release to `main@sha`. If a
    schema is missing, record "schema unavailable" and continue. Diff schemas and
    validate the inventory against each new schema. Attach selecting methods and
    `file:line`.
    - Classify validation failures, selected removals and selected non-null →
    nullable changes as `bug`. Classify unselected additions and new deprecations as
    enhancements. Flag selected deprecations. Keep nullable → non-null, unselected
    removals and non-breaking enum or argument changes in the summary only.
    - Suppress change IDs already present in markers. Include the existing issue
    number as "already reported" in the summary.
    - Read new release bodies and linked `unraid/api` PR titles and bodies up to a
    fixed limit. Apply the request-handling keyword rules from Task 2.3. Put
    behaviour notes in a separate "Behaviour changes" section of the release's bug
    group. Create a bug payload even when behaviour notes are the only findings.
    - Emit the report fields and group keys specified in Task 2.4. Include all
    required markers, schema diffs, source provenance and release/PR links in issue
    bodies. Remove HTML comments, neutralise mentions and truncate below the GitHub
    body limit.
    - Use a fake fetcher and deterministic fixtures for tests. Cover the full
    v4.37.6 → v4.37.9 acceptance scenario and every marker, range, missing-schema,
    pagination, sanitisation and CLI case in Task 2.5. Keep the real range scan as a
    manual check. Do not fix the null-root `AttributeError`; #135 tracks that fix.
    

    Phase 3: Offline --schema-file for scripts/validate-schema.py

    Remove the dependency on introspection.

    Task 1: Schema source selection
    • Update scripts/validate-schema.py. Insert script/ into sys.path. Import the inventory, SDL loader and validation from schema_tools. Remove the moved extractor.
    • Add these options:
      • --schema-file PATH: SDL file.
      • --api-version: optional override for the default fetch.
      • --introspect: the old path.
    • With no options, connect, call get_version(), normalise to v{api}, and fetch upstream SDL through the shared fetch helper. If api is "unknown" or does not parse, exit 1. Tell the user to pass --schema-file or --api-version.
    • If --schema-file is given with --schema-only, branch before load_env() and UnraidClient creation. Do not require credentials or a server.
    Task 2: Reporting and model comparison on SDL
    • For SDL sources, validate the full inventory with graphql.validate. Map errors to ValidationIssue ERROR entries. Keep print_validation_report and exit codes unchanged.
    • Add an adapter from the graphql-core schema to the existing SchemaType/SchemaField map. Keep print_schema_summary, compare_models_to_schema and --dump-schema working.
    • Update the module docstring and argparse epilog.
    Task 3: Tests for the validator
    • Create tests/test_validate_schema_script.py. Load the script with importlib.util.spec_from_file_location. Register it in sys.modules.
    • Cover these cases:
      • --schema-file --schema-only does not call load_env or UnraidClient.
      • The default path fetches SDL for the mocked server version.
      • An unknown version exits 1.
      • The adapter output.
      • A failing operation gives exit 1.
    🤖 Prompt for AI agents
    Update `scripts/validate-schema.py` to use shared schema tooling and support
    offline SDL validation. Fetch upstream SDL by default and retain introspection
    as an explicit option.
    
    - Insert `script/` into `sys.path`. Import the inventory, SDL loader and
    validation from `schema_tools`. Remove the moved extractor.
    - Add `--schema-file PATH`, `--api-version` and `--introspect`. For the default
    path, connect, call `get_version()`, normalise the API version to `v{api}` and
    fetch SDL through the shared fetch helper. If the version is `"unknown"` or does
    not parse, exit 1 and direct the user to `--schema-file` or `--api-version`.
    - When `--schema-file` and `--schema-only` are both set, branch before
    `load_env()` and client creation. Require no credentials or server.
    - Validate the full SDL-backed inventory with `graphql.validate`. Convert errors
    to `ValidationIssue` ERROR entries. Preserve report behaviour and exit codes.
    - Adapt graphql-core schemas to the existing `SchemaType`/`SchemaField` map.
    Preserve schema summaries, model comparison and `--dump-schema`. Update the
    docstring and argparse epilog.
    - Load the script in tests with `importlib.util.spec_from_file_location` and
    register it in `sys.modules`. Test offline isolation, version-based fetching,
    unknown-version failure, adapter output and validation-failure exit status.
    - Do not add `typed_get_containers_safe` to the live-method list.
    

    Phase 4: Workflow, reporting job and documentation

    Connect the scan and reporting to CI automation. Document the change.

    Task 1: Upstream watch workflow
    • Create .github/workflows/upstream-watch.yml.
    • Add a daily schedule trigger with the timezone pattern from stale.yml. Add workflow_dispatch with optional since/until inputs.
    • Set permissions: {} at the top level. Add a concurrency group without cancel-in-progress.
    • Configure the scan job:
      • Permissions: contents: read, issues: read.
      • Steps: checkout with persist-credentials: false; astral-sh/setup-uv with python-version: "3.14.2" and no setup-python; uv sync --frozen --all-extras; uv run python script/upstream_watch.py scan.
      • Pass dispatch inputs through env. Do not use inline expression interpolation.
      • Upload report.json as an artifact.
    • Pin actions/upload-artifact, actions/download-artifact and actions/github-script to verified full commit SHAs with version comments. These actions are new to this repository. Look up the SHAs. Do not guess them.
    Task 2: Report job
    • Configure the report job with needs: scan, issues: write only, and no checkout.
    • Download the artifact. Then run actions/github-script inline:
      • List issues with state: all and pagination. Skip pull requests. Read markers from bot authors only.
      • Ensure bug, enhancement and upstream exist. Create a missing label before use.
      • For each payload whose group marker does not exist, create the issue. Include labels in the create call.
      • Then create or update the closed Upstream watch state issue with the new upstream-release/upstream-main markers. Label it upstream only. Never label it bug or enhancement.
      • Write summary_markdown and links to created or existing issues with core.summary.
    Task 3: Documentation and changelog
    • Update CONTRIBUTING.md:
      • Add an "Upstream watch" section. Cover the schedule, labels, markers, state issue, local scan command and validate-schema.py --schema-file usage.
      • Add upstream to the labels section.
    • Update AGENTS.md to state that script/ is linted and type-checked.
    • Update CHANGELOG.md under [Unreleased]:
      • Added: the workflow, scan and --schema-file.
      • Changed: the validator's default schema source and the graphql-core dev dependency.
    • Run locally: uv run pytest, ruff check, ruff format --check, mypy src script, pre-commit run --all-files, and an Aikido scan on new and modified files.
    🤖 Prompt for AI agents
    Add the `Upstream watch` workflow with separate read-only scan and issue-writing
    report jobs. Document the tooling and run the required checks.
    
    - Create `.github/workflows/upstream-watch.yml`. Use a daily schedule with the
    timezone pattern from `stale.yml` and optional dispatch inputs `since`/`until`.
    Set top-level `permissions: {}` and a concurrency group without
    cancel-in-progress.
    - Give `scan` only `contents: read` and `issues: read`. Check out with
    `persist-credentials: false`. Use `astral-sh/setup-uv` with Python `3.14.2`,
    without `setup-python`. Run `uv sync --frozen --all-extras` and the scan
    command. Pass dispatch inputs through `env`, not inline expression
    interpolation. Upload `report.json`.
    - Look up and verify full commit SHAs for upload-artifact, download-artifact and
    github-script against release tags. Add version comments. Do not guess SHAs.
    - Give `report` only `issues: write`, set `needs: scan` and omit checkout.
    Download the report and use a thin inline github-script step. List all issues
    with pagination, skip pull requests and trust bot-authored markers only.
    - Ensure the three labels exist. Create issues only when their group markers are
    absent. Include labels in the issue create call. Then create or update the
    closed state issue with release and main markers. Apply only `upstream` to the
    state issue. Write the report summary and created or existing issue links with
    `core.summary`.
    - Update CONTRIBUTING with schedule, labels, markers, state issue and local
    command usage. Update AGENTS for linting and type-checking of `script/`. Add the
    specified Added and Changed entries under `[Unreleased]` in CHANGELOG.
    - Run `uv run pytest`, `ruff check`, `ruff format --check`, `mypy src script`,
    `pre-commit run --all-files` and an Aikido scan on new and modified files. Keep
    upstream text sanitised before it reaches issue bodies.
    
    Research

    Ticket Summary

    Add an Upstream watch workflow. The workflow scans unraid/api releases and api/generated-schema.graphql at release tags and at main. It validates every library operation against the new schema. It opens deduplicated issues with bug or enhancement, plus upstream. Add --schema-file to scripts/validate-schema.py so the validator works on servers that refuse introspection (4.37.8 and later).

    Codebase Summary

    All GraphQL operations are inline in src/unraid_api/client.py. build_introspection_query() in src/unraid_api/capabilities.py builds capability introspection. The container queries (_build_containers_query, _build_containers_query_safe) and the VM query in typed_get_vms are capability-gated f-strings.

    scripts/validate-schema.py has an AST extractor, extract_queries_from_source. The extractor drops f-string interpolations, which empties the container selections. It attributes operations to builders, not public callers. The validator always introspects a live server. It has no SDL loader. parse_schema() is unused.

    scripts/ is excluded from Ruff. mypy checks only src/. Coverage measures only src/unraid_api at 95%. graphql-core is not a dependency.

    Workflows pin actions to full SHAs. They use uv sync --frozen --all-extras, credential-free checkout and job-scoped permissions. Tests use pytest classes with unittest.mock and fixtures in tests/conftest.py. CHANGELOG follows Keep a Changelog under [Unreleased].

    File-level Change Summary

    File Change
    pyproject.toml, uv.lock Add graphql-core to the dev extra. Include script in Ruff src, pytest pythonpath and coverage source.
    .github/workflows/ci.yml, .pre-commit-config.yaml Make mypy check script and src.
    script/schema_tools.py (new) Add the SDL loader, operation inventory (static extraction, dynamic registry and caller attribution), validation, coordinate mapping and schema diff.
    script/upstream_watch.py (new) Add the scan CLI, GitHub fetch, markers, range, classification, behaviour notes, JSON report and issue payloads.
    scripts/validate-schema.py Import shared tooling. Add --schema-file, --api-version and --introspect. Use upstream SDL by default. Add the SDL → SchemaType adapter.
    .github/workflows/upstream-watch.yml (new) Add the read-only scan job and the report job with issues: write.
    tests/test_schema_tools.py, tests/test_upstream_watch.py, tests/test_validate_schema_script.py, tests/fixtures/upstream/ (new) Add full coverage, including the v4.37.6 → v4.37.9 fixture scenario.
    CONTRIBUTING.md, AGENTS.md, CHANGELOG.md Add documentation and [Unreleased] entries.

    Instructions for follow-up agents

    • Do not fix the library's AttributeError on a null docker/vms root. #135 tracks that fix.
    • Do not add typed_get_containers_safe to the validator's live-method list. That change is out of scope.
    • Verify each new action SHA against its release tag before you commit.
    • Sanitise upstream text before it reaches issue bodies.

    🚀 Next Steps

    🤖 All AI agent prompts combined
    Task: 1
    
    Implement shared schema analysis in `script/schema_tools.py` for the watcher and
    validator. Build an accurate operation inventory with provenance and
    schema-derived capability variants.
    
    - Configure `graphql-core` in the `dev` extra and regenerate `uv.lock` with `uv
    lock`. Include `script` in Ruff `src`, pytest `pythonpath`, coverage `source`
    and mypy checks. Update the CI workflow and pre-commit configuration. If the
    mypy hook is isolated, add `graphql-core` to its dependencies.
    - Move `ExtractedQuery` and `extract_queries_from_source` into the shared
    module. Scan `client.py` and `capabilities.py`. Skip docstrings, enforce
    operation keyword boundaries and record interpolated operations as dynamic
    rather than emitting incomplete documents.
    - Register the container builders, the `typed_get_vms` query and
    `build_introspection_query()`. Render permissive and target-schema capability
    variants. Derive capabilities with `graphql_sync` and
    `ServerCapabilities.from_introspection_response`. Use dummy client credentials,
    preset `_capabilities` and a recording `AsyncMock` for the VM query. Do not
    access the network.
    - Fail on unregistered interpolated operation f-strings. Build an intra-file
    `self.<method>(` call graph. Attribute operations to all transitive public
    callers. Preserve all provenance during document deduplication. Exclude
    caller-supplied and script-owned documents.
    - Load SDL with `graphql.build_schema`. Validate operations with `graphql.parse`
    and `graphql.validate`. Include operations, methods and `file:line` in errors.
    - Use `TypeInfo`/`TypeInfoVisitor` to map selected coordinates to methods and
    source locations. Use string start lines plus document offsets for literals. Use
    field literal lines for dynamic sources, with builder start lines as fallback.
    - Emit typed schema changes with stable IDs based on kind, coordinate and
    before/after values. Cover every diff kind listed in Task 1.3.
    - Add `Test*` classes and SDL fixtures. Cover extraction, both capability
    variants, VM `uuid`, introspection inventory, registry failures, caller
    attribution, validation, coordinates and all diff kinds. Reach full module
    coverage.
    ===============================================================================
    
    Task: 2
    
    Implement the read-only `scan` command in `script/upstream_watch.py`. Produce a
    JSON report with classified findings, sanitised issue payloads and a complete
    summary.
    
    - Add `--since`, `--until`, `--output` and `--repo`. Accept an optional
    `GITHUB_TOKEN`. Use one injectable HTTPS helper with the specified host
    allowlist, timeout and clear errors. Keep `S310` handling local.
    - Validate tags against `^v\d+\.\d+\.\d+$` and commits as 40-hex SHAs before URL
    use. Sort releases with `packaging.version`. Skip drafts and pre-releases.
    Resolve `main` to its HEAD SHA.
    - Read issues through paginated REST listing with `state: all`. Skip pull
    requests. Do not use search. Trust markers only from `github-actions[bot]`. Read
    release, main, change and group markers. Apply explicit range overrides,
    recorded-release defaults and the latest-versus-predecessor fallback. Fail
    clearly for missing explicit tags.
    - Fetch schemas for consecutive tag pairs and latest release to `main@sha`. If a
    schema is missing, record "schema unavailable" and continue. Diff schemas and
    validate the inventory against each new schema. Attach selecting methods and
    `file:line`.
    - Classify validation failures, selected removals and selected non-null →
    nullable changes as `bug`. Classify unselected additions and new deprecations as
    enhancements. Flag selected deprecations. Keep nullable → non-null, unselected
    removals and non-breaking enum or argument changes in the summary only.
    - Suppress change IDs already present in markers. Include the existing issue
    number as "already reported" in the summary.
    - Read new release bodies and linked `unraid/api` PR titles and bodies up to a
    fixed limit. Apply the request-handling keyword rules from Task 2.3. Put
    behaviour notes in a separate "Behaviour changes" section of the release's bug
    group. Create a bug payload even when behaviour notes are the only findings.
    - Emit the report fields and group keys specified in Task 2.4. Include all
    required markers, schema diffs, source provenance and release/PR links in issue
    bodies. Remove HTML comments, neutralise mentions and truncate below the GitHub
    body limit.
    - Use a fake fetcher and deterministic fixtures for tests. Cover the full
    v4.37.6 → v4.37.9 acceptance scenario and every marker, range, missing-schema,
    pagination, sanitisation and CLI case in Task 2.5. Keep the real range scan as a
    manual check. Do not fix the null-root `AttributeError`; #135 tracks that fix.
    ===============================================================================
    
    Task: 3
    
    Update `scripts/validate-schema.py` to use shared schema tooling and support
    offline SDL validation. Fetch upstream SDL by default and retain introspection
    as an explicit option.
    
    - Insert `script/` into `sys.path`. Import the inventory, SDL loader and
    validation from `schema_tools`. Remove the moved extractor.
    - Add `--schema-file PATH`, `--api-version` and `--introspect`. For the default
    path, connect, call `get_version()`, normalise the API version to `v{api}` and
    fetch SDL through the shared fetch helper. If the version is `"unknown"` or does
    not parse, exit 1 and direct the user to `--schema-file` or `--api-version`.
    - When `--schema-file` and `--schema-only` are both set, branch before
    `load_env()` and client creation. Require no credentials or server.
    - Validate the full SDL-backed inventory with `graphql.validate`. Convert errors
    to `ValidationIssue` ERROR entries. Preserve report behaviour and exit codes.
    - Adapt graphql-core schemas to the existing `SchemaType`/`SchemaField` map.
    Preserve schema summaries, model comparison and `--dump-schema`. Update the
    docstring and argparse epilog.
    - Load the script in tests with `importlib.util.spec_from_file_location` and
    register it in `sys.modules`. Test offline isolation, version-based fetching,
    unknown-version failure, adapter output and validation-failure exit status.
    - Do not add `typed_get_containers_safe` to the live-method list.
    ===============================================================================
    
    Task: 4
    
    Add the `Upstream watch` workflow with separate read-only scan and issue-writing
    report jobs. Document the tooling and run the required checks.
    
    - Create `.github/workflows/upstream-watch.yml`. Use a daily schedule with the
    timezone pattern from `stale.yml` and optional dispatch inputs `since`/`until`.
    Set top-level `permissions: {}` and a concurrency group without
    cancel-in-progress.
    - Give `scan` only `contents: read` and `issues: read`. Check out with
    `persist-credentials: false`. Use `astral-sh/setup-uv` with Python `3.14.2`,
    without `setup-python`. Run `uv sync --frozen --all-extras` and the scan
    command. Pass dispatch inputs through `env`, not inline expression
    interpolation. Upload `report.json`.
    - Look up and verify full commit SHAs for upload-artifact, download-artifact and
    github-script against release tags. Add version comments. Do not guess SHAs.
    - Give `report` only `issues: write`, set `needs: scan` and omit checkout.
    Download the report and use a thin inline github-script step. List all issues
    with pagination, skip pull requests and trust bot-authored markers only.
    - Ensure the three labels exist. Create issues only when their group markers are
    absent. Include labels in the issue create call. Then create or update the
    closed state issue with release and main markers. Apply only `upstream` to the
    state issue. Write the report summary and created or existing issue links with
    `core.summary`.
    - Update CONTRIBUTING with schedule, labels, markers, state issue and local
    command usage. Update AGENTS for linting and type-checking of `script/`. Add the
    specified Added and Changed entries under `[Unreleased]` in CHANGELOG.
    - Run `uv run pytest`, `ruff check`, `ruff format --check`, `mypy src script`,
    `pre-commit run --all-files` and an Aikido scan on new and modified files. Keep
    upstream text sanitised before it reaches issue bodies.
    
    💡 Iterate on the plan with: @coderabbitai <feedback>
    Example Feedback
    - @coderabbitai You can skip phase 3. Add a simple unit test case for phase 2.
    - @coderabbitai For design choice 1 go ahead with option 3 and replan.
    

    Start Coding →

  3. coderabbitai commented on Oct 30, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #139 - feat: watch unraid/api releases and schema changes [closed]
    #137 - fix: handle Unraid API 4.37.8 nullable roots and disabled introspection [closed]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or improvement request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions