githubquestions

package
v0.9.20 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package githubquestions implements suspend.QuestionStore over GitHub PR (or issue) comments, so the human half of an ask-a-friend pause happens where humans already are: the question appears as a comment on the conversation the paused run belongs to, and a collaborator answers by replying

/answer <text>

How state is carried

The conversation itself is the store. Ask posts a comment whose visible text is the question and whose hidden HTML marker carries the full suspend.Question as base64 JSON — base64 so the marker survives any prompt content (a literal "-->" would otherwise terminate the HTML comment). The newest marker-bearing comment is the pending question; Consume rewrites that comment (marker retired, "answered" note appended) so it stops being pending. Nothing is stored outside GitHub, so a different process — or a different machine — resumes the lifecycle from the conversation alone.

Authorization

Only replies whose author association is OWNER, MEMBER, or COLLABORATOR are accepted, and app/bot authors are ignored. Answering steers a paused agent, so the ability to answer is deliberately tied to write-side repository membership rather than to anyone who can comment.

The question channel is gated the other way around: pending-question markers are only honored in app/bot-authored comments, since questions are posted by the reconciler's app identity. A marker forged by a human commenter — which would otherwise supersede the real question and deny the resume — is ignored. Apps installed on the repository remain trusted; they are admin-granted.

This transport's friend is deliberately a human: app and bot replies are rejected. An agent answering as a peer arrives as a sibling QuestionStore with its own authorization model, not by loosening this one.

Race semantics

GitHub comment edits are not compare-and-swap. The lifecycle's exactly-once guarantee comes from the checkpoint Store's claim (a CAS delete on the parked envelope), not from this transport: a racing Consume or a superseding Ask degrades to an extra comment edit, never a double resume. Nonce binding still applies — an answer is only surfaced for the question whose marker is currently pending, so a reply to a stale, superseded question is never injected.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is a suspend.QuestionStore that keeps the human transport on GitHub: Ask posts the question as a PR (or issue) comment carrying the pause nonce in a hidden marker, a collaborator replies with "/answer <text>", and Consume edits the question comment so it stops being pending. The conversation itself is the store — no state lives anywhere else, so a completely different process can pick the answer up.

Keys must be in the githubreconciler PR/issue form "owner/repo#number".

Unlike a CAS-backed store, GitHub comment edits are not atomic; the exactly-once guarantee of the lifecycle comes from the checkpoint Store's claim, not from this transport. Answer authorization is by the commenter's author association: OWNER, MEMBER, or COLLABORATOR.

func New

func New(clients *githubreconciler.ClientCache) *Store

New returns a Store that talks to GitHub through clients, which supplies a per-repository authenticated client (the same OctoSTS-backed cache the reconciler uses).

Example

ExampleNew demonstrates the question lifecycle on a PR conversation: the coordinator posts the question as a comment, polls for a collaborator's "/answer <text>" reply, and retires the comment once the answer is consumed.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"chainguard.dev/driftlessaf/agents/suspend"
	"chainguard.dev/driftlessaf/agents/suspend/githubquestions"
	"chainguard.dev/driftlessaf/reconcilers/githubreconciler"
)

func main() {
	ctx := context.Background()

	// The same OctoSTS-backed client cache the reconciler uses.
	clients := githubreconciler.NewClientCache(nil)
	questions := githubquestions.New(clients)

	const key = "org/repo#42" // githubreconciler PR/issue key form
	if err := questions.Ask(ctx, key, suspend.Question{
		ID:      "pause-nonce-1",
		Key:     key,
		Prompt:  "Deploy to staging or production?",
		AskedAt: time.Now().UTC(),
	}); err != nil {
		log.Fatal(err)
	}

	if q, ok, err := questions.Pending(ctx, key); err != nil {
		log.Fatal(err)
	} else if ok {
		fmt.Println("pending:", q.Prompt)
	}

	// A collaborator replies "/answer staging" on the PR; the next poll
	// surfaces it, bound to the pending question's nonce.
	ans, ok, err := questions.Answer(ctx, key)
	if err != nil {
		log.Fatal(err)
	}
	if ok {
		fmt.Println("resume with:", ans.Text)
		if err := questions.Consume(ctx, key, ans.QuestionID); err != nil {
			log.Fatal(err)
		}
	}
}

func (*Store) Answer

func (s *Store) Answer(ctx context.Context, key string) (suspend.Answer, bool, error)

Answer scans the comments that arrived after the pending question comment for the first authorized "/answer <text>" reply, and returns it bound to the pending question's nonce. Replies from non-collaborators (or from apps and bots) are ignored. The conversation is listed exactly once — question and replies are located in the same page fetch, which matters because Answer runs on every poll wake of a parked run.

func (*Store) Ask

func (s *Store) Ask(ctx context.Context, key string, q suspend.Question) error

Ask posts q as a comment on the PR/issue named by key, superseding any previously pending question comment (its marker is retired so it can no longer be answered).

func (*Store) Consume

func (s *Store) Consume(ctx context.Context, key, questionID string) error

Consume retires the pending question comment when questionID matches its nonce, so a later Answer returns false until a new Ask. A nonce mismatch or an already-consumed question is a no-op.

func (*Store) Pending

func (s *Store) Pending(ctx context.Context, key string) (suspend.Question, bool, error)

Pending returns the question carried by the newest question comment.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL