Cloudflare Pages 不支持直接集成饭桶集线器饭桶实验室以外的远程饭桶服务器。但是,用 wrangler CLI 上传静态网页是被支持的^1,这个过程可以在私有的 CI/CD Runner 上执行。

此处的 git 服务器是自建的 forgejo(gitea 也适用),与 Woodpecker CI 共同作用。Woodpecker 的搭建可以看这里


打镜像

Woodpecker 的 workflow 操作均在 Docker 容器中进行。想在其中调用特殊的命令行程序,事先构建一个镜像会节省很多时间。

wrangler

wrangler 是一个 Node.js 包,不妨用 node 打个镜像。

Dockerfile

FROM node:20-alpine3.18

WORKDIR /tmp

RUN npm install wrangler -g

构建

docker build . -t wrangler-node

mkdocs-material

mkdocs-material 需要添加 jieba 来优化中文搜索,然而原版镜像锁死了 ENTRYPOINT,不如自己打镜像。

Dockerfile

FROM python:3.11-alpine3.18

WORKDIR /tmp

RUN pip install --no-cache-dir mkdocs-material jieba

构建

docker build . -t mkdocs-material-jieba

以上 DockerFile 都锁定了版本,未来可能会出现安全性问题,请注意及时更新。

此外,为减小镜像体积,使用基于 alpine 的镜像。


设置 Secrets

Secrets 可以让你安全地将 Token 等重要凭据添加在环境变量中。Woodpecker 的文档中提及了 secrets 的用法

secrets 的作用范围可以是单个 repo、组织内的 repo, 或者整个实例的所有 repo. 在对应层次(repo/org/global)的设定中都可以看到 secrets 的设置。

repo secret
org secret
global secret
点击 add secret 即可添加新的 secret. 设置好 name value 和 scope 就能在 workflow 里面引用了。

需要注意的是,Woodpecker 中的 secrets 命名为小写,但是在 command 中引用时为大写

似乎是出于安全考虑,在修改 secret 时,无法看到之前填写的 value,如需修改可以用新的值覆盖,留空则不改变原有的值。

获取 Cloudflare API Token

新版 wrangler 的默认登录方式似乎是浏览器弹窗,但是在其他地方还是能找到 API Token 的用法。

前往 https://dash.cloudflare.com/profile/api-tokens 获取一个 API Token 用来读写 Cloudlfare Workers 的内容。

或者点击任意一个域名进入 overview, 也能看到 “Get your API token”, 并且在上方能看到 Account ID. (此时在地址栏也能看到 Account ID)

对于 Pages 的情形,直接使用 Edit Cloudflare Workers 的模板就可以了。其他诸如作用范围、Client IP 白名单和有效时间等属性,自行斟酌。

拿到 Token 以后记得及时复制,并添加到 secrets 中。


编写 pipeline

