📌你刚接手一个项目,在仓库根目录看到一个
azure-pipelines.yml,里面写满了trigger、pool、task。这东西是干嘛的?能删吗?和Jenkins 是一回事吗?
本文从一份真实在跑的前端部署流水线出发,把 CI/CD 这件事从原理讲到实践:它怎么运作、那份 YAML 每一段在干嘛、同样一件事用 Jenkins 和 GitHub Actions 又怎么写,以及三者到底怎么选。适合会写业务代码、但对 CI/CD 还是“能看懂不会写”的前端 / 全栈同学。
一、先把问题摆上桌:没有 CI/CD 的时候有多痛
设想你负责一个前端项目,每次发版要手动走这么一套:
- 本地
git pull拉最新代码; npm install装依赖;npm run build打包出dist/;- 打开 FTP / 控制台,把
dist/里的文件拖上服务器; - 祈祷没传错。
这套流程的问题不是“麻烦”这么简单:
- 人会错:忘了
git pull、传错环境、只传了一半、忘了备份旧版本。 - 不可复现:“在我机器上是好的”—— 你本地 Node 是 18,同事是 20,打出来的包不一样。
- 出事难回滚:上线发现白屏,想退回上一版,发现没人备份。
- 不透明:谁、什么时间、用哪个 commit 部署的,全靠口头问。
CI/CD 要解决的就是这些。一句话:把“从代码到上线”这条路变成一条自动、可重复、可追溯的流水线。
二、CI/CD 到底是什么:一条流水线的通用心智模型
2.1 CI 和 CD 拆开看
- CI(Continuous Integration,持续集成):代码一提交就自动拉下来、装依赖、跑 lint / 测试 / 构建。目的是尽早发现“合进来的代码能不能成功构建”。
- CD(Continuous Delivery / Deployment,持续交付 / 部署):构建产物自动部署到测试 / 生产环境。
对前端来说,最常见的形态就是本文这种:push 代码 → 自动构建 → 自动部署到 CDN / 对象存储。
2.2 记住这四个词,换什么工具都一样
不管是 Azure Pipelines、Jenkins 还是 GitHub Actions,一条流水线都是这四个部件拼出来的:
| 部件 | 是什么 | 前端类比 |
|---|---|---|
| trigger(触发) | 什么事件启动流水线(push 到某分支 / 定时 / 手动) | 事件监听器addEventListener |
| runner / agent(机器) | 在哪台机器上跑这些命令 | 跑npm run build的那台电脑 |
| steps / stages(步骤) | 依次执行的命令列表 | package.json里串起来的 scripts |
| artifact(产物) | 构建产出、用于部署的东西(如dist/) | npm run build出的dist/ |
💡这是本文最值钱的一句话:学会任何一个 CI/CD 工具,理解了trigger → runner → steps → artifact这套通用模型,换另一个主要就是查语法。核心是“你懂不懂 CI/CD 这件事”,而不是“你会不会 Jenkins 某个插件”。
这条流水线画成图是这样的:
三、解剖一份真实的前端部署流水线(Azure Pipelines)
先上完整代码:
trigger:-dev# 只有推到 dev 分支才触发pool:vmImage:'ubuntu-latest'# 微软托管的临时构建机variables:environment:'dev'bucketName:'my-app-fe-$(environment)'# 例如 my-app-fe-devbuildCommand:'build:dev'steps:# checkout 是隐式的,Azure 自动把代码拉到机器上-task:NodeTool@0inputs:versionSpec:'20.x'displayName:'Install Node.js 20.x'-script:npm installdisplayName:'Install dependencies'-script:npm run $(buildCommand)displayName:'Build'-task:AWSShellScript@1inputs:awsCredentials:'aws-connection-dev'# Azure DevOps 里配的 Service ConnectionregionName:'ap-southeast-1'scriptType:'inline'inlineScript:|set -e BUCKET=$(bucketName) TIMESTAMP=$(date +%Y%m%d%H%M%S%3N) LIVE=s3://$BUCKET/live TEMP=s3://$BUCKET/temp BACKUP=s3://$BUCKET/backup/$TIMESTAMP/aws s3 sync $LIVE $BACKUP||echo "no live to backup"# ① 备份当前线上aws s3 rm $TEMP--recursive||echo "no temp to clean"# ② 清空临时目录aws s3 sync ./dist $TEMP--cache-control "max-age=31536000"--exclude "index.html"# ③ 静态资源aws s3 cp ./dist/index.html $TEMP/index.html--cache-control "no-cache,no-store,must-revalidate"# ④ HTMLaws s3 sync $TEMP/ $LIVE/--delete# ⑤ 原子切换到线上displayName:'Deploy to S3'3.1 逐段拆:头部都在声明“什么情况下、在哪里跑”
trigger: - dev:只有推送到dev分支才启动。这就是上面说的trigger。pool: vmImage: 'ubuntu-latest':告诉微软“给我开一台临时 Ubuntu 机器跑,干完就销毁”。这就是runner。注意:你不拥有任何机器。variables:变量。$(environment)是 Azure 的变量插值语法,拼出my-app-fe-dev。steps:按顺序跑的步骤。task是官方 / 市场提供的现成能力(如NodeTool@0装 Node、AWSShellScript@1跑 AWS 脚本);script则是直接跑 shell。
⚠️一个容易被忽略、但迁移时最坑的点:Azure 会隐式自动拉代码。你在
steps里根本没看到 checkout 一步,但代码就是在机器上。后面会看到 Jenkins / GitHub Actions 都必须手动写这一步。
3.2 部署脚本里的门道:backup → temp → live 三段式
很多人以为“部署”就是aws s3 sync ./dist s3://bucket一行事。这份脚本多了几道工序,每一道都有理由:
- 先备份
live/到backup/时间戳/—— 出事能回滚。 - 先传到
temp/而不是直接覆盖live/—— 上传过程中用户访问的还是旧版,不会看到“传一半”的破站。 - 最后
temp/ → live/ --delete一次性同步—— 接近原子切换,并且--delete会清掉 live 里新版已不存在的旧文件。
这是一个很值得学的部署思路:用一个中转目录 + 备份,把“部署”从“边传边生效”变成“准备好再一键切换”。
3.3 缓存策略:为什么 index.html 不缓存、其余缓存一年
这是整份配置里最能体现“懂行”的两行:
# 静态资源(JS/CSS/图片):缓存一年aws s3sync./dist$TEMP--cache-control"max-age=31536000"--exclude"index.html"# index.html:绝不缓存aws s3cp./dist/index.html$TEMP/index.html --cache-control"no-cache, no-store, must-revalidate"原理是现代前端打包的文件指纹(hash)机制:
- Vite / Webpack 打包出的 JS/CSS 文件名都带内容 hash,如
index-a1b2c3.js。内容一变,文件名就变——所以旧文件永远不会被覆盖,让浏览器狠狠地缓存一年都安全。 index.html是唯一的“入口”,名字固定,里面引用的是那些带 hash 的文件名。它必须不缓存,才能保证用户一刷新就拿到指向新 JS/CSS 的最新 HTML。
💡 一句话:带 hash 的资源长缓存,不带 hash 的入口 HTML 不缓存——这是 SPA 部署的标准姿势。配错了会出现“发了新版但用户还是旧页面”或“新 HTML 配旧 JS 白屏”。
四、同一条管道,三种方言
概念通用,但语法各不相同。先看一张概念映射表(横扫一行 = 同一件事的三种写法):
| 概念 | Azure Pipelines | Jenkins | GitHub Actions |
|---|---|---|---|
| 配置文件 / 语法 | azure-pipelines.yml·YAML | Jenkinsfile·Groovy | .github/workflows/*.yml·YAML |
| 触发 | trigger: [dev] | 多分支流水线靠分支名筛选 | on: push: branches: [dev] |
| 执行机器 | pool: vmImage(微软托管) | agent any(你自己的机器) | runs-on(GitHub 托管) |
| 拉代码 | 隐式·自动 | checkout scm | uses: actions/checkout@v4 |
| 装 Node | task: NodeTool@0 | tools { nodejs 'node-20' } | uses: actions/setup-node@v4 |
| 凭证 | awsCredentials(Service Connection) | withCredentials([...]) | secrets.AWS_ACCESS_KEY_ID |
| 部署 shell | aws s3 sync ... | aws s3 sync ... | aws s3 sync ... |
🔑最关键的认知:那段真正干活的
aws s3 sync部署 shell,三家一字不差(看上表最后一行)。工具差异只在“怎么声明触发 / 机器 / 步骤 / 凭证”这层外壳,里面的活是纯 shell,跟用哪个 CI 无关。
4.1 Jenkinsfile 版
pipeline{agent any// 用你自己挂的 Jenkins 机器environment{BUCKET_NAME='my-app-fe-dev'BUILD_COMMAND='build:dev'AWS_REGION='ap-southeast-1'}tools{nodejs'node-20'}// 需先装 NodeJS 插件,并配置名为 node-20 的工具stages{stage('Checkout'){steps{checkout scm}}// <- Azure 不用写,这里必须写stage('Install'){steps{sh'npm install'}}stage('Build'){steps{sh"npm run${BUILD_COMMAND}"}}stage('Deploy to S3'){steps{withCredentials([[$class:'AmazonWebServicesCredentialsBinding',credentialsId:'aws-connection-dev']]){sh''' set -e BUCKET=$BUCKET_NAME TIMESTAMP=$(date +%Y%m%d%H%M%S%3N) aws s3 sync s3://$BUCKET/live s3://$BUCKET/backup/$TIMESTAMP/ || true aws s3 rm s3://$BUCKET/temp --recursive || true aws s3 sync ./dist s3://$BUCKET/temp --cache-control "max-age=31536000" --exclude "index.html" aws s3 cp ./dist/index.html s3://$BUCKET/temp/index.html --cache-control "no-cache, no-store, must-revalidate" aws s3 sync s3://$BUCKET/temp/ s3://$BUCKET/live/ --delete '''}}}}}4.2 GitHub Actions 版
name:Deploy (dev)on:push:branches:[dev]env:BUCKET_NAME:my-app-fe-devBUILD_COMMAND:build:devjobs:build-and-deploy:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v4# 必须显式写,否则没代码-uses:actions/setup-node@v4with:node-version:'20.x'-run:npm install-run:npm run ${{env.BUILD_COMMAND}}-uses:aws-actions/configure-aws-credentials@v4with:aws-access-key-id:${{secrets.AWS_ACCESS_KEY_ID}}aws-secret-access-key:${{secrets.AWS_SECRET_ACCESS_KEY}}aws-region:ap-southeast-1-name:Deploy to S3run:|set -e BUCKET=${{ env.BUCKET_NAME }} TIMESTAMP=$(date +%Y%m%d%H%M%S%3N) aws s3 sync s3://$BUCKET/live s3://$BUCKET/backup/$TIMESTAMP/ || true aws s3 rm s3://$BUCKET/temp --recursive || true aws s3 sync ./dist s3://$BUCKET/temp --cache-control "max-age=31536000" --exclude "index.html" aws s3 cp ./dist/index.html s3://$BUCKET/temp/index.html --cache-control "no-cache, no-store, must-revalidate" aws s3 sync s3://$BUCKET/temp/ s3://$BUCKET/live/ --delete五、三大工具的真实差异(选型参考)
| Azure Pipelines | Jenkins | GitHub Actions | |
|---|---|---|---|
| 本质 | 微软托管的云服务 | 你自己搭的一台 CI 服务器 | GitHub 托管的云服务 |
| 机器谁维护 | 微软 | 你(装、升级、修磁盘) | GitHub |
| 语法 | YAML | Groovy(现代也支持 Jenkinsfile) | YAML |
| 与仓库关系 | Azure DevOps 自带、一体 | 独立系统,需接 Git | 和 GitHub 一体 |
| 生态 | Task(开箱即用) | 插件极丰富但要自己维护 | Marketplace Actions,生态最活跃 |
| 适合谁 | 代码在 Azure DevOps | 要高度定制 / 内网私有部署 | 代码在 GitHub(开源项目首选) |
💡 一句话选型:代码托管在哪,就用配套的那个。GitHub → GitHub Actions;Azure DevOps → Azure Pipelines;需要完全自控 / 内网环境 / 超多定制 → Jenkins。
六、从 Azure 迁到别家:踩坑清单
如果有一天你要把这条流水线从 Azure 搬到 Jenkins / GitHub Actions,以下几点最容易翻车:
- checkout 必须显式写。Azure 自动拉代码,另两家不写就是空目录,
npm install直接报找不到package.json。 - 凭证机制不同。Azure 用 Service Connection(后台配好名字引用);GitHub 用 Secrets;Jenkins 用 Credentials 插件。别把密钥硬编码进 YAML。
- 语法不能复制粘贴。Azure / GitHub 是 YAML,Jenkins 经典写法是 Groovy,迁移 = 重写外壳(但里面那段 shell 可以直接搬)。
- 变量插值语法各不一样:Azure
$(var)、GitHub${{ env.var }}、Jenkins${var}。 - 隐藏的环境差异:Jenkins 的
agent是你自己的机器,可能没装awsCLI / Node;托管的 runner 预装了常见工具,自托管要自己保证环境齐全。
七、最佳实践清单(可直接抄走)
- 部署前先备份当前线上(
backup/时间戳/),留回滚后路 - 用 temp 中转 + 一次性同步到 live,避免“边传边生效”的中间态
- 静态资源(带 hash)长缓存
max-age=31536000 - 入口
index.html绝不缓存no-cache, no-store, must-revalidate - 部署脚本开头加
set -e,任何一步失败立即中断,不要带病上线 - 密钥走平台凭证机制(Secrets / Service Connection / Credentials),绝不硬编码
- 跨工具迁移时,记得手动补上 checkout 一步
- 把环境相关变量(bucket、region、build 命令)抽成变量,别散在脚本里
八、一句话总结
CI/CD 工具是“一个抽象概念 + 多个具体实现”,就像前端框架之下有 React / Vue / Svelte。记住 trigger → runner → steps → artifact 这条通用链,你看懂的就不只是一份 YAML,而是 CI/CD 这件事本身。真正干活的那段 shell,换哪个工具都一样。