@atomservice/gitea 0.1.5
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 +36 -0
- package/src/gitea.compose.ts +60 -0
- package/src/gitea.consts.ts +1 -0
- package/src/gitea.instance.ts +33 -0
- package/src/gitea.options.ts +75 -0
- package/src/gitea.service.ts +105 -0
- package/src/gitea.types.ts +4 -0
- package/src/index.ts +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atomservice/gitea",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Gitea 原子服务",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "openorson",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/openorson/atomservice.git",
|
|
10
|
+
"directory": "services/gitea"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/openorson/atomservice/issues",
|
|
13
|
+
"homepage": "https://github.com/openorson/atomservice/tree/main/services/gitea#readme",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"atomservice",
|
|
16
|
+
"gitea",
|
|
17
|
+
"git",
|
|
18
|
+
"self-hosted",
|
|
19
|
+
"podman"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"bun": ">=1.3.14"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@atomservice/gateway": "0.1.5"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@atomservice/core": "0.1.5"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { tmpl } from "@atomservice/core"
|
|
2
|
+
import { GITEA_IMAGE } from "./gitea.consts.ts"
|
|
3
|
+
|
|
4
|
+
export interface GiteaComposeOptions {
|
|
5
|
+
rootless: boolean
|
|
6
|
+
environment?: Record<string, string>
|
|
7
|
+
hostPort?: number
|
|
8
|
+
sshHostPort?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function giteaCompose(id: string, version: string, dataDir: string, opts: GiteaComposeOptions): string {
|
|
12
|
+
const { rootless, environment, hostPort, sshHostPort } = opts
|
|
13
|
+
const port = 3000
|
|
14
|
+
const uid = 1000
|
|
15
|
+
const gid = 1000
|
|
16
|
+
|
|
17
|
+
const imageTag = rootless ? `${version}-rootless` : version
|
|
18
|
+
const volumeTarget = rootless ? "/var/lib/gitea" : "/data"
|
|
19
|
+
const containerSshPort = rootless ? 2222 : 22
|
|
20
|
+
|
|
21
|
+
const httpPort = hostPort ? `\n - "${hostPort}:${port}"` : ""
|
|
22
|
+
const sshPort = sshHostPort ? `\n - "${sshHostPort}:${containerSshPort}"` : ""
|
|
23
|
+
const portsBlock = httpPort || sshPort ? `\n ports:${httpPort}${sshPort}` : ""
|
|
24
|
+
|
|
25
|
+
const sshEnv =
|
|
26
|
+
rootless && sshHostPort ? `\n - SSH_PORT=${sshHostPort}\n - SSH_LISTEN_PORT=${containerSshPort}` : ""
|
|
27
|
+
|
|
28
|
+
const envBlock =
|
|
29
|
+
environment && Object.keys(environment).length > 0
|
|
30
|
+
? `\n${Object.entries(environment)
|
|
31
|
+
.map(([k, v]) => ` - ${k}=${v}`)
|
|
32
|
+
.join("\n")}`
|
|
33
|
+
: ""
|
|
34
|
+
|
|
35
|
+
return tmpl`\
|
|
36
|
+
services:
|
|
37
|
+
${id}:
|
|
38
|
+
image: ${GITEA_IMAGE}:${imageTag}
|
|
39
|
+
container_name: ${id}
|
|
40
|
+
environment:${envBlock}
|
|
41
|
+
- USER_UID=${uid}
|
|
42
|
+
- USER_GID=${gid}${sshEnv}${portsBlock}
|
|
43
|
+
volumes:
|
|
44
|
+
- ${dataDir}:${volumeTarget}
|
|
45
|
+
networks:
|
|
46
|
+
- atomservice
|
|
47
|
+
restart: unless-stopped
|
|
48
|
+
healthcheck:
|
|
49
|
+
test: ["CMD", "curl", "-sf", "http://localhost:${port}/api/healthz"]
|
|
50
|
+
interval: 10s
|
|
51
|
+
timeout: 5s
|
|
52
|
+
retries: 5
|
|
53
|
+
start_period: 15s
|
|
54
|
+
|
|
55
|
+
networks:
|
|
56
|
+
atomservice:
|
|
57
|
+
name: atomservice
|
|
58
|
+
external: true
|
|
59
|
+
`
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const GITEA_IMAGE = "docker.gitea.com/gitea"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface GiteaInstance {
|
|
2
|
+
/**
|
|
3
|
+
* 容器主机名
|
|
4
|
+
*
|
|
5
|
+
* - 同容器名
|
|
6
|
+
* - 容器网络内可直接访问
|
|
7
|
+
*/
|
|
8
|
+
readonly host: string
|
|
9
|
+
/**
|
|
10
|
+
* 容器内 `HTTP` 监听端口
|
|
11
|
+
*/
|
|
12
|
+
readonly port: number
|
|
13
|
+
/**
|
|
14
|
+
* 容器内 `SSH` 监听端口
|
|
15
|
+
*
|
|
16
|
+
* - `rootful` 为 `22`
|
|
17
|
+
* - `rootless` 为 `2222`
|
|
18
|
+
*/
|
|
19
|
+
readonly sshPort: number
|
|
20
|
+
/**
|
|
21
|
+
* 容器网络内访问地址
|
|
22
|
+
*
|
|
23
|
+
* - 如 `http://gitea:3000/`
|
|
24
|
+
*/
|
|
25
|
+
readonly url: string
|
|
26
|
+
/**
|
|
27
|
+
* 对外域名地址
|
|
28
|
+
*
|
|
29
|
+
* - 如 `https://git.example.com/`
|
|
30
|
+
* - 仅配置 `domain` 后可用
|
|
31
|
+
*/
|
|
32
|
+
readonly externalUrl: string | undefined
|
|
33
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CallableService } from "@atomservice/core"
|
|
2
|
+
import type { GatewayInstance } from "@atomservice/gateway"
|
|
3
|
+
|
|
4
|
+
export interface GiteaOptions {
|
|
5
|
+
/**
|
|
6
|
+
* 实例标识
|
|
7
|
+
*
|
|
8
|
+
* - 用于区分不同实例
|
|
9
|
+
*
|
|
10
|
+
* @default "default"
|
|
11
|
+
*/
|
|
12
|
+
id?: string
|
|
13
|
+
/**
|
|
14
|
+
* 镜像版本
|
|
15
|
+
*
|
|
16
|
+
* @default "1"
|
|
17
|
+
*/
|
|
18
|
+
version?: string
|
|
19
|
+
/**
|
|
20
|
+
* 映射到宿主机的端口
|
|
21
|
+
*
|
|
22
|
+
* - 不填则不对外暴露,仅容器网络内访问
|
|
23
|
+
*/
|
|
24
|
+
hostPort?: number
|
|
25
|
+
/**
|
|
26
|
+
* 对外域名
|
|
27
|
+
*
|
|
28
|
+
* - 填写后自动在网关注册反代路由
|
|
29
|
+
* - 需要和 `gateway` 原子一起使用
|
|
30
|
+
*/
|
|
31
|
+
domain?: string
|
|
32
|
+
/**
|
|
33
|
+
* `SSH` 映射到宿主机的端口
|
|
34
|
+
*
|
|
35
|
+
* - 不设置则仅支持 `HTTP(S)` 方式克隆
|
|
36
|
+
* - 填写后额外支持 `git@host:org/repo.git` 克隆
|
|
37
|
+
* - 建议使用 `2222`,避免与宿主机默认 `SSH` 端口 `22` 冲突
|
|
38
|
+
*/
|
|
39
|
+
sshHostPort?: number
|
|
40
|
+
/**
|
|
41
|
+
* 使用 `rootless` 镜像
|
|
42
|
+
*
|
|
43
|
+
* - `rootless` 安全性更高,但权限要求更严格
|
|
44
|
+
* - 一旦初始化数据目录后不可切换 `rootful` / `rootless`
|
|
45
|
+
*
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
rootless?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* 自定义环境变量
|
|
51
|
+
*
|
|
52
|
+
* - 支持 Gitea 的 `GITEA__section__KEY` 格式
|
|
53
|
+
* - 用于配置数据库、邮件等任意 `app.ini` 设置
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* // 配置 SMTP 邮件
|
|
58
|
+
* environment: {
|
|
59
|
+
* GITEA__mailer__ENABLED: "true",
|
|
60
|
+
* GITEA__mailer__FROM: "noreply@example.com",
|
|
61
|
+
* GITEA__mailer__HOST: "smtp.example.com:587",
|
|
62
|
+
* GITEA__mailer__USER: "noreply@example.com",
|
|
63
|
+
* GITEA__mailer__PASSWD: "xxx",
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
environment?: Record<string, string>
|
|
68
|
+
/**
|
|
69
|
+
* 网关原子实例
|
|
70
|
+
*
|
|
71
|
+
* - 填写后自动向网关注册反代路由
|
|
72
|
+
* - 通常和 `domain` 一起使用
|
|
73
|
+
*/
|
|
74
|
+
gateway?: CallableService<GatewayInstance>
|
|
75
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import type { CallableService } from "@atomservice/core"
|
|
3
|
+
import { defineService, onDown, onHealth, onUp, useConfig, useLogger, useShell, useState } from "@atomservice/core"
|
|
4
|
+
import { giteaCompose } from "./gitea.compose.ts"
|
|
5
|
+
import type { GiteaInstance } from "./gitea.instance.ts"
|
|
6
|
+
import type { GiteaOptions } from "./gitea.options.ts"
|
|
7
|
+
import type { GiteaState } from "./gitea.types.ts"
|
|
8
|
+
|
|
9
|
+
export function gitea(opts: GiteaOptions = {}): CallableService<GiteaInstance> {
|
|
10
|
+
const serviceId = opts.id && opts.id !== "default" ? opts.id : "default"
|
|
11
|
+
const containerName = serviceId !== "default" ? `gitea-${serviceId}` : "gitea"
|
|
12
|
+
const version = opts.version ?? "1"
|
|
13
|
+
const rootless = opts.rootless ?? false
|
|
14
|
+
|
|
15
|
+
return defineService({
|
|
16
|
+
name: "gitea",
|
|
17
|
+
id: opts.id,
|
|
18
|
+
setup: () => {
|
|
19
|
+
const logger = useLogger()
|
|
20
|
+
const $ = useShell()
|
|
21
|
+
const config = useConfig()
|
|
22
|
+
const gw = opts.gateway?.()
|
|
23
|
+
|
|
24
|
+
const dir = path.join(config.root, containerName)
|
|
25
|
+
const composePath = path.join(dir, "compose.yml")
|
|
26
|
+
|
|
27
|
+
const { save, watch, checkChanges } = useState<GiteaState>()
|
|
28
|
+
|
|
29
|
+
watch("version", {
|
|
30
|
+
risk: "low",
|
|
31
|
+
changed: (previousValue, currentValue) =>
|
|
32
|
+
`Gitea 版本已变更 ${previousValue} → ${currentValue},将在下次启动时拉取新镜像`,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
watch("rootless", {
|
|
36
|
+
risk: "high",
|
|
37
|
+
changed: () => `rootless 模式已切换,rootful 与 rootless 镜像不兼容,已有数据目录无法复用`,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
onUp(async () => {
|
|
41
|
+
const currentState: GiteaState = { version, rootless }
|
|
42
|
+
await checkChanges(currentState)
|
|
43
|
+
|
|
44
|
+
await $`mkdir -p ${dir}/data`
|
|
45
|
+
await Bun.write(
|
|
46
|
+
composePath,
|
|
47
|
+
giteaCompose(containerName, version, `${dir}/data`, {
|
|
48
|
+
rootless,
|
|
49
|
+
environment: opts.environment,
|
|
50
|
+
hostPort: opts.hostPort,
|
|
51
|
+
sshHostPort: opts.sshHostPort,
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
logger.info(`正在启动 ${containerName}`)
|
|
56
|
+
await $`podman compose -f ${composePath} up -d`
|
|
57
|
+
logger.success(`${containerName} 已启动`)
|
|
58
|
+
|
|
59
|
+
if (opts.domain && gw) {
|
|
60
|
+
logger.info(`注册路由 ${opts.domain} → ${containerName}:3000`)
|
|
61
|
+
await gw.registerRoute(opts.domain, `${containerName}:3000`)
|
|
62
|
+
logger.success(`路由已注册,访问 https://${opts.domain}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await save(currentState)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
onDown(async () => {
|
|
69
|
+
logger.info(`正在停止 ${containerName}`)
|
|
70
|
+
await $`podman compose -f ${composePath} down`
|
|
71
|
+
logger.success(`${containerName} 已停止`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
onHealth(async () => {
|
|
75
|
+
try {
|
|
76
|
+
const result = await $`podman exec ${containerName} curl -sf http://localhost:3000/api/healthz`
|
|
77
|
+
.quiet()
|
|
78
|
+
.nothrow()
|
|
79
|
+
if (result.exitCode === 0) return { status: "healthy" }
|
|
80
|
+
return { status: "unhealthy", message: `Gitea 健康检查返回 ${result.exitCode}` }
|
|
81
|
+
} catch {
|
|
82
|
+
return { status: "unhealthy", message: `容器 ${containerName} 不存在` }
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
get host() {
|
|
88
|
+
return containerName
|
|
89
|
+
},
|
|
90
|
+
get port() {
|
|
91
|
+
return 3000
|
|
92
|
+
},
|
|
93
|
+
get sshPort() {
|
|
94
|
+
return rootless ? 2222 : 22
|
|
95
|
+
},
|
|
96
|
+
get url() {
|
|
97
|
+
return `http://${containerName}:3000/`
|
|
98
|
+
},
|
|
99
|
+
get externalUrl() {
|
|
100
|
+
return opts.domain ? `https://${opts.domain}/` : undefined
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
}
|
package/src/index.ts
ADDED