Skip to content

Commit 7585edd

Browse files
authored
Merge pull request #210 from ProverCoderAI/fix/restore-check-workflow
fix(ci): restore check workflow compliance
2 parents 7f4cb2c + 01baedc commit 7585edd

File tree

8 files changed

+271
-218
lines changed

8 files changed

+271
-218
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { FetchHttpClient, HttpClient } from "@effect/platform"
2+
import * as ParseResult from "@effect/schema/ParseResult"
3+
import * as Schema from "@effect/schema/Schema"
4+
import { Effect, Either } from "effect"
5+
6+
import { buildApiBaseUrlCandidates, resolveApiPort, resolveConfiguredApiBaseUrl } from "./controller-reachability.js"
7+
import type { ControllerBootstrapError } from "./host-errors.js"
8+
9+
type HealthProbeResult = {
10+
readonly apiBaseUrl: string
11+
readonly revision: string | null
12+
}
13+
14+
const HealthProbeBodySchema = Schema.Struct({
15+
revision: Schema.optional(Schema.String)
16+
})
17+
18+
const HealthProbeBodyFromStringSchema = Schema.parseJson(HealthProbeBodySchema)
19+
20+
const controllerBootstrapError = (message: string): ControllerBootstrapError => ({
21+
_tag: "ControllerBootstrapError",
22+
message
23+
})
24+
25+
const parseHealthRevision = (text: string): string | null =>
26+
Either.match(ParseResult.decodeUnknownEither(HealthProbeBodyFromStringSchema)(text), {
27+
onLeft: () => null,
28+
onRight: (body) => {
29+
const revision = body.revision
30+
return revision !== undefined && revision.trim().length > 0 ? revision.trim() : null
31+
}
32+
})
33+
34+
const probeHealth = (apiBaseUrl: string): Effect.Effect<HealthProbeResult, ControllerBootstrapError> =>
35+
Effect.gen(function*(_) {
36+
const client = yield* _(HttpClient.HttpClient)
37+
const response = yield* _(client.get(`${apiBaseUrl}/health`, { headers: { accept: "application/json" } }))
38+
const bodyText = yield* _(response.text)
39+
40+
if (response.status >= 200 && response.status < 300) {
41+
return {
42+
apiBaseUrl,
43+
revision: parseHealthRevision(bodyText)
44+
}
45+
}
46+
47+
return yield* _(
48+
Effect.fail(
49+
controllerBootstrapError(
50+
`docker-git controller health returned ${response.status} at ${apiBaseUrl}/health`
51+
)
52+
)
53+
)
54+
}).pipe(
55+
Effect.provide(FetchHttpClient.layer),
56+
Effect.mapError((error): ControllerBootstrapError =>
57+
error._tag === "ControllerBootstrapError"
58+
? error
59+
: {
60+
_tag: "ControllerBootstrapError",
61+
message: `docker-git controller health probe failed at ${apiBaseUrl}/health\nDetails: ${String(error)}`
62+
}
63+
)
64+
)
65+
66+
const findReachableHealthProbe = (
67+
candidateUrls: ReadonlyArray<string>
68+
): Effect.Effect<HealthProbeResult, ControllerBootstrapError> =>
69+
Effect.gen(function*(_) {
70+
if (candidateUrls.length === 0) {
71+
return yield* _(
72+
Effect.fail(controllerBootstrapError("No docker-git controller endpoint candidates were generated."))
73+
)
74+
}
75+
76+
for (const candidateUrl of candidateUrls) {
77+
const healthy = yield* _(probeHealth(candidateUrl).pipe(Effect.either))
78+
if (Either.isRight(healthy)) {
79+
return healthy.right
80+
}
81+
}
82+
83+
return yield* _(Effect.fail(controllerBootstrapError("No docker-git controller endpoint responded to /health.")))
84+
})
85+
86+
const findReachableHealthProbeOrNull = (
87+
candidateUrls: ReadonlyArray<string>
88+
): Effect.Effect<HealthProbeResult | null> =>
89+
findReachableHealthProbe(candidateUrls).pipe(
90+
Effect.match({
91+
onFailure: () => null,
92+
onSuccess: (probe) => probe
93+
})
94+
)
95+
96+
export const findReachableApiBaseUrl = (
97+
candidateUrls: ReadonlyArray<string>
98+
): Effect.Effect<string, ControllerBootstrapError> =>
99+
findReachableHealthProbe(candidateUrls).pipe(Effect.map(({ apiBaseUrl }) => apiBaseUrl))
100+
101+
export const findReachableDirectHealthProbe = (options: {
102+
readonly explicitApiBaseUrl: string | undefined
103+
readonly cachedApiBaseUrl: string | undefined
104+
}): Effect.Effect<HealthProbeResult | null> =>
105+
findReachableHealthProbeOrNull(
106+
buildApiBaseUrlCandidates({
107+
explicitApiBaseUrl: options.explicitApiBaseUrl,
108+
cachedApiBaseUrl: options.cachedApiBaseUrl,
109+
defaultApiBaseUrl: resolveConfiguredApiBaseUrl(),
110+
currentContainerNetworks: {},
111+
controllerNetworks: {},
112+
port: resolveApiPort()
113+
})
114+
)

