在基于GitHub Pages的博客更新时同步更新GitHub Profile个人信息页最近博文部分

我在GitHub的自定义个人信息页中添加了展示最近更新的博文的部分,但是每次更新完博客还要再来这里手动更新一下,就很麻烦。于是我本着"偷懒是人类进步的动力"的原则,搞出了一种博客部署完成后,使用GitHub Actions自动更新个人信息页的最近博文部分的方法。

提醒:本文中使用的博客框架为Hexo,博客主题为Next。RSS Feed采用Atom 1.0格式

基本原理

auto_update_profile-cn.webp

此方法的基本原理如上图所示,我们首先要在博客仓库与Profile仓库中各设置一个Workflow,然后当我们执行hexo deploy操作将博客文件推送到GitHub仓库后,GitHub Pages会自动运行一个Workflow来部署博客网站,当部署完成后,我们设置的Trigger Profile Update Workflow会被唤起,它的作用是调用Profile仓库中的另一个Workflow:Update Latest Blog Posts。Profile仓库中的Workflow会运行update-readme.mjs脚本,脚本首先会访问博客的RSS Feed,读取其中的内容,并使用正则表达式提取博文的标题与链接,接着读取README.md文件,找到我们预先设置的标识符,使用提取到的内容替换标识符中原有的内容,并将所做的修改写回README.md文件。最后Workflow会将更改的内容push回仓库中。

启用RSS功能

首先我们需要开启博客的RSS功能,以便脚本获取博客的标题与URL。开启Hexo框架的RSS功能需要安装hexo-generator-feed插件,这个插件支持生成Atom 1.0 与 RSS 2.0 的feed。

1
npm install hexo-generator-feed --save

关于Atom 1.0 与 RSS 2.0

Atom 1.0 和 RSS 2.0 都是用于发布博客、新闻和其他网页内容的标准化格式。它们有一些区别,主要包括以下几个方面:

1. 数据格式:

  • Atom 1.0 使用一种 XML 数据格式,它具有更统一和一致的结构,更适合处理机器读取。
  • RSS 2.0 也使用 XML,但它的结构相对不太统一,因此在解析时可能需要更多的处理。

2. 标题和作者:

  • Atom 1.0 对标题和作者等元数据有更严格的要求,其中标题和作者是必需字段。
  • RSS 2.0 对标题和作者等元数据的要求相对较松,这些字段是可选的。

3. 扩展性:

  • Atom 1.0 提供了更强大的扩展性,允许添加自定义元数据和命名空间,以满足不同应用的需求。
  • RSS 2.0 也支持扩展,但相对较为有限,扩展性不如 Atom。

4. 发布日期:

  • Atom 1.0 使用 “published” 元素来表示博文的发布日期,这是一个标准化的日期格式。
  • RSS 2.0 使用 “pubDate” 元素来表示发布日期,这个日期格式相对自由,可能需要更多解析。

5. 条目链接:

  • Atom 1.0 使用 “link” 元素来表示博文的链接,这是博文的固定链接。
  • RSS 2.0 也使用 “link” 元素,但它有时可能表示不同的链接类型(例如,博文的固定链接或者评论链接)。

总的来说,Atom 1.0 更加规范和一致,适用于需要高度结构化数据的应用。而 RSS 2.0 在一些方面更加灵活,适合一些较为简单的应用场景。选择使用哪种标准通常取决于你的具体需求以及你希望的数据结构和格式。

源自Chat GPT

安装完成后打开Hexo框架的配置文件_config.yml,向其中添加下面的配置:

_config.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Rss Feed
feed:
enable: true
type: atom
path: atom.xml
limit: 20
hub:
content:
content_limit: 140
content_limit_delim: ' '
order_by: -date
icon: icon.png
autodiscovery: true
template:

接下来的步骤用于在博客网站中添加一个RSS订阅按钮,没有需要的话可以跳过

打开Next主题的配置文件_config.next.yml,使用搜索功能快读定位到# Social Links配置,在social:下面添加一行RSS Feed: /atom.xml || fa fa-rss,示例如下:

_config.next.yml
1
2
3
4
5
6
7
# Social Links
# Usage: `Key: permalink || icon`
# Key is the link label showing to end users.
# Value before `||` delimiter is the target permalink, value after `||` delimiter is the name of Font Awesome icon.
social:
GitHub: https://github.com/Siriusq || fab fa-github
RSS Feed: /atom.xml || fa fa-rss

这样就可以在侧栏中显示RSS订阅按钮了。

提示:当博客的标题含有中文时,直接在浏览器中查看atom.xml可能会出现乱码,但是右键另存到本地后再用VSCode打开的话是正常的,不用管。

权限提升

获取GitHub Token

在我们的操作中,涉及到了跨仓库调用GitHub Actions的情况,GitHub Actions默认的权限不能满足这种操作,我们需要申请一个Personal Access Token,用它来赋予GitHub Actions的额外的访问权限。

关于Personal Access Token

GitHub Personal Access Token(个人访问令牌)是一种用于身份验证和访问 GitHub API 的令牌。它具有以下作用:

  1. 身份验证:个人访问令牌允许你以你的 GitHub 帐户身份访问 GitHub 资源,而无需使用密码。这提供了更安全的身份验证方式,同时不需要暴露你的 GitHub 帐户密码。

  2. 访问权限:你可以为每个个人访问令牌设置不同的权限,从只读访问到完全仓库访问权限。这使得你可以根据不同的用例和应用程序需求创建有限的、仅适用于特定操作的令牌。

  3. API 访问:你可以使用个人访问令牌来通过 GitHub API 进行各种操作,如获取仓库信息、创建问题、推送代码等。这对于自动化操作和集成 GitHub 到应用程序或工作流程中非常有用。

  4. 绕过限制:在某些情况下,GitHub 有一些 API 限制,如访问频率限制。使用个人访问令牌,你可以以你的帐户身份绕过这些限制。

  5. 应用程序集成:个人访问令牌还可以用于将 GitHub 集成到第三方应用程序中,允许这些应用程序执行特定的 GitHub 操作。

个人访问令牌是一个强大的工具,但应该小心管理它们,以确保安全性。不要轻易分享或泄露你的令牌,只授予必要的权限,并定期审查和撤销不再需要的令牌,以降低潜在的风险。

源自Chat GPT

  1. 点击GitHub网页右上角的头像
  2. 在弹出的面板中点击Settings
  3. 点击左侧边栏最下方的Developer seetings
  4. 点击Personal access tokens中的Tokens(classic)
  5. 点击右上方的Generate new token
  6. 选择Generate new token(classic)
  7. Expiration设置为No expiration
  8. Select scopes中勾选repo选项
  9. 滚动到页面最下方,点击Generate token
  10. 复制生成的Token,注意在离开这个页面后就再也见不到这个Token了,所以一定妥善保管
1-2.webp
3.webp
4-6.webp
7-8.webp
9.webp
10.webp

设置Secret

我们申请的Token具有对仓库的完全访问权限,自然不能以明文方式出现在Workflow文件中,GitHub允许我们使用repository secret功能,将Token以变量的方式存储在当前仓库安全存储中,降低泄露的可能性。

关于Repository Secret

GitHub 仓库(Repository)的 Secrets 是一种用于存储敏感信息或机密数据的方式,以便在 GitHub Actions Workflow、GitHub Actions Job 和 GitHub Repository 的其他部分中使用。这些 Secrets 的作用包括:

  1. 安全地存储敏感数据:Secrets 提供了一个安全的存储方式,可用于存储敏感数据,如访问令牌、密码、API 密钥等。这些数据通常不应该明文存储在代码中或 Workflow 文件中,因为这会增加安全风险。

  2. 用于 GitHub Actions Workflow:你可以将 Secrets 传递给 GitHub Actions Workflow,以用于自动化工作流程中的不同操作,如构建、部署和测试。这有助于确保敏感信息不会在 Workflow 输出中泄露。

  3. 访问外部服务:Secrets 可以用于访问外部服务和 API,如部署到云服务或与其他集成进行身份验证。它们使你可以在 Workflow 中以安全方式使用这些服务,同时不会泄露机密数据。

  4. 灵活性:Secrets 允许你根据需要创建和管理多个 Secrets。这使得你可以为不同的工作流程和场景使用不同的密钥和令牌,以确保最小化安全风险。

  5. 自动化操作:GitHub Actions 可以在 Workflow 中自动使用 Secrets,从而不需要手动输入敏感信息,提高了自动化操作的效率。

  6. 更新和轮换:GitHub 仓库 Secrets 允许你定期更新或轮换敏感信息,以增加安全性。

