Compare commits
2
Commits
fc1c5de0a7
...
1119723e16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1119723e16 | ||
|
|
707529cbf4 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
.agents/
|
||||
.claude/
|
||||
skills-lock.json
|
||||
@@ -37,6 +37,7 @@ npx skills remove runtime-image -g
|
||||
| [runtime-image](skills/runtime-image/SKILL.md) | 探测项目技术栈,按 runtime-base 镜像矩阵选型并生成/修正 Dockerfile |
|
||||
| [skill-forge](skills/skill-forge/SKILL.md) | 交互式创建新 Skill 的流水线,一问一答采集触发词/流程/红线,产出可直接使用的 SKILL.md |
|
||||
| [push-and-report](skills/push-and-report/SKILL.md) | 标准收尾流程:盘点 git 变更 → 英文 conventional commit → 推送远程 → SPMS 日报 |
|
||||
| [gitea-actions](skills/gitea-actions/SKILL.md) | 按统一规范给项目生成/评审优化 Gitea Actions 构建部署工作流(buildx 推 Gitea Packages → 本机重建容器 → 清理旧版本) |
|
||||
|
||||
## 目录结构
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: gitea-actions
|
||||
description: 按统一规范给项目生成或评审优化 Gitea Actions 构建部署工作流(.gitea/workflows/docker-deploy.yaml):推 v 开头 tag 触发 → buildx 构建推 Gitea Packages → 本机 docker 重建容器 → 清理本机与注册表旧版本;零第三方 action、INNEV_ 前缀变量/密钥、凭证回退链。凡用户要求「给项目加构建工作流 / 加个 CI / 配发版流水线 / 加 Gitea Actions / 生成 docker-deploy.yaml / 打 tag 自动构建部署 / 这个项目怎么自动发版 / 看看这个 workflow 规范不规范 / 帮我评审一下这个流水线」,或出现 推了 tag Actions 没动静 / workflow 不触发 / push 镜像 denied / unauthorized / 健康检查超时 等症状,或提到 .gitea/workflows / workflow_dispatch / buildx / Gitea Packages / self-hosted runner / INNEV_ 变量 时,务必使用本 skill——即使用户没说「Gitea Actions」这几个字。Use whenever the user wants to add, review, or standardize a Gitea Actions build/deploy workflow for a project. 边界:写/改 Dockerfile 用 runtime-image;提交推送代码+写日报用 push-and-report;手工推镜像发版用 xgent-image-push。
|
||||
---
|
||||
|
||||
# gitea-actions — 按统一规范给项目装构建部署工作流
|
||||
|
||||
把实战中固化下来的 docker-deploy 流水线规范安装到任意项目。核心理念(推理链,所有约束都挂在这上面):
|
||||
|
||||
- **版本号即镜像 tag**:发布 = 打 v 开头 git tag,镜像 tag 与之一一对应,可回溯;推 main 不发版
|
||||
- **runner 与部署目标同机**:self-hosted 实例级 runner 直连宿主 docker,构建完即本机部署
|
||||
- **零外部依赖**:不用任何第三方 action,run 步骤直连 git/docker,内网 runner 无需访问 github.com
|
||||
|
||||
## 默认流程(不带任何附加提示词也照此执行)
|
||||
|
||||
检测项目是否存在 `.gitea/workflows/` 下的工作流文件(yml/yaml),自动分流:
|
||||
|
||||
- **不存在 → 新建模式**:按规范从零生成 `docker-deploy.yaml`
|
||||
- **已存在 → 评审优化模式**:对照规范逐条审计,差异摊开,用户确认后按规范改写优化(默认不直接覆盖)
|
||||
|
||||
## 总流程
|
||||
|
||||
```
|
||||
第 0 步 探测项目(绝不可跳过)→ 第 1 步 槽位定参 → 第 2 步 生成/改写(确认闸)
|
||||
→ 第 3 步 验证 → 第 4 步 汇报
|
||||
```
|
||||
|
||||
除非用户明确只要聊聊思路,完整走全部 5 步。
|
||||
|
||||
## 第 0 步:探测项目(绝不可跳过)
|
||||
|
||||
不许凭用户一句话生成——读项目文件坐实,缺料就停下来问,不猜、不翻别的项目抄。
|
||||
|
||||
| 探测项 | 怎么探 | 判定/用途 |
|
||||
|---|---|---|
|
||||
| 已有 workflow(**最先做,决定模式**) | glob `.gitea/workflows/*.yml`、`.yaml` | 有 → 评审优化模式;无 → 新建模式 |
|
||||
| 项目名 | git remote basename → package.json name → 目录名,用户确认 | `{{PROJECT}}`:镜像名/容器名/builder 名 |
|
||||
| Dockerfile | glob 项目内 `Dockerfile*` | 无 → die:先走 runtime-image 生成 Dockerfile——流水线对着空气构建无意义 |
|
||||
| 启动模型 | 读 Dockerfile 的 FROM | runtime-node 系(容器启动时 install+build)→ 健康检查窗口放宽 ~7.5 分钟;预构建产物 → 窗口可缩短 |
|
||||
| 端口 | Dockerfile EXPOSE / 项目配置 / 问用户 | `{{APP_PORT}}` 容器内端口 + `{{HOST_PORT}}` 对外端口默认值 |
|
||||
| 健康检查端点 | grep 代码里 `/health`、`/healthz`、`/api/health` 等路由 | 有 → `{{HEALTH_PATH}}`;无 → warn:不虚构,生成时整块省略健康检查步,汇报大字声明 |
|
||||
| 必需 .env key | 读 `.env.example` | 渲染 deploy 步的 env key 检查行;无 `.env.example` → 整块省略 |
|
||||
| 特殊预构建 | 问用户(如从源码构建工具二进制) | 对应模板 `[OPTIONAL:PRE_BUILD]` 块,插入"构建并推送镜像"步之前 |
|
||||
| 额外运行参数 | 读 Dockerfile/运行时要求(如 runtime-node 的 `APP_ENTRY`)、.env 是否引用宿主主机名 | 对应 `[OPTIONAL:EXTRA_ENV]` / `[OPTIONAL:ADD_HOST]` 块 |
|
||||
|
||||
→ 验收:模式(新建/评审优化)有 glob 结论背书;每个槽位有文件证据(路径)或用户确认,拿不准的在生成前问清。
|
||||
|
||||
## 第 1 步:槽位定参
|
||||
|
||||
读 `references/docker-deploy-template.yaml`(模板即规范,含全部机制与尾部人工准备清单),把探测结论映射到槽位:
|
||||
|
||||
| 槽位/可选块 | 来源 | 无对应值时 |
|
||||
|---|---|---|
|
||||
| `{{PROJECT}}` / `{{APP_PORT}}` / `{{HOST_PORT}}` / `{{HEALTH_PATH}}` | 第 0 步探测 | 前三个必给(问用户);HEALTH_PATH 无 → 删 `[OPTIONAL:HEALTH_CHECK]` 整块 |
|
||||
| `[OPTIONAL:ENV_KEY_CHECKS]` | `.env.example` 渲染,每个必需 key 一行 grep 检查 | 整块删除 |
|
||||
| `[OPTIONAL:PRE_BUILD]` | 用户确认的特殊预构建步骤 | 整块删除 |
|
||||
| `[OPTIONAL:EXTRA_ENV]` / `[OPTIONAL:ADD_HOST]` | 运行时/依赖拓扑要求 | 整块删除 |
|
||||
|
||||
槽位写作 `{{SLOT}}`(无 `$` 前缀),与 Gitea 表达式 `${{ ... }}` 区分;渲染后模板里不应残留任何 `{{...}}`。
|
||||
|
||||
→ 验收:槽位对照表逐项有值或有"整块删除"结论,摊开给用户过目。
|
||||
|
||||
## 第 2 步:生成/改写(确认闸)
|
||||
|
||||
默认动作:**先把渲染后的 workflow 全文摊给用户确认,再落盘**到 `.gitea/workflows/docker-deploy.yaml`;改写已有文件时给 diff。已有 workflow 里与规范不冲突的定制(额外 job、项目专用步骤)保留,并在汇报里列出。
|
||||
|
||||
→ 验收:用户确认后文件落盘;保留的定制逐项在汇报出现,不静默丢弃(红线)。
|
||||
|
||||
## 评审优化模式(第 0 步发现已有 workflow 时走此分支)
|
||||
|
||||
先做完第 0、1 步得出"应该长什么样",再拿现状逐条对照审计。**只读审计,审计报告出来之前不改任何文件**。
|
||||
|
||||
### 审计清单(依据列挂着硬约束编号)
|
||||
|
||||
| # | 检查项 | 依据 | 级别 |
|
||||
|---|---|---|---|
|
||||
| B1 | 出现 `uses:`(第三方 action) | ① | 必须修 |
|
||||
| B2 | 触发不是 push v* tag(如推 main 即构建部署) | ② | 必须修 |
|
||||
| B3 | 缺 workflow_dispatch job 单跑选项 | ② | 建议修 |
|
||||
| B4 | buildx 未经 buildkitd.toml 注入 registry `http=true` + docker.io mirrors | ③ | 必须修 |
|
||||
| B5 | 凭证写死,或无 `INNEV_*` → `gitea.actor`/`GITHUB_TOKEN` 回退链 | ④ | 必须修 |
|
||||
| B6 | cleanup 全局 prune,或容器 name 过滤无 `^...$` 锚点 | ⑤ | 必须修 |
|
||||
| B7 | 镜像只打 latest、无版本 tag | ⑥ | 必须修 |
|
||||
| B8 | 明文密钥 / clone 日志未 `grep -v` 过滤 token / docker login 未走 `--password-stdin` | ⑦ | 必须修(安全项,优先处理) |
|
||||
| B9 | 健康检查缺失或窗口与启动模型不匹配(runtime-node 首启数分钟) | ⑧ | 建议修 |
|
||||
| B10 | 自定义变量/密钥未用 INNEV_ 前缀 | 命名约定 | 建议修 |
|
||||
|
||||
级别定义:**必须修** = 会挂/泄密/误删邻项目产物;**建议修** = 能跑但稳定性/可维护性受损。
|
||||
|
||||
### 审计报告格式(固定)
|
||||
|
||||
- 结论一行:当前流程 → 规范流程差距概述(一致则明说"已符合规范")
|
||||
- 问题表:级别 | 行号 | 问题 | 依据(约束编号) | 修法
|
||||
- 已过项也要逐条报"已过"(B1-B10 逐条过,漏报和误报一样有害)
|
||||
|
||||
报告摊开、用户确认要修 → 转第 2 步给 diff,再进第 3 步验证。用户说"只评审不动手" → 报告即交付物。
|
||||
|
||||
## 第 3 步:验证
|
||||
|
||||
1. `bash scripts/check-workflow.sh <生成的 workflow 路径>` 规范自检 8 项,全过(SKIP 项人工知悉)
|
||||
2. YAML 语法由脚本第 ⑧ 项覆盖(pyyaml 可用时)
|
||||
3. 真触发(打 tag 发版)不在授权范围——**显式声明"未真机验证"**,给出手动验证路径:仓库 Actions → Run workflow 单跑 `image` 任务,绿了再全链路
|
||||
|
||||
完成定义 = 规范自检全过 + 用户拿到可执行的首次发布步骤;不是"文件写完了"。
|
||||
|
||||
→ 验收:check-workflow.sh 输出全 PASS;未验证项在汇报显式声明。
|
||||
|
||||
## 第 4 步:汇报
|
||||
|
||||
固定结构:
|
||||
- 模式:新建 / 评审优化
|
||||
- 探测结论:项目名 / Dockerfile / 启动模型 / 端口 / 健康端点 / env key(附证据文件路径)
|
||||
- 【评审优化】审计结果:必须修 N 条 / 建议修 N 条 / 已过 N 条(附问题表),用户是否授权修正
|
||||
- 写了什么:workflow 文件路径(新建 / 修改 diff);保留的项目定制清单
|
||||
- 被跳过/未做的:如无健康端点省略健康检查步、无 .env.example 省略 key 检查
|
||||
- 验证做到哪一步:check-workflow.sh 结果;**真触发没做就说没做**
|
||||
- 待人工补:runner 实例级登记 / 宿主 daemon.json insecure-registries 登记并重启 docker / 仓库 Units 启用 Actions / 服务器按 .env.example 建 .env / 仓库 Variables+Secrets 配置(清单在生成的 workflow 尾部注释)/ 首次打 tag 发布
|
||||
|
||||
## 硬约束
|
||||
|
||||
- ① **零第三方 action**。为什么:self-hosted runner 在内网,访问不了 github.com,`uses:` 一出现 workflow 直接跑不起来;run 步骤直连 git/docker 即可。
|
||||
- ② **仅 push v* tag 触发全链路,推 main 不触发**。为什么:版本号即镜像 tag 的模型——每次提交都发版会让 Packages 堆满无版本意义的镜像,且无法回溯;手动兜底用 workflow_dispatch 的 job 选项单跑。
|
||||
- ③ **buildx 必须经 buildkitd.toml 注入 registry `http=true` 与 docker.io mirrors**。为什么:docker-container 驱动的 buildkitd 不继承宿主 daemon.json 的 insecure-registries/registry-mirrors——不声明 http=true 推送阶段必失败,不注入 mirrors 拉基础镜像超时。
|
||||
- ④ **凭证回退链 `INNEV_REGISTRY_USER/INNEV_REGISTRY_TOKEN` → `gitea.actor`/`GITHUB_TOKEN`,`INNEV_CI_TOKEN` → `GITHUB_TOKEN`**。为什么:内置 token 同命名空间够用,配了 INNEV_* 即覆盖生效——不强制每个仓库都配密钥,也不许把凭证写死。
|
||||
- ⑤ **cleanup 只动本项目名下的镜像 tag 与 `^<项目名>$` 锚点过滤的容器**。为什么:部署机上还跑着其他项目的镜像与容器,全局 prune 或无锚点过滤(名字包含项目名的 buildx_buildkit_* 容器)就是误删事故。
|
||||
- ⑥ **镜像 tag = git tag 版本号 + latest,禁 latest-only**。为什么:latest 无法回溯版本;部署出问题想回退时,没有版本 tag 就没有可拉的历史镜像。
|
||||
- ⑦ **密钥全部走 secrets 引用,clone 日志 `grep -v "$GIT_TOKEN"` 过滤,docker login 走 `--password-stdin`**。为什么:明文 token 进 workflow 文件 = 进 git 历史;密码进 argv 会留在日志与进程列表。
|
||||
- ⑧ **健康检查窗口按启动模型放宽**。为什么:runtime-node 系容器首启要 install+build 数分钟,窗口太短会把正常启动当失败杀掉(spms 实测放宽到 ~7.5 分钟);项目没有健康端点时不虚构——省略该步并显式声明。
|
||||
- ⑨ **生成/改写前先摊全文或 diff 给用户确认**。为什么:workflow 直接决定发布行为,未经确认的写入超出授权;默认动作永远是"不写"。
|
||||
|
||||
## 凭证与配置
|
||||
|
||||
本 skill 自身**无外部凭证**。生成的 workflow 引用的 `INNEV_*` 变量/密钥由人在 Gitea「仓库 → 设置 → Actions」里配置(配置清单自动生成在 workflow 尾部注释里,照单配即可)。用户在对话中贴过来的真实令牌/密钥:不落进任何文件,并提醒轮换。
|
||||
|
||||
## 人机权责
|
||||
|
||||
- 写操作:生成/覆盖 `.gitea/workflows/docker-deploy.yaml`——默认摊全文确认后落盘。
|
||||
- 终态是人的按钮:首次发版(`git tag v0.1.0 && git push origin v0.1.0`)、仓库 Variables/Secrets 配置、服务器 .env 创建,agent 一律不执行。
|
||||
- 待人工补:runner 登记、宿主 daemon.json insecure-registries、仓库 Units 启用 Actions、数据库迁移(如适用)——落成清单交付,不绕道、不假装完成。
|
||||
|
||||
## 排障对照表
|
||||
|
||||
| 症状 | 原因 | 怎么办 |
|
||||
|---|---|---|
|
||||
| 推 tag 后 Actions 没动静 | 仓库未启用 Actions Units / tag 不是 v 开头 | 仓库设置启用 Actions;tag 改 v 开头重打 |
|
||||
| 镜像 push 阶段失败(http 相关报错) | buildkitd.toml 未声明 `http=true`(buildkitd 不继承宿主 insecure-registries) | 按约束③补齐注入(本规范模板已含) |
|
||||
| 构建拉基础镜像超时 | docker-container 驱动不继承宿主 registry-mirrors | 经 buildkitd.toml 注入 INNEV_DOCKERHUB_MIRRORS |
|
||||
| 部署机 `docker pull` 报 `HTTP response to HTTPS client` | 宿主 daemon.json 未登记 LAN 明文 registry | 登记 insecure-registries 并重启 docker(人工),或前缀改 TLS 地址 |
|
||||
| 健康检查超时/失败 | runtime-node 首启 install+build 数分钟窗口太短;或 .env 缺 key | 按约束⑧放宽窗口;看容器日志确认缺哪个 key |
|
||||
| cleanup 删注册表版本 403 | token 缺 write:package scope | 该步 best-effort 只告警不阻塞;补 scope 后自然恢复 |
|
||||
| 容器内连宿主数据库 5432 不通 | 容器经 host-gateway 访问不到宿主系统服务端口 | .env 改指同机 docker 发布端口(如 5433)——实测踩坑 |
|
||||
| git clone 源码失败 | INNEV_CI_TOKEN 缺失或 scope 不足 | 配 INNEV_CI_TOKEN(read:repository),或确认内置 GITHUB_TOKEN 够用 |
|
||||
|
||||
die 消息统一三段式:是什么 + 为什么(后果) + 怎么办。
|
||||
|
||||
## 红线(任何情况下不违反)
|
||||
|
||||
- 不替用户打 tag、不替用户触发首次发布——终态是人的按钮
|
||||
- 不往任何文件里写真实密钥/令牌;用户贴过来的提醒轮换
|
||||
- 不静默丢弃已有 workflow 的项目定制——差异必须摊开、保留项必须进汇报
|
||||
- 不虚构健康检查端点——探测不到就省略该步并显式声明,不写个 `/api/health` 假装有
|
||||
- 不擅自加第三方 action——用户坚持要加时,先复述约束①的后果再让其拍板
|
||||
- 不静默截断——被省略的可选块、被跳过的检查必须出现在汇报里
|
||||
|
||||
## 边界:这些不归本 skill 管
|
||||
|
||||
- 写/改/评审项目 Dockerfile(归 runtime-image;本 skill 第 0 步发现没有 Dockerfile 时指路过去)
|
||||
- 提交推送代码并写日报(归 push-and-report)
|
||||
- 手工构建推送业务镜像发版(归 xgent-image-push / xgent-app-release)
|
||||
- 修改 runtime 基础镜像本身(归 runtime-base 仓库维护)
|
||||
@@ -0,0 +1,286 @@
|
||||
# =============================================================================
|
||||
# gitea-actions 统一规范模板 —— 由 gitea-actions skill 渲染后落盘为项目
|
||||
# .gitea/workflows/docker-deploy.yaml。本文件不直接使用。
|
||||
#
|
||||
# 槽位(写作 {{SLOT}},无 $ 前缀,与 Gitea 表达式 ${{ ... }} 区分):
|
||||
# {{PROJECT}} 项目名——镜像名/容器名/builder 名的共同基础
|
||||
# {{APP_PORT}} 容器内应用监听端口
|
||||
# {{HOST_PORT}} 容器对外发布端口的默认值(仓库变量 INNEV_HOST_PORT 可覆盖)
|
||||
# {{HEALTH_PATH}} 健康检查路径(如 /api/health)
|
||||
# 可省略块:# [OPTIONAL:名称] ... # [/OPTIONAL] —— 条件不满足时连同标记整块删除
|
||||
# =============================================================================
|
||||
# Docker 化部署流水线:
|
||||
# push tag v* → ① buildx 构建镜像并推送 Gitea Packages → ② 本机 docker 拉取镜像、重建并启动容器
|
||||
# → ③ 清理旧版本镜像/退出容器(本机 + Gitea Packages 注册表),只保留当前部署的成品镜像。
|
||||
# 发布方式:git tag v1.2.3 && git push origin v1.2.3(推 main 不再触发)。
|
||||
# 手动触发(Actions → Run workflow)可用 job 选项单跑某个任务:all(全链路)/ image / deploy / cleanup。
|
||||
# 零第三方 action:run 步骤直连 git/docker,runner 无需访问 github.com。
|
||||
# 命名约定:自定义变量/密钥一律 INNEV_ 前缀(仓库 → 设置 → Actions 配置,清单见本文件尾部注释)。
|
||||
# 凭证回退链:INNEV_REGISTRY_USER / INNEV_REGISTRY_TOKEN → gitea.actor / 内置 GITHUB_TOKEN;
|
||||
# INNEV_CI_TOKEN → GITHUB_TOKEN。加了即生效。
|
||||
# 镜像 tag = 版本号(git tag,如 v1.2.3)+ latest;手动 dispatch 无 tag 上下文时回退提交短 sha。
|
||||
name: docker-deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
job:
|
||||
description: 要运行的任务(all = 构建+部署+清理)
|
||||
type: choice
|
||||
options: [all, image, deploy, cleanup]
|
||||
default: all
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read # git clone 源码(内置 GITHUB_TOKEN)
|
||||
packages: write # 推 Gitea Packages(内置 GITHUB_TOKEN)
|
||||
|
||||
env:
|
||||
INNEV_REGISTRY: ${{ vars.INNEV_REGISTRY || 'livebook:8418' }}
|
||||
INNEV_IMAGE_PREFIX: ${{ vars.INNEV_IMAGE_PREFIX || 'livebook:8418/projects' }}
|
||||
# docker.io 加速镜像(逗号分隔)——buildx docker-container 驱动的 buildkitd 不继承 runner 宿主
|
||||
# daemon.json 的 registry-mirrors,必须经 buildkitd.toml 注入
|
||||
INNEV_DOCKERHUB_MIRRORS: ${{ vars.INNEV_DOCKERHUB_MIRRORS || 'docker.fnnas.com,registry.docker-cn.com,registry.hub.docker.com,docker.1panel.live,docker.1panel.dev,docker.1ms.run' }}
|
||||
INNEV_HOST_PORT: ${{ vars.INNEV_HOST_PORT || '{{HOST_PORT}}' }}
|
||||
# 服务器上单独维护的 .env 绝对路径(key 与仓库 .env.example 一致);.env 不进 git/镜像
|
||||
INNEV_ENV_FILE: ${{ vars.INNEV_ENV_FILE || '/etc/{{PROJECT}}/.env' }}
|
||||
|
||||
jobs:
|
||||
# 任务 1:构建镜像并推送到本机 Gitea Packages($INNEV_IMAGE_PREFIX/{{PROJECT}})
|
||||
# push 或手动选择 all/image 时运行
|
||||
image:
|
||||
if: github.event_name == 'push' || github.event.inputs.job == 'all' || github.event.inputs.job == 'image'
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
# 仓库变量误带 http:// scheme 时在此剥掉,避免拼出非法镜像引用(docker tag 不允许 "://")
|
||||
- name: 归一化镜像变量(剥离误配的 URL scheme)
|
||||
run: |
|
||||
PREFIX="${INNEV_IMAGE_PREFIX#*://}"; PREFIX="${PREFIX%/}"
|
||||
echo "INNEV_REGISTRY=${INNEV_REGISTRY#*://}" >> "$GITHUB_ENV"
|
||||
echo "INNEV_IMAGE_PREFIX=$PREFIX" >> "$GITHUB_ENV"
|
||||
# 镜像 tag:tag 推送取版本号(如 v1.2.3);手动 dispatch(branch 上下文)回退提交短 sha
|
||||
if [ "$GITHUB_REF_TYPE" = "tag" ]; then TAG="$GITHUB_REF_NAME"; else TAG="${GITHUB_SHA:0:8}"; fi
|
||||
echo "IMAGE_TAG=$TAG" >> "$GITHUB_ENV"
|
||||
# 日志开头明示推送目标,变量配错时第一时间可见
|
||||
echo "构建目标镜像:$PREFIX/{{PROJECT}}:$TAG"
|
||||
|
||||
- name: 取出源码(免 actions/checkout,纯 git)
|
||||
env:
|
||||
GIT_TOKEN: ${{ secrets.INNEV_CI_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git clone --depth 1 --branch "$GITHUB_REF_NAME" \
|
||||
"http://x-access-token:${GIT_TOKEN}@${INNEV_REGISTRY#*://}/${{ gitea.repository }}.git" src 2>&1 | grep -v "$GIT_TOKEN" || true
|
||||
[ -f src/Dockerfile ] || { echo "源码克隆失败"; exit 1; }
|
||||
cd src && git checkout "$GITHUB_SHA"
|
||||
|
||||
- name: 登录 Gitea Packages
|
||||
env:
|
||||
LOGIN_USER: ${{ secrets.INNEV_REGISTRY_USER || gitea.actor }}
|
||||
LOGIN_TOKEN: ${{ secrets.INNEV_REGISTRY_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: echo "$LOGIN_TOKEN" | docker login "$INNEV_REGISTRY" -u "$LOGIN_USER" --password-stdin
|
||||
|
||||
# buildkitd.toml 里对 $INNEV_REGISTRY 声明 http=true(强制明文 HTTP):buildkitd 不继承宿主
|
||||
# daemon.json 的 insecure-registries,纯 HTTP registry 不声明会在 push 阶段失败。
|
||||
# 每次重建构建器以确保 buildkitd.toml 变更生效(无外部层缓存,每次构建均为全量)。
|
||||
- name: 创建 buildx 构建器(注入 docker.io 加速镜像 + 推送目标 HTTP 声明)
|
||||
run: |
|
||||
docker buildx rm -f {{PROJECT}}builder 2>/dev/null || true
|
||||
{
|
||||
echo '[registry."docker.io"]'
|
||||
printf '%s' "$INNEV_DOCKERHUB_MIRRORS" | sed 's/[^,]*/"&"/g; s/,/, /g' | sed 's/^/ mirrors = [/; s/$/]/'
|
||||
echo ''
|
||||
echo "[registry.\"$INNEV_REGISTRY\"]"
|
||||
echo ' http = true'
|
||||
} > buildkitd.toml
|
||||
cat buildkitd.toml
|
||||
docker buildx create --use --name {{PROJECT}}builder --config buildkitd.toml
|
||||
|
||||
# [OPTIONAL:PRE_BUILD] 项目有特殊预构建步骤(如从源码构建工具二进制)时取消注释并改写本步,
|
||||
# 否则连同标记整块删除。插入位置必须在"构建并推送镜像"步之前。
|
||||
# - name: 预构建(按项目需要改写)
|
||||
# run: cd src && bash scripts/<your-pre-build>.sh
|
||||
# [/OPTIONAL]
|
||||
|
||||
# 构建上下文 = 仓库根(根级 Dockerfile)。不使用层缓存(每次全量构建)。
|
||||
# 单架构原生构建——部署目标即本机,无需 QEMU 多架构。
|
||||
- name: 构建并推送 {{PROJECT}} 镜像
|
||||
run: |
|
||||
docker buildx build --push \
|
||||
--label org.opencontainers.image.source="http://$INNEV_REGISTRY/${{ gitea.repository }}" \
|
||||
-t "$INNEV_IMAGE_PREFIX/{{PROJECT}}:$IMAGE_TAG" \
|
||||
-t "$INNEV_IMAGE_PREFIX/{{PROJECT}}:latest" \
|
||||
src
|
||||
|
||||
# docker-container 驱动的构建器会常驻一个 buildx_buildkit_{{PROJECT}}builder0 容器,用完即删(无论成败)。
|
||||
- name: 清理 buildx 构建器容器
|
||||
if: always()
|
||||
run: docker buildx rm -f {{PROJECT}}builder 2>/dev/null || true
|
||||
|
||||
# 任务 2:把刚推送的镜像拉到本机 docker,重建并启动容器
|
||||
# push 或手动选择 all/deploy 时运行;自定义 if 取代默认 success() 门控——
|
||||
# 手动单跑 deploy 时 image 被跳过(needs 结果为 skipped),此处显式放行
|
||||
deploy:
|
||||
needs: image
|
||||
if: ${{ !failure() && !cancelled() && (github.event_name == 'push' || github.event.inputs.job == 'all' || github.event.inputs.job == 'deploy') }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: 归一化镜像变量(剥离误配的 URL scheme)
|
||||
run: |
|
||||
PREFIX="${INNEV_IMAGE_PREFIX#*://}"; PREFIX="${PREFIX%/}"
|
||||
echo "INNEV_REGISTRY=${INNEV_REGISTRY#*://}" >> "$GITHUB_ENV"
|
||||
echo "INNEV_IMAGE_PREFIX=$PREFIX" >> "$GITHUB_ENV"
|
||||
if [ "$GITHUB_REF_TYPE" = "tag" ]; then TAG="$GITHUB_REF_NAME"; else TAG="${GITHUB_SHA:0:8}"; fi
|
||||
echo "IMAGE_TAG=$TAG" >> "$GITHUB_ENV"
|
||||
|
||||
# docker pull/login 走宿主 daemon:LAN 明文 registry 需在宿主 daemon.json 登记
|
||||
# insecure-registries(见文件尾部注释),或把 INNEV_IMAGE_PREFIX 配成 TLS 地址。
|
||||
- name: 登录并拉取镜像
|
||||
env:
|
||||
LOGIN_USER: ${{ secrets.INNEV_REGISTRY_USER || gitea.actor }}
|
||||
LOGIN_TOKEN: ${{ secrets.INNEV_REGISTRY_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
IMAGE="$INNEV_IMAGE_PREFIX/{{PROJECT}}:$IMAGE_TAG"
|
||||
echo "$LOGIN_TOKEN" | docker login "$INNEV_REGISTRY" -u "$LOGIN_USER" --password-stdin
|
||||
docker pull "$IMAGE"
|
||||
|
||||
# 运行期环境变量来自服务器上单独维护的 .env(key 与仓库 .env.example 一致),
|
||||
# 路径由变量 INNEV_ENV_FILE 指定;.env 不进 git/镜像,仅在此做 --env-file 注入。
|
||||
- name: 重建 {{PROJECT}} 容器
|
||||
run: |
|
||||
IMAGE="$INNEV_IMAGE_PREFIX/{{PROJECT}}:$IMAGE_TAG"
|
||||
[ -f "$INNEV_ENV_FILE" ] || { echo "::error::服务器上未找到 $INNEV_ENV_FILE——请按 .env.example 的 key 创建,或用仓库变量 INNEV_ENV_FILE 指向实际路径"; exit 1; }
|
||||
# [OPTIONAL:ENV_KEY_CHECKS] 项目有硬性必需的 .env key 时保留本块(按 .env.example 渲染,
|
||||
# 每个 key 一行 grep 检查),否则连同标记整块删除。示例:
|
||||
# grep -q '^DATABASE_URL=.' "$INNEV_ENV_FILE" || { echo "::error::$INNEV_ENV_FILE 缺少 DATABASE_URL"; exit 1; }
|
||||
# [/OPTIONAL]
|
||||
docker rm -f {{PROJECT}} 2>/dev/null || true
|
||||
# 靠后的 -e 覆盖 --env-file 同名变量(容器端口固定 {{APP_PORT}},不受 .env 影响)。
|
||||
# [OPTIONAL:EXTRA_ENV] 运行时镜像要求的额外 -e 参数(如 runtime-node 的
|
||||
# -e APP_ENTRY=.next/standalone/server.js)按需添加,无则连同标记删除。
|
||||
# [/OPTIONAL]
|
||||
# [OPTIONAL:ADD_HOST] 容器内需解析宿主/同机服务主机名(如数据库指向宿主)时保留本行
|
||||
# 并按需追加项目专用主机名(如 --add-host livebook:host-gateway),否则连同标记删除。
|
||||
# 注意:容器经 host-gateway 访问的是同机 docker 发布端口(如 5433),
|
||||
# 宿主系统服务端口(如 pg 5432)从容器访问不通——实测踩坑。
|
||||
# [/OPTIONAL]
|
||||
docker run -d --name {{PROJECT}} --restart unless-stopped \
|
||||
-p "${INNEV_HOST_PORT}:{{APP_PORT}}" \
|
||||
--env-file "$INNEV_ENV_FILE" \
|
||||
-e HOSTNAME=0.0.0.0 \
|
||||
-e PORT={{APP_PORT}} \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
"$IMAGE"
|
||||
|
||||
# [OPTIONAL:HEALTH_CHECK] 项目有健康检查端点时保留本步(渲染 {{HEALTH_PATH}}),
|
||||
# 没有则连同标记整块删除,并在汇报中显式声明"无健康检查,部署后需人工确认"。
|
||||
# runtime-node 类容器启动时要先 install+build(数分钟),健康检查窗口放宽到 ~7.5 分钟,
|
||||
# 日志可见 install/build 进度,别当挂起杀掉;预构建产物类镜像可把 90 次循环缩短。
|
||||
- name: 健康检查
|
||||
run: |
|
||||
for i in $(seq 1 90); do
|
||||
if curl -fsS "http://127.0.0.1:${INNEV_HOST_PORT}{{HEALTH_PATH}}"; then
|
||||
echo ""
|
||||
echo "部署完成:http://127.0.0.1:${INNEV_HOST_PORT}(镜像 $INNEV_IMAGE_PREFIX/{{PROJECT}}:$IMAGE_TAG)"
|
||||
exit 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
echo "::error::健康检查失败,容器最近日志:" >&2
|
||||
docker logs --tail 50 {{PROJECT}} >&2 || true
|
||||
exit 1
|
||||
# [/OPTIONAL]
|
||||
|
||||
# 任务 3:清理旧版本产物,只保留当前部署的成品镜像(当前版本 tag + latest,二者同 ID)。
|
||||
# tag 全链路在部署成功后自动跑;也可手动单跑(job=cleanup,保留当前 ref 解析出的镜像 tag)。
|
||||
# 只动 $INNEV_IMAGE_PREFIX/{{PROJECT}} 名下的 tag 与名为 {{PROJECT}} 的退出容器——
|
||||
# 本机还跑着其他项目的镜像与容器,不做全局 prune。
|
||||
cleanup:
|
||||
needs: deploy
|
||||
if: ${{ !failure() && !cancelled() && (github.event.inputs.job == 'cleanup' || needs.deploy.result == 'success') }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: 归一化镜像变量(剥离误配的 URL scheme)
|
||||
run: |
|
||||
PREFIX="${INNEV_IMAGE_PREFIX#*://}"; PREFIX="${PREFIX%/}"
|
||||
echo "INNEV_REGISTRY=${INNEV_REGISTRY#*://}" >> "$GITHUB_ENV"
|
||||
echo "INNEV_IMAGE_PREFIX=$PREFIX" >> "$GITHUB_ENV"
|
||||
if [ "$GITHUB_REF_TYPE" = "tag" ]; then TAG="$GITHUB_REF_NAME"; else TAG="${GITHUB_SHA:0:8}"; fi
|
||||
echo "IMAGE_TAG=$TAG" >> "$GITHUB_ENV"
|
||||
|
||||
- name: 清理本机旧镜像与退出容器
|
||||
run: |
|
||||
KEEP="$INNEV_IMAGE_PREFIX/{{PROJECT}}:$IMAGE_TAG"
|
||||
# 旧版本 tag:容器按镜像 ID 引用,删 tag 不影响正在运行的容器;
|
||||
# 若某 tag 仍被容器独占引用,docker rmi 会拒绝,|| true 跳过
|
||||
docker images "$INNEV_IMAGE_PREFIX/{{PROJECT}}" --format '{{.Repository}}:{{.Tag}}' | while read -r ref; do
|
||||
case "$ref" in
|
||||
"$KEEP" | "$INNEV_IMAGE_PREFIX/{{PROJECT}}:latest") echo "保留 $ref" ;;
|
||||
*) echo "删除 $ref"; docker rmi "$ref" || true ;;
|
||||
esac
|
||||
done
|
||||
# 退出/未启动的 {{PROJECT}} 容器(正常不会有——deploy 已 docker rm -f;name 过滤加锚点,
|
||||
# 避免误伤 buildx_buildkit_{{PROJECT}}builder0 这类名字包含 {{PROJECT}} 的容器)
|
||||
docker ps -aq --filter 'name=^{{PROJECT}}$' --filter status=exited --filter status=created | xargs -r docker rm
|
||||
echo "当前保留的 {{PROJECT}} 镜像:"
|
||||
docker images "$INNEV_IMAGE_PREFIX/{{PROJECT}}"
|
||||
|
||||
# 注册表侧同步清理:本机删 tag 不影响 Gitea Packages 里已推送的旧版本,版本号模型下
|
||||
# 每个 tag 都会在 Packages 堆积,必须经 API 删除。列 tag 走 OCI Registry API(/v2/.../tags/list),
|
||||
# 删除走 Gitea Packages API(DELETE /api/v1/packages/<owner>/<type>/<name>/<version>)。
|
||||
# best-effort:全程 || true,token 缺 scope 或 API 不通只告警不阻塞流水线。
|
||||
- name: 清理 Gitea Packages 旧版本(best-effort)
|
||||
env:
|
||||
LOGIN_USER: ${{ secrets.INNEV_REGISTRY_USER || gitea.actor }}
|
||||
LOGIN_TOKEN: ${{ secrets.INNEV_REGISTRY_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
REPO_PATH="${INNEV_IMAGE_PREFIX#$INNEV_REGISTRY/}/{{PROJECT}}" # 如 projects/{{PROJECT}}
|
||||
OWNER="${INNEV_IMAGE_PREFIX#$INNEV_REGISTRY/}"; OWNER="${OWNER%%/*}" # 如 projects
|
||||
TAGS=$(curl -fsS -u "$LOGIN_USER:$LOGIN_TOKEN" \
|
||||
"http://$INNEV_REGISTRY/v2/$REPO_PATH/tags/list" \
|
||||
| grep -o '"[^"]*"' | grep -v '^"name"$' | grep -v '^"tags"$' | tr -d '"' \
|
||||
| grep -v "^$REPO_PATH$") || { echo "::warning::列出注册表 tag 失败,跳过注册表清理"; exit 0; }
|
||||
for tag in $TAGS; do
|
||||
case "$tag" in
|
||||
"$IMAGE_TAG" | latest) echo "注册表保留 $tag" ;;
|
||||
*) echo "注册表删除 $tag"
|
||||
curl -fsS -X DELETE -u "$LOGIN_USER:$LOGIN_TOKEN" \
|
||||
"http://$INNEV_REGISTRY/api/v1/packages/$OWNER/container/{{PROJECT}}/$tag" \
|
||||
|| echo "::warning::删除注册表版本 $tag 失败(token 需 write:package scope)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
# 一次性人工准备:
|
||||
# 1. runner 用实例级(标签 self-hosted),与 Docker 同机、可直接执行宿主 docker 命令
|
||||
# (job 里的 docker run/pull 操作的就是部署目标机的 daemon)。
|
||||
# 2. deploy job 的 docker pull/login 走宿主 daemon:LAN 明文 registry(如 livebook:8418)需在宿主
|
||||
# /etc/docker/daemon.json 的 insecure-registries 中登记并重启 docker——推送侧已由 buildkitd.toml
|
||||
# http=true 覆盖,不受此限;或将 INNEV_IMAGE_PREFIX 配成 TLS 地址(如 code.innev.cn/...)。
|
||||
# 3. 仓库 → 设置 → 仓库 → 功能(Units)中启用 Actions。
|
||||
# 4. 首次运行后到 Packages 确认镜像包 {{PROJECT}} 已生成。
|
||||
# 5. 数据库迁移与种子数据(如适用)为手动步骤——镜像不含迁移工具,在本地 checkout 里把
|
||||
# 数据库地址指向同一库后执行项目自己的迁移命令。
|
||||
# 6. 在部署服务器上按仓库 .env.example 的 key 新建专用 .env(默认路径 /etc/{{PROJECT}}/.env,
|
||||
# 可用仓库变量 INNEV_ENV_FILE 改指别处)。
|
||||
# 数据库地址指向宿主/同机服务时,容器内由 --add-host ...:host-gateway 解析;
|
||||
# 注意访问的是同机 docker 发布端口(如 5433),宿主系统服务端口(如 pg 5432)从容器经
|
||||
# host-gateway 访问不通——实测踩坑,切勿写错。
|
||||
# 7. 日常发布:git tag v1.2.3 && git push origin v1.2.3(tag 需 v 开头才会触发流水线);
|
||||
# 部署成功后 cleanup 自动删除本机与 Gitea Packages 里的旧版本镜像,无需人工干预。
|
||||
#
|
||||
# 仓库 → 设置 → Actions 配置清单
|
||||
# 变量(Variables):
|
||||
# INNEV_IMAGE_PREFIX 镜像前缀(推送/拉取地址),默认 livebook:8418/projects——
|
||||
# 镜像全名 $INNEV_IMAGE_PREFIX/{{PROJECT}}:<版本tag|latest>
|
||||
# INNEV_REGISTRY registry / git 端点,默认 livebook:8418(与 Gitea 同机,LAN 明文 HTTP)
|
||||
# INNEV_DOCKERHUB_MIRRORS docker.io 加速镜像(逗号分隔),默认 docker.fnnas.com,registry.docker-cn.com,
|
||||
# registry.hub.docker.com,docker.1panel.live,docker.1panel.dev,docker.1ms.run
|
||||
# INNEV_HOST_PORT 容器对外发布端口,默认 {{HOST_PORT}}(容器内固定 {{APP_PORT}})
|
||||
# INNEV_ENV_FILE 服务器上 .env 的绝对路径,默认 /etc/{{PROJECT}}/.env(见上方准备 6)
|
||||
# 密钥(Secrets):
|
||||
# INNEV_REGISTRY_USER docker login 账号——跨命名空间推包时必需(同命名空间可回退 gitea.actor)
|
||||
# INNEV_REGISTRY_TOKEN docker login token(scope: write:package + read:package);
|
||||
# cleanup 的注册表旧版本删除也用它,缺 write:package 时该步只告警不阻塞
|
||||
# INNEV_CI_TOKEN git clone token(scope: read:repository);仅内置 GITHUB_TOKEN 不足时添加
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
# check-workflow.sh — gitea-actions 统一规范自检(零依赖 bash+grep;YAML 语法校验需 python3+pyyaml,缺则跳过)
|
||||
# 用法: check-workflow.sh <workflow.yaml>
|
||||
# 逐项输出 PASS/FAIL/SKIP,有任一 FAIL 即 exit 1。
|
||||
set -u
|
||||
|
||||
FILE="${1:-}"
|
||||
fail=0
|
||||
|
||||
say() { printf '%s\n' "$*"; }
|
||||
pass() { say "PASS $1"; }
|
||||
bad() { say "FAIL $1"; fail=1; }
|
||||
skip() { say "SKIP $1"; }
|
||||
|
||||
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
|
||||
say "用法: $0 <workflow.yaml>"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ① 零第三方 action(内网 runner 无法访问 github.com)
|
||||
if grep -qE '^[[:space:]]*-?[[:space:]]*uses:' "$FILE"; then
|
||||
bad "① 发现 uses:(第三方 action)——规范要求零第三方 action,run 步骤直连 git/docker"
|
||||
else
|
||||
pass "① 零第三方 action(无 uses:)"
|
||||
fi
|
||||
|
||||
# ② 仅 push v* tag 触发(版本号即镜像 tag,推 main 不发版)
|
||||
if grep -qE "tags:[[:space:]]*\[?[[:space:]]*['\"]?v\*" "$FILE"; then
|
||||
pass "② push tag v* 触发"
|
||||
else
|
||||
bad "② 缺少 push tags 'v*' 触发"
|
||||
fi
|
||||
|
||||
# ③ workflow_dispatch 带 job 单跑选项(all/image/deploy/cleanup)
|
||||
if grep -q 'workflow_dispatch:' "$FILE" \
|
||||
&& grep -qE 'options:[[:space:]]*\[.*all.*image.*deploy.*cleanup' "$FILE"; then
|
||||
pass "③ workflow_dispatch 带 job 选项(all/image/deploy/cleanup)"
|
||||
else
|
||||
bad "③ workflow_dispatch 缺 job 单跑选项"
|
||||
fi
|
||||
|
||||
# ④ buildkitd.toml 注入 registry http=true(buildkitd 不继承宿主 insecure-registries)
|
||||
if grep -q 'buildkitd.toml' "$FILE" && grep -qE 'http[[:space:]]*=[[:space:]]*true' "$FILE"; then
|
||||
pass "④ buildkitd.toml 注入 registry http=true"
|
||||
else
|
||||
bad "④ 缺 buildkitd.toml http=true 注入(LAN 明文 registry 推送阶段必失败)"
|
||||
fi
|
||||
|
||||
# ⑤ docker login 走 --password-stdin(密码不进 argv/日志)
|
||||
if grep -q 'docker login' "$FILE"; then
|
||||
if grep -q 'docker login.*--password-stdin' "$FILE"; then
|
||||
pass "⑤ docker login 走 --password-stdin"
|
||||
else
|
||||
bad "⑤ docker login 未用 --password-stdin(口令会进 argv 与日志)"
|
||||
fi
|
||||
else
|
||||
skip "⑤ 未发现 docker login"
|
||||
fi
|
||||
|
||||
# ⑥ cleanup 容器名过滤带 ^...$ 锚点(防误删名字包含项目名的邻项目容器)
|
||||
if grep -qE "name=\^[^'\"]*\\$" "$FILE"; then
|
||||
pass "⑥ cleanup name 过滤带 ^...\$ 锚点"
|
||||
else
|
||||
bad "⑥ cleanup name 过滤缺 ^...\$ 锚点(会误伤 buildx_buildkit_* 等容器)"
|
||||
fi
|
||||
|
||||
# ⑦ 无明文密钥特征(常见 token 前缀 / 40 位 hex / URL 内嵌静态口令)
|
||||
if grep -nE 'ghp_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{20,}|\b[a-f0-9]{40}\b' "$FILE"; then
|
||||
bad "⑦ 发现疑似明文 token——密钥必须走 secrets 引用"
|
||||
elif grep -E '://[^/$"'"'"'{ ]+:[^/$"'"'"'{@ ]+@' "$FILE" | grep -vq 'x-access-token'; then
|
||||
bad "⑦ 发现 URL 内嵌静态口令——token 必须走 env 变量引用"
|
||||
else
|
||||
pass "⑦ 无明文密钥特征"
|
||||
fi
|
||||
|
||||
# ⑧ YAML 语法(python3+pyyaml 可用时;不可用则跳过并声明)
|
||||
if command -v python3 >/dev/null 2>&1 && python3 -c 'import yaml' >/dev/null 2>&1; then
|
||||
if python3 -c 'import sys, yaml; yaml.safe_load(open(sys.argv[1], encoding="utf-8"))' "$FILE" 2>/dev/null; then
|
||||
pass "⑧ YAML 语法合法"
|
||||
else
|
||||
bad "⑧ YAML 语法错误"
|
||||
fi
|
||||
else
|
||||
skip "⑧ python3+pyyaml 不可用,YAML 语法未校验"
|
||||
fi
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
say "---"
|
||||
say "规范自检未通过:存在 FAIL 项,逐条修正后重跑。"
|
||||
exit 1
|
||||
fi
|
||||
say "---"
|
||||
say "规范自检通过(SKIP 项需人工知悉)。"
|
||||
Reference in new issue
Block a user