Skip to content

Platform adapters

How to connect a hosting platform (GitHub, GitLab, Codeberg, Jira, a local directory, …) to the nebuko engine, and what the engine expects from it.

src/nebuko/adapters/base.py::PlatformAdapter is the single platform seam (issue #590, Stage A). The engine is platform-free: nebuko.engine.* speaks only the types defined in the port module — Trigger, CommentEntry (engine side), and, since the port redesign, PostedComment, PosterCapabilities and HistoryPage. A host implements the port once; the engine never learns which platform it is talking to.

Every adapter implements the required methods; the opt-in methods may raise NotImplementedError until the platform needs them.

MethodReturnRequired
read_trigger()Triggeryes
load_history(limit=None)Sequence[CommentEntry]yes
post(body)PostedCommentyes
capabilities()PosterCapabilitiesyes
load_state() / save_state(state)WorkflowState | None / Noneyes
delete_issue_comments(issue, from_comment="")intyes
artifact_dir() / write_artifact(name, content)Path / Noneyes
edit_comment(comment_id, body)PostedCommentopt-in
add_reaction(comment_id, emoji)Noneopt-in
load_history_page(cursor=None)HistoryPageopt-in

The typed result of post() — callers no longer parse an untyped id/url string:

class PostedComment(BaseModel):
comment_id: str
permalink: Optional[str] = None # human-readable URL, when the platform has one
anchor: Optional[str] = None # in-page anchor, when distinct from the permalink
author: Optional[str] = None
created_at: Optional[str] = None # ISO-8601, when the platform reports it

On GitHub, permalink and anchor are the same html_url; platforms with separate in-page anchors (e.g. GitLab note anchors) populate them differently.

A frozen model declaring what the platform’s comment renderer supports:

FlagDefaultMeaning
markdownTrueMarkdown is rendered at all
detailsTrue<details> collapsible blocks render
tasklistTrue- [ ] task lists render
comment_anchorTruecomments have stable in-page anchors
edit_commentFalseedit_comment is implemented
reactionsFalseadd_reaction is implemented
history_paginationFalseload_history_page is implemented
raw_htmlFalsearbitrary raw HTML survives rendering

Declare honestly: a flag claims a capability the platform actually has, not one you wish it had.

One page of paginated history: entries (untyped this round — refined when the first paginating adapter lands) and next_cursor (None when exhausted).

Checklist:

  1. Implement every required method above, plus capabilities().
  2. Return PostedComment from post() — populate at least comment_id.
  3. Declare honest capability flags in capabilities().
  4. Add the opt-in methods you do not support yet as explicit raise NotImplementedError(...) one-liners, so the port surface stays uniform and callers fail loudly instead of silently.
  5. Extend PORT_METHODS and the conformance tests in tests/engine/test_platform_adapter.py (see below).

Engine renderers must not emit markup a platform cannot render. They build a CommentRenderer from the adapter’s capabilities once, then route the assembled body through it (nebuko/adapters/rendering.py):

from nebuko.adapters.rendering import CommentRenderer
renderer = CommentRenderer(self.adapter.capabilities())
body = renderer.render(assembled_body)

Degradation rules: when details is False, <details> blocks are unwrapped to their content with the <summary> kept as a heading line; when tasklist is False, - [ ] / - [x] markers become plain - bullets; raw HTML is escaped unless raw_html is True.

Before (GitHub, all capabilities on):

<details>
<summary>Cost breakdown</summary>
- [x] measured
- [ ] estimated
</details>

After (Jira, details=False, tasklist=False):

Cost breakdown
- measured
- estimated

Each step’s output is the comment that posts it. The engine records a StepOutputRecord (step, comment_id, plus permalink, anchor, posted_at taken from the PostedComment) in the workflow state under current_context["step_outputs"]. upsert_into is latest write wins: a re-run of the same step replaces the existing locator for that step, so state always points at the newest version of the comment. On state restore, the engine resolves the locator against the bounded thread read to find the prior comment’s body.

Copy the conformance pattern from tests/engine/test_platform_adapter.py:

  • every name in PORT_METHODS exists on your adapter class;
  • post() returns a PostedComment instance with a non-empty comment_id;
  • capabilities() returns a PosterCapabilities instance;
  • unsupported opt-in methods raise NotImplementedError when called;
  • a degradation test drives CommentRenderer with reduced capabilities over a details/task-list body and asserts plain-text output.