Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/workflows/canary-aliyun.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Canary 验证 —— 阿里云 CDN + OSS 轨(将来承载 open.longbridge.cn)。
# 把本分支产物发到专用桶 lb-openapi-canary,由 https://open-canary.longbridge.xyz 承载。
# 该域名已 CNAME 到阿里云 CDN(…w.cdngslb.com),私有 Bucket 回源也已验证通过
# (响应头 x-oss-cdn-auth: success)——不是早先以为的「纯 OSS 无 CDN」。
#
# 产物形态:显式设 BUILD_FORMAT=directory。本轨靠 OSS 静态网站托管的
# 「子目录首页 + 文件404规则=Index」让 /foo 直接 200;而 astro.config.ts 的默认值是
# 'file',那是 Cloudflare Pages 轨需要的形态(用 directory 会把规范 URL 变成带尾斜杠,
# 与 canonical 自相矛盾,见方案 §15.9)。
#
# 刻意不复用 canary.yml:那条的上传目标是 lb-assets/github/canary/... 共享前缀,
# 旧 nginx 取扁平 key 且全链路无 --delete,目录索引产物传进去不会报错,
# 而是让现有站点静默冻结在上一次构建。
#
# ⚠️ 当前阻塞:lb-openapi-canary 的写权限。本流水线用的 AK 属于 RAM 用户
# lb-assets-github(STS GetCallerIdentity 实测),对该桶仍是 AccessDenied——
# 运维已加的授权没有落到这个用户上。
name: Canary · Aliyun CDN + OSS

on:
# 只手动触发:上面的写权限阻塞未解,挂在 push 上每次都会失败。阻塞解除后再决定挂哪个分支
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
OSS_BUCKET: oss://lb-openapi-canary
OSS_ENDPOINT: oss-cn-hongkong.aliyuncs.com
SITE_HOSTNAME: https://open-canary.longbridge.xyz

jobs:
deploy_canary_oss:
name: build and deploy to lb-openapi-canary
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Install ossutil
run: |
wget -q https://github.com/aliyun/ossutil/releases/download/v1.7.17/ossutil-v1.7.17-linux-amd64.zip
unzip -q ossutil-v1.7.17-linux-amd64.zip
cp ./ossutil-v1.7.17-linux-amd64/ossutil ${{github.workspace}}/ossutil
chmod +x ${{github.workspace}}/ossutil
${{github.workspace}}/ossutil --version

- uses: oven-sh/setup-bun@v2

- name: Cache bun dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependency
run: bun install --frozen-lockfile

- name: Build canary
run: bun run build:canary
env:
# 覆盖 build:canary 的默认站点域名,让 canonical / sitemap / llms.txt
# 都指向验证域名,验证时点链接不会跳出环境
VITE_SITE_HOSTNAME: ${{ env.SITE_HOSTNAME }}
VITE_PORTAL_GATEWAY_BASE_URL: 'https://mr.longbridge.xyz'
NODE_OPTIONS: --max-old-space-size=14336 --expose-gc
# 本流水线验证的是 OSS 静态网站托管形态,必须显式要目录索引;
# astro.config.ts 的默认值是 'file'(供 Cloudflare Pages 轨用)
BUILD_FORMAT: directory
ASSETS_DIR: _docs # 资源目录分轨,见 astro.config.ts

- name: Sanity check dist shape
run: |
set -e
echo "--- 目录索引形态抽查 ---"
test -f dist/index.html || { echo "缺 dist/index.html"; exit 1; }
test -f dist/docs/getting-started/index.html || { echo "缺 docs/getting-started/index.html(format:directory 未生效?)"; exit 1; }
test -f dist/longbridge/longbridge-terminal/install || { echo "缺 longbridge/longbridge-terminal/install"; exit 1; }
test -d dist/_docs || { echo "缺 _docs/(assets 命名空间未生效?)"; exit 1; }
echo "--- .md 端点是否仍为扁平文件 ---"
ls dist/docs/getting-started.md
echo "--- 不应再有扁平 HTML ---"
test ! -f dist/docs/getting-started.html || { echo "仍存在扁平 docs/getting-started.html"; exit 1; }
echo "✓ 产物形态符合预期"

