Help us shape the future of Go! Take the 2026 Go Developer Survey

githubquestions

package
v0.11.78 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 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"
	"time"

	"github.com/chainguard-dev/clog"

	"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 {
		clog.FatalContextf(ctx, "%v", err)
	}

	if q, ok, err := questions.Pending(ctx, key); err != nil {
		clog.FatalContextf(ctx, "%v", 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 {
		clog.FatalContextf(ctx, "%v", err)
	}
	if ok {
		fmt.Println("resume with:", ans.Text)
		if err := questions.Consume(ctx, key, ans.QuestionID); err != nil {
			clog.FatalContextf(ctx, "%v", 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