@avi2dg/checks 0.30.0 → 0.32.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/CHANGELOG.md +17 -0
- package/README.md +5 -2
- package/dist/readability/index.js +94 -1
- package/docs/configs/commit-messages.md +5 -0
- package/docs/configs/typescript-rules.md +10 -0
- package/docs/design.md +38 -4
- package/docs/gates/checks-commit-identity.md +3 -0
- package/docs/gates/checks-lint-coverage.md +9 -8
- package/docs/gates/checks-lint.md +3 -2
- package/docs/gates/checks-release-notes.md +10 -1
- package/docs/gates/checks-release-pr.md +185 -0
- package/docs/gates/checks-release-report.md +4 -2
- package/docs/gates/checks-release-tag.md +63 -0
- package/docs/gates/checks-secrets.md +93 -0
- package/docs/gates/checks-unused.md +7 -6
- package/oxlintrc.json +7 -0
- package/package.json +22 -3
- package/src/complexity/exports.ts +2 -1
- package/src/complexity/knip.ts +4 -5
- package/src/complexity/unused.ts +4 -3
- package/src/core/gates.ts +7 -4
- package/src/delivery/commit-identity.ts +18 -2
- package/src/delivery/github.ts +38 -0
- package/src/delivery/gitleaks.toml +113 -0
- package/src/delivery/gitleaks.ts +111 -0
- package/src/delivery/release-pr.ts +195 -0
- package/src/delivery/release-report.ts +3 -30
- package/src/delivery/release-tag.ts +64 -0
- package/src/delivery/release.ts +79 -0
- package/src/delivery/secrets.ts +86 -0
- package/src/dependencies/osv-scanner.ts +3 -45
- package/src/dependencies/pinned-binary.ts +79 -0
- package/src/quality/lint-coverage.sh +11 -6
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Effect, Option, Path, Schema } from "effect";
|
|
2
|
+
import { collect } from "../core/git.ts";
|
|
3
|
+
import { installPinned } from "../dependencies/pinned-binary.ts";
|
|
4
|
+
|
|
5
|
+
const GITLEAKS_VERSION = "8.30.1";
|
|
6
|
+
const RELEASES = `https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}`;
|
|
7
|
+
const MEMBER = "gitleaks";
|
|
8
|
+
|
|
9
|
+
type Build = { readonly archive: string; readonly sha256: string; readonly binarySha256: string };
|
|
10
|
+
|
|
11
|
+
// The archive's SHA-256 as gitleaks_<version>_checksums.txt of the release lists it, and the SHA-256 of the binary inside.
|
|
12
|
+
const BUILDS: Readonly<Record<string, Build>> = {
|
|
13
|
+
"darwin-arm64": {
|
|
14
|
+
archive: `gitleaks_${GITLEAKS_VERSION}_darwin_arm64.tar.gz`,
|
|
15
|
+
sha256: "b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5",
|
|
16
|
+
binarySha256: "ba52fb1bfabbcde42f032afad3d6e0b19dff8ed105229a16e7caa338bbc0e84f",
|
|
17
|
+
},
|
|
18
|
+
"darwin-x64": {
|
|
19
|
+
archive: `gitleaks_${GITLEAKS_VERSION}_darwin_x64.tar.gz`,
|
|
20
|
+
sha256: "dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709",
|
|
21
|
+
binarySha256: "cee01fea7173f1b779dff188e1c26ecbcb4027d394acc573b23aaf0be260e291",
|
|
22
|
+
},
|
|
23
|
+
"linux-arm64": {
|
|
24
|
+
archive: `gitleaks_${GITLEAKS_VERSION}_linux_arm64.tar.gz`,
|
|
25
|
+
sha256: "e4a487ee7ccd7d3a7f7ec08657610aa3606637dab924210b3aee62570fb4b080",
|
|
26
|
+
binarySha256: "00e91bbe655bd7c47753e8cfe61cb76ea1a5d7e7702fe161ee40102b46b3823b",
|
|
27
|
+
},
|
|
28
|
+
"linux-x64": {
|
|
29
|
+
archive: `gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz`,
|
|
30
|
+
sha256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb",
|
|
31
|
+
binarySha256: "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
class GitleaksError extends Schema.TaggedError<GitleaksError>()("GitleaksError", {
|
|
36
|
+
message: Schema.String,
|
|
37
|
+
}) {}
|
|
38
|
+
|
|
39
|
+
export function gitleaksBuildFor(platform: string, arch: string): Option.Option<Build> {
|
|
40
|
+
return Option.fromNullishOr(BUILDS[`${platform}-${arch}`]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const pinnedGitleaks = Effect.fn("pinnedGitleaks")(function* (cache: string) {
|
|
44
|
+
const build = gitleaksBuildFor(process.platform, process.arch);
|
|
45
|
+
if (Option.isNone(build)) {
|
|
46
|
+
return yield* new GitleaksError({ message: `no pinned gitleaks build runs on ${process.platform}-${process.arch}` });
|
|
47
|
+
}
|
|
48
|
+
const path = yield* Path.Path;
|
|
49
|
+
const { archive, sha256, binarySha256 } = build.value;
|
|
50
|
+
return yield* installPinned(
|
|
51
|
+
{ kind: "tar.gz", url: `${RELEASES}/${archive}`, sha256, member: MEMBER, memberSha256: binarySha256 },
|
|
52
|
+
path.join(cache, "gitleaks", GITLEAKS_VERSION, MEMBER),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const Leak = Schema.Struct({
|
|
57
|
+
RuleID: Schema.String,
|
|
58
|
+
Description: Schema.String,
|
|
59
|
+
File: Schema.String,
|
|
60
|
+
StartLine: Schema.Int,
|
|
61
|
+
Commit: Schema.String,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type Leak = typeof Leak.Type;
|
|
65
|
+
|
|
66
|
+
const decodeGitleaksReport = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(Leak)));
|
|
67
|
+
|
|
68
|
+
export type Scan = {
|
|
69
|
+
readonly binary: string;
|
|
70
|
+
readonly gitDir: string;
|
|
71
|
+
readonly logOptions: string;
|
|
72
|
+
readonly config: string;
|
|
73
|
+
readonly ignoreDir: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// gitleaks exits 1 both on a leak and on a fatal error, so it exits 0 on a leak here and the report alone tells them apart.
|
|
77
|
+
// It exits 0 with an empty report when git log fails, and logs only errors here, so any line it logs fails the scan.
|
|
78
|
+
// It reads a .gitleaksignore at its source whatever --gitleaks-ignore-path says, so the source is the git directory, where no tracked file lands.
|
|
79
|
+
export const scanCommits = Effect.fn("scanCommits")(function* ({ binary, gitDir, logOptions, config, ignoreDir }: Scan) {
|
|
80
|
+
const args = [
|
|
81
|
+
"git",
|
|
82
|
+
gitDir,
|
|
83
|
+
"--log-opts",
|
|
84
|
+
logOptions,
|
|
85
|
+
"--config",
|
|
86
|
+
config,
|
|
87
|
+
"--gitleaks-ignore-path",
|
|
88
|
+
ignoreDir,
|
|
89
|
+
"--ignore-gitleaks-allow",
|
|
90
|
+
"--redact",
|
|
91
|
+
"--no-banner",
|
|
92
|
+
"--no-color",
|
|
93
|
+
"--log-level",
|
|
94
|
+
"error",
|
|
95
|
+
"--exit-code",
|
|
96
|
+
"0",
|
|
97
|
+
"--report-format",
|
|
98
|
+
"json",
|
|
99
|
+
"--report-path",
|
|
100
|
+
"-",
|
|
101
|
+
];
|
|
102
|
+
const { stdout, stderr, exitCode } = yield* collect(binary, args, ignoreDir).pipe(
|
|
103
|
+
Effect.mapError((cause) => new GitleaksError({ message: `cannot run ${binary}: ${cause.message}` })),
|
|
104
|
+
);
|
|
105
|
+
if (exitCode !== 0 || stderr.trim() !== "") {
|
|
106
|
+
return yield* new GitleaksError({ message: `${binary} failed: ${stderr.trim() || `exit code ${exitCode}`}` });
|
|
107
|
+
}
|
|
108
|
+
return yield* decodeGitleaksReport(stdout).pipe(
|
|
109
|
+
Effect.mapError((cause) => new GitleaksError({ message: `cannot read the report gitleaks wrote: ${cause.message}` })),
|
|
110
|
+
);
|
|
111
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Console, Effect, Encoding, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { dispatch, gitHubJson, REPOSITORY } from "./github.ts";
|
|
4
|
+
import { nextVersion, type Pending, readPending, releaseTitle, runBuild, withVersion } from "./release.ts";
|
|
5
|
+
import { git } from "../core/git.ts";
|
|
6
|
+
import { runMain, Usage } from "../core/main.ts";
|
|
7
|
+
|
|
8
|
+
class ReleaseRefused extends Schema.TaggedError<ReleaseRefused>()("ReleaseRefused", {
|
|
9
|
+
message: Schema.String,
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
type Staged =
|
|
13
|
+
| { readonly kind: "written"; readonly path: string; readonly mode: string }
|
|
14
|
+
| { readonly kind: "deleted"; readonly path: string; readonly mode: string };
|
|
15
|
+
|
|
16
|
+
type Read =
|
|
17
|
+
| { readonly kind: "written"; readonly path: string; readonly mode: string; readonly content: string }
|
|
18
|
+
| { readonly kind: "deleted"; readonly path: string; readonly mode: string };
|
|
19
|
+
|
|
20
|
+
type Built = {
|
|
21
|
+
readonly tree: string;
|
|
22
|
+
readonly files: readonly Read[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type Remote = {
|
|
26
|
+
readonly parents: readonly string[];
|
|
27
|
+
readonly version: string | undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const NAME = "checks-release-pr";
|
|
31
|
+
const USAGE = "usage: release-pr.ts <workflow>...";
|
|
32
|
+
const MANIFEST = "package.json";
|
|
33
|
+
const REGULAR_FILE_MODES = new Set(["100644", "100755"]);
|
|
34
|
+
const RAW_ENTRY = /^:(\d{6}) (\d{6}) [0-9a-f]+ [0-9a-f]+ ([A-Z])/;
|
|
35
|
+
|
|
36
|
+
const decodeVersion = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ version: Schema.String })));
|
|
37
|
+
const decodeSha = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ sha: Schema.String })));
|
|
38
|
+
const decodeRef = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ ref: Schema.String })));
|
|
39
|
+
const PullRequest = Schema.Struct({ number: Schema.Int, title: Schema.String, html_url: Schema.String });
|
|
40
|
+
type PullRequest = typeof PullRequest.Type;
|
|
41
|
+
const decodePullRequest = Schema.decodeUnknownEffect(Schema.fromJsonString(PullRequest));
|
|
42
|
+
const decodePullRequests = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(PullRequest)));
|
|
43
|
+
|
|
44
|
+
export function releaseBranchOf(base: string): string {
|
|
45
|
+
return `release/${base}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function stagedOf(raw: string): readonly Staged[] {
|
|
49
|
+
const fields = raw.split("\0");
|
|
50
|
+
const staged: Staged[] = [];
|
|
51
|
+
for (let index = 0; index + 1 < fields.length; index += 2) {
|
|
52
|
+
const [, from = "", to = "", status] = RAW_ENTRY.exec(fields[index] ?? "") ?? [];
|
|
53
|
+
const path = fields[index + 1] ?? "";
|
|
54
|
+
staged.push(status === "D" ? { kind: "deleted", path, mode: from } : { kind: "written", path, mode: to });
|
|
55
|
+
}
|
|
56
|
+
return staged;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const refused = (message: string) => new ReleaseRefused({ message });
|
|
60
|
+
|
|
61
|
+
const versionIn = (manifest: string) =>
|
|
62
|
+
decodeVersion(manifest).pipe(
|
|
63
|
+
Effect.map(({ version }) => version),
|
|
64
|
+
Effect.orElseSucceed(() => undefined),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const releaseVersion = Effect.fn("releaseVersion")(function* (manifest: string, { since, unreleased }: Pending) {
|
|
68
|
+
const { version } = yield* decodeVersion(manifest).pipe(Effect.mapError((cause) => refused(`${MANIFEST}: ${cause.message}`)));
|
|
69
|
+
if (since !== undefined && since !== `v${version}`) {
|
|
70
|
+
return yield* refused(
|
|
71
|
+
`${MANIFEST} holds ${version}, but the last release tag is ${since}, so v${version} is untagged; rerun the tag job of daily-release on the release commit, whose refusal says what to fix, or return its version to ${since.slice(1)}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const next = nextVersion(version, unreleased);
|
|
75
|
+
if (next === undefined) return yield* refused(`${MANIFEST} holds ${version}, which is no plain major.minor.patch version`);
|
|
76
|
+
const bumped = withVersion(manifest, version, next);
|
|
77
|
+
if (bumped === undefined || (yield* versionIn(bumped)) !== next) {
|
|
78
|
+
return yield* refused(`cannot rewrite the version ${version} in ${MANIFEST} as ${next}`);
|
|
79
|
+
}
|
|
80
|
+
return { next, bumped };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const currentBranch = Effect.fn("currentBranch")(function* (root: string) {
|
|
84
|
+
const branch = yield* git(["symbolic-ref", "--quiet", "--short", "HEAD"], root).pipe(
|
|
85
|
+
Effect.map((name) => name.trim()),
|
|
86
|
+
Effect.catchTag("GitFailure", () => Effect.succeed("")),
|
|
87
|
+
);
|
|
88
|
+
if (branch === "") return yield* refused("HEAD is detached; check out the branch the release goes to");
|
|
89
|
+
return branch;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const readRemote = Effect.fn("readRemote")(function* (root: string, branch: string) {
|
|
93
|
+
if ((yield* git(["ls-remote", "origin", `refs/heads/${branch}`], root)).trim() === "") return undefined;
|
|
94
|
+
const tracking = `refs/remotes/origin/${branch}`;
|
|
95
|
+
yield* git(["fetch", "--quiet", "--no-tags", "origin", `+refs/heads/${branch}:${tracking}`], root);
|
|
96
|
+
const [, ...parents] = (yield* git(["rev-list", "--parents", "-n", "1", tracking], root)).trim().split(" ");
|
|
97
|
+
const version = yield* git(["show", `${tracking}:${MANIFEST}`], root).pipe(
|
|
98
|
+
Effect.flatMap(versionIn),
|
|
99
|
+
Effect.orElseSucceed(() => undefined),
|
|
100
|
+
);
|
|
101
|
+
return { parents, version } satisfies Remote;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const readStaged = Effect.fn("readStaged")(function* (root: string, staged: Staged) {
|
|
105
|
+
if (staged.kind === "deleted") return staged;
|
|
106
|
+
if (!REGULAR_FILE_MODES.has(staged.mode)) return yield* refused(`the build wrote ${staged.path} with mode ${staged.mode}, which is no regular file`);
|
|
107
|
+
const bytes = yield* (yield* FileSystem.FileSystem).readFile((yield* Path.Path).join(root, staged.path));
|
|
108
|
+
return { ...staged, content: Encoding.encodeBase64(bytes) };
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// The build writes into the working tree, so the tree it starts from has to hold nothing a release would sweep in.
|
|
112
|
+
const buildRelease = Effect.fn("buildRelease")(function* (root: string, bumped: string) {
|
|
113
|
+
if ((yield* git(["status", "--porcelain", "--untracked-files=no"], root)).trim() !== "") {
|
|
114
|
+
return yield* refused("the working tree has changes to tracked files; the release commits only what the build writes");
|
|
115
|
+
}
|
|
116
|
+
const restore = git(["reset", "--quiet", "--hard", "HEAD"], root).pipe(
|
|
117
|
+
Effect.catchTag("GitFailure", (failure) => Console.error(`${NAME}: cannot restore the working tree: ${failure.message}`)),
|
|
118
|
+
);
|
|
119
|
+
return yield* Effect.gen(function* () {
|
|
120
|
+
yield* (yield* FileSystem.FileSystem).writeFileString((yield* Path.Path).join(root, MANIFEST), bumped);
|
|
121
|
+
yield* runBuild(root);
|
|
122
|
+
yield* git(["add", "--update"], root);
|
|
123
|
+
const tree = (yield* git(["write-tree"], root)).trim();
|
|
124
|
+
const staged = stagedOf(yield* git(["diff", "--cached", "--raw", "-z", "--no-renames", "HEAD"], root));
|
|
125
|
+
return { tree, files: yield* Effect.forEach(staged, (one) => readStaged(root, one)) } satisfies Built;
|
|
126
|
+
}).pipe(Effect.ensuring(restore));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const treeEntry = Effect.fn("treeEntry")(function* (file: Read) {
|
|
130
|
+
if (file.kind === "deleted") return { path: file.path, mode: file.mode, type: "blob", sha: null };
|
|
131
|
+
const { sha } = yield* gitHubJson(decodeSha, "POST", `${REPOSITORY}/git/blobs`, { content: file.content, encoding: "base64" });
|
|
132
|
+
return { path: file.path, mode: file.mode, type: "blob", sha };
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const commitRelease = Effect.fn("commitRelease")(function* (root: string, head: string, built: Built, version: string) {
|
|
136
|
+
const tree = yield* Effect.forEach(built.files, treeEntry);
|
|
137
|
+
const baseTree = (yield* git(["rev-parse", `${head}^{tree}`], root)).trim();
|
|
138
|
+
const made = yield* gitHubJson(decodeSha, "POST", `${REPOSITORY}/git/trees`, { base_tree: baseTree, tree });
|
|
139
|
+
if (made.sha !== built.tree) return yield* refused(`GitHub built the tree ${made.sha}, but the build wrote ${built.tree}`);
|
|
140
|
+
const commit = { message: releaseTitle(version), tree: made.sha, parents: [head] };
|
|
141
|
+
return (yield* gitHubJson(decodeSha, "POST", `${REPOSITORY}/git/commits`, commit)).sha;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const pointBranch = Effect.fn("pointBranch")(function* (branch: string, sha: string, exists: boolean) {
|
|
145
|
+
yield* exists
|
|
146
|
+
? gitHubJson(decodeRef, "PATCH", `${REPOSITORY}/git/refs/heads/${branch}`, { sha, force: true })
|
|
147
|
+
: gitHubJson(decodeRef, "POST", `${REPOSITORY}/git/refs`, { ref: `refs/heads/${branch}`, sha });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const findOpen = Effect.fn("findOpen")(function* (base: string, branch: string) {
|
|
151
|
+
const query = `head={owner}:${encodeURIComponent(branch)}&base=${encodeURIComponent(base)}&state=open`;
|
|
152
|
+
const [open] = yield* gitHubJson(decodePullRequests, "GET", `${REPOSITORY}/pulls?${query}`);
|
|
153
|
+
return open;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const titlePullRequest = Effect.fn("titlePullRequest")(function* (open: PullRequest | undefined, base: string, branch: string, version: string) {
|
|
157
|
+
const title = releaseTitle(version);
|
|
158
|
+
const body = `Release ${version}.`;
|
|
159
|
+
if (open === undefined) {
|
|
160
|
+
const created = yield* gitHubJson(decodePullRequest, "POST", `${REPOSITORY}/pulls`, { title, head: branch, base, body });
|
|
161
|
+
return { verb: "opened", url: created.html_url };
|
|
162
|
+
}
|
|
163
|
+
if (open.title !== title) yield* gitHubJson(decodePullRequest, "PATCH", `${REPOSITORY}/pulls/${open.number}`, { title, body });
|
|
164
|
+
return { verb: "refreshed", url: open.html_url };
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const releasePullRequest = Effect.gen(function* () {
|
|
168
|
+
const workflows = process.argv.slice(2);
|
|
169
|
+
if (workflows.length === 0) return yield* new Usage({ message: USAGE });
|
|
170
|
+
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
171
|
+
const pending = yield* readPending(root);
|
|
172
|
+
if (pending.unreleased.length === 0) {
|
|
173
|
+
yield* Console.log(`release-pr: no unreleased changes ${pending.since === undefined ? "with no tag yet" : `since ${pending.since}`}`);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
const base = yield* currentBranch(root);
|
|
177
|
+
const branch = releaseBranchOf(base);
|
|
178
|
+
const head = (yield* git(["rev-parse", "HEAD"], root)).trim();
|
|
179
|
+
const manifest = yield* (yield* FileSystem.FileSystem).readFileString((yield* Path.Path).join(root, MANIFEST));
|
|
180
|
+
const { next, bumped } = yield* releaseVersion(manifest, pending);
|
|
181
|
+
const remote = yield* readRemote(root, branch);
|
|
182
|
+
const current = remote?.version === next && remote.parents.length === 1 && remote.parents[0] === head;
|
|
183
|
+
const open = yield* findOpen(base, branch);
|
|
184
|
+
if (current && open?.title === releaseTitle(next)) {
|
|
185
|
+
yield* Console.log(`release-pr: ${open.html_url} releases ${next} from ${head.slice(0, 12)} and is current`);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (!current) yield* pointBranch(branch, yield* commitRelease(root, head, yield* buildRelease(root, bumped), next), remote !== undefined);
|
|
189
|
+
const { verb, url } = yield* titlePullRequest(open, base, branch, next);
|
|
190
|
+
yield* Effect.forEach(workflows, (workflow) => dispatch(workflow, branch));
|
|
191
|
+
yield* Console.log(`release-pr: ${verb} ${url} to release ${next}, and dispatched ${workflows.join(", ")} on ${branch}`);
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (import.meta.main) runMain(NAME, releasePullRequest);
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { Console, Effect
|
|
3
|
-
import {
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
import { readPending } from "./release.ts";
|
|
4
4
|
import { git } from "../core/git.ts";
|
|
5
5
|
import { runMain, Usage } from "../core/main.ts";
|
|
6
6
|
|
|
7
|
-
export class ReleaseReportUnreadable extends Schema.TaggedError<ReleaseReportUnreadable>()("ReleaseReportUnreadable", {
|
|
8
|
-
message: Schema.String,
|
|
9
|
-
}) {}
|
|
10
|
-
|
|
11
7
|
const USAGE = "usage: release-report.ts";
|
|
12
|
-
const RELEASE_TAG = "v[0-9]*.[0-9]*.[0-9]*";
|
|
13
|
-
|
|
14
|
-
export function unreleasedOf(subjects: readonly string[]): readonly string[] {
|
|
15
|
-
return subjects.filter((subject) => groupOf(subject) !== undefined);
|
|
16
|
-
}
|
|
17
8
|
|
|
18
9
|
export function formatReport(unreleased: readonly string[], since: string | undefined): string {
|
|
19
10
|
const where = since === undefined ? "with no tag yet" : `since ${since}`;
|
|
@@ -21,28 +12,10 @@ export function formatReport(unreleased: readonly string[], since: string | unde
|
|
|
21
12
|
return [`release-report: ${unreleased.length} unreleased change(s) ${where}:`, ...unreleased.map((subject) => ` ${subject}`)].join("\n");
|
|
22
13
|
}
|
|
23
14
|
|
|
24
|
-
const lastTag = Effect.fn("lastTag")(function* (root: string) {
|
|
25
|
-
const described = yield* git(["describe", "--tags", "--abbrev=0", `--match=${RELEASE_TAG}`, "HEAD"], root).pipe(
|
|
26
|
-
Effect.map((tag) => tag.trim()),
|
|
27
|
-
Effect.catchTag("GitFailure", () => Effect.succeed("")),
|
|
28
|
-
);
|
|
29
|
-
return described === "" ? undefined : described;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const subjectsSince = Effect.fn("subjectsSince")(function* (root: string, since: string | undefined) {
|
|
33
|
-
const range = since === undefined ? "HEAD" : `${since}..HEAD`;
|
|
34
|
-
const log = yield* git(["log", "--topo-order", "--no-merges", "--format=%s", range], root);
|
|
35
|
-
return log.split("\n").filter((subject) => subject !== "");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
15
|
const report = Effect.gen(function* () {
|
|
39
16
|
if (process.argv.slice(2).length > 0) return yield* new Usage({ message: USAGE });
|
|
40
17
|
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
41
|
-
|
|
42
|
-
return yield* new ReleaseReportUnreadable({ message: "the checkout is shallow, so the tag it sees may not be the last one; fetch all of it" });
|
|
43
|
-
}
|
|
44
|
-
const since = yield* lastTag(root);
|
|
45
|
-
const unreleased = unreleasedOf(yield* subjectsSince(root, since));
|
|
18
|
+
const { since, unreleased } = yield* readPending(root);
|
|
46
19
|
yield* Console.log(formatReport(unreleased, since));
|
|
47
20
|
return unreleased.length === 0;
|
|
48
21
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { dispatch, GitHubFailure } from "./github.ts";
|
|
4
|
+
import { lastTag, releasedVersionOf, runBuild } from "./release.ts";
|
|
5
|
+
import { git } from "../core/git.ts";
|
|
6
|
+
import { runMain, Usage } from "../core/main.ts";
|
|
7
|
+
|
|
8
|
+
class TagRefused extends Schema.TaggedError<TagRefused>()("TagRefused", {
|
|
9
|
+
message: Schema.String,
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
const NAME = "checks-release-tag";
|
|
13
|
+
const USAGE = "usage: release-tag.ts <workflow>";
|
|
14
|
+
const MANIFEST = "package.json";
|
|
15
|
+
|
|
16
|
+
const decodeVersion = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ version: Schema.String })));
|
|
17
|
+
|
|
18
|
+
// An annotated tag lists its own object first and the commit it tags on a `^{}` line after it.
|
|
19
|
+
export function taggedCommitOf(listed: string): string | undefined {
|
|
20
|
+
const lines = listed.split("\n").filter((line) => line !== "");
|
|
21
|
+
const peeled = lines.find((line) => line.endsWith("^{}")) ?? lines[0];
|
|
22
|
+
return peeled?.split("\t")[0];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const releaseTag = Effect.gen(function* () {
|
|
26
|
+
const [workflow, ...extra] = process.argv.slice(2);
|
|
27
|
+
if (workflow === undefined || extra.length > 0) return yield* new Usage({ message: USAGE });
|
|
28
|
+
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
29
|
+
const [head = "", subject = ""] = (yield* git(["log", "-1", "--format=%H%n%s", "HEAD"], root)).split("\n");
|
|
30
|
+
const version = releasedVersionOf(subject);
|
|
31
|
+
if (version === undefined) {
|
|
32
|
+
yield* Console.log(`release-tag: ${head.slice(0, 12)} is no release commit`);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const manifest = yield* (yield* FileSystem.FileSystem).readFileString((yield* Path.Path).join(root, MANIFEST));
|
|
36
|
+
const { version: held } = yield* decodeVersion(manifest).pipe(Effect.mapError((cause) => new TagRefused({ message: `${MANIFEST}: ${cause.message}` })));
|
|
37
|
+
if (held !== version) return yield* new TagRefused({ message: `${head.slice(0, 12)} releases ${version}, but ${MANIFEST} holds ${held}` });
|
|
38
|
+
const tag = `v${version}`;
|
|
39
|
+
const tagged = taggedCommitOf(yield* git(["ls-remote", "--tags", "origin", `refs/tags/${tag}`, `refs/tags/${tag}^{}`], root));
|
|
40
|
+
if (tagged === head) {
|
|
41
|
+
yield* Console.log(`release-tag: ${tag} already tags ${head.slice(0, 12)}`);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (tagged !== undefined) return yield* new TagRefused({ message: `${tag} already tags ${tagged.slice(0, 12)}, not ${head.slice(0, 12)}` });
|
|
45
|
+
yield* runBuild(root);
|
|
46
|
+
const rebuilt = (yield* git(["diff", "--name-only"], root)).trim().split("\n").filter((file) => file !== "");
|
|
47
|
+
if (rebuilt.length > 0) {
|
|
48
|
+
const since = yield* lastTag(root);
|
|
49
|
+
const returned = since === undefined ? "the version it held before this release" : since.slice(1);
|
|
50
|
+
return yield* new TagRefused({
|
|
51
|
+
message: `the build rewrites ${rebuilt.join(", ")} at ${head.slice(0, 12)}, which the release workflow's build check refuses; open a \`chore: cancel the unpublished ${version}\` pull request that returns ${MANIFEST} to ${returned} and commits what \`bun run build\` then writes to CHANGELOG.md, and the next daily-release run cuts the release again with every change since ${since ?? "the first commit"}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
yield* git(["push", "--quiet", "origin", `${head}:refs/tags/${tag}`], root);
|
|
55
|
+
yield* dispatch(workflow, tag).pipe(
|
|
56
|
+
Effect.mapError(
|
|
57
|
+
(failure) => new GitHubFailure({ message: `${failure.message}; ${tag} is pushed, so dispatch the release by hand with \`gh workflow run ${workflow} --ref ${tag}\`` }),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
yield* Console.log(`release-tag: tagged ${head.slice(0, 12)} as ${tag}, and dispatched ${workflow} on it`);
|
|
61
|
+
return true;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (import.meta.main) runMain(NAME, releaseTag);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
3
|
+
import { groupOf } from "./changelog.ts";
|
|
4
|
+
import { git } from "../core/git.ts";
|
|
5
|
+
|
|
6
|
+
class ReleaseUnreadable extends Schema.TaggedError<ReleaseUnreadable>()("ReleaseUnreadable", {
|
|
7
|
+
message: Schema.String,
|
|
8
|
+
}) {}
|
|
9
|
+
|
|
10
|
+
class BuildFailed extends Schema.TaggedError<BuildFailed>()("BuildFailed", {
|
|
11
|
+
message: Schema.String,
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
export type Pending = {
|
|
15
|
+
readonly since: string | undefined;
|
|
16
|
+
readonly unreleased: readonly string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const RELEASE_TAG = "v[0-9]*.[0-9]*.[0-9]*";
|
|
20
|
+
const PLAIN_VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
21
|
+
const RELEASE_SUBJECT = /^chore: release (\d+\.\d+\.\d+)(?: \(#\d+\))?$/;
|
|
22
|
+
|
|
23
|
+
export function unreleasedOf(subjects: readonly string[]): readonly string[] {
|
|
24
|
+
return subjects.filter((subject) => groupOf(subject) !== undefined);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Under 0.x a breaking change moves the minor, as semver leaves a 0.x version free to break.
|
|
28
|
+
export function nextVersion(version: string, unreleased: readonly string[]): string | undefined {
|
|
29
|
+
const [, major, minor, patch] = PLAIN_VERSION.exec(version)?.map(Number) ?? [];
|
|
30
|
+
if (major === undefined || minor === undefined || patch === undefined) return undefined;
|
|
31
|
+
const groups = new Set(unreleased.map(groupOf));
|
|
32
|
+
if (groups.has("Breaking changes") && major > 0) return `${major + 1}.0.0`;
|
|
33
|
+
if (groups.has("Breaking changes") || groups.has("Features")) return `${major}.${minor + 1}.0`;
|
|
34
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Rewrites only the version's own text, so the rest of the manifest keeps the formatting its repository gave it.
|
|
38
|
+
export function withVersion(manifest: string, from: string, to: string): string | undefined {
|
|
39
|
+
const field = new RegExp(`("version"\\s*:\\s*")${from.replaceAll(".", "\\.")}(")`);
|
|
40
|
+
return field.test(manifest) ? manifest.replace(field, `$1${to}$2`) : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function releaseTitle(version: string): string {
|
|
44
|
+
return `chore: release ${version}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function releasedVersionOf(subject: string): string | undefined {
|
|
48
|
+
return RELEASE_SUBJECT.exec(subject)?.[1];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const runBuild = Effect.fn("runBuild")(function* (root: string) {
|
|
52
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
53
|
+
const exitCode = yield* spawner.exitCode(
|
|
54
|
+
ChildProcess.make(process.execPath, ["run", "build"], { cwd: root, stdin: "ignore", stdout: "inherit", stderr: "inherit" }),
|
|
55
|
+
);
|
|
56
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) return yield* new BuildFailed({ message: `bun run build exited ${exitCode}` });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const lastTag = Effect.fn("lastTag")(function* (root: string) {
|
|
60
|
+
const described = yield* git(["describe", "--tags", "--abbrev=0", `--match=${RELEASE_TAG}`, "HEAD"], root).pipe(
|
|
61
|
+
Effect.map((tag) => tag.trim()),
|
|
62
|
+
Effect.catchTag("GitFailure", () => Effect.succeed("")),
|
|
63
|
+
);
|
|
64
|
+
return described === "" ? undefined : described;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const subjectsSince = Effect.fn("subjectsSince")(function* (root: string, since: string | undefined) {
|
|
68
|
+
const range = since === undefined ? "HEAD" : `${since}..HEAD`;
|
|
69
|
+
const log = yield* git(["log", "--topo-order", "--no-merges", "--format=%s", range], root);
|
|
70
|
+
return log.split("\n").filter((subject) => subject !== "");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const readPending = Effect.fn("readPending")(function* (root: string) {
|
|
74
|
+
if ((yield* git(["rev-parse", "--is-shallow-repository"], root)).trim() === "true") {
|
|
75
|
+
return yield* new ReleaseUnreadable({ message: "the checkout is shallow, so the tag it sees may not be the last one; fetch all of it" });
|
|
76
|
+
}
|
|
77
|
+
const since = yield* lastTag(root);
|
|
78
|
+
return { since, unreleased: unreleasedOf(yield* subjectsSince(root, since)) } satisfies Pending;
|
|
79
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { commitOf, git, refArgs } from "../core/git.ts";
|
|
4
|
+
import { runMain } from "../core/main.ts";
|
|
5
|
+
import { cacheRoot } from "../dependencies/cache-root.ts";
|
|
6
|
+
import { pinnedGitleaks, scanCommits, type Leak } from "./gitleaks.ts";
|
|
7
|
+
|
|
8
|
+
const NAME = "secrets";
|
|
9
|
+
const USAGE = "usage: secrets.ts <ref> | <base-ref> <head-ref>";
|
|
10
|
+
const SHORT_SHA = 8;
|
|
11
|
+
const NEW_SIDE_PREFIX = "b/";
|
|
12
|
+
const OCTOPUS_PARENTS = 3;
|
|
13
|
+
|
|
14
|
+
class OctopusMerge extends Schema.TaggedError<OctopusMerge>()("OctopusMerge", {
|
|
15
|
+
message: Schema.String,
|
|
16
|
+
}) {}
|
|
17
|
+
|
|
18
|
+
// git reads a lone commit as its whole ancestry, and -1 limits only what a walk prints, so a single ref is bounded with --no-walk.
|
|
19
|
+
const revisionsOf = Effect.fn("revisionsOf")(function* (args: readonly string[], root: string) {
|
|
20
|
+
const { first, second } = yield* refArgs(args, USAGE);
|
|
21
|
+
if (second === undefined) return ["--no-walk", yield* commitOf(first, root)];
|
|
22
|
+
const head = yield* commitOf(second, root);
|
|
23
|
+
const base = (yield* git(["merge-base", yield* commitOf(first, root), head], root)).trim();
|
|
24
|
+
return [`${base}..${head}`];
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Without --remerge-diff git log prints no diff for a merge, and a first-parent diff would rescan what the merge brings in.
|
|
28
|
+
// git skips --remerge-diff for an octopus merge with only a warning, so the scan would pass a commit it never read.
|
|
29
|
+
const logOptionsOf = Effect.fn("logOptionsOf")(function* (args: readonly string[], root: string) {
|
|
30
|
+
const revisions = yield* revisionsOf(args, root);
|
|
31
|
+
const [octopus] = (yield* git(["rev-list", `--min-parents=${OCTOPUS_PARENTS}`, ...revisions], root)).split("\n");
|
|
32
|
+
if (octopus !== undefined && octopus !== "") {
|
|
33
|
+
return yield* new OctopusMerge({
|
|
34
|
+
message: `git gives no resolution diff for the octopus merge ${octopus}, so the gate cannot scan it`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return ["--remerge-diff", ...revisions].join(" ");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// gitleaks names a file under a remerge conflict header by its `+++ b/` line, so a path the commit does not hold carries that prefix.
|
|
41
|
+
const atRepositoryPath = Effect.fn("atRepositoryPath")(function* (leak: Leak, root: string) {
|
|
42
|
+
if (!leak.File.startsWith(NEW_SIDE_PREFIX)) return leak;
|
|
43
|
+
const held = yield* git(["cat-file", "-e", `${leak.Commit}:${leak.File}`], root).pipe(
|
|
44
|
+
Effect.as(true),
|
|
45
|
+
Effect.catchTag("GitFailure", () => Effect.succeed(false)),
|
|
46
|
+
);
|
|
47
|
+
return held ? leak : { ...leak, File: leak.File.slice(NEW_SIDE_PREFIX.length) };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function leakLine({ File, StartLine, RuleID, Commit, Description }: Leak): string {
|
|
51
|
+
return ` ${File}:${StartLine} ${RuleID} in ${Commit.slice(0, SHORT_SHA)}: ${Description}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function byPlace(left: Leak, right: Leak): number {
|
|
55
|
+
return (
|
|
56
|
+
left.File.localeCompare(right.File) ||
|
|
57
|
+
left.StartLine - right.StartLine ||
|
|
58
|
+
left.RuleID.localeCompare(right.RuleID) ||
|
|
59
|
+
left.Commit.localeCompare(right.Commit)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function report(leaks: readonly Leak[]): string {
|
|
64
|
+
if (leaks.length === 0) return `${NAME}: the range adds no secret`;
|
|
65
|
+
return [
|
|
66
|
+
`${NAME}: the range adds ${leaks.length} secret(s); put a placeholder such as <private-key> in its place in the commit that added it, and rotate any secret that left this machine:`,
|
|
67
|
+
...leaks.toSorted(byPlace).map(leakLine),
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const secrets = Effect.gen(function* () {
|
|
72
|
+
const fs = yield* FileSystem.FileSystem;
|
|
73
|
+
const path = yield* Path.Path;
|
|
74
|
+
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
75
|
+
const logOptions = yield* logOptionsOf(process.argv.slice(2), root);
|
|
76
|
+
const binary = yield* pinnedGitleaks(yield* cacheRoot());
|
|
77
|
+
const config = path.join(import.meta.dir, "gitleaks.toml");
|
|
78
|
+
const ignoreDir = yield* fs.makeTempDirectoryScoped({ prefix: "checks-secrets-" });
|
|
79
|
+
const gitDir = (yield* git(["rev-parse", "--absolute-git-dir"], root)).trim();
|
|
80
|
+
const reported = yield* scanCommits({ binary, gitDir, logOptions, config, ignoreDir });
|
|
81
|
+
const leaks = yield* Effect.forEach(reported, (leak) => atRepositoryPath(leak, root));
|
|
82
|
+
yield* Console.log(report(leaks));
|
|
83
|
+
return leaks.length === 0;
|
|
84
|
+
}).pipe(Effect.scoped);
|
|
85
|
+
|
|
86
|
+
if (import.meta.main) runMain(NAME, secrets);
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { Clock, Context,
|
|
2
|
-
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
|
|
3
|
-
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
1
|
+
import { Clock, Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
|
|
4
2
|
import { collect } from "../core/git.ts";
|
|
3
|
+
import { installPinned } from "./pinned-binary.ts";
|
|
5
4
|
|
|
6
5
|
export const OSV_SCANNER_VERSION = "2.6.0";
|
|
7
6
|
const RELEASES = `https://github.com/google/osv-scanner/releases/download/v${OSV_SCANNER_VERSION}`;
|
|
8
|
-
const EXECUTABLE_MODE = 0o755;
|
|
9
7
|
const HOUR_MS = 3_600_000;
|
|
10
8
|
const DAY_MS = 24 * HOUR_MS;
|
|
11
9
|
export const REFRESH_HOURS = 24;
|
|
@@ -33,46 +31,6 @@ export function buildFor(platform: string, arch: string): Option.Option<Build> {
|
|
|
33
31
|
return Option.fromNullishOr(BUILDS[`${platform}-${arch}`]);
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
const sha256Of = Effect.fn("sha256Of")(function* (bytes: Uint8Array) {
|
|
37
|
-
const crypto = yield* Crypto.Crypto;
|
|
38
|
-
return Encoding.encodeHex(yield* crypto.digest("SHA-256", bytes));
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const verified = Effect.fn("verified")(function* (binary: string, sha256: string) {
|
|
42
|
-
const fs = yield* FileSystem.FileSystem;
|
|
43
|
-
const found = yield* sha256Of(yield* fs.readFile(binary));
|
|
44
|
-
if (found !== sha256) {
|
|
45
|
-
return yield* new OsvScannerError({ message: `${binary} has SHA-256 ${found}, not the pinned ${sha256}; delete it and rerun` });
|
|
46
|
-
}
|
|
47
|
-
return binary;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const download = Effect.fn("download")(function* (url: string) {
|
|
51
|
-
const response = yield* HttpClient.get(url).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk));
|
|
52
|
-
return new Uint8Array(yield* response.arrayBuffer);
|
|
53
|
-
}, Effect.provide(FetchHttpClient.layer));
|
|
54
|
-
|
|
55
|
-
// The binary lands through a rename in its own directory, so a concurrent run sees it whole or not at all.
|
|
56
|
-
export const installPinned = Effect.fn("installPinned")(function* (url: string, sha256: string, binary: string) {
|
|
57
|
-
const fs = yield* FileSystem.FileSystem;
|
|
58
|
-
const path = yield* Path.Path;
|
|
59
|
-
if (yield* fs.exists(binary)) return yield* verified(binary, sha256);
|
|
60
|
-
const bytes = yield* download(url).pipe(
|
|
61
|
-
Effect.mapError((cause) => new OsvScannerError({ message: `cannot download ${url}: ${cause.message}` })),
|
|
62
|
-
);
|
|
63
|
-
const found = yield* sha256Of(bytes);
|
|
64
|
-
if (found !== sha256) {
|
|
65
|
-
return yield* new OsvScannerError({ message: `${url} has SHA-256 ${found}, not the pinned ${sha256}, so nothing was installed` });
|
|
66
|
-
}
|
|
67
|
-
yield* fs.makeDirectory(path.dirname(binary), { recursive: true });
|
|
68
|
-
const staging = yield* fs.makeTempDirectoryScoped({ directory: path.dirname(binary), prefix: `.${path.basename(binary)}-` });
|
|
69
|
-
const staged = path.join(staging, path.basename(binary));
|
|
70
|
-
yield* fs.writeFile(staged, bytes);
|
|
71
|
-
yield* fs.chmod(staged, EXECUTABLE_MODE);
|
|
72
|
-
yield* fs.rename(staged, binary);
|
|
73
|
-
return binary;
|
|
74
|
-
}, Effect.scoped);
|
|
75
|
-
|
|
76
34
|
const pinnedBinary = Effect.fn("pinnedBinary")(function* (cache: string) {
|
|
77
35
|
const build = buildFor(process.platform, process.arch);
|
|
78
36
|
if (Option.isNone(build)) {
|
|
@@ -80,7 +38,7 @@ const pinnedBinary = Effect.fn("pinnedBinary")(function* (cache: string) {
|
|
|
80
38
|
}
|
|
81
39
|
const path = yield* Path.Path;
|
|
82
40
|
const { asset, sha256 } = build.value;
|
|
83
|
-
return yield* installPinned(`${RELEASES}/${asset}`, sha256, path.join(cache, "osv-scanner", OSV_SCANNER_VERSION, asset));
|
|
41
|
+
return yield* installPinned({ kind: "binary", url: `${RELEASES}/${asset}`, sha256 }, path.join(cache, "osv-scanner", OSV_SCANNER_VERSION, asset));
|
|
84
42
|
});
|
|
85
43
|
|
|
86
44
|
type PinnedBinary = ReturnType<typeof pinnedBinary>;
|