diff --git a/README.md b/README.md index 068e39b..c17a666 100644 --- a/README.md +++ b/README.md @@ -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 → 本机重建容器 → 清理旧版本) | ## 目录结构 diff --git a/skills/gitea-actions/SKILL.md b/skills/gitea-actions/SKILL.md new file mode 100644 index 0000000..07d9730 --- /dev/null +++ b/skills/gitea-actions/SKILL.md @@ -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 仓库维护) diff --git a/skills/gitea-actions/references/docker-deploy-template.yaml b/skills/gitea-actions/references/docker-deploy-template.yaml new file mode 100644 index 0000000..33cf95b --- /dev/null +++ b/skills/gitea-actions/references/docker-deploy-template.yaml @@ -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/.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////)。 + # 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 不足时添加 diff --git a/skills/gitea-actions/scripts/check-workflow.sh b/skills/gitea-actions/scripts/check-workflow.sh new file mode 100644 index 0000000..74e4571 --- /dev/null +++ b/skills/gitea-actions/scripts/check-workflow.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# check-workflow.sh — gitea-actions 统一规范自检(零依赖 bash+grep;YAML 语法校验需 python3+pyyaml,缺则跳过) +# 用法: check-workflow.sh +# 逐项输出 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 " + 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 项需人工知悉)。"