- 首先解释:.github是什么?workflows是什么?ci.yml是什么?
- 详细了解ci.yml中的每一个内容
- 基于此上述两个内容,我们将向当前文件中添加一个自动的更新贡献者信息的功能
.github是什么?workflows是什么?ci.yml是什么?
.github目录
这个名词是固定的,这是 GitHub 存放特定配置文件的地方。
这是一个特殊的目录,用于存放所有与 GitHub 特定功能相关的配置文件。 这包括 GitHub Actions 工作流配置、issue 模板、pull request 模板 等等
workflows目录
在 .github 目录下,专门用于存放 GitHub Actions 的工作流配置文件。 这些配置通常是 YAML 格式,主要定义一些自动化步骤,在指定事件被触发时自动执行某些操作
ci.yml文件
这就是一个工作流配置文件, ci 代表 “Continuous Integration”(持续集成)
详细描述 ci.yml 中的内容
name: CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install and Build 🔧
run: |
pnpm install
pnpm build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4
with:
TOKEN: ${{ secrets.ACCESS_TOKEN }}
FOLDER: dist
CLEAN: true
name on push push_request 分别是什么?
name:工作流名称,帮助用户快速识别工作流的目的和功能。
on:指定了触发工作流的事件。在这个上下文中,on 关键字后面列出了哪些 GitHub 事件应该触发这个工作流。
push: branches: ["main"]
:这段代码指定当有新的代码被推送(push)到 main 分支时,触发 CI 工作流。这是持续集成中最常见的设置,明确列出了哪个分支(main)的推送会触发工作流
pull_request.branches: ["main"]
:在处理 GitHub 上的拉取请求(Pull Requests)时,触发 CI 工作流。
jobs build-and-deploy runs-on 分别是什么?
jobs:jobs 是 GitHub Actions 工作流中的顶级键,用于定义工作流中包含的一个或多个作业。每个作业包含一系列步骤,这些步骤定义了具体要执行的任务,如代码检出、依赖安装、测试运行、构建应用和部署等。
build-and-deploy:是作业的唯一标识符,也是该作业的名称。在 YAML 中,作业的标识符(如 build-and-deploy)直接作为键出现,这是定义作业的必需部分。
runs-on:指定作业环境的键,定义了作业应该在哪种类型的虚拟机或容器上执行。 ubuntu-latest
指定作业将在 GitHub 提供的最新稳定版本的 Ubuntu 虚拟环境中运行。
作业?:build-and-deploy 就是一个作业,你也可以理解为一个工作,其父级名称就是 jobs 特点就是:每个作业都在一个全新的运行器实例中启动,拥有干净的、隔离的执行环境。这意味着作业之间不会共享环境状态,例如环境变量或文件系统的变化。多个作业可以并行执行,除非它们之间存在明确的依赖关系。作业可以配置为依赖于其他作业的完成。 在 GitHub Actions 中,作业(Job) 是构成工作流(Workflow)的基本组成部分。
strategy matrix node-version 分别是什么?
strategy(策略/战略):用于指定一个或多个策略这些策略控制如何运行作业。
matrix(矩阵/模型):是 strategy 下的一个常见策略,允许定义多个不同的配置变量,GitHub Actions会为矩阵中的每个组合创建并运行作业。主要是在多环境下测试应用,确保应用在不同配置或不同高版本的环境中均能正确运行
node-version
它指定 Node.js 版本,在这个例子中 node-version: [16.x]
指明使用 Node.js 的16.x版本进行构建和测试
数组形式:[16.x]
表示这是一个数组,当前只包含一个元素(16.x)。如过失 [14.x, 16.x, 18.x],这样的话 GitHub Actions 将为这三个 Node.js 版本各自执行一次作业。
指定 Node.js 版本时,16.x 表示使用 Node.js 16 的任何次版本(minor version),也称为“次要”版本。x 代表的是通配符,意味着它会匹配该主版本(major version)下的最新发布的次版本。使用 x 不是必需的,但它是一种常见的做法,它提供了一种确保使用最新兼容次版本的自动方式,无需每次手动更新配置文件。你完全可以直接指定完整的版本号,例如 16.13.0。
如果应用需要支持多个node版本,可以进行扩展,GitHub Action 会自动为每个版本运行相同的构建和测试脚本,确保应用和这些版本是兼容的。 tip:对于大多数前端开源项目,不需要为多个Node.js本不能构建和测试,一种常见的做法是选择一个活跃维护状态的LTS版本。就当前项目而言构建工具依赖于Vite应该参考 Node 支持 Node.js 18 / 20+
steps步骤
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
Checkout:这一行提供了步骤的描述性名称,这里使用 “Checkout 🛎️” 来说明这个步骤的作用是检出(Checkout)代码
uses: actions/checkout@v3:关键字指定了要在此步骤中使用的动作(Action)。这里使用的是官方提供的 actions/checkout 动作,版本为 3。用于检出仓库中的代码,使其可在后续步骤中使用,例如构建或测试。这是大多数工作流中的第一步,后续操作通常需要访问这些仓库代码。就当前写这篇文章的时间来看, actions/checkout@v3
应该升级到v4,actions/checkout@v3 action在后台使用了node 16,这被github弃用了。Node16于2023年9月11日终止生命周期。v4运行环境为node20
Setup pnpm:描述性名称,这一步骤用于设置 pnpm 包管理器。这是一个由社区或第三方开发的 GitHub Action,专门用于在 GitHub Actions 的运行环境中安装和设置 pnpm(Performant npm),一个流行的 Node.js 包管理器。
pnpm是什么? npm是什么?yargs又是什么?
pnpm
是一种用于 Node.js 的包管理器,它类似于 npm 和 yarn,用于自动化管理项目的依赖项。pnpm 的核心特性是高效地管理节点模块,从而优化了存储空间和加快了依赖项的安装速度。它通过使用硬链接(hard links)和符号链接(symlinks)来共享一个版本的模块多次使用而不是重复下载,从而达到节省空间的效果。
npm
是 Node.js 的官方包管理器,广泛使用于 JavaScript 生态中。它易于使用,支持广泛,并直接集成在 Node.js 安装包中。
yarn
是由Facebook提供的包管理工具,旨在提高性能和安全性。
就当前时间节点的社区下载量看,pnpm下载量是npm的2倍,也是yarn的2倍。
社区的开源项目,都在向新的node版本转移,就当前项目的自动化构建和部署
pnpm: https://github.com/pnpm/pnpm/releases/tag/v9.0.0
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
name: Setup Node.js ${{ matrix.node-version }}
:指定了一个名称,显示在 GitHub Actions 的执行日志中。这里使用了表达式 ${{ matrix.node-version }} 来动态显示使用的 Node.js 版本,这个版本是通过策略矩阵(strategy matrix)指定的。例如,如果矩阵中指定了版本 16.x,步骤名称将显示为 “Setup Node.js 16.x”。
uses: actions/setup-node@v3
:指令用来指定 GitHub Action。在这里,它使用了 setup-node Action 的第三版(v3),这是一个官方提供的用于安装 Node.js 的 Action。
node-version: ${{ matrix.node-version }}
:这条指令设置 Action 要安装的 Node.js 的版本。这里同样使用了 ${{ matrix.node-version }},它会从工作流的策略矩阵中取得 Node.js 的版本。
cache: 'pnpm'
:这条指令启用缓存,用于加速后续的包管理操作。在这个配置中,它设置为使用 pnpm 的缓存机制。这意味着 GitHub Actions 将会缓存 pnpm 的依赖文件,以便在后续的运行中重用,减少安装依赖所需的时间。
- name: Install and Build 🔧
run: |
pnpm install
pnpm build
这段代码是 GitHub Actions 工作流中的一个步骤(step),用于在设置好的 Node.js 环境中执行 pnpm 命令来安装依赖并构建项目。
run
指令用于执行命令行操作。它可以执行单个命令或多行命令。在这里,它通过 | 符号引入了一个命令块,允许多个命令顺序执行。
pnpm install
:这个命令用于安装项目的依赖。pnpm 是一个包管理器,类似于 npm 和 yarn,但它以一种更高效的方式处理依赖和磁盘空间。这一步确保所有在项目的 package.json 文件中声明的依赖项都被安装到虚拟环境中。
pnpm build
:这个命令通常用于执行项目的构建脚本,这在 package.json 文件中的 scripts 部分定义。构建过程可能包括编译代码、压缩资源、打包等,具体取决于项目的具体配置和需求。
此步骤是许多 CI/CD 流程中的核心部分
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4
with:
TOKEN: ${{ secrets.ACCESS_TOKEN }}
FOLDER: dist
CLEAN: true
代码配置了 GitHub Actions 工作流中的一个部署步骤,专门用来将项目部署到 GitHub Pages。这里使用的是 James Ives 开发的 github-pages-deploy-action。
name: Deploy 🚀
:此行定义了步骤的名称,“Deploy”,配以一个火箭 emoji 🚀,表示这个步骤是将项目部署到服务器或云环境。
uses: JamesIves/github-pages-deploy-action@v4
:使用 uses 关键词来引入外部的 GitHub Action。这里指定的是 James Ives 的 github-pages-deploy-action,版本号为 v4。
这个 Action 封装了将代码部署到 GitHub Pages 的过程,包括处理文件上传、历史版本控制等,简化了部署步骤。
with
:关键词用来定义传递给 Action 的参数。
TOKEN: ${{ secrets.ACCESS_TOKEN }}
:TOKEN 参数需要一个访问令牌,这里使用 secrets.ACCESS_TOKEN。这是一个在 GitHub 仓库设置的密钥,用于授权这个 Action 访问你的 GitHub 仓库。这样的设置增强了安全性,确保敏感数据不在配置文件中硬编码。
FOLDER: dist
:指定要部署的文件夹,这里是 dist。通常这个文件夹包含了构建过程生成的所有静态文件,这些是要被上传到 GitHub Pages 的内容。
CLEAN: true
:启用清理选项。当设置为 true 时,这个 Action 会在部署前清除旧的文件,确保只有最新的构建输出被发布。这有助于避免旧文件积累或可能的冲突。
这个部署步骤是自动化流程的关键环节,允许开发者将静态网站或文档自动部署到 GitHub Pages。
ACCESS_TOKEN是什么?GITHUB_TOKEN是什么?那么他们是否可以互相替换?
ACCESS_TOKEN
通常指的是一个个人访问令牌(Personal Access Token, PAT)。这种令牌由用户手动在 GitHub 中创建,并可以具有广泛的权限,从而让持有者可以进行各种操作,比如访问私有仓库、写入仓库、管理组织和项目等。
GITHUB_TOKEN
是自动由 GitHub Actions 生成的,用于授权在当前仓库中进行操作的内置令牌。每次工作流运行时,GitHub 都会自动创建一个新的 GITHUB_TOKEN,并在运行结束后使其失效。
在大多数常规的部署场景中,GITHUB_TOKEN 是足够的,特别是当操作限于当前仓库时。如果你的部署任务(例如部署到 GitHub Pages)不需要跨仓库或其他高级权限,可以使用 GITHUB_TOKEN。
我们来实现一个, 在github中根据PR的提交信息来自动化添加贡献者的功能,通过all-contributors
on:
pull_request_target:
branches:
- main
types: [closed] # 仅在合并后触发
pull_request_target 是什么? pull_request 又是什么? 有什么区别?
它们的定义和触发条件都是一致的:在创建、更新、重新打开或关闭拉取请求(PR)时触发。当有人向你的仓库提出拉取请求时,或更新了现有的拉取请求时。 pull_request
- 在默认情况下,是无法访问 GitHub Secrets。目的是为了防止潜在的恶意代码泄露机密信息
- 运行在拉取请求的源代码分支上(即外部贡献者的分支上)。
pull_request_target
- 可以访问 GitHub Secrets,因为运行的代码是能仓库中受信任的代码(主分支)
- 运行在拉取请求的目标分支上(通常是你的主分支或其他目标分支)。虽然运行的是目标分支的代码,但同时也可以获取到 PR 的上下文信息。
根据操作需求选择 pull_request 或 pull_request_target。
- 使用 pull_request 进行代码验证和测试,确保不访问机密信息。
- 使用 pull_request_target 进行需要机密信息的操作,如推送、部署等。
根据当前子标题,看出来我们的需求是,当仓库收到PR时候,action 或监听到并且找到PR的提交者信息和提交的类型,在通过 all-contributors 工具 去自动生成一些信息,让后自动将信息提交到主分支,让后push到仓库中。 这些就需要用到
pul_request_target
事件,因为在这个自动化流程中需要访问SUCCESS_TOKEN
来自动将生成的新的代码提交到主分支。
jobs:
test-pat:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
strategy:
matrix:
node-version: [20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # 确保使用PR的最新提交的内容和commit
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install
- name: Echo Token Length # 添加一个步骤来检查 token 长度
run: echo "Token Length is ${#PERSONAL_ACCESS_TOKEN}"
env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Setup Git
run: |
git config --global user.email "ntscshen@163.com"
git config --global user.name "ntscshen"
git remote set-url origin https://x-access-token:${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/ntscshen/pr_test.git
env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Switch to main branch and pull latest
run: |
git fetch --all
git checkout -f main
git pull origin main
- name: Add Contributor
run: |
export GITHUB_ACTOR=${{ github.event.pull_request.user.login }}
node scripts/updateContributors.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTOR: ${{ github.event.pull_request.user.login }} # 使用PR创建者的用户名
- name: Push changes to remote
run: git push origin main
env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
github.event.pull_request.head.sha
这是PR创建者在其分支上做的最后一次提交,通常用于合并前的各种检查。
当PR被接受并合并到目标分支(如main
)时,通常会生成一个新的”合并提交”。 github.event.pull_request.head.sha
不包括这个合并操作产生的提交。
当工作流由 pull_request_target
事件触发时,默认情况下 actions/checkout 会检出一个虚拟的 merge commit,这是 GitHub
动态创建的。当PR被接受并合并进目标分支,这个虚拟commit会包含这次合并的commit。当使用 actions/checkout 而没有指定 ref 时,检出的是虚拟合并分支的状态,这个状态包含所有的PR相关的commit信息。
为什么要指定ref:精准控制检出内容,有时候可能需要确保工作流使用的PR的确切内容,在这些情况下,通过明确指定 ref 参数可以确保检出的是我们确切希望的代码状态。
step步骤如下
- actions/checkout@4
检出PR分支的代码,确保工作流最新提交的内容上运行,
ref: ${{ github.event.pull_request.head.sha }}
确保检出的代码是PR中最新的提交 - pnpm/action-setup@v4
设置
pnpm
包管理并指定版本9 - actions/setup-node@v4 设置 Node.js 环境,并启用 pnpm 缓存
- pnpm install:安装项目的依赖项
- echo “Token Length is ${#PERSONAL_ACCESS_TOKEN}” 用于验证token的有效性
在
pull_request
当前值为0,在pull_request_target
当前值为40或>0 - Setup Git
配置 Git 用户信息,设置远程仓库的URL地址,使用
PERSONAL_ACCESS_TOKEN
进行身份验证git config --global user.email "ntscshen@163.com"
git config --global user.name "ntscshen"
git remote set-url origin https://x-access-token:${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/ntscshen/pr_test.git
使用 PERSONAL_ACCESS_TOKEN 代替用户名和密码进行身份验证。这对于 GitHub Actions 工作流中的自动化操作是必要的,因为你不能在工作流中手动输入用户名和密码。 - Switch to main branch add pull latest
切换主分支并拉取最新代码
- 获取所有分支的最新更新
- 强制切换到主分支
- 从远程主分支拉取最新的代码
- Add Contributor:根据PR创建者的信息,运行脚本更新贡献者列表
- git push origin main:将更改推送到远程分支
node scripts/updateContributors.js
import { execSync } from 'node:child_process';
import * as fs from 'node:fs';
const typeMap = {
feat: 'code',
style: 'code',
refactor: 'code',
perf: 'code',
revert: 'code',
types: 'code',
wip: 'code',
chore: 'tool',
build: 'tool',
ci: 'tool',
test: 'test',
fix: 'bug',
docs: 'doc',
};
function updateContributors(username, type) {
const content = fs.readFileSync('.all-contributorsrc', 'utf-8');
const contributors = JSON.parse(content);
console.log('contributors: ', contributors);
console.log('username', username);
console.log('type: ', type);
// 检查用户是否已存在
const exists = contributors.contributors.some((contributor) => contributor.login === username);
if (!exists) {
console.log(`Adding new contributor: ${username}`);
const command = `npx all-contributors-cli add ${username} ${type}`;
console.log(`Running command: ${command}`);
execSync(command, { stdio: 'inherit' });
console.log('成功添加贡献者.');
// Generate the contributors list after adding a new contributor
console.log('生成贡献者名单...');
const generateCommand = 'npx all-contributors-cli generate';
console.log(`Running command: ${generateCommand}`);
execSync(generateCommand, { stdio: 'inherit' });
console.log('贡献者名单已更新.');
// 阅读更新后的 README.md 内容
const readmeContent = fs.readFileSync('README.md', 'utf-8');
// 提取贡献者徽章部分
const badgeStartMarker = '<!-- ALL-CONTRIBUTORS-BADGE:START -->';
const badgeEndMarker = '<!-- ALL-CONTRIBUTORS-BADGE:END -->';
const badgeStartIndex = readmeContent.indexOf(badgeStartMarker);
const badgeEndIndex = readmeContent.indexOf(badgeEndMarker) + badgeEndMarker.length;
const contributorsBadgeSection = readmeContent.substring(badgeStartIndex, badgeEndIndex);
// 提取撰稿人名单部分
const listStartMarker = '<!-- ALL-CONTRIBUTORS-LIST:START -->';
const listEndMarker = '<!-- ALL-CONTRIBUTORS-LIST:END -->';
const listStartIndex = readmeContent.indexOf(listStartMarker);
const listEndIndex = readmeContent.indexOf(listEndMarker) + listEndMarker.length;
const contributorsListSection = readmeContent.substring(listStartIndex, listEndIndex);
// 阅读 README.zh-CN.md 内容
let readmeZhCnContent = fs.readFileSync('README.zh-CN.md', 'utf-8');
// 删除 README.zh-CN.md 中现有的贡献者徽章部分
const existingBadgeStartIndex = readmeZhCnContent.indexOf(badgeStartMarker);
const existingBadgeEndIndex = readmeZhCnContent.indexOf(badgeEndMarker) + badgeEndMarker.length;
if (existingBadgeStartIndex !== -1 && existingBadgeEndIndex !== -1) {
readmeZhCnContent = readmeZhCnContent.slice(0, existingBadgeStartIndex) + readmeZhCnContent.slice(existingBadgeEndIndex);
}
// 在 README.zh-CN.md 中插入翻译好的贡献者徽章部分
readmeZhCnContent = readmeZhCnContent.slice(0, existingBadgeStartIndex) + contributorsBadgeSection + readmeZhCnContent.slice(existingBadgeStartIndex);
// 删除 README.zh-CN.md 中现有的贡献者列表部分
const existingListStartIndex = readmeZhCnContent.indexOf(listStartMarker);
const existingListEndIndex = readmeZhCnContent.indexOf(listEndMarker) + listEndMarker.length;
if (existingListStartIndex !== -1 && existingListEndIndex !== -1) {
readmeZhCnContent = readmeZhCnContent.slice(0, existingListStartIndex) + readmeZhCnContent.slice(existingListEndIndex);
}
// 在 README.zh-CN.md 中插入已翻译的贡献者名单部分
readmeZhCnContent = readmeZhCnContent.slice(0, existingListStartIndex) + contributorsListSection + readmeZhCnContent.slice(existingListStartIndex);
// 将更新内容写入 README.zh-CN.md
fs.writeFileSync('README.zh-CN.md', readmeZhCnContent);
} else {
console.log('已存在贡献者,跳过...');
}
}
function main() {
const username = process.env.GITHUB_ACTOR;
console.log('fix: 111updateContribcutors.js - GITHUB_ACTOR :>> ', username);
if (!username) {
console.error('未定义 GITHUB_ACTOR。.');
process.exit(1);
}
const lastCommitMessage = execSync('git log -1 --pretty=%B').toString().trim();
const commitType = lastCommitMessage.split(' ')[0];
const contributionType = typeMap[commitType] || 'code';
updateContributors(username, contributionType);
// 检查文件状态
const checkFileStatus = (filePath) => {
const status = execSync(`git status --porcelain ${filePath}`).toString().trim();
console.log(`${filePath} status: ${status}`);
return status;
};
const allContributorsrcStatus = checkFileStatus('.all-contributorsrc');
const readmeStatus = checkFileStatus('README.md');
// 检查 git status 以确认有变更
const changes = execSync('git status --porcelain').toString().trim();
console.log('Git changes:', changes);
if (changes) {
console.log('检测到更改,继续提交.');
} else {
console.log('未检测到更改,跳过提交.');
}
}
main();
- 获取PR作者信息、获取commit信息(commitType)
- 更新contributors列表
- 判断当前PR用户是不是之前就存在在列表中的,如果是则不需要做任何更新
- 如果不是则触发列表更新
npx all-contributors-cli add ${username} ${type}
npx all-contributors-cli generate
all-contributors
只会更新 README.md 文件,但是我们文档中还有README.zh-CN.md
文件- 所以需要将对应的中文文档也进行更新