@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 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
+ }
@@ -0,0 +1,4 @@
1
+ export type GiteaState = {
2
+ version: string
3
+ rootless: boolean
4
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type { GiteaInstance } from "./gitea.instance.ts"
2
+ export type { GiteaOptions } from "./gitea.options.ts"
3
+ export { gitea } from "./gitea.service.ts"