Initial version
This commit is contained in:
18
src/argv.ts
Normal file
18
src/argv.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as S from "@effect/schema/Schema"
|
||||
import { Opaque } from "type-fest"
|
||||
|
||||
|
||||
export type BunPath = Opaque<string, "BunPath">
|
||||
export type MainPath = Opaque<string, "MainPath">
|
||||
export type GitPath = Opaque<string, "RepoPath">
|
||||
|
||||
export const ArgvSchema = S.tuple(
|
||||
S.literal<[BunPath]>(Bun.which("bun") as BunPath),
|
||||
S.literal<[MainPath]>(Bun.main as MainPath),
|
||||
S.string as unknown as S.Schema<GitPath, GitPath>,
|
||||
)
|
||||
|
||||
export type Argv = S.To<typeof ArgvSchema>
|
||||
|
||||
export const parseArgv = () =>
|
||||
S.parse(ArgvSchema)(process.argv)
|
||||
16
src/env.ts
Normal file
16
src/env.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as S from "@effect/schema/Schema"
|
||||
|
||||
|
||||
export const EnvSchema = S.struct({
|
||||
NODE_ENV: S.union(
|
||||
S.literal("development"),
|
||||
S.literal("production"),
|
||||
),
|
||||
|
||||
// PLUGIN_CALVER_FORMAT: S.optional(S.string),
|
||||
})
|
||||
|
||||
export type Env = S.To<typeof EnvSchema>
|
||||
|
||||
export const parseEnv = () =>
|
||||
S.parse(EnvSchema)(process.env)
|
||||
104
src/git.ts
Normal file
104
src/git.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Context, Effect, pipe } from "effect"
|
||||
import { GitConstructError, GitError, GitPluginError, GitResponseError, SimpleGit } from "simple-git"
|
||||
import { trimNewlines } from "trim-newlines"
|
||||
import { Opaque } from "type-fest"
|
||||
|
||||
|
||||
export const SimpleGitService = Context.Tag<SimpleGit>()
|
||||
|
||||
function mapSimpleGitErrors(e: unknown): GitError
|
||||
| GitConstructError
|
||||
| GitPluginError
|
||||
| GitResponseError
|
||||
| Error
|
||||
{
|
||||
if (e instanceof GitError
|
||||
|| e instanceof GitConstructError
|
||||
|| e instanceof GitPluginError
|
||||
|| e instanceof GitResponseError)
|
||||
return e
|
||||
|
||||
return new Error(`simpleGit failed with a unknown error: ${ e }`)
|
||||
}
|
||||
|
||||
|
||||
export type GitRefName = Opaque<string, "GitRefName">
|
||||
export type GitRefCommitHash = Opaque<string, "GitRefCommitHash">
|
||||
|
||||
export class GitRef {
|
||||
constructor(
|
||||
readonly name: GitRefName,
|
||||
readonly commit: GitRefCommitHash,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class GitBranchRef extends GitRef {}
|
||||
export class GitTagRef extends GitRef {}
|
||||
|
||||
|
||||
export class GitRefNullNameError {}
|
||||
|
||||
export const getGitRefName = pipe(
|
||||
SimpleGitService,
|
||||
|
||||
Effect.flatMap(git =>
|
||||
Effect.tryPromise({
|
||||
try: async () =>
|
||||
(await git.status()).current as GitRefName | null,
|
||||
|
||||
catch: mapSimpleGitErrors,
|
||||
})
|
||||
),
|
||||
|
||||
Effect.flatMap(name =>
|
||||
name
|
||||
? Effect.succeed(name)
|
||||
: Effect.fail(new GitRefNullNameError)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
export const getGitRefTagName = pipe(
|
||||
SimpleGitService,
|
||||
|
||||
Effect.flatMap(git =>
|
||||
Effect.tryPromise({
|
||||
try: async () =>
|
||||
trimNewlines( await git.raw("describe", "--tags", "--exact-match") ) as GitRefName,
|
||||
|
||||
catch: mapSimpleGitErrors,
|
||||
})
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
export const getGitRefCommitHash = pipe(
|
||||
SimpleGitService,
|
||||
|
||||
Effect.flatMap(git =>
|
||||
Effect.tryPromise({
|
||||
try: async () =>
|
||||
trimNewlines( await git.raw("show", "--format=%h", "--no-patch") ) as GitRefCommitHash,
|
||||
|
||||
catch: mapSimpleGitErrors,
|
||||
})
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
export const getGitRef = pipe(
|
||||
Effect.all([
|
||||
getGitRefName,
|
||||
getGitRefCommitHash,
|
||||
]),
|
||||
|
||||
Effect.flatMap(([name, commit]) =>
|
||||
name !== "HEAD" ?
|
||||
Effect.succeed(new GitBranchRef(name, commit) as GitRef)
|
||||
:
|
||||
pipe(
|
||||
getGitRefTagName,
|
||||
Effect.map(name => new GitTagRef(name, commit) as GitRef),
|
||||
)
|
||||
),
|
||||
)
|
||||
41
src/index.ts
Normal file
41
src/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Console, Context, Effect, pipe } from "effect"
|
||||
import simpleGit from "simple-git"
|
||||
import { parseArgv } from "./argv"
|
||||
import { parseEnv } from "./env"
|
||||
import { SimpleGitService, getGitRef } from "./git"
|
||||
import { DefaultMatcher, SemVerMatcher, generateDockerTagsFromGitRef } from "./matchers"
|
||||
import { writeDockerTagsFile } from "./tags"
|
||||
|
||||
|
||||
const [ _env, [,, gitPath] ] = await Effect.runPromise(
|
||||
Effect.all([parseEnv(), parseArgv()])
|
||||
)
|
||||
|
||||
const context = Context.empty().pipe(
|
||||
Context.add(
|
||||
SimpleGitService,
|
||||
SimpleGitService.of(simpleGit(gitPath)),
|
||||
)
|
||||
)
|
||||
|
||||
const main = pipe(
|
||||
getGitRef,
|
||||
|
||||
Effect.flatMap(ref => generateDockerTagsFromGitRef(ref, [
|
||||
new SemVerMatcher,
|
||||
new DefaultMatcher,
|
||||
])),
|
||||
|
||||
Effect.tap(tags =>
|
||||
Console.log(`Generated tags: ${ tags.join(", ") }`)
|
||||
),
|
||||
|
||||
Effect.flatMap(tags => writeDockerTagsFile(gitPath, tags)),
|
||||
)
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.provideContext(
|
||||
main,
|
||||
context,
|
||||
)
|
||||
)
|
||||
17
src/matchers/CalVerMatcher.ts
Normal file
17
src/matchers/CalVerMatcher.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Effect } from "effect"
|
||||
import { GitRef } from "../git"
|
||||
import { GitRefMatcher } from "./GitRefMatcher"
|
||||
|
||||
|
||||
export class CalVerMatcher
|
||||
extends GitRefMatcher<never, Error> {
|
||||
|
||||
constructor(private format: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
match(_ref: GitRef) {
|
||||
return Effect.fail(new Error("Not implemented"))
|
||||
}
|
||||
|
||||
}
|
||||
19
src/matchers/DefaultMatcher.ts
Normal file
19
src/matchers/DefaultMatcher.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Effect, Option } from "effect"
|
||||
import { GitRef } from "../git"
|
||||
import { DockerTag } from "../tags"
|
||||
import { GitRefMatcher } from "./GitRefMatcher"
|
||||
|
||||
|
||||
export class DefaultMatcher
|
||||
extends GitRefMatcher<never, never> {
|
||||
|
||||
match(ref: GitRef) {
|
||||
return Effect.succeed(
|
||||
Option.some([
|
||||
ref.name,
|
||||
`${ ref.name }-${ ref.commit }`,
|
||||
] as DockerTag[])
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
8
src/matchers/GitRefMatcher.ts
Normal file
8
src/matchers/GitRefMatcher.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Effect, Option } from "effect"
|
||||
import { GitRef } from "../git"
|
||||
import { DockerTag } from "../tags"
|
||||
|
||||
|
||||
export abstract class GitRefMatcher<R, E> {
|
||||
abstract match(ref: GitRef): Effect.Effect<R, E, Option.Option<DockerTag[]>>
|
||||
}
|
||||
56
src/matchers/SemVerMatcher.ts
Normal file
56
src/matchers/SemVerMatcher.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Effect, Option } from "effect"
|
||||
import { isEmpty, reverse, tail } from "lodash-es"
|
||||
import semver from "semver"
|
||||
import { GitRef, GitTagRef } from "../git"
|
||||
import { DockerTag } from "../tags"
|
||||
import { GitRefMatcher } from "./GitRefMatcher"
|
||||
|
||||
|
||||
export class SemVerMatcher
|
||||
extends GitRefMatcher<never, TypeError | Error> {
|
||||
|
||||
match(ref: GitRef) {
|
||||
return (ref instanceof GitTagRef && semver.valid(ref.name)) ?
|
||||
Effect.try({
|
||||
try: () => {
|
||||
const prerelease = semver.prerelease(ref.name)
|
||||
|
||||
return Option.some(
|
||||
!prerelease ? [
|
||||
"latest",
|
||||
...this.generateTags([
|
||||
semver.major(ref.name),
|
||||
semver.minor(ref.name),
|
||||
semver.patch(ref.name),
|
||||
]),
|
||||
] :
|
||||
this.generateTags(prerelease).map(tag =>
|
||||
`${ semver.coerce(ref.name) }-${ tag }` as DockerTag
|
||||
)
|
||||
) as Option.Option<DockerTag[]>
|
||||
},
|
||||
|
||||
catch: (e): TypeError | Error => {
|
||||
if (e instanceof TypeError)
|
||||
return e
|
||||
|
||||
return new Error(`An unknown error happened while parsing SemVer: ${ e }`)
|
||||
},
|
||||
})
|
||||
:
|
||||
Effect.succeed(Option.none())
|
||||
}
|
||||
|
||||
private generateTags(numbers: readonly (string | number)[]) {
|
||||
const rec = (numbers: readonly (string | number)[]): DockerTag[] =>
|
||||
isEmpty(numbers)
|
||||
? []
|
||||
: [
|
||||
...rec( tail(numbers) ),
|
||||
reverse(numbers).join(".") as DockerTag,
|
||||
]
|
||||
|
||||
return rec( reverse(numbers) )
|
||||
}
|
||||
|
||||
}
|
||||
12
src/matchers/StubMatcher.ts
Normal file
12
src/matchers/StubMatcher.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Effect, Option } from "effect"
|
||||
import { GitRefMatcher } from "./GitRefMatcher"
|
||||
|
||||
|
||||
export class StubMatcher
|
||||
extends GitRefMatcher<never, never> {
|
||||
|
||||
match() {
|
||||
return Effect.succeed(Option.none())
|
||||
}
|
||||
|
||||
}
|
||||
31
src/matchers/index.ts
Normal file
31
src/matchers/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Effect, Option } from "effect"
|
||||
import { isEmpty, tail } from "lodash-es"
|
||||
import { GitRef } from "../git"
|
||||
import { DockerTag } from "../tags"
|
||||
import { GitRefMatcher } from "./GitRefMatcher"
|
||||
|
||||
|
||||
export * from "./CalVerMatcher"
|
||||
export * from "./DefaultMatcher"
|
||||
export * from "./GitRefMatcher"
|
||||
export * from "./SemVerMatcher"
|
||||
export * from "./StubMatcher"
|
||||
|
||||
|
||||
export class NoMatcherFoundError {}
|
||||
|
||||
export const generateDockerTagsFromGitRef = <R, E>(
|
||||
ref: GitRef,
|
||||
matchers: GitRefMatcher<R, E>[],
|
||||
): Effect.Effect<R, E | NoMatcherFoundError, DockerTag[]> =>
|
||||
isEmpty(matchers) ?
|
||||
Effect.fail(new NoMatcherFoundError)
|
||||
:
|
||||
matchers[0].match(ref).pipe(
|
||||
Effect.flatMap(res =>
|
||||
Option.match(res, {
|
||||
onSome: Effect.succeed,
|
||||
onNone: () => generateDockerTagsFromGitRef(ref, tail(matchers))
|
||||
})
|
||||
)
|
||||
)
|
||||
18
src/tags.ts
Normal file
18
src/tags.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Effect } from "effect"
|
||||
import path from "node:path"
|
||||
import { Opaque } from "type-fest"
|
||||
import { GitPath } from "./argv"
|
||||
|
||||
|
||||
export type DockerTag = Opaque<string, "Tag">
|
||||
|
||||
|
||||
export const writeDockerTagsFile = (gitPath: GitPath, tags: DockerTag[]) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
await Bun.write( path.join(gitPath, ".tags"), tags.join(",") )
|
||||
},
|
||||
|
||||
catch: e =>
|
||||
new Error(`Could not write file '${ path.join(gitPath, ".tags") }': ${ e }`),
|
||||
})
|
||||
Reference in New Issue
Block a user