这里使用 node 作为上传服务
在目录创建upload.js
文件,假设已经存在一个publicServe
的方法上传文章
获取修改文件
通过git diff
可以判断哪些文章有修改。要注意如果文章名包含中文,需要设置git config --global core.quotepath false
,防止出现中文连码的情况。
import { execSync } from "child_process";
/**
* 获取两个 Git 提交之间的文件差异
* @param {string} latestCommitSha - 最新的 Git 提交 SHA
* @param {string} prevCommitSha - 上一个 Git 提交 SHA
* @return {{ oldFilePath: string; newFilePath: string }[]} - 返回一个对象,包含 status, oldFilePath, newFilePath
*/
function getDiffFiles(latestCommitSha, prevCommitSha) {
let diffOutput;
if (!prevCommitSha || !latestCommitSha) {
diffOutput = execSync(`git diff --name-status HEAD^ HEAD`).toString();
} else {
diffOutput = execSync(`git diff --name-status ${prevCommitSha} ${latestCommitSha}`).toString();
}
const diffArray = diffOutput.split("\n");
// 根据文件状态将文件分组
return diffArray
.map((line) => {
const [_, status, oldFilePath, newFilePath] = line.match(/(?:^([AMDR])\d*\s+([^\s]*)\s*([^\s]*)$)/) || [];
return status && oldFilePath ? { status, oldFilePath, newFilePath: newFilePath || oldFilePath || "" } : null;
})
.filter((item) => item && item.newFilePath.toLocaleLowerCase().endsWith(".md"))
.reduce((acc, { oldFilePath, newFilePath }) => {
acc.push({ oldFilePath, newFilePath });
return acc;
}, []);
}
这里并没有返回文件状态(修改,新增等),因为目前没有不考虑删除的情况,考虑到上传失败的情况,这些状态并没有太多的参考价值。
生成文章开篇
这里使用 chatgpt 生成开篇,使用chatgpt包,chatgpt 的 apiKey 通常保存在github secrets中,通过环境变量或者参数传递进来。考虑到 gpt 服务可能不稳定,一般需要多试几遍。
import { ChatGPTAPI } from "chatgpt";
const apiKey = process.env.API_KEY;
async function generateArticleIntro(content) {
for (let i = 1; i < 3; i++) {
try {
const api = new ChatGPTAPI({
apiKey,
});
const res = await api.sendMessage(`给下面的文章添加一个简要的开篇\n ${content}`);
if (res.text) {
return res.text;
}
} catch (e) {
if (i < 3) console.log("重试第" + i + "次");
else throw e;
}
}
return "";
}
获取之前执行失败的文件
生成开篇或者上传都可能存在失败的情况,需要我们记录下来,读取并添加到本次的流程中。
/**
* 获取修改文件并与上传失败记录合并
* @param {string} latestCommitSha - 最新的 Git 提交 SHA
* @param {string} prevCommitSha - 上一个 Git 提交 SHA
* @param {{ [oldFilePath: string]: { oldFilePath: string; newFilePath: string } }} preFailRecord - 上传失败记录
* @return {{ oldFilePath: string; newFilePath: string }[]} - 返回一个对象,包含 status, oldFilePath, newFilePath
*/
function ensureDiffFiles(latestCommitSha, prevCommitSha, preFailRecord) {
const diffFiles = getDiffFiles(latestCommitSha, prevCommitSha);
for (const [failFilename, file] of Object.entries(preFailRecord)) {
if (diffFiles.some((item) => item.newFilePath === file.newFilePath && item.oldFilePath === file.oldFilePath)) {
continue;
// 当前旧文件名和记录旧文件名相同,说明文件二次重名名,删除旧的失败记录
} else if (diffFiles.some((item) => item.oldFilePath === file.oldFilePath)) {
delete preFailRecord[failFilename];
continue;
// 当前新文件名和记录旧文件名相同,说明文件在失败基础上修改,旧文件名改为失败记录的
} else if (diffFiles.some((item) => item.oldFilePath === file.newFilePath)) {
const index = diffFiles.findIndex((item) => item.oldFilePath === file.newFilePath);
diffFiles[index].oldFilePath = file.oldFilePath;
delete preFailRecord[failFilename];
continue;
} else {
diffFiles.push(file);
}
}
return diffFiles;
}
preFailRecord 是记录上次执行失败的文件。这里需要考虑文件重命名的情况,删除等更复杂的情况暂时没有处理。
保存记录
在这里有三类数据需要额外记录
-
上次触发 action 的 sha,因为操作提交多次然后才上传的情况,不可能直接取上一次提交 sha。
-
失败记录,就是前面 preFailRecord 参数,因为不保证上传能一次性成功,需要加入后续 action 中重新执行
-
文件对应的上传数据,例如上传完成后返回的文件 id,用于更新或者判断是否需要生成开篇。
后面的问题是在哪里保存这些数据
-
在本仓库下保存,好处是容易读取和修改,缺点是提交记录会很难看
-
通过 action 缓存记录,缺点不易于本地测试,读取和修改也比较麻烦
-
通过服务器缓存,缺点是比较繁琐,需要额外的维护
-
子模块,这个无疑是最合适的方案,随时修改和读取,也可以免费托管到 github 上,也不会影响主仓库 git 提交记录
git 子模块可以参考这个文档
首先在 github 创建一个缓存仓库,这里起名 cache
然后添加到仓库下
git submodule add https://github.com/user/cache
会看到一个 cache 目录,然后在主模块目录下推送上去就好了。
这里需要在子模块(cache 目录下)创建 3 个文件, sha.json
recordFail.json
record.json
,对应上面三类数据,初始值写入{}
就可以。
然后在 cache 目录下执行git push
就可以更新子模块了
整合
根据上传结果更新 json 文件就可以
import { readFileSync, writeFileSync } from "fs";
async function main() {
const prevCommitSha = readJsonFile("./cache/sha.json")?.sha;
const latestCommitSha = process.env.GITHUB_SHA;
const fileRecord = readJsonFile("./cache/record.json");
const preFailRecord = readJsonFile("./cache/recordFail.json");
const diffFiles = ensureDiffFiles(latestCommitSha, prevCommitSha, preFailRecord);
for (const file of diffFiles) {
const { oldFilePath, newFilePath } = file;
const toSaveDate = {
title: getFileName(newFilePath),
content: readFileSync(newFilePath, "utf-8"),
};
// 是否未记录,未记录则生成开篇
try {
if (!fileRecord[oldFilePath]) {
const overview = await generateArticleIntro(toSaveDate.content);
toSaveDate.overview = overview;
} else {
toSaveDate.id = fileRecord[oldFilePath];
}
const id = await publicServe(toSaveDate);
if (id) {
// 处理重命名
if (oldFilePath !== newFilePath) delete fileRecord[oldFilePath];
fileRecord[newFilePath] = id;
delete preFailRecord[newFilePath];
} else {
preFailRecord[newFilePath] = file;
}
} catch (e) {
preFailRecord[newFilePath] = file;
console.log(e);
}
}
saveJsonFile("./cache/sha.json", { sha: latestCommitSha });
saveJsonFile("./cache/record.json", fileRecord);
saveJsonFile("./cache/recordFail.json", preFailRecord);
}
function getFileName(path) {
return path.split("/").pop().split(".")[0];
}
// 读取 json 文件
function readJsonFile(path) {
return JSON.parse(readFileSync(path, "utf-8"));
}
// 保存 json 文件
function saveJsonFile(path, data) {
writeFileSync(path, JSON.stringify(data, null, 2));
}
增加 action
创建文件.github/workflows/upload.yml
name: Upload
on:
push:
branches: [main]
permissions: write-all
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
with:
submodules: recursive
token: ${{ secrets.TOKEN }}
fetch-depth: 0
- uses: pnpm/action-setup@v2
with:
version: 6.0.2
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "pnpm"
- name: update submodule
run: git submodule update --recursive --remote
- name: run upload
run: |
git config --global core.quotepath false
pnpm install
node upload.js
env:
API_KEY: ${{ secrets.API_KEY }}
- name: commit changes
run: |
git config --global user.name "yjrhgvbn"
git config --global user.email "yjrhgvbn@gmail.com"
cd cache
git commit -a -m "Add record"
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.TOKEN }}
directory: "./cache"
repository: "yjrhgvbn/cache"
force: true
action 这里不多结束,只有有几个注意的地方
-
permissions: write-all
需要增加写入权限,不然无法更新文件 -
fetch-depth: 0
,默认情况下actions/checkout@v3只会拉取最后一次提交,这里需要拉取之前的提交进行 diff -
git submodule update --recursive --remote
,拉取最新的子模块 -
git config --global core.quotepath false
,防止 diff 时中文乱码
最后
完整项目可以查看项目。