随后在 repo 里面添加 .woodpecker/*.yml 或者 .woodpecker.yml 作为 pipeline 配置文件。

对于这次的情况,基本上就三个 workflow,按部就班写出来就行

  • clone
  • build
  • deploy

Woodpecker 的 clone 操作就是 pull 外加自动初始化 submodule 的过程。不太需要修改了。因此只需要处理后两步。

mkdocs-material

构建过程一目了然,除了添加一个聊胜于无的 robots.txt 以外。

publish 步骤的指令也只有一句。CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKEN 已经由 secrets 引入环境变量了。

注意,这里的 project name 最好事先确定好,这会在建立 Cloudflare Pages 项目时使用到。

--branch-name 已隐去,直接部署到 Production Branch. 如果指定的是 Production Branch 以外的值,会以 preview 形式部署。^3

Cloudflare Pages 默认的 Production Branch 是 main, 如需更改,需要使用能够读写 Workers 的 API Token, 执行一次以下指令,以 master 为例:^4

(请不要忘记 export 前面的空格,防止 Token 进入 shell history)

 export CLOUDFLARE_ACCOUNT_ID=<YOUR_ACCOUNT_ID> 
 export CLOUDFLARE_API_TOKEN=<YOUR_API_TOKEN>
 export PROJECT_NAME=<YOUR_PROJECT_NAME>
 export PRODUCTION_BRANCH=master
curl --request PATCH \
     --url "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects/$PROJECT_NAME" \
     --header 'Content-Type: application/json' \
     --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
     --data '{ "production_branch":  "'"$PRODUCTION_BRANCH"'" }'

回到 pipeline 配置文件:

# .woodpecker/default.yml
steps:
  - name: build
    image: mkdocs-material-jieba:latest
    commands: 
      - mkdocs build
      - cp robots.txt site/

  - name: publish
    image: wrangler-node:latest 
    commands:
      - npx wrangler pages deploy site --project-name=<YOUR_PROJECT_NAME> # --branch-name=master
    secrets: [ cloudflare_account_id, cloudflare_api_token ]
    # when:
    #   - branch: master

docsify

docsify 自身就是一个完整的静态网站了,直接送上去就可以。当然,因为我之前的一些历史遗留,把它做成了和 mkdocs 一样的结构。如下:

├── docker-compose.yml
├── Dockerfile
├── docs
│   ├── 00
│   │   └── README.md
│   ├── 01
│   │   └── index.md
│   ├── 02
│   │   └── index.md
│   ├── index.html
│   ├── README.md
│   └── _sidebar.md -> sidebar/_sidebar.md
├── robots.txt
└── sidebar
    └── _sidebar.md

这里因为要推送到多个上游,在一个完整的 pipeline 里用到了不同的 Account ID 和 API Token, 因此定义了多个 id 和 token,用 export 指令赋值给 CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKEN 这两个环境变量。当然也可以直接写在 command 最前面,但我暂时不想折腾 $ 的 escape 问题。

同时,使用 when 可以限定各个 workflow 的触发条件。

# .woodpecker/default.yml
steps:
  - name: rearrange
    image: alpine
    commands:
      - rm docs/_sidebar.md
      - cp sidebar/_sidebar.md docs/
      - cp robots.txt docs/

  - name: publish-to-1
    image: wrangler-node:latest
    commands:
      - export CLOUDFLARE_ACCOUNT_ID=$ID1
      - export CLOUDFLARE_API_TOKEN=$TOKEN1
      - npx wrangler pages deploy docs --project-name=<PROJECT_NAME_1> # --branch-name=master
    secrets: [ id1, token1 ]
    when: 
      - branch: master

  - name: publish-to-2
    image: wrangler-node:latest
    commands:
      - export CLOUDFLARE_ACCOUNT_ID=$ID2
      - export CLOUDFLARE_API_TOKEN=$TOKEN2
      - npx wrangler pages deploy docs --project-name=<PROJECT_NAME_2> # --branch-name=master
    secrets: [ id2, token2 ]
    when: 
      - branch: master
      

我实际部署的方式可能更混乱一些,并且注意到最终部署的结果更像是一个 overlayfs, 还不好删除,非常奇妙。


部署

在 Cloudflare Dashboard 中点击左侧 “Workers & Pages” 进入,随后点击 “Create Application” 开始创建新项目。这是为了产生一个 Project Name, 否则无法上传资源。

随后选择应用类型,点击 “Pages” 后向下滑,点击 “Upload Assets”.

随后键入事先确定好的 Project Name 并点击 “Create Project”, 固化 Project Name.

随后进入上传页面。Project Name 已经固化,可以退回到 overview, 就能看到空的 Project.

此时,在 Woodpecker 中点击 “Add repository” 启用对应的 repo, 将 pipeline 文件 push 上去,即可触发构建。或者也可以点击 Run Pipeline 手动触发。

记得关注构建结果,只要最后 ci 结果是绿色的,应该就大功告成了。