packages/app/src/docker-git/controller.ts

Lines changed: 25 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { FetchHttpClient, HttpClient } from "@effect/platform"
21
import { Duration, Effect, pipe, Schedule } from "effect"
32

43
import {
@@ -13,6 +12,7 @@ import {
1312
resolveCurrentContainerNetworks,
1413
runCompose
1514
} from "./controller-docker.js"
15+
import { findReachableApiBaseUrl, findReachableDirectHealthProbe } from "./controller-health.js"
1616
import {
1717
buildApiBaseUrlCandidates,
1818
type DockerNetworkIps,
@@ -31,11 +31,6 @@ export { buildApiBaseUrlCandidates, isRemoteDockerHost } from "./controller-reac
3131

3232
let selectedApiBaseUrl: string | undefined
3333

34-
type HealthProbeResult = {
35-
readonly apiBaseUrl: string
36-
readonly revision: string | null
37-
}
38-
3934
const controllerBootstrapError = (message: string): ControllerBootstrapError => ({
4035
_tag: "ControllerBootstrapError",
4136
message
@@ -48,84 +43,6 @@ const rememberSelectedApiBaseUrl = (value: string): void => {
4843
export const resolveApiBaseUrl = (): string =>
4944
resolveExplicitApiBaseUrl() ?? selectedApiBaseUrl ?? resolveConfiguredApiBaseUrl()
5045

51-
const parseHealthRevision = (text: string): string | null => {
52-
try {
53-
const parsed: unknown = JSON.parse(text)
54-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
55-
return null
56-
}
57-
const revision = Reflect.get(parsed, "revision")
58-
return typeof revision === "string" && revision.trim().length > 0 ? revision.trim() : null
59-
} catch {
60-
return null
61-
}
62-
}
63-
64-
const probeHealth = (apiBaseUrl: string): Effect.Effect<HealthProbeResult, ControllerBootstrapError> =>
65-
Effect.gen(function*(_) {
66-
const client = yield* _(HttpClient.HttpClient)
67-
const response = yield* _(client.get(`${apiBaseUrl}/health`, { headers: { accept: "application/json" } }))
68-
const bodyText = yield* _(response.text)
69-
70-
if (response.status >= 200 && response.status < 300) {
71-
return {
72-
apiBaseUrl,
73-
revision: parseHealthRevision(bodyText)
74-
}
75-
}
76-
77-
return yield* _(
78-
Effect.fail(
79-
controllerBootstrapError(
80-
`docker-git controller health returned ${response.status} at ${apiBaseUrl}/health`
81-
)
82-
)
83-
)
84-
}).pipe(
85-
Effect.provide(FetchHttpClient.layer),
86-
Effect.mapError((error): ControllerBootstrapError =>
87-
error._tag === "ControllerBootstrapError"
88-
? error
89-
: {
90-
_tag: "ControllerBootstrapError",
91-
message: `docker-git controller health probe failed at ${apiBaseUrl}/health\nDetails: ${String(error)}`
92-
}
93-
)
94-
)
95-
96-
const findReachableApiBaseUrl = (
97-
candidateUrls: ReadonlyArray<string>
98-
): Effect.Effect<string, ControllerBootstrapError> =>
99-
findReachableHealthProbe(candidateUrls).pipe(Effect.map(({ apiBaseUrl }) => apiBaseUrl))
100-
101-
const findReachableHealthProbe = (
102-
candidateUrls: ReadonlyArray<string>
103-
): Effect.Effect<HealthProbeResult, ControllerBootstrapError> =>
104-
Effect.gen(function*(_) {
105-
if (candidateUrls.length === 0) {
106-
return yield* _(
107-
Effect.fail(controllerBootstrapError("No docker-git controller endpoint candidates were generated."))
108-
)
109-
}
110-
111-
for (const candidateUrl of candidateUrls) {
112-
const healthy = yield* _(
113-
probeHealth(candidateUrl).pipe(
114-
Effect.match({
115-
onFailure: () => undefined,
116-
onSuccess: (result) => result
117-
})
118-
)
119-
)
120-
121-
if (healthy !== undefined) {
122-
return healthy
123-
}
124-
}
125-
126-
return yield* _(Effect.fail(controllerBootstrapError("No docker-git controller endpoint responded to /health.")))
127-
})
128-
12946
const collectReachabilityDiagnostics = (
13047
candidateUrls: ReadonlyArray<string>,
13148
currentContainerNetworks: DockerNetworkIps,
@@ -193,40 +110,16 @@ const failIfRemoteDockerWithoutApiUrl = (): Effect.Effect<void, ControllerBootst
193110
)
194111
}
195112

196-
const findReachableApiBaseUrlOption = (
113+
const findReachableApiBaseUrlOrNull = (
197114
candidateUrls: ReadonlyArray<string>
198-
): Effect.Effect<string | undefined, ControllerBootstrapError> =>
115+
): Effect.Effect<string | null> =>
199116
findReachableApiBaseUrl(candidateUrls).pipe(
200117
Effect.match({
201-
onFailure: (): string | undefined => undefined,
118+
onFailure: () => null,
202119
onSuccess: (apiBaseUrl) => apiBaseUrl
203120
})
204121
)
205122

206-
const findReachableHealthProbeOption = (
207-
candidateUrls: ReadonlyArray<string>
208-
): Effect.Effect<HealthProbeResult | undefined, ControllerBootstrapError> =>
209-
findReachableHealthProbe(candidateUrls).pipe(
210-
Effect.match({
211-
onFailure: (): HealthProbeResult | undefined => undefined,
212-
onSuccess: (probe) => probe
213-
})
214-
)
215-
216-
const findReachableDirectHealthProbe = (
217-
explicitApiBaseUrl: string | undefined
218-
): Effect.Effect<HealthProbeResult | undefined, ControllerBootstrapError> =>
219-
findReachableHealthProbeOption(
220-
buildApiBaseUrlCandidates({
221-
explicitApiBaseUrl,
222-
cachedApiBaseUrl: selectedApiBaseUrl,
223-
defaultApiBaseUrl: resolveConfiguredApiBaseUrl(),
224-
currentContainerNetworks: {},
225-
controllerNetworks: {},
226-
port: resolveApiPort()
227-
})
228-
)
229-
230123
const failIfExplicitApiUrlIsUnreachable = (
231124
explicitApiBaseUrl: string | undefined
232125
): Effect.Effect<void, ControllerBootstrapError> =>
@@ -294,15 +187,15 @@ const buildBootstrapCandidateUrls = (
294187
const reuseReachableControllerIfPossible = (
295188
context: ControllerBootstrapContext
296189
): Effect.Effect<boolean, ControllerBootstrapError> =>
297-
findReachableApiBaseUrlOption(
190+
findReachableApiBaseUrlOrNull(
298191
buildBootstrapCandidateUrls(
299192
context.explicitApiBaseUrl,
300193
context.currentContainerNetworks,
301194
context.initialControllerNetworks
302195
)
303196
).pipe(
304197
Effect.map((reachableApiBaseUrl) => {
305-
if (reachableApiBaseUrl === undefined || context.forceRecreateController) {
198+
if (reachableApiBaseUrl === null || context.forceRecreateController) {
306199
return false
307200
}
308201
rememberSelectedApiBaseUrl(reachableApiBaseUrl)
@@ -362,22 +255,32 @@ export const ensureControllerReady = (): Effect.Effect<void, ControllerBootstrap
362255
yield* _(failIfRemoteDockerWithoutApiUrl())
363256
const explicitApiBaseUrl = resolveExplicitApiBaseUrl()
364257
const localControllerRevision = yield* _(prepareLocalControllerRevision())
365-
if (explicitApiBaseUrl !== undefined) {
366-
const reachableBeforeDocker = yield* _(findReachableDirectHealthProbe(explicitApiBaseUrl))
367-
if (reachableBeforeDocker !== undefined) {
258+
if (explicitApiBaseUrl === undefined) {
259+
const reachableBeforeDocker = yield* _(
260+
findReachableDirectHealthProbe({
261+
explicitApiBaseUrl,
262+
cachedApiBaseUrl: selectedApiBaseUrl
263+
})
264+
)
265+
if (
266+
reachableBeforeDocker !== null &&
267+
reachableBeforeDocker.revision === localControllerRevision
268+
) {
368269
rememberSelectedApiBaseUrl(reachableBeforeDocker.apiBaseUrl)
369270
return
370271
}
371-
yield* _(failIfExplicitApiUrlIsUnreachable(explicitApiBaseUrl))
372272
} else {
373-
const reachableBeforeDocker = yield* _(findReachableDirectHealthProbe(undefined))
374-
if (
375-
reachableBeforeDocker !== undefined &&
376-
reachableBeforeDocker.revision === localControllerRevision
377-
) {
273+
const reachableBeforeDocker = yield* _(
274+
findReachableDirectHealthProbe({
275+
explicitApiBaseUrl,
276+
cachedApiBaseUrl: selectedApiBaseUrl
277+
})
278+
)
279+
if (reachableBeforeDocker !== null) {
378280
rememberSelectedApiBaseUrl(reachableBeforeDocker.apiBaseUrl)
379281
return
380282
}
283+
yield* _(failIfExplicitApiUrlIsUnreachable(explicitApiBaseUrl))
381284
}
382285

383286
const bootstrapContext = yield* _(loadControllerBootstrapContext())

0 commit comments

Comments
 (0)