- name: Upload to Aliyun OSS (lb-openapi-canary)
env:
# 优先用按桶授权的专用 AK;没配就回退到现有的 FE_LB_ASSET_*。
# 后者是配 lb-assets(杭州)用的,对香港的 lb-openapi-canary 有没有写权限取决于
# 其 RAM 策略是按桶还是账号级——跑一次就知道,失败会是 AccessDenied。
AK: ${{ secrets.FE_LB_OPENAPI_ACCESS_KEY_ID || secrets.FE_LB_ASSET_ACCESS_KEY_ID }}
SK: ${{ secrets.FE_LB_OPENAPI_ACCESS_KEY_SECRET || secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }}
# 只暴露"用了哪一把",不暴露值;失败时据此判断是凭证没填还是策略没给
HAS_OPENAPI_AK: ${{ secrets.FE_LB_OPENAPI_ACCESS_KEY_ID != '' && 'yes' || 'no' }}
CRED_SOURCE: ${{ secrets.FE_LB_OPENAPI_ACCESS_KEY_ID != '' && 'FE_LB_OPENAPI_*' || 'FE_LB_ASSET_*(回退)' }}
run: |
set -e
OSS="${{github.workspace}}/ossutil"
BASE="-u -r -j 10 -e $OSS_ENDPOINT -i $AK -k $SK"
echo "--- 凭证来源 ---"
echo " FE_LB_OPENAPI_ACCESS_KEY_ID 已配置: $HAS_OPENAPI_AK"
echo " 本次实际使用: $CRED_SOURCE"
echo " AccessKeyId 长度: ${#AK}"

echo "--- 写权限预检 ---"
echo ok > /tmp/_probe.txt
if ! $OSS cp /tmp/_probe.txt $OSS_BUCKET/.ci-probe -f -e $OSS_ENDPOINT -i $AK -k $SK; then
echo "::error::$CRED_SOURCE 这把 AK 无法写入 $OSS_BUCKET(region: $OSS_ENDPOINT)。"
if [ "$HAS_OPENAPI_AK" = "no" ]; then
echo "::error::当前走的是回退凭证。若运维是给另一把 AK(如写 whale-assets 那把)放的权,"
echo "::error::需要把那把的值填进 Secret FE_LB_OPENAPI_ACCESS_KEY_ID / FE_LB_OPENAPI_ACCESS_KEY_SECRET。"
else
echo "::error::已在用专用 AK 仍被拒,说明该 AK 的 RAM 策略里没有本桶,或本桶不属于该 AK 所在账号。"
fi
exit 1
fi
$OSS rm $OSS_BUCKET/.ci-probe -f -e $OSS_ENDPOINT -i $AK -k $SK || true
echo "✓ 写权限正常"
# 桶根即站点根,不带任何前缀
$OSS cp ./dist/ $OSS_BUCKET/ $BASE --exclude "*.html" --exclude "*.md" --exclude "*.txt" --exclude "*.zip" --meta=Cache-Control:max-age=31536000
$OSS cp ./dist/ $OSS_BUCKET/ $BASE --include "*.zip" --meta "Cache-Control:no-cache#Content-type:application/zip;charset=utf-8"
$OSS cp ./dist/ $OSS_BUCKET/ $BASE --include "*.md" --meta "Cache-Control:no-cache#Content-type:text/markdown;charset=utf-8"
$OSS cp ./dist/ $OSS_BUCKET/ $BASE --include "*.txt" --meta "Cache-Control:no-cache#Content-type:text/plain;charset=utf-8"
$OSS cp ./dist/ $OSS_BUCKET/ $BASE --include "*.html" --meta "Cache-Control:no-cache#Content-type:text/html;charset=utf-8"

- name: Summary
run: |
echo "### Canary OSS 部署完成" >> $GITHUB_STEP_SUMMARY
echo "- 桶: \`lb-openapi-canary\` (hongkong)" >> $GITHUB_STEP_SUMMARY
echo "- 验证地址: $SITE_HOSTNAME" >> $GITHUB_STEP_SUMMARY
echo "- commit: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
167 changes: 167 additions & 0 deletions .github/workflows/canary-cloudflare.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Canary 验证 —— Cloudflare Pages 轨。测试域名 open-canary.longbridge.xyz,
# 文档与 SPA 同域(SPA 由 Function 转发,见 functions/[[path]].ts 的 SITES)。
#
# 发到 canary 专用 project longbridge-developers-canary,并以它的 **production**
# 对外,而不是做 longbridge-developers 的预览分支:测试域名的 DNS 在 Route 53,
# Cloudflare 对外部 DNS 的自定义域名只路由到 production 分支,挂不到预览分支
# (官方文档 pages/how-to/custom-branch-aliases 的明文限制)。
#
# 由 canary 分支触发,与 canary.yml(nginx 版 open.longbridge.xyz)同一分支。git 分支、
# 部署标签与该 project 的 production 分支三处都叫 canary。
name: Canary · Cloudflare Pages

