Initial version
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
/.git
|
||||
/node_modules
|
||||
/test-repo
|
||||
/.tags
|
||||
71
.drone.jsonnet
Normal file
71
.drone.jsonnet
Normal file
@@ -0,0 +1,71 @@
|
||||
local bun_image = "oven/bun:0.8.1";
|
||||
|
||||
|
||||
local install_run_step = {
|
||||
name: "install-run",
|
||||
image: bun_image,
|
||||
commands: [
|
||||
"apt update -y && apt full-upgrade -y && apt install -y --no-install-recommends git",
|
||||
"bun install --production --frozen-lockfile --no-cache",
|
||||
"bun start .",
|
||||
],
|
||||
};
|
||||
|
||||
local build_docker_step(publish) = {
|
||||
name: "build-" + (if publish then "publish-" else "") + "docker",
|
||||
image: "plugins/docker",
|
||||
|
||||
settings: {
|
||||
dry_run: !publish,
|
||||
registry: "git.jvalver.de",
|
||||
username: { from_secret: "docker_username" },
|
||||
password: { from_secret: "docker_password" },
|
||||
repo: "git.jvalver.de/thilawyn/drone-better-docker-autotag",
|
||||
dockerfile: "Dockerfile",
|
||||
context: ".",
|
||||
compress: true,
|
||||
platform: "linux/amd64",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
[
|
||||
// Build docker images without publishing them for pull requests
|
||||
{
|
||||
kind: "pipeline",
|
||||
type: "docker",
|
||||
name: "build-docker",
|
||||
|
||||
trigger: {
|
||||
ref: {
|
||||
include: ["refs/pull/**"]
|
||||
}
|
||||
},
|
||||
|
||||
steps: [
|
||||
install_run_step,
|
||||
build_docker_step(false),
|
||||
],
|
||||
},
|
||||
|
||||
// Build docker images and publish them for master and tags
|
||||
{
|
||||
kind: "pipeline",
|
||||
type: "docker",
|
||||
name: "build-publish-docker",
|
||||
|
||||
trigger: {
|
||||
ref: {
|
||||
include: [
|
||||
"refs/heads/master",
|
||||
"refs/tags/**",
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
steps: [
|
||||
install_run_step,
|
||||
build_docker_step(true),
|
||||
],
|
||||
},
|
||||
]
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/node_modules
|
||||
/test-repo
|
||||
/.tags
|
||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM oven/bun:0.8.1
|
||||
|
||||
RUN apt update -y && \
|
||||
apt full-upgrade -y && \
|
||||
apt install -y --no-install-recommends git && \
|
||||
apt autoremove -y --purge && \
|
||||
apt clean
|
||||
|
||||
COPY ./ ./
|
||||
RUN bun install --production --frozen-lockfile --no-cache && \
|
||||
rm -rf ~/.bun
|
||||
ENTRYPOINT ["bun", "start", "/drone/src"]
|
||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "drone-better-docker-autotag",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "NODE_ENV=production bun src/index.ts",
|
||||
"start:dev": "NODE_ENV=development bun src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/semver": "^7.5.0",
|
||||
"bun-types": "latest",
|
||||
"type-fest": "^4.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/schema": "^0.33.2",
|
||||
"effect": "^2.0.0-next.29",
|
||||
"lodash-es": "^4.17.21",
|
||||
"semver": "^7.5.4",
|
||||
"simple-git": "^3.19.1",
|
||||
"trim-newlines": "^5.0.0"
|
||||
}
|
||||
}
|
||||
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 }`),
|
||||
})
|
||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"allowImportingTsExtensions": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "preserve",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user