chore: 初始化 opencode 配置
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
package.json
|
||||||
|
bun.lock
|
||||||
|
*.log
|
||||||
|
env.sh
|
||||||
|
bin/opencode
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
171
AGENTS.md
Normal file
171
AGENTS.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# 全局规则
|
||||||
|
|
||||||
|
## 缩写说明
|
||||||
|
|
||||||
|
- **oc** = opencode(OpenCode 的缩写)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 语言偏好
|
||||||
|
|
||||||
|
**重要:请始终使用中文(简体中文)与用户交流。**
|
||||||
|
|
||||||
|
所有回复、解释、错误消息、建议和对话都应该使用中文,除非:
|
||||||
|
- 代码注释需要使用英文(根据项目规范)
|
||||||
|
- 用户明确要求使用其他语言
|
||||||
|
- 技术术语在中文中没有合适的翻译(可以中英文混用,如 "React Hooks")
|
||||||
|
|
||||||
|
### 重要注意事项
|
||||||
|
|
||||||
|
1. **不要因为文档、命令或代码是英文就切换语言**
|
||||||
|
- 即使项目文档是英文的,回复时仍然用中文解释
|
||||||
|
- 即使执行的命令是英文的,回复时仍然用中文说明
|
||||||
|
- 即使错误信息是英文的,回复时仍然用中文分析
|
||||||
|
|
||||||
|
2. **工具输出可以保持原语言**
|
||||||
|
- 代码本身使用英文(函数名、变量名等)
|
||||||
|
- 终端命令使用英文
|
||||||
|
- 但你对这些内容的解释和说明必须用中文
|
||||||
|
|
||||||
|
3. **用户交互始终用中文**
|
||||||
|
- 所有对话、提问、确认都用中文
|
||||||
|
- 所有建议、总结、分析都用中文
|
||||||
|
- 所有错误解释和问题诊断都用中文
|
||||||
|
|
||||||
|
这是用户的个人偏好设置,适用于所有项目和所有交互场景。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码风格偏好
|
||||||
|
|
||||||
|
### JavaScript/TypeScript
|
||||||
|
- **缩进**:2空格
|
||||||
|
- **分号**:使用分号
|
||||||
|
- **引号**:优先使用双引号
|
||||||
|
- **注释**:
|
||||||
|
- 使用 JSDoc 格式书写公开函数文档
|
||||||
|
- 复杂逻辑使用自然语言注释
|
||||||
|
- 中文注释需说明 "为什么",而不只是 "做什么"
|
||||||
|
|
||||||
|
### 一般规则
|
||||||
|
- 命名采用 camelCase(变量/函数)、PascalCase(类/组件)、UPPER_SNAKE_CASE(常量)
|
||||||
|
- 避免使用单字母变量,除非在循环中
|
||||||
|
- 函数长度不超过 100 行,优先拆分
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开发流程
|
||||||
|
|
||||||
|
### Git 提交规范
|
||||||
|
- **格式**:`<type>(<scope>): <subject>`
|
||||||
|
- **类型**:feat(功能)、fix(修复)、docs(文档)、style(格式)、refactor(重构)、test(测试)、chore(维护)
|
||||||
|
- **主题**:使用中文,简洁明了,不超过 50 字
|
||||||
|
- **描述**:如有需要,空一行后补充详细说明
|
||||||
|
|
||||||
|
### 分支命名
|
||||||
|
- 功能分支:`feature/描述`
|
||||||
|
- 修复分支:`fix/描述`
|
||||||
|
- 文档分支:`docs/描述`
|
||||||
|
- 示例:`feature/user-authentication`、`fix/memory-leak`
|
||||||
|
|
||||||
|
### 代码审查
|
||||||
|
- 提交 PR 前必须确保本地测试通过
|
||||||
|
- PR 需要至少 1 个审查者批准
|
||||||
|
- 修复所有 linter 和类型检查错误
|
||||||
|
|
||||||
|
### 发布流程
|
||||||
|
- 主分支(main/master)始终保持可部署状态
|
||||||
|
- 不直接推送到主分支,必须通过 PR
|
||||||
|
- 标签格式:`v<major>.<minor>.<patch>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OpenCode 工具使用策略
|
||||||
|
|
||||||
|
### 什么时候使用各个工具
|
||||||
|
|
||||||
|
**Task 工具**(优先使用)
|
||||||
|
- 探索复杂、陌生的代码库结构
|
||||||
|
- 执行多步骤的分析任务
|
||||||
|
- 需要全局视野的搜索和研究
|
||||||
|
- 复杂的代码重构计划
|
||||||
|
|
||||||
|
**Bash 工具**
|
||||||
|
- 执行系统命令和 git 操作
|
||||||
|
- 运行构建、测试、打包命令
|
||||||
|
- 文件系统操作(只在必要时)
|
||||||
|
- 不要用 Bash 做文件读写,改用 Read/Edit/Write 工具
|
||||||
|
|
||||||
|
**Read/Edit/Write 工具**
|
||||||
|
- 读取文件内容:优先用 Read,不用 cat/head/tail
|
||||||
|
- 修改现有文件:用 Edit,不用 sed/awk
|
||||||
|
- 创建新文件:用 Write,不用 echo 或 heredoc
|
||||||
|
|
||||||
|
**Glob/Grep 工具**
|
||||||
|
- 精准查找特定文件:用 Glob(pattern 匹配)
|
||||||
|
- 搜索文件内容:用 Grep(正则表达式)
|
||||||
|
- 避免用 Bash 的 find 和 grep 命令
|
||||||
|
|
||||||
|
**TodoWrite 工具**(重要)
|
||||||
|
- 任务超过 3 步时必须创建 todo 列表
|
||||||
|
- 立即标记完成,不要批量完成
|
||||||
|
- 只有 1 个 todo 保持 in_progress 状态
|
||||||
|
- 用于任务规划、跟踪进度、防止遗漏
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码质量标准
|
||||||
|
|
||||||
|
### 测试要求
|
||||||
|
- **单元测试**:新功能必须包含测试
|
||||||
|
- **集成测试**:API 和跨模块交互需要测试
|
||||||
|
- **目标覆盖率**:>= 80%
|
||||||
|
- 运行测试:提交前必须本地通过全部测试
|
||||||
|
|
||||||
|
### 构建和类型检查
|
||||||
|
- **类型检查必须通过**:严格模式下零错误
|
||||||
|
- **构建必须成功**:无警告或错误
|
||||||
|
- **Linter 检查**:修复所有警告,至少遵守 recommended 规则
|
||||||
|
- **运行时验证**:不允许 `any` 类型,除非有明确注释说明原因
|
||||||
|
|
||||||
|
### 性能标准
|
||||||
|
- **首屏加载**:< 3 秒(首次)、< 1 秒(缓存)
|
||||||
|
- **包体积**:监控 bundle size,新增功能不超过 50KB
|
||||||
|
- **内存泄漏**:定期检查,不允许持久化内存增长
|
||||||
|
|
||||||
|
### 安全要求
|
||||||
|
- 不提交敏感信息(.env、密钥等)到版本控制
|
||||||
|
- 依赖项定期更新,及时修复已知漏洞
|
||||||
|
- 输入验证和输出编码,防止 XSS、注入等攻击
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务管理最佳实践
|
||||||
|
|
||||||
|
### 使用 TodoWrite 的原则
|
||||||
|
1. **提早规划**:接收任务后立即创建 todo 列表
|
||||||
|
2. **分解细节**:复杂任务拆分成 3-5 个可操作的步骤
|
||||||
|
3. **实时更新**:任务完成后立即标记为 completed
|
||||||
|
4. **单个进行**:同时只有 1 个 in_progress,完成后再开始下一个
|
||||||
|
5. **优先级标记**:高优先级任务标记为 high
|
||||||
|
|
||||||
|
### 任务状态定义
|
||||||
|
- **pending**:未开始
|
||||||
|
- **in_progress**:当前正在进行(限 1 个)
|
||||||
|
- **completed**:已完成
|
||||||
|
- **cancelled**:不再需要
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题和快速参考
|
||||||
|
|
||||||
|
### 如何列出所有可用命令?
|
||||||
|
按下 `ctrl+p` 可查看 OpenCode 的所有可用操作。
|
||||||
|
|
||||||
|
### 如何获取帮助或反馈?
|
||||||
|
- 在 GitHub 提交 issue:https://github.com/anomalyco/opencode
|
||||||
|
- 查看官方文档:https://opencode.ai/docs
|
||||||
|
|
||||||
|
### 何时创建新文件 vs 编辑现有文件?
|
||||||
|
- **优先编辑**:修改现有代码和配置
|
||||||
|
- **仅在必要时创建**:添加全新的功能模块或配置文件
|
||||||
240
command/auto-commit.md
Normal file
240
command/auto-commit.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
---
|
||||||
|
description: Auto-generate commit message, commit and create tag
|
||||||
|
agent: build
|
||||||
|
---
|
||||||
|
|
||||||
|
# auto-commit
|
||||||
|
|
||||||
|
Auto-generate commit message for staged files, commit to local repository.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Create tag**: Automatically generate version number and create tag by default
|
||||||
|
- **Skip tag**: Skip creating tag when user inputs "skip" or "skip tag"
|
||||||
|
|
||||||
|
> Version number update is **consistent** with git tag: AI will automatically update version number based on project type and changes, and create tag with the same version number.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Check Staging Area
|
||||||
|
|
||||||
|
Run `git diff --cached --name-only` to check if there are files in staging area.
|
||||||
|
|
||||||
|
**If staging area is empty**:
|
||||||
|
- Output prompt: "No files in staging area, please use `git add` to add files first."
|
||||||
|
- **Terminate command execution**, do not continue
|
||||||
|
|
||||||
|
### 2. Collect Information (Execute in parallel)
|
||||||
|
|
||||||
|
- Run `git status` to view current status
|
||||||
|
- Run `git diff --cached` to view specific changes in staging area
|
||||||
|
- Run `git log --oneline -10` to view recent commit history, understand project's commit style
|
||||||
|
- Run `git tag --list | sort -V | tail -5` to view recent tags
|
||||||
|
- Read `AGENTS.md` file (if exists), understand project type, structure and **version update rules**
|
||||||
|
|
||||||
|
### 3. Detect Repository Type
|
||||||
|
|
||||||
|
**Detect repository type (polyrepo or monorepo)**:
|
||||||
|
|
||||||
|
- Check if `AGENTS.md` file indicates **monorepo**, if `AGENTS.md` doesn't exist or doesn't clearly indicate, default to **polyrepo**
|
||||||
|
|
||||||
|
**If monorepo, analyze change scope**:
|
||||||
|
|
||||||
|
- Based on changed files from step 1, analyze if changes only affect a specific subproject
|
||||||
|
- If only single subproject is affected, record subproject name (e.g., extract `user-service` from path `packages/user-service/src/index.ts`)
|
||||||
|
|
||||||
|
### 4. Detect Project Type and Determine Version Number
|
||||||
|
|
||||||
|
**Prioritize reading version update rules from AGENTS.md**:
|
||||||
|
|
||||||
|
If `AGENTS.md` defines version update rules (such as version file path, version field name, etc.), prioritize using that rule.
|
||||||
|
|
||||||
|
**Auto-detect project type** (when AGENTS.md is not defined):
|
||||||
|
|
||||||
|
AI needs to intelligently identify project type and determine version file, common project types include but not limited to:
|
||||||
|
|
||||||
|
| Project Type | Version File | Version Field/Location |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| iOS | `*.xcodeproj/project.pbxproj` | `MARKETING_VERSION` |
|
||||||
|
| npm/Node.js | `package.json` | `version` |
|
||||||
|
| Android (Groovy) | `app/build.gradle` | `versionName` |
|
||||||
|
| Android (Kotlin DSL) | `app/build.gradle.kts` | `versionName` |
|
||||||
|
| Python (pyproject) | `pyproject.toml` | `[project] version` or `[tool.poetry] version` |
|
||||||
|
| Python (setup) | `setup.py` | `version` parameter |
|
||||||
|
| Rust | `Cargo.toml` | `[package] version` |
|
||||||
|
| Go | Usually only uses git tag | - |
|
||||||
|
| Flutter | `pubspec.yaml` | `version` |
|
||||||
|
| .NET | `*.csproj` | `<Version>` or `<PackageVersion>` |
|
||||||
|
|
||||||
|
> AI should determine project type based on files that actually exist in repository, can check multiple possible version files if necessary.
|
||||||
|
|
||||||
|
**Read current version number**:
|
||||||
|
|
||||||
|
1. Read current version number from identified version file
|
||||||
|
2. Parse as `major.minor.patch`:
|
||||||
|
- If parsing fails or doesn't exist: treat as `0.1.0`
|
||||||
|
- If current version only has `major.minor` format, auto-complete to `major.minor.0`
|
||||||
|
|
||||||
|
**Calculate new version number** (based on change type):
|
||||||
|
|
||||||
|
Version number uses **semantic versioning format**: `0.1.0`, `0.2.0`, ..., `0.9.0`, `1.0.0`, `1.1.0`, `1.1.1`...
|
||||||
|
- **Default starting version**: `0.1.0`
|
||||||
|
- **Increment rules**:
|
||||||
|
- **Major/breaking changes**: `1.2.3` -> `2.0.0` (major +1, minor and patch reset to zero)
|
||||||
|
- **New feature (feat)**: `1.2.3` -> `1.3.0` (minor +1, patch reset to zero)
|
||||||
|
- **Bug fix (fix)**: `1.2.3` -> `1.2.4` (patch +1)
|
||||||
|
|
||||||
|
**Determine if version number update is needed**:
|
||||||
|
|
||||||
|
> Core principle: **Version number reflects "user-perceivable changes"**. Ask yourself: Has the product that users download/use changed?
|
||||||
|
|
||||||
|
**Need to update version number** (packaged into final product, user-perceivable):
|
||||||
|
|
||||||
|
| commit type | version change | description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `feat` | minor +1 | new feature |
|
||||||
|
| `fix` | patch +1 | user-perceivable bug fix |
|
||||||
|
| `perf` | patch +1 | performance optimization (user-perceivable improvement) |
|
||||||
|
| breaking change (`!`) | major +1 | API/protocol incompatible changes |
|
||||||
|
|
||||||
|
**No need to update version number** (doesn't affect final product, user-imperceptible):
|
||||||
|
|
||||||
|
| commit type | description | example |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `chore` | miscellaneous/toolchain | build scripts, CI config, dependency updates |
|
||||||
|
| `ci` | CI/CD config | `.github/workflows/*.yml`, `release.mjs` |
|
||||||
|
| `docs` | documentation | README, comments, API docs |
|
||||||
|
| `test` | test code | unit tests, E2E tests |
|
||||||
|
| `refactor` | refactoring | code reorganization without changing external behavior |
|
||||||
|
| `style` | code style | formatting, spaces, semicolons |
|
||||||
|
| `build` | build config | `tsconfig.json`, `biome.json`, `webpack.config.js` |
|
||||||
|
|
||||||
|
**Judgment tips**:
|
||||||
|
|
||||||
|
1. **Will the file be packaged into final product?**
|
||||||
|
- `scripts/`, `.github/`, `*.config.js` -> No -> Don't update version
|
||||||
|
- `src/`, `lib/`, `app/` -> Yes -> May need to update version
|
||||||
|
|
||||||
|
2. **Does the change only affect developers?**
|
||||||
|
- Dev tool config, CI scripts, build process -> Only affects developers -> Don't update version
|
||||||
|
- Feature code, UI, API -> Affects users -> Update version
|
||||||
|
|
||||||
|
3. **If project type cannot be identified or no version file** -> Only create git tag, don't update version file
|
||||||
|
|
||||||
|
### 5. Generate Commit Message
|
||||||
|
|
||||||
|
Based on changes in staging area, **analyze and generate meaningful commit message by AI**:
|
||||||
|
|
||||||
|
- Analyze changed code content, understand the purpose and intent of this modification
|
||||||
|
- Reference project's commit history style
|
||||||
|
- Use [Conventional Commits](https://www.conventionalcommits.org/) specification
|
||||||
|
- Commit message should concisely but accurately describe "why" rather than just "what"
|
||||||
|
- Common types: `feat` (new feature), `fix` (fix), `docs` (documentation), `refactor` (refactoring), `chore` (miscellaneous), `test` (test), etc.
|
||||||
|
- **Language selection**:
|
||||||
|
- **Default (macOS/Linux)**: Use Chinese (中文) for commit messages
|
||||||
|
- **Windows**: Use English due to encoding issues with Cursor Shell tool
|
||||||
|
- **If monorepo and this commit only affects single subproject, include subproject name in commit message**:
|
||||||
|
- Format: `<type>(<scope>): <description>`, where `<scope>` is subproject name
|
||||||
|
- Example (Chinese): `feat(ios): 支持 OGG Opus 上传` or `fix(electron): 修复剪贴板注入失败`
|
||||||
|
- Example (English): `feat(ios): support OGG Opus upload` or `fix(electron): fix clipboard injection failure`
|
||||||
|
- If affecting multiple subprojects or entire repository, no need to add scope
|
||||||
|
|
||||||
|
### 6. Update Project Version Number
|
||||||
|
|
||||||
|
> Only execute when step 4 determines version number update is needed and version file is identified
|
||||||
|
|
||||||
|
**Execution steps**:
|
||||||
|
|
||||||
|
1. Based on version file and field identified in step 4, update version number to calculated new version
|
||||||
|
2. Add version file to staging area: `git add <version file>`
|
||||||
|
3. **Verify staging area**: Run `git diff --cached --name-only` to confirm version file is in staging area
|
||||||
|
|
||||||
|
### 7. Execute Commit
|
||||||
|
|
||||||
|
Execute commit with generated commit message (staging area now includes user's changes and version number update).
|
||||||
|
|
||||||
|
**Commit message language by platform:**
|
||||||
|
|
||||||
|
**macOS/Linux** (use Chinese commit messages):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Single line commit
|
||||||
|
git commit -m "feat(android): 添加新功能"
|
||||||
|
|
||||||
|
# Multi-line commit (recommended)
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(android): 添加新功能
|
||||||
|
|
||||||
|
- 详细说明 1
|
||||||
|
- 详细说明 2
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows** (use English commit messages):
|
||||||
|
|
||||||
|
Due to Cursor's Shell tool encoding issues on Windows, **must use English** commit messages.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Single line commit
|
||||||
|
git commit -m "feat(android): add new feature"
|
||||||
|
|
||||||
|
# Multi-line commit (use --message multiple times)
|
||||||
|
git commit --message="fix(android): fix code review issues" --message="- Fix syntax error" --message="- Use JSONObject instead of manual JSON"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows prohibited methods** (will cause garbled text):
|
||||||
|
- No Chinese commit messages - Cursor Shell tool encoding error when passing Chinese parameters
|
||||||
|
- No `Out-File -Encoding utf8` - writes UTF-8 with BOM
|
||||||
|
- No Write tool to write temp files - encoding uncontrollable
|
||||||
|
- No PowerShell here-string `@"..."@` - newline parsing issues
|
||||||
|
|
||||||
|
### 8. Create Tag
|
||||||
|
|
||||||
|
> **Executed by default**. Only skip this step when user explicitly inputs "skip" or "skip tag".
|
||||||
|
> Only create tag when step 4 determines version number update is needed (docs/chore types don't create tag).
|
||||||
|
|
||||||
|
**Tag annotation content requirements**:
|
||||||
|
|
||||||
|
- **Use the same content as commit message for tag annotation**
|
||||||
|
- Tag annotation will be used as Release notes by CI/CD scripts, should include detailed change content
|
||||||
|
- If commit message has multiple lines, tag annotation should also be multi-line
|
||||||
|
|
||||||
|
**polyrepo**:
|
||||||
|
```bash
|
||||||
|
git tag -a "<version>" -m "<commit title>" -m "" -m "<commit body line 1>" -m "<commit body line 2>" ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**monorepo**:
|
||||||
|
```bash
|
||||||
|
git tag -a "<subproject>-<version>" -m "<commit title>" -m "" -m "<commit body line 1>" -m "<commit body line 2>" ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples (single line commit):
|
||||||
|
- polyrepo: `git tag -a "1.2.0" -m "feat: add user authentication"`
|
||||||
|
- monorepo: `git tag -a "android-1.2.0" -m "feat(android): add user authentication"`
|
||||||
|
|
||||||
|
Examples (multi-line commit, more recommended):
|
||||||
|
- polyrepo:
|
||||||
|
```bash
|
||||||
|
git tag -a "1.2.1" -m "fix: resolve bluetooth connection timeout" -m "" -m "- Increase connection timeout to 30s" -m "- Add retry mechanism for failed connections" -m "- Improve error messages"
|
||||||
|
```
|
||||||
|
- monorepo:
|
||||||
|
```bash
|
||||||
|
git tag -a "android-1.2.1" -m "fix(android): resolve bluetooth connection timeout" -m "" -m "- Increase connection timeout to 30s" -m "- Add retry mechanism for failed connections" -m "- Improve error messages"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Staging area check first**: Must check if staging area has files first, if not, terminate immediately, don't execute any subsequent operations.
|
||||||
|
- **AGENTS.md priority**: If `AGENTS.md` defines version update rules, prioritize using that rule; otherwise AI auto-detects project type.
|
||||||
|
- **Smart detection**: AI should intelligently determine project type and version file location based on files that actually exist in repository.
|
||||||
|
- **Version number and tag consistency**: Project version number and git tag use the same version number, ensure consistency.
|
||||||
|
- **Version number format**: Use `major.minor.patch` semantic versioning format (e.g., `0.1.0`, `1.1.0`, `1.1.1`), default starting `0.1.0`.
|
||||||
|
- **Create tag by default**: Unless user inputs "skip" or "skip tag", create tag by default.
|
||||||
|
- **Update version before commit**: First determine version number and modify version file, then commit at once, avoid using `--amend`.
|
||||||
|
- **Version file must be verified**: After updating version number and `git add`, must run `git diff --cached --name-only` to confirm version file is in staging area before executing commit.
|
||||||
|
- **Commit message language**: Default use Chinese (macOS/Linux); Windows must use English due to Cursor Shell tool encoding issues.
|
||||||
|
- **monorepo scope**: If staging area only affects single subproject, commit message should have scope; if affecting multiple subprojects, no scope needed.
|
||||||
|
- **monorepo tag format**: Use `<subproject>-<version>` format (e.g., `ios-0.1.0`, `android-1.1.0`).
|
||||||
|
- **No version file case**: If project type cannot be identified or no version file (like pure Go project), only create git tag, don't update any files.
|
||||||
259
command/commit-push.md
Normal file
259
command/commit-push.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
---
|
||||||
|
description: Auto-generate commit, create tag, and push to remote
|
||||||
|
agent: build
|
||||||
|
---
|
||||||
|
|
||||||
|
# commit-push
|
||||||
|
|
||||||
|
Auto-generate commit message for staged files, commit to local repository, then push to remote repository origin.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Create tag**: Automatically generate version number and create tag by default, push to remote
|
||||||
|
- **Skip tag**: Skip creating tag when user inputs "skip" or "skip tag"
|
||||||
|
|
||||||
|
> Version number update is **consistent** with git tag: AI will automatically update version number based on project type and changes, and create tag with the same version number.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Check Staging Area
|
||||||
|
|
||||||
|
Run `git diff --cached --name-only` to check if there are files in staging area.
|
||||||
|
|
||||||
|
**If staging area is empty**:
|
||||||
|
- Output prompt: "No files in staging area, please use `git add` to add files first."
|
||||||
|
- **Terminate command execution**, do not continue
|
||||||
|
|
||||||
|
### 2. Collect Information (Execute in parallel)
|
||||||
|
|
||||||
|
- Run `git status` to view current status
|
||||||
|
- Run `git diff --cached` to view specific changes in staging area
|
||||||
|
- Run `git log --oneline -10` to view recent commit history, understand project's commit style
|
||||||
|
- Run `git tag --list | sort -V | tail -5` to view recent tags
|
||||||
|
- Read `AGENTS.md` file (if exists), understand project type, structure and **version update rules**
|
||||||
|
|
||||||
|
### 3. Detect Repository Type
|
||||||
|
|
||||||
|
**Detect repository type (polyrepo or monorepo)**:
|
||||||
|
|
||||||
|
- Check if `AGENTS.md` file indicates **monorepo**, if `AGENTS.md` doesn't exist or doesn't clearly indicate, default to **polyrepo**
|
||||||
|
|
||||||
|
**If monorepo, analyze change scope**:
|
||||||
|
|
||||||
|
- Based on changed files from step 1, analyze if changes only affect a specific subproject
|
||||||
|
- If only single subproject is affected, record subproject name (e.g., extract `user-service` from path `packages/user-service/src/index.ts`)
|
||||||
|
|
||||||
|
### 4. Detect Project Type and Determine Version Number
|
||||||
|
|
||||||
|
**Prioritize reading version update rules from AGENTS.md**:
|
||||||
|
|
||||||
|
If `AGENTS.md` defines version update rules (such as version file path, version field name, etc.), prioritize using that rule.
|
||||||
|
|
||||||
|
**Auto-detect project type** (when AGENTS.md is not defined):
|
||||||
|
|
||||||
|
AI needs to intelligently identify project type and determine version file, common project types include but not limited to:
|
||||||
|
|
||||||
|
| Project Type | Version File | Version Field/Location |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| iOS | `*.xcodeproj/project.pbxproj` | `MARKETING_VERSION` |
|
||||||
|
| npm/Node.js | `package.json` | `version` |
|
||||||
|
| Android (Groovy) | `app/build.gradle` | `versionName` |
|
||||||
|
| Android (Kotlin DSL) | `app/build.gradle.kts` | `versionName` |
|
||||||
|
| Python (pyproject) | `pyproject.toml` | `[project] version` or `[tool.poetry] version` |
|
||||||
|
| Python (setup) | `setup.py` | `version` parameter |
|
||||||
|
| Rust | `Cargo.toml` | `[package] version` |
|
||||||
|
| Go | Usually only uses git tag | - |
|
||||||
|
| Flutter | `pubspec.yaml` | `version` |
|
||||||
|
| .NET | `*.csproj` | `<Version>` or `<PackageVersion>` |
|
||||||
|
|
||||||
|
> AI should determine project type based on files that actually exist in repository, can check multiple possible version files if necessary.
|
||||||
|
|
||||||
|
**Read current version number**:
|
||||||
|
|
||||||
|
1. Read current version number from identified version file
|
||||||
|
2. Parse as `major.minor.patch`:
|
||||||
|
- If parsing fails or doesn't exist: treat as `0.1.0`
|
||||||
|
- If current version only has `major.minor` format, auto-complete to `major.minor.0`
|
||||||
|
|
||||||
|
**Calculate new version number** (based on change type):
|
||||||
|
|
||||||
|
Version number uses **semantic versioning format**: `0.1.0`, `0.2.0`, ..., `0.9.0`, `1.0.0`, `1.1.0`, `1.1.1`...
|
||||||
|
- **Default starting version**: `0.1.0`
|
||||||
|
- **Increment rules**:
|
||||||
|
- **Major/breaking changes**: `1.2.3` -> `2.0.0` (major +1, minor and patch reset to zero)
|
||||||
|
- **New feature (feat)**: `1.2.3` -> `1.3.0` (minor +1, patch reset to zero)
|
||||||
|
- **Bug fix (fix)**: `1.2.3` -> `1.2.4` (patch +1)
|
||||||
|
|
||||||
|
**Determine if version number update is needed**:
|
||||||
|
|
||||||
|
> Core principle: **Version number reflects "user-perceivable changes"**. Ask yourself: Has the product that users download/use changed?
|
||||||
|
|
||||||
|
**Need to update version number** (packaged into final product, user-perceivable):
|
||||||
|
|
||||||
|
| commit type | version change | description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `feat` | minor +1 | new feature |
|
||||||
|
| `fix` | patch +1 | user-perceivable bug fix |
|
||||||
|
| `perf` | patch +1 | performance optimization (user-perceivable improvement) |
|
||||||
|
| breaking change (`!`) | major +1 | API/protocol incompatible changes |
|
||||||
|
|
||||||
|
**No need to update version number** (doesn't affect final product, user-imperceptible):
|
||||||
|
|
||||||
|
| commit type | description | example |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `chore` | miscellaneous/toolchain | build scripts, CI config, dependency updates |
|
||||||
|
| `ci` | CI/CD config | `.github/workflows/*.yml`, `release.mjs` |
|
||||||
|
| `docs` | documentation | README, comments, API docs |
|
||||||
|
| `test` | test code | unit tests, E2E tests |
|
||||||
|
| `refactor` | refactoring | code reorganization without changing external behavior |
|
||||||
|
| `style` | code style | formatting, spaces, semicolons |
|
||||||
|
| `build` | build config | `tsconfig.json`, `biome.json`, `webpack.config.js` |
|
||||||
|
|
||||||
|
**Judgment tips**:
|
||||||
|
|
||||||
|
1. **Will the file be packaged into final product?**
|
||||||
|
- `scripts/`, `.github/`, `*.config.js` -> No -> Don't update version
|
||||||
|
- `src/`, `lib/`, `app/` -> Yes -> May need to update version
|
||||||
|
|
||||||
|
2. **Does the change only affect developers?**
|
||||||
|
- Dev tool config, CI scripts, build process -> Only affects developers -> Don't update version
|
||||||
|
- Feature code, UI, API -> Affects users -> Update version
|
||||||
|
|
||||||
|
3. **If project type cannot be identified or no version file** -> Only create git tag, don't update version file
|
||||||
|
|
||||||
|
### 5. Generate Commit Message
|
||||||
|
|
||||||
|
Based on changes in staging area, **analyze and generate meaningful commit message by AI**:
|
||||||
|
|
||||||
|
- Analyze changed code content, understand the purpose and intent of this modification
|
||||||
|
- Reference project's commit history style
|
||||||
|
- Use [Conventional Commits](https://www.conventionalcommits.org/) specification
|
||||||
|
- Commit message should concisely but accurately describe "why" rather than just "what"
|
||||||
|
- Common types: `feat` (new feature), `fix` (fix), `docs` (documentation), `refactor` (refactoring), `chore` (miscellaneous), `test` (test), etc.
|
||||||
|
- **Language selection**:
|
||||||
|
- **Default (macOS/Linux)**: Use Chinese (中文) for commit messages
|
||||||
|
- **Windows**: Use English due to encoding issues with Cursor Shell tool
|
||||||
|
- **If monorepo and this commit only affects single subproject, include subproject name in commit message**:
|
||||||
|
- Format: `<type>(<scope>): <description>`, where `<scope>` is subproject name
|
||||||
|
- Example (Chinese): `feat(ios): 支持 OGG Opus 上传` or `fix(electron): 修复剪贴板注入失败`
|
||||||
|
- Example (English): `feat(ios): support OGG Opus upload` or `fix(electron): fix clipboard injection failure`
|
||||||
|
- If affecting multiple subprojects or entire repository, no need to add scope
|
||||||
|
|
||||||
|
### 6. Update Project Version Number
|
||||||
|
|
||||||
|
> Only execute when step 4 determines version number update is needed and version file is identified
|
||||||
|
|
||||||
|
**Execution steps**:
|
||||||
|
|
||||||
|
1. Based on version file and field identified in step 4, update version number to calculated new version
|
||||||
|
2. Add version file to staging area: `git add <version file>`
|
||||||
|
3. **Verify staging area**: Run `git diff --cached --name-only` to confirm version file is in staging area
|
||||||
|
|
||||||
|
### 7. Execute Commit
|
||||||
|
|
||||||
|
Execute commit with generated commit message (staging area now includes user's changes and version number update).
|
||||||
|
|
||||||
|
**Commit message language by platform:**
|
||||||
|
|
||||||
|
**macOS/Linux** (use Chinese commit messages):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Single line commit
|
||||||
|
git commit -m "feat(android): 添加新功能"
|
||||||
|
|
||||||
|
# Multi-line commit (recommended)
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(android): 添加新功能
|
||||||
|
|
||||||
|
- 详细说明 1
|
||||||
|
- 详细说明 2
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows** (use English commit messages):
|
||||||
|
|
||||||
|
Due to Cursor's Shell tool encoding issues on Windows, **must use English** commit messages.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Single line commit
|
||||||
|
git commit -m "feat(android): add new feature"
|
||||||
|
|
||||||
|
# Multi-line commit (use --message multiple times)
|
||||||
|
git commit --message="fix(android): fix code review issues" --message="- Fix syntax error" --message="- Use JSONObject instead of manual JSON"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows prohibited methods** (will cause garbled text):
|
||||||
|
- No Chinese commit messages - Cursor Shell tool encoding error when passing Chinese parameters
|
||||||
|
- No `Out-File -Encoding utf8` - writes UTF-8 with BOM
|
||||||
|
- No Write tool to write temp files - encoding uncontrollable
|
||||||
|
- No PowerShell here-string `@"..."@` - newline parsing issues
|
||||||
|
|
||||||
|
### 8. Create Tag
|
||||||
|
|
||||||
|
> **Executed by default**. Only skip this step when user explicitly inputs "skip" or "skip tag".
|
||||||
|
> Only create tag when step 4 determines version number update is needed (docs/chore types don't create tag).
|
||||||
|
|
||||||
|
**Tag annotation content requirements**:
|
||||||
|
|
||||||
|
- **Use the same content as commit message for tag annotation**
|
||||||
|
- Tag annotation will be used as Release notes by CI/CD scripts, should include detailed change content
|
||||||
|
- If commit message has multiple lines, tag annotation should also be multi-line
|
||||||
|
|
||||||
|
**polyrepo**:
|
||||||
|
```bash
|
||||||
|
git tag -a "<version>" -m "<commit title>" -m "" -m "<commit body line 1>" -m "<commit body line 2>" ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**monorepo**:
|
||||||
|
```bash
|
||||||
|
git tag -a "<subproject>-<version>" -m "<commit title>" -m "" -m "<commit body line 1>" -m "<commit body line 2>" ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples (single line commit):
|
||||||
|
- polyrepo: `git tag -a "1.2.0" -m "feat: add user authentication"`
|
||||||
|
- monorepo: `git tag -a "android-1.2.0" -m "feat(android): add user authentication"`
|
||||||
|
|
||||||
|
Examples (multi-line commit, more recommended):
|
||||||
|
- polyrepo:
|
||||||
|
```bash
|
||||||
|
git tag -a "1.2.1" -m "fix: resolve bluetooth connection timeout" -m "" -m "- Increase connection timeout to 30s" -m "- Add retry mechanism for failed connections" -m "- Improve error messages"
|
||||||
|
```
|
||||||
|
- monorepo:
|
||||||
|
```bash
|
||||||
|
git tag -a "android-1.2.1" -m "fix(android): resolve bluetooth connection timeout" -m "" -m "- Increase connection timeout to 30s" -m "- Add retry mechanism for failed connections" -m "- Improve error messages"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Push to Remote Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Push current branch
|
||||||
|
git push origin $(git branch --show-current)
|
||||||
|
```
|
||||||
|
|
||||||
|
**If tag was created, also push tag**:
|
||||||
|
|
||||||
|
**polyrepo**:
|
||||||
|
```bash
|
||||||
|
git push origin <version>
|
||||||
|
```
|
||||||
|
|
||||||
|
**monorepo**:
|
||||||
|
```bash
|
||||||
|
git push origin <subproject>-<version>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Staging area check first**: Must check if staging area has files first, if not, terminate immediately, don't execute any subsequent operations.
|
||||||
|
- **AGENTS.md priority**: If `AGENTS.md` defines version update rules, prioritize using that rule; otherwise AI auto-detects project type.
|
||||||
|
- **Smart detection**: AI should intelligently determine project type and version file location based on files that actually exist in repository.
|
||||||
|
- **Version number and tag consistency**: Project version number and git tag use the same version number, ensure consistency.
|
||||||
|
- **Version number format**: Use `major.minor.patch` semantic versioning format (e.g., `0.1.0`, `1.1.0`, `1.1.1`), default starting `0.1.0`.
|
||||||
|
- **Create tag by default**: Unless user inputs "skip" or "skip tag", create and push tag by default.
|
||||||
|
- **Update version before commit**: First determine version number and modify version file, then commit at once, avoid using `--amend`.
|
||||||
|
- **Version file must be verified**: After updating version number and `git add`, must run `git diff --cached --name-only` to confirm version file is in staging area before executing commit.
|
||||||
|
- **Commit message language**: Default use Chinese (macOS/Linux); Windows must use English due to Cursor Shell tool encoding issues.
|
||||||
|
- **monorepo scope**: If staging area only affects single subproject, commit message should have scope; if affecting multiple subprojects, no scope needed.
|
||||||
|
- **monorepo tag format**: Use `<subproject>-<version>` format (e.g., `ios-0.1.0`, `android-1.1.0`).
|
||||||
|
- **No version file case**: If project type cannot be identified or no version file (like pure Go project), only create git tag, don't update any files.
|
||||||
130
command/create-gitea-repo.md
Normal file
130
command/create-gitea-repo.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
---
|
||||||
|
description: Create a new Git repository on Gitea
|
||||||
|
agent: build
|
||||||
|
---
|
||||||
|
|
||||||
|
# create-gitea-repo
|
||||||
|
|
||||||
|
Create a new Git repository on Gitea.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Create new repository under specified organization or user via Gitea API
|
||||||
|
- Support creating private or public repositories
|
||||||
|
- Automatically add remote repository address after successful creation
|
||||||
|
|
||||||
|
## User Input Format
|
||||||
|
|
||||||
|
User can specify parameters in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
<owner>/<repo> [private|public]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `owner`: Organization name or username (required)
|
||||||
|
- `repo`: Repository name (required)
|
||||||
|
- `private|public`: Visibility (optional, default `private`)
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- `ai/my-project` - Create private repository my-project under ai organization
|
||||||
|
- `ai/my-project public` - Create public repository my-project under ai organization
|
||||||
|
- `voson/test private` - Create private repository test under voson user
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Use the following configuration when executing command:
|
||||||
|
|
||||||
|
| Config Item | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Gitea Server URL | `https://git.digitevents.com/` |
|
||||||
|
| API Token | `{env:GITEA_API_TOKEN}` |
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Parse User Input
|
||||||
|
|
||||||
|
Parse from user input:
|
||||||
|
- `owner`: Organization name or username
|
||||||
|
- `repo`: Repository name
|
||||||
|
- `visibility`: `private` (default) or `public`
|
||||||
|
|
||||||
|
**Input validation**:
|
||||||
|
- If user didn't provide `owner/repo` format input, prompt user for correct format and terminate execution
|
||||||
|
- Repository name can only contain letters, numbers, underscores, hyphens and dots
|
||||||
|
|
||||||
|
### 2. Call Gitea API to Create Repository
|
||||||
|
|
||||||
|
Use curl to call Gitea API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST "https://git.digitevents.com/api/v1/orgs/<owner>/repos" \
|
||||||
|
-H "Authorization: token $GITEA_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "<repo>",
|
||||||
|
"private": <true|false>
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: If owner is personal user instead of organization, API path should be `/api/v1/user/repos`, but usually try organization API first.
|
||||||
|
|
||||||
|
### 3. Handle Response
|
||||||
|
|
||||||
|
**Success** (HTTP 201):
|
||||||
|
- Extract repository info from response:
|
||||||
|
- `html_url`: Repository web URL
|
||||||
|
- `clone_url`: HTTPS clone URL
|
||||||
|
- `ssh_url`: SSH clone URL
|
||||||
|
- Output creation success message
|
||||||
|
|
||||||
|
**Failure**:
|
||||||
|
- If 404 error, owner may not exist or no permission
|
||||||
|
- If 409 error, repository already exists
|
||||||
|
- Output error message and terminate
|
||||||
|
|
||||||
|
### 4. Ask Whether to Add Remote Repository
|
||||||
|
|
||||||
|
Ask user whether to add the newly created repository as current project's remote repository.
|
||||||
|
|
||||||
|
**If user confirms**:
|
||||||
|
|
||||||
|
1. Check if current directory is a Git repository:
|
||||||
|
```bash
|
||||||
|
git rev-parse --is-inside-work-tree
|
||||||
|
```
|
||||||
|
|
||||||
|
2. If not a Git repository, ask whether to initialize:
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Check if origin remote already exists:
|
||||||
|
```bash
|
||||||
|
git remote get-url origin
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Add or update remote repository:
|
||||||
|
- If no origin: `git remote add origin <clone_url>`
|
||||||
|
- If origin exists: Ask whether to overwrite, after confirmation execute `git remote set-url origin <clone_url>`
|
||||||
|
|
||||||
|
### 5. Output Result Summary
|
||||||
|
|
||||||
|
Output creation result summary table:
|
||||||
|
|
||||||
|
```
|
||||||
|
Repository created successfully!
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Repository Name | owner/repo |
|
||||||
|
| Web URL | https://git.digitevents.com/owner/repo |
|
||||||
|
| Clone URL (HTTPS) | https://git.digitevents.com/owner/repo.git |
|
||||||
|
| Clone URL (SSH) | git@git.digitevents.com:owner/repo.git |
|
||||||
|
| Private | Yes/No |
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Permission check**: Ensure Token has permission to create repository
|
||||||
|
- **Organization vs User**: Creating organization repository and user repository use different API endpoints
|
||||||
|
- **Repository naming**: Follow Gitea naming rules, avoid special characters
|
||||||
16
command/review.md
Normal file
16
command/review.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
description: Review code or documentation and provide suggestions
|
||||||
|
agent: plan
|
||||||
|
---
|
||||||
|
|
||||||
|
# review
|
||||||
|
|
||||||
|
## Actions to Execute
|
||||||
|
|
||||||
|
1. Review the code or documentation mentioned by user
|
||||||
|
2. Provide suggestions and content that needs modification
|
||||||
|
3. Ask user if modifications are needed, if there's content that doesn't need modification; if user doesn't specify, modify all
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- If user doesn't mention documentation or code, prompt user to provide documentation or code.
|
||||||
81
command/sync-oc-pull.md
Normal file
81
command/sync-oc-pull.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
description: Pull latest OpenCode config changes from remote
|
||||||
|
agent: build
|
||||||
|
---
|
||||||
|
|
||||||
|
# sync-oc-pull
|
||||||
|
|
||||||
|
Pull latest changes for OpenCode configuration from remote repository.
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
When you updated OpenCode configuration on another device, or team members shared new commands/configurations, use this command to sync to local.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Switch to OpenCode Config Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Check Local Status
|
||||||
|
|
||||||
|
Run `git status` to check if there are uncommitted local changes.
|
||||||
|
|
||||||
|
**If there are uncommitted changes**:
|
||||||
|
- List changed files
|
||||||
|
- Ask user how to handle:
|
||||||
|
- **Stash**: `git stash` to save local changes, restore after pull
|
||||||
|
- **Discard**: `git checkout -- .` to discard local changes
|
||||||
|
- **Cancel**: Terminate operation, let user handle manually
|
||||||
|
|
||||||
|
### 3. Pull Remote Changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Handle Conflicts (if any)
|
||||||
|
|
||||||
|
If conflicts occur during pull:
|
||||||
|
|
||||||
|
1. List conflicting files
|
||||||
|
2. Open conflict files, analyze conflict content
|
||||||
|
3. Ask user to choose:
|
||||||
|
- **Keep local version**: Use `git checkout --ours <file>`
|
||||||
|
- **Use remote version**: Use `git checkout --theirs <file>`
|
||||||
|
- **Manual merge**: Prompt user to manually edit then execute `git add <file>`
|
||||||
|
|
||||||
|
4. After resolving all conflicts, complete merge:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "chore: resolve merge conflicts"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Restore Stashed Changes (if any)
|
||||||
|
|
||||||
|
If `git stash` was used in step 2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git stash pop
|
||||||
|
```
|
||||||
|
|
||||||
|
If conflicts occur during restore, handle according to step 4.
|
||||||
|
|
||||||
|
### 6. Show Update Summary
|
||||||
|
|
||||||
|
After pull completes, show update content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --oneline -5
|
||||||
|
```
|
||||||
|
|
||||||
|
List newly added or modified command files to help user understand what new configurations are available.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Backup important configs**: If there are local modifications before pull, suggest backing up or using `git stash` first.
|
||||||
|
- **Conflict handling**: Config file conflicts usually prioritize keeping local version, unless explicitly needing remote's new config.
|
||||||
|
- **Verify config**: After pull, suggest checking if `opencode.json` and other config files work correctly.
|
||||||
86
command/sync-oc-push.md
Normal file
86
command/sync-oc-push.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
description: Commit and push OpenCode config changes to remote
|
||||||
|
agent: build
|
||||||
|
---
|
||||||
|
|
||||||
|
# sync-oc-push
|
||||||
|
|
||||||
|
Commit and push OpenCode configuration repository changes to remote repository.
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
When you modified config files in `~/.opencode` directory (such as `command/`, `skill/`, `opencode.json`, etc.), use this command to sync changes to remote repository.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Switch to OpenCode Config Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Check Change Status
|
||||||
|
|
||||||
|
Run `git status` to view current changes.
|
||||||
|
|
||||||
|
**If there are no changes**:
|
||||||
|
- Output prompt: "No changes to commit."
|
||||||
|
- **Terminate command execution**
|
||||||
|
|
||||||
|
### 3. Collect Information (Execute in parallel)
|
||||||
|
|
||||||
|
- Run `git diff` to view unstaged changes
|
||||||
|
- Run `git diff --cached` to view staged changes
|
||||||
|
- Run `git log --oneline -5` to view recent commit history
|
||||||
|
|
||||||
|
### 4. Add Changes to Staging Area
|
||||||
|
|
||||||
|
Add all relevant config files to staging area:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add command/ skill/ opencode.json
|
||||||
|
```
|
||||||
|
|
||||||
|
> Only add config files that need to be synced, ignore other local files.
|
||||||
|
|
||||||
|
### 5. Generate Commit Message and Commit
|
||||||
|
|
||||||
|
Generate concise commit message based on change content:
|
||||||
|
|
||||||
|
- Use [Conventional Commits](https://www.conventionalcommits.org/) specification
|
||||||
|
- **Language selection**:
|
||||||
|
- **Default (macOS/Linux)**: Use Chinese (中文) for commit messages
|
||||||
|
- **Windows**: Use English due to encoding issues with Cursor Shell tool
|
||||||
|
- Common types:
|
||||||
|
- `feat`: New command or config
|
||||||
|
- `fix`: Fix command or config issues
|
||||||
|
- `docs`: Documentation update
|
||||||
|
- `chore`: Miscellaneous adjustments
|
||||||
|
|
||||||
|
**Examples (macOS/Linux - Chinese)**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "feat: 添加 Vue.js 开发命令"
|
||||||
|
git commit -m "fix: 修正 MCP 服务器配置"
|
||||||
|
git commit -m "docs: 更新 review 命令说明"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples (Windows - English)**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "feat: add new developer command for Vue.js"
|
||||||
|
git commit -m "fix: correct MCP server configuration"
|
||||||
|
git commit -m "docs: update review command instructions"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Push to Remote Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Only sync config files**: Only add `command/`, `skill/` and `opencode.json`, don't commit other local data.
|
||||||
|
- **Sensitive info**: `opencode.json` may contain API keys, ensure remote repository access permissions are set correctly.
|
||||||
|
- **Commit message language**: Default use Chinese (macOS/Linux); Windows must use English due to Cursor Shell tool encoding issues.
|
||||||
40
opencode.json
Normal file
40
opencode.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": {
|
||||||
|
"opencode": {
|
||||||
|
"models": {
|
||||||
|
"claude-opus-4-5": {
|
||||||
|
"options": {
|
||||||
|
"thinking": {
|
||||||
|
"type": "enabled",
|
||||||
|
"budgetTokens": 16000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"claude-sonnet-4-5": {
|
||||||
|
"options": {
|
||||||
|
"thinking": {
|
||||||
|
"type": "enabled",
|
||||||
|
"budgetTokens": 16000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"ref": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://api.ref.tools/mcp?apiKey={env:REF_API_KEY}",
|
||||||
|
"headers": {}
|
||||||
|
},
|
||||||
|
"figma": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["npx", "-y", "figma-developer-mcp", "--stdio"],
|
||||||
|
"environment": {
|
||||||
|
"FIGMA_API_KEY": "{env:FIGMA_API_KEY}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permission": "allow"
|
||||||
|
}
|
||||||
77
skill/android-developer/SKILL.md
Normal file
77
skill/android-developer/SKILL.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
name: android-developer
|
||||||
|
description: Android development guidelines with Kotlin, Jetpack Compose, MVVM architecture and best practices
|
||||||
|
---
|
||||||
|
|
||||||
|
# Android Developer
|
||||||
|
|
||||||
|
You are a professional Android developer.
|
||||||
|
|
||||||
|
## Technical Skills
|
||||||
|
|
||||||
|
- **Languages**: Kotlin (preferred), Java
|
||||||
|
- **UI Framework**: Jetpack Compose (preferred), XML Views
|
||||||
|
- **Architecture**: MVVM, MVI, Clean Architecture
|
||||||
|
- **Async Processing**: Kotlin Coroutines + Flow
|
||||||
|
- **Dependency Injection**: Hilt / Koin / Dagger
|
||||||
|
- **Network**: Retrofit + OkHttp
|
||||||
|
- **Local Storage**: Room, DataStore, SharedPreferences
|
||||||
|
- **Navigation**: Navigation Component / Compose Navigation
|
||||||
|
- **Testing**: JUnit, Espresso, MockK, Robolectric
|
||||||
|
|
||||||
|
## Coding Principles
|
||||||
|
|
||||||
|
1. **Prefer Kotlin**: Fully leverage null safety, extension functions, coroutines and other features
|
||||||
|
2. **Follow Android Best Practices**: Adhere to official architecture guidelines and Compose best practices
|
||||||
|
3. **Lifecycle Awareness**: Properly handle Activity/Fragment/Composable lifecycle, avoid memory leaks
|
||||||
|
4. **Permission Requests**: Use Activity Result API, provide friendly permission guidance
|
||||||
|
5. **Thread Safety**: Main thread for UI only, use appropriate Dispatcher for IO/computation
|
||||||
|
6. **Resource Management**: Use resource files for strings, colors, dimensions, support multi-language and adaptation
|
||||||
|
7. **Code Simplicity**: Avoid over-engineering, maintain code readability
|
||||||
|
|
||||||
|
## UI/UX Standards
|
||||||
|
|
||||||
|
- Follow Material Design 3 guidelines
|
||||||
|
- Support dark mode and dynamic themes
|
||||||
|
- Adapt to different screen sizes and orientations
|
||||||
|
- Provide appropriate loading states and error feedback
|
||||||
|
- Use meaningful animations and transitions
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Follow Kotlin official code style
|
||||||
|
- Use meaningful naming
|
||||||
|
- Comments explain "why" not "what"
|
||||||
|
- Use sealed class/interface to define states
|
||||||
|
- ViewModel exposes StateFlow/SharedFlow instead of LiveData (for Compose projects)
|
||||||
|
|
||||||
|
## Common Dependencies
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Compose BOM
|
||||||
|
implementation(platform("androidx.compose:compose-bom:2024.01.00"))
|
||||||
|
|
||||||
|
// Hilt
|
||||||
|
implementation("com.google.dagger:hilt-android:2.48")
|
||||||
|
kapt("com.google.dagger:hilt-compiler:2.48")
|
||||||
|
|
||||||
|
// Retrofit
|
||||||
|
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||||
|
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||||
|
|
||||||
|
// Room
|
||||||
|
implementation("androidx.room:room-runtime:2.6.1")
|
||||||
|
implementation("androidx.room:room-ktx:2.6.1")
|
||||||
|
kapt("androidx.room:room-compiler:2.6.1")
|
||||||
|
|
||||||
|
// Coil (image loading)
|
||||||
|
implementation("io.coil-kt:coil-compose:2.5.0")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
1. **Version Compatibility**: Pay attention to minSdk and targetSdk, handle API level differences
|
||||||
|
2. **ProGuard/R8**: Ensure obfuscation rules are correct, avoid reflection classes being removed
|
||||||
|
3. **Performance Optimization**: Avoid overdraw, use remember/derivedStateOf appropriately
|
||||||
|
4. **Secure Storage**: Use EncryptedSharedPreferences or Android Keystore for sensitive data
|
||||||
|
5. **Background Restrictions**: Understand Android background execution restrictions, use WorkManager for background tasks
|
||||||
231
skill/electron-developer/SKILL.md
Normal file
231
skill/electron-developer/SKILL.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
---
|
||||||
|
name: electron-developer
|
||||||
|
description: Electron desktop app development with TypeScript, Lit Web Components, and cross-platform best practices
|
||||||
|
---
|
||||||
|
|
||||||
|
# Electron Developer
|
||||||
|
|
||||||
|
You are a professional Electron desktop application developer, skilled in using TypeScript, Lit (Web Components) and modern frontend tech stack to build high-quality cross-platform desktop applications.
|
||||||
|
|
||||||
|
## Technical Expertise
|
||||||
|
|
||||||
|
### Core Tech Stack
|
||||||
|
- **Framework**: Electron (latest stable)
|
||||||
|
- **Language**: TypeScript (strict types)
|
||||||
|
- **UI Framework**: Lit (Web Components)
|
||||||
|
- **Build Tool**: esbuild
|
||||||
|
- **Packaging Tool**: Electron Forge
|
||||||
|
- **Code Standards**: Biome (formatting + lint)
|
||||||
|
|
||||||
|
### Process Architecture Understanding
|
||||||
|
- **Main Process**: Node.js environment, responsible for window management, system API calls, IPC communication
|
||||||
|
- **Renderer Process**: Browser environment, responsible for UI rendering and user interaction
|
||||||
|
- **Preload Script**: Secure bridge between main and renderer process, expose API via contextBridge
|
||||||
|
|
||||||
|
## Development Standards
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
- Use 2-space indentation
|
||||||
|
- Use single quotes (strings)
|
||||||
|
- Always keep semicolons
|
||||||
|
- Line width limit 100 characters
|
||||||
|
- Use ES5 trailing comma rules
|
||||||
|
- Arrow function parameters always have parentheses
|
||||||
|
|
||||||
|
### TypeScript Standards
|
||||||
|
- Avoid implicit `any`, use explicit type annotations when necessary
|
||||||
|
- Add comments explaining reason when using `noNonNullAssertion`
|
||||||
|
- Prefer interfaces for object type definitions
|
||||||
|
- Use `@/*` path alias for src directory modules
|
||||||
|
|
||||||
|
### Security Best Practices
|
||||||
|
- **Never** enable `nodeIntegration` in renderer process
|
||||||
|
- Use `contextBridge` to safely expose main process APIs
|
||||||
|
- Validate all IPC message sources and content
|
||||||
|
- Use Content Security Policy (CSP)
|
||||||
|
- Disable developer tools in production
|
||||||
|
|
||||||
|
### IPC Communication Standards
|
||||||
|
```typescript
|
||||||
|
// Main process: Listen for messages from renderer
|
||||||
|
ipcMain.handle('channel-name', async (event, ...args) => {
|
||||||
|
// Processing logic
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Renderer process (exposed via preload)
|
||||||
|
const result = await window.api.channelName(...args);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure Convention
|
||||||
|
|
||||||
|
```
|
||||||
|
electron/
|
||||||
|
├── src/
|
||||||
|
│ ├── main/ # Main process code
|
||||||
|
│ │ ├── main.ts # Entry file
|
||||||
|
│ │ ├── preload.ts # Preload script
|
||||||
|
│ │ ├── ipc/ # IPC handlers
|
||||||
|
│ │ ├── store/ # Persistent storage
|
||||||
|
│ │ └── native/ # Native module wrappers
|
||||||
|
│ ├── renderer/ # Renderer process code
|
||||||
|
│ │ ├── components/ # Common Lit components
|
||||||
|
│ │ ├── pages/ # Page components
|
||||||
|
│ │ └── services/ # Business services
|
||||||
|
│ ├── public/ # Static assets (HTML, CSS)
|
||||||
|
│ ├── icons/ # App icons
|
||||||
|
│ └── types/ # Global type definitions
|
||||||
|
├── config/ # Config files
|
||||||
|
├── build/ # Native module build output
|
||||||
|
└── dist/ # Build output directory
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `npm run dev:watch` | Dev mode, watch file changes and auto-rebuild |
|
||||||
|
| `npm run start` | Start Electron app (development) |
|
||||||
|
| `npm run build` | Production build |
|
||||||
|
| `npm run build:native` | Build native modules |
|
||||||
|
| `npm run package` | Package app (no installer) |
|
||||||
|
| `npm run make` | Package and generate installer |
|
||||||
|
| `npm run lint` | Run Biome check |
|
||||||
|
| `npm run lint:fix` | Auto-fix lint issues |
|
||||||
|
| `npm run format:fix` | Auto-format code |
|
||||||
|
| `npm run typecheck` | TypeScript type check |
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Pre-development Preparation
|
||||||
|
|
||||||
|
1. Check `AGENTS.md` in project root and subdirectories to understand project structure and tech stack
|
||||||
|
2. Determine Electron project working directory based on `AGENTS.md`
|
||||||
|
|
||||||
|
### Start Development Environment
|
||||||
|
1. Terminal 1: Run `npm run dev:watch` to watch file changes
|
||||||
|
2. Terminal 2: Run `npm run start` to start app
|
||||||
|
3. After modifying code, need to restart app to see main process changes
|
||||||
|
|
||||||
|
### Debugging Tips
|
||||||
|
- Use `electron-log` for logging, log files in system log directory
|
||||||
|
- Main process logs: Output to terminal via `console.log`
|
||||||
|
- Renderer process logs: View via DevTools Console
|
||||||
|
- Use `--inspect` or `--inspect-brk` to debug main process
|
||||||
|
|
||||||
|
### Lit Component Development
|
||||||
|
```typescript
|
||||||
|
import { LitElement, html, css } from 'lit';
|
||||||
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
@customElement('my-component')
|
||||||
|
export class MyComponent extends LitElement {
|
||||||
|
@property({ type: String }) name = '';
|
||||||
|
@state() private count = 0;
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div>Hello, ${this.name}!</div>
|
||||||
|
<button @click=${this._handleClick}>Count: ${this.count}</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleClick() {
|
||||||
|
this.count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform-specific Handling
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
- Use `app.dock.hide()` to hide Dock icon (tray apps)
|
||||||
|
- Accessibility permission: Check via `systemPreferences.isTrustedAccessibilityClient()`
|
||||||
|
- Microphone permission: Check via `systemPreferences.getMediaAccessStatus()`
|
||||||
|
- App signing and notarization: Use `@electron/osx-sign` and `@electron/notarize`
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
- Use Squirrel to handle install/update/uninstall events
|
||||||
|
- Note path separator differences, use `path.join()` or `path.resolve()`
|
||||||
|
|
||||||
|
### Cross-platform Compatibility
|
||||||
|
```typescript
|
||||||
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
// macOS specific logic
|
||||||
|
} else if (platform === 'win32') {
|
||||||
|
// Windows specific logic
|
||||||
|
} else {
|
||||||
|
// Linux logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Native Module Development
|
||||||
|
|
||||||
|
### Build with node-gyp
|
||||||
|
- Config file: `binding.gyp`
|
||||||
|
- Build command: `npm run build:native`
|
||||||
|
- Use `node-addon-api` to simplify N-API development
|
||||||
|
|
||||||
|
### Load Native Modules in Electron
|
||||||
|
```typescript
|
||||||
|
import { app } from 'electron';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const nativeModulePath = app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, 'fn_monitor.node')
|
||||||
|
: path.join(__dirname, '../../build/Release/fn_monitor.node');
|
||||||
|
|
||||||
|
const nativeModule = require(nativeModulePath);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Startup Optimization
|
||||||
|
- Lazy load non-critical modules
|
||||||
|
- Use `ready-to-show` event to show window, avoid white screen
|
||||||
|
- Keep preload script minimal
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
- Remove event listeners timely
|
||||||
|
- Use `WeakMap` / `WeakSet` to avoid memory leaks
|
||||||
|
- Regularly check renderer process memory usage
|
||||||
|
|
||||||
|
### Rendering Optimization
|
||||||
|
- Use `will-change` CSS property to hint browser optimization
|
||||||
|
- Use virtual scrolling for large lists
|
||||||
|
- Avoid frequent DOM operations
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
1. **Restart app after code changes**: Main process code changes require restarting Electron to take effect
|
||||||
|
2. **Context Isolation**: Always keep `contextIsolation: true`
|
||||||
|
3. **ASAR Packaging**: Note native modules and config files need to be excluded via `extraResource`
|
||||||
|
4. **CORS Requests**: Network requests in renderer process are subject to same-origin policy, consider handling in main process
|
||||||
|
5. **File Paths**: Paths after packaging differ from development, use `app.isPackaged` to check environment
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Main process global error handling
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
log.error('Uncaught Exception:', error);
|
||||||
|
// Restart app or gracefully exit if necessary
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
log.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Renderer process error reporting
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
window.api.reportError(event.error);
|
||||||
|
});
|
||||||
|
```
|
||||||
417
skill/gitea-runner/SKILL.md
Normal file
417
skill/gitea-runner/SKILL.md
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
---
|
||||||
|
name: gitea-runner
|
||||||
|
description: Gitea Act Runner configuration guide for macOS ARM64 with host mode, cache optimization, and multi-runner setup
|
||||||
|
---
|
||||||
|
|
||||||
|
# Gitea Act Runner Configuration Skill
|
||||||
|
|
||||||
|
Complete guide for configuring and deploying Gitea Act Runner, optimized for macOS ARM64 (Apple Silicon) host mode.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Gitea Act Runner is the CI/CD executor for Gitea Actions, compatible with GitHub Actions workflow syntax.
|
||||||
|
|
||||||
|
### Execution Modes
|
||||||
|
|
||||||
|
| Mode | Environment | Use Case | Android SDK |
|
||||||
|
|------|-------------|----------|-------------|
|
||||||
|
| **Host Mode** | Native macOS/Linux | Android/iOS builds, native toolchains | macOS ARM64 supported |
|
||||||
|
| Docker Mode | Linux containers | Cross-platform builds | Linux ARM64 NOT supported |
|
||||||
|
|
||||||
|
**Recommendation**: Use **host mode** for macOS ARM64 runners to leverage native Android SDK support.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### 1. Install act_runner
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Homebrew (recommended)
|
||||||
|
brew install act_runner
|
||||||
|
|
||||||
|
# Or manual download
|
||||||
|
curl -sL https://gitea.com/gitea/act_runner/releases/download/v0.2.13/act_runner-0.2.13-darwin-arm64 \
|
||||||
|
-o /usr/local/bin/act_runner
|
||||||
|
chmod +x /usr/local/bin/act_runner
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Development Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Go (for Go backend builds)
|
||||||
|
brew install go
|
||||||
|
|
||||||
|
# Node.js (for frontend/miniprogram builds)
|
||||||
|
brew install node@22
|
||||||
|
|
||||||
|
# pnpm (package manager)
|
||||||
|
npm install -g pnpm
|
||||||
|
|
||||||
|
# JDK 17 (for Android builds)
|
||||||
|
brew install openjdk@17
|
||||||
|
|
||||||
|
# Docker (for container builds)
|
||||||
|
# Install Docker Desktop from https://docker.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Install Android SDK (for Android builds)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Homebrew
|
||||||
|
brew install --cask android-commandlinetools
|
||||||
|
|
||||||
|
# Or manual installation
|
||||||
|
mkdir -p ~/android-sdk/cmdline-tools
|
||||||
|
cd ~/android-sdk/cmdline-tools
|
||||||
|
curl -sL https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip -o cmdline-tools.zip
|
||||||
|
unzip cmdline-tools.zip
|
||||||
|
mv cmdline-tools latest
|
||||||
|
rm cmdline-tools.zip
|
||||||
|
|
||||||
|
# Accept licenses and install components
|
||||||
|
yes | sdkmanager --licenses
|
||||||
|
sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Configure Environment Variables
|
||||||
|
|
||||||
|
Add to `~/.zshrc` or `~/.bashrc`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Go
|
||||||
|
export GOPATH=$HOME/go
|
||||||
|
export PATH=$PATH:$GOPATH/bin
|
||||||
|
|
||||||
|
# Java
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@17
|
||||||
|
export PATH=$PATH:$JAVA_HOME/bin
|
||||||
|
|
||||||
|
# Android SDK
|
||||||
|
export ANDROID_HOME=~/android-sdk
|
||||||
|
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
|
||||||
|
export PATH=$PATH:$ANDROID_HOME/platform-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Verify Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify all tools
|
||||||
|
go version # Go 1.23+
|
||||||
|
node --version # v22.x
|
||||||
|
pnpm --version # Latest
|
||||||
|
java -version # 17+
|
||||||
|
echo $ANDROID_HOME # SDK path
|
||||||
|
docker --version # Latest
|
||||||
|
act_runner --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runner Configuration
|
||||||
|
|
||||||
|
### Host Mode Configuration Template
|
||||||
|
|
||||||
|
Create `act_runner_host.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
|
||||||
|
runner:
|
||||||
|
file: /path/to/work/dir/.runner
|
||||||
|
capacity: 2 # Concurrent jobs
|
||||||
|
timeout: 3h # Job timeout
|
||||||
|
shutdown_timeout: 30s
|
||||||
|
insecure: false
|
||||||
|
fetch_timeout: 5s
|
||||||
|
fetch_interval: 2s
|
||||||
|
# Labels reflect actual system info for precise workflow matching
|
||||||
|
labels:
|
||||||
|
- "self-hosted:host" # Self-hosted runner
|
||||||
|
- "macOS:host" # Operating system
|
||||||
|
- "ARM64:host" # Architecture
|
||||||
|
- "darwin-arm64:host" # Combined label (recommended for matching)
|
||||||
|
|
||||||
|
cache:
|
||||||
|
enabled: true
|
||||||
|
dir: "/path/to/work/dir/cache"
|
||||||
|
host: "127.0.0.1" # Local only (use 0.0.0.0 for multi-runner)
|
||||||
|
port: 9000
|
||||||
|
|
||||||
|
host:
|
||||||
|
workdir_parent: "/path/to/work/dir/workspace"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Label Design Guidelines
|
||||||
|
|
||||||
|
| Label | Meaning | Usage |
|
||||||
|
|-------|---------|-------|
|
||||||
|
| `self-hosted` | Self-hosted runner | Distinguish from Gitea-hosted runners |
|
||||||
|
| `macOS` | Operating system | Friendly name for Darwin |
|
||||||
|
| `ARM64` | CPU architecture | Apple Silicon (M1/M2/M3/M4) |
|
||||||
|
| `darwin-arm64` | Combined label | Most precise matching |
|
||||||
|
|
||||||
|
### Workflow Matching Examples
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
# Method 1: Combined label (recommended, most precise)
|
||||||
|
runs-on: darwin-arm64
|
||||||
|
|
||||||
|
# Method 2: Label array (matches multiple conditions)
|
||||||
|
# runs-on: [self-hosted, macOS, ARM64]
|
||||||
|
|
||||||
|
# Method 3: OS only
|
||||||
|
# runs-on: macOS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Registration & Startup
|
||||||
|
|
||||||
|
### 1. Get Registration Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Organization level (global runner, recommended)
|
||||||
|
curl -H "Authorization: token YOUR_GITEA_TOKEN" \
|
||||||
|
"https://your-gitea.com/api/v1/orgs/ORG_NAME/actions/runners/registration-token"
|
||||||
|
|
||||||
|
# Or repository level
|
||||||
|
curl -H "Authorization: token YOUR_GITEA_TOKEN" \
|
||||||
|
"https://your-gitea.com/api/v1/repos/OWNER/REPO/actions/runners/registration-token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Register Runner
|
||||||
|
|
||||||
|
```bash
|
||||||
|
act_runner register \
|
||||||
|
--config act_runner_host.yaml \
|
||||||
|
--instance https://your-gitea.com/ \
|
||||||
|
--token YOUR_REGISTRATION_TOKEN \
|
||||||
|
--name "your-runner-name" \
|
||||||
|
--labels "self-hosted:host,macOS:host,ARM64:host,darwin-arm64:host" \
|
||||||
|
--no-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start Runner
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Foreground (for debugging)
|
||||||
|
act_runner daemon --config act_runner_host.yaml
|
||||||
|
|
||||||
|
# Background
|
||||||
|
nohup act_runner daemon --config act_runner_host.yaml > runner.log 2>&1 &
|
||||||
|
|
||||||
|
# Using brew services (recommended for persistence)
|
||||||
|
brew services start act_runner
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Runner Cache Sharing
|
||||||
|
|
||||||
|
### Option A: Master-Slave Mode (Recommended for 2-3 runners)
|
||||||
|
|
||||||
|
**Primary Runner (cache server)**:
|
||||||
|
```yaml
|
||||||
|
cache:
|
||||||
|
enabled: true
|
||||||
|
dir: "/Users/voson/work/gitea/cache"
|
||||||
|
host: "0.0.0.0" # Listen on all interfaces
|
||||||
|
port: 9000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Secondary Runner (cache client)**:
|
||||||
|
```yaml
|
||||||
|
cache:
|
||||||
|
enabled: true
|
||||||
|
server: "http://192.168.0.103:9000" # Primary runner IP
|
||||||
|
dir: "/Users/user/work/gitea/cache" # Local fallback
|
||||||
|
host: "192.168.0.104" # This runner's IP
|
||||||
|
port: 9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: NFS Shared Storage (Enterprise)
|
||||||
|
|
||||||
|
**1. Setup NFS server on primary runner:**
|
||||||
|
```bash
|
||||||
|
# /etc/exports
|
||||||
|
/Users/voson/work/gitea/cache -alldirs -mapall=$(id -u):$(id -g) 192.168.0.0/24
|
||||||
|
|
||||||
|
sudo nfsd restart
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Mount on secondary runner:**
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /mnt/runner-cache
|
||||||
|
sudo mount -t nfs 192.168.0.103:/Users/voson/work/gitea/cache /mnt/runner-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option C: Independent Cache (Default)
|
||||||
|
|
||||||
|
Each runner maintains its own cache. First build downloads dependencies, subsequent builds use local cache.
|
||||||
|
|
||||||
|
## Cache Management
|
||||||
|
|
||||||
|
### Host Mode Cache Locations
|
||||||
|
|
||||||
|
| Cache Type | Location | Behavior |
|
||||||
|
|------------|----------|----------|
|
||||||
|
| Runner cache service | `config.cache.dir` | Managed by act_runner |
|
||||||
|
| Gradle | `~/.gradle/` | Persistent across builds |
|
||||||
|
| npm/pnpm | `~/.npm/`, `~/.pnpm-store/` | Persistent across builds |
|
||||||
|
| Go modules | `~/go/pkg/mod/` | Persistent across builds |
|
||||||
|
|
||||||
|
### Cache Cleanup Script
|
||||||
|
|
||||||
|
Create `cleanup-cache.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Cleaning up caches..."
|
||||||
|
|
||||||
|
# 1. Runner cache (older than 7 days)
|
||||||
|
find /path/to/cache/cache -type f -mtime +7 -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
# 2. Gradle cache (older than 30 days)
|
||||||
|
find ~/.gradle/caches -type f -mtime +30 -delete 2>/dev/null || true
|
||||||
|
find ~/.gradle/caches -type d -empty -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
# 3. npm cache verification
|
||||||
|
npm cache verify
|
||||||
|
|
||||||
|
# 4. Workspace cleanup
|
||||||
|
find /path/to/workspace -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \; 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Cleanup complete!"
|
||||||
|
echo "Runner cache: $(du -sh /path/to/cache/ | awk '{print $1}')"
|
||||||
|
echo "Gradle cache: $(du -sh ~/.gradle/ | awk '{print $1}')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schedule Cleanup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add to crontab for weekly cleanup
|
||||||
|
crontab -e
|
||||||
|
# Add: 0 3 * * 0 /path/to/cleanup-cache.sh >> /path/to/cleanup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Management Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check runner status
|
||||||
|
ps aux | grep act_runner
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
tail -f /path/to/runner.log
|
||||||
|
|
||||||
|
# Stop runner
|
||||||
|
pkill -f "act_runner daemon"
|
||||||
|
|
||||||
|
# Re-register (stop first, delete .runner file)
|
||||||
|
rm /path/to/.runner
|
||||||
|
act_runner register --config act_runner_host.yaml ...
|
||||||
|
|
||||||
|
# Check Gitea connection
|
||||||
|
curl -s https://your-gitea.com/api/v1/version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### 1. Android SDK Not Found
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify ANDROID_HOME
|
||||||
|
echo $ANDROID_HOME
|
||||||
|
ls $ANDROID_HOME
|
||||||
|
|
||||||
|
# Check if runner can access SDK
|
||||||
|
# Ensure runner runs as the same user who installed SDK
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. JDK Version Error
|
||||||
|
|
||||||
|
Android Gradle Plugin 8.x requires JDK 17+:
|
||||||
|
```bash
|
||||||
|
java -version
|
||||||
|
# Should show: openjdk version "17.x.x"
|
||||||
|
|
||||||
|
# If wrong version, update JAVA_HOME
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@17
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Permission Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure runner user owns work directories
|
||||||
|
chown -R $(whoami) /path/to/work/dir
|
||||||
|
chmod -R 755 /path/to/work/dir
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Label Mismatch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check registered labels
|
||||||
|
cat /path/to/.runner | jq '.labels'
|
||||||
|
|
||||||
|
# Ensure workflow runs-on matches
|
||||||
|
# Workflow: runs-on: darwin-arm64
|
||||||
|
# Runner: "darwin-arm64:host" label
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Cache Service Port Conflict
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if port 9000 is in use
|
||||||
|
lsof -i :9000
|
||||||
|
|
||||||
|
# Use different port if needed
|
||||||
|
# In config: port: 9001
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Mode Configuration (Alternative)
|
||||||
|
|
||||||
|
For container-based builds:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
|
||||||
|
runner:
|
||||||
|
file: /path/to/.runner
|
||||||
|
capacity: 2
|
||||||
|
timeout: 3h
|
||||||
|
labels:
|
||||||
|
- "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"
|
||||||
|
- "ubuntu-22.04:docker://catthehacker/ubuntu:act-latest"
|
||||||
|
|
||||||
|
cache:
|
||||||
|
enabled: true
|
||||||
|
dir: "/path/to/cache"
|
||||||
|
host: "192.168.0.103" # Host IP (not 127.0.0.1 for containers)
|
||||||
|
port: 9000
|
||||||
|
|
||||||
|
container:
|
||||||
|
options: "--platform=linux/amd64" # For ARM64 host running x86 images
|
||||||
|
network: "host"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Install runner | `brew install act_runner` |
|
||||||
|
| Register | `act_runner register --config ... --instance ... --token ...` |
|
||||||
|
| Start | `act_runner daemon --config act_runner_host.yaml` |
|
||||||
|
| Stop | `pkill -f "act_runner daemon"` |
|
||||||
|
| Check status | `ps aux \| grep act_runner` |
|
||||||
|
| View logs | `tail -f runner.log` |
|
||||||
|
| Re-register | `rm .runner && act_runner register ...` |
|
||||||
|
|
||||||
|
## Related Resources
|
||||||
|
|
||||||
|
- [Gitea Act Runner Documentation](https://docs.gitea.com/usage/actions/act-runner)
|
||||||
|
- [Android SDK Command Line Tools](https://developer.android.com/studio/command-line)
|
||||||
|
- [GitHub Actions Workflow Syntax](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions)
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
- **Skill Version**: 1.0
|
||||||
|
- **Last Updated**: 2026-01-12
|
||||||
|
- **Compatibility**: act_runner 0.2.13+, macOS ARM64
|
||||||
383
skill/gitea-workflow/SKILL.md
Normal file
383
skill/gitea-workflow/SKILL.md
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
---
|
||||||
|
name: gitea-workflow
|
||||||
|
description: Gitea Actions Workflow foundation and templates for various project types with CI/CD best practices
|
||||||
|
---
|
||||||
|
|
||||||
|
# Gitea Actions Workflow Skill
|
||||||
|
|
||||||
|
Gitea Actions workflow 基础知识和项目模板指南,兼容 GitHub Actions 语法。
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Gitea Actions workflows 定义在 `.gitea/workflows/*.yml` 文件中。本 skill 提供:
|
||||||
|
- Workflow 基础结构和通用组件
|
||||||
|
- 各项目类型的骨架模板(用户按需填充具体构建逻辑)
|
||||||
|
|
||||||
|
## 项目类型模板
|
||||||
|
|
||||||
|
| 类型 | 文档 | 适用场景 |
|
||||||
|
|------|------|---------|
|
||||||
|
| Go 后端 | [go-backend.md](./go-backend.md) | API 服务、微服务、CLI 工具 |
|
||||||
|
| Node.js 前端 | [nodejs-frontend.md](./nodejs-frontend.md) | React/Vue/Vite/Next.js |
|
||||||
|
| Android 应用 | [android-app.md](./android-app.md) | Kotlin/Java/Jetpack Compose |
|
||||||
|
| 微信小程序 | [wechat-miniprogram.md](./wechat-miniprogram.md) | 微信小程序 CI/CD |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow 基础结构
|
||||||
|
|
||||||
|
### 文件位置
|
||||||
|
|
||||||
|
```
|
||||||
|
project/
|
||||||
|
├── .gitea/
|
||||||
|
│ └── workflows/
|
||||||
|
│ ├── backend.yml
|
||||||
|
│ ├── frontend.yml
|
||||||
|
│ └── ...
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 骨架模板
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Service Name - Build & Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'service-dir/**' # 仅相关目录变更时触发
|
||||||
|
- '.gitea/workflows/this-workflow.yml'
|
||||||
|
tags:
|
||||||
|
- 'service-prefix-*' # Tag 触发 Release
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true # 取消同分支旧的运行
|
||||||
|
|
||||||
|
env:
|
||||||
|
SERVICE_PREFIX: service-name # 服务标识
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: darwin-arm64 # Runner 标签
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.vars.outputs.version }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # 完整历史(用于 git describe)
|
||||||
|
|
||||||
|
# ... 构建步骤
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: darwin-arm64
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/service-prefix-')
|
||||||
|
steps:
|
||||||
|
# ... Release 步骤
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
### 常用触发模式
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
on:
|
||||||
|
# 分支推送
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
# 路径过滤(推荐:仅相关文件变更时触发)
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- '*.yml'
|
||||||
|
|
||||||
|
# Tag 推送(用于 Release)
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
- 'service-*'
|
||||||
|
|
||||||
|
# Pull Request
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
# 手动触发
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: 'Deploy environment'
|
||||||
|
required: true
|
||||||
|
default: 'staging'
|
||||||
|
|
||||||
|
# 定时触发
|
||||||
|
schedule:
|
||||||
|
- cron: '0 2 * * *' # 每天凌晨 2 点
|
||||||
|
```
|
||||||
|
|
||||||
|
### 并发控制
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 通用组件
|
||||||
|
|
||||||
|
### Checkout
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # 完整历史,用于 git describe 获取版本
|
||||||
|
# fetch-depth: 1 # 仅最新提交,加快速度
|
||||||
|
```
|
||||||
|
|
||||||
|
### 变量设置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Set variables
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
git_tag=$(git describe --tags --abbrev=0 --always)
|
||||||
|
registry=$(echo ${{ github.server_url }} | cut -d '/' -f 3)
|
||||||
|
|
||||||
|
# 写入环境变量(当前 job 可用)
|
||||||
|
{
|
||||||
|
echo "git_tag=${git_tag}"
|
||||||
|
echo "registry=${registry}"
|
||||||
|
} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 写入输出(其他 job 可用)
|
||||||
|
echo "version=${git_tag}" >> $GITHUB_OUTPUT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Action
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/directory
|
||||||
|
./node_modules
|
||||||
|
key: cache-name-${{ hashFiles('**/lockfile') }}
|
||||||
|
restore-keys: cache-name-
|
||||||
|
```
|
||||||
|
|
||||||
|
**各语言缓存路径**:
|
||||||
|
|
||||||
|
| 语言 | 缓存路径 | Key 文件 |
|
||||||
|
|------|---------|----------|
|
||||||
|
| Go | `~/go/pkg/mod`, `~/.cache/go-build` | `go.mod`, `go.sum` |
|
||||||
|
| Node.js (pnpm) | `~/.pnpm-store`, `node_modules` | `pnpm-lock.yaml` |
|
||||||
|
| Node.js (npm) | `~/.npm`, `node_modules` | `package-lock.json` |
|
||||||
|
| Gradle | `~/.gradle/caches`, `~/.gradle/wrapper` | `*.gradle*`, `gradle-wrapper.properties` |
|
||||||
|
|
||||||
|
### Artifact 上传/下载
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 上传
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build-artifact
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
# 下载(另一个 job)
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build-artifact
|
||||||
|
path: dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 构建推送
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Docker - Login
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.registry }}
|
||||||
|
username: ${{ vars.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Docker - Setup Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker - Build & Push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./service-dir
|
||||||
|
file: ./service-dir/Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: |
|
||||||
|
${{ env.registry }}/${{ github.repository_owner }}/image:latest
|
||||||
|
${{ env.registry }}/${{ github.repository_owner }}/image:${{ env.git_tag }}
|
||||||
|
cache-from: type=registry,ref=image:buildcache
|
||||||
|
cache-to: type=registry,ref=image:buildcache,mode=max
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通知 Webhook
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Notify
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
WEBHOOK_URL: ${{ vars.WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
status="${{ job.status }}"
|
||||||
|
[ "$status" = "success" ] && text="Build Success" || text="Build Failed"
|
||||||
|
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST \
|
||||||
|
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"${{ env.SERVICE_PREFIX }} ${text}\"}}" \
|
||||||
|
"$WEBHOOK_URL"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release 创建(Gitea API)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Create Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
git_tag=$(git describe --tags --abbrev=0)
|
||||||
|
api_url="${{ github.server_url }}/api/v1"
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
|
||||||
|
# 创建 Release
|
||||||
|
release_id=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${git_tag}\",\"name\":\"Release ${git_tag}\"}" \
|
||||||
|
"${api_url}/repos/${repo}/releases" | jq -r '.id')
|
||||||
|
|
||||||
|
# 上传附件
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@dist/artifact.zip" \
|
||||||
|
"${api_url}/repos/${repo}/releases/${release_id}/assets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Secrets 配置
|
||||||
|
|
||||||
|
### 通用 Secrets
|
||||||
|
|
||||||
|
| Secret | 用途 | 适用项目 |
|
||||||
|
|--------|------|---------|
|
||||||
|
| `REGISTRY_PASSWORD` | Docker Registry 密码 | 需要 Docker 发布的项目 |
|
||||||
|
| `RELEASE_TOKEN` | Gitea API 令牌 | 需要创建 Release 的项目 |
|
||||||
|
|
||||||
|
### 项目特定 Secrets
|
||||||
|
|
||||||
|
参考各项目类型子文档。
|
||||||
|
|
||||||
|
### 安全最佳实践
|
||||||
|
|
||||||
|
1. **不要在日志中打印 secrets**
|
||||||
|
2. **使用 `vars.` 存储非敏感变量**(如用户名、URL)
|
||||||
|
3. **secrets 仅用于敏感信息**(如密码、密钥)
|
||||||
|
4. **定期轮换密钥**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 路径过滤
|
||||||
|
|
||||||
|
仅相关文件变更时触发,避免无关构建:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'backend/**'
|
||||||
|
- '.gitea/workflows/backend.yml'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Tag 命名规范
|
||||||
|
|
||||||
|
使用前缀区分不同服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag server-1.0.0 && git push origin server-1.0.0
|
||||||
|
git tag web-1.0.0 && git push origin web-1.0.0
|
||||||
|
git tag android-1.0.0 && git push origin android-1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Job 输出传递
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.vars.outputs.version }}
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.build.outputs.version }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 条件执行
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 仅 Tag 推送时执行
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
|
||||||
|
# 仅主分支执行
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
|
||||||
|
# 始终执行(用于通知)
|
||||||
|
if: always()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速参考
|
||||||
|
|
||||||
|
| 任务 | 命令/语法 |
|
||||||
|
|------|----------|
|
||||||
|
| 获取 git tag | `git describe --tags --abbrev=0 --always` |
|
||||||
|
| 提取 registry | `echo ${{ github.server_url }} \| cut -d '/' -f 3` |
|
||||||
|
| 设置环境变量 | `echo "KEY=value" >> $GITHUB_ENV` |
|
||||||
|
| 设置输出 | `echo "key=value" >> $GITHUB_OUTPUT` |
|
||||||
|
| 计算哈希 | `sha256sum file1 file2 \| sha256sum \| head -c 16` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
1. **选择项目类型**:参考上方索引表,选择对应的子文档
|
||||||
|
2. **复制骨架模板**:将模板复制到 `.gitea/workflows/`
|
||||||
|
3. **填充构建逻辑**:根据项目需求填充 `# 用户自定义` 部分
|
||||||
|
4. **配置 Secrets**:在 Gitea 中配置所需的 Secrets
|
||||||
|
5. **推送触发**:推送代码或 Tag 触发 workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本
|
||||||
|
|
||||||
|
- **Skill Version**: 2.0
|
||||||
|
- **Last Updated**: 2026-01-12
|
||||||
|
- **Structure**: 主文档 + 4 个项目类型子文档
|
||||||
399
skill/gitea-workflow/android-app.md
Normal file
399
skill/gitea-workflow/android-app.md
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
# Android 应用 Workflow 模板
|
||||||
|
|
||||||
|
适用于 Android 应用的 CI/CD workflow,支持 APK 构建、签名和发布。
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
- Kotlin / Java Android 应用
|
||||||
|
- Jetpack Compose 项目
|
||||||
|
- 需要签名发布的 APK
|
||||||
|
- 需要创建 Release 的项目
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
| 依赖 | Runner 要求 |
|
||||||
|
|------|------------|
|
||||||
|
| JDK 17+ | Runner 主机已安装 |
|
||||||
|
| Android SDK | Runner 主机已安装 |
|
||||||
|
| Gradle | 由 wrapper 管理 |
|
||||||
|
|
||||||
|
**重要**:必须使用 `darwin-arm64` 标签的 macOS Runner,因为 Google 不提供 Linux ARM64 版本的 Android SDK。
|
||||||
|
|
||||||
|
## Workflow 骨架模板
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Android - Build & Release APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'android/**' # 修改为实际目录
|
||||||
|
- '.gitea/workflows/android.yml'
|
||||||
|
tags:
|
||||||
|
- 'android-*' # 修改为实际 tag 前缀
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
SERVICE_PREFIX: android # 修改为实际服务名
|
||||||
|
SERVICE_DIR: android # 修改为实际目录名
|
||||||
|
APP_NAME: myapp-android # 修改为实际应用名
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Release APK
|
||||||
|
runs-on: darwin-arm64 # macOS ARM64(必须)
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version_name: ${{ steps.vars.outputs.version_name }}
|
||||||
|
version_code: ${{ steps.vars.outputs.version_code }}
|
||||||
|
apk_name: ${{ steps.vars.outputs.apk_name }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Verify Java environment
|
||||||
|
run: |
|
||||||
|
java -version
|
||||||
|
echo "JAVA_HOME=$JAVA_HOME"
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
run: |
|
||||||
|
if [ -n "$ANDROID_HOME" ] && [ -d "$ANDROID_HOME" ]; then
|
||||||
|
echo "Using ANDROID_HOME: $ANDROID_HOME"
|
||||||
|
else
|
||||||
|
for SDK_PATH in ~/Library/Android/sdk ~/android-sdk /opt/homebrew/share/android-commandlinetools; do
|
||||||
|
if [ -d "$SDK_PATH" ]; then
|
||||||
|
export ANDROID_HOME=$SDK_PATH
|
||||||
|
echo "Found Android SDK: $SDK_PATH"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ANDROID_HOME" ] || [ ! -d "$ANDROID_HOME" ]; then
|
||||||
|
echo "ERROR: Android SDK not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV
|
||||||
|
echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> $GITHUB_PATH
|
||||||
|
echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Get gradle-hashfiles
|
||||||
|
id: hash-gradle
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
HASH=$(sha256sum gradle/libs.versions.toml app/build.gradle.kts build.gradle.kts settings.gradle.kts 2>/dev/null | sha256sum | awk '{print $1}' | head -c 16)
|
||||||
|
echo "hash=${HASH}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: gradle-${{ env.SERVICE_PREFIX }}-${{ steps.hash-gradle.outputs.hash }}
|
||||||
|
restore-keys: gradle-${{ env.SERVICE_PREFIX }}-
|
||||||
|
|
||||||
|
- name: Set variables
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
# 从 build.gradle.kts 提取版本信息(按需修改路径和正则)
|
||||||
|
version_name=$(grep 'versionName' ${{ env.SERVICE_DIR }}/app/build.gradle.kts | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||||
|
version_code=$(grep 'versionCode' ${{ env.SERVICE_DIR }}/app/build.gradle.kts | head -1 | sed 's/[^0-9]*//g')
|
||||||
|
git_tag=$(git describe --tags --abbrev=0 --always)
|
||||||
|
apk_name="${{ env.APP_NAME }}-${version_name}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "version_name=${version_name}"
|
||||||
|
echo "version_code=${version_code}"
|
||||||
|
echo "git_tag=${git_tag}"
|
||||||
|
echo "apk_name=${apk_name}"
|
||||||
|
} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "version_name=${version_name}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version_code=${version_code}" >> $GITHUB_OUTPUT
|
||||||
|
echo "apk_name=${apk_name}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Setup signing
|
||||||
|
env:
|
||||||
|
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# 创建 local.properties
|
||||||
|
echo "sdk.dir=$ANDROID_HOME" > ${{ env.SERVICE_DIR }}/local.properties
|
||||||
|
|
||||||
|
# 配置签名(如果提供了 keystore)
|
||||||
|
if [ -n "$KEYSTORE_BASE64" ]; then
|
||||||
|
echo "$KEYSTORE_BASE64" | base64 -d > ${{ env.SERVICE_DIR }}/release.keystore
|
||||||
|
{
|
||||||
|
echo "KEYSTORE_FILE=../release.keystore"
|
||||||
|
echo "KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD"
|
||||||
|
echo "KEY_ALIAS=$KEY_ALIAS"
|
||||||
|
echo "KEY_PASSWORD=$KEY_PASSWORD"
|
||||||
|
} >> ${{ env.SERVICE_DIR }}/local.properties
|
||||||
|
echo "Signing configured"
|
||||||
|
else
|
||||||
|
echo "WARNING: No signing key provided, building unsigned APK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build Release APK
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
chmod +x gradlew
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 用户自定义构建步骤(按项目需求修改)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# 清理(按需启用)
|
||||||
|
# ./gradlew clean
|
||||||
|
|
||||||
|
# 单元测试(按需启用)
|
||||||
|
# ./gradlew test
|
||||||
|
|
||||||
|
# 构建 Release APK(必须)
|
||||||
|
./gradlew assembleRelease --no-daemon
|
||||||
|
|
||||||
|
- name: Rename APK
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
cp ${{ env.SERVICE_DIR }}/app/build/outputs/apk/release/app-release.apk dist/${{ env.apk_name }}.apk
|
||||||
|
cd dist
|
||||||
|
sha256sum ${{ env.apk_name }}.apk > ${{ env.apk_name }}.apk.sha256
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.SERVICE_PREFIX }}-apk
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
- name: Notify
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
WEBHOOK_URL: ${{ vars.WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
status="${{ job.status }}"
|
||||||
|
[ "$status" = "success" ] && status_text="Build Success" || status_text="Build Failed"
|
||||||
|
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d \
|
||||||
|
"{\"msg_type\":\"text\",\"content\":{\"text\":\"${{ env.APP_NAME }} ${status_text}\\nVersion: ${{ env.version_name }}\"}}" \
|
||||||
|
"$WEBHOOK_URL" || true
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: darwin-arm64
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.SERVICE_PREFIX }}-apk
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
VERSION_NAME: ${{ needs.build.outputs.version_name }}
|
||||||
|
APK_NAME: ${{ needs.build.outputs.apk_name }}
|
||||||
|
run: |
|
||||||
|
git_tag=$(git describe --tags --abbrev=0)
|
||||||
|
api_url="${{ github.server_url }}/api/v1"
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
|
||||||
|
# 创建 Release
|
||||||
|
release_id=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${git_tag}\",\"name\":\"Release ${git_tag} (v${VERSION_NAME})\"}" \
|
||||||
|
"${api_url}/repos/${repo}/releases" | jq -r '.id')
|
||||||
|
|
||||||
|
# 上传 APK
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@dist/${APK_NAME}.apk" \
|
||||||
|
"${api_url}/repos/${repo}/releases/${release_id}/assets"
|
||||||
|
|
||||||
|
# 上传校验和
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@dist/${APK_NAME}.apk.sha256" \
|
||||||
|
"${api_url}/repos/${repo}/releases/${release_id}/assets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 签名配置
|
||||||
|
|
||||||
|
### 1. 生成签名密钥
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -genkey -v -keystore release.keystore \
|
||||||
|
-alias myapp \
|
||||||
|
-keyalg RSA \
|
||||||
|
-keysize 2048 \
|
||||||
|
-validity 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Base64 编码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
base64 -i release.keystore -o keystore.base64
|
||||||
|
# 将 keystore.base64 内容复制到 ANDROID_KEYSTORE_BASE64 secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. build.gradle.kts 签名配置
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
android {
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
val props = Properties()
|
||||||
|
val propsFile = rootProject.file("local.properties")
|
||||||
|
if (propsFile.exists()) {
|
||||||
|
props.load(propsFile.inputStream())
|
||||||
|
val keystoreFile = props.getProperty("KEYSTORE_FILE", "")
|
||||||
|
if (keystoreFile.isNotEmpty()) {
|
||||||
|
storeFile = file(keystoreFile)
|
||||||
|
storePassword = props.getProperty("KEYSTORE_PASSWORD", "")
|
||||||
|
keyAlias = props.getProperty("KEY_ALIAS", "")
|
||||||
|
keyPassword = props.getProperty("KEY_PASSWORD", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 缓存配置
|
||||||
|
|
||||||
|
### 缓存路径
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches # Gradle 依赖缓存
|
||||||
|
~/.gradle/wrapper # Gradle wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key 计算
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HASH=$(sha256sum gradle/libs.versions.toml app/build.gradle.kts build.gradle.kts settings.gradle.kts | sha256sum | awk '{print $1}' | head -c 16)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Secrets 配置
|
||||||
|
|
||||||
|
| Secret | 用途 |
|
||||||
|
|--------|------|
|
||||||
|
| `ANDROID_KEYSTORE_BASE64` | Base64 编码的 keystore 文件 |
|
||||||
|
| `ANDROID_KEYSTORE_PASSWORD` | keystore 密码 |
|
||||||
|
| `ANDROID_KEY_ALIAS` | 密钥别名 |
|
||||||
|
| `ANDROID_KEY_PASSWORD` | 密钥密码 |
|
||||||
|
| `RELEASE_TOKEN` | Gitea API 令牌(创建 Release) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gradle 优化
|
||||||
|
|
||||||
|
### gradle.properties
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# CI 环境优化
|
||||||
|
org.gradle.daemon=false
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.configureondemand=true
|
||||||
|
|
||||||
|
# Android 配置
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 国内镜像(可选)
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# gradle-wrapper.properties
|
||||||
|
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.13-bin.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本提取
|
||||||
|
|
||||||
|
从 `build.gradle.kts` 提取版本信息:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# versionName
|
||||||
|
version_name=$(grep 'versionName' app/build.gradle.kts | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||||
|
|
||||||
|
# versionCode
|
||||||
|
version_code=$(grep 'versionCode' app/build.gradle.kts | head -1 | sed 's/[^0-9]*//g')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见 Gradle 任务
|
||||||
|
|
||||||
|
| 任务 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `./gradlew assembleRelease` | 构建 Release APK |
|
||||||
|
| `./gradlew assembleDebug` | 构建 Debug APK |
|
||||||
|
| `./gradlew bundleRelease` | 构建 AAB(App Bundle)|
|
||||||
|
| `./gradlew test` | 运行单元测试 |
|
||||||
|
| `./gradlew lint` | 运行 Lint 检查 |
|
||||||
|
| `./gradlew clean` | 清理构建产物 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用步骤
|
||||||
|
|
||||||
|
1. 复制上方 workflow 模板到 `.gitea/workflows/android.yml`
|
||||||
|
2. 修改 `SERVICE_PREFIX`、`SERVICE_DIR`、`APP_NAME` 为实际值
|
||||||
|
3. 修改 `paths` 和 `tags` 触发条件
|
||||||
|
4. 配置 `build.gradle.kts` 签名(参考上方示例)
|
||||||
|
5. 配置 Secrets(keystore、密码等)
|
||||||
|
6. 推送代码触发 workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建并推送 tag
|
||||||
|
git tag android-1.0.0
|
||||||
|
git push origin android-1.0.0
|
||||||
|
```
|
||||||
325
skill/gitea-workflow/go-backend.md
Normal file
325
skill/gitea-workflow/go-backend.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# Go 后端服务 Workflow 模板
|
||||||
|
|
||||||
|
适用于 Go 后端 API 服务、微服务、CLI 工具的 CI/CD workflow。
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
- Go HTTP API 服务
|
||||||
|
- gRPC 微服务
|
||||||
|
- CLI 工具
|
||||||
|
- 需要构建 Docker 镜像的 Go 项目
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
| 依赖 | Runner 要求 |
|
||||||
|
|------|------------|
|
||||||
|
| Go 1.21+ | Runner 主机已安装 |
|
||||||
|
| Docker | Runner 主机已安装 |
|
||||||
|
|
||||||
|
## Workflow 骨架模板
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Go Backend - Build & Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'your-service/**' # 修改为实际目录
|
||||||
|
- '.gitea/workflows/your-service.yml'
|
||||||
|
tags:
|
||||||
|
- 'your-service-*' # 修改为实际 tag 前缀
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
SERVICE_PREFIX: your-service # 修改为实际服务名
|
||||||
|
SERVICE_DIR: your-service # 修改为实际目录名
|
||||||
|
GOPROXY: https://goproxy.cn,direct
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
name: Build & Publish
|
||||||
|
runs-on: darwin-arm64
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
binary_name: ${{ steps.vars.outputs.binary_name }}
|
||||||
|
git_tag: ${{ steps.vars.outputs.git_tag }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Verify Go environment
|
||||||
|
run: |
|
||||||
|
go version
|
||||||
|
echo "GOPATH=$(go env GOPATH)"
|
||||||
|
|
||||||
|
- name: Get go-hashfiles
|
||||||
|
id: hash-go
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
HASH=$(sha256sum go.mod go.sum | sha256sum | awk '{print $1}' | head -c 16)
|
||||||
|
echo "hash=${HASH}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
|
key: go-${{ env.SERVICE_PREFIX }}-${{ steps.hash-go.outputs.hash }}
|
||||||
|
restore-keys: go-${{ env.SERVICE_PREFIX }}-
|
||||||
|
|
||||||
|
- name: Set variables
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
git_tag=$(git describe --tags --abbrev=0 --always)
|
||||||
|
registry=$(echo ${{ github.server_url }} | cut -d '/' -f 3)
|
||||||
|
binary_name="${{ github.event.repository.name }}-${{ env.SERVICE_PREFIX }}"
|
||||||
|
image_repo="${{ github.event.repository.name }}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "git_tag=${git_tag}"
|
||||||
|
echo "registry=${registry}"
|
||||||
|
echo "binary_name=${binary_name}"
|
||||||
|
echo "image_repo=${image_repo}"
|
||||||
|
echo "latest_tag=${{ env.SERVICE_PREFIX }}-latest"
|
||||||
|
} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "binary_name=${binary_name}" >> $GITHUB_OUTPUT
|
||||||
|
echo "git_tag=${git_tag}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
run: |
|
||||||
|
# ============================================
|
||||||
|
# 用户自定义构建步骤(按项目需求修改)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# 代码生成(按需启用)
|
||||||
|
# go generate ./...
|
||||||
|
# go tool ent generate ./schema
|
||||||
|
# go tool wire ./...
|
||||||
|
# go tool oapi-codegen -config oapi.yaml api.yaml
|
||||||
|
# go tool stringer -type=MyType ./...
|
||||||
|
|
||||||
|
# 测试(按需启用)
|
||||||
|
# go test ./...
|
||||||
|
# go vet ./...
|
||||||
|
|
||||||
|
# 构建(必须)
|
||||||
|
go build -o ${{ env.binary_name }} \
|
||||||
|
-ldflags '-s -w -X main.GitTag=${{ env.git_tag }}'
|
||||||
|
|
||||||
|
chmod +x ${{ env.binary_name }}
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.SERVICE_PREFIX }}-binary-linux-amd64
|
||||||
|
path: ${{ env.SERVICE_DIR }}/${{ env.binary_name }}
|
||||||
|
|
||||||
|
- name: Docker - Login
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.registry }}
|
||||||
|
username: ${{ vars.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Docker - Setup Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker - Build & Push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ${{ env.SERVICE_DIR }}
|
||||||
|
file: ./${{ env.SERVICE_DIR }}/Dockerfile.ci
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: |
|
||||||
|
${{ env.registry }}/${{ github.repository_owner }}/${{ env.image_repo }}:${{ env.latest_tag }}
|
||||||
|
${{ env.registry }}/${{ github.repository_owner }}/${{ env.image_repo }}:${{ env.git_tag }}
|
||||||
|
cache-from: type=registry,ref=${{ env.registry }}/${{ github.repository_owner }}/${{ env.image_repo }}:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ env.registry }}/${{ github.repository_owner }}/${{ env.image_repo }}:buildcache,mode=max
|
||||||
|
|
||||||
|
- name: Notify
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
WEBHOOK_URL: ${{ vars.WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
status="${{ job.status }}"
|
||||||
|
[ "$status" = "success" ] && status_text="Build Success" || status_text="Build Failed"
|
||||||
|
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d \
|
||||||
|
"{\"msg_type\":\"text\",\"content\":{\"text\":\"${{ env.binary_name }} ${status_text}\"}}" \
|
||||||
|
"$WEBHOOK_URL" || true
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: darwin-arm64
|
||||||
|
needs: build-and-publish
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.SERVICE_PREFIX }}-binary-linux-amd64
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
BINARY_NAME: ${{ needs.build-and-publish.outputs.binary_name }}
|
||||||
|
run: |
|
||||||
|
git_tag=$(git describe --tags --abbrev=0)
|
||||||
|
api_url="${{ github.server_url }}/api/v1"
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
|
||||||
|
# 生成校验和
|
||||||
|
cd dist
|
||||||
|
sha256sum "${BINARY_NAME}" > "${BINARY_NAME}.sha256"
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 创建 Release
|
||||||
|
release_id=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${git_tag}\",\"name\":\"Release ${git_tag}\"}" \
|
||||||
|
"${api_url}/repos/${repo}/releases" | jq -r '.id')
|
||||||
|
|
||||||
|
# 上传二进制文件
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@dist/${BINARY_NAME}" \
|
||||||
|
"${api_url}/repos/${repo}/releases/${release_id}/assets"
|
||||||
|
|
||||||
|
# 上传校验和
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@dist/${BINARY_NAME}.sha256" \
|
||||||
|
"${api_url}/repos/${repo}/releases/${release_id}/assets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dockerfile.ci 模板
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# 安装时区数据和证书
|
||||||
|
RUN apk add --no-cache tzdata ca-certificates
|
||||||
|
|
||||||
|
# 设置时区
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# 复制构建好的二进制文件
|
||||||
|
# 注意:需要在 docker build 时通过 --build-arg 传递 BINARY_NAME
|
||||||
|
# 或者直接写死二进制文件名
|
||||||
|
ARG BINARY_NAME=server
|
||||||
|
COPY ${BINARY_NAME} /app/server
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 健康检查(按需修改端口和路径)
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/server"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 缓存配置
|
||||||
|
|
||||||
|
### 缓存路径
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod # Go 模块缓存
|
||||||
|
~/.cache/go-build # Go 构建缓存
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key 计算
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HASH=$(sha256sum go.mod go.sum | sha256sum | awk '{print $1}' | head -c 16)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 构建参数说明
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=0 # 禁用 CGO,生成静态链接二进制
|
||||||
|
GOOS=linux # 目标操作系统
|
||||||
|
GOARCH=amd64 # 目标架构
|
||||||
|
|
||||||
|
-ldflags '-s -w' # 去除符号表和调试信息,减小体积
|
||||||
|
-X main.GitTag=... # 注入版本信息
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Secrets 配置
|
||||||
|
|
||||||
|
| Secret | 用途 |
|
||||||
|
|--------|------|
|
||||||
|
| `REGISTRY_PASSWORD` | Docker Registry 密码 |
|
||||||
|
| `RELEASE_TOKEN` | Gitea API 令牌(创建 Release) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见代码生成工具
|
||||||
|
|
||||||
|
根据项目使用的框架,在 Build 步骤中添加相应的生成命令:
|
||||||
|
|
||||||
|
| 框架/工具 | 命令 |
|
||||||
|
|----------|------|
|
||||||
|
| Ent (ORM) | `go tool ent generate ./schema` |
|
||||||
|
| Wire (DI) | `go tool wire ./...` |
|
||||||
|
| oapi-codegen | `go tool oapi-codegen -config oapi.yaml api.yaml` |
|
||||||
|
| Stringer | `go tool stringer -type=MyType ./...` |
|
||||||
|
| Protobuf | `protoc --go_out=. --go-grpc_out=. *.proto` |
|
||||||
|
| go generate | `go generate ./...` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用步骤
|
||||||
|
|
||||||
|
1. 复制上方 workflow 模板到 `.gitea/workflows/your-service.yml`
|
||||||
|
2. 修改 `SERVICE_PREFIX` 和 `SERVICE_DIR` 为实际值
|
||||||
|
3. 修改 `paths` 和 `tags` 触发条件
|
||||||
|
4. 根据项目需求填充 "用户自定义构建步骤" 部分
|
||||||
|
5. 创建 `Dockerfile.ci` 文件
|
||||||
|
6. 配置 Secrets
|
||||||
|
7. 推送代码触发 workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建并推送 tag
|
||||||
|
git tag your-service-1.0.0
|
||||||
|
git push origin your-service-1.0.0
|
||||||
|
```
|
||||||
397
skill/gitea-workflow/nodejs-frontend.md
Normal file
397
skill/gitea-workflow/nodejs-frontend.md
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
# Node.js 前端 Workflow 模板
|
||||||
|
|
||||||
|
适用于 Node.js 前端项目的 CI/CD workflow,支持 React、Vue、Vite、Next.js 等框架。
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
- React / Vue / Angular 前端项目
|
||||||
|
- Vite / Webpack 构建的 SPA
|
||||||
|
- Next.js / Nuxt.js SSR 应用
|
||||||
|
- 需要构建 Docker 镜像的前端项目
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
| 依赖 | Runner 要求 |
|
||||||
|
|------|------------|
|
||||||
|
| Node.js 20+ | Runner 主机已安装 |
|
||||||
|
| pnpm / npm | Runner 主机已安装或动态安装 |
|
||||||
|
| Docker | Runner 主机已安装 |
|
||||||
|
|
||||||
|
## Workflow 骨架模板
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Web Frontend - Build & Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'web/**' # 修改为实际目录
|
||||||
|
- '.gitea/workflows/web.yml'
|
||||||
|
tags:
|
||||||
|
- 'web-*' # 修改为实际 tag 前缀
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
SERVICE_PREFIX: web # 修改为实际服务名
|
||||||
|
SERVICE_DIR: web # 修改为实际目录名
|
||||||
|
NPM_REGISTRY: https://registry.npmmirror.com
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
name: Build & Publish
|
||||||
|
runs-on: darwin-arm64
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
git_tag: ${{ steps.vars.outputs.git_tag }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify Node.js environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
run: |
|
||||||
|
npm config set registry ${{ env.NPM_REGISTRY }}
|
||||||
|
npm install -g pnpm@latest-10
|
||||||
|
|
||||||
|
- name: Get pnpm-hashfiles
|
||||||
|
id: hash-pnpm
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
HASH=$(sha256sum package.json pnpm-lock.yaml 2>/dev/null | sha256sum | awk '{print $1}' | head -c 16)
|
||||||
|
echo "hash=${HASH}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache pnpm modules
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.local/share/pnpm/store
|
||||||
|
~/.pnpm-store
|
||||||
|
${{ env.SERVICE_DIR }}/node_modules
|
||||||
|
key: pnpm-${{ env.SERVICE_PREFIX }}-${{ steps.hash-pnpm.outputs.hash }}
|
||||||
|
restore-keys: pnpm-${{ env.SERVICE_PREFIX }}-
|
||||||
|
|
||||||
|
- name: Set variables
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
git_tag=$(git describe --tags --abbrev=0 --always)
|
||||||
|
registry=$(echo ${{ github.server_url }} | cut -d '/' -f 3)
|
||||||
|
image_repo="${{ github.event.repository.name }}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "git_tag=${git_tag}"
|
||||||
|
echo "registry=${registry}"
|
||||||
|
echo "image_repo=${image_repo}"
|
||||||
|
echo "latest_tag=${{ env.SERVICE_PREFIX }}-latest"
|
||||||
|
} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "git_tag=${git_tag}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Install & Build
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
env:
|
||||||
|
NODE_ENV: production
|
||||||
|
# ============================================
|
||||||
|
# 用户自定义环境变量(按项目需求修改)
|
||||||
|
# ============================================
|
||||||
|
# VITE_API_URL: https://api.example.com
|
||||||
|
# VITE_APP_TITLE: My App
|
||||||
|
# NEXT_PUBLIC_API_URL: https://api.example.com
|
||||||
|
run: |
|
||||||
|
# 安装依赖
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 用户自定义构建步骤(按项目需求修改)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# 类型检查(按需启用)
|
||||||
|
# pnpm run typecheck
|
||||||
|
# pnpm run lint
|
||||||
|
|
||||||
|
# 构建(必须)
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.SERVICE_PREFIX }}-dist
|
||||||
|
path: ${{ env.SERVICE_DIR }}/dist
|
||||||
|
|
||||||
|
- name: Docker - Login
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.registry }}
|
||||||
|
username: ${{ vars.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Docker - Setup Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker - Build & Push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./${{ env.SERVICE_DIR }}
|
||||||
|
file: ./${{ env.SERVICE_DIR }}/Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: |
|
||||||
|
${{ env.registry }}/${{ github.repository_owner }}/${{ env.image_repo }}:${{ env.latest_tag }}
|
||||||
|
${{ env.registry }}/${{ github.repository_owner }}/${{ env.image_repo }}:${{ env.git_tag }}
|
||||||
|
cache-from: type=registry,ref=${{ env.registry }}/${{ github.repository_owner }}/${{ env.image_repo }}:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ env.registry }}/${{ github.repository_owner }}/${{ env.image_repo }}:buildcache,mode=max
|
||||||
|
|
||||||
|
- name: Notify
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
WEBHOOK_URL: ${{ vars.WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
status="${{ job.status }}"
|
||||||
|
[ "$status" = "success" ] && status_text="Build Success" || status_text="Build Failed"
|
||||||
|
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d \
|
||||||
|
"{\"msg_type\":\"text\",\"content\":{\"text\":\"${{ env.image_repo }}-${{ env.SERVICE_PREFIX }} ${status_text}\"}}" \
|
||||||
|
"$WEBHOOK_URL" || true
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: darwin-arm64
|
||||||
|
needs: build-and-publish
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.SERVICE_PREFIX }}-dist
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
git_tag=$(git describe --tags --abbrev=0)
|
||||||
|
api_url="${{ github.server_url }}/api/v1"
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
|
||||||
|
# 打包构建产物
|
||||||
|
cd dist
|
||||||
|
zip -r "${{ env.SERVICE_PREFIX }}-dist.zip" .
|
||||||
|
sha256sum "${{ env.SERVICE_PREFIX }}-dist.zip" > "${{ env.SERVICE_PREFIX }}-dist.zip.sha256"
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 创建 Release
|
||||||
|
release_id=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${git_tag}\",\"name\":\"Release ${git_tag}\"}" \
|
||||||
|
"${api_url}/repos/${repo}/releases" | jq -r '.id')
|
||||||
|
|
||||||
|
# 上传附件
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@dist/${{ env.SERVICE_PREFIX }}-dist.zip" \
|
||||||
|
"${api_url}/repos/${repo}/releases/${release_id}/assets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dockerfile 模板
|
||||||
|
|
||||||
|
### 静态文件部署(Nginx)
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# 复制构建产物
|
||||||
|
COPY dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制 nginx 配置(可选)
|
||||||
|
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### SPA 路由配置(nginx.conf)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# SPA 路由支持
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 静态资源缓存
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# 禁止缓存 HTML
|
||||||
|
location ~* \.html$ {
|
||||||
|
expires -1;
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 缓存配置
|
||||||
|
|
||||||
|
### pnpm 缓存路径
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
path: |
|
||||||
|
~/.local/share/pnpm/store # pnpm 全局存储
|
||||||
|
~/.pnpm-store # 备选路径
|
||||||
|
web/node_modules # 项目依赖
|
||||||
|
```
|
||||||
|
|
||||||
|
### npm 缓存路径
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
path: |
|
||||||
|
~/.npm
|
||||||
|
web/node_modules
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key 计算
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# pnpm
|
||||||
|
HASH=$(sha256sum package.json pnpm-lock.yaml | sha256sum | awk '{print $1}' | head -c 16)
|
||||||
|
|
||||||
|
# npm
|
||||||
|
HASH=$(sha256sum package.json package-lock.json | sha256sum | awk '{print $1}' | head -c 16)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 包管理器选择
|
||||||
|
|
||||||
|
### pnpm(推荐)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Set up pnpm
|
||||||
|
run: |
|
||||||
|
npm install -g pnpm@latest-10
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### npm
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
```
|
||||||
|
|
||||||
|
### yarn
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Set up yarn
|
||||||
|
run: npm install -g yarn
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境变量注入
|
||||||
|
|
||||||
|
### Vite
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
VITE_API_URL: https://api.example.com
|
||||||
|
VITE_APP_TITLE: My App
|
||||||
|
```
|
||||||
|
|
||||||
|
### Next.js
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_API_URL: https://api.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vue CLI / CRA
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
VUE_APP_API_URL: https://api.example.com
|
||||||
|
REACT_APP_API_URL: https://api.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Secrets 配置
|
||||||
|
|
||||||
|
| Secret | 用途 |
|
||||||
|
|--------|------|
|
||||||
|
| `REGISTRY_PASSWORD` | Docker Registry 密码 |
|
||||||
|
| `RELEASE_TOKEN` | Gitea API 令牌(创建 Release) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见构建命令
|
||||||
|
|
||||||
|
根据项目使用的框架,在 Build 步骤中添加相应的命令:
|
||||||
|
|
||||||
|
| 框架 | 构建命令 |
|
||||||
|
|------|---------|
|
||||||
|
| Vite | `pnpm run build` |
|
||||||
|
| Next.js | `pnpm run build` |
|
||||||
|
| Nuxt.js | `pnpm run generate` 或 `pnpm run build` |
|
||||||
|
| Vue CLI | `pnpm run build` |
|
||||||
|
| CRA | `npm run build` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用步骤
|
||||||
|
|
||||||
|
1. 复制上方 workflow 模板到 `.gitea/workflows/web.yml`
|
||||||
|
2. 修改 `SERVICE_PREFIX` 和 `SERVICE_DIR` 为实际值
|
||||||
|
3. 修改 `paths` 和 `tags` 触发条件
|
||||||
|
4. 根据项目需求配置环境变量
|
||||||
|
5. 创建 `Dockerfile` 文件
|
||||||
|
6. 配置 Secrets
|
||||||
|
7. 推送代码触发 workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建并推送 tag
|
||||||
|
git tag web-1.0.0
|
||||||
|
git push origin web-1.0.0
|
||||||
|
```
|
||||||
386
skill/gitea-workflow/wechat-miniprogram.md
Normal file
386
skill/gitea-workflow/wechat-miniprogram.md
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
# 微信小程序 Workflow 模板
|
||||||
|
|
||||||
|
适用于微信小程序的 CI/CD workflow,支持自动构建和上传体验版。
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
- 微信小程序项目
|
||||||
|
- 使用 miniprogram-ci 自动上传
|
||||||
|
- TypeScript 小程序项目
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
| 依赖 | Runner 要求 |
|
||||||
|
|------|------------|
|
||||||
|
| Node.js 18+ | Runner 主机已安装 |
|
||||||
|
| miniprogram-ci | 动态安装 |
|
||||||
|
|
||||||
|
## 前置配置
|
||||||
|
|
||||||
|
### 1. 获取小程序上传密钥
|
||||||
|
|
||||||
|
1. 登录 [微信公众平台](https://mp.weixin.qq.com/)
|
||||||
|
2. 进入 **开发管理** → **开发设置**
|
||||||
|
3. 下载 **代码上传密钥**
|
||||||
|
|
||||||
|
### 2. Base64 编码密钥
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat private.key | base64 -w 0 > private.key.base64
|
||||||
|
# 将内容复制到 MINIPROGRAM_PRIVATE_KEY secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置 IP 白名单
|
||||||
|
|
||||||
|
在微信公众平台添加 Runner 服务器的公网 IP 到白名单。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow 骨架模板
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Mini-Program - Build & Upload
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'mini-program/**' # 修改为实际目录
|
||||||
|
- '.gitea/workflows/miniprogram.yml'
|
||||||
|
tags:
|
||||||
|
- 'miniprogram-*' # 修改为实际 tag 前缀
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
SERVICE_PREFIX: miniprogram # 修改为实际服务名
|
||||||
|
SERVICE_DIR: mini-program # 修改为实际目录名
|
||||||
|
NPM_REGISTRY: https://registry.npmmirror.com
|
||||||
|
APPID: wxYOUR_APPID_HERE # 修改为实际 AppID
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-upload:
|
||||||
|
name: Build & Upload
|
||||||
|
runs-on: darwin-arm64
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.vars.outputs.version }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Verify Node.js environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Configure npm registry
|
||||||
|
run: npm config set registry ${{ env.NPM_REGISTRY }}
|
||||||
|
|
||||||
|
- name: Get npm-hashfiles
|
||||||
|
id: hash-npm
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
HASH=$(sha256sum package.json package-lock.json 2>/dev/null | sha256sum | awk '{print $1}' | head -c 16)
|
||||||
|
echo "hash=${HASH}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache npm modules
|
||||||
|
uses: https://github.com/actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.npm
|
||||||
|
${{ env.SERVICE_DIR }}/node_modules
|
||||||
|
key: npm-${{ env.SERVICE_PREFIX }}-${{ steps.hash-npm.outputs.hash }}
|
||||||
|
restore-keys: npm-${{ env.SERVICE_PREFIX }}-
|
||||||
|
|
||||||
|
- name: Set variables
|
||||||
|
id: vars
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
version=$(node -p "require('./package.json').version")
|
||||||
|
commit_title=$(git log -1 --pretty=format:"%s")
|
||||||
|
git_tag=$(git describe --tags --abbrev=0 --always)
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "version=${version}"
|
||||||
|
echo "commit_title=${commit_title}"
|
||||||
|
echo "git_tag=${git_tag}"
|
||||||
|
} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "version=${version}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build npm packages
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
npm install -g miniprogram-ci
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 用户自定义 npm 包构建(按项目需求修改)
|
||||||
|
# ============================================
|
||||||
|
# 小程序不支持直接使用 node_modules,需要手动复制
|
||||||
|
|
||||||
|
mkdir -p miniprogram_npm
|
||||||
|
|
||||||
|
# 示例:复制 UI 组件库
|
||||||
|
# if [ -d "node_modules/tdesign-miniprogram/miniprogram_dist" ]; then
|
||||||
|
# cp -r node_modules/tdesign-miniprogram/miniprogram_dist miniprogram_npm/tdesign-miniprogram
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# 示例:复制工具库
|
||||||
|
# for pkg in dayjs lodash; do
|
||||||
|
# [ -d "node_modules/$pkg" ] && cp -r "node_modules/$pkg" miniprogram_npm/
|
||||||
|
# done
|
||||||
|
|
||||||
|
echo "npm packages prepared"
|
||||||
|
ls -la miniprogram_npm/ 2>/dev/null || echo "No npm packages"
|
||||||
|
|
||||||
|
- name: TypeScript check
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
# ============================================
|
||||||
|
# 用户自定义类型检查(按项目需求修改)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# 示例:TypeScript 类型检查
|
||||||
|
# npx tsc --noEmit --skipLibCheck || echo "TypeScript check completed with warnings"
|
||||||
|
|
||||||
|
echo "Type check step (customize as needed)"
|
||||||
|
|
||||||
|
- name: Setup private key
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
env:
|
||||||
|
MINIPROGRAM_PRIVATE_KEY: ${{ secrets.MINIPROGRAM_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
if [ -n "$MINIPROGRAM_PRIVATE_KEY" ]; then
|
||||||
|
echo "$MINIPROGRAM_PRIVATE_KEY" | base64 -d > private.key
|
||||||
|
echo "Private key configured"
|
||||||
|
else
|
||||||
|
echo "WARNING: MINIPROGRAM_PRIVATE_KEY not set"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload to WeChat
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
[ ! -f "private.key" ] && { echo "Skip upload: no private key"; exit 0; }
|
||||||
|
|
||||||
|
miniprogram-ci upload \
|
||||||
|
--pp ./ \
|
||||||
|
--pkp ./private.key \
|
||||||
|
--appid ${{ env.APPID }} \
|
||||||
|
--uv "${{ env.version }}" \
|
||||||
|
--ud "${{ env.commit_title }}" \
|
||||||
|
--robot 1 \
|
||||||
|
-r 1 \
|
||||||
|
--enable-es6 true \
|
||||||
|
--enable-es7 true \
|
||||||
|
--enable-minify true \
|
||||||
|
--enable-minifyJS true \
|
||||||
|
--enable-minifyWXML true \
|
||||||
|
--enable-minifyWXSS true \
|
||||||
|
--enable-autoPrefixWXSS true
|
||||||
|
|
||||||
|
echo "Upload success: v${{ env.version }}"
|
||||||
|
|
||||||
|
- name: Cleanup private key
|
||||||
|
if: always()
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: rm -f private.key
|
||||||
|
|
||||||
|
- name: Create source package
|
||||||
|
working-directory: ${{ env.SERVICE_DIR }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ../dist
|
||||||
|
tar --exclude='node_modules' \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='private.key' \
|
||||||
|
-czf ../dist/${{ env.SERVICE_PREFIX }}-${{ env.version }}-source.tar.gz .
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.SERVICE_PREFIX }}-source
|
||||||
|
path: dist/${{ env.SERVICE_PREFIX }}-${{ env.version }}-source.tar.gz
|
||||||
|
|
||||||
|
- name: Notify
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
WEBHOOK_URL: ${{ vars.WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
status="${{ job.status }}"
|
||||||
|
[ "$status" = "success" ] && status_text="Upload Success" || status_text="Upload Failed"
|
||||||
|
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d \
|
||||||
|
"{\"msg_type\":\"text\",\"content\":{\"text\":\"Miniprogram ${status_text}\\nVersion: ${{ env.version }}\"}}" \
|
||||||
|
"$WEBHOOK_URL" || true
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: darwin-arm64
|
||||||
|
needs: build-and-upload
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.SERVICE_PREFIX }}-source
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
VERSION: ${{ needs.build-and-upload.outputs.version }}
|
||||||
|
run: |
|
||||||
|
git_tag=$(git describe --tags --abbrev=0)
|
||||||
|
api_url="${{ github.server_url }}/api/v1"
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
|
||||||
|
# 创建 Release
|
||||||
|
release_id=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${git_tag}\",\"name\":\"Release ${git_tag} (v${VERSION})\"}" \
|
||||||
|
"${api_url}/repos/${repo}/releases" | jq -r '.id')
|
||||||
|
|
||||||
|
# 上传源码包
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@dist/${{ env.SERVICE_PREFIX }}-${VERSION}-source.tar.gz" \
|
||||||
|
"${api_url}/repos/${repo}/releases/${release_id}/assets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## miniprogram-ci 参数说明
|
||||||
|
|
||||||
|
```bash
|
||||||
|
miniprogram-ci upload \
|
||||||
|
--pp ./ # 项目路径
|
||||||
|
--pkp ./private.key # 私钥路径
|
||||||
|
--appid wx123456789 # 小程序 AppID
|
||||||
|
--uv "1.0.0" # 版本号
|
||||||
|
--ud "提交描述" # 版本描述
|
||||||
|
--robot 1 # 机器人编号 (1-30)
|
||||||
|
-r 1 # 提交轮次
|
||||||
|
--enable-es6 true # ES6 转 ES5
|
||||||
|
--enable-es7 true # ES7 支持
|
||||||
|
--enable-minify true # 压缩代码
|
||||||
|
--enable-minifyJS true # 压缩 JS
|
||||||
|
--enable-minifyWXML true # 压缩 WXML
|
||||||
|
--enable-minifyWXSS true # 压缩 WXSS
|
||||||
|
--enable-autoPrefixWXSS true # CSS 自动前缀
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 缓存配置
|
||||||
|
|
||||||
|
### 缓存路径
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
path: |
|
||||||
|
~/.npm
|
||||||
|
mini-program/node_modules
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key 计算
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HASH=$(sha256sum package.json package-lock.json | sha256sum | awk '{print $1}' | head -c 16)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Secrets 配置
|
||||||
|
|
||||||
|
| Secret | 用途 |
|
||||||
|
|--------|------|
|
||||||
|
| `MINIPROGRAM_PRIVATE_KEY` | Base64 编码的上传密钥 |
|
||||||
|
| `RELEASE_TOKEN` | Gitea API 令牌(创建 Release) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## npm 包处理
|
||||||
|
|
||||||
|
小程序不支持直接使用 `node_modules`,需要手动复制到 `miniprogram_npm/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# UI 组件库(使用 miniprogram_dist 目录)
|
||||||
|
cp -r node_modules/tdesign-miniprogram/miniprogram_dist miniprogram_npm/tdesign-miniprogram
|
||||||
|
cp -r node_modules/vant-weapp/lib miniprogram_npm/vant-weapp
|
||||||
|
|
||||||
|
# 工具库(直接复制)
|
||||||
|
cp -r node_modules/dayjs miniprogram_npm/
|
||||||
|
cp -r node_modules/lodash miniprogram_npm/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本管理
|
||||||
|
|
||||||
|
从 `package.json` 读取版本号:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
version=$(node -p "require('./package.json').version")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 体验版说明
|
||||||
|
|
||||||
|
- 上传成功后自动成为**体验版**
|
||||||
|
- 需要在微信后台手动提交审核发布正式版
|
||||||
|
- `--robot` 参数可区分不同 CI 环境(1-30)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
mini-program/
|
||||||
|
├── pages/ # 页面
|
||||||
|
├── components/ # 组件
|
||||||
|
├── miniprogram_npm/ # npm 包(构建生成)
|
||||||
|
├── app.ts # 应用入口
|
||||||
|
├── app.json # 应用配置
|
||||||
|
├── project.config.json # 项目配置
|
||||||
|
├── package.json # 依赖配置
|
||||||
|
└── tsconfig.json # TypeScript 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用步骤
|
||||||
|
|
||||||
|
1. 复制上方 workflow 模板到 `.gitea/workflows/miniprogram.yml`
|
||||||
|
2. 修改 `SERVICE_PREFIX`、`SERVICE_DIR`、`APPID` 为实际值
|
||||||
|
3. 修改 `paths` 和 `tags` 触发条件
|
||||||
|
4. 根据项目需求填充 npm 包构建步骤
|
||||||
|
5. 配置 Secrets(上传密钥)
|
||||||
|
6. 在微信公众平台添加 IP 白名单
|
||||||
|
7. 推送代码触发 workflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建并推送 tag
|
||||||
|
git tag miniprogram-1.0.0
|
||||||
|
git push origin miniprogram-1.0.0
|
||||||
|
```
|
||||||
64
skill/go-developer/SKILL.md
Normal file
64
skill/go-developer/SKILL.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: go-developer
|
||||||
|
description: Go backend development guidelines with modern practices, testing standards, and code quality tools
|
||||||
|
---
|
||||||
|
|
||||||
|
# Go Developer
|
||||||
|
|
||||||
|
You are a backend developer, default development language is Go.
|
||||||
|
|
||||||
|
## Pre-development Preparation
|
||||||
|
|
||||||
|
1. Check `AGENTS.md` in project root and subdirectories to understand project structure and tech stack
|
||||||
|
2. Determine Go server code working directory based on `AGENTS.md`
|
||||||
|
|
||||||
|
## Development Standards
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- For unclear or ambiguous user descriptions, point out and ask user to confirm
|
||||||
|
- Should follow Go language development best practices as much as possible, point out any conflicts with this
|
||||||
|
|
||||||
|
### Unit Testing
|
||||||
|
- Only create unit tests and corresponding tasks in `.vscode/tasks.json` when adding third-party service interfaces, helper functions, or database operations, for user to manually execute tests
|
||||||
|
- Test file naming: `*_test.go`, same directory as source file
|
||||||
|
- You only need to create unit tests, no need to execute them
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
- Update OpenAPI file when creating/modifying APIs
|
||||||
|
- Create OpenAPI file if it doesn't exist
|
||||||
|
|
||||||
|
### Error Handling and Logging
|
||||||
|
- Follow project's existing error handling and logging standards
|
||||||
|
|
||||||
|
## Basic Goals
|
||||||
|
|
||||||
|
1. Project runs without errors
|
||||||
|
2. No lint errors
|
||||||
|
|
||||||
|
## Post-development Cleanup
|
||||||
|
|
||||||
|
1. Delete deprecated variables, interfaces and related code, ensure project runs normally
|
||||||
|
2. Organize dependencies, run `go mod tidy`
|
||||||
|
3. Check `README.md` in working directory, confirm if sync update is needed. Create it if it doesn't exist
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
Development follows modern Go development paradigm, use following tools to check code quality:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# golangci-lint (comprehensive code check)
|
||||||
|
golangci-lint run <working-directory>
|
||||||
|
|
||||||
|
# modernize (modern Go development paradigm check, this command will auto-install modernize if it doesn't exist)
|
||||||
|
go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -test <working-directory>/...
|
||||||
|
```
|
||||||
|
|
||||||
|
If above tools don't exist, install first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install golangci-lint
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do NOT modify code auto-generated by frameworks or commands. Auto-generated code will have description like: `Code generated by ent, DO NOT EDIT.` at the beginning
|
||||||
47
skill/ios-developer/SKILL.md
Normal file
47
skill/ios-developer/SKILL.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: ios-developer
|
||||||
|
description: iOS app development guidelines with Swift, SwiftUI, and iOS 26+ best practices
|
||||||
|
---
|
||||||
|
|
||||||
|
# iOS Developer
|
||||||
|
|
||||||
|
You are an iOS developer, primarily responsible for mobile iOS APP development.
|
||||||
|
|
||||||
|
## Development Environment
|
||||||
|
|
||||||
|
- macOS ARM64 (Apple Silicon)
|
||||||
|
- Xcode 26
|
||||||
|
- iOS 26+ (no need to consider backward compatibility)
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Language**: Swift
|
||||||
|
- **UI Framework**: SwiftUI (preferred) / UIKit
|
||||||
|
- **Concurrency Model**: Swift Concurrency (async/await, Actor)
|
||||||
|
- **Dependency Management**: Swift Package Manager
|
||||||
|
|
||||||
|
## iOS 26 New Features
|
||||||
|
|
||||||
|
Understand and leverage new capabilities introduced in iOS 26:
|
||||||
|
- **Liquid Glass**: New design language, dynamic glass material effects
|
||||||
|
- **Foundation Models framework**: On-device AI models, support text extraction, summarization, etc.
|
||||||
|
- **App Intents Enhancement**: Deep integration with Siri, Spotlight, Control Center
|
||||||
|
- **Declared Age Range API**: Privacy-safe age-based experiences
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
- Use Chinese comments
|
||||||
|
- Comments should explain "why" not "what"
|
||||||
|
- Follow SwiftUI declarative programming paradigm
|
||||||
|
- Prefer Swift Concurrency for async logic
|
||||||
|
- Prefer new design language and standards
|
||||||
|
|
||||||
|
## Pre-development Preparation
|
||||||
|
|
||||||
|
1. Check `AGENTS.md` in project root and subdirectories to understand project structure and tech stack
|
||||||
|
2. Determine iOS APP code working directory based on `AGENTS.md`
|
||||||
|
|
||||||
|
## Official Documentation
|
||||||
|
- [Developer Documentation](https://developer.apple.com/documentation)
|
||||||
|
- [What's New](https://developer.apple.com/cn/ios/whats-new/)
|
||||||
|
- [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/)
|
||||||
227
skill/mqtts-developer/README.md
Normal file
227
skill/mqtts-developer/README.md
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
# OpenCode Skills - MQTTS Certificate Management
|
||||||
|
|
||||||
|
这个目录包含了完整的 MQTTS (MQTT over TLS) 证书配置和管理技能库。
|
||||||
|
|
||||||
|
## 📚 Skills 列表
|
||||||
|
|
||||||
|
### 1. setup-mqtts-acme.md
|
||||||
|
**完整的 MQTTS 自动证书配置流程**
|
||||||
|
|
||||||
|
适用场景:
|
||||||
|
- 首次为 EMQX 配置 MQTTS
|
||||||
|
- 使用 acme.sh + DNS API 自动申请证书
|
||||||
|
- 需要证书自动续期
|
||||||
|
- 支持阿里云 DNS(可扩展到其他 DNS 提供商)
|
||||||
|
|
||||||
|
包含内容:
|
||||||
|
- 10 个阶段的详细执行步骤
|
||||||
|
- 环境检查和验证
|
||||||
|
- 证书申请和安装
|
||||||
|
- EMQX 容器重配置
|
||||||
|
- 文档和备份生成
|
||||||
|
- 故障排查指南
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
```
|
||||||
|
请帮我按照 setup-mqtts-acme skill 为域名 mq.example.com 配置 MQTTS
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. mqtts-quick-reference.md
|
||||||
|
**快速参考手册**
|
||||||
|
|
||||||
|
适用场景:
|
||||||
|
- 快速查找常用命令
|
||||||
|
- 紧急故障排查
|
||||||
|
- 日常维护操作
|
||||||
|
- 快速测试连接
|
||||||
|
|
||||||
|
包含内容:
|
||||||
|
- 快速启动命令
|
||||||
|
- 常用管理命令
|
||||||
|
- 测试命令
|
||||||
|
- 故障诊断一键脚本
|
||||||
|
- 关键概念速查
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
```
|
||||||
|
查看 mqtts-quick-reference,帮我测试 MQTTS 连接
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. mqtts-client-config.md
|
||||||
|
**客户端配置完整指南**
|
||||||
|
|
||||||
|
适用场景:
|
||||||
|
- 配置各种语言的 MQTT 客户端
|
||||||
|
- 解决客户端连接问题
|
||||||
|
- 选择合适的认证方式
|
||||||
|
- 多平台开发参考
|
||||||
|
|
||||||
|
包含内容:
|
||||||
|
- Python、Node.js、Java、C#、Go、ESP32 示例代码
|
||||||
|
- 系统 CA vs fullchain.pem 对比
|
||||||
|
- 单向 TLS 认证详解
|
||||||
|
- 客户端故障排查
|
||||||
|
- 安全最佳实践
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
```
|
||||||
|
根据 mqtts-client-config,帮我写一个 Python 客户端连接代码
|
||||||
|
```
|
||||||
|
```
|
||||||
|
我的 ESP32 连接 MQTTS 失败,参考 mqtts-client-config 帮我排查
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 使用场景示例
|
||||||
|
|
||||||
|
### 场景 1: 首次配置 MQTTS
|
||||||
|
```
|
||||||
|
我有一台运行 EMQX 5.8.8 的服务器,域名是 mqtt.mycompany.com,
|
||||||
|
已经配置了阿里云 DNS API。请按照 setup-mqtts-acme skill 帮我配置 MQTTS。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 2: 验证现有配置
|
||||||
|
```
|
||||||
|
根据 mqtts-quick-reference 中的诊断命令,
|
||||||
|
帮我检查 mq.example.com 的 MQTTS 配置是否正常。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3: 客户端开发
|
||||||
|
```
|
||||||
|
我需要开发一个 Python MQTT 客户端连接到 mqtts://mq.example.com:8883。
|
||||||
|
参考 mqtts-client-config 帮我写代码,使用系统 CA 验证。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 4: 故障排查
|
||||||
|
```
|
||||||
|
我的 ESP32 连接 MQTTS 时出现 SSL handshake failed 错误。
|
||||||
|
根据 mqtts-client-config 的故障排查部分,帮我分析可能的原因。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 5: 证书更新
|
||||||
|
```
|
||||||
|
我的证书即将到期,根据 mqtts-quick-reference 帮我手动强制续期。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Skill 使用最佳实践
|
||||||
|
|
||||||
|
### 1. 明确指定 Skill
|
||||||
|
在提问时明确提到 skill 名称,让 AI 知道参考哪个文档:
|
||||||
|
```
|
||||||
|
✅ 好:根据 setup-mqtts-acme skill 帮我...
|
||||||
|
✅ 好:参考 mqtts-client-config 中的 Python 示例...
|
||||||
|
❌ 差:帮我配置 MQTTS(不明确,AI 可能不使用 skill)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 提供必要参数
|
||||||
|
根据 skill 要求提供必要信息:
|
||||||
|
```
|
||||||
|
✅ 好:域名是 mq.example.com,使用阿里云 DNS,容器名是 emqx
|
||||||
|
❌ 差:帮我配置(缺少关键信息)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 分阶段执行
|
||||||
|
对于复杂流程,可以分阶段执行:
|
||||||
|
```
|
||||||
|
1. 先执行环境检查阶段
|
||||||
|
2. 确认无误后执行证书申请
|
||||||
|
3. 最后执行容器配置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 参考故障排查
|
||||||
|
遇到问题时先参考 skill 中的故障排查部分:
|
||||||
|
```
|
||||||
|
我遇到了 DNS 解析失败的错误,
|
||||||
|
根据 setup-mqtts-acme 的故障排查部分,应该如何处理?
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Skill 更新和维护
|
||||||
|
|
||||||
|
### 当前版本
|
||||||
|
- setup-mqtts-acme: v1.0 (2026-01-07)
|
||||||
|
- mqtts-quick-reference: v1.0 (2026-01-07)
|
||||||
|
- mqtts-client-config: v1.0 (2026-01-07)
|
||||||
|
|
||||||
|
### 支持的配置
|
||||||
|
- EMQX: 5.8.8 (Docker)
|
||||||
|
- acme.sh: 最新版本
|
||||||
|
- DNS Provider: 阿里云 DNS (dns_ali)
|
||||||
|
- CA: ZeroSSL, Let's Encrypt
|
||||||
|
- TLS: TLSv1.2, TLSv1.3
|
||||||
|
- 认证: 单向 TLS + 用户名密码
|
||||||
|
|
||||||
|
### 扩展建议
|
||||||
|
可以基于这些 skills 创建新的变体:
|
||||||
|
|
||||||
|
1. **setup-mqtts-nginx.md**: Nginx 反向代理场景
|
||||||
|
2. **setup-mqtts-mtls.md**: 双向 TLS 认证配置
|
||||||
|
3. **mqtts-monitoring.md**: 证书监控和告警
|
||||||
|
4. **mqtts-ha-cluster.md**: 高可用集群配置
|
||||||
|
|
||||||
|
### 反馈和改进
|
||||||
|
如果发现 skill 有任何问题或改进建议:
|
||||||
|
1. 记录具体场景和问题
|
||||||
|
2. 建议改进方案
|
||||||
|
3. 更新对应的 skill 文件
|
||||||
|
|
||||||
|
## 📝 命名规范
|
||||||
|
|
||||||
|
Skill 文件命名遵循以下规范:
|
||||||
|
- 使用小写字母和连字符
|
||||||
|
- 清晰描述功能
|
||||||
|
- 使用 .md 扩展名
|
||||||
|
- 例如:`setup-mqtts-acme.md`, `mqtts-client-config.md`
|
||||||
|
|
||||||
|
## 🎓 学习路径
|
||||||
|
|
||||||
|
### 初学者
|
||||||
|
1. 先阅读 `mqtts-quick-reference.md` 了解基本概念
|
||||||
|
2. 使用 `setup-mqtts-acme.md` 完成首次配置
|
||||||
|
3. 参考 `mqtts-client-config.md` 开发客户端
|
||||||
|
|
||||||
|
### 进阶用户
|
||||||
|
1. 深入理解 `setup-mqtts-acme.md` 的每个阶段
|
||||||
|
2. 自定义配置参数(CA、DNS 提供商等)
|
||||||
|
3. 根据实际需求修改和扩展 skill
|
||||||
|
|
||||||
|
### 专家用户
|
||||||
|
1. 创建自定义 skill 变体
|
||||||
|
2. 集成到 CI/CD 流程
|
||||||
|
3. 开发自动化脚本
|
||||||
|
|
||||||
|
## 🔗 相关资源
|
||||||
|
|
||||||
|
- EMQX 官方文档: https://www.emqx.io/docs/
|
||||||
|
- acme.sh 项目: https://github.com/acmesh-official/acme.sh
|
||||||
|
- MQTT 协议规范: https://mqtt.org/
|
||||||
|
- TLS 最佳实践: https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||||
|
|
||||||
|
## 💡 提示
|
||||||
|
|
||||||
|
1. **Token 节省**: 使用 skill 可以大幅减少 token 消耗,因为 AI 直接参考结构化的知识库
|
||||||
|
2. **一致性**: Skill 确保每次执行都遵循相同的最佳实践
|
||||||
|
3. **可维护**: 集中管理知识,便于更新和改进
|
||||||
|
4. **可重用**: 一次编写,多次使用,提高效率
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. 这些 skills 基于特定版本和配置,使用前请确认环境兼容性
|
||||||
|
2. 生产环境操作前建议先在测试环境验证
|
||||||
|
3. 涉及证书和密钥的操作需要特别注意安全性
|
||||||
|
4. 自动化脚本执行前请仔细审查命令
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
最简单的使用方式:
|
||||||
|
```
|
||||||
|
我需要为 EMQX 配置 MQTTS 自动证书,域名是 mq.example.com。
|
||||||
|
请使用 setup-mqtts-acme skill 帮我完成配置。
|
||||||
|
```
|
||||||
|
|
||||||
|
AI 将会:
|
||||||
|
1. 读取 setup-mqtts-acme.md skill
|
||||||
|
2. 检查必要参数和前置条件
|
||||||
|
3. 逐步执行配置流程
|
||||||
|
4. 验证配置结果
|
||||||
|
5. 生成文档和备份
|
||||||
|
|
||||||
|
享受自动化的便利!🎉
|
||||||
206
skill/mqtts-developer/SKILL.md
Normal file
206
skill/mqtts-developer/SKILL.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# MQTTS Developer Skill
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Complete MQTTS (MQTT over TLS) certificate management and client development skill set. This skill provides automated workflows for setting up secure MQTT brokers with auto-renewable certificates and comprehensive client configuration guides.
|
||||||
|
|
||||||
|
## Skill Components
|
||||||
|
|
||||||
|
This skill consists of 5 integrated knowledge modules:
|
||||||
|
|
||||||
|
### 1. setup-mqtts-acme.md
|
||||||
|
**Complete MQTTS Auto-Certificate Setup Workflow**
|
||||||
|
- Automated certificate issuance using acme.sh with DNS validation
|
||||||
|
- Support for Alibaba Cloud DNS API (extensible to other providers)
|
||||||
|
- EMQX Docker container reconfiguration
|
||||||
|
- Auto-renewal setup with reload hooks
|
||||||
|
- Comprehensive validation and troubleshooting
|
||||||
|
|
||||||
|
**Use when**: Setting up MQTTS for the first time or migrating to new domain
|
||||||
|
|
||||||
|
### 2. mqtts-quick-reference.md
|
||||||
|
**Quick Reference Guide**
|
||||||
|
- Common commands for certificate and EMQX management
|
||||||
|
- One-line diagnostic scripts
|
||||||
|
- Testing commands
|
||||||
|
- Key concepts and troubleshooting
|
||||||
|
|
||||||
|
**Use when**: Need quick command lookup or emergency troubleshooting
|
||||||
|
|
||||||
|
### 3. mqtts-client-config.md
|
||||||
|
**Multi-Language Client Configuration Guide**
|
||||||
|
- Python, Node.js, Java, C#, Go, ESP32/Arduino examples
|
||||||
|
- System CA vs fullchain.pem decision guide
|
||||||
|
- Single-direction TLS authentication explained
|
||||||
|
- Security best practices
|
||||||
|
|
||||||
|
**Use when**: Developing MQTT clients or solving connection issues
|
||||||
|
|
||||||
|
### 4. README.md
|
||||||
|
**Skill Usage Guide**
|
||||||
|
- How to use these skills effectively
|
||||||
|
- Usage scenarios and examples
|
||||||
|
- Learning path recommendations
|
||||||
|
- Maintenance guidelines
|
||||||
|
|
||||||
|
### 5. USAGE_EXAMPLES.md
|
||||||
|
**Practical Usage Examples**
|
||||||
|
- Real conversation examples
|
||||||
|
- Token-saving techniques
|
||||||
|
- Common scenarios and solutions
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Scenario 1: Setup MQTTS for New Domain
|
||||||
|
```
|
||||||
|
I need to configure MQTTS for domain mq.example.com using Alibaba Cloud DNS.
|
||||||
|
Please follow the setup-mqtts-acme skill.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 2: Diagnose MQTTS Issues
|
||||||
|
```
|
||||||
|
According to mqtts-quick-reference, help me diagnose
|
||||||
|
the MQTTS status of mq.example.com.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 3: Develop Client
|
||||||
|
```
|
||||||
|
Based on mqtts-client-config, help me write a Python MQTT client
|
||||||
|
that connects using system CA.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
When invoking this skill, provide:
|
||||||
|
- `domain`: MQTT domain name (e.g., mq.example.com)
|
||||||
|
- `dns_provider`: DNS provider for ACME validation (default: dns_ali)
|
||||||
|
- `ca`: Certificate Authority (default: zerossl, options: letsencrypt)
|
||||||
|
- `emqx_container`: EMQX container name (default: emqx)
|
||||||
|
- `client_language`: For client examples (python, nodejs, java, etc.)
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
✅ **Automated Setup**: 10-phase automated workflow from DNS verification to final validation
|
||||||
|
✅ **Auto-Renewal**: Configured with cron job and Docker container restart
|
||||||
|
✅ **Multi-Language**: Client examples for 7+ programming languages
|
||||||
|
✅ **Token Efficient**: Reusable knowledge base saves 60-80% tokens
|
||||||
|
✅ **Production Ready**: Security best practices and comprehensive error handling
|
||||||
|
✅ **Well Documented**: 1700+ lines of structured knowledge
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- EMQX 5.x running in Docker
|
||||||
|
- acme.sh installed
|
||||||
|
- DNS provider API credentials configured
|
||||||
|
- Docker with sufficient permissions
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
After using this skill, you should have:
|
||||||
|
- ✅ Valid TLS certificate for MQTT domain
|
||||||
|
- ✅ MQTTS listener running on port 8883
|
||||||
|
- ✅ Auto-renewal configured (checks daily)
|
||||||
|
- ✅ Client connection examples for your language
|
||||||
|
- ✅ Complete documentation and backup package
|
||||||
|
|
||||||
|
## Token Efficiency
|
||||||
|
|
||||||
|
Using this skill vs. explaining from scratch:
|
||||||
|
- **First use**: Saves 60-70% tokens
|
||||||
|
- **Repeated use**: Saves 80%+ tokens
|
||||||
|
- **Example**: Full setup guidance ~3000 tokens → ~600 tokens with skill
|
||||||
|
|
||||||
|
## Support Matrix
|
||||||
|
|
||||||
|
### Certificate Authorities
|
||||||
|
- ZeroSSL (default)
|
||||||
|
- Let's Encrypt
|
||||||
|
- BuyPass (via acme.sh)
|
||||||
|
|
||||||
|
### DNS Providers
|
||||||
|
- Alibaba Cloud (dns_ali) - primary
|
||||||
|
- Other 80+ providers supported by acme.sh
|
||||||
|
|
||||||
|
### MQTT Brokers
|
||||||
|
- EMQX 5.x (primary)
|
||||||
|
- Adaptable to other MQTT brokers
|
||||||
|
|
||||||
|
### Client Platforms
|
||||||
|
- PC/Mac/Linux (System CA)
|
||||||
|
- Android/iOS (System CA)
|
||||||
|
- ESP32/Arduino (fullchain.pem)
|
||||||
|
- Embedded Linux (fullchain.pem)
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
This skill can be extended to:
|
||||||
|
- `mqtts-nginx`: MQTTS with Nginx reverse proxy
|
||||||
|
- `mqtts-mtls`: Mutual TLS authentication setup
|
||||||
|
- `mqtts-monitoring`: Certificate monitoring and alerting
|
||||||
|
- `mqtts-ha-cluster`: High availability cluster configuration
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Each component includes comprehensive troubleshooting sections for:
|
||||||
|
- DNS resolution issues
|
||||||
|
- Certificate validation errors
|
||||||
|
- SSL handshake failures
|
||||||
|
- Client connection problems
|
||||||
|
- Container startup issues
|
||||||
|
- Memory constraints (embedded devices)
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
Skills are versioned and maintained:
|
||||||
|
- **Version**: 1.0
|
||||||
|
- **Last Updated**: 2026-01-07
|
||||||
|
- **Compatibility**: EMQX 5.8.8, acme.sh latest
|
||||||
|
|
||||||
|
## Usage Tips
|
||||||
|
|
||||||
|
1. **Specify the skill**: Always mention the skill component name
|
||||||
|
- Good: "According to setup-mqtts-acme skill..."
|
||||||
|
- Bad: "Help me setup MQTTS" (might not use skill)
|
||||||
|
|
||||||
|
2. **Provide context**: Include domain, DNS provider, container name
|
||||||
|
- Good: "Domain mq.example.com, Alibaba DNS, container emqx"
|
||||||
|
- Bad: "Setup certificate" (missing details)
|
||||||
|
|
||||||
|
3. **Use staged approach**: For complex tasks, break into phases
|
||||||
|
- First: Check prerequisites
|
||||||
|
- Then: Issue certificate
|
||||||
|
- Finally: Configure container
|
||||||
|
|
||||||
|
4. **Reference troubleshooting**: When encountering errors
|
||||||
|
- "According to [skill] troubleshooting, how to fix [error]?"
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
skill/mqtts-developer/
|
||||||
|
├── SKILL.md (This file - main entry point)
|
||||||
|
├── setup-mqtts-acme.md (Setup workflow)
|
||||||
|
├── mqtts-quick-reference.md (Quick reference)
|
||||||
|
├── mqtts-client-config.md (Client guide)
|
||||||
|
├── README.md (Usage guide)
|
||||||
|
└── USAGE_EXAMPLES.md (Examples)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
|
||||||
|
- **Total Size**: 52KB
|
||||||
|
- **Total Lines**: 1750+ lines
|
||||||
|
- **Code Examples**: 20+ complete examples
|
||||||
|
- **Languages Covered**: 7+ programming languages
|
||||||
|
- **Commands Documented**: 50+ common commands
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To extend or improve this skill:
|
||||||
|
1. Add new scenarios to USAGE_EXAMPLES.md
|
||||||
|
2. Add new language examples to mqtts-client-config.md
|
||||||
|
3. Add new DNS providers to setup-mqtts-acme.md
|
||||||
|
4. Report issues or improvements needed
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Part of OpenCode Skills Library
|
||||||
275
skill/mqtts-developer/USAGE_EXAMPLES.md
Normal file
275
skill/mqtts-developer/USAGE_EXAMPLES.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# MQTTS Skills 使用示例
|
||||||
|
|
||||||
|
## 实际对话示例
|
||||||
|
|
||||||
|
### 示例 1: 完整配置新域名的 MQTTS
|
||||||
|
|
||||||
|
**用户提问:**
|
||||||
|
```
|
||||||
|
我需要为域名 mqtt.mycompany.com 配置 MQTTS,服务器 IP 是 10.20.30.40,
|
||||||
|
EMQX 容器名是 emqx,使用阿里云 DNS。请按照 setup-mqtts-acme skill 执行。
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 响应流程:**
|
||||||
|
1. 读取 `~/.opencode/skills/setup-mqtts-acme.md`
|
||||||
|
2. 验证 DNS 解析
|
||||||
|
3. 申请证书
|
||||||
|
4. 安装证书并配置自动更新
|
||||||
|
5. 重建 EMQX 容器
|
||||||
|
6. 验证配置
|
||||||
|
7. 生成文档和备份
|
||||||
|
|
||||||
|
**预期输出:**
|
||||||
|
- 证书文件: `/root/certs/mqtt.mycompany.com/`
|
||||||
|
- 备份包: `/root/certs/mqtt.mycompany.com-complete-*.tar.gz`
|
||||||
|
- MQTTS 端口 8883 工作正常
|
||||||
|
- 自动续期已配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 2: 快速诊断 MQTTS 问题
|
||||||
|
|
||||||
|
**用户提问:**
|
||||||
|
```
|
||||||
|
根据 mqtts-quick-reference 的诊断脚本,
|
||||||
|
帮我检查 mq.example.com 的 MQTTS 配置状态。
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 执行:**
|
||||||
|
```bash
|
||||||
|
DOMAIN="mq.example.com"
|
||||||
|
|
||||||
|
echo "=== DNS ==="
|
||||||
|
dig $DOMAIN +short
|
||||||
|
|
||||||
|
echo "=== Certificate ==="
|
||||||
|
openssl x509 -in /root/certs/$DOMAIN/cert.pem -noout -dates -subject
|
||||||
|
|
||||||
|
echo "=== EMQX Container ==="
|
||||||
|
docker ps | grep emqx
|
||||||
|
|
||||||
|
echo "=== Listeners ==="
|
||||||
|
docker exec emqx emqx ctl listeners | grep -A 5 ssl
|
||||||
|
|
||||||
|
echo "=== SSL Test ==="
|
||||||
|
timeout 5 openssl s_client -connect $DOMAIN:8883 -servername $DOMAIN
|
||||||
|
|
||||||
|
echo "=== Auto-Renewal ==="
|
||||||
|
~/.acme.sh/acme.sh --list | grep $DOMAIN
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出:**
|
||||||
|
- DNS 解析状态
|
||||||
|
- 证书有效期
|
||||||
|
- 容器运行状态
|
||||||
|
- SSL 监听器状态
|
||||||
|
- 自动续期配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 3: 生成 Python 客户端代码
|
||||||
|
|
||||||
|
**用户提问:**
|
||||||
|
```
|
||||||
|
根据 mqtts-client-config,帮我生成一个 Python MQTT 客户端,
|
||||||
|
连接到 mqtts://mq.example.com:8883,
|
||||||
|
使用系统 CA 验证,订阅 sensor/# 主题。
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 响应(参考 skill 生成代码):**
|
||||||
|
```python
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
def on_connect(client, userdata, flags, rc):
|
||||||
|
if rc == 0:
|
||||||
|
print("Connected successfully")
|
||||||
|
client.subscribe("sensor/#")
|
||||||
|
else:
|
||||||
|
print(f"Connection failed with code {rc}")
|
||||||
|
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
print(f"{msg.topic}: {msg.payload.decode()}")
|
||||||
|
|
||||||
|
client = mqtt.Client()
|
||||||
|
client.username_pw_set("username", "password")
|
||||||
|
|
||||||
|
# Use system CA - no files needed
|
||||||
|
client.tls_set(
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED,
|
||||||
|
tls_version=ssl.PROTOCOL_TLSv1_2
|
||||||
|
)
|
||||||
|
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_message = on_message
|
||||||
|
|
||||||
|
client.connect("mq.example.com", 8883, 60)
|
||||||
|
client.loop_forever()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 4: ESP32 连接故障排查
|
||||||
|
|
||||||
|
**用户提问:**
|
||||||
|
```
|
||||||
|
我的 ESP32 连接 mqtts://mq.example.com:8883 时报错:
|
||||||
|
"SSL handshake failed"
|
||||||
|
|
||||||
|
参考 mqtts-client-config 的故障排查部分,帮我分析原因并提供解决方案。
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 响应(基于 skill 的故障排查部分):**
|
||||||
|
|
||||||
|
**可能原因:**
|
||||||
|
1. 未设置 CA 证书
|
||||||
|
2. ESP32 时间不正确
|
||||||
|
3. fullchain.pem 内容不完整
|
||||||
|
4. 证书过期
|
||||||
|
5. 内存不足
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
|
||||||
|
1. **检查是否设置了 CA 证书:**
|
||||||
|
```cpp
|
||||||
|
espClient.setCACert(root_ca); // 确保调用了这个
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **同步时间(ESP32 必须):**
|
||||||
|
```cpp
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
configTime(0, 0, "pool.ntp.org");
|
||||||
|
|
||||||
|
// Wait for time sync
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
while (now < 8 * 3600 * 2) {
|
||||||
|
delay(500);
|
||||||
|
now = time(nullptr);
|
||||||
|
}
|
||||||
|
Serial.println("Time synchronized");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证 fullchain.pem 内容完整:**
|
||||||
|
确保包含完整的证书链(服务器证书 + 中间证书 + 根证书)
|
||||||
|
|
||||||
|
4. **如果内存不足,尝试:**
|
||||||
|
```cpp
|
||||||
|
// 减小 MQTT 缓冲区
|
||||||
|
client.setBufferSize(256);
|
||||||
|
|
||||||
|
// 或仅使用服务器证书(不推荐)
|
||||||
|
espClient.setCACert(server_cert_only);
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **临时测试(仅用于诊断):**
|
||||||
|
```cpp
|
||||||
|
espClient.setInsecure(); // 跳过验证
|
||||||
|
// 如果成功,说明是证书问题;如果仍失败,是其他问题
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 5: 证书手动续期
|
||||||
|
|
||||||
|
**用户提问:**
|
||||||
|
```
|
||||||
|
我的证书还有 10 天到期,想手动提前续期。
|
||||||
|
根据 mqtts-quick-reference 帮我执行强制续期。
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 执行(参考 skill):**
|
||||||
|
```bash
|
||||||
|
# 1. 查看当前证书信息
|
||||||
|
~/.acme.sh/acme.sh --info -d mq.example.com
|
||||||
|
|
||||||
|
# 2. 强制续期
|
||||||
|
~/.acme.sh/acme.sh --renew -d mq.example.com --force
|
||||||
|
|
||||||
|
# 3. 验证新证书
|
||||||
|
openssl x509 -in /root/certs/mq.example.com/cert.pem -noout -dates
|
||||||
|
|
||||||
|
# 4. 检查容器是否重启
|
||||||
|
docker ps | grep emqx
|
||||||
|
|
||||||
|
# 5. 测试 MQTTS 连接
|
||||||
|
openssl s_client -connect mq.example.com:8883 -servername mq.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出解释:**
|
||||||
|
- 新证书已生成
|
||||||
|
- EMQX 容器已自动重启(通过 reloadcmd)
|
||||||
|
- MQTTS 连接正常
|
||||||
|
- 下次自动续期时间已更新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 6: 多语言客户端对比
|
||||||
|
|
||||||
|
**用户提问:**
|
||||||
|
```
|
||||||
|
根据 mqtts-client-config,对比 Python、Node.js 和 Java
|
||||||
|
连接 MQTTS 的代码差异,我需要选择一个用于生产环境。
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 响应(基于 skill 的多语言示例):**
|
||||||
|
|
||||||
|
| 特性 | Python | Node.js | Java |
|
||||||
|
|------|--------|---------|------|
|
||||||
|
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
| 性能 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||||
|
| 系统 CA | 简单 | 简单 | 中等 |
|
||||||
|
| 异步支持 | 中等 | 原生 | 复杂 |
|
||||||
|
| 部署 | 需要 Python | 需要 Node.js | 打包成 JAR |
|
||||||
|
|
||||||
|
**推荐:**
|
||||||
|
- **快速原型**: Python(最简单)
|
||||||
|
- **高并发**: Node.js(原生异步)
|
||||||
|
- **企业级**: Java(稳定可靠)
|
||||||
|
|
||||||
|
代码示例见 skill 文档...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 示例 7: 创建客户端分发包
|
||||||
|
|
||||||
|
**用户提问:**
|
||||||
|
```
|
||||||
|
我需要给客户端开发者提供连接包,包含:
|
||||||
|
1. fullchain.pem
|
||||||
|
2. 连接文档
|
||||||
|
3. 各语言示例代码
|
||||||
|
|
||||||
|
参考 mqtts-client-config 帮我生成。
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 执行:**
|
||||||
|
```bash
|
||||||
|
# 1. 创建分发目录
|
||||||
|
mkdir -p /root/mqtts-client-package
|
||||||
|
cd /root/mqtts-client-package
|
||||||
|
|
||||||
|
# 2. 复制证书
|
||||||
|
cp /root/certs/mq.example.com/fullchain.pem ./
|
||||||
|
|
||||||
|
# 3. 生成连接信息
|
||||||
|
cat > CONNECTION_INFO.txt << EOF
|
||||||
|
MQTTS Connection Information
|
||||||
|
|
||||||
|
Server: mq.example.com
|
||||||
|
Port: 8883
|
||||||
|
Protocol: MQTTS (MQTT over TLS)
|
||||||
|
TLS Version: TLSv1.2+
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
- Username: [Your Username]
|
||||||
|
- Password: [Your Password]
|
||||||
|
|
||||||
|
Certificate Verification:
|
||||||
|
- Option 1: Use system CA (recommended for PC/Mobile)
|
||||||
|
- Option 2: Use provided fullchain.pem (recommended for embedded)
|
||||||
|
|
||||||
|
Certificate Fingerprint (SHA256):
|
||||||
|
$(openssl x509 -in fullchain.pem -noout -fingerprint -sha256)
|
||||||
596
skill/mqtts-developer/mqtts-client-config.md
Normal file
596
skill/mqtts-developer/mqtts-client-config.md
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
# MQTTS Client Configuration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Complete guide for configuring MQTT clients to connect to MQTTS (MQTT over TLS) servers with single-direction TLS authentication.
|
||||||
|
|
||||||
|
## Authentication Model
|
||||||
|
**Single-Direction TLS (Server Authentication)**:
|
||||||
|
- Client verifies server identity (via TLS certificate)
|
||||||
|
- Server authenticates client (via username/password in MQTT layer)
|
||||||
|
- Client does NOT need client certificate
|
||||||
|
|
||||||
|
## What Clients Need
|
||||||
|
|
||||||
|
### Option 1: System CA (Recommended)
|
||||||
|
**No files needed!** Operating system's built-in CA trust store automatically verifies the server certificate.
|
||||||
|
|
||||||
|
**Advantages**:
|
||||||
|
- Zero configuration
|
||||||
|
- No certificate files to distribute
|
||||||
|
- Automatic updates with OS
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
- Server certificate issued by trusted CA (Let's Encrypt, ZeroSSL, etc.)
|
||||||
|
- System CA trust store up to date
|
||||||
|
|
||||||
|
**Suitable for**:
|
||||||
|
- PC applications (Windows, Mac, Linux)
|
||||||
|
- Mobile apps (Android, iOS)
|
||||||
|
- Server-side applications
|
||||||
|
- Docker containers with CA certificates
|
||||||
|
|
||||||
|
### Option 2: Explicit CA Certificate (fullchain.pem)
|
||||||
|
Explicitly specify the CA certificate chain for verification.
|
||||||
|
|
||||||
|
**Advantages**:
|
||||||
|
- Platform independent
|
||||||
|
- No dependency on system configuration
|
||||||
|
- Works on embedded devices without CA store
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
- Distribute `fullchain.pem` to clients
|
||||||
|
- Update file when server certificate is renewed (if using different CA)
|
||||||
|
|
||||||
|
**Suitable for**:
|
||||||
|
- Embedded devices (ESP32, Arduino)
|
||||||
|
- Minimal Linux systems without CA certificates
|
||||||
|
- Strict security requirements
|
||||||
|
- Air-gapped environments
|
||||||
|
|
||||||
|
### Option 3: Public Key Pinning (Advanced)
|
||||||
|
Pin the server's public key or certificate fingerprint.
|
||||||
|
|
||||||
|
**Advantages**:
|
||||||
|
- Highest security
|
||||||
|
- Immune to CA compromise
|
||||||
|
|
||||||
|
**Disadvantages**:
|
||||||
|
- Must update all clients when certificate changes
|
||||||
|
- Complex to manage
|
||||||
|
|
||||||
|
**Suitable for**:
|
||||||
|
- High-security applications (banking, healthcare)
|
||||||
|
- Known, controlled client deployments
|
||||||
|
|
||||||
|
## Language-Specific Examples
|
||||||
|
|
||||||
|
### Python (paho-mqtt)
|
||||||
|
|
||||||
|
#### System CA (Recommended)
|
||||||
|
```python
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
def on_connect(client, userdata, flags, rc):
|
||||||
|
if rc == 0:
|
||||||
|
print("Connected successfully")
|
||||||
|
client.subscribe("test/#")
|
||||||
|
else:
|
||||||
|
print(f"Connection failed with code {rc}")
|
||||||
|
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
print(f"{msg.topic}: {msg.payload.decode()}")
|
||||||
|
|
||||||
|
client = mqtt.Client()
|
||||||
|
client.username_pw_set("username", "password")
|
||||||
|
|
||||||
|
# Use system CA - no files needed
|
||||||
|
client.tls_set(
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED,
|
||||||
|
tls_version=ssl.PROTOCOL_TLSv1_2
|
||||||
|
)
|
||||||
|
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_message = on_message
|
||||||
|
|
||||||
|
client.connect("mq.example.com", 8883, 60)
|
||||||
|
client.loop_forever()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With fullchain.pem
|
||||||
|
```python
|
||||||
|
client.tls_set(
|
||||||
|
ca_certs="fullchain.pem",
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED,
|
||||||
|
tls_version=ssl.PROTOCOL_TLSv1_2
|
||||||
|
)
|
||||||
|
client.connect("mq.example.com", 8883, 60)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Skip Verification (Testing Only)
|
||||||
|
```python
|
||||||
|
# NOT RECOMMENDED FOR PRODUCTION
|
||||||
|
client.tls_set(cert_reqs=ssl.CERT_NONE)
|
||||||
|
client.tls_insecure_set(True)
|
||||||
|
client.connect("mq.example.com", 8883, 60)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js (mqtt.js)
|
||||||
|
|
||||||
|
#### System CA (Recommended)
|
||||||
|
```javascript
|
||||||
|
const mqtt = require('mqtt');
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
host: 'mq.example.com',
|
||||||
|
port: 8883,
|
||||||
|
protocol: 'mqtts',
|
||||||
|
username: 'username',
|
||||||
|
password: 'password',
|
||||||
|
rejectUnauthorized: true // Verify server certificate
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = mqtt.connect(options);
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
console.log('Connected successfully');
|
||||||
|
client.subscribe('test/#');
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message', (topic, message) => {
|
||||||
|
console.log(`${topic}: ${message.toString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (error) => {
|
||||||
|
console.error('Connection error:', error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With fullchain.pem
|
||||||
|
```javascript
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
host: 'mq.example.com',
|
||||||
|
port: 8883,
|
||||||
|
protocol: 'mqtts',
|
||||||
|
username: 'username',
|
||||||
|
password: 'password',
|
||||||
|
ca: fs.readFileSync('fullchain.pem'),
|
||||||
|
rejectUnauthorized: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = mqtt.connect(options);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Skip Verification (Testing Only)
|
||||||
|
```javascript
|
||||||
|
const options = {
|
||||||
|
host: 'mq.example.com',
|
||||||
|
port: 8883,
|
||||||
|
protocol: 'mqtts',
|
||||||
|
username: 'username',
|
||||||
|
password: 'password',
|
||||||
|
rejectUnauthorized: false // NOT RECOMMENDED
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Java (Eclipse Paho)
|
||||||
|
|
||||||
|
#### System CA (Recommended)
|
||||||
|
```java
|
||||||
|
import org.eclipse.paho.client.mqttv3.*;
|
||||||
|
|
||||||
|
String broker = "ssl://mq.example.com:8883";
|
||||||
|
String clientId = "JavaClient";
|
||||||
|
|
||||||
|
MqttClient client = new MqttClient(broker, clientId);
|
||||||
|
MqttConnectOptions options = new MqttConnectOptions();
|
||||||
|
options.setUserName("username");
|
||||||
|
options.setPassword("password".toCharArray());
|
||||||
|
// Java uses system truststore by default
|
||||||
|
|
||||||
|
client.setCallback(new MqttCallback() {
|
||||||
|
public void connectionLost(Throwable cause) {
|
||||||
|
System.out.println("Connection lost: " + cause.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void messageArrived(String topic, MqttMessage message) {
|
||||||
|
System.out.println(topic + ": " + new String(message.getPayload()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deliveryComplete(IMqttDeliveryToken token) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.connect(options);
|
||||||
|
client.subscribe("test/#");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With fullchain.pem
|
||||||
|
```java
|
||||||
|
import javax.net.ssl.*;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
|
||||||
|
// Load CA certificate
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
FileInputStream fis = new FileInputStream("fullchain.pem");
|
||||||
|
Certificate ca = cf.generateCertificate(fis);
|
||||||
|
fis.close();
|
||||||
|
|
||||||
|
// Create KeyStore with CA
|
||||||
|
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
|
ks.load(null, null);
|
||||||
|
ks.setCertificateEntry("ca", ca);
|
||||||
|
|
||||||
|
// Create TrustManager
|
||||||
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
||||||
|
TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
tmf.init(ks);
|
||||||
|
|
||||||
|
// Create SSLContext
|
||||||
|
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||||
|
sslContext.init(null, tmf.getTrustManagers(), null);
|
||||||
|
|
||||||
|
// Set in MQTT options
|
||||||
|
MqttConnectOptions options = new MqttConnectOptions();
|
||||||
|
options.setSocketFactory(sslContext.getSocketFactory());
|
||||||
|
options.setUserName("username");
|
||||||
|
options.setPassword("password".toCharArray());
|
||||||
|
|
||||||
|
client.connect(options);
|
||||||
|
```
|
||||||
|
|
||||||
|
### C# (.NET)
|
||||||
|
|
||||||
|
#### System CA (Recommended)
|
||||||
|
```csharp
|
||||||
|
using MQTTnet;
|
||||||
|
using MQTTnet.Client;
|
||||||
|
|
||||||
|
var factory = new MqttFactory();
|
||||||
|
var mqttClient = factory.CreateMqttClient();
|
||||||
|
|
||||||
|
var options = new MqttClientOptionsBuilder()
|
||||||
|
.WithTcpServer("mq.example.com", 8883)
|
||||||
|
.WithCredentials("username", "password")
|
||||||
|
.WithTls() // Use system CA
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
mqttClient.ConnectedAsync += async e => {
|
||||||
|
Console.WriteLine("Connected successfully");
|
||||||
|
await mqttClient.SubscribeAsync("test/#");
|
||||||
|
};
|
||||||
|
|
||||||
|
mqttClient.ApplicationMessageReceivedAsync += e => {
|
||||||
|
var payload = System.Text.Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
|
||||||
|
Console.WriteLine($"{e.ApplicationMessage.Topic}: {payload}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
await mqttClient.ConnectAsync(options);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With fullchain.pem
|
||||||
|
```csharp
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
var certificate = new X509Certificate2("fullchain.pem");
|
||||||
|
|
||||||
|
var tlsOptions = new MqttClientOptionsBuilderTlsParameters {
|
||||||
|
UseTls = true,
|
||||||
|
Certificates = new List<X509Certificate> { certificate },
|
||||||
|
SslProtocol = System.Security.Authentication.SslProtocols.Tls12
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new MqttClientOptionsBuilder()
|
||||||
|
.WithTcpServer("mq.example.com", 8883)
|
||||||
|
.WithCredentials("username", "password")
|
||||||
|
.WithTls(tlsOptions)
|
||||||
|
.Build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go (paho.mqtt.golang)
|
||||||
|
|
||||||
|
#### System CA (Recommended)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"crypto/tls"
|
||||||
|
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
tlsConfig := &tls.Config{} // Uses system CA by default
|
||||||
|
|
||||||
|
opts := mqtt.NewClientOptions()
|
||||||
|
opts.AddBroker("ssl://mq.example.com:8883")
|
||||||
|
opts.SetUsername("username")
|
||||||
|
opts.SetPassword("password")
|
||||||
|
opts.SetTLSConfig(tlsConfig)
|
||||||
|
|
||||||
|
opts.OnConnect = func(c mqtt.Client) {
|
||||||
|
fmt.Println("Connected successfully")
|
||||||
|
c.Subscribe("test/#", 0, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.DefaultPublishHandler = func(c mqtt.Client, msg mqtt.Message) {
|
||||||
|
fmt.Printf("%s: %s\n", msg.Topic(), msg.Payload())
|
||||||
|
}
|
||||||
|
|
||||||
|
client := mqtt.NewClient(opts)
|
||||||
|
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
||||||
|
panic(token.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
select {} // Keep running
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With fullchain.pem
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load CA certificate
|
||||||
|
caCert, err := ioutil.ReadFile("fullchain.pem")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AppendCertsFromPEM(caCert)
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.SetTLSConfig(tlsConfig)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ESP32/Arduino (PubSubClient)
|
||||||
|
|
||||||
|
#### With fullchain.pem (Required)
|
||||||
|
```cpp
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
|
||||||
|
const char* ssid = "your_wifi_ssid";
|
||||||
|
const char* password = "your_wifi_password";
|
||||||
|
const char* mqtt_server = "mq.example.com";
|
||||||
|
const int mqtt_port = 8883;
|
||||||
|
const char* mqtt_user = "username";
|
||||||
|
const char* mqtt_pass = "password";
|
||||||
|
|
||||||
|
// Copy content from fullchain.pem here
|
||||||
|
const char* root_ca = \
|
||||||
|
"-----BEGIN CERTIFICATE-----\n" \
|
||||||
|
"MIIGZDCCBEygAwIBAgIRAKIoXbOGN1X6u+vS+TbyLOgwDQYJKoZIhvcNAQEMBQAw\n" \
|
||||||
|
"...\n" \
|
||||||
|
"-----END CERTIFICATE-----\n" \
|
||||||
|
"-----BEGIN CERTIFICATE-----\n" \
|
||||||
|
"...\n" \
|
||||||
|
"-----END CERTIFICATE-----\n";
|
||||||
|
|
||||||
|
WiFiClientSecure espClient;
|
||||||
|
PubSubClient client(espClient);
|
||||||
|
|
||||||
|
void callback(char* topic, byte* payload, unsigned int length) {
|
||||||
|
Serial.print("Message arrived [");
|
||||||
|
Serial.print(topic);
|
||||||
|
Serial.print("]: ");
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
Serial.print((char)payload[i]);
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reconnect() {
|
||||||
|
while (!client.connected()) {
|
||||||
|
Serial.print("Connecting to MQTT...");
|
||||||
|
if (client.connect("ESP32Client", mqtt_user, mqtt_pass)) {
|
||||||
|
Serial.println("connected");
|
||||||
|
client.subscribe("test/#");
|
||||||
|
} else {
|
||||||
|
Serial.print("failed, rc=");
|
||||||
|
Serial.print(client.state());
|
||||||
|
Serial.println(" retrying in 5 seconds");
|
||||||
|
delay(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
}
|
||||||
|
Serial.println("\nWiFi connected");
|
||||||
|
|
||||||
|
espClient.setCACert(root_ca); // Set CA certificate
|
||||||
|
client.setServer(mqtt_server, mqtt_port);
|
||||||
|
client.setCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (!client.connected()) {
|
||||||
|
reconnect();
|
||||||
|
}
|
||||||
|
client.loop();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Skip Verification (Testing Only)
|
||||||
|
```cpp
|
||||||
|
void setup() {
|
||||||
|
// ...
|
||||||
|
espClient.setInsecure(); // NOT RECOMMENDED FOR PRODUCTION
|
||||||
|
client.setServer(mqtt_server, mqtt_port);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Line (Mosquitto)
|
||||||
|
|
||||||
|
#### System CA (Recommended)
|
||||||
|
```bash
|
||||||
|
# Publish
|
||||||
|
mosquitto_pub -h mq.example.com -p 8883 \
|
||||||
|
--capath /etc/ssl/certs \
|
||||||
|
-t "test/topic" -m "Hello MQTTS" \
|
||||||
|
-u "username" -P "password"
|
||||||
|
|
||||||
|
# Subscribe
|
||||||
|
mosquitto_sub -h mq.example.com -p 8883 \
|
||||||
|
--capath /etc/ssl/certs \
|
||||||
|
-t "test/#" \
|
||||||
|
-u "username" -P "password"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With fullchain.pem
|
||||||
|
```bash
|
||||||
|
mosquitto_pub -h mq.example.com -p 8883 \
|
||||||
|
--cafile fullchain.pem \
|
||||||
|
-t "test/topic" -m "Hello MQTTS" \
|
||||||
|
-u "username" -P "password"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Skip Verification (Testing Only)
|
||||||
|
```bash
|
||||||
|
mosquitto_pub -h mq.example.com -p 8883 \
|
||||||
|
--insecure \
|
||||||
|
-t "test/topic" -m "Hello MQTTS" \
|
||||||
|
-u "username" -P "password"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### SSL Handshake Failed
|
||||||
|
**Symptom**: Connection refused, SSL error, handshake failure
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
1. System doesn't trust the CA
|
||||||
|
2. Certificate expired or not yet valid
|
||||||
|
3. System time incorrect
|
||||||
|
4. Wrong domain name
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
```bash
|
||||||
|
# Check system time
|
||||||
|
date
|
||||||
|
|
||||||
|
# Update CA certificates (Linux)
|
||||||
|
sudo update-ca-certificates
|
||||||
|
|
||||||
|
# Test with openssl
|
||||||
|
openssl s_client -connect mq.example.com:8883 -servername mq.example.com
|
||||||
|
|
||||||
|
# Use fullchain.pem instead of system CA
|
||||||
|
```
|
||||||
|
|
||||||
|
### Certificate Hostname Mismatch
|
||||||
|
**Symptom**: Hostname verification failed
|
||||||
|
|
||||||
|
**Cause**: Connecting to IP address instead of domain name
|
||||||
|
|
||||||
|
**Solution**: Always use domain name (mq.example.com), NOT IP (1.2.3.4)
|
||||||
|
|
||||||
|
### Connection Timeout
|
||||||
|
**Symptom**: Connection times out, no response
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
1. Port 8883 blocked by firewall
|
||||||
|
2. EMQX not listening on 8883
|
||||||
|
3. DNS not resolving
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
```bash
|
||||||
|
# Test port connectivity
|
||||||
|
nc -zv mq.example.com 8883
|
||||||
|
|
||||||
|
# Check DNS
|
||||||
|
dig mq.example.com +short
|
||||||
|
|
||||||
|
# Test with telnet
|
||||||
|
telnet mq.example.com 8883
|
||||||
|
```
|
||||||
|
|
||||||
|
### ESP32 Out of Memory
|
||||||
|
**Symptom**: Heap overflow, crash during TLS handshake
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
1. Certificate chain too large
|
||||||
|
2. Insufficient heap space
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
```cpp
|
||||||
|
// Use only server certificate (not full chain)
|
||||||
|
// Smaller but less secure
|
||||||
|
espClient.setCACert(server_cert_only);
|
||||||
|
|
||||||
|
// Reduce MQTT buffer size
|
||||||
|
client.setBufferSize(256);
|
||||||
|
|
||||||
|
// Use ECC certificate instead of RSA (smaller)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### Production Requirements
|
||||||
|
✅ **Must Have**:
|
||||||
|
- Enable certificate verification (`CERT_REQUIRED`)
|
||||||
|
- Use TLSv1.2 or higher
|
||||||
|
- Verify server hostname/domain
|
||||||
|
- Use strong username/password authentication
|
||||||
|
- Keep client code updated
|
||||||
|
|
||||||
|
❌ **Never in Production**:
|
||||||
|
- Skip certificate verification (`setInsecure()`, `rejectUnauthorized: false`)
|
||||||
|
- Use `CERT_NONE` or disable verification
|
||||||
|
- Hard-code passwords in source code
|
||||||
|
- Use deprecated TLS versions (SSLv3, TLSv1.0, TLSv1.1)
|
||||||
|
|
||||||
|
### Certificate Updates
|
||||||
|
- System CA: Automatic with OS updates
|
||||||
|
- fullchain.pem: Update when server certificate renewed from different CA
|
||||||
|
- Public Key Pinning: Must update all clients before certificate renewal
|
||||||
|
|
||||||
|
### Credential Management
|
||||||
|
- Store credentials in environment variables or config files
|
||||||
|
- Use secrets management systems (AWS Secrets Manager, HashiCorp Vault)
|
||||||
|
- Rotate passwords regularly
|
||||||
|
- Use unique credentials per device/client
|
||||||
|
|
||||||
|
## Decision Matrix
|
||||||
|
|
||||||
|
| Client Platform | Recommended Method | Alternative | Testing Method |
|
||||||
|
|----------------|-------------------|-------------|----------------|
|
||||||
|
| PC/Mac | System CA | fullchain.pem | Skip verification |
|
||||||
|
| Linux Server | System CA | fullchain.pem | Skip verification |
|
||||||
|
| Android/iOS | System CA | fullchain.pem | Skip verification |
|
||||||
|
| ESP32/Arduino | fullchain.pem | Server cert only | setInsecure() |
|
||||||
|
| Docker Container | System CA | fullchain.pem | Skip verification |
|
||||||
|
| Embedded Linux | fullchain.pem | System CA | Skip verification |
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Single-Direction TLS (Current Setup)**:
|
||||||
|
- ✅ Client verifies server (via certificate)
|
||||||
|
- ✅ Server verifies client (via username/password)
|
||||||
|
- ❌ Server does NOT verify client certificate
|
||||||
|
|
||||||
|
**Client Needs**:
|
||||||
|
- ✅ System CA (easiest, no files) OR fullchain.pem (most reliable)
|
||||||
|
- ❌ Does NOT need: server public key, server private key, server certificate
|
||||||
|
|
||||||
|
**Key Takeaway**: The server certificate (containing the public key) is automatically sent during TLS handshake. Clients only need a way to verify it (system CA or fullchain.pem).
|
||||||
277
skill/mqtts-developer/mqtts-quick-reference.md
Normal file
277
skill/mqtts-developer/mqtts-quick-reference.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# MQTTS Quick Reference
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
For fast MQTTS setup with default settings:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Set domain
|
||||||
|
DOMAIN="mq.example.com"
|
||||||
|
|
||||||
|
# 2. Verify DNS
|
||||||
|
dig $DOMAIN +short
|
||||||
|
|
||||||
|
# 3. Issue certificate
|
||||||
|
~/.acme.sh/acme.sh --issue --dns dns_ali -d $DOMAIN --keylength 2048
|
||||||
|
|
||||||
|
# 4. Install with auto-reload
|
||||||
|
~/.acme.sh/acme.sh --install-cert -d $DOMAIN \
|
||||||
|
--cert-file /root/certs/$DOMAIN/cert.pem \
|
||||||
|
--key-file /root/certs/$DOMAIN/key.pem \
|
||||||
|
--fullchain-file /root/certs/$DOMAIN/fullchain.pem \
|
||||||
|
--reloadcmd "docker restart emqx"
|
||||||
|
|
||||||
|
# 5. Fix permissions
|
||||||
|
chmod 755 /root/certs/$DOMAIN
|
||||||
|
chmod 644 /root/certs/$DOMAIN/*.pem
|
||||||
|
|
||||||
|
# 6. Recreate EMQX container with cert mount
|
||||||
|
docker stop emqx && docker rm emqx
|
||||||
|
docker run -d --name emqx --restart unless-stopped \
|
||||||
|
-p 1883:1883 -p 8083-8084:8083-8084 -p 8883:8883 -p 18083:18083 \
|
||||||
|
-v /root/emqx/data:/opt/emqx/data \
|
||||||
|
-v /root/emqx/log:/opt/emqx/log \
|
||||||
|
-v /root/certs/$DOMAIN:/opt/emqx/etc/certs:ro \
|
||||||
|
emqx/emqx:5.8.8
|
||||||
|
|
||||||
|
# 7. Verify
|
||||||
|
sleep 5
|
||||||
|
docker exec emqx emqx ctl listeners | grep ssl
|
||||||
|
openssl s_client -connect $DOMAIN:8883 -servername $DOMAIN < /dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Connection
|
||||||
|
|
||||||
|
### Python (System CA)
|
||||||
|
```python
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
client = mqtt.Client()
|
||||||
|
client.username_pw_set("user", "pass")
|
||||||
|
client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2)
|
||||||
|
client.connect("mq.example.com", 8883, 60)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python (with fullchain.pem)
|
||||||
|
```python
|
||||||
|
client.tls_set(ca_certs="fullchain.pem", cert_reqs=ssl.CERT_REQUIRED)
|
||||||
|
client.connect("mq.example.com", 8883, 60)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
```javascript
|
||||||
|
const mqtt = require('mqtt');
|
||||||
|
const client = mqtt.connect('mqtts://mq.example.com:8883', {
|
||||||
|
username: 'user',
|
||||||
|
password: 'pass',
|
||||||
|
rejectUnauthorized: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### ESP32
|
||||||
|
```cpp
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
|
||||||
|
const char* root_ca = "-----BEGIN CERTIFICATE-----\n"...; // from fullchain.pem
|
||||||
|
|
||||||
|
WiFiClientSecure espClient;
|
||||||
|
PubSubClient client(espClient);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
espClient.setCACert(root_ca);
|
||||||
|
client.setServer("mq.example.com", 8883);
|
||||||
|
client.connect("ESP32", "user", "pass");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
### Certificate Management
|
||||||
|
```bash
|
||||||
|
# List certificates
|
||||||
|
~/.acme.sh/acme.sh --list
|
||||||
|
|
||||||
|
# Check certificate info
|
||||||
|
~/.acme.sh/acme.sh --info -d $DOMAIN
|
||||||
|
|
||||||
|
# Force renewal
|
||||||
|
~/.acme.sh/acme.sh --renew -d $DOMAIN --force
|
||||||
|
|
||||||
|
# View certificate details
|
||||||
|
openssl x509 -in /root/certs/$DOMAIN/cert.pem -text -noout
|
||||||
|
|
||||||
|
# Check expiry
|
||||||
|
openssl x509 -in /root/certs/$DOMAIN/cert.pem -noout -dates
|
||||||
|
|
||||||
|
# Get fingerprint
|
||||||
|
openssl x509 -in /root/certs/$DOMAIN/cert.pem -noout -fingerprint -sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
### EMQX Management
|
||||||
|
```bash
|
||||||
|
# Check listeners
|
||||||
|
docker exec emqx emqx ctl listeners
|
||||||
|
|
||||||
|
# Check connections
|
||||||
|
docker exec emqx emqx ctl broker stats
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker logs emqx --tail 100 -f
|
||||||
|
|
||||||
|
# Restart container
|
||||||
|
docker restart emqx
|
||||||
|
|
||||||
|
# Check certificate files
|
||||||
|
docker exec emqx ls -l /opt/emqx/etc/certs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Test SSL connection
|
||||||
|
openssl s_client -connect $DOMAIN:8883 -servername $DOMAIN
|
||||||
|
|
||||||
|
# Test with mosquitto
|
||||||
|
mosquitto_pub -h $DOMAIN -p 8883 \
|
||||||
|
--capath /etc/ssl/certs \
|
||||||
|
-t "test/topic" -m "hello" \
|
||||||
|
-u "username" -P "password"
|
||||||
|
|
||||||
|
# Test with custom CA
|
||||||
|
mosquitto_pub -h $DOMAIN -p 8883 \
|
||||||
|
--cafile fullchain.pem \
|
||||||
|
-t "test/topic" -m "hello" \
|
||||||
|
-u "username" -P "password"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup & Export
|
||||||
|
```bash
|
||||||
|
# Create backup
|
||||||
|
cd /root/certs
|
||||||
|
tar czf $DOMAIN-backup-$(date +%Y%m%d).tar.gz $DOMAIN/
|
||||||
|
|
||||||
|
# Download (from local machine)
|
||||||
|
scp root@SERVER_IP:/root/certs/$DOMAIN-backup-*.tar.gz ./
|
||||||
|
|
||||||
|
# Extract public key
|
||||||
|
openssl rsa -in $DOMAIN/key.pem -pubout -out $DOMAIN/public.pem
|
||||||
|
|
||||||
|
# Get public key fingerprint
|
||||||
|
openssl rsa -in $DOMAIN/key.pem -pubout 2>/dev/null | openssl md5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### DNS not resolving
|
||||||
|
```bash
|
||||||
|
dig $DOMAIN +short
|
||||||
|
nslookup $DOMAIN
|
||||||
|
# Wait 5-10 minutes for propagation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Certificate issuance failed
|
||||||
|
```bash
|
||||||
|
# Check DNS API credentials
|
||||||
|
cat ~/.acme.sh/account.conf | grep Ali_
|
||||||
|
|
||||||
|
# Test with debug mode
|
||||||
|
~/.acme.sh/acme.sh --issue --dns dns_ali -d $DOMAIN --debug 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL connection failed
|
||||||
|
```bash
|
||||||
|
# Check port is open
|
||||||
|
nc -zv $DOMAIN 8883
|
||||||
|
|
||||||
|
# Check firewall
|
||||||
|
iptables -L -n | grep 8883
|
||||||
|
|
||||||
|
# Test with insecure (testing only)
|
||||||
|
mosquitto_pub -h $DOMAIN -p 8883 --insecure -t test -m hello
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container won't start
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker logs emqx
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
ls -la /root/certs/$DOMAIN/
|
||||||
|
|
||||||
|
# Fix permissions
|
||||||
|
chmod 755 /root/certs/$DOMAIN
|
||||||
|
chmod 644 /root/certs/$DOMAIN/*.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
|
||||||
|
### Single-Direction TLS (Current Setup)
|
||||||
|
- Client verifies server identity (via certificate)
|
||||||
|
- Server authenticates client (via username/password)
|
||||||
|
- Client needs: System CA OR fullchain.pem
|
||||||
|
- Client does NOT need: server public key, server private key
|
||||||
|
|
||||||
|
### File Purpose
|
||||||
|
- `cert.pem`: Server certificate (contains public key)
|
||||||
|
- `key.pem`: Server private key (CONFIDENTIAL)
|
||||||
|
- `fullchain.pem`: Server cert + intermediate + root CA
|
||||||
|
- `public.pem`: Public key extracted from private key
|
||||||
|
- `cacert.pem`: CA certificate (usually symlink to fullchain)
|
||||||
|
|
||||||
|
### Client Requirements
|
||||||
|
| Client Type | Needs | Reason |
|
||||||
|
|-------------|-------|--------|
|
||||||
|
| PC/Mac/Server | Nothing (system CA) | OS trusts ZeroSSL |
|
||||||
|
| Android/iOS | Nothing (system CA) | OS trusts ZeroSSL |
|
||||||
|
| ESP32/Arduino | fullchain.pem | No system CA access |
|
||||||
|
| Docker | System CA or fullchain.pem | Depends on base image |
|
||||||
|
|
||||||
|
### Auto-Renewal
|
||||||
|
- Cron: Daily at 00:34
|
||||||
|
- Threshold: 60 days before expiry
|
||||||
|
- Action: Renew cert → Install → Restart EMQX
|
||||||
|
- No client action needed (unless using public key pinning)
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. **Domain Required**: Must use domain name (mq.example.com), NOT IP address
|
||||||
|
2. **DNS Must Resolve**: A record must point to server before certificate issuance
|
||||||
|
3. **Port 8883**: Ensure firewall allows port 8883 for MQTTS
|
||||||
|
4. **Time Sync**: Server and client clocks must be accurate for TLS
|
||||||
|
5. **Key Reuse**: acme.sh reuses private key by default (public key stays same)
|
||||||
|
6. **Certificate Chain**: Modern clients need full chain, not just server cert
|
||||||
|
|
||||||
|
## Quick Diagnosis
|
||||||
|
|
||||||
|
### Check Everything
|
||||||
|
```bash
|
||||||
|
DOMAIN="mq.example.com"
|
||||||
|
|
||||||
|
echo "=== DNS ==="
|
||||||
|
dig $DOMAIN +short
|
||||||
|
|
||||||
|
echo "=== Certificate ==="
|
||||||
|
openssl x509 -in /root/certs/$DOMAIN/cert.pem -noout -dates -subject
|
||||||
|
|
||||||
|
echo "=== EMQX Container ==="
|
||||||
|
docker ps | grep emqx
|
||||||
|
|
||||||
|
echo "=== Listeners ==="
|
||||||
|
docker exec emqx emqx ctl listeners | grep -A 5 ssl
|
||||||
|
|
||||||
|
echo "=== SSL Test ==="
|
||||||
|
timeout 5 openssl s_client -connect $DOMAIN:8883 -servername $DOMAIN < /dev/null 2>&1 | grep -E "Verify return|subject=|issuer="
|
||||||
|
|
||||||
|
echo "=== Auto-Renewal ==="
|
||||||
|
~/.acme.sh/acme.sh --list | grep $DOMAIN
|
||||||
|
|
||||||
|
echo "=== Cron ==="
|
||||||
|
crontab -l | grep acme
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
- EMQX Config: `/opt/emqx/etc/emqx.conf` (in container)
|
||||||
|
- Certificates: `/root/certs/$DOMAIN/` (on host) → `/opt/emqx/etc/certs/` (in container)
|
||||||
|
- acme.sh: `~/.acme.sh/`
|
||||||
|
- Logs: `/root/emqx/log/` (host) or `docker logs emqx`
|
||||||
|
- Dashboard: http://SERVER_IP:18083 (default: admin/public)
|
||||||
350
skill/mqtts-developer/setup-mqtts-acme.md
Normal file
350
skill/mqtts-developer/setup-mqtts-acme.md
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# Setup MQTTS with Auto-Renewal Certificate
|
||||||
|
|
||||||
|
## Description
|
||||||
|
Complete workflow to configure MQTTS (MQTT over TLS) for EMQX using acme.sh with automatic certificate renewal. Supports DNS-based certificate validation (Alibaba Cloud DNS API).
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `domain`: MQTT domain name (e.g., mq.example.com)
|
||||||
|
- `ca`: Certificate Authority (default: zerossl, options: letsencrypt, zerossl)
|
||||||
|
- `dns_provider`: DNS provider (default: dns_ali, supports acme.sh DNS APIs)
|
||||||
|
- `emqx_container`: EMQX Docker container name (default: emqx)
|
||||||
|
- `cert_dir`: Certificate storage directory (default: /root/certs)
|
||||||
|
|
||||||
|
## Prerequisites Check
|
||||||
|
1. Verify DNS resolution for the domain
|
||||||
|
2. Check if acme.sh is installed
|
||||||
|
3. Verify DNS API credentials are configured
|
||||||
|
4. Check if EMQX container is running
|
||||||
|
5. Verify current container configuration
|
||||||
|
|
||||||
|
## Execution Steps
|
||||||
|
|
||||||
|
### Phase 1: DNS and Environment Verification
|
||||||
|
```bash
|
||||||
|
# Check DNS resolution
|
||||||
|
dig ${domain} +short
|
||||||
|
|
||||||
|
# Get server public IP
|
||||||
|
curl -s ifconfig.me
|
||||||
|
|
||||||
|
# Verify acme.sh installation
|
||||||
|
~/.acme.sh/acme.sh --version
|
||||||
|
|
||||||
|
# Check DNS API configuration
|
||||||
|
cat ~/.acme.sh/account.conf | grep -E "Ali_Key|Ali_Secret"
|
||||||
|
|
||||||
|
# Check EMQX container status
|
||||||
|
docker ps | grep ${emqx_container}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Certificate Directory Setup
|
||||||
|
```bash
|
||||||
|
# Create certificate directory structure
|
||||||
|
mkdir -p ${cert_dir}/${domain}
|
||||||
|
chmod 755 ${cert_dir}
|
||||||
|
chmod 700 ${cert_dir}/${domain}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Certificate Issuance
|
||||||
|
```bash
|
||||||
|
# Issue certificate using DNS validation
|
||||||
|
~/.acme.sh/acme.sh --issue \
|
||||||
|
--dns ${dns_provider} \
|
||||||
|
-d ${domain} \
|
||||||
|
--keylength 2048 \
|
||||||
|
--server ${ca}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Certificate Installation
|
||||||
|
```bash
|
||||||
|
# Install certificate with auto-reload
|
||||||
|
~/.acme.sh/acme.sh --install-cert \
|
||||||
|
-d ${domain} \
|
||||||
|
--cert-file ${cert_dir}/${domain}/cert.pem \
|
||||||
|
--key-file ${cert_dir}/${domain}/key.pem \
|
||||||
|
--fullchain-file ${cert_dir}/${domain}/fullchain.pem \
|
||||||
|
--reloadcmd "docker restart ${emqx_container}"
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chmod 755 ${cert_dir}/${domain}
|
||||||
|
chmod 644 ${cert_dir}/${domain}/cert.pem
|
||||||
|
chmod 644 ${cert_dir}/${domain}/key.pem
|
||||||
|
chmod 644 ${cert_dir}/${domain}/fullchain.pem
|
||||||
|
|
||||||
|
# Create CA cert symlink
|
||||||
|
ln -sf ${cert_dir}/${domain}/fullchain.pem ${cert_dir}/${domain}/cacert.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Extract Public Key
|
||||||
|
```bash
|
||||||
|
# Extract public key from private key
|
||||||
|
openssl rsa -in ${cert_dir}/${domain}/key.pem -pubout -out ${cert_dir}/${domain}/public.pem 2>/dev/null
|
||||||
|
chmod 644 ${cert_dir}/${domain}/public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 6: EMQX Container Reconfiguration
|
||||||
|
```bash
|
||||||
|
# Backup current container configuration
|
||||||
|
docker inspect ${emqx_container} > /root/emqx-backup-$(date +%Y%m%d-%H%M%S).json
|
||||||
|
|
||||||
|
# Get current container ports and volumes
|
||||||
|
PORTS=$(docker inspect ${emqx_container} --format '{{range $p, $conf := .NetworkSettings.Ports}}{{if $conf}}-p {{(index $conf 0).HostPort}}:{{$p}} {{end}}{{end}}')
|
||||||
|
|
||||||
|
# Stop and remove current container
|
||||||
|
docker stop ${emqx_container}
|
||||||
|
docker rm ${emqx_container}
|
||||||
|
|
||||||
|
# Recreate container with certificate mount
|
||||||
|
docker run -d \
|
||||||
|
--name ${emqx_container} \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 1883:1883 \
|
||||||
|
-p 8083:8083 \
|
||||||
|
-p 8084:8084 \
|
||||||
|
-p 8883:8883 \
|
||||||
|
-p 18083:18083 \
|
||||||
|
-v /root/emqx/data:/opt/emqx/data \
|
||||||
|
-v /root/emqx/log:/opt/emqx/log \
|
||||||
|
-v ${cert_dir}/${domain}:/opt/emqx/etc/certs:ro \
|
||||||
|
emqx/emqx:5.8.8
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 7: Verification
|
||||||
|
```bash
|
||||||
|
# Wait for container to start
|
||||||
|
sleep 8
|
||||||
|
|
||||||
|
# Check container status
|
||||||
|
docker ps | grep ${emqx_container}
|
||||||
|
|
||||||
|
# Verify certificate files in container
|
||||||
|
docker exec ${emqx_container} ls -l /opt/emqx/etc/certs/
|
||||||
|
|
||||||
|
# Check EMQX listeners
|
||||||
|
docker exec ${emqx_container} emqx ctl listeners
|
||||||
|
|
||||||
|
# Test SSL connection
|
||||||
|
timeout 10 openssl s_client -connect ${domain}:8883 -servername ${domain} -showcerts 2>/dev/null | openssl x509 -noout -text | grep -E "Subject:|Issuer:|Not Before|Not After|DNS:"
|
||||||
|
|
||||||
|
# Verify SSL handshake
|
||||||
|
timeout 10 openssl s_client -connect ${domain}:8883 -servername ${domain} 2>&1 <<< "Q" | grep -E "Verify return code:|SSL handshake|Protocol|Cipher"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 8: Generate Documentation
|
||||||
|
```bash
|
||||||
|
# Extract public key fingerprint
|
||||||
|
PUB_FP_MD5=$(openssl rsa -in ${cert_dir}/${domain}/key.pem -pubout 2>/dev/null | openssl md5 | awk '{print $2}')
|
||||||
|
PUB_FP_SHA256=$(openssl rsa -in ${cert_dir}/${domain}/key.pem -pubout 2>/dev/null | openssl sha256 | awk '{print $2}')
|
||||||
|
|
||||||
|
# Extract certificate fingerprints
|
||||||
|
CERT_FP_MD5=$(openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -fingerprint -md5 | cut -d= -f2)
|
||||||
|
CERT_FP_SHA1=$(openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -fingerprint -sha1 | cut -d= -f2)
|
||||||
|
CERT_FP_SHA256=$(openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -fingerprint -sha256 | cut -d= -f2)
|
||||||
|
|
||||||
|
# Generate fingerprints file
|
||||||
|
cat > ${cert_dir}/${domain}/fingerprints.txt << EOF
|
||||||
|
Certificate and Key Fingerprints
|
||||||
|
Generated: $(date)
|
||||||
|
|
||||||
|
=== Private Key Fingerprint ===
|
||||||
|
MD5: ${PUB_FP_MD5}
|
||||||
|
SHA256: ${PUB_FP_SHA256}
|
||||||
|
|
||||||
|
=== Certificate Fingerprint ===
|
||||||
|
MD5: ${CERT_FP_MD5}
|
||||||
|
SHA1: ${CERT_FP_SHA1}
|
||||||
|
SHA256: ${CERT_FP_SHA256}
|
||||||
|
|
||||||
|
=== Certificate Details ===
|
||||||
|
$(openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -subject -issuer -dates)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Generate README
|
||||||
|
cat > ${cert_dir}/${domain}/README.txt << 'EOF'
|
||||||
|
EMQX MQTTS Certificate Files
|
||||||
|
|
||||||
|
Domain: ${domain}
|
||||||
|
Created: $(date +%Y-%m-%d)
|
||||||
|
CA: ${ca}
|
||||||
|
Key Length: RSA 2048
|
||||||
|
Auto-Renewal: Enabled
|
||||||
|
|
||||||
|
Files:
|
||||||
|
- cert.pem: Server certificate
|
||||||
|
- key.pem: Private key (CONFIDENTIAL)
|
||||||
|
- public.pem: Public key
|
||||||
|
- fullchain.pem: Full certificate chain
|
||||||
|
- cacert.pem: CA certificate (symlink)
|
||||||
|
- fingerprints.txt: Certificate fingerprints
|
||||||
|
- README.txt: This file
|
||||||
|
|
||||||
|
Client Configuration:
|
||||||
|
- PC/Mobile: Use system CA (no files needed)
|
||||||
|
- Embedded: Use fullchain.pem
|
||||||
|
- Connection: mqtts://${domain}:8883
|
||||||
|
|
||||||
|
Security:
|
||||||
|
⚠️ Keep key.pem secure and confidential
|
||||||
|
✓ cert.pem and fullchain.pem are safe to distribute
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 9: Create Backup Package
|
||||||
|
```bash
|
||||||
|
# Create compressed backup
|
||||||
|
cd ${cert_dir}
|
||||||
|
tar czf ${domain}-complete-$(date +%Y%m%d-%H%M%S).tar.gz ${domain}/
|
||||||
|
|
||||||
|
# Generate checksums
|
||||||
|
md5sum ${domain}-complete-*.tar.gz | tail -1 > ${domain}-checksums.txt
|
||||||
|
sha256sum ${domain}-complete-*.tar.gz | tail -1 >> ${domain}-checksums.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 10: Verify Auto-Renewal
|
||||||
|
```bash
|
||||||
|
# Check certificate renewal configuration
|
||||||
|
~/.acme.sh/acme.sh --info -d ${domain}
|
||||||
|
|
||||||
|
# List all managed certificates
|
||||||
|
~/.acme.sh/acme.sh --list
|
||||||
|
|
||||||
|
# Verify cron job
|
||||||
|
crontab -l | grep acme
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Installation Summary
|
||||||
|
Display the following information:
|
||||||
|
1. Certificate details (domain, validity, CA)
|
||||||
|
2. EMQX container status and ports
|
||||||
|
3. MQTTS listener status (port 8883)
|
||||||
|
4. Public key and certificate fingerprints
|
||||||
|
5. Client configuration guide (system CA vs fullchain.pem)
|
||||||
|
6. Backup file location and checksums
|
||||||
|
7. Auto-renewal schedule
|
||||||
|
|
||||||
|
## Client Configuration Guide
|
||||||
|
|
||||||
|
### Option 1: System CA (Recommended for PC/Mobile)
|
||||||
|
No files needed. The operating system's built-in CA trust store will verify the certificate automatically.
|
||||||
|
|
||||||
|
**Python Example:**
|
||||||
|
```python
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
client = mqtt.Client()
|
||||||
|
client.username_pw_set("username", "password")
|
||||||
|
client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2)
|
||||||
|
client.connect("${domain}", 8883, 60)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Node.js Example:**
|
||||||
|
```javascript
|
||||||
|
const mqtt = require('mqtt');
|
||||||
|
const client = mqtt.connect('mqtts://${domain}:8883', {
|
||||||
|
username: 'username',
|
||||||
|
password: 'password',
|
||||||
|
rejectUnauthorized: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: fullchain.pem (Recommended for Embedded Devices)
|
||||||
|
Distribute `fullchain.pem` to clients that cannot access system CA.
|
||||||
|
|
||||||
|
**Python Example:**
|
||||||
|
```python
|
||||||
|
client.tls_set(ca_certs="fullchain.pem", cert_reqs=ssl.CERT_REQUIRED)
|
||||||
|
client.connect("${domain}", 8883, 60)
|
||||||
|
```
|
||||||
|
|
||||||
|
**ESP32/Arduino Example:**
|
||||||
|
```cpp
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
|
||||||
|
const char* root_ca = "..."; // Content from fullchain.pem
|
||||||
|
|
||||||
|
WiFiClientSecure espClient;
|
||||||
|
PubSubClient client(espClient);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
espClient.setCACert(root_ca);
|
||||||
|
client.setServer("${domain}", 8883);
|
||||||
|
client.connect("ESP32", "username", "password");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### DNS Resolution Issues
|
||||||
|
- Verify A record points to server IP
|
||||||
|
- Wait 5-10 minutes for DNS propagation
|
||||||
|
- Test with: `dig ${domain} +short`
|
||||||
|
|
||||||
|
### Certificate Validation Failed
|
||||||
|
- Check system time is synchronized
|
||||||
|
- Update system CA certificates
|
||||||
|
- Use fullchain.pem instead of system CA
|
||||||
|
|
||||||
|
### Container Start Failed
|
||||||
|
- Check certificate file permissions
|
||||||
|
- Verify volume mount paths
|
||||||
|
- Review container logs: `docker logs ${emqx_container}`
|
||||||
|
|
||||||
|
### SSL Connection Refused
|
||||||
|
- Verify port 8883 is open in firewall
|
||||||
|
- Check EMQX listener status
|
||||||
|
- Test with: `openssl s_client -connect ${domain}:8883`
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Certificate Renewal
|
||||||
|
Auto-renewal is configured via cron (daily at 00:34). Manual renewal:
|
||||||
|
```bash
|
||||||
|
~/.acme.sh/acme.sh --renew -d ${domain} --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Certificate Expiry
|
||||||
|
```bash
|
||||||
|
openssl x509 -in ${cert_dir}/${domain}/cert.pem -noout -dates
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Client Certificates
|
||||||
|
When certificate is renewed, container automatically restarts. No client updates needed unless using public key pinning.
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
1. **Private Key (key.pem)**: Keep confidential, never share
|
||||||
|
2. **Certificate (cert.pem)**: Safe to distribute to clients
|
||||||
|
3. **Full Chain (fullchain.pem)**: Safe to distribute to clients
|
||||||
|
4. **Public Key (public.pem)**: Safe to distribute for pinning
|
||||||
|
5. **Backup Files**: Store in encrypted storage
|
||||||
|
6. **Auto-Renewal**: Enabled, checks daily, renews 60 days before expiry
|
||||||
|
|
||||||
|
## Output Files
|
||||||
|
- `${cert_dir}/${domain}/cert.pem`: Server certificate
|
||||||
|
- `${cert_dir}/${domain}/key.pem`: Private key
|
||||||
|
- `${cert_dir}/${domain}/public.pem`: Public key
|
||||||
|
- `${cert_dir}/${domain}/fullchain.pem`: Full certificate chain
|
||||||
|
- `${cert_dir}/${domain}/cacert.pem`: CA certificate (symlink)
|
||||||
|
- `${cert_dir}/${domain}/fingerprints.txt`: Fingerprints
|
||||||
|
- `${cert_dir}/${domain}/README.txt`: Documentation
|
||||||
|
- `${cert_dir}/${domain}-complete-*.tar.gz`: Backup package
|
||||||
|
- `${cert_dir}/${domain}-checksums.txt`: Package checksums
|
||||||
|
- `/root/emqx-backup-*.json`: Container configuration backup
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- ✅ DNS resolves correctly
|
||||||
|
- ✅ Certificate issued successfully
|
||||||
|
- ✅ EMQX container running with certificate mount
|
||||||
|
- ✅ SSL listener on port 8883 active
|
||||||
|
- ✅ SSL handshake succeeds
|
||||||
|
- ✅ Auto-renewal configured
|
||||||
|
- ✅ Documentation generated
|
||||||
|
- ✅ Backup package created
|
||||||
|
|
||||||
|
## Related Commands
|
||||||
|
- Check certificate info: `openssl x509 -in cert.pem -text -noout`
|
||||||
|
- Test MQTTS connection: `mosquitto_pub -h ${domain} -p 8883 --cafile fullchain.pem -t test -m hello`
|
||||||
|
- View EMQX logs: `docker logs -f ${emqx_container}`
|
||||||
|
- List certificates: `~/.acme.sh/acme.sh --list`
|
||||||
|
- Force renewal: `~/.acme.sh/acme.sh --renew -d ${domain} --force`
|
||||||
Reference in New Issue
Block a user