terramend 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +39 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { log } from "#app/utils/cli";
|
|
4
|
+
import { retry } from "#app/utils/retry";
|
|
5
|
+
|
|
6
|
+
vi.mock("#app/utils/cli", () => ({
|
|
7
|
+
log: {
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
debug: vi.fn(),
|
|
10
|
+
warning: vi.fn(),
|
|
11
|
+
error: vi.fn(),
|
|
12
|
+
success: vi.fn(),
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("node:timers/promises", () => ({
|
|
17
|
+
setTimeout: vi.fn(async () => undefined),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const sleepMock = vi.mocked(sleep);
|
|
21
|
+
const logInfoMock = vi.mocked(log.info);
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("retry", () => {
|
|
28
|
+
it("returns the result on first success without sleeping", async () => {
|
|
29
|
+
const fn = vi.fn(async () => "ok");
|
|
30
|
+
await expect(retry(fn)).resolves.toBe("ok");
|
|
31
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(sleepMock).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("retries transient 'fetch failed' errors with default linear backoff", async () => {
|
|
36
|
+
const fn = vi
|
|
37
|
+
.fn<() => Promise<string>>()
|
|
38
|
+
.mockRejectedValueOnce(new Error("fetch failed"))
|
|
39
|
+
.mockRejectedValueOnce(new Error("fetch failed"))
|
|
40
|
+
.mockResolvedValueOnce("ok");
|
|
41
|
+
|
|
42
|
+
await expect(retry(fn)).resolves.toBe("ok");
|
|
43
|
+
|
|
44
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
45
|
+
expect(sleepMock).toHaveBeenNthCalledWith(1, 1000);
|
|
46
|
+
expect(sleepMock).toHaveBeenNthCalledWith(2, 2000);
|
|
47
|
+
expect(logInfoMock).toHaveBeenCalledWith(
|
|
48
|
+
"» operation failed (attempt 1/3), retrying in 1000ms...",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("retries AbortError by name (default shouldRetry)", async () => {
|
|
53
|
+
const abortError = new Error("aborted");
|
|
54
|
+
abortError.name = "AbortError";
|
|
55
|
+
const fn = vi
|
|
56
|
+
.fn<() => Promise<string>>()
|
|
57
|
+
.mockRejectedValueOnce(abortError)
|
|
58
|
+
.mockResolvedValueOnce("ok");
|
|
59
|
+
|
|
60
|
+
await expect(retry(fn)).resolves.toBe("ok");
|
|
61
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("retries ECONNRESET errors (default shouldRetry)", async () => {
|
|
65
|
+
const fn = vi
|
|
66
|
+
.fn<() => Promise<string>>()
|
|
67
|
+
.mockRejectedValueOnce(new Error("read ECONNRESET"))
|
|
68
|
+
.mockResolvedValueOnce("ok");
|
|
69
|
+
|
|
70
|
+
await expect(retry(fn)).resolves.toBe("ok");
|
|
71
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("does not retry non-transient errors", async () => {
|
|
75
|
+
const fn = vi.fn(async () => {
|
|
76
|
+
throw new Error("boom");
|
|
77
|
+
});
|
|
78
|
+
await expect(retry(fn)).rejects.toThrow("boom");
|
|
79
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(sleepMock).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not retry non-Error rejections (default shouldRetry)", async () => {
|
|
84
|
+
const fn = vi.fn(async () => {
|
|
85
|
+
throw "string failure";
|
|
86
|
+
});
|
|
87
|
+
await expect(retry(fn)).rejects.toBe("string failure");
|
|
88
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("throws the last error after exhausting all attempts", async () => {
|
|
92
|
+
const fn = vi.fn(async () => {
|
|
93
|
+
throw new Error("fetch failed");
|
|
94
|
+
});
|
|
95
|
+
await expect(retry(fn, { maxAttempts: 2 })).rejects.toThrow("fetch failed");
|
|
96
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
97
|
+
expect(sleepMock).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(sleepMock).toHaveBeenCalledWith(1000);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("uses delayMs as a linear backoff base", async () => {
|
|
102
|
+
const fn = vi
|
|
103
|
+
.fn<() => Promise<string>>()
|
|
104
|
+
.mockRejectedValueOnce(new Error("e1"))
|
|
105
|
+
.mockRejectedValueOnce(new Error("e2"))
|
|
106
|
+
.mockResolvedValueOnce("ok");
|
|
107
|
+
|
|
108
|
+
await expect(retry(fn, { maxAttempts: 3, delayMs: 10, shouldRetry: () => true })).resolves.toBe(
|
|
109
|
+
"ok",
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(sleepMock).toHaveBeenNthCalledWith(1, 10);
|
|
113
|
+
expect(sleepMock).toHaveBeenNthCalledWith(2, 20);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("honors an explicit delaysMs schedule over maxAttempts", async () => {
|
|
117
|
+
const fn = vi.fn(async () => {
|
|
118
|
+
throw new Error("always");
|
|
119
|
+
});
|
|
120
|
+
await expect(
|
|
121
|
+
retry(fn, { delaysMs: [5, 7], maxAttempts: 99, shouldRetry: () => true }),
|
|
122
|
+
).rejects.toThrow("always");
|
|
123
|
+
|
|
124
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
125
|
+
expect(sleepMock).toHaveBeenNthCalledWith(1, 5);
|
|
126
|
+
expect(sleepMock).toHaveBeenNthCalledWith(2, 7);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("uses the custom label in the retry log line", async () => {
|
|
130
|
+
const fn = vi
|
|
131
|
+
.fn<() => Promise<string>>()
|
|
132
|
+
.mockRejectedValueOnce(new Error("transient"))
|
|
133
|
+
.mockResolvedValueOnce("ok");
|
|
134
|
+
|
|
135
|
+
await retry(fn, { label: "token exchange", shouldRetry: () => true });
|
|
136
|
+
|
|
137
|
+
expect(logInfoMock).toHaveBeenCalledWith(
|
|
138
|
+
"» token exchange failed (attempt 1/3), retrying in 1000ms...",
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("passes the thrown error to a custom shouldRetry and stops when it returns false", async () => {
|
|
143
|
+
const error = new Error("fetch failed");
|
|
144
|
+
const shouldRetry = vi.fn(() => false);
|
|
145
|
+
const fn = vi.fn(async () => {
|
|
146
|
+
throw error;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await expect(retry(fn, { shouldRetry })).rejects.toBe(error);
|
|
150
|
+
expect(shouldRetry).toHaveBeenCalledWith(error);
|
|
151
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { log } from "#app/utils/cli";
|
|
3
|
+
|
|
4
|
+
export type RetryOptions = {
|
|
5
|
+
maxAttempts?: number;
|
|
6
|
+
delayMs?: number;
|
|
7
|
+
/**
|
|
8
|
+
* explicit delay schedule — one entry per retry (length N ⇒ N+1 attempts).
|
|
9
|
+
* when set, overrides `maxAttempts` and `delayMs`. e.g. `[1_000, 3_000]`
|
|
10
|
+
* means up to 3 attempts, sleeping 1s before retry 2 and 3s before retry 3.
|
|
11
|
+
*/
|
|
12
|
+
delaysMs?: readonly number[];
|
|
13
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
14
|
+
label?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const defaultShouldRetry = (error: unknown): boolean => {
|
|
18
|
+
if (!(error instanceof Error)) return false;
|
|
19
|
+
// retry on transient network errors
|
|
20
|
+
return (
|
|
21
|
+
error.name === "AbortError" ||
|
|
22
|
+
error.message.includes("fetch failed") ||
|
|
23
|
+
error.message.includes("ECONNRESET") ||
|
|
24
|
+
error.message.includes("ETIMEDOUT")
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
|
|
29
|
+
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
|
30
|
+
const label = options.label ?? "operation";
|
|
31
|
+
const delays = options.delaysMs
|
|
32
|
+
? Array.from(options.delaysMs)
|
|
33
|
+
: Array.from(
|
|
34
|
+
{ length: (options.maxAttempts ?? 3) - 1 },
|
|
35
|
+
(_, i) => (options.delayMs ?? 1000) * (i + 1),
|
|
36
|
+
);
|
|
37
|
+
const maxAttempts = delays.length + 1;
|
|
38
|
+
|
|
39
|
+
let lastError: unknown;
|
|
40
|
+
|
|
41
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
42
|
+
try {
|
|
43
|
+
return await fn();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
lastError = error;
|
|
46
|
+
|
|
47
|
+
if (attempt === maxAttempts || !shouldRetry(error)) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const delay = delays[attempt - 1]!;
|
|
52
|
+
log.info(`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
|
53
|
+
await sleep(delay);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw lastError;
|
|
58
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { WriteablePayload } from "#app/external";
|
|
2
|
+
import { reportReviewNodeId } from "#app/mcp/review";
|
|
3
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
4
|
+
import { log } from "#app/utils/cli";
|
|
5
|
+
|
|
6
|
+
const RE_REVIEW_PREAMBLE =
|
|
7
|
+
"Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* post-agent review lifecycle: runs after the agent exits (success or timeout).
|
|
11
|
+
*
|
|
12
|
+
* normally the agent handles new commits inline: create_pull_request_review
|
|
13
|
+
* detects HEAD movement and tells the agent to pull and review the delta.
|
|
14
|
+
* this dispatch is a safety net for cases where the agent couldn't handle
|
|
15
|
+
* it (timeout, error, etc).
|
|
16
|
+
*
|
|
17
|
+
* ordering matters: reportReviewNodeId marks this run "done" FIRST so push
|
|
18
|
+
* webhooks stop being suppressed by dedup. the HEAD check runs SECOND to
|
|
19
|
+
* catch any pushes that were suppressed while this run was in-flight.
|
|
20
|
+
*/
|
|
21
|
+
export async function postReviewCleanup(ctx: ToolContext): Promise<void> {
|
|
22
|
+
const review = ctx.toolState.review;
|
|
23
|
+
if (!review) return;
|
|
24
|
+
delete ctx.toolState.review;
|
|
25
|
+
|
|
26
|
+
// mark review as submitted — unlocks webhook dedup for new pushes
|
|
27
|
+
await bestEffort(() => reportReviewNodeId(ctx, { nodeId: review.nodeId }), "reportReviewNodeId");
|
|
28
|
+
|
|
29
|
+
// dispatch follow-up if PR HEAD moved past the reviewed commit
|
|
30
|
+
if (review.reviewedSha) {
|
|
31
|
+
await bestEffort(
|
|
32
|
+
() => dispatchFollowUpReReview(ctx, review.reviewedSha!),
|
|
33
|
+
"follow-up re-review dispatch",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function bestEffort(fn: () => Promise<unknown>, label: string): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
await fn();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
log.debug(`${label} failed: ${error}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function dispatchFollowUpReReview(ctx: ToolContext, reviewedSha: string): Promise<void> {
|
|
47
|
+
const issueNumber = ctx.payload.event.issue_number;
|
|
48
|
+
if (!issueNumber) return;
|
|
49
|
+
|
|
50
|
+
const pr = await ctx.octokit.rest.pulls.get({
|
|
51
|
+
owner: ctx.repo.owner,
|
|
52
|
+
repo: ctx.repo.name,
|
|
53
|
+
pull_number: issueNumber,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (pr.data.head.sha === reviewedSha) return;
|
|
57
|
+
if (pr.data.state !== "open") return;
|
|
58
|
+
if (pr.data.draft) return;
|
|
59
|
+
|
|
60
|
+
log.info(
|
|
61
|
+
`safety net: pr HEAD moved from ${reviewedSha.slice(0, 7)} to ${pr.data.head.sha.slice(0, 7)} ` +
|
|
62
|
+
`and agent did not review inline — dispatching follow-up re-review`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const event: WriteablePayload["event"] = {
|
|
66
|
+
trigger: "pull_request_synchronize",
|
|
67
|
+
issue_number: issueNumber,
|
|
68
|
+
is_pr: true,
|
|
69
|
+
title: pr.data.title,
|
|
70
|
+
body: null,
|
|
71
|
+
branch: pr.data.head.ref,
|
|
72
|
+
before_sha: reviewedSha,
|
|
73
|
+
silent: true,
|
|
74
|
+
};
|
|
75
|
+
if (ctx.payload.event.authorPermission) {
|
|
76
|
+
event.authorPermission = ctx.payload.event.authorPermission;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const payload: WriteablePayload = {
|
|
80
|
+
"~terramend": true,
|
|
81
|
+
version: ctx.payload.version,
|
|
82
|
+
model: ctx.payload.model,
|
|
83
|
+
prompt: "",
|
|
84
|
+
eventInstructions: RE_REVIEW_PREAMBLE,
|
|
85
|
+
event,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
await ctx.octokit.rest.actions.createWorkflowDispatch({
|
|
89
|
+
owner: ctx.repo.owner,
|
|
90
|
+
repo: ctx.repo.name,
|
|
91
|
+
workflow_id: getCurrentWorkflowFilename(),
|
|
92
|
+
ref: pr.data.base.repo.default_branch,
|
|
93
|
+
inputs: { prompt: JSON.stringify(payload) },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* derive the running workflow's filename from `GITHUB_WORKFLOW_REF`, which has the form
|
|
99
|
+
* `<owner>/<repo>/.github/workflows/<filename>@<ref>` (e.g. `.../terramend.yaml@refs/heads/main`).
|
|
100
|
+
* falls back to `terramend.yml` if the env var is missing or malformed (shouldn't happen in CI).
|
|
101
|
+
*/
|
|
102
|
+
function getCurrentWorkflowFilename(): string {
|
|
103
|
+
const ref = process.env.GITHUB_WORKFLOW_REF ?? "";
|
|
104
|
+
const match = ref.match(/\/([^/]+)@/);
|
|
105
|
+
return match?.[1] ?? "terramend.yml";
|
|
106
|
+
}
|
package/src/utils/run.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { AgentResult } from "#app/agents/shared";
|
|
2
|
+
import type { MainResult } from "#app/main";
|
|
3
|
+
import { reportProgress } from "#app/mcp/comment";
|
|
4
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
5
|
+
import { log } from "#app/utils/cli";
|
|
6
|
+
import { reportErrorToComment } from "#app/utils/errorReport";
|
|
7
|
+
|
|
8
|
+
export interface HandleAgentResultParams {
|
|
9
|
+
result: AgentResult;
|
|
10
|
+
toolContext: ToolContext;
|
|
11
|
+
silent: boolean | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getErrorMessage(error: unknown): string {
|
|
15
|
+
return error instanceof Error ? error.message : String(error);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function handleAgentResult(ctx: HandleAgentResultParams): Promise<MainResult> {
|
|
19
|
+
if (!ctx.result.success) {
|
|
20
|
+
// rendering + posting for the `!success` branch lives in
|
|
21
|
+
// `finalizeSuccessRun` (called immediately before this function) so the
|
|
22
|
+
// BYOK billing-exhausted, hang, and api-key bodies land on a single
|
|
23
|
+
// surface — both for runs with a pre-existing progress comment AND for
|
|
24
|
+
// silent triggers via `createIfMissing`. see #835.
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: ctx.result.error || "Agent execution failed",
|
|
28
|
+
output: ctx.result.output!,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// IncrementalReview's non-substantive path exits cleanly without
|
|
33
|
+
// submitting any review, so no MCP write tool flips wasUpdated and the
|
|
34
|
+
// strict completion check below would otherwise fail the run. The
|
|
35
|
+
// isReviewMode skip is load-bearing for that path: the agent's exit
|
|
36
|
+
// code is the completion signal, not a progress-comment write.
|
|
37
|
+
// (Review mode that submits a real review now flips wasUpdated via
|
|
38
|
+
// create_pull_request_review, so the skip is redundant for the
|
|
39
|
+
// substantive-review path but kept for symmetry with IncrementalReview.)
|
|
40
|
+
// See plans/review_progress_comment_cleanup_b0120f6c.plan.md.
|
|
41
|
+
const toolState = ctx.toolContext.toolState;
|
|
42
|
+
const mode = toolState.selectedMode;
|
|
43
|
+
const isReviewMode = mode === "Review" || mode === "IncrementalReview";
|
|
44
|
+
if (!isReviewMode && !toolState.wasUpdated && toolState.hadProgressComment && !ctx.silent) {
|
|
45
|
+
// the agent exited successfully but never landed a GitHub write — either it
|
|
46
|
+
// answered the mention in raw assistant text (which is never posted, only
|
|
47
|
+
// logged) or a write tool failed (e.g. report_progress hit a 401). salvage
|
|
48
|
+
// by delivering the content we have to the progress comment so the user
|
|
49
|
+
// actually gets the answer instead of a failed run. (finalizeSuccessRun
|
|
50
|
+
// preserves the progress comment whenever wasUpdated is false, so the write
|
|
51
|
+
// lands here.)
|
|
52
|
+
//
|
|
53
|
+
// stop the todo tracker first: on this path the agent used todowrite but
|
|
54
|
+
// never called report_progress, so a pending debounced render could still
|
|
55
|
+
// be queued — draining it keeps it from clobbering the salvaged answer and
|
|
56
|
+
// from holding the event loop open. mirrors ReportProgressTool.
|
|
57
|
+
const tracker = toolState.todoTracker;
|
|
58
|
+
if (tracker) {
|
|
59
|
+
tracker.cancel();
|
|
60
|
+
await tracker.settled();
|
|
61
|
+
}
|
|
62
|
+
// `lastProgressBody` is the body a failed report_progress already assembled
|
|
63
|
+
// (task-list collapsible included), so use it verbatim; otherwise fall back
|
|
64
|
+
// to the agent's final assistant text and append the collapsible the way
|
|
65
|
+
// report_progress would.
|
|
66
|
+
let salvage = toolState.lastProgressBody?.trim();
|
|
67
|
+
if (!salvage) {
|
|
68
|
+
const output = ctx.result.output?.trim();
|
|
69
|
+
const collapsible = tracker?.renderCollapsible({ completeInProgress: true });
|
|
70
|
+
salvage = output && collapsible ? `${output}\n\n${collapsible}` : output;
|
|
71
|
+
}
|
|
72
|
+
if (salvage) {
|
|
73
|
+
try {
|
|
74
|
+
await reportProgress(ctx.toolContext, { body: salvage });
|
|
75
|
+
log.success("Task complete.");
|
|
76
|
+
return { success: true, output: ctx.result.output || "" };
|
|
77
|
+
} catch (writeError) {
|
|
78
|
+
// the write itself is failing (auth/permissions) — surface THAT, not
|
|
79
|
+
// the generic "no progress" message, so the real cause isn't masked.
|
|
80
|
+
const error = `failed to deliver agent result: ${getErrorMessage(writeError)}`;
|
|
81
|
+
await reportErrorToComment({ toolState, error, title: "Error" }).catch(() => {});
|
|
82
|
+
return { success: false, error, output: ctx.result.output || "" };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const error = ctx.result.error || "agent completed without reporting progress";
|
|
87
|
+
try {
|
|
88
|
+
await reportErrorToComment({ toolState, error, title: "Error" });
|
|
89
|
+
} catch {}
|
|
90
|
+
return { success: false, error, output: ctx.result.output || "" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
log.success("Task complete.");
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
output: ctx.result.output || "",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { PushPermission, ShellPermission } from "#app/external";
|
|
2
|
+
import { apiFetch } from "#app/utils/apiFetch";
|
|
3
|
+
import type { RepoContext } from "#app/utils/github";
|
|
4
|
+
|
|
5
|
+
export interface Mode {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
prompt: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* server-parsed TOC entry for `Repo.learnings`. depth is 1-6 (h1-h6),
|
|
14
|
+
* line numbers are 1-indexed against the raw body. computed by
|
|
15
|
+
* `parseLearningsHeadings` in `utils/learningsToc.ts` (server side) and
|
|
16
|
+
* shipped over the run-context JSON boundary; the canonical declaration
|
|
17
|
+
* lives there. duplicated here because the action runtime can't reach
|
|
18
|
+
* across into the proprietary root-level codebase, and the JSON wire
|
|
19
|
+
* means typecheck can't enforce shape equality across both sides.
|
|
20
|
+
*/
|
|
21
|
+
export interface LearningsHeading {
|
|
22
|
+
depth: 1 | 2 | 3 | 4 | 5 | 6;
|
|
23
|
+
title: string;
|
|
24
|
+
startLine: number;
|
|
25
|
+
endLine: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RepoSettings {
|
|
29
|
+
model: string | null;
|
|
30
|
+
modes: Mode[];
|
|
31
|
+
setupScript: string | null;
|
|
32
|
+
postCheckoutScript: string | null;
|
|
33
|
+
prepushScript: string | null;
|
|
34
|
+
stopScript: string | null;
|
|
35
|
+
push: PushPermission;
|
|
36
|
+
shell: ShellPermission;
|
|
37
|
+
prApproveEnabled: boolean;
|
|
38
|
+
modeInstructions: Record<string, string>;
|
|
39
|
+
learnings: string | null;
|
|
40
|
+
learningsHeadings: LearningsHeading[];
|
|
41
|
+
envAllowlist: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Account-level billing plan. Orthogonal to repo-level OSS status. Mirrors
|
|
46
|
+
* the server's `AccountPlan` in `utils/billing.ts`. `"none"` = free tier,
|
|
47
|
+
* `"payg"` = card on file / pay-as-you-go.
|
|
48
|
+
*/
|
|
49
|
+
export type AccountPlan = "none" | "payg";
|
|
50
|
+
|
|
51
|
+
export interface RunContext {
|
|
52
|
+
settings: RepoSettings;
|
|
53
|
+
apiToken: string;
|
|
54
|
+
oss: boolean;
|
|
55
|
+
plan: AccountPlan;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const defaultSettings: RepoSettings = {
|
|
59
|
+
model: null,
|
|
60
|
+
modes: [],
|
|
61
|
+
setupScript: null,
|
|
62
|
+
postCheckoutScript: null,
|
|
63
|
+
prepushScript: null,
|
|
64
|
+
stopScript: null,
|
|
65
|
+
push: "restricted",
|
|
66
|
+
shell: "restricted",
|
|
67
|
+
prApproveEnabled: false,
|
|
68
|
+
modeInstructions: {},
|
|
69
|
+
learnings: null,
|
|
70
|
+
learningsHeadings: [],
|
|
71
|
+
envAllowlist: null,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const defaultRunContext: RunContext = {
|
|
75
|
+
settings: defaultSettings,
|
|
76
|
+
apiToken: "",
|
|
77
|
+
oss: false,
|
|
78
|
+
plan: "none",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* fetch run context from Terramend API
|
|
83
|
+
* returns settings + API token for subsequent calls
|
|
84
|
+
* returns defaults if fetch fails
|
|
85
|
+
*/
|
|
86
|
+
export async function fetchRunContext(params: {
|
|
87
|
+
token: string;
|
|
88
|
+
repoContext: RepoContext;
|
|
89
|
+
oidcToken?: string | undefined;
|
|
90
|
+
}): Promise<RunContext> {
|
|
91
|
+
const timeoutMs = 30000;
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const headers: Record<string, string> = {
|
|
97
|
+
Authorization: `Bearer ${params.token}`,
|
|
98
|
+
};
|
|
99
|
+
if (params.oidcToken) {
|
|
100
|
+
headers["X-GitHub-OIDC-Token"] = params.oidcToken;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const response = await apiFetch({
|
|
104
|
+
path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
|
|
105
|
+
headers,
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
clearTimeout(timeoutId);
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
return defaultRunContext;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = (await response.json()) as {
|
|
116
|
+
settings: RepoSettings | null;
|
|
117
|
+
apiToken: string;
|
|
118
|
+
oss?: boolean;
|
|
119
|
+
plan?: AccountPlan;
|
|
120
|
+
} | null;
|
|
121
|
+
|
|
122
|
+
if (data === null) {
|
|
123
|
+
return defaultRunContext;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
settings: {
|
|
128
|
+
...defaultSettings,
|
|
129
|
+
...data.settings,
|
|
130
|
+
modes: data.settings?.modes ?? [],
|
|
131
|
+
setupScript: data.settings?.setupScript ?? null,
|
|
132
|
+
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
|
|
133
|
+
prepushScript: data.settings?.prepushScript ?? null,
|
|
134
|
+
stopScript: data.settings?.stopScript ?? null,
|
|
135
|
+
learningsHeadings: data.settings?.learningsHeadings ?? [],
|
|
136
|
+
},
|
|
137
|
+
apiToken: data.apiToken,
|
|
138
|
+
oss: data.oss ?? false,
|
|
139
|
+
plan: data.plan ?? "none",
|
|
140
|
+
};
|
|
141
|
+
} catch {
|
|
142
|
+
clearTimeout(timeoutId);
|
|
143
|
+
return defaultRunContext;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as core from "@actions/core";
|
|
2
|
+
import type { Octokit } from "@octokit/rest";
|
|
3
|
+
import { log } from "#app/utils/cli";
|
|
4
|
+
import { type OctokitWithPlugins, parseRepoContext } from "#app/utils/github";
|
|
5
|
+
import { type AccountPlan, fetchRunContext, type RepoSettings } from "#app/utils/runContext";
|
|
6
|
+
import packageJson from "#package.json" with { type: "json" };
|
|
7
|
+
|
|
8
|
+
export interface RunContextData {
|
|
9
|
+
repo: {
|
|
10
|
+
owner: string;
|
|
11
|
+
name: string;
|
|
12
|
+
data: Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
|
13
|
+
};
|
|
14
|
+
repoSettings: RepoSettings;
|
|
15
|
+
apiToken: string;
|
|
16
|
+
oss: boolean;
|
|
17
|
+
plan: AccountPlan;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ResolveRunContextDataParams {
|
|
21
|
+
octokit: OctokitWithPlugins;
|
|
22
|
+
token: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* initialize run context data: parse context, fetch repo info and settings
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveRunContextData(
|
|
29
|
+
params: ResolveRunContextDataParams,
|
|
30
|
+
): Promise<RunContextData> {
|
|
31
|
+
log.info(`» running Terramend v${packageJson.version}...`);
|
|
32
|
+
|
|
33
|
+
const repoContext = parseRepoContext();
|
|
34
|
+
|
|
35
|
+
let oidcToken: string | undefined;
|
|
36
|
+
try {
|
|
37
|
+
oidcToken = await core.getIDToken("terramend-api");
|
|
38
|
+
} catch {
|
|
39
|
+
// OIDC not available (local dev, non-actions environment, fork PRs)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const [repoResponse, runContext] = await Promise.all([
|
|
43
|
+
params.octokit.repos.get({ owner: repoContext.owner, repo: repoContext.name }),
|
|
44
|
+
fetchRunContext({ token: params.token, repoContext, oidcToken }),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
repo: {
|
|
49
|
+
owner: repoContext.owner,
|
|
50
|
+
name: repoContext.name,
|
|
51
|
+
data: repoResponse.data,
|
|
52
|
+
},
|
|
53
|
+
repoSettings: runContext.settings,
|
|
54
|
+
apiToken: runContext.apiToken,
|
|
55
|
+
oss: runContext.oss,
|
|
56
|
+
plan: runContext.plan,
|
|
57
|
+
};
|
|
58
|
+
}
|