Skip to content

✨ publish: Linear target (issue + sub-issues, blocks relations) - #118

Merged
nicholasjackson merged 2 commits into
feat/publish-jirafrom
feat/publish-linear
Nov 9, 2026
Merged

nicholasjackson merged 2 commits into
feat/publish-jirafrom
feat/publish-linear

Conversation

@clubanderson

@clubanderson clubanderson commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Part of #113 — what is still open there: parent_as: project (RFC as a Linear project rather than a parent issue). Everything else in #113 is here.

Stacked on #117 (Jira) because it shares Filed.Key / SameAsFiled / ResultChild.Key from that PR; once #117 merges this rebases to a single commit. Follows the GitHub target pattern exactly: same Target interface, same ordering (RFC → children → relations → RFC checklist rewrite), same refusal codes with next_action, dry-run with zero network calls, httptest GraphQL fake.

What it does

  • RFC → an issue in publish.linear.team; each child → a sub-issue of it (parentId). Hive's Linear source already marks an issue with children (or a marker without task=) as a tracker, so the RFC is never claimed.
  • depends_on → issueRelationCreate(issueId: blocker, relatedIssueId: dependant, type: blocks) (Linear's enum has blocks, not blockedBy; the dependant's inverseRelations then carries the blocker, which is what linearDependencies in hive reads). Parent and relations are read back (parent { id }, inverseRelations) and reconciled on every run; anything missing is refiled and reported as linked. Failures are warnings.
  • Labels and publish.linear.parent_label (default epic) applied only when the team or workspace has them; never created (GitHub semantics).
  • publish.linear.state optional: unset → the team's Todo state, else its first unstarted state, else the team default (so children don't land in Backlog/Triage where a hive doesn't look). publish.linear.assignee (email / display name / name via users) → assigneeId on every issue. Unknown team, state or assignee is refused before any mutation; these lookups only run when something is created, so a no-op re-run makes a single issues query.
  • Find: issues(filter: {team: {key: {eqIgnoreCase}}, description: {contains: "artifact_id=<id>"}}, includeArchived: true), paged via pageInfo, so an auto-archived completed RFC is still found and refused as closed.
  • Idempotency: a <!-- spek:digest <sha256> --> line before the marker carries the digest of the rendered body; Find reports BodyDigest, so comparison does not depend on Linear returning Markdown byte-for-byte (the fake now normalises whitespace on store to prove it). Caveat, stated plainly: whether Linear preserves HTML comments in descriptions at all is something I have not verified against a real workspace (no key available here); the marker itself depends on it exactly as the GitHub target depends on GitHub keeping them. If it strips them, the fallback would be a visible footer line — flagging for whoever has a workspace.
  • Auth: LINEAR_API_KEY sent bare in Authorization (hive convention). LINEAR_API_URL overrides the endpoint for tests.
  • Refusals: publish_target_unconfigured (no team / unknown target), publish_auth_missing.

Crux

Checked crux/proxy/linear_rules.go: issueCreate, issueUpdate, issueRelationCreate are allowed at ModeIssuesOnly and queries pass, so publishing from inside a hive needs no crux change. Recorded in docs/publish-hive-handshake.md.

Tests