on:
push:
branches:
- canary # 改分支时同步 pack-skills.yml 的 CANARY_REF
paths-ignore:
- '.github/workflows/oss-probe.yml'
- 'docs/superpowers/**'
workflow_dispatch:
inputs:
cf_branch:
description: 'Cloudflare 部署标签。canary = 测试域名那份;其它值发成临时预览'
required: false
default: canary

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CF_ACCOUNT_ID: 335162bab35b74f065a0351212011d5a
CF_PROJECT: longbridge-developers-canary

jobs:
preview:
name: build & deploy preview
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1

- uses: oven-sh/setup-bun@v2

- name: Cache bun dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependency
run: bun install --frozen-lockfile

# wrangler 的 --branch 是部署标签。canary 即该 project 的 production(建 project
# 时声明),由测试域名承载;其它标签发成 <label>.<project>.pages.dev 临时预览
# (临时预览不在 Function 的站点表里,不转发 SPA 与登录页)。
- name: Resolve deploy target
id: alias
env:
RAW: ${{ inputs.cf_branch || 'canary' }}
run: |
label=$(echo "$RAW" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')
if [ "$label" = "canary" ]; then
host=https://open-canary.longbridge.xyz
else
host="https://${label}.${CF_PROJECT}.pages.dev"
fi
echo "label=$label" >> "$GITHUB_OUTPUT"
echo "host=$host" >> "$GITHUB_OUTPUT"
echo "部署标签: $label 对外域名: $host"

- name: Build canary
run: bun run build:canary
env:
# canonical / sitemap / llms.txt 里的链接指向本次部署的对外域名,不跳出验证环境
VITE_SITE_HOSTNAME: ${{ steps.alias.outputs.host }}
VITE_PORTAL_GATEWAY_BASE_URL: 'https://mr.longbridge.xyz'
NODE_OPTIONS: --max-old-space-size=14336 --expose-gc
ASSETS_DIR: _docs # 资源目录分轨,见 astro.config.ts

# 只断言本轮四项改动的产物形态,不做内容断言
- name: Sanity check dist shape
run: |
set -e
fail=0
need () { [ -e "dist/$1" ] || { echo "::error::缺少 dist/$1"; fail=1; }; }

need index.html
need docs/getting-started.html # format:'file'(CF 轨规范 URL 无尾斜杠)
need docs/getting-started.md # .md 端点
need _docs # 独立资源命名空间
need longbridge/longbridge-terminal/install # 官方安装脚本
need 404.html # G1:没有它 Pages 会 200 兜底
need _redirects # G4

# G2:zip 清单应与线上一致(1 个仓库整包 + 1 个 all + 每技能各一个)
zips=$(ls dist/skill/*.zip 2>/dev/null | wc -l | tr -d ' ')
echo "dist/skill 下 zip 数量: $zips"
[ "$zips" -ge 15 ] || { echo "::error::zip 数量 $zips < 15,prebuild-skills 可能失败"; fail=1; }
[ -e dist/skill/longbridge-all.zip ] || { echo "::error::缺少 longbridge-all.zip"; fail=1; }
if unzip -l dist/skill/skills.zip 2>/dev/null | grep -q '\.git/'; then
echo "::error::skills.zip 里仍含 .git/"; fail=1
fi

# 目录索引形态是 .cn 轨专用;出现在这里说明 BUILD_FORMAT 被误设,
# 会导致 Pages 把规范 URL 变成带尾斜杠、与 canonical 自相矛盾
[ ! -e dist/docs/getting-started/index.html ] || { echo "::error::出现目录索引产物,BUILD_FORMAT 被误设为 directory"; fail=1; }

# canary 必须全站禁爬:它以 production 身份挂在自定义域名上,Cloudflare 不会
# 像对预览部署那样自动补 noindex,这一行是唯一的防线
grep -qx 'Disallow: /' dist/robots.txt || { echo "::error::robots.txt 未全站禁爬,canary 会被搜索引擎收录"; fail=1; }

exit $fail

- name: Ensure Pages project exists
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ env.CF_ACCOUNT_ID }}
run: |
# project 已存在时这条必然报错,属正常,不中断流程;但输出与退出码要打出来,
# 权限不足长得一样,吞掉的话下一步部署失败会查不出根因。
# 钉 3.x:wrangler 4 要求 node >= 22,3.114.x 是 >= 16.17。
set +e
out=$(npx --yes wrangler@3 pages project create "$CF_PROJECT" --production-branch=canary 2>&1)
code=$?
echo "$out" | tail -5
echo "(exit=$code;非 0 若是「already exists」属正常,其余看上面输出)"
exit 0

- name: Deploy to Cloudflare Pages
id: deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ env.CF_ACCOUNT_ID }}
command: pages deploy dist --project-name=${{ env.CF_PROJECT }} --branch=${{ steps.alias.outputs.label }} --commit-dirty=true

# 地址以 wrangler 实际返回为准;steps.alias 那个是本地推算的,仅作兜底
- name: Summary
env:
URL_EXACT: ${{ steps.deploy.outputs.deployment-url }}
URL_ALIAS: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
URL_GUESS: ${{ steps.alias.outputs.host }}
run: |
exact="${URL_EXACT:-(wrangler 未返回)}"
alias="${URL_ALIAS:-$URL_GUESS}"
{
echo "### Cloudflare Pages canary"
echo ""
echo "| | 地址 |"
echo "|---|---|"
echo "| 本次部署(唯一) | $exact |"
echo "| 稳定入口 | $alias |"
echo "| 测试域名(需已绑定) | https://open-canary.longbridge.xyz |"
echo ""
echo "验证清单:"
echo '- `/login`、`/tfa`、`/binding`、`/password/reset` 应为 **200**(登录页)'
echo '- `/dashboard` 200,且其 `/_app/*` 资源全部 200'
echo '- `/robots.txt` 应为 `Disallow: /`'
echo '- `/zh-CN/llms.txt` 200;`/longbridge/longbridge-terminal/releases/latest` 200 且返回版本号'
echo '- `/definitely-not-exist-xyz` 应为 **404**'
echo '- `/en` 应 302 到 `/`'
} >> "$GITHUB_STEP_SUMMARY"
echo "预览地址: $exact | $alias"
13 changes: 12 additions & 1 deletion .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,20 @@ jobs:
VITE_PORTAL_GATEWAY_BASE_URL: 'https://mr.longbridge.xyz'
NODE_OPTIONS: --max-old-space-size=14336 --expose-gc

# nginx 的 location / 只认目录索引(foo/index.html),/docs、/sdk 只认扁平(foo.html)
- name: Reshape routes for nginx
run: |
bun run scripts/copy-routes.ts
test -f dist/skill/index.html && test ! -e dist/skill.html || { echo "::error::copy-routes 未生效,nginx 的 location / 会取不到页面"; exit 1; }

# 产物由 nginx 承载(open.longbridge.xyz),资源必须在 /assets(见 astro.config.ts)
- name: Assert assets dir
run: test ! -e dist/_docs && find dist/assets -maxdepth 1 \( -name '*.js' -o -name '*.css' \) | grep -q . || { echo "::error::OSS 产物的资源目录不是 /assets,nginx 会 404"; exit 1; }

- name: Upload to Aliyun OSS
run: |
${{github.workspace}}/ossutil cp ./dist oss://lb-assets/github/canary/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --exclude "*.html" --exclude "*.md" --exclude "*.zip" --meta=Cache-Control:max-age=31536000
${{github.workspace}}/ossutil cp ./dist oss://lb-assets/github/canary/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --exclude "*.html" --exclude "*.md" --exclude "*.zip" --exclude "*.xml" --meta=Cache-Control:max-age=31536000
${{github.workspace}}/ossutil cp ./dist oss://lb-assets/github/canary/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.xml" --meta "Cache-Control:no-cache#Content-type:application/xml;charset=utf-8"
${{github.workspace}}/ossutil cp ./dist oss://lb-assets/github/canary/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.zip" --meta "Cache-Control:no-cache#Content-type:application/zip;charset=utf-8"
${{github.workspace}}/ossutil cp ./dist oss://lb-assets/github/canary/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.md" --meta "Cache-Control:no-cache#Content-type:text/markdown;charset=utf-8"
${{github.workspace}}/ossutil cp ./dist oss://lb-assets/github/canary/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.html" --meta "Cache-Control:no-cache#Content-type:text/html;charset=utf-8"
Loading
Loading