Jul 24, 2024

Turborepo + GitHub Actions + CF Pages:打造完美的持续部署流程

分享一下最近将官网和exportx.dev两个网站(Turborepo管理的)从Vercel迁移到Cloudflare Pages的过程,期间也使用了Github Action来做CI/CD自动化测试和部署,有兴趣的同学可以一起阅读交流下。


背景

我有多个前端项目使用Turborepo管理起来的,之前一直关联在Vercel进行部署,Vercel确实也挺好用,但是平台限制和朋友推荐让我想拿其中两个项目做一次测试。

为了使开发,预览,发布流程更加丝滑,迁移到Cloudflare Pages还需要解决下面几个问题:

  1. 怎么使用Git Tag管理多个项目的发布、预览版本
  2. 怎么使用Github Action自动化打包、部署到Cloudflare Page平台

如何使用Git Tag管理多项目,多版本

Vercel在每次提交都会帮我们构建一个Preview版本,Cloudflare暂时没有这个功能,但是实现起来也不难。

首先我们要定义好一个规范:什么情况下才触发构建?构建Preview还是Production?

这里我使用了Tag来触发构建,Tag规范如下:

*[email protected]*
*[email protected]
//* @前面代表了某个项目
// 后面代表了版本
// -rc 代表是否是Preview版本

这样我们在需要发布的时候可以再当前版本打一个Tag,然后推送,Github Action根据流水线脚本进行构建,Github Action的触发条件如下:

# .github/workflows/deploy.yml
on:
  push:
    tags:
      - '*@*.*.*-rc'  # 匹配预发布版本 
      - '*@*.*.*'      # 匹配正式版本

因为我们要在脚本区分项目、版本、是否为预览,所以要加上解析Tag的脚本方便后续的判断,即下面steps里面的的 Parse Tag

# .github/workflows/deploy.yml
jobs:
  deploy-prod:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Node.js
        uses: pnpm/action-setup@v4

      - name: Parse tag
        id: parse-tag
        run: |
          TAG=${{ github.ref_name }}
          PROJECT=$(echo $TAG | cut -d'@' -f1)
          VERSION=$(echo $TAG | cut -d'@' -f2)
          if [[ $TAG == *-rc ]]; then
            STAGE="preview"
          else
            STAGE="main"
          fi
          echo "::set-output name=project::$PROJECT"
          echo "::set-output name=version::$VERSION"
          echo "::set-output name=stage::$STAGE"

此段脚本中包含了Setup Node.js用来搭建Node+Pnpm环境,代码Checkout为了在构建机下载代码,这两步是后续脚本执行的必要条件。

使用Turborepo来构建指定项目

上面的步骤中我们已经获取到了project名称,我们可以使用Turborepo自带的过滤npx turbo build --filter=project能力来对指定项目进行构建,脚本如下

      - name: Install dependencies & Build
        run: pnpm install && npx turbo build --filter=${{ steps.parse-tag.outputs.project }}

此时,我们已经构建好了我们的dist(取决你的项目)产物,只剩下最后一步了,发布~

在Github Action中使用Wrangler部署到Cloudflare

Cloudflare推荐我们使用 https://github.com/cloudflare/wrangler-action来做部署发布,脚本如下:

      - name: Deploy Worker
        uses: cloudflare/wrangler-action@v3
        with:
          packageManager: pnpm
          workingDirectory: ./apps/${{ steps.parse-tag.outputs.project }}
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy dist --project-name=${{ steps.parse-tag.outputs.project }} --branch=${{ steps.parse-tag.outputs.stage }}

这里刚好提一下Parse tag阶段的STAGESTAGE用于区分PreviewProduction

CLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID这两个secrets需要你到Github仓库里面配置,如何获取可以看文章最后的Tips。

img.png

恭喜你,已经完成自动构建和发布。

等等!我完全不记得我上一个tag是什么了怎么办?

再优化一下打Tag的过程吧

我们将版本分为["major", "minor", "patch"],当我想发布的时候,我需要手动管理版本,我想要脚本帮我自动创建一个自增tag。

// scripts/auto-tags.js
#!/usr/bin/env node

const { program } = require("commander");
const inquirer = require("inquirer");
const semver = require("semver");
const simpleGit = require("simple-git");

const git = simpleGit();

program
    .version("1.0.0")
    .description("A version tag generator for your projects");

program.parse(process.argv);

async function getLatestTag(project) {
    const tags = await git.tags();
    const projectTags = tags.all
        .filter((tag) => tag.startsWith(`${project}@`))
        .map((tag) => {
            const version = tag.split("@")[1];
            return { tag, version };
        })
        .filter((tag) => semver.valid(tag.version))
        .sort((a, b) => semver.rcompare(a.version, b.version));

    return projectTags.length > 0 ? projectTags[0].version : "0.0.0";
}

async function main() {
    // 如果本地有未提交的更改,直接退出
    const status = await git.status();
    if (status.files.length > 0) {
        console.log("Please commit your changes before creating a new tag.");
        return;
    }

    const { project } = await inquirer.prompt([
        {
            type: "list",
            name: "project",
            message: "Please select a project:",
            choices: ["exportx", "abfree-com"],
        },
    ]);
    const currentVersion = await getLatestTag(project);
    console.log(`Current Version: ${currentVersion}`);

    const { versionType } = await inquirer.prompt([
        {
            type: "list",
            name: "versionType",
            message: "Please select a version type:",
            choices: ["major", "minor", "patch"],
        },
    ]);

    const newVersion = semver.inc(currentVersion, versionType);

    const { isRC } = await inquirer.prompt([
        {
            type: "confirm",
            name: "isRC",
            message: "Is this a release candidate?",
            default: true,
        },
    ]);

    let newTag;
    if (isRC) {
        newTag = `${project}@${newVersion}-rc`;
    } else {
        newTag = `${project}@${newVersion}`;
    }

    console.log(`The new tag will be: ${newTag}`);

    const { confirm } = await inquirer.prompt([
        {
            type: "confirm",
            name: "confirm",
            message: "Confirm to create the new tag?",
            default: true,
        },
    ]);

    if (confirm) {
        await git.addTag(newTag);
        console.log(`Tag ${newTag} has been created successfully! 🎉`);
        // 推送到远程仓库
        await git.pushTags();
        console.log(`Tag ${newTag} has been pushed to the remote repository! 🚀`);
    } else {
        console.log("The tag creation has been cancelled.");
    }
}

main().catch(console.error);


我们顺便把它加入到package.json吧,

{
  "scripts": {
    "tag": "node scripts/auto-tags.js"
  }
}

运行 pnpm tag

img_1.png

大功告成~

当然这样生成Tag在协作模式可能会有些问题,比如两个同事分别执行了这个脚本,则会出现tag冲突,有2个方式可能会帮到你:

  • 打tag前先git pull,生成tag后立即git push
  • 放github action打tag

感谢您的阅读,如果你有更好的想法我们可以一起交流沟通~

小广告:

ExportX超快的在线压缩工具,支持Figma Plugin!

ABFree 我的个人博客,记录一些技术和生活的点滴~

Tips

如何找到我的CLOUDFLARE_API_TOKEN

CLOUDFLARE_ACCOUNT_ID:

img_2.png

CLOUDFLARE_API_TOKEN:

https://dash.cloudflare.com/profile/api-tokens