ADR 0013: CI/CD Pipeline — GitHub Actions → COS → EdgeOne CDN¶
Status¶
Proposed
Date¶
2026-08-07
Context¶
OVES produces static documents through five distinct types, all converging on COS as the single point of truth (per ADR 0009):
| Type | Source | Trigger | Lifetime | Auth (post-ICP) |
|---|---|---|---|---|
| Docs | GitHub mkdocs (23 repos) | git push (GitHub Actions) |
Permanent | WeCom SSO |
| Decks | WorkBuddy / Build tools | Manual | Permanent | WeCom + Keycloak |
| Memo | WorkBuddy generates HTML | Manual | 7-30 days, auto-expire | WeCom SSO |
| Training | WorkBuddy / Build tools | Manual | Permanent | WeCom SSO |
| Media | WorkBuddy / Build tools | Manual | Permanent | WeCom + Keycloak |
All five upload to COS and serve via flat subdomains ([type].omnivoltaic.cn, per ADR 0011). Currently deployed to Cloudflare Pages — migrating to Tencent Cloud.
Key requirements:
1. Automated for docs (git push → live)
2. Manual but streamlined for all other types (one command from WorkBuddy)
3. Memos auto-expire (COS lifecycle rules)
4. Cross-repo hyperlinks survive migration (registry-based resolution)
5. All artifacts in one bucket, one lifecycle policy
6. Subdomain name determines auth: WeCom-only (docs, memo, training) vs WeCom+Keycloak (decks, media)
Decision¶
GitHub Actions for docs; WorkBuddy + publish-doc-site skill for decks, memo, training, and media. All converge on COS → EdgeOne CDN (post-ICP).
Deployment Matrix¶
| Type | COS Prefix | Domain (post-ICP) | Auth | Trigger |
|---|---|---|---|---|
| Docs | docs/<repo>/ |
docs.omnivoltaic.cn/<repo>/ |
WeCom SSO | GitHub Actions |
| Decks | decks/<name>/ |
decks.omnivoltaic.cn/<name>/ |
WeCom + Keycloak | WorkBuddy |
| Memo | memo/<title>/ |
memo.omnivoltaic.cn/<title>/ |
WeCom SSO | WorkBuddy |
| Training | training/<title>/ |
training.omnivoltaic.cn/<title>/ |
WeCom SSO | WorkBuddy |
| Media | media/<title>/ |
media.omnivoltaic.cn/<title>/ |
WeCom + Keycloak | WorkBuddy |
GitHub Actions Workflow (Docs Only)¶
git push main
│
▼
GitHub Actions (.github/workflows/deploy.yml)
│
├── pip install mkdocs mkdocs-material
├── mkdocs build -d site
└── COS SDK upload
└── internal/<repo-name>/** (permanent)
│
▼ (post-ICP)
EdgeOne CDN — COS origin
https://internal.docs.omnivoltaic.cn/<repo-name>/
GitHub Actions Workflow¶
# .github/workflows/deploy.yml — shared across all 23 repos
name: Build & Deploy to COS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install mkdocs mkdocs-material
- run: mkdocs build -d site
- name: Upload to COS
env:
TENCENT_COS_SECRET_ID: ${{ secrets.TENCENT_COS_SECRET_ID }}
TENCENT_COS_SECRET_KEY: ${{ secrets.TENCENT_COS_SECRET_KEY }}
run: |
npm install cos-nodejs-sdk-v5
node deploy-cos.js
Credentials via GitHub Organization Secrets — set once, shared across all 23 repos.
Cross-Repo Link Registry¶
Each repo currently links to other repos via absolute URLs (e.g., https://docs.omnivoltaic.com/internal/emob-commercial-models/...). After migration to [doc-type].omnivoltaic.cn, these URLs break.
Solution: Registry at internal/registry.json
{
"sites": {
"tencent-cloud": {
"path": "internal/tencent-cloud/",
"title": "Tencent Cloud Migration",
"baseUrl": "https://internal.docs.omnivoltaic.cn/tencent-cloud/"
},
"emob-commercial-models": {
"path": "internal/emob-commercial-models/",
"title": "eMob Commercial Models",
"baseUrl": "https://internal.docs.omnivoltaic.cn/emob-commercial-models/"
}
},
"updated": "2026-08-07T03:30:00Z"
}
Each deploy workflow optionally updates its entry in the registry. A separate registry-updater job (or standalone GitHub Action in a canonical repo) scans COS after each deploy and regenerates registry.json.
Cross-site link resolution in mkdocs:
<!-- Before migration: absolute URL -->
[ADR 0009](https://docs.omnivoltaic.com/internal/tencent-cloud/adr/0009)
<!-- After migration: registry-relative -->
{{ registry.tencent-cloud.baseUrl }}adr/0009-edgeone-pages-static-hosting/
mkdocs page templates resolve {{ registry.X.baseUrl }} at build time from the registry.
Per-Repo Deploy Script (deploy-cos.js)¶
const COS = require('cos-nodejs-sdk-v5');
const fs = require('fs');
const path = require('path');
const cos = new COS({
SecretId: process.env.TENCENT_COS_SECRET_ID,
SecretKey: process.env.TENCENT_COS_SECRET_KEY
});
const config = {
bucket: 'oves-cos-bucket-1437812500',
region: 'ap-singapore',
prefix: 'docs/tencent-cloud/' // per-repo value
};
function walk(dir, fn) {
fs.readdirSync(dir).forEach(f => {
const fp = path.join(dir, f);
fs.statSync(fp).isDirectory() ? walk(fp, fn) : fn(fp);
});
}
let count = 0;
walk('site', file => {
cos.putObject({
Bucket: config.bucket,
Region: config.region,
Key: config.prefix + file.replace('site/', ''),
Body: fs.readFileSync(file)
}, err => {
if (err) { console.error(file, err); process.exit(1); }
if (++count % 20 === 0) console.log(count, 'files...');
});
});
setTimeout(() => console.log('Done:', count, 'files'), 5000);
All Other Types (WorkBuddy + publish-doc-site)¶
Decks, memo, training, and media all use the same WorkBuddy workflow:
WorkBuddy generates/builds artifacts → publish-doc-site skill → COS → EdgeOne CDN (now)
| Type | Command | COS Path | Post-ICP URL |
|---|---|---|---|
| Decks | bash scripts/publish.sh ./deck decks/pitch-name |
decks/pitch-name/ |
decks.omnivoltaic.cn/pitch-name/ |
| Memo | bash scripts/publish.sh ./html memo/title |
memo/title/ |
memo.omnivoltaic.cn/title/ |
| Training | bash scripts/publish.sh ./training training/title |
training/title/ |
training.omnivoltaic.cn/title/ |
| Media | bash scripts/publish.sh ./media media/title |
media/title/ |
media.omnivoltaic.cn/title/ |
- No GitHub Actions — manual from WorkBuddy
- Artifacts are self-contained (single page or multi-page with relative links)
- Per ADR 0009 + 0014: EdgeOne CDN serves from COS origin
- Memos auto-expire via COS lifecycle rules
COS Storage Layout¶
oves-cos-bucket-1437812500/
├── docs/ ← docs.omnivoltaic.cn (WeCom SSO)
│ ├── registry.json ← cross-repo link registry
│ ├── tencent-cloud/ ← auto-deployed on push
│ ├── emob-commercial-models/ ← auto-deployed on push
│ └── ... (23 repos)
├── decks/ ← decks.omnivoltaic.cn (WeCom + Keycloak)
├── memo/ ← memo.omnivoltaic.cn (WeCom SSO, auto-expire)
├── training/ ← training.omnivoltaic.cn (WeCom SSO)
└── media/ ← media.omnivoltaic.cn (WeCom + Keycloak)
Consequences¶
Benefits¶
- Zero-touch deploys:
git push→ live in ~30s (COS upload) → cached at EdgeOne edge - Single credential surface: GitHub Org Secrets — set once, rotate centrally
- Registry as link resolver: cross-repo links survive domain changes
- Same pipeline for all repos: copy
.github/workflows/deploy.yml+ change prefix per repo - Memo path preserved: WorkBuddy deploys to
memo/via same COS bucket - No Cloudflare dependency: full Tencent-native pipeline
Trade-Offs¶
- First deploy uploads all ~80-130 files (~30MB per repo) — subsequent diffs are faster
- Registry adds a build-time dependency — must be fetched before
mkdocs buildif using template resolution - GitHub Actions minutes: ~1 min/repo/deploy × 23 repos = modest consumption on free tier
- COS API costs: ~$0.001 per deploy (PUT requests) — negligible
Migration Steps (Per Repo)¶
- Copy
.github/workflows/deploy.ymlfrom tencent-cloud - Update
prefixindeploy-cos.jsto match repo name - Add
TENCENT_COS_SECRET_ID+TENCENT_COS_SECRET_KEYto GitHub Secrets - Update cross-repo links to use registry template syntax
- Push → verify COS upload → update registry