Skip to content

Commit 5f64876

Browse files
saphidclaude
authored andcommitted
fix(server): a publish whose first push fails still shows the new remote
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 55e2538 commit 5f64876

2 files changed

Lines changed: 94 additions & 2 deletions

File tree

‎apps/server/src/ws.test.ts‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
33
import { assert, it } from "@effect/vitest";
44
import {
55
AuthSessionId,
6+
AuthSourceControlWriteScope,
67
EnvironmentId,
78
ProjectId,
89
ThreadId,
910
ProviderDriverKind,
1011
ProviderInstanceId,
12+
SourceControlRepositoryError,
1113
WS_METHODS,
14+
WsRpcGroup,
1215
WsServerRefreshProvidersRpc,
1316
WsSubscribeServerConfigRpc,
1417
type OrchestrationV2StoredEvent,
@@ -58,6 +61,7 @@ import * as ModelManifest from "./provider/ModelManifest.ts";
5861
import * as Orchestrator from "./orchestration-v2/Orchestrator.ts";
5962
import * as OtlpEndpointCheck from "./observability/OtlpEndpointCheck.ts";
6063
import * as PairingGrantStore from "./auth/PairingGrantStore.ts";
64+
import * as RpcAuthorization from "./auth/RpcAuthorization.ts";
6165
import * as PeerLinkRequests from "./peer/PeerLinkRequests.ts";
6266
import * as PeerLinks from "./peer/PeerLinks.ts";
6367
import * as PortScanner from "./preview/PortScanner.ts";
@@ -1035,6 +1039,90 @@ it.effect("on-demand clients get provider config without workspace snapshots", (
10351039
),
10361040
);
10371041

1042+
it.effect("a publish whose push fails still refreshes the new remote", () =>
1043+
Effect.gen(function* () {
1044+
const refreshes = yield* Queue.unbounded<string>();
1045+
const exits = yield* Queue.unbounded<string>();
1046+
const services = Layer.mergeAll(
1047+
layerUnusedConfigRpcServices,
1048+
Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({
1049+
publishRepository: (input) =>
1050+
Effect.fail(
1051+
new SourceControlRepositoryError({
1052+
provider: input.provider,
1053+
operation: "publishRepository",
1054+
detail: 'Created owner/repo and added it as remote "origin", but the push failed.',
1055+
}),
1056+
),
1057+
}),
1058+
Layer.mock(RepositoryIdentityResolver.RepositoryIdentityResolver)({
1059+
resolve: (cwd) => Queue.offer(refreshes, `identity ${cwd}`).pipe(Effect.as(null)),
1060+
}),
1061+
Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({
1062+
refreshStatus: (cwd) =>
1063+
Queue.offer(refreshes, `status ${cwd}`).pipe(Effect.andThen(Effect.die("unused"))),
1064+
}),
1065+
Layer.mock(ExternalLauncher.ExternalLauncher)({}),
1066+
Layer.mock(ProviderRegistry.ProviderRegistry)({}),
1067+
Layer.mock(UsageLimitSources.UsageLimitSources)({}),
1068+
Layer.mock(RemoteOpenTargets.RemoteOpenTargets)({}),
1069+
Layer.mock(DirectEndpoints.DirectEndpoints)({}),
1070+
Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ namedProjectsRoot: "/unused" }),
1071+
Layer.mock(ScratchThreadPromotion.ScratchThreadPromotion)({}),
1072+
Layer.mock(ServerEnvironment.ServerEnvironment)({}),
1073+
Layer.mock(EnvironmentAuth.EnvironmentAuth)({}),
1074+
);
1075+
const server = yield* RpcServer.makeNoSerialization(WsRpcGroup, {
1076+
...WS_RPC_SERVER_OPTIONS,
1077+
onFromServer: (response) =>
1078+
response._tag === "Exit" ? Queue.offer(exits, response.exit._tag) : Effect.void,
1079+
}).pipe(
1080+
Effect.provide(
1081+
layerWsRpc(
1082+
{
1083+
sessionId: AuthSessionId.make("publish-test"),
1084+
subject: "publish-test",
1085+
method: "browser-session-cookie",
1086+
scopes: [AuthSourceControlWriteScope],
1087+
},
1088+
{},
1089+
yield* ServerBrowser.ServerBrowser,
1090+
() => Effect.void,
1091+
).pipe(
1092+
Layer.provide(services),
1093+
Layer.merge(RpcAuthorization.layer([AuthSourceControlWriteScope])),
1094+
),
1095+
),
1096+
);
1097+
yield* server.write(0, {
1098+
_tag: "Request",
1099+
id: RpcMessage.RequestId("publish"),
1100+
tag: WS_METHODS.sourceControlPublishRepository,
1101+
payload: {
1102+
cwd: "/repo",
1103+
provider: "github",
1104+
repository: "owner/repo",
1105+
visibility: "private",
1106+
},
1107+
headers: Headers.empty,
1108+
});
1109+
assert.equal(yield* Queue.take(exits), "Failure");
1110+
assert.deepEqual(
1111+
[yield* Queue.take(refreshes), yield* Queue.take(refreshes)],
1112+
["identity /repo", "status /repo"],
1113+
);
1114+
}).pipe(
1115+
Effect.provide(
1116+
Layer.mergeAll(
1117+
Layer.fresh(layerConfigSources),
1118+
Layer.mock(PreviewAutomationBroker.PreviewAutomationBroker)({}),
1119+
Layer.mock(ServerBrowser.ServerBrowser)({}),
1120+
),
1121+
),
1122+
Effect.scoped,
1123+
),
1124+
);
1125+
10381126
// Compaction keeps the oldest and newest event per message, so a cursor older
10391127
// than the deleted rows still replays each message where the client would
10401128
// first have seen it (reducers append on first sight) and reaches the same

‎apps/server/src/ws.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2815,8 +2815,12 @@ export const layerWsRpc = (
28152815
[WS_METHODS.subscribeProjectClones]: () => projectCloneTracker.stream,
28162816
[WS_METHODS.sourceControlPublishRepository]: (input) =>
28172817
sourceControlRepositories.publishRepository(input).pipe(
2818-
Effect.tap(() => repositoryIdentityResolver.resolve(input.cwd, { refresh: true })),
2819-
Effect.tap(() => refreshGitStatus(input.cwd)),
2818+
// A failed push still leaves the new remote added.
2819+
Effect.ensuring(
2820+
repositoryIdentityResolver
2821+
.resolve(input.cwd, { refresh: true })
2822+
.pipe(Effect.andThen(refreshGitStatus(input.cwd))),
2823+
),
28202824
),
28212825
[WS_METHODS.projectsSearchEntries]: (input) =>
28222826
workspaceEntries.search(input).pipe(

0 commit comments

Comments
 (0)