请注意,GitHub 仓库 Secrets 是受到限制和安全审查的,只有拥有适当权限的人员可以访问和管理它们。这有助于确保 Secrets 的安全性和保密性。

源自Chat GPT

在 Workflow 中使用 Repository Secrets 的原理

GitHub Actions 在 Workflow 中使用 Repository Secrets 的原理如下:

  1. 创建 Repository Secret:在 GitHub 仓库的设置中,你创建了一个 Repository Secret,为其指定了一个名称(key)和对应的值。这个 Secret 存储在 GitHub 的安全存储中,只有有权访问仓库的用户和 Workflow 可以使用它。

  2. Workflow 中的环境变量:在你的 GitHub Actions Workflow 文件中,你可以通过 $ 语法来引用 Repository Secret。这个语法允许你访问 Secret 的值,而不需要直接在 Workflow 文件中明文包含敏感信息。

  3. 传递给 Workflow 环境变量:GitHub Actions 在运行 Workflow 时,会将 Repository Secrets 的值传递给 Workflow 中的环境变量。这些环境变量在 Workflow 的执行环境中可用,并可以像普通环境变量一样使用。

  4. 安全性:GitHub 保护了这些 Secrets,确保它们不会被泄露或直接暴露给 Workflow 外部。这意味着 Secrets 在 Workflow 的输出、日志或其他可视化输出中不可见。只有 Workflow 内部的代码可以访问它们。

  5. 使用 Secret:在 Workflow 中,你可以使用这些环境变量,如访问令牌或 API 密钥,来执行需要身份验证或敏感数据的操作,比如访问私有仓库、部署到云服务、与外部 API 通信等。

总之,Repository Secrets 提供了一种在 Workflow 中安全存储和访问敏感信息的方式,同时不会将这些信息暴露给 Workflow 的外部。这使得 GitHub Actions 可以在自动化流程中处理敏感数据,而无需将其硬编码到 Workflow 文件中,从而提高了安全性和可维护性。

源自Chat GPT

设置步骤如下:

  1. 打开博客仓库的Settings页面
  2. 点击左侧边栏中Secrets and variables下的Actions
  3. 点击New repository secret
  4. 我这里将SecretsName设置为了PAT_TOKEN_PROFILE
  5. 将上一步申请的Token粘贴到Secret
  6. 点击Add Secret
1-3.webp
4-6.webp
a.webp

配置Workflow

接下来让我们配置两个仓库的Workflow,博客仓库只需要简单的设置一个Workflow,而Profile仓库需要再创建一个脚本文件,并向README文件中添加标识符。

Profile仓库

首先是Profile仓库的设置,先将仓库Clone到本地,在本地编辑完成后再推送到GitHub。

创建Workflow

在本地目录中创建./source/.github/workflows/update-readme.yml文件,./.github/workflows/是GitHub Actions默认用于存放Workflow文件的路径,注意新建文件夹时不要漏掉.github文件夹名称前面的.。将下面的代码复制到这个文件中。

update-readme.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
name: Update Latest Blog Posts

on:
repository_dispatch:
types: [update-my-profile]

jobs:
update-readme:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14

- name: Install dependencies
run: npm install

- name: Install node-fetch
run: npm install node-fetch

- name: Install feedparser
run: npm install feedparser

- name: Update README
run: node update-readme.mjs

- name: Check if README has changed
id: readme-check
run: |
git fetch origin master
changed_files=$(git diff --name-only origin/master)
if echo "$changed_files" | grep -q 'README.md'; then
echo "readme_changed=true" >> $GITHUB_ENV
else
echo "readme_changed=false" >> $GITHUB_ENV
fi

- name: Commit and push changes
run: |
if [[ "${{ env.readme_changed }}" == "true" ]]; then
git config --local user.name 'github-actions[bot]'
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
git add README.md
git commit -m "Update README with latest blog posts"
git push
else
echo "README.md has not changed. Skipping commit and push."
fi

这个Workflow的作用如下:

  1. 在收到博客仓库发送的repository_dispatch事件后开始运行
  2. 安装与设置Node相关环境
    • 这里我使用的Node 14,原因是后续新版本的Node会导致GitHub Actions在运行过程中莫名其妙的终止,没找到具体原因。
  3. 安装所需的Node.js模块,这里我们用到了node-fetchfeedparser
    • node-fetch 是一个用于在 Node.js 环境中进行 HTTP 请求的库。它提供了一种简单的方式来发起 HTTP 请求并处理响应,类似于浏览器中的 Fetch API。
    • feedparser 是一个用于解析 RSS 和 Atom 格式的订阅源(feed),并将其转化为易于处理的 JavaScript 对象的 Node.js 模块。
  4. 运行Node脚本
  5. 检查README.md是否发生变动,添加这步是因为有时我会更新老博文,这样最近博文部分就不会有变化,直接推送的话Workflow会报错
    • 如果发生变动则提交并推送相关更改
    • 没有变动则跳过

注意事项:如果你的主分支名称是main的话,需要在Check if README has changed中修改下面两条,将master改为main

  • git fetch origin master
  • changed_files=$(git diff –name-only origin/master)

关于repository_dispatch

repository_dispatch 是 GitHub Actions 中的一种事件类型,它允许你手动触发仓库中定义的 Workflow。这个功能对于需要在特定情况下手动启动自动化任务的场景非常有用。

下面是关于 repository_dispatch 的详细解释:

  • 手动触发工作流程repository_dispatch 允许你手动触发在仓库中定义的 GitHub Actions Workflow。这意味着你可以在需要时手动启动 Workflow 的执行,而不依赖于特定事件(如 push、pull request 等)来触发。

  • 自定义事件类型:你可以为 repository_dispatch 指定自定义的事件类型。这个事件类型可以是你自己定义的,以描述触发的目的或操作。例如,你可以定义一个事件类型为 “update-my-profile”,表示触发 Workflow 来更新用户配置文件。

  • 携带有效负载数据:你还可以选择携带一些有效负载数据(payload),这些数据可以在触发 Workflow 时一同发送。有效负载数据是一个 JSON 对象,它可以包含任何需要传递给 Workflow 的信息,以帮助它执行特定的操作。

  • 访问 GitHub API 权限:触发 repository_dispatch 事件需要有适当的 GitHub API 权限。通常,你需要具有推送代码(push)的权限,或者是仓库管理员,才能触发 repository_dispatch 事件。

源自Chat GPT

设置Workflow读写权限

默认情况下,Workflow对所在仓库的文件只有读取权限,写入权限需要手动开启,步骤如下:

  1. 打开Profile仓库的Settings
  2. 点击左侧边栏中Actions选项下的General选项
  3. Workflow permissions修改为Read and write permissions
  4. 点击Save

1-4.webp

创建Node脚本

在Profile仓库的根目录中创建update-readme.mjs文件,注意文件格式是mjs而不是js

我起初使用的是js格式,但是在GitHub Actions运行时会因为无法导入模块而报错,原因未知。

关于js与mjs格式

.mjs 文件和 .js 文件都是 JavaScript 文件的扩展名,但它们有一些关键的区别:

  1. 模块类型:

    • .mjs 文件被视为 ECMAScript 模块(ES 模块),这意味着它们使用了 JavaScript 的原生模块系统,可以使用 importexport 语句来导入和导出模块。ES 模块是 JavaScript 的官方模块标准,通常用于现代 JavaScript 开发。
    • .js 文件可以是传统 CommonJS 模块(Node.js 模块),这些模块使用 require()module.exports 语法来导入和导出模块。CommonJS 是 Node.js 最早引入的模块系统,但也可以在浏览器中使用,通过工具如 Browserify 或 Webpack 来支持。
  2. 默认行为:

    • Node.js 中,默认情况下,.js 文件被视为 CommonJS 模块,除非在代码中明确指定了使用 ES 模块。这意味着你可以省略文件扩展名(如 .js),并且 Node.js 会自动查找 CommonJS 模块。
    • .mjs 文件则默认被视为 ES 模块,你需要在导入或运行时明确指定文件扩展名(如 import foo from './module.mjs')。
  3. 浏览器支持:

    • 浏览器普遍支持 ES 模块,因此,你可以在浏览器环境中使用 .mjs 文件作为模块。
    • 浏览器对 CommonJS 模块的支持有限,通常需要使用工具(如 Browserify)来将 CommonJS 模块转化为可在浏览器中运行的代码。
  4. Node.js 版本:

    • .mjs 文件需要 Node.js 13.2.0 版本或更高版本的运行环境,以支持 ES 模块的默认行为。在早期版本的 Node.js 中,你需要显式启用 ES 模块支持。
    • .js 文件则在早期版本的 Node.js 中被广泛支持,无需特殊配置。

总之,选择使用 .mjs.js 取决于你的项目需求和目标环境。如果你在 Node.js 中并且想要使用 ES 模块,可以选择使用 .mjs 文件。如果你需要在浏览器中使用模块,ES 模块是更常见的选择,但你也可以将 CommonJS 模块转化为 ES 模块。

源自Chat GPT

脚本代码如下:

update-readme.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import fetch from 'node-fetch';
import { readFile, writeFile } from 'fs/promises';
import FeedParser from 'feedparser';

async function updateReadme() {
const rssUrl = 'https://siriusq.top/en/atom.xml'; // 替换成你的博客的 RSS Feed 链接

try {
const response = await fetch(rssUrl);
const feedparser = new FeedParser();

response.body.pipe(feedparser);

const posts = [];

feedparser.on('readable', function () {
let item;
while ((item = this.read())) {
if (posts.length < 7) { // 获取前7篇博文
const title = item.title;
const link = item.link;
posts.push(`- [${title}](${link})`);
} else {
break;
}
}
});

feedparser.on('end', async () => {
if (posts.length > 0) {
// 读取 README 文件
const readme = await readFile('README.md', 'utf-8');

// 更新 README 文件中的标识符内容
const updatedReadme = readme.replace(
/<!-- Start_Position -->.*<!-- End_Position -->/s,
`<!-- Start_Position -->\n${posts.join('\n')}\n<!-- End_Position -->`
);

// 写回 README 文件
await writeFile('README.md', updatedReadme);
console.log('README 更新成功!');
} else {
console.error('未能找到博文。');
}
});
} catch (error) {
console.error('更新失败:', error);
}
}

updateReadme();

注意事项:

  • const rssUrl = 'https://siriusq.top/en/atom.xml'的值替换成你的博客的 RSS Feed 链接
  • if (posts.length < 7)用于设置展示的博文数量,默认是7篇

向README添加标识符

接下来需要我们在Profile仓库的README文件中添加两个标识符,当Workflow被触发后,两个标识符中间的内容就会自动更新为最新的博文标题与链接。

1
2
<!-- Start_Position -->
<!-- End_Position -->

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Other Contents......

### 📝 Latest Blog Posts
The following are my latest 7 blog posts:

<!-- Start_Position -->
- [Quick Setup Guide for LaTeX in VSCode](https://siriusq.top/en/latex-vscode-quick-config.html)
- [Accessing Windows Shared Folders on iPhone](https://siriusq.top/en/ios-windows-file-share.html)
- [Stellaris Mod Creation and Upload Log](https://siriusq.top/en/stellaris-mod.html)
- [Bongo Paw Clicker Development Summary](https://siriusq.top/en/bongo-paw-blicker.html)
- [Enabling Integrated Graphics with Discrete Graphics Installed](https://siriusq.top/en/turn-on-igpu.html)
- [MacBook Pro Headless](https://siriusq.top/en/macbook-pro-headless.html)
<!-- End_Position -->

Other Contents......

最后,将所做的更改推送到GitHub中

博客仓库

创建Workflow

博客仓库的Workflow同样需要在本地目录中创建,然后再推送到GitHub仓库,因为Hexo框架在部署时会完全替换掉GitHub远程仓库中的所有文件,包括自定义的GitHub Actions。

首先在博客source目录下创建.github/workflows/update-my-profile.yml文件,然后粘贴下面的代码:

update-my-profile.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: Trigger Profile Update

on:
page_build

jobs:
trigger-my-profile-update:
runs-on: ubuntu-latest

steps:
- name: Dispatch event to Profile Repo
env:
GITHUB_PROFILE_TOKEN: ${{ secrets.PAT_TOKEN_PROFILE }}
run: |
curl -X POST \
-H "Authorization: token $GITHUB_PROFILE_TOKEN" \
-H "Accept: application/vnd.github.everest-preview+json" \
https://api.github.com/repos/[USERNAME]/[USERNAME]/dispatches \
--data '{"event_type": "update-my-profile"}'

这个Workflow的作用是,当博客的Page成功构建后,通过我们先前设置的Repository Secret获取对仓库的完全访问权限,然后通过repository_dispatch事件触发Profile仓库中的另一个Workflow,即我们先前设置的update-my-profile

注意事项:

  • 将倒数第二行中的两个[USERNAME]替换为你自己的GitHub用户名,去掉两边的中括号
  • GITHUB_PROFILE_TOKEN变量的数值就是我们前面设置的SecretName

修改Hexo框架配置文件

如果你在此时尝试重新生成博客静态文件并部署到远程仓库,你会发现GitHub Actions中并没有出现刚刚设置的Workflow,这是因为前缀为.的文件和文件夹在 Unix 和 Linux 系统中被视为隐藏文件,hexo ghexo d两条命令会默认忽略这些隐藏文件,所以用于存放我们Workflow的文件夹.github被视为隐藏文件忽略掉了。接下来我们会通过修改配置文件让Hexo框架在生成和部署过程中捎带上我们的Workflow。

首先打开Hexo框架的配置文件_config.yml,通过搜索定位到include设置,添加Workflow文件的路径

Workflow文件路径
1
.github/workflows/update-my-profile.yml

示例如下:

_config.yml
1
2
3
4
5
# Include / Exclude file(s)
## include:/exclude: options only apply to the 'source/' folder
include: .github/workflows/update-my-profile.yml
exclude:
ignore:

然后通过搜索定位到deploy设置,在最下面添加一行ignore_hidden: false,示例如下:

_config.yml
1
2
3
4
5
6
7
# Deployment
## Docs: https://hexo.io/docs/one-command-deployment
deploy:
type: git
repo: git@github.com:Siriusq/en.git
branch: master
ignore_hidden: false

此外,Hexo框架在生成静态文件时会将yml格式的文件转换为GitHub Actions不支持的json格式,这也会导致此前设置的Workflow失效。接下来需要我们将workflow文件路径添加到skip_render中,这样Hexo框架在生成文件时就会将它原封不动的复制到public文件夹中。示例如下:

_config.yml
1
2
3
4
5
6
7
8
9
# Directory
source_dir: source
public_dir: public
tag_dir: tags
archive_dir: archives
category_dir: categories
code_dir: downloads/code
i18n_dir: :lang
skip_render: .github/workflows/update-my-profile.yml

保存更改,然后运行hexo clhexo g -d命令,不出意外的话几分钟后就能看到自动更新后的Profile了。

21.webp
22.webp

PS:我本来想让Chat GPT给我出一套包含代码的完整的解决方案,就是我只需要复制粘贴就能迅速完成任务的那种。但是吧,Chat GPT让我深刻体会到了代码生成5分钟,Debug一整天的无奈,偷懒大失败。

下图就是我写这篇博文时的心理状态

how-openai-chatgpt-helps-software-development.webp