Skip to content

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.

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 build if 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)

  1. Copy .github/workflows/deploy.yml from tencent-cloud
  2. Update prefix in deploy-cos.js to match repo name
  3. Add TENCENT_COS_SECRET_ID + TENCENT_COS_SECRET_KEY to GitHub Secrets
  4. Update cross-repo links to use registry template syntax
  5. Push → verify COS upload → update registry