make lint && make test green locally (internal/publish/linear: reconciliation, archived + paging, digest vs normalised store, Todo/unstarted default, assignee, workspace labels + key case; lineartest fake routes on the document's first field, uses assert on handler goroutines, Snapshot(); cmd tests for dry-run with zero calls, refusals, file-then-update, assignee + archived RFC conflict).

@kubestellar-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign nicholasjackson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two gaps need addressing before this fulfills #113:

  • Medium — dependency relations are never retried or added between existing children (internal/publish/linear/linear.go:306). blockedBy runs only when one endpoint appears in res.Created. After a relation fails, both issues still exist; the next --update finds them and skips the relation permanently. Adding a dependency between two existing children likewise updates the Markdown but creates no native blocks relation. Hive can then schedule the dependant without its blocker. This contradicts the PR body's promise that relation failures are retried on --update. Reconcile missing relations for existing children too, with coverage for a failed first attempt and a newly added dependency.

  • Medium — the required assignment option is absent (internal/config/config.go:441, internal/publish/linear/linear.go:341). #113 explicitly requires publish.linear.assignee so children are visible when Hive uses assigned_only. The config has no assignee field and the create input never supplies assigneeId, leaving children unassigned and invisible to that configuration. Implement the option and exercise it in the GraphQL fixture, or explicitly leave this part of #113 open rather than closing the entire issue.

Read the command's Find/Diff/Publish path, rendering, target implementation and fake. The build/test CI check is green; local test execution is disabled in this reviewer environment.

— hive: agent=reviewer backend=codex model=gpt-6.1-sol codex=0.167.0

@nicholasjackson

Copy link
Copy Markdown
Collaborator

@clubanderson some comments from claude when it was revieing

Relations and parent are never repaired. blocks relations are created only for new issues (linear.go:301-308), and updateIssue (:373) never sets parentId for existing children (:262-272). “Retried on --update” in the description isn’t true. This is the same gap as in #117 and main’s GitHub target, so it’s best fixed once across all three.
Archived issues. The Find query (:79) doesn’t pass includeArchived: true. Once Linear auto-archives a completed RFC, a re-publish can’t see it: it skips the closed-RFC refusal and files everything again as new open work.
Description round-trip. Idempotency compares the stored description verbatim, marker included (:128). Does Linear return the markdown and the HTML comment exactly as sent? The fake stores strings verbatim, so the tests can’t tell. If Linear changes them, every re-run either reports a conflict or duplicates. A body digest like the Jira target’s, or a recorded round-trip against a real workspace, would settle it.
“Fixes #113” overclaims. There’s no assignee, no parent_as: project, and children land in the team’s default state (often Backlog or Triage), not Todo. Either implement these or change it to “Part of #113”.
Smaller items:
Labels are read only from team.labels (:157). Are workspace-level labels found?
Team and state lookups run on every Publish, including no-op re-runs (:229-236). A failure there reports publish_target_failed, which reads as “failed part way”.
Team keys are compared case-sensitively.
first: 250 has no pagination (:92).

Add a Linear work-source target behind the publish Target interface. The RFC
is filed as an issue in publish.linear.team and each child as a sub-issue of
it (parentId), which hive already reads as a tracker; depends_on edges become
blocks relations from the blocker, best effort. Labels (and the parent label
on the RFC) are applied only when the team has them, new issues go to the
team default state or publish.linear.state, and an unknown team or state is
refused before anything is filed. Linear stores Markdown, so bodies round-trip
verbatim and a re-publish finds its items by the marker in the description.
Auth is LINEAR_API_KEY, sent bare as hive does; LINEAR_API_URL points tests
at a fake.

Includes an in-process fake GraphQL API (lineartest) and unit/command tests
covering create, idempotent re-run, --update, relation failures as warnings,
closed-state detection, refusals and dry-run making no network call.

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
…t, assignee, Todo default

- parent and blocks relations are read back (parent { id },
  inverseRelations) and refiled when missing on every run, reported as
  linked
- Find uses includeArchived: true and pages with pageInfo/after, so an
  auto-archived completed RFC is found and refused as closed rather than
  duplicated; team key compared with eqIgnoreCase
- a <!-- spek:digest --> line before the marker carries the digest of
  the rendered body; Find reports BodyDigest and the comparison no longer
  assumes Linear returns Markdown verbatim (the fake now normalises
  whitespace on store)
- publish.linear.assignee resolved through users (email, display name or
  name) to assigneeId on every filed issue
- default state is the team's Todo, else its first unstarted state, else
  the team default; workspace labels count as existing
- team, state, label and user lookups happen only when something is
  created, so a no-op run makes one issues query
- fake routes on the document's first field, asserts instead of
  require on handler goroutines, Snapshot/Calls accessors, Archived and
  NoTodo options

Signed-off-by: Andrew Anderson <andy@clubanderson.com>

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decision needed: @nicholasjackson should decide whether the Linear endpoint override must enforce the existing HTTPS credential-transport policy before landing.

Credential transport needs a maintainer decision before this lands.

Medium — API key can be sent over non-loopback HTTP. cmd/publish.go:445-446 accepts LINEAR_API_URL verbatim; internal/publish/linear/linear.go:630-641 then sends Authorization with the API key to that endpoint. An operator setting an HTTP proxy endpoint therefore exposes the credential in cleartext. Unlike jiraTarget, this path never calls the existing requireHTTPS guard. Please enforce HTTPS while retaining loopback HTTP for the fake server, and add a refusal test for non-loopback HTTP.

The current head repairs parents/relations and searches archived issues, addressing those earlier review concerns. Per #113 and the PR body, project-mode publishing is deliberately left open; that is not a scope defect. I read the adapter, fake, tests, command callers and base implementation. Reported Dagger build-and-test checks pass; local tests were blocked by hive policy. Real-workspace HTML-comment preservation remains unverified as the body acknowledges.

— hive: agent=reviewer backend=codex model=gpt-6.1-sol codex=0.167.0

@clubanderson

clubanderson commented Nov 8, 2026 •

Copy link
Copy Markdown
Member Author

Thanks — all addressed in 0cf22c8 (re-based on #117's a151ea7).

Relations and parent never repaired. Fixed. Publish now reads each existing child's parent { id } and inverseRelations { type issue { id } } and reconciles parentId and blocks relations on every run, including no-op re-runs and existing children, so a failed link, a new depends_on or a re-created RFC is repaired. Same shape as the Jira fix in #117; the GitHub target on main will get it in a separate PR.

Archived issues. Fixed. The Find query passes includeArchived: true and pages (first/after, PageSize), so a completed-and-auto-archived RFC is found and refused as closed rather than re-filed. The fake has an Archived flag and the #119 e2e closes the RFC with StateType=completed, Archived=true and asserts the refusal.

Description round-trip. Fixed structurally: the description now carries <!-- spek:digest <sha256> --> next to the marker and idempotence compares digests, not stored text, so Linear re-rendering markdown can no longer cause spurious conflicts or duplicates. The fake normalises stored descriptions (whitespace/line endings) rather than storing verbatim, so the tests do exercise a non-identical round-trip. What I can't do from here is a recorded run against a real workspace (I have no Linear key); if Linear were to strip HTML comments entirely the marker would be lost too, and that's stated as an open caveat in the PR body and the handshake doc rather than claimed.

"Fixes #113" overclaims. Agreed; changed to "Part of #113". Of the three gaps: assignee is now implemented (publish.linear.assignee, resolved via users); children are filed in Todo — the target resolves the team's Todo state, else its first unstarted state, else the team default, so they land where a hive reads rather than Backlog/Triage; parent_as: project is still open on #113.

Smaller items, all fixed: workspace labels are found (issueLabels(filter: { team: { null: true } }) is merged with team.labels); team/state/label/user lookups are lazy, so a no-op re-run does Find only (the #119 e2e asserts the fake's issue/relation counts are unchanged and that a dry run makes zero calls); team keys compare with eqIgnoreCase; first: 250 is replaced by paging with pageInfo { hasNextPage endCursor }, and linear_test.go runs with PageSize = 2 so paging is exercised.

Green locally and in CI.

@nicholasjackson

Copy link
Copy Markdown
Collaborator

/approve

@nicholasjackson
nicholasjackson merged commit 8f6fd66 into main Nov 9, 2026
6 of 7 checks passed
@kubestellar-prow
kubestellar-prow Bot deleted the feat/publish-linear branch November 9, 2026 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants