@olivervea/pipelines 2.0.0-beta.18 → 2.0.0-beta.19
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/package.json +4 -2
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +111 -43
- package/src/build/cli.ts +28 -21
- package/src/build/elf.ts +8 -7
- package/src/build/hashes.ts +61 -28
- package/src/build/project.ts +69 -30
- package/src/builder/pipeline.ts +274 -102
- package/src/builder/refs.ts +12 -6
- package/src/core/brands.ts +6 -6
- package/src/core/config-refs.ts +46 -20
- package/src/core/config.ts +110 -33
- package/src/core/external.ts +30 -12
- package/src/core/handlers.ts +54 -14
- package/src/core/models.ts +29 -14
- package/src/core/origin.ts +7 -4
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +39 -23
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +111 -40
- package/src/ctx/index.ts +75 -34
- package/src/ctx/sh.ts +104 -44
- package/src/ctx/types.ts +4 -4
- package/src/describe/describe.ts +550 -196
- package/src/describe/manifest-schema.ts +210 -80
- package/src/describe/mapper-source.ts +249 -104
- package/src/describe/record.ts +67 -37
- package/src/dev/cli.ts +64 -34
- package/src/harness/env.ts +33 -27
- package/src/harness/execute.ts +55 -29
- package/src/harness/index.ts +21 -8
- package/src/harness/main.ts +197 -74
- package/src/harness/result.ts +38 -22
- package/src/harness/validate.ts +62 -19
- package/src/index.ts +123 -14
- package/src/library/checks.ts +11 -6
- package/src/library/compose-deploy.ts +40 -23
- package/src/library/github-source.ts +69 -39
- package/src/library/helm-deploy-ssh.ts +213 -76
- package/src/library/helm-deploy.ts +263 -85
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +293 -117
- package/src/library/index.ts +47 -15
- package/src/library/models.ts +17 -9
- package/src/library/ssh.ts +31 -14
- package/src/testing/index.ts +63 -24
- package/src/util/canonical.ts +7 -4
- package/src/util/handler-hash.ts +15 -5
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +74 -37
package/src/index.ts
CHANGED
|
@@ -1,16 +1,125 @@
|
|
|
1
1
|
/// <reference path="./assets.d.ts" />
|
|
2
2
|
// @olivervea/pipelines — authoring API (contracts, handlers, pipeline builder).
|
|
3
|
-
export { z } from
|
|
4
|
-
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export {
|
|
3
|
+
export { z } from 'zod';
|
|
4
|
+
export {
|
|
5
|
+
model,
|
|
6
|
+
fileRef,
|
|
7
|
+
optional,
|
|
8
|
+
type Model,
|
|
9
|
+
type AnyModel,
|
|
10
|
+
type Infer,
|
|
11
|
+
type InferInput,
|
|
12
|
+
type Plain,
|
|
13
|
+
type FileRef,
|
|
14
|
+
type Optional,
|
|
15
|
+
} from './core/models.ts';
|
|
16
|
+
export { secret, type Secret, type DeclaredSecrets } from './core/secrets.ts';
|
|
17
|
+
export {
|
|
18
|
+
config,
|
|
19
|
+
secretRef,
|
|
20
|
+
isConfigRef,
|
|
21
|
+
type ConfigEntry,
|
|
22
|
+
type AnyConfigEntry,
|
|
23
|
+
type ConfigValues,
|
|
24
|
+
type ConfigRef,
|
|
25
|
+
type ConfigNode,
|
|
26
|
+
type Configurable,
|
|
27
|
+
type DiscriminatorOf,
|
|
28
|
+
type VariantOf,
|
|
29
|
+
} from './core/config.ts';
|
|
30
|
+
export {
|
|
31
|
+
hmacSha256,
|
|
32
|
+
githubSignature,
|
|
33
|
+
bearer,
|
|
34
|
+
type WebhookVerify,
|
|
35
|
+
type WebhookEvent,
|
|
36
|
+
} from './core/webhooks.ts';
|
|
37
|
+
export { resolveConfigRefs, isLiteral, type Resolved } from './core/config-refs.ts';
|
|
38
|
+
export {
|
|
39
|
+
callback,
|
|
40
|
+
external,
|
|
41
|
+
person,
|
|
42
|
+
poll,
|
|
43
|
+
type External,
|
|
44
|
+
type ExternalOptions,
|
|
45
|
+
type ExternalSource,
|
|
46
|
+
type PollOptions,
|
|
47
|
+
} from './core/external.ts';
|
|
48
|
+
export {
|
|
49
|
+
step,
|
|
50
|
+
type Step,
|
|
51
|
+
type AnyStep,
|
|
52
|
+
type StepDef,
|
|
53
|
+
type StepInput,
|
|
54
|
+
type StepKind,
|
|
55
|
+
type ConfigOf,
|
|
56
|
+
type InputEntry,
|
|
57
|
+
type InputSpec,
|
|
58
|
+
type LiteralName,
|
|
59
|
+
type OutputValue,
|
|
60
|
+
} from './core/steps.ts';
|
|
61
|
+
export type { Simplify } from './core/types.ts';
|
|
62
|
+
export {
|
|
63
|
+
implement,
|
|
64
|
+
type Implementation,
|
|
65
|
+
type Handler,
|
|
66
|
+
type HandlerResult,
|
|
67
|
+
type Skipped,
|
|
68
|
+
} from './core/handlers.ts';
|
|
69
|
+
export type {
|
|
70
|
+
Ctx,
|
|
71
|
+
RunInfo,
|
|
72
|
+
Sh,
|
|
73
|
+
ShOptions,
|
|
74
|
+
ExecOptions,
|
|
75
|
+
Http,
|
|
76
|
+
HttpInit,
|
|
77
|
+
RetryOptions,
|
|
78
|
+
OutputFileOptions,
|
|
79
|
+
} from './ctx/types.ts';
|
|
80
|
+
export type { Ref, Mappable, MapperResult } from './builder/refs.ts';
|
|
81
|
+
export {
|
|
82
|
+
pipeline,
|
|
83
|
+
Pipeline,
|
|
84
|
+
type PipelineDef,
|
|
85
|
+
type RetentionPolicy,
|
|
86
|
+
type InRefs,
|
|
87
|
+
type InitialAvail,
|
|
88
|
+
type WhenCond,
|
|
89
|
+
type PushEvent,
|
|
90
|
+
type StageEntry,
|
|
91
|
+
type CheckStage,
|
|
92
|
+
type MapperFn,
|
|
93
|
+
} from './builder/pipeline.ts';
|
|
94
|
+
export {
|
|
95
|
+
steps,
|
|
96
|
+
Source,
|
|
97
|
+
BuiltImage,
|
|
98
|
+
PushedImage,
|
|
99
|
+
HelmRelease,
|
|
100
|
+
FailureContext,
|
|
101
|
+
type GitHubSourceDef,
|
|
102
|
+
type ImageBuildDef,
|
|
103
|
+
type ImageBuildTarballDef,
|
|
104
|
+
type ImageBuildPushDef,
|
|
105
|
+
type RegistryAuth,
|
|
106
|
+
type RegistryAuthConfig,
|
|
107
|
+
type HelmDeployOverSshDef,
|
|
108
|
+
helmTarget,
|
|
109
|
+
deployTarget,
|
|
110
|
+
type DeployTargetOptions,
|
|
111
|
+
type ComposeDeployDef,
|
|
112
|
+
type ComposeTargetValue,
|
|
113
|
+
type HelmDeployInClusterDef,
|
|
114
|
+
type HelmDeploySshTargetDef,
|
|
115
|
+
type HelmTargetDeployDef,
|
|
116
|
+
type HelmTargetValue,
|
|
117
|
+
type InClusterTargetValue,
|
|
118
|
+
type SshTargetValue,
|
|
119
|
+
httpHealthCheck,
|
|
120
|
+
type HttpHealthCheckOptions,
|
|
121
|
+
sshSession,
|
|
122
|
+
type SshSession,
|
|
123
|
+
serviceUrl,
|
|
124
|
+
checkSpa,
|
|
125
|
+
} from './library/index.ts';
|
package/src/library/checks.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Checks for deployed apps, used from custom test handlers.
|
|
2
|
-
import type { Ctx } from
|
|
2
|
+
import type { Ctx } from '../ctx/types.ts';
|
|
3
3
|
|
|
4
4
|
/** The in-cluster URL of a release's Service, assuming the chart names it after the release. */
|
|
5
5
|
export function serviceUrl(release: { release: string; namespace: string }): string {
|
|
@@ -10,14 +10,19 @@ export function serviceUrl(release: { release: string; namespace: string }): str
|
|
|
10
10
|
* Checks `url` serves a single-page app: `GET <url>/` references a `/assets/*.js` bundle (Vite's
|
|
11
11
|
* layout), and the bundle is served as JavaScript. Returns the page, for app-specific checks.
|
|
12
12
|
*/
|
|
13
|
-
export async function checkSpa(
|
|
14
|
-
|
|
13
|
+
export async function checkSpa(
|
|
14
|
+
ctx: Pick<Ctx, 'http' | 'log'>,
|
|
15
|
+
url: string,
|
|
16
|
+
): Promise<{ html: string; bundle: string }> {
|
|
17
|
+
const base = url.replace(/\/$/, '');
|
|
15
18
|
const html = await (await ctx.http.expectOk(`${base}/`)).text();
|
|
16
19
|
const bundle = html.match(/src="(\/assets\/[^"]+\.js)"/)?.[1];
|
|
17
|
-
if (bundle === undefined)
|
|
20
|
+
if (bundle === undefined)
|
|
21
|
+
throw new Error(`${base}/ does not reference an /assets/*.js bundle`);
|
|
18
22
|
const js = await ctx.http.expectOk(`${base}${bundle}`);
|
|
19
23
|
await js.body?.cancel().catch(() => {});
|
|
20
|
-
if (!(js.headers.get(
|
|
21
|
-
|
|
24
|
+
if (!(js.headers.get('content-type')?.includes('javascript') ?? false))
|
|
25
|
+
throw new Error(`${base}${bundle} is not served as JavaScript`);
|
|
26
|
+
ctx.log('SPA served', { url: base, bundle });
|
|
22
27
|
return { html, bundle };
|
|
23
28
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
// steps.composeDeploy — ask a Docker Compose deployer to recreate a service with an image. The step is
|
|
2
2
|
// thin on purpose: the deployer (pull, recreate, health, rollback) lives beside the compose project
|
|
3
3
|
// and is fixed there, behind a versioned HTTP API, without editing pipelines.
|
|
4
|
-
import type { Ctx } from
|
|
5
|
-
import type { Configurable } from
|
|
6
|
-
import { resolveConfigRefs } from
|
|
7
|
-
import { implement, type Implementation } from
|
|
8
|
-
import type { Secret } from
|
|
9
|
-
import { step, type LiteralName, type Step } from
|
|
10
|
-
import { httpHealthCheck } from
|
|
11
|
-
import { HelmRelease, PushedImage } from
|
|
4
|
+
import type { Ctx } from '../ctx/types.ts';
|
|
5
|
+
import type { Configurable } from '../core/config.ts';
|
|
6
|
+
import { resolveConfigRefs } from '../core/config-refs.ts';
|
|
7
|
+
import { implement, type Implementation } from '../core/handlers.ts';
|
|
8
|
+
import type { Secret } from '../core/secrets.ts';
|
|
9
|
+
import { step, type LiteralName, type Step } from '../core/steps.ts';
|
|
10
|
+
import { httpHealthCheck } from './http-health.ts';
|
|
11
|
+
import { HelmRelease, PushedImage } from './models.ts';
|
|
12
12
|
|
|
13
13
|
/** The step's own image: the work is one HTTP call from the harness. */
|
|
14
|
-
export const COMPOSE_DEPLOY_IMAGE =
|
|
14
|
+
export const COMPOSE_DEPLOY_IMAGE = 'alpine:3.22';
|
|
15
15
|
|
|
16
16
|
/** A resolved `kind: compose` target. */
|
|
17
17
|
export interface ComposeTargetValue {
|
|
18
|
-
kind:
|
|
18
|
+
kind: 'compose';
|
|
19
19
|
deployerUrl: string;
|
|
20
20
|
service: string;
|
|
21
21
|
token: { secret: string };
|
|
@@ -24,7 +24,7 @@ export interface ComposeTargetValue {
|
|
|
24
24
|
|
|
25
25
|
/** What the deployer answers to `POST /v1/deploy`. */
|
|
26
26
|
export interface ComposeDeployResult {
|
|
27
|
-
status:
|
|
27
|
+
status: 'deployed' | 'rolled-back' | 'failed';
|
|
28
28
|
service: string;
|
|
29
29
|
image: string;
|
|
30
30
|
previousImage?: string;
|
|
@@ -58,7 +58,7 @@ export function composeDeploy<const N extends string, const K extends string>(
|
|
|
58
58
|
const contract = step({
|
|
59
59
|
name: def.name,
|
|
60
60
|
description: def.description,
|
|
61
|
-
kind:
|
|
61
|
+
kind: 'composeDeploy',
|
|
62
62
|
image: COMPOSE_DEPLOY_IMAGE,
|
|
63
63
|
input,
|
|
64
64
|
output: HelmRelease,
|
|
@@ -68,15 +68,25 @@ export function composeDeploy<const N extends string, const K extends string>(
|
|
|
68
68
|
});
|
|
69
69
|
return implement(contract, async ({ image }, ctx: Ctx) => {
|
|
70
70
|
const target = resolveConfigRefs(def.target, ctx.config ?? {}) as ComposeTargetValue;
|
|
71
|
-
if (target?.kind !==
|
|
72
|
-
|
|
71
|
+
if (target?.kind !== 'compose')
|
|
72
|
+
throw new Error(
|
|
73
|
+
`composeDeploy '${def.name}': the target is kind '${String(target?.kind)}', not 'compose'`,
|
|
74
|
+
);
|
|
75
|
+
if (target.token.secret !== def.token.name)
|
|
76
|
+
throw new Error(
|
|
77
|
+
`composeDeploy '${def.name}': the target's token names secret ${target.token.secret}, but the step declares ${def.token.name}`,
|
|
78
|
+
);
|
|
73
79
|
const token = (ctx.secrets as Record<string, string>)[def.token.name];
|
|
74
80
|
const ref = `${image.repository}@${image.digest}`;
|
|
75
|
-
const url = `${target.deployerUrl.replace(/\/+$/,
|
|
76
|
-
ctx.log(
|
|
81
|
+
const url = `${target.deployerUrl.replace(/\/+$/, '')}/v1/deploy`;
|
|
82
|
+
ctx.log('deploying', {
|
|
83
|
+
service: target.service,
|
|
84
|
+
image: ref,
|
|
85
|
+
deployer: target.deployerUrl,
|
|
86
|
+
});
|
|
77
87
|
const res = await ctx.http.fetch(url, {
|
|
78
|
-
method:
|
|
79
|
-
headers: { authorization: `Bearer ${token}`,
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
80
90
|
body: JSON.stringify({ service: target.service, image: ref }),
|
|
81
91
|
});
|
|
82
92
|
const text = await res.text();
|
|
@@ -87,10 +97,17 @@ export function composeDeploy<const N extends string, const K extends string>(
|
|
|
87
97
|
// reported below
|
|
88
98
|
}
|
|
89
99
|
for (const line of result?.log ?? []) ctx.log(`deployer: ${line}`);
|
|
90
|
-
if (!res.ok || result?.status !==
|
|
91
|
-
throw new Error(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
if (!res.ok || result?.status !== 'deployed')
|
|
101
|
+
throw new Error(
|
|
102
|
+
`the deployer answered HTTP ${res.status}: ${result ? `${result.status}${result.message ? `: ${result.message}` : ''}` : text.slice(0, 300)}`,
|
|
103
|
+
);
|
|
104
|
+
if (target.healthUrl)
|
|
105
|
+
await httpHealthCheck(ctx, target.healthUrl, { attempts: 12, delaySeconds: 5 });
|
|
106
|
+
ctx.log('deployed', {
|
|
107
|
+
service: target.service,
|
|
108
|
+
image: ref,
|
|
109
|
+
previous: result.previousImage ?? '-',
|
|
110
|
+
});
|
|
111
|
+
return { release: target.service, namespace: 'compose', image: ref };
|
|
95
112
|
}) as never;
|
|
96
113
|
}
|
|
@@ -2,22 +2,25 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Runs entirely inside the harness (HTTP + tar extraction in Bun): no git, wget or tar is needed
|
|
4
4
|
// in the image, so v1's busybox-wget TLS footgun (`--no-check-certificate`) is gone.
|
|
5
|
-
import { mkdirSync, mkdtempSync, readdirSync, statSync } from
|
|
6
|
-
import { join } from
|
|
7
|
-
import { z } from
|
|
8
|
-
import type { Ctx } from
|
|
9
|
-
import type { Secret } from
|
|
10
|
-
import type { Configurable } from
|
|
11
|
-
import { resolveConfigRefs } from
|
|
12
|
-
import { implement, type Implementation } from
|
|
13
|
-
import { step, type LiteralName, type Step } from
|
|
14
|
-
import { extractTar } from
|
|
15
|
-
import { Source } from
|
|
5
|
+
import { mkdirSync, mkdtempSync, readdirSync, statSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import type { Ctx } from '../ctx/types.ts';
|
|
9
|
+
import type { Secret } from '../core/secrets.ts';
|
|
10
|
+
import type { Configurable } from '../core/config.ts';
|
|
11
|
+
import { resolveConfigRefs } from '../core/config-refs.ts';
|
|
12
|
+
import { implement, type Implementation } from '../core/handlers.ts';
|
|
13
|
+
import { step, type LiteralName, type Step } from '../core/steps.ts';
|
|
14
|
+
import { extractTar } from '../util/tar.ts';
|
|
15
|
+
import { Source } from './models.ts';
|
|
16
16
|
|
|
17
17
|
/** Image the step runs in. The work happens in the harness, so any image would do. */
|
|
18
|
-
export const GITHUB_SOURCE_IMAGE =
|
|
18
|
+
export const GITHUB_SOURCE_IMAGE = 'alpine:3.22';
|
|
19
19
|
|
|
20
|
-
const input = {
|
|
20
|
+
const input = {
|
|
21
|
+
repo: z.string().regex(/^[\w.-]+\/[\w.-]+$/, 'expected owner/name'),
|
|
22
|
+
sha: z.string().min(1),
|
|
23
|
+
};
|
|
21
24
|
export type GitHubSourceInput = typeof input;
|
|
22
25
|
|
|
23
26
|
export interface GitHubSourceDef<N extends string, T extends string> {
|
|
@@ -31,20 +34,24 @@ export interface GitHubSourceDef<N extends string, T extends string> {
|
|
|
31
34
|
timeoutSeconds?: number;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
export const GITHUB_API_URL =
|
|
37
|
+
export const GITHUB_API_URL = 'https://api.github.com';
|
|
35
38
|
|
|
36
39
|
/** A declared secret's value, or "" when it is not set. */
|
|
37
|
-
function optionalSecret(ctx: Pick<Ctx,
|
|
40
|
+
function optionalSecret(ctx: Pick<Ctx, 'secrets'>, name: string): string {
|
|
38
41
|
try {
|
|
39
|
-
return ((ctx.secrets as Record<string, string>)[name] ??
|
|
42
|
+
return ((ctx.secrets as Record<string, string>)[name] ?? '').trim();
|
|
40
43
|
} catch {
|
|
41
|
-
return
|
|
44
|
+
return '';
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/** Tarball URL of `repo` at `sha` (the API redirects to codeload.github.com). */
|
|
46
|
-
export const gitHubTarballUrl = (
|
|
47
|
-
|
|
49
|
+
export const gitHubTarballUrl = (
|
|
50
|
+
repo: string,
|
|
51
|
+
sha: string,
|
|
52
|
+
apiUrl = GITHUB_API_URL,
|
|
53
|
+
): string =>
|
|
54
|
+
`${apiUrl.replace(/\/+$/, '')}/repos/${repo}/tarball/${encodeURIComponent(sha)}`;
|
|
48
55
|
|
|
49
56
|
/**
|
|
50
57
|
* Downloads `repo` at `sha` from GitHub as a tarball and outputs it as a `Source` whose `tree` is the
|
|
@@ -57,44 +64,67 @@ export function gitHubSource<const N extends string, const T extends string = ne
|
|
|
57
64
|
const contract = step({
|
|
58
65
|
name: def.name,
|
|
59
66
|
description: def.description,
|
|
60
|
-
kind:
|
|
67
|
+
kind: 'gitHubSource',
|
|
61
68
|
image: GITHUB_SOURCE_IMAGE,
|
|
62
69
|
input,
|
|
63
70
|
output: Source,
|
|
64
71
|
secrets: def.token ? [def.token] : [],
|
|
65
72
|
timeoutSeconds: def.timeoutSeconds,
|
|
66
|
-
settings: {
|
|
73
|
+
settings: {
|
|
74
|
+
...(def.token ? { token: def.token.name } : {}),
|
|
75
|
+
...(def.apiUrl !== undefined ? { apiUrl: def.apiUrl } : {}),
|
|
76
|
+
},
|
|
67
77
|
});
|
|
68
78
|
return implement(contract, async ({ repo, sha }, ctx: Ctx) => {
|
|
69
|
-
const headers: Record<string, string> = {
|
|
70
|
-
|
|
79
|
+
const headers: Record<string, string> = {
|
|
80
|
+
accept: 'application/vnd.github+json',
|
|
81
|
+
'user-agent': 'olve-pipelines',
|
|
82
|
+
};
|
|
83
|
+
const token = def.token ? optionalSecret(ctx, def.token.name) : '';
|
|
71
84
|
if (token) headers.authorization = `Bearer ${token}`;
|
|
72
|
-
else if (def.token)
|
|
73
|
-
|
|
85
|
+
else if (def.token)
|
|
86
|
+
ctx.log(
|
|
87
|
+
`${def.token.name} is not set: fetching anonymously (public repositories only)`,
|
|
88
|
+
);
|
|
89
|
+
const url = gitHubTarballUrl(
|
|
90
|
+
repo,
|
|
91
|
+
sha,
|
|
92
|
+
resolveConfigRefs(def.apiUrl, ctx.config ?? {}) ?? GITHUB_API_URL,
|
|
93
|
+
);
|
|
74
94
|
mkdirSync(ctx.tmpDir, { recursive: true });
|
|
75
|
-
const dir = mkdtempSync(join(ctx.tmpDir,
|
|
76
|
-
const archive = join(dir,
|
|
95
|
+
const dir = mkdtempSync(join(ctx.tmpDir, 'github-source-'));
|
|
96
|
+
const archive = join(dir, 'repo.tar.gz');
|
|
77
97
|
|
|
78
|
-
ctx.log(
|
|
98
|
+
ctx.log('fetching source', { repo, sha });
|
|
79
99
|
// Retries network errors and 5xx; a 4xx (bad sha, no access) fails at once.
|
|
80
|
-
const status = await ctx.retry(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
const status = await ctx.retry(
|
|
101
|
+
{ attempts: 4, delaySeconds: 2, backoff: 2 },
|
|
102
|
+
async () => {
|
|
103
|
+
const res = await ctx.http.fetch(url, { headers });
|
|
104
|
+
if (res.status >= 500) throw new Error(`HTTP ${res.status} from ${url}`);
|
|
105
|
+
if (res.ok) await Bun.write(archive, res);
|
|
106
|
+
return res.status;
|
|
107
|
+
},
|
|
108
|
+
);
|
|
86
109
|
if (status < 200 || status >= 300) {
|
|
87
|
-
throw new Error(
|
|
110
|
+
throw new Error(
|
|
111
|
+
`cannot fetch ${repo}@${sha}: HTTP ${status} from ${url}${def.token ? '' : ' (no token configured: a private repository needs one)'}`,
|
|
112
|
+
);
|
|
88
113
|
}
|
|
89
114
|
|
|
90
|
-
const extracted = join(dir,
|
|
115
|
+
const extracted = join(dir, 'x');
|
|
91
116
|
mkdirSync(extracted);
|
|
92
117
|
await extractTar(archive, extracted);
|
|
93
118
|
// GitHub tarballs hold one top-level directory, plus a pax_global_header entry (the commit id)
|
|
94
119
|
// that some extractors materialise as a file.
|
|
95
|
-
const roots = readdirSync(extracted).filter(
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
const roots = readdirSync(extracted).filter(
|
|
121
|
+
(e) => e !== 'pax_global_header' && statSync(join(extracted, e)).isDirectory(),
|
|
122
|
+
);
|
|
123
|
+
if (roots.length !== 1)
|
|
124
|
+
throw new Error(
|
|
125
|
+
`unexpected tarball layout for ${repo}@${sha}: top-level entries ${JSON.stringify(readdirSync(extracted))}`,
|
|
126
|
+
);
|
|
127
|
+
const tree = await ctx.output.dir(join(extracted, roots[0]!), { name: 'tree' });
|
|
98
128
|
return { repo, sha, tree };
|
|
99
129
|
}) as never;
|
|
100
130
|
}
|