chore: 初始化 opencode 配置

This commit is contained in:
Voson
2026-01-12 17:39:49 +08:00
commit ae0cb58a94
25 changed files with 5688 additions and 0 deletions

View 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

View 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
View 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

View 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 个项目类型子文档

View 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` | 构建 AABApp 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. 配置 Secretskeystore、密码等
6. 推送代码触发 workflow
---
## 版本发布
```bash
# 创建并推送 tag
git tag android-1.0.0
git push origin android-1.0.0
```

View 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
```

View 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
```

View 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
```

View 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

View 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/)

View 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. 生成文档和备份
享受自动化的便利!🎉

View 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

View 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)

View 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).

View 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)

View 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`