Initial version
Some checks failed
continuous-integration/drone Build is passing
continuous-integration/drone/tag Build is failing

This commit is contained in:
Julien Valverdé
2023-08-31 02:16:43 +02:00
parent 416a133d82
commit cfd90744bc
18 changed files with 477 additions and 0 deletions

18
src/argv.ts Normal file
View 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
View 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
View 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
View 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,
)
)

View 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"))
}
}

View 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[])
)
}
}

View 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[]>>
}

View 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) )
}
}

View 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
View 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
View 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 }`),
})