From a560722f0b28a7b7a136a35f0a6208ce87f79696 Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:18:34 -0700 Subject: [PATCH 01/16] add continue ci --- .github/workflows/continue-pr-review.yml | 120 +++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/workflows/continue-pr-review.yml diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml new file mode 100644 index 0000000..799ad4c --- /dev/null +++ b/.github/workflows/continue-pr-review.yml @@ -0,0 +1,120 @@ +name: Continue PR Review + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + +permissions: + contents: read + pull-requests: write + +jobs: + continue-review: + if: github.event.pull_request.draft == false + runs-on: ubuntu-24.04 + env: + OPUB_HOME: ${{ runner.temp }}/opub + REVIEW_FILE: ${{ runner.temp }}/continue-review.md + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install GitHub CLI + run: | + type -p gh >/dev/null || { + sudo apt-get update + sudo apt-get install -y gh + } + + - name: Install opub + run: curl -fsSL https://opub.dev/install.sh | sh + + - name: Add installed tools to PATH + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "$HOME/go/bin" >> "$GITHUB_PATH" + + - name: Install Continue CLI + run: npm install -g @continuedev/cli@latest + + - name: Configure opub for Continue + env: + OPUB_PROVIDER_KEY: ${{ secrets.CONTINUE_CI_OPUB }} + run: | + printf '%s\n' "$OPUB_PROVIDER_KEY" | opub setup continue \ + --project 'opubdev/opub-cli' \ + --project-id 4 \ + --compute-key-id 3 \ + --api-key-hash '231ed9ad...90f1e7d8' \ + --provider-key-stdin + + - name: Fetch base branch + run: git fetch origin "${{ github.event.pull_request.base.ref }}" + + - name: Run Continue review + run: | + BASE_REF="origin/${{ github.event.pull_request.base.ref }}" + DIFF_FILE="$RUNNER_TEMP/pr.diff" + git diff --merge-base "$BASE_REF" HEAD > "$DIFF_FILE" + + cat > "$RUNNER_TEMP/review-prompt.txt" <<'PROMPT' + Review this pull request diff for bugs, regressions, security issues, and missing tests. + Focus especially on opub invariants around secrets, MCP response discipline, session linking, and target completeness. + Keep the review concise and practical. + Output markdown with these sections exactly: + + ## Summary + + ## Findings + - Use bullets. If there are no issues, write `- No actionable issues found.` + + ## Suggested follow-ups + - Use bullets. If none, write `- None.` + PROMPT + + { + echo "You are reviewing PR #$PR_NUMBER for repository $GITHUB_REPOSITORY." + echo + echo "Base ref: $BASE_REF" + echo + echo "Changed files:" + git diff --name-only --merge-base "$BASE_REF" HEAD + echo + echo "Diff:" + cat "$DIFF_FILE" + echo + cat "$RUNNER_TEMP/review-prompt.txt" + } > "$RUNNER_TEMP/review-input.txt" + + opub run continue -- \ + -p "$(cat "$RUNNER_TEMP/review-input.txt")" \ + --silent > "$REVIEW_FILE" + + - name: Post PR comment + run: | + { + echo "## Continue PR Review" + echo + echo "_Automated review via Continue CLI run through opub._" + echo + cat "$REVIEW_FILE" + } > "$RUNNER_TEMP/pr-comment.md" + + gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file "$RUNNER_TEMP/pr-comment.md" From abb035cc477cda06d4dda0d28e46cdbd0658b126 Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:18:40 -0700 Subject: [PATCH 02/16] add continue findings --- main_test.go | 159 +++++++++++++++++++++++++++++++++++++++------------ target.go | 34 ++++++----- 2 files changed, 143 insertions(+), 50 deletions(-) diff --git a/main_test.go b/main_test.go index 25c3787..399c30a 100644 --- a/main_test.go +++ b/main_test.go @@ -157,6 +157,88 @@ func TestUnhandledTargetPanics(t *testing.T) { _ = Target(999).name() } +func TestAllTargetsFullyWired(t *testing.T) { + repoRoot := t.TempDir() + stateDir := t.TempDir() + homeDir := t.TempDir() + binDir := t.TempDir() + stateRoot := t.TempDir() + copilotArgs := filepath.Join(t.TempDir(), "copilot.args") + + t.Setenv("HOME", homeDir) + t.Setenv("OPUB_HOME", stateRoot) + t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) + writeExecutable(t, filepath.Join(binDir, "copilot"), "#!/bin/sh\nprintf '%s\\n' \"$*\" > \""+copilotArgs+"\"\n") + + cfg := &LocalConfig{ + Project: ProjectConfig{FullName: "owner/repo", ID: stringPtr("proj_123")}, + ComputeKey: ComputeKeyConfig{ID: "ck_123"}, + Provider: ProviderConfig{Name: "openrouter", KeyHash: stringPtr("hash_123")}, + } + session := &LocalSession{ID: "local-1", LaunchProofToken: "runtime-token"} + assets := launchAssets{ + opubSkill: "skill", + copilotPlugin: "plugin", + copilotPluginMCP: "mcp", + copilotSkill: "copilot-skill", + } + + for _, target := range allTargets { + target := target + t.Run(target.name(), func(t *testing.T) { + cfg.Target = TargetConfig{Name: target.name()} + paths := &RepoPaths{ + Root: repoRoot, + StateDir: filepath.Join(stateDir, target.name()), + GithubOrigin: "git@github.com:owner/repo.git", + } + if err := os.MkdirAll(paths.StateDir, 0755); err != nil { + t.Fatalf("mkdir state dir: %v", err) + } + + parsed, err := parseTarget(target.name()) + if err != nil { + t.Fatalf("parseTarget(%q): %v", target.name(), err) + } + if parsed != target { + t.Fatalf("parseTarget(%q) = %v, want %v", target.name(), parsed, target) + } + + if got := target.name(); got == "" { + t.Fatal("empty target name") + } + if got := target.executable(); got == "" { + t.Fatal("empty executable") + } + + env := target.env(cfg, session, "secret", paths) + if !contains(env, "OPUB_TARGET="+target.name()) { + t.Fatalf("env missing OPUB_TARGET for %s: %#v", target.name(), env) + } + + cmd := target.buildCommand([]string{"--help"}, paths, assets) + if len(cmd.Args) == 0 { + t.Fatalf("empty command args for %s", target.name()) + } + if cmd.Args[0] != target.executable() { + t.Fatalf("command executable = %q, want %q", cmd.Args[0], target.executable()) + } + + if err := target.refresh(nil, paths, assets); err != nil { + t.Fatalf("refresh(%s): %v", target.name(), err) + } + + _ = skillDir(target) + if got := skillStatus(target); got == "" { + t.Fatalf("empty skill status for %s", target.name()) + } + if got := mcpStatus(target); got == "" { + t.Fatalf("empty mcp status for %s", target.name()) + } + }) + } +} + func TestTargetRunEnvironmentPerTarget(t *testing.T) { cfg := &LocalConfig{ Project: ProjectConfig{FullName: "owner/repo", ID: stringPtr("proj_123")}, @@ -276,18 +358,11 @@ func TestMCPToolListAndSessionCalls(t *testing.T) { if missing["state"] != "missing" || missing["active"] != false { t.Fatalf("missing session state = %#v", missing) } - assertNoArtifactFields(t, missing) + assertMinimalMCPResponse(t, missing) + assertMinimalLinkJSON(t, missing, false) status := computeStatus(state) - assertNoArtifactFields(t, status) - if _, ok := status["workflow_guidance"]; ok { - t.Fatalf("compute status should not include workflow_guidance: %#v", status) - } - if _, ok := status["evidence"]; ok { - t.Fatalf("compute status should not include evidence: %#v", status) - } - if link := status["link"].(map[string]interface{}); len(link) != 1 || link["linked"] != false { - t.Fatalf("compute status link should be minimal: %#v", link) - } + assertMinimalMCPResponse(t, status) + assertMinimalLinkJSON(t, status, false) session, err := writeSession(paths, state.config) if err != nil { @@ -298,8 +373,8 @@ func TestMCPToolListAndSessionCalls(t *testing.T) { if err != nil { t.Fatalf("load active state: %v", err) } - assertNoArtifactFields(t, activeWorkSession(state)) - assertNoArtifactFields(t, computeStatus(state)) + assertMinimalMCPResponse(t, activeWorkSession(state)) + assertMinimalMCPResponse(t, computeStatus(state)) input := strings.Join([]string{ `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`, @@ -337,15 +412,8 @@ func TestMCPToolListAndSessionCalls(t *testing.T) { t.Fatalf("linked MCP response missing:\n%s", out.String()) } activeResponse := structuredContent(t, responses[2]) - if _, ok := activeResponse["workflow_guidance"]; ok { - t.Fatalf("active session response should not include workflow_guidance: %#v", activeResponse) - } - if _, ok := activeResponse["evidence"]; ok { - t.Fatalf("active session response should not include evidence: %#v", activeResponse) - } - if link := activeResponse["link"].(map[string]interface{}); len(link) != 1 || link["linked"] != true { - t.Fatalf("active session link should be minimal: %#v", link) - } + assertMinimalMCPResponse(t, activeResponse) + assertMinimalLinkJSON(t, activeResponse, true) finished, err := readSession(paths) if err != nil { @@ -506,10 +574,8 @@ func TestLegacyLinkedArtifactsSessionLoadsUnverifiedAndCannotFinish(t *testing.T if _, err := finishWorkSession(state); err == nil || !strings.Contains(err.Error(), "opub work session is unverified") { t.Fatalf("legacy finish error = %v", err) } - resultJSON, _ := json.Marshal(active) - if strings.Contains(string(resultJSON), "linked_artifacts") || strings.Contains(string(resultJSON), "linked_artifacts_count") { - t.Fatalf("legacy artifact fields leaked into active response: %s", string(resultJSON)) - } + assertMinimalMCPResponse(t, active) + assertMinimalLinkJSON(t, active, false) sessionBytes, err := os.ReadFile(paths.Session) if err != nil { @@ -567,8 +633,8 @@ func TestSessionReuseStaleFinishedAndMismatchBehavior(t *testing.T) { if _, err := finishWorkSession(staleState); err == nil || !strings.Contains(err.Error(), "opub work session is stale") { t.Fatalf("stale finish error = %v", err) } - assertNoArtifactFields(t, activeWorkSession(staleState)) - assertNoArtifactFields(t, computeStatus(staleState)) + assertMinimalMCPResponse(t, activeWorkSession(staleState)) + assertMinimalMCPResponse(t, computeStatus(staleState)) now := unixNow() stale.FinishedAtUnix = &now @@ -594,8 +660,8 @@ func TestSessionReuseStaleFinishedAndMismatchBehavior(t *testing.T) { if _, err := finishWorkSession(finishedState); err == nil || !strings.Contains(err.Error(), "opub work session is finished") { t.Fatalf("finished finish error = %v", err) } - assertNoArtifactFields(t, activeWorkSession(finishedState)) - assertNoArtifactFields(t, computeStatus(finishedState)) + assertMinimalMCPResponse(t, activeWorkSession(finishedState)) + assertMinimalMCPResponse(t, computeStatus(finishedState)) fresh.ProjectFullName = "other/repo" if err := persistSession(paths, fresh); err != nil { @@ -620,8 +686,8 @@ func TestFinishWorkSessionRejectsMissingSession(t *testing.T) { if _, err := finishWorkSession(state); err == nil || !strings.Contains(err.Error(), "opub work session is missing") { t.Fatalf("missing finish error = %v", err) } - assertNoArtifactFields(t, activeWorkSession(state)) - assertNoArtifactFields(t, computeStatus(state)) + assertMinimalMCPResponse(t, activeWorkSession(state)) + assertMinimalMCPResponse(t, computeStatus(state)) } func TestVibeEnvironment(t *testing.T) { @@ -947,14 +1013,37 @@ func structuredContent(t *testing.T, msg map[string]interface{}) map[string]inte return content } -func assertNoArtifactFields(t *testing.T, value interface{}) { +func assertMinimalLinkJSON(t *testing.T, value map[string]interface{}, wantLinked bool) { + t.Helper() + link, ok := value["link"].(map[string]interface{}) + if !ok { + t.Fatalf("missing or invalid link object: %#v", value) + } + if len(link) != 1 { + t.Fatalf("link should be minimal: %#v", link) + } + if linked, ok := link["linked"].(bool); !ok || linked != wantLinked { + t.Fatalf("link.linked = %#v, want %v in %#v", link["linked"], wantLinked, link) + } +} + +func assertMinimalMCPResponse(t *testing.T, value interface{}) { t.Helper() data, err := json.Marshal(value) if err != nil { t.Fatalf("marshal value: %v", err) } - if strings.Contains(string(data), "linked_artifacts") || strings.Contains(string(data), "linked_artifacts_count") { - t.Fatalf("artifact fields present in %s", string(data)) + text := string(data) + for _, bad := range []string{ + "workflow_guidance", + "evidence", + "linked_artifacts", + "linked_artifacts_count", + "runtime_proof", + } { + if strings.Contains(text, bad) { + t.Fatalf("forbidden field %q present in %s", bad, text) + } } } diff --git a/target.go b/target.go index c4f52ff..2092b1b 100644 --- a/target.go +++ b/target.go @@ -28,6 +28,15 @@ const ( TargetContinue Target = iota ) +var allTargets = []Target{ + TargetClaude, + TargetCodex, + TargetCopilot, + TargetVibe, + TargetOpenCode, + TargetContinue, +} + const ( claudeDefaultSonnetModel = "anthropic/claude-sonnet-4.6" claudeDefaultHaikuModel = "anthropic/claude-haiku-4.5" @@ -38,22 +47,17 @@ const ( ) func parseTarget(s string) (Target, error) { - switch s { - case "claude": - return TargetClaude, nil - case "codex": - return TargetCodex, nil - case "copilot": - return TargetCopilot, nil - case "vibe": - return TargetVibe, nil - case "opencode": - return TargetOpenCode, nil - case "continue": - return TargetContinue, nil - default: - return 0, fmt.Errorf("unsupported target %q; supported targets: claude, codex, copilot, vibe, opencode, continue", s) + for _, t := range allTargets { + if t.name() == s { + return t, nil + } + } + + var names []string + for _, t := range allTargets { + names = append(names, t.name()) } + return 0, fmt.Errorf("unsupported target %q; supported targets: %s", s, strings.Join(names, ", ")) } func (t Target) name() string { From 552f94c54dcf0f98c131433ea9ebdaeb51ff0574 Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:22:12 -0700 Subject: [PATCH 03/16] add additional perms to ci --- .github/workflows/continue-pr-review.yml | 38 +++++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml index 799ad4c..0753345 100644 --- a/.github/workflows/continue-pr-review.yml +++ b/.github/workflows/continue-pr-review.yml @@ -11,14 +11,13 @@ on: permissions: contents: read pull-requests: write + issues: write jobs: continue-review: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04 env: - OPUB_HOME: ${{ runner.temp }}/opub - REVIEW_FILE: ${{ runner.temp }}/continue-review.md PR_NUMBER: ${{ github.event.pull_request.number }} GH_TOKEN: ${{ github.token }} steps: @@ -36,6 +35,11 @@ jobs: with: node-version: 20 + - name: Initialize workflow environment + run: | + echo "OPUB_HOME=$RUNNER_TEMP/opub" >> "$GITHUB_ENV" + echo "REVIEW_FILE=$RUNNER_TEMP/continue-review.md" >> "$GITHUB_ENV" + - name: Install GitHub CLI run: | type -p gh >/dev/null || { @@ -54,10 +58,17 @@ jobs: - name: Install Continue CLI run: npm install -g @continuedev/cli@latest + - name: Verify installed CLIs + run: | + opub --version + cn --help >/dev/null + gh --version + - name: Configure opub for Continue env: OPUB_PROVIDER_KEY: ${{ secrets.CONTINUE_CI_OPUB }} run: | + test -n "$OPUB_PROVIDER_KEY" printf '%s\n' "$OPUB_PROVIDER_KEY" | opub setup continue \ --project 'opubdev/opub-cli' \ --project-id 4 \ @@ -65,14 +76,22 @@ jobs: --api-key-hash '231ed9ad...90f1e7d8' \ --provider-key-stdin + opub doctor continue + - name: Fetch base branch - run: git fetch origin "${{ github.event.pull_request.base.ref }}" + run: git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.ref }}" - name: Run Continue review run: | + set -euo pipefail + BASE_REF="origin/${{ github.event.pull_request.base.ref }}" DIFF_FILE="$RUNNER_TEMP/pr.diff" + CHANGED_FILE_LIST="$RUNNER_TEMP/changed-files.txt" + REVIEW_INPUT="$RUNNER_TEMP/review-input.txt" + git diff --merge-base "$BASE_REF" HEAD > "$DIFF_FILE" + git diff --name-only --merge-base "$BASE_REF" HEAD > "$CHANGED_FILE_LIST" cat > "$RUNNER_TEMP/review-prompt.txt" <<'PROMPT' Review this pull request diff for bugs, regressions, security issues, and missing tests. @@ -95,26 +114,29 @@ jobs: echo "Base ref: $BASE_REF" echo echo "Changed files:" - git diff --name-only --merge-base "$BASE_REF" HEAD + cat "$CHANGED_FILE_LIST" echo echo "Diff:" cat "$DIFF_FILE" echo cat "$RUNNER_TEMP/review-prompt.txt" - } > "$RUNNER_TEMP/review-input.txt" + } > "$REVIEW_INPUT" opub run continue -- \ - -p "$(cat "$RUNNER_TEMP/review-input.txt")" \ + -p "$(cat "$REVIEW_INPUT")" \ --silent > "$REVIEW_FILE" + test -s "$REVIEW_FILE" + - name: Post PR comment run: | + COMMENT_FILE="$RUNNER_TEMP/pr-comment.md" { echo "## Continue PR Review" echo echo "_Automated review via Continue CLI run through opub._" echo cat "$REVIEW_FILE" - } > "$RUNNER_TEMP/pr-comment.md" + } > "$COMMENT_FILE" - gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file "$RUNNER_TEMP/pr-comment.md" + gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file "$COMMENT_FILE" From 37380487fa0ec5f6d3d4b326a1b074e2dddf8ef7 Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:26:45 -0700 Subject: [PATCH 04/16] no keyring --- .github/workflows/continue-pr-review.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml index 0753345..316e489 100644 --- a/.github/workflows/continue-pr-review.yml +++ b/.github/workflows/continue-pr-review.yml @@ -74,6 +74,7 @@ jobs: --project-id 4 \ --compute-key-id 3 \ --api-key-hash '231ed9ad...90f1e7d8' \ + --insecure-storage \ --provider-key-stdin opub doctor continue From 26aebba20d4ea5d17263abb33f83d33c585938f9 Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:33:21 -0700 Subject: [PATCH 05/16] prevent issue spam, upsert --- .github/workflows/continue-pr-review.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml index 316e489..9b05ea0 100644 --- a/.github/workflows/continue-pr-review.yml +++ b/.github/workflows/continue-pr-review.yml @@ -129,10 +129,15 @@ jobs: test -s "$REVIEW_FILE" - - name: Post PR comment + - name: Upsert PR comment run: | + set -euo pipefail + COMMENT_FILE="$RUNNER_TEMP/pr-comment.md" + MARKER="" + { + echo "$MARKER" echo "## Continue PR Review" echo echo "_Automated review via Continue CLI run through opub._" @@ -140,4 +145,19 @@ jobs: cat "$REVIEW_FILE" } > "$COMMENT_FILE" - gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file "$COMMENT_FILE" + EXISTING_COMMENT_ID="$({ + gh api \ + "/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + --paginate \ + --jq '.[] | select(.user.login == "github-actions[bot]" and (.body | contains(""))) | .id' \ + | tail -n1 + } || true)" + + if [ -n "$EXISTING_COMMENT_ID" ]; then + gh api \ + --method PATCH \ + "/repos/$GITHUB_REPOSITORY/issues/comments/$EXISTING_COMMENT_ID" \ + --field body="$(cat "$COMMENT_FILE")" + else + gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file "$COMMENT_FILE" + fi From d66faa73a354458890fdb26d9f377a0578546d3d Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:34:44 -0700 Subject: [PATCH 06/16] allTarget fix --- main_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/main_test.go b/main_test.go index 399c30a..f40ef41 100644 --- a/main_test.go +++ b/main_test.go @@ -157,6 +157,22 @@ func TestUnhandledTargetPanics(t *testing.T) { _ = Target(999).name() } +func TestParseTargetSupportedTargetsMessageFollowsAllTargets(t *testing.T) { + _, err := parseTarget("nope") + if err == nil { + t.Fatal("expected unsupported target error") + } + + var names []string + for _, target := range allTargets { + names = append(names, target.name()) + } + want := `unsupported target "nope"; supported targets: ` + strings.Join(names, ", ") + if err.Error() != want { + t.Fatalf("parseTarget error = %q, want %q", err.Error(), want) + } +} + func TestAllTargetsFullyWired(t *testing.T) { repoRoot := t.TempDir() stateDir := t.TempDir() From 9623efa47bae4b8dc36264d328f92bfc306a8baa Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:36:36 -0700 Subject: [PATCH 07/16] fork PR flow --- .github/workflows/continue-pr-review.yml | 48 +++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml index 9b05ea0..4a5ad86 100644 --- a/.github/workflows/continue-pr-review.yml +++ b/.github/workflows/continue-pr-review.yml @@ -1,5 +1,10 @@ name: Continue PR Review +# Security: this workflow uses a repository secret to configure opub/Continue. +# It is intentionally limited to non-fork pull_request events. Do not switch this +# workflow to pull_request_target while checking out or executing PR code. +# If fork support is needed later, split trusted/internal and untrusted/fork paths. + on: pull_request: types: @@ -14,8 +19,49 @@ permissions: issues: write jobs: + fork-notice: + if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == true + runs-on: ubuntu-24.04 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} + steps: + - name: Upsert fork notice comment + run: | + set -euo pipefail + + COMMENT_FILE="$RUNNER_TEMP/pr-comment.md" + MARKER="" + + { + echo "$MARKER" + echo "## Continue PR Review" + echo + echo "_Continue review is only run automatically for trusted non-fork pull requests because this workflow requires repository secrets._" + echo + echo "- This PR comes from a fork, so the secret-backed review job was skipped by design." + echo "- Maintainers can run an internal review workflow or review locally if needed." + } > "$COMMENT_FILE" + + EXISTING_COMMENT_ID="$({ + gh api \ + "/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + --paginate \ + --jq '.[] | select(.user.login == "github-actions[bot]" and (.body | contains(""))) | .id' \ + | tail -n1 + } || true)" + + if [ -n "$EXISTING_COMMENT_ID" ]; then + gh api \ + --method PATCH \ + "/repos/$GITHUB_REPOSITORY/issues/comments/$EXISTING_COMMENT_ID" \ + --field body="$(cat "$COMMENT_FILE")" + else + gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file "$COMMENT_FILE" + fi + continue-review: - if: github.event.pull_request.draft == false + if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false runs-on: ubuntu-24.04 env: PR_NUMBER: ${{ github.event.pull_request.number }} From 1f96089470ab912e3a75da69a6d29879636d918a Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:39:13 -0700 Subject: [PATCH 08/16] pin continue --- .github/workflows/continue-pr-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml index 4a5ad86..abfc000 100644 --- a/.github/workflows/continue-pr-review.yml +++ b/.github/workflows/continue-pr-review.yml @@ -102,7 +102,7 @@ jobs: echo "$HOME/go/bin" >> "$GITHUB_PATH" - name: Install Continue CLI - run: npm install -g @continuedev/cli@latest + run: npm install -g @continuedev/cli@1.5.45 - name: Verify installed CLIs run: | From e02ab80d7a9b8728e209e21e3cc585657e9cb47a Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:39:18 -0700 Subject: [PATCH 09/16] allTargets test --- main_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/main_test.go b/main_test.go index f40ef41..93ccd52 100644 --- a/main_test.go +++ b/main_test.go @@ -173,6 +173,17 @@ func TestParseTargetSupportedTargetsMessageFollowsAllTargets(t *testing.T) { } } +func TestAllTargetsOrdering(t *testing.T) { + var got []string + for _, target := range allTargets { + got = append(got, target.name()) + } + want := []string{"claude", "codex", "copilot", "vibe", "opencode", "continue"} + if strings.Join(got, "\x00") != strings.Join(want, "\x00") { + t.Fatalf("allTargets ordering = %#v, want %#v", got, want) + } +} + func TestAllTargetsFullyWired(t *testing.T) { repoRoot := t.TempDir() stateDir := t.TempDir() From 468f47578fd870e80a5beac54c230d4964e5d03c Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:47:40 -0700 Subject: [PATCH 10/16] cli anti-abuse --- .github/workflows/continue-pr-review.yml | 47 ++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml index abfc000..ec31b0b 100644 --- a/.github/workflows/continue-pr-review.yml +++ b/.github/workflows/continue-pr-review.yml @@ -85,6 +85,7 @@ jobs: run: | echo "OPUB_HOME=$RUNNER_TEMP/opub" >> "$GITHUB_ENV" echo "REVIEW_FILE=$RUNNER_TEMP/continue-review.md" >> "$GITHUB_ENV" + echo "SANITIZED_REVIEW_FILE=$RUNNER_TEMP/continue-review-sanitized.md" >> "$GITHUB_ENV" - name: Install GitHub CLI run: | @@ -136,14 +137,40 @@ jobs: DIFF_FILE="$RUNNER_TEMP/pr.diff" CHANGED_FILE_LIST="$RUNNER_TEMP/changed-files.txt" REVIEW_INPUT="$RUNNER_TEMP/review-input.txt" + REVIEW_PROMPT_FILE="$RUNNER_TEMP/review-prompt.txt" + MAX_DIFF_BYTES=120000 + MAX_CHANGED_FILES=250 git diff --merge-base "$BASE_REF" HEAD > "$DIFF_FILE" git diff --name-only --merge-base "$BASE_REF" HEAD > "$CHANGED_FILE_LIST" - cat > "$RUNNER_TEMP/review-prompt.txt" <<'PROMPT' + DIFF_BYTES=$(wc -c < "$DIFF_FILE" | tr -d '[:space:]') + CHANGED_FILES_COUNT=$(wc -l < "$CHANGED_FILE_LIST" | tr -d '[:space:]') + DIFF_TRUNCATED=false + + if [ "$CHANGED_FILES_COUNT" -gt "$MAX_CHANGED_FILES" ]; then + echo "PR changes $CHANGED_FILES_COUNT files; max supported is $MAX_CHANGED_FILES for automatic review." >&2 + exit 1 + fi + + if [ "$DIFF_BYTES" -gt "$MAX_DIFF_BYTES" ]; then + python3 - <<'PY' "$DIFF_FILE" "$MAX_DIFF_BYTES" +import pathlib +import sys +path = pathlib.Path(sys.argv[1]) +limit = int(sys.argv[2]) +data = path.read_text(encoding='utf-8', errors='replace') +truncated = data[:limit] +path.write_text(truncated, encoding='utf-8') +PY + DIFF_TRUNCATED=true + fi + + cat > "$REVIEW_PROMPT_FILE" <<'PROMPT' Review this pull request diff for bugs, regressions, security issues, and missing tests. Focus especially on opub invariants around secrets, MCP response discipline, session linking, and target completeness. Keep the review concise and practical. + If the input says the diff was truncated, mention that limitation in the summary and avoid overclaiming coverage. Output markdown with these sections exactly: ## Summary @@ -160,13 +187,17 @@ jobs: echo echo "Base ref: $BASE_REF" echo + echo "Changed files count: $CHANGED_FILES_COUNT" + echo "Diff bytes before truncation: $DIFF_BYTES" + echo "Diff truncated: $DIFF_TRUNCATED" + echo echo "Changed files:" cat "$CHANGED_FILE_LIST" echo echo "Diff:" cat "$DIFF_FILE" echo - cat "$RUNNER_TEMP/review-prompt.txt" + cat "$REVIEW_PROMPT_FILE" } > "$REVIEW_INPUT" opub run continue -- \ @@ -182,13 +213,23 @@ jobs: COMMENT_FILE="$RUNNER_TEMP/pr-comment.md" MARKER="" + python3 - <<'PY' "$REVIEW_FILE" "$SANITIZED_REVIEW_FILE" +import pathlib +import re +import sys +source = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8', errors='replace') +source = re.sub(r'(^|[^\w`])@([A-Za-z0-9][A-Za-z0-9_-]*(?:/[A-Za-z0-9][A-Za-z0-9_-]*)?)', lambda m: f"{m.group(1)}@\u200b{m.group(2)}", source) +source = re.sub(r'(? "$COMMENT_FILE" EXISTING_COMMENT_ID="$({ From 4072deb3490f9dcb5ea1416b2b69f8c079e5e18f Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:47:46 -0700 Subject: [PATCH 11/16] improve test --- main_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/main_test.go b/main_test.go index 93ccd52..132b539 100644 --- a/main_test.go +++ b/main_test.go @@ -254,6 +254,7 @@ func TestAllTargetsFullyWired(t *testing.T) { if err := target.refresh(nil, paths, assets); err != nil { t.Fatalf("refresh(%s): %v", target.name(), err) } + assertTargetRefreshEffects(t, target, paths, assets, stateRoot, copilotArgs) _ = skillDir(target) if got := skillStatus(target); got == "" { @@ -865,6 +866,69 @@ func TestRefreshContinueConfigWritesSecretlessYaml(t *testing.T) { } } +func assertTargetRefreshEffects(t *testing.T, target Target, paths *RepoPaths, assets launchAssets, stateRoot, copilotArgs string) { + t.Helper() + + if target != TargetClaude { + skillPath := filepath.Join(skillDir(target), "SKILL.md") + got, err := os.ReadFile(skillPath) + if err != nil { + t.Fatalf("read skill for %s: %v", target.name(), err) + } + if string(got) != assets.opubSkill { + t.Fatalf("skill content for %s = %q, want %q", target.name(), got, assets.opubSkill) + } + } + + switch target { + case TargetCopilot: + for path, want := range map[string]string{ + filepath.Join(stateRoot, "plugins", "copilot-opub", "plugin.json"): assets.copilotPlugin, + filepath.Join(stateRoot, "plugins", "copilot-opub", ".mcp.json"): assets.copilotPluginMCP, + filepath.Join(stateRoot, "plugins", "copilot-opub", "skills", "opub-funded-work", "SKILL.md"): assets.copilotSkill, + } { + got, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if string(got) != want { + t.Fatalf("%s = %q, want %q", path, got, want) + } + } + args, err := os.ReadFile(copilotArgs) + if err != nil { + t.Fatalf("read copilot invocation: %v", err) + } + if !strings.HasPrefix(string(args), "plugin install "+filepath.Join(stateRoot, "plugins", "copilot-opub")) { + t.Fatalf("unexpected copilot invocation: %q", args) + } + case TargetVibe: + got, err := os.ReadFile(vibeConfigPath(paths.StateDir)) + if err != nil { + t.Fatalf("read vibe config: %v", err) + } + if !strings.Contains(string(got), `"--target", "vibe"`) { + t.Fatalf("vibe config missing target marker:\n%s", string(got)) + } + case TargetOpenCode: + got, err := os.ReadFile(filepath.Join(paths.StateDir, "opencode.json")) + if err != nil { + t.Fatalf("read opencode config: %v", err) + } + if !strings.Contains(string(got), `"--target",`) || !strings.Contains(string(got), `"opencode"`) { + t.Fatalf("opencode config missing target marker:\n%s", string(got)) + } + case TargetContinue: + got, err := os.ReadFile(continueConfigPath(paths.StateDir)) + if err != nil { + t.Fatalf("read continue config: %v", err) + } + if !strings.Contains(string(got), "- continue") { + t.Fatalf("continue config missing target marker:\n%s", string(got)) + } + } +} + func TestCommandHelpCoverage(t *testing.T) { root := newRootForTest() for _, args := range [][]string{ From ea1c99ca8a729c9c8308f5e9ddbc89f58c1a6138 Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:47:53 -0700 Subject: [PATCH 12/16] tmp blog from session --- tmp/blog.md | 549 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 tmp/blog.md diff --git a/tmp/blog.md b/tmp/blog.md new file mode 100644 index 0000000..5e6e085 --- /dev/null +++ b/tmp/blog.md @@ -0,0 +1,549 @@ +# Building PR Review on Donated Compute with opub + Continue + +There’s a simple but powerful idea here: + +- developers want useful AI review in CI +- maintainers want project-linked usage +- contributors want a low-friction workflow +- teams want a setup that is explicit, auditable, and easy to reason about + +In this session, we wired up exactly that for `opubdev/opub-cli`: + +- local funded work ran through `opub run continue` +- CI review will also run through `opub` +- spend links to the configured opub project because the execution path goes through the opub CLI +- Continue provides the actual review behavior +- GitHub Actions provides the automation + +This post walks through what we built, why it matters, and how to reproduce it. + +--- + +## Why this integration is compelling + +Using Continue directly in CI is useful. + +Using Continue through `opub` is more interesting. + +That gives you: + +- a project-aware execution path +- local work sessions that can be linked to a project +- CI review that runs through the same opub entrypoint +- provider usage that is associated with the configured compute key/project context + +In short: + +> We are not just “calling an AI in CI.” +> We are running AI review through the opub CLI, so spend links to the project. + +That is the story. + +--- + +## What we accomplished + +We did two things in parallel: + +1. **Hardened opub invariants in tests/code** +2. **Added a Continue-powered PR review workflow in GitHub Actions** + +### 1) Hardening invariants + +We verified and strengthened several important behaviors. + +#### Secrets do not leak +We confirmed the system keeps raw provider credentials and launch proof tokens out of persisted config/session JSON and out of secretless MCP responses. + +Examples of what matters: + +- provider key should not be written into config +- raw launch token should not be written into `session.json` +- token should only be exposed where needed for the launched target environment +- generated Continue config should not contain the actual provider secret + +#### Target completeness is tested exhaustively +We identified a maintenance hazard: adding a new target should require wiring all target-specific behavior everywhere. + +To reduce drift, we introduced a canonical target list and tests that walk every target through all expected operations. + +That now exercises things like: + +- parsing +- display name +- executable resolution +- env generation +- command construction +- refresh/install steps +- skill dir/status +- MCP status + +We also added an explicit ordering test so message generation and iteration stay stable. + +#### MCP response discipline is enforced +We tightened tests around secretless/minimal MCP response behavior. + +In particular: + +- `get_active_work_session` +- `get_compute_status` + +must keep `link` minimal: + +```json +{"linked": true} +``` + +and must not expose rich/session-internal structures like: + +- `workflow_guidance` +- `evidence` +- `linked_artifacts` +- `runtime_proof` + +That matters because these “safe surface area” guarantees are easy to erode unless they are actively tested. + +--- + +## The CI workflow we added + +We created a GitHub Actions workflow that reviews pull requests with Continue, but runs Continue through opub. + +Conceptually, the command looks like this: + +```bash +opub run continue -- -p "Review this PR diff..." --silent +``` + +That’s the key integration point. + +Continue handles the review. +opub handles project-linked execution context. + +--- + +## The workflow design + +The workflow is intentionally conservative. + +### Trigger +It runs on pull request events like: + +- opened +- synchronize +- reopened +- ready_for_review + +### Security posture +We explicitly kept this on `pull_request`, not `pull_request_target`. + +That matters because the workflow uses repository secrets and checks out PR code. + +So the rule is: + +- **trusted non-fork PRs**: run review +- **fork PRs**: skip review and post a notice comment + +That avoids the common footgun of mixing secrets with untrusted PR execution. + +--- + +## How the GitHub Actions flow works + +At a high level, the CI job does this: + +1. checks out the repo +2. sets up Go and Node +3. installs `opub` +4. installs Continue CLI +5. configures opub for Continue using a repo secret +6. computes the PR diff against the base branch +7. builds a review prompt +8. runs Continue through opub +9. posts or updates a PR comment with the review + +--- + +## Example workflow shape + +Here is the core idea in simplified form. + +```yaml +name: Continue PR Review + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + continue-review: + if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-24.04 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Initialize workflow environment + run: | + echo "OPUB_HOME=$RUNNER_TEMP/opub" >> "$GITHUB_ENV" + echo "REVIEW_FILE=$RUNNER_TEMP/continue-review.md" >> "$GITHUB_ENV" + + - name: Install opub + run: curl -fsSL https://opub.dev/install.sh | sh + + - name: Add installed tools to PATH + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "$HOME/go/bin" >> "$GITHUB_PATH" + + - name: Install Continue CLI + run: npm install -g @continuedev/cli@1.5.45 + + - name: Configure opub for Continue + env: + OPUB_PROVIDER_KEY: ${{ secrets.CONTINUE_CI_OPUB }} + run: | + test -n "$OPUB_PROVIDER_KEY" + printf '%s\n' "$OPUB_PROVIDER_KEY" | opub setup continue \ + --project 'opubdev/opub-cli' \ + --project-id 4 \ + --compute-key-id 3 \ + --api-key-hash '231ed9ad...90f1e7d8' \ + --insecure-storage \ + --provider-key-stdin + + - name: Run Continue review + run: | + opub run continue -- \ + -p "Review this pull request diff for bugs, regressions, security issues, and missing tests." \ + --silent > "$REVIEW_FILE" +``` + +That is the essence. + +--- + +## The opub setup command + +A key part of this integration is that Continue is configured via opub, not by bypassing it. + +Example: + +```bash +printf '%s\n' "$CONTINUE_CI_OPUB" | opub setup continue \ + --project 'opubdev/opub-cli' \ + --project-id 4 \ + --compute-key-id 3 \ + --api-key-hash '231ed9ad...90f1e7d8' \ + --insecure-storage \ + --provider-key-stdin +``` + +Why this matters: + +- the project is explicit +- the compute key is explicit +- the provider key comes from CI secret storage +- the runtime path remains `opub run continue` + +So both local use and CI use flow through the same project-aware mechanism. + +--- + +## Why `opub run continue -- ...` is the important part + +It would be easy to install Continue and just run `cn` directly. + +But that would weaken the story. + +Instead, we use: + +```bash +opub run continue -- -p "..." --silent +``` + +That means: + +- opub launches the target +- opub provides the target environment +- opub injects the linked/project-aware session context +- usage can be associated with the configured project/compute key path + +That is what makes this more than a generic CI AI wrapper. + +--- + +## Local usage example + +The local developer flow is just as important as CI. + +A typical local run looks like: + +```bash +opub setup continue \ + --project 'opubdev/opub-cli' \ + --project-id 4 \ + --compute-key-id 3 \ + --api-key-hash '231ed9ad...90f1e7d8' +``` + +Then: + +```bash +opub run continue -- -p "Review my staged changes for regressions and missing tests" +``` + +Or more generally: + +```bash +opub run continue -- -p "Summarize the design tradeoffs in this refactor" +``` + +That gives a consistent operator model: + +- local work uses opub +- CI work uses opub +- both link back to the same project configuration model + +--- + +## PR review prompt example + +We shaped the CI prompt so the output is predictable and practical. + +Example prompt: + +```text +Review this pull request diff for bugs, regressions, security issues, and missing tests. +Focus especially on opub invariants around secrets, MCP response discipline, session linking, and target completeness. +Keep the review concise and practical. +Output markdown with these sections exactly: + +## Summary + +## Findings +- Use bullets. If there are no issues, write `- No actionable issues found.` + +## Suggested follow-ups +- Use bullets. If none, write `- None.` +``` + +This is a strong pattern because it: + +- narrows the task +- focuses the model on project-specific invariants +- produces machine-friendly, human-readable output +- makes comment upserts stable + +--- + +## Example generated PR comment format + +The workflow posts a single maintained comment, rather than spamming new comments every run. + +Example shape: + +```markdown + +## Continue PR Review + +_Automated review via Continue CLI run through opub._ + +## Summary + +Short overview of the PR quality and risk. + +## Findings +- Missing a test for stale-session behavior in the new target path. +- Error handling in the new workflow may fail silently if the review file is empty. + +## Suggested follow-ups +- Add a regression test for the new target integration. +- Consider validating required secret presence earlier in the workflow. +``` + +The marker comment lets the workflow update the previous review instead of creating a new one every time. + +--- + +## Fork handling + +One of the subtle but important parts of this setup is fork behavior. + +Because the workflow requires repository secrets, it does **not** run the real review job for fork PRs. + +Instead, it posts a neutral notice comment. + +Example message: + +```markdown + +## Continue PR Review + +_Continue review is only run automatically for trusted non-fork pull requests because this workflow requires repository secrets._ + +- This PR comes from a fork, so the secret-backed review job was skipped by design. +- Maintainers can run an internal review workflow or review locally if needed. +``` + +That is better than silently doing nothing. + +It keeps contributor experience clear without weakening the trust model. + +--- + +## Why we used insecure storage in CI + +In GitHub-hosted Linux runners, storing credentials in the system keyring can fail because the expected keyring service may not exist. + +We hit exactly that issue. + +The practical fix was: + +```bash +--insecure-storage +``` + +This is acceptable here because: + +- the runner is ephemeral +- `OPUB_HOME` is placed under `$RUNNER_TEMP` +- the environment is short-lived +- the workflow is secret-scoped already + +This is a good example of making CI-specific tradeoffs explicit rather than pretending desktop assumptions apply unchanged in automation. + +--- + +## Why pin Continue but not opub + +For this setup, Continue CLI was pinned, while opub was intentionally left unpinned. + +That split makes sense when: + +- opub is under direct project/operator control +- Continue is an external dependency where deterministic behavior is useful + +Example pinned install: + +```bash +npm install -g @continuedev/cli@1.5.45 +``` + +Example unpinned opub install: + +```bash +curl -fsSL https://opub.dev/install.sh | sh +``` + +That gives stability where desired without constraining the opub release path. + +--- + +## The test story matters too + +The integration story is stronger because we did not just add CI glue. + +We also made the codebase safer around the core concepts that opub depends on. + +### Example: target completeness +We added a canonical target list and tests that ensure each target participates in all the expected behaviors. + +That protects against “added new target, forgot to wire one method” failures. + +### Example: session proof handling +We validated that launch proof tokens are not persisted raw, but are still passed where they need to go. + +### Example: MCP minimal response shape +We locked down what secretless MCP responses may and may not expose. + +This matters because integration confidence is not just about “the workflow runs.” +It is about preserving the invariants that make the workflow trustworthy. + +--- + +## End-to-end story in one sentence + +Here is the whole value proposition in one line: + +> Continue gives us high-quality PR review, and opub makes that review project-linked so donated compute usage runs through a clear, reusable, project-aware execution path both locally and in CI. + +--- + +## Minimal how-to recap + +If you want to reproduce the pattern, the shortest version is: + +### 1. Install opub +```bash +curl -fsSL https://opub.dev/install.sh | sh +``` + +### 2. Configure Continue through opub +```bash +opub setup continue \ + --project 'opubdev/opub-cli' \ + --project-id 4 \ + --compute-key-id 3 \ + --api-key-hash '231ed9ad...90f1e7d8' +``` + +### 3. Run locally through opub +```bash +opub run continue -- -p "Review the current diff for bugs and missing tests" +``` + +### 4. In CI, provide the provider key as a secret +For this repo, the secret is: + +```text +CONTINUE_CI_OPUB +``` + +### 5. In GitHub Actions, install Continue and invoke it through opub +```bash +printf '%s\n' "$CONTINUE_CI_OPUB" | opub setup continue ... --provider-key-stdin +opub run continue -- -p "$PROMPT" --silent +``` + +### 6. Post the output back to the PR +Use a marker comment and upsert it each run. + +--- + +## Final thoughts + +This integration is compelling because it aligns incentives and mechanics: + +- maintainers get automated review +- projects get linked usage context +- local and CI workflows use the same entrypoint +- the review path is explicit +- the secret/trust boundary is handled deliberately +- the codebase has stronger tests around the invariants that make all of this credible + +Most importantly, this was not just a demo. + +This entire session ran through donated compute, and the CI workflow is designed to do the same. + +Because we are using the opub CLI, spend links to the project. + +That is the story worth telling. From 99d0c616e115aaaea2accbbfa9f1581494dee26e Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:49:48 -0700 Subject: [PATCH 13/16] target exclusions --- main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main_test.go b/main_test.go index 132b539..abc5131 100644 --- a/main_test.go +++ b/main_test.go @@ -869,7 +869,7 @@ func TestRefreshContinueConfigWritesSecretlessYaml(t *testing.T) { func assertTargetRefreshEffects(t *testing.T, target Target, paths *RepoPaths, assets launchAssets, stateRoot, copilotArgs string) { t.Helper() - if target != TargetClaude { + if target != TargetClaude && target != TargetCopilot && target != TargetContinue { skillPath := filepath.Join(skillDir(target), "SKILL.md") got, err := os.ReadFile(skillPath) if err != nil { From f8fc20f3bc852529454282274cc4290a459a73c3 Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Thu, 14 May 2026 11:50:34 -0700 Subject: [PATCH 14/16] sanitizing action --- .github/workflows/continue-pr-review.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml index ec31b0b..747edaf 100644 --- a/.github/workflows/continue-pr-review.yml +++ b/.github/workflows/continue-pr-review.yml @@ -154,9 +154,10 @@ jobs: fi if [ "$DIFF_BYTES" -gt "$MAX_DIFF_BYTES" ]; then - python3 - <<'PY' "$DIFF_FILE" "$MAX_DIFF_BYTES" + python3 - "$DIFF_FILE" "$MAX_DIFF_BYTES" <<'PY' import pathlib import sys + path = pathlib.Path(sys.argv[1]) limit = int(sys.argv[2]) data = path.read_text(encoding='utf-8', errors='replace') @@ -213,10 +214,11 @@ PY COMMENT_FILE="$RUNNER_TEMP/pr-comment.md" MARKER="" - python3 - <<'PY' "$REVIEW_FILE" "$SANITIZED_REVIEW_FILE" + python3 - "$REVIEW_FILE" "$SANITIZED_REVIEW_FILE" <<'PY' import pathlib import re import sys + source = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8', errors='replace') source = re.sub(r'(^|[^\w`])@([A-Za-z0-9][A-Za-z0-9_-]*(?:/[A-Za-z0-9][A-Za-z0-9_-]*)?)', lambda m: f"{m.group(1)}@\u200b{m.group(2)}", source) source = re.sub(r'(? Date: Thu, 14 May 2026 11:51:20 -0700 Subject: [PATCH 15/16] green up cli, yaml formatting --- .github/workflows/continue-pr-review.yml | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml index 747edaf..be910ba 100644 --- a/.github/workflows/continue-pr-review.yml +++ b/.github/workflows/continue-pr-review.yml @@ -154,16 +154,7 @@ jobs: fi if [ "$DIFF_BYTES" -gt "$MAX_DIFF_BYTES" ]; then - python3 - "$DIFF_FILE" "$MAX_DIFF_BYTES" <<'PY' -import pathlib -import sys - -path = pathlib.Path(sys.argv[1]) -limit = int(sys.argv[2]) -data = path.read_text(encoding='utf-8', errors='replace') -truncated = data[:limit] -path.write_text(truncated, encoding='utf-8') -PY + python3 -c 'import pathlib, sys; path = pathlib.Path(sys.argv[1]); limit = int(sys.argv[2]); data = path.read_text(encoding="utf-8", errors="replace"); path.write_text(data[:limit], encoding="utf-8")' "$DIFF_FILE" "$MAX_DIFF_BYTES" DIFF_TRUNCATED=true fi @@ -214,16 +205,7 @@ PY COMMENT_FILE="$RUNNER_TEMP/pr-comment.md" MARKER="" - python3 - "$REVIEW_FILE" "$SANITIZED_REVIEW_FILE" <<'PY' -import pathlib -import re -import sys - -source = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8', errors='replace') -source = re.sub(r'(^|[^\w`])@([A-Za-z0-9][A-Za-z0-9_-]*(?:/[A-Za-z0-9][A-Za-z0-9_-]*)?)', lambda m: f"{m.group(1)}@\u200b{m.group(2)}", source) -source = re.sub(r'(? Date: Thu, 14 May 2026 11:53:44 -0700 Subject: [PATCH 16/16] security pass --- .github/workflows/continue-pr-review.yml | 30 +++++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/continue-pr-review.yml b/.github/workflows/continue-pr-review.yml index be910ba..caff5fd 100644 --- a/.github/workflows/continue-pr-review.yml +++ b/.github/workflows/continue-pr-review.yml @@ -3,6 +3,8 @@ name: Continue PR Review # Security: this workflow uses a repository secret to configure opub/Continue. # It is intentionally limited to non-fork pull_request events. Do not switch this # workflow to pull_request_target while checking out or executing PR code. +# This job must not check out or execute PR-head code before secret-bearing steps; +# it reviews metadata/diff only from the trusted base checkout plus GitHub API. # If fork support is needed later, split trusted/internal and untrusted/fork paths. on: @@ -69,7 +71,8 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 + ref: ${{ github.event.pull_request.base.sha }} + fetch-depth: 1 - name: Set up Go uses: actions/setup-go@v5 @@ -124,16 +127,25 @@ jobs: --insecure-storage \ --provider-key-stdin - opub doctor continue - - name: Fetch base branch - run: git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.ref }}" + - name: Fetch PR metadata and diff + run: | + set -euo pipefail + + DIFF_FILE="$RUNNER_TEMP/pr.diff" + CHANGED_FILE_LIST="$RUNNER_TEMP/changed-files.txt" + gh api \ + -H "Accept: application/vnd.github.v3.diff" \ + "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" > "$DIFF_FILE" + gh api \ + "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \ + --paginate \ + --jq '.[].filename' > "$CHANGED_FILE_LIST" - name: Run Continue review run: | set -euo pipefail - BASE_REF="origin/${{ github.event.pull_request.base.ref }}" DIFF_FILE="$RUNNER_TEMP/pr.diff" CHANGED_FILE_LIST="$RUNNER_TEMP/changed-files.txt" REVIEW_INPUT="$RUNNER_TEMP/review-input.txt" @@ -141,9 +153,6 @@ jobs: MAX_DIFF_BYTES=120000 MAX_CHANGED_FILES=250 - git diff --merge-base "$BASE_REF" HEAD > "$DIFF_FILE" - git diff --name-only --merge-base "$BASE_REF" HEAD > "$CHANGED_FILE_LIST" - DIFF_BYTES=$(wc -c < "$DIFF_FILE" | tr -d '[:space:]') CHANGED_FILES_COUNT=$(wc -l < "$CHANGED_FILE_LIST" | tr -d '[:space:]') DIFF_TRUNCATED=false @@ -162,6 +171,7 @@ jobs: Review this pull request diff for bugs, regressions, security issues, and missing tests. Focus especially on opub invariants around secrets, MCP response discipline, session linking, and target completeness. Keep the review concise and practical. + The review input was collected without checking out PR-head code; reason only from the provided diff and changed file list. If the input says the diff was truncated, mention that limitation in the summary and avoid overclaiming coverage. Output markdown with these sections exactly: @@ -177,7 +187,9 @@ jobs: { echo "You are reviewing PR #$PR_NUMBER for repository $GITHUB_REPOSITORY." echo - echo "Base ref: $BASE_REF" + echo "Base branch: ${{ github.event.pull_request.base.ref }}" + echo "Base SHA checked out in this job: ${{ github.event.pull_request.base.sha }}" + echo "PR head SHA under review: ${{ github.event.pull_request.head.sha }}" echo echo "Changed files count: $CHANGED_FILES_COUNT" echo "Diff bytes before truncation: $DIFF_BYTES"