神觉晓

@shenjuexiao

神觉晓

Resolving "npm package not found" Errors

This error occurs when npm cannot locate a required package in your project, often due to missing installations, incorrect paths, or misconfigured project settings.

Example

1
2
$ npm run build
Error: NPM packages not found. Please confirm npm packages which need to build are in the correct directory

A common cause is that dependencies listed in package.json have not been installed. Running npm install in the project root usually resolves this by downloading all required packages into the node_modules directory.

If you are working with environments like WeChat Mini Programs, the issue can also arise if npm packages are outside the miniprogramRoot directory. In such cases, you may need to adjust project.config.json:

1
2
3
4
5
6
7
\"packNpmManually\": true,
\"packNpmRelationList\": [
{
\"packageJsonPath\": \"./package.json\",
\"miniprogramNpmDistDir\": \"./miniprogram/\"
}
]

After updating, restart the development tool and rebuild npm.

Sometimes, the error appears because npm itself is not installed or not accessible. Verify installation with:

1
npm -v

If it fails, reinstall Node.js (which includes npm) and ensure its binary path is added to your system’s PATH environment variable.

For corrupted or incomplete installs, delete node_modules and package-lock.json, then reinstall:

1
2
rm -rf node_modules package-lock.json
npm install

In summary, ensure dependencies are installed, paths are correctly configured, and npm is properly set up in your environment. Restarting your IDE or build tool after changes often helps apply configurations.

了解详细信息

actions-gh-pages

内容来源

actions-gh-pages 这个项目非常不错,利用 Github Actions 部署静态页面。

This is a GitHub Action to deploy your static files to GitHub Pages. This deploy action can be combined simply and freely with Static Site Generators. (Hugo, MkDocs, Gatsby, mdBook, Next, Nuxt, and so on.)

这是一个用于将静态文件部署到 GitHub PagesGitHub Action。此部署操作可与静态网站生成器简单灵活地结合使用(Hugo、MkDocs、Gatsby、mdBook、Next、Nuxt等)。

1
2
3
4
5
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public

值得注意的是,github_token 可以选择三种的类型,即该项目支持的三种。

Three tokens are supported.

Token Private repo Public repo Protocol Setup
github_token HTTPS Unnecessary
deploy_key SSH Necessary
personal_token HTTPS Necessary

Notes: Actually, the GITHUB_TOKEN works for deploying to GitHub Pages but it has still some limitations. For the first deployment, we need to select the gh-pages branch or another branch on the repository settings tab. See First Deployment with GITHUB_TOKEN.

实际上,GITHUB_TOKEN 可用于部署到 GitHub Pages,但仍存在一些限制。对于首次部署,我们需要在仓库设置的选项卡中选择 gh-pages 分支或其他分支。请参阅“使用 GITHUB_TOKEN 进行首次部署”。

区别

GITHUB_TOKEN

GitHub Actions 工作流的"内置身份",由系统自动创建和管理,权限仅限于当前仓库。

DEPLOY_KEY

一种 SSH 密钥,权限被锁定在单个仓库,常用于从服务器拉取或推送代码到此仓库。

PERSONAL_TOKEN

关联到你个人账户的令牌,拥有你在 GitHub 上的全部或部分权限,是最强大但也最需谨慎管理的一种。

对比表格

特性 GITHUB_TOKEN DEPLOY_KEY PERSONAL_TOKEN
关联对象 当前工作流运行 单个仓库 你的个人账户
权限范围 仅限于当前仓库 仅限于绑定的单个仓库(可读或读写) 可访问你账户有权访问的所有仓库及资源
身份验证方式 HTTPS (Token) SSH (公钥/私钥) HTTPS (Token)
有效期 每次工作流作业开始时创建,作业完成后失效 长期有效,直到手动删除 长期有效,直到手动撤销或过期
典型使用场景 GitHub Actions 中拉取当前仓库代码、进行常规操作 在 Actions 中拉取或推送代码到特定仓库(尤其是私有仓库) 需要在 Actions 中访问其他仓库、调用 GitHub API

如何选择

  • 优先使用 GITHUB_TOKEN:如果工作流只需要访问当前仓库的代码,这是最安全、最便捷的选择,无需任何手动配置。

  • 需要访问特定私有仓库时,使用 DEPLOY_KEY:当工作流需要从另一个特定的私有仓库拉取代码(例如一个公共工具库),但又不想把个人账户的所有权限都暴露出去时,DEPLOY_KEY 是绝佳选择。它的权限被"锁死"在目标仓库上。

  • 万不得已时,使用 PERSONAL_TOKEN:当 GITHUB_TOKEN 权限不足(比如需要触发新的工作流),或者操作涉及到当前仓库之外的资源时,才考虑使用个人访问令牌。务必注意:由于它关联的是你的个人账户,权限非常宽泛,潜在风险也最大,因此要谨慎使用,并遵循最小权限原则(只授予必要的权限)

如何设置

  • GITHUB_TOKEN 并不需要你手动去“设置”或“创建”。它是在 GitHub Actions 工作流运行时,由 GitHub 系统自动生成的一个临时凭证。你只需要在工作流的 YAML 文件中通过特定的语法来使用它。

  • DEPLOY_KEY 的设置需要分两步走:生成密钥对,然后分别把公钥和私钥添加到正确的地方。它本质上是一个 SSH 密钥对,专门用于授权访问单个 GitHub 仓库。

  • 设置 PERSONAL_TOKEN(个人访问令牌,简称 PAT)主要分几步:进入创建页面、配置权限、生成并安全保存。你可以把它当作 GitHub 密码的替代品,用于命令行或 API 操作。

Astro Template

Astro 模板

Astro Paper

satnaing/astro-paper
高评分、暗黑模式、RSS/SEO 完善
Demo: astro-paper.pages.dev
Github: satnaing/astro-paper

Astrofy

manuelernestog/astrofy
含简历/商店/博客多合一,CV+ 项目画廊
Demo: astrofy-template.netlify.app
Github: manuelernestog/astrofy

EV0

gndx/ev0-astro-theme
极简响应式
Demo: ev0.gndx.dev
Github: withastro/starlight

Firefly

CuteLeaf/Firefly
专为技术文档设计,支持多语言、搜索、版本切换
Demo: firefly.cuteleaf.cn
Github: CuteLeaf/Firefly
Docs: docs-firefly.cuteleaf.cn

Pure

cworld1/astro-theme-pure
极简 + 全文搜索
Demo: astro-pure.js.org
Github: cworld1/astro-theme-pure

这位作者的 Github 主页,可以追溯到 1990 年,不知道怎么弄出来的。

Sorolla

jramma/sorollaportfolio
画册式排版
Demo: sorolla.casset.cat
Github: jramma/sorollaportfolio

Starlight‌ 官方

withastro/starlight
专为技术文档设计,支持多语言、搜索、版本切换
Demo: starlight.astro.build
Github: withastro/starlight

The requested URL returned error: 403

GitHub Actions push 403 权限拒绝解决

内容来源

Giscafer GitHub Actions push 403

fatal: unable to access

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Run yarn sync-post
yarn sync-post
git add .
git commit -m 'chore(ci): blog sync'
git push
shell: /usr/bin/bash -e {0}
env:
GH_TOKEN: ***
GH_USER: ***
GH_PROJECT_NAME: ***
yarn run v1.22.22
$ node ./scripts/sync-post.js
文章全部同步成功! 0
Done in 0.33s.
No staged files found.
[main 7226b3a] chore(ci): blog sync
3 files changed, 33 deletions(-)
delete mode 100644 data/blog/post-1.mdx
delete mode 100644 data/blog/post-2.mdx
delete mode 100644 data/blog/post-3.mdx
remote: Permission to ***/***.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/***/***/': The requested URL returned error: 403
Error: Process completed with exit code 128.

文件:giscafer/.github/workflows/sync-post.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
name: Sync Post

# Controls when the workflow will run
on:
# schedule:
# - cron: \"30 1 * * *\"
# https://docs.github.com/cn/developers/webhooks-and-events/events/issue-event-types
issues:
types:
- opened
- closed
- renamed
- labeled
- unlabeled
- reopened
- committed # 修改?
workflow_dispatch:
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_USER: ${{ secrets.GH_USER }}
GH_PROJECT_NAME: ${{ secrets.GH_PROJECT_NAME }}
jobs:
Publish:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2

- name: Git config 🔧
run: |
git config --global user.name \"shenjuexiao\"
git config --global user.email \"shenjuexiao@qq.com\"

- name: Display runtime info
run: |
echo '当前目录:'
pwd

- name: Install 🔧
run: yarn install

# - name: Build ⛏️
# run: yarn build

- name: Update blog files ⛏️
run: |
yarn sync-post
git add .
git commit -m 'chore(ci): blog sync'
git push

github-actions[bot] 没有仓库写入推送权限,触发 403 forbidden。

三种常见场景

  1. 仅配置 GH_TOKEN,但仓库默认工作流令牌无写权限(默认只读)
  2. 个人 Token 权限范围不足、过期,或未正确注入
  3. Git remote 使用 https 但未自动替换授权头

方案一:开启仓库工作流写入权限(最简单,推荐)

步骤
仓库首页 :Settings -> Actions -> General
下滑找到 Workflow permissions
勾选: Read and write permissions
(默认是 Read repository contents and packages permissions
保存设置,重新运行 Actions。
原理:内置 GITHUB_TOKEN 会获得推送、修改文件权限,无需额外个人 Token。

Giscafer Settings Actions General Workflow-permissions

方案二:改用个人 PAT Token(适合需要跨仓库 / 更多权限)

  1. 创建 GitHub 个人访问令牌 PAT
    GitHub 头像,Settings -> Developer settings -> Personal access tokens -> Tokens (classic)
    新建 Token,勾选权限:repo(全部仓库读写权限)。
    复制生成的 Token(仅展示一次,丢失需重建)。
  2. 仓库配置 Secret
    仓库首页, Settings -> Secrets and variables -> Actions -> New repository secret
    Name:GH_TOKEN。
    Value:粘贴上面的 PAT。
  3. 修改 workflow 注入授权(关键,你现有脚本缺授权)
    你的脚本直接 git push,但 https 地址没带上 token,需要在 shell 里替换 remote url:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
steps:
- uses: actions/checkout@v4
# 必须加上 fetch-depth=0,否则无法正常commit push
with:
fetch-depth: 0

- name: Sync blog post
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }} # 如果用PAT填GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_USER: ${{ secrets.GH_USER }}
GH_PROJECT_NAME: ${{ secrets.GH_PROJECT_NAME }}
run: |
yarn sync-post
# 核心:替换remote地址,带上token鉴权,解决403
git remote set-url origin https://${GH_USER}:${GH_TOKEN}@github.com/${GH_USER}/${GH_PROJECT_NAME}.git
git add .
git commit -m 'chore(ci): blog sync'
git push
shell: /usr/bin/bash -e {0}

关键遗漏点(原配置2个问题)

  1. checkout 未加 fetch-depth:0
    默认只拉取最新一次提交,缺少完整 git 历史,commit/push 极易异常。
  2. 未配置带 Token 的 remote origin
    直接 git push 使用无授权的 https 地址,bot 被拦截 403。

补充排查点

  1. 如果你用内置 ${{ secrets.GITHUB_TOKEN }}
    必须完成【方案一:开启读写权限】,否则就算改 remote 也推不上去。
  2. PAT 失效处理
    重新生成 Token,更新仓库 Secret 内的 GH_TOKEN
  3. 分支保护规则
    若 main 分支开启 Require pull request reviews before merging,直接 push 会失败,两种解决:
  • 临时放开分支保护允许 bot 直接推送
  • 脚本改成自动创建 PR,而非直接 push

完整可直接复制的 job 模板

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
jobs:
sync-blog:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node & Yarn
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'

- name: Install dependencies
run: yarn install

- name: Sync post & push
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_USER: ${{ secrets.GH_USER }}
GH_PROJECT_NAME: ${{ secrets.GH_PROJECT_NAME }}
run: |
yarn sync-post
git remote set-url origin https://${GH_USER}:${GH_TOKEN}@github.com/${GH_USER}/${GH_PROJECT_NAME}.git
git config --global user.name \"github-actions[bot]\"
git config --global user.email \"github-actions[bot]@users.noreply.github.com\"
git add .
git commit -m 'chore(ci): blog sync' || echo \"无文件变更,跳过提交\"
git push

新增 git config 配置提交者用户名邮箱,避免无身份提交报错;
|| echo 兼容「无修改文件」场景,不会因为没有变更直接终止任务。

Gitblog main.py

内容来源

main.py

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# main.py
# -*- coding: utf-8 -*-
import argparse
import os
import re

import markdown
from feedgen.feed import FeedGenerator
from github import Github
from lxml.etree import CDATA
from marko.ext.gfm import gfm as marko

MD_HEAD = \"\"\"## [Gitblog](https://yihong0618.github.io/gitblog/)
My personal blog([About Me](https://github.com/yihong0618/gitblog/issues/282)) using issues and GitHub Actions (随意转载,无需署名)
[Things I like](https://github.com/yihong0618/gitblog/issues/311)
![image](https://github.com/user-attachments/assets/a168bf11-661e-4566-b042-7fc9544de528)
[RSS Feed](https://raw.githubusercontent.com/{repo_name}/master/feed.xml)
\"\"\"

BACKUP_DIR = \"BACKUP\"
ANCHOR_NUMBER = 5
TOP_ISSUES_LABELS = [\"Top\"]
TODO_ISSUES_LABELS = [\"TODO\"]
FRIENDS_LABELS = [\"Friends\"]
ABOUT_LABELS = [\"About\"]
THINGS_LABELS = [\"Things\"]
IGNORE_LABELS = (
FRIENDS_LABELS
+ TOP_ISSUES_LABELS
+ TODO_ISSUES_LABELS
+ ABOUT_LABELS
+ THINGS_LABELS
)

FRIENDS_TABLE_HEAD = \"| Name | Link | Desc | \\n | ---- | ---- | ---- |\\n\"
FRIENDS_TABLE_TEMPLATE = \"| {name} | {link} | {desc} |\\n\"
FRIENDS_INFO_DICT = {
\"名字\": \"\",
\"链接\": \"\",
\"描述\": \"\",
}


def get_me(user):
return user.get_user().login


def is_me(issue, me):
return issue.user.login == me


def is_hearted_by_me(comment, me):
reactions = list(comment.get_reactions())
for r in reactions:
if r.content == \"heart\" and r.user.login == me:
return True
return False


def _make_friend_table_string(s):
info_dict = FRIENDS_INFO_DICT.copy()
try:
string_list = s.splitlines()
# drop empty line
string_list = [l for l in string_list if l and not l.isspace()]
for l in string_list:
string_info_list = re.split(\":\", l)
if len(string_info_list) < 2:
continue
info_dict[string_info_list[0]] = string_info_list[1]
return FRIENDS_TABLE_TEMPLATE.format(
name=info_dict[\"名字\"], link=info_dict[\"链接\"], desc=info_dict[\"描述\"]
)
except Exception as e:
print(str(e))
return


# help to covert xml vaild string
def _valid_xml_char_ordinal(c):
codepoint = ord(c)
# conditions ordered by presumed frequency
return (
0x20 <= codepoint <= 0xD7FF
or codepoint in (0x9, 0xA, 0xD)
or 0xE000 <= codepoint <= 0xFFFD
or 0x10000 <= codepoint <= 0x10FFFF
)


def format_time(time):
return str(time)[:10]


def login(token):
return Github(token)


def get_repo(user: Github, repo: str):
return user.get_repo(repo)


def parse_TODO(issue):
body = issue.body.splitlines()
todo_undone = [l for l in body if l.startswith(\"- [ ] \")]
todo_done = [l for l in body if l.startswith(\"- [x] \")]
# just add info all done
if not todo_undone:
return f\"[{issue.title}]({issue.html_url}) all done\", []
return (
f\"[{issue.title}]({issue.html_url})--{len(todo_undone)} jobs to do--{len(todo_done)} jobs done\",
todo_done + todo_undone,
)


def get_top_issues(repo):
return repo.get_issues(labels=TOP_ISSUES_LABELS)


def get_todo_issues(repo):
return repo.get_issues(labels=TODO_ISSUES_LABELS)


def get_repo_labels(repo):
return [l for l in repo.get_labels()]


def get_issues_from_label(repo, label):
return repo.get_issues(labels=(label,))


def add_issue_info(issue, md):
time = format_time(issue.created_at)
md.write(f\"- [{issue.title}]({issue.html_url})--{time}\\n\")


def add_md_todo(repo, md, me):
todo_issues = list(get_todo_issues(repo))
if not TODO_ISSUES_LABELS or not todo_issues:
return
with open(md, \"a+\", encoding=\"utf-8\") as md:
md.write(\"## TODO\\n\")
for issue in todo_issues:
if is_me(issue, me):
todo_title, todo_list = parse_TODO(issue)
md.write(\"TODO list from \" + todo_title + \"\\n\")
for t in todo_list:
md.write(t + \"\\n\")
# new line
md.write(\"\\n\")


def add_md_top(repo, md, me):
top_issues = list(get_top_issues(repo))
if not TOP_ISSUES_LABELS or not top_issues:
return
with open(md, \"a+\", encoding=\"utf-8\") as md:
md.write(\"## 置顶文章\\n\")
for issue in top_issues:
if is_me(issue, me):
add_issue_info(issue, md)


def add_md_firends(repo, md, me):

s = FRIENDS_TABLE_HEAD
friends_issues = list(repo.get_issues(labels=FRIENDS_LABELS))
if not FRIENDS_LABELS or not friends_issues:
return
friends_issue_number = friends_issues[0].number
for issue in friends_issues:
for comment in issue.get_comments():
if is_hearted_by_me(comment, me):
try:
s += _make_friend_table_string(comment.body or \"\")
except Exception as e:
print(str(e))
pass
s = markdown.markdown(s, output_format=\"html\", extensions=[\"extra\"])
with open(md, \"a+\", encoding=\"utf-8\") as md:
md.write(
f\"## [友情链接](https://github.com/{str(me)}/gitblog/issues/{friends_issue_number})\\n\"
)
md.write(\"<details><summary>显示</summary>\\n\")
md.write(s)
md.write(\"</details>\\n\")
md.write(\"\\n\\n\")


def add_md_recent(repo, md, me, limit=5):
count = 0
with open(md, \"a+\", encoding=\"utf-8\") as md:
# one the issue that only one issue and delete (pyGitHub raise an exception)
try:
md.write(\"## 最近更新\\n\")
for issue in repo.get_issues(sort=\"created\", direction=\"desc\"):
if is_me(issue, me):
add_issue_info(issue, md)
count += 1
if count >= limit:
break
except Exception as e:
print(str(e))


def add_md_header(md, repo_name):
with open(md, \"w\", encoding=\"utf-8\") as md:
md.write(MD_HEAD.format(repo_name=repo_name))
md.write(\"\\n\")


def add_md_label(repo, md, me):
labels = get_repo_labels(repo)

# sort lables by description info if it exists, otherwise sort by name,
# for example, we can let the description start with a number (1#Java, 2#Docker, 3#K8s, etc.)
labels = sorted(
labels,
key=lambda x: (
x.description is None,
x.description == \"\",
x.description,
x.name,
),
)

with open(md, \"a+\", encoding=\"utf-8\") as md:
for label in labels:
# we don't need add top label again
if label.name in IGNORE_LABELS:
continue

issues = get_issues_from_label(repo, label)
issues = list(sorted(issues, key=lambda x: x.created_at, reverse=True))
if len(issues) != 0:
md.write(\"## \" + label.name + \"\\n\\n\")
i = 0
for issue in issues:
if not issue:
continue
if is_me(issue, me):
if i == ANCHOR_NUMBER:
md.write(\"<details><summary>显示更多</summary>\\n\")
md.write(\"\\n\")
add_issue_info(issue, md)
i += 1
if i > ANCHOR_NUMBER:
md.write(\"</details>\\n\")
md.write(\"\\n\")


def get_to_generate_issues(repo, dir_name, issue_number=None):
md_files = os.listdir(dir_name)
generated_issues_numbers = [
int(i.split(\"_\")[0]) for i in md_files if i.split(\"_\")[0].isdigit()
]
to_generate_issues = [
i
for i in list(repo.get_issues())
if int(i.number) not in generated_issues_numbers
]
if issue_number:
to_generate_issues.append(repo.get_issue(int(issue_number)))
return to_generate_issues


def generate_rss_feed(repo, filename, me):
generator = FeedGenerator()
generator.id(repo.html_url)
generator.title(f\"RSS feed of {repo.owner.login}'s {repo.name}\")
generator.author(
{\"name\": os.getenv(\"GITHUB_NAME\"), \"email\": os.getenv(\"GITHUB_EMAIL\")}
)
generator.link(href=repo.html_url)
generator.link(
href=f\"https://raw.githubusercontent.com/{repo.full_name}/master/{filename}\",
rel=\"self\",
)
for issue in repo.get_issues():
if not issue.body or not is_me(issue, me) or issue.pull_request:
continue
item = generator.add_entry(order=\"append\")
item.id(issue.html_url)
item.link(href=issue.html_url)
item.title(issue.title)
item.published(issue.created_at.strftime(\"%Y-%m-%dT%H:%M:%SZ\"))
for label in issue.labels:
item.category({\"term\": label.name})
body = \"\".join(c for c in issue.body if _valid_xml_char_ordinal(c))
item.content(CDATA(marko.convert(body)), type=\"html\")
generator.atom_file(filename)


def main(token, repo_name, issue_number=None, dir_name=BACKUP_DIR):
user = login(token)
me = get_me(user)
repo = get_repo(user, repo_name)
# add to readme one by one, change order here
add_md_header(\"README.md\", repo_name)
for func in [add_md_firends, add_md_top, add_md_recent, add_md_label, add_md_todo]:
func(repo, \"README.md\", me)

generate_rss_feed(repo, \"feed.xml\", me)
to_generate_issues = get_to_generate_issues(repo, dir_name, issue_number)

# save md files to backup folder
for issue in to_generate_issues:
save_issue(issue, me, dir_name)


def save_issue(issue, me, dir_name=BACKUP_DIR):
md_name = os.path.join(
dir_name, f\"{issue.number}_{issue.title.replace('/', '-').replace(' ', '.')}.md\"
)
with open(md_name, \"w\") as f:
f.write(f\"# [{issue.title}]({issue.html_url})\\n\\n\")
f.write(issue.body or \"\")
if issue.comments:
for c in issue.get_comments():
if is_me(c, me):
f.write(\"\\n\\n---\\n\\n\")
f.write(c.body or \"\")


if __name__ == \"__main__\":
if not os.path.exists(BACKUP_DIR):
os.mkdir(BACKUP_DIR)
parser = argparse.ArgumentParser()
parser.add_argument(\"github_token\", help=\"github_token\")
parser.add_argument(\"repo_name\", help=\"repo_name\")
parser.add_argument(
\"--issue_number\", help=\"issue_number\", default=None, required=False
)
options = parser.parse_args()
main(options.github_token, options.repo_name, options.issue_number)

这是一个使用GitHub Issues作为博客系统的Python脚本,名为Gitblog。它通过GitHub API将issues转换为静态博客内容。我来详细解析它的功能:

主要功能

1. README.md生成

脚本会生成一个README.md文件,包含以下部分:

  • 置顶文章:带有"Top"标签的issues
  • 最近更新:最新的5篇文章
  • 按标签分类:其他所有issues按标签分组显示
  • 友情链接:通过"Friends"标签的issue评论区管理
  • TODO列表:带有"TODO"标签的issue中的任务列表

2. RSS Feed生成

自动生成feed.xml文件,支持订阅博客更新。

3. 文章备份

将所有文章备份到BACKUP目录,以{issue编号}_{标题}.md格式保存。

代码结构

核心函数:

函数 作用
login() 使用GitHub token登录
get_repo() 获取指定的仓库
is_me() 判断issue是否由自己创建
is_hearted_by_me() 判断评论是否被自己点赞(❤️)
parse_TODO() 解析TODO列表,统计完成/未完成项
generate_rss_feed() 生成RSS订阅文件
save_issue() 备份单个issue到本地

特殊标签处理:

  • Top:置顶文章
  • TODO:任务列表
  • Friends:友情链接
  • About/Things:特殊页面(被忽略不显示在普通标签中)

使用方式

1
python main.py <github_token> <repo_name> [--issue_number <number>]

参数说明:

  • github_token:GitHub个人访问令牌
  • repo_name:仓库名称(格式:username/repo
  • --issue_number:可选,指定备份特定文章

技术特点

  1. 依赖库

    • feedgen:生成RSS feed
    • github:GitHub API客户端(PyGithub)
    • markdown/marko:Markdown解析
    • lxml:XML处理
  2. 自动化:可通过GitHub Actions定时运行,自动更新博客

  3. 数据持久化:通过BACKUP目录缓存,避免重复处理

  4. 评论支持:可以保留issue的评论内容

这个项目特别适合开发者用GitHub Issues管理个人博客,无需额外服务器,完全利用GitHub平台。

Gmeek Deploy

Gmeek 部署

内容来源

Gmeek 博客框架

  • 超轻量级个人博客模板
  • 完全基于 Github PagesGithub IssuesGithub Actions
  • 不需要本地部署

一、资源

Gmeek

Markdown 语法

在线编辑工具

二、安装

创建仓库

启用 Pages

  • 仓库页面导航:Settings -> Pages -> Build and deployment
  • Source 下拉选择 Github Actions

<img src="https://cdn.jsdelivr.net/gh/shenjuexiao/shenjuexiao@main/Github_png/Gmeek_Settings_Github-Pages_Github-Actions.png\“ />

开始写作

  • 仓库页面导航:Issues -> New issue
  • Add a title(写标题),Add a description(写描述)。至少添加一个 Labels(标签),Create(创建)
  • 片刻后可通过<用户名>.github.io 访问
  • 仓库页面导航:Actions -> build Gmeek,可查看构建进度

<img src="https://cdn.jsdelivr.net/gh/shenjuexiao/shenjuexiao@main/Github_png/Gmeek_Issues_New-issue_Labels.png\“ />

手动全局生成(修改 config.json 或按需要)

  • 仓库页面导航:Actions -> build Gmeek -> Run workflow

<img src="https://cdn.jsdelivr.net/gh/shenjuexiao/shenjuexiao@main/Github_png/Gmeek_Actions_build-Gmeek_Run-workflow.png\“ />

网友制作的视频教程:
https://www.bilibili.com/video/BV1GM4m1m7ZD/

三、配置

config.json 必要字段

1
2
3
4
5
6
{
\"title\":\"Meekdai\",
\"subTitle\":\"童话是一种生活态度,仅此而已。\",
\"avatarUrl\":\"https://github.githubassets.com/favicons/favicon.svg\",
\"GMEEK_VERSION\":\"last\"
}
配置参数 必填 说明
title 博客标题
subTitle 博客描述&自述
avatarUrl 博客头像
GMEEK_VERSION Gmeek 版本,一般写 last 也可以用具体 tag 版本

config.json 其他字段

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
{
\"displayTitle\":\"Meekdai\",
\"homeUrl\":\"http://blog.meekdai.com\",
\"faviconUrl\":\"https://github.githubassets.com/favicons/favicon.svg\",
\"email\":\"meekdai@163.com\",
\"startSite\":\"02/16/2015\",
\"filingNum\":\"浙ICP备20023628号\",
\"onePageListNum\":15,
\"singlePage\":[\"about\"],
\"iconList\":{\"music\":\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Z\"},
\"exlink\":{\"music\":\"https://music.meekdai.com\"},
\"commentLabelColor\":\"#006b75\",
\"yearColorList\":[\"#bc4c00\", \"#0969da\", \"#1f883d\", \"#A333D0\"],
\"i18n\":\"CN\",
\"UTC\":8,
\"themeMode\":\"manual\",
\"dayTheme\":\"light\",
\"nightTheme\":\"dark_colorblind\",
\"urlMode\":\"pinyin\",
\"style\":\"\",
\"script\":\"\",
\"head\":\"\",
\"allHead\":\"\",
\"indexStyle\":\"\",
\"indexScript\":\"\",
\"showPostSource\":1,
\"rssSplit\":\"sentence\",
\"bottomText\":\"转载请注明出处\",
\"ogImage\":\"https://cdn.jsdelivr.net/gh/Meekdai/meekdai.github.io/logo64.jpg\",
\"primerCSS\":\"<link href='https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/Primer/21.0.7/primer.css' rel='stylesheet' />\",
\"needComment\":0
}

最后一行配置末尾不需要逗号,其他行末尾都需要逗号(英文逗号)

配置参数 必填 说明
displayTitle 用于头像后面的标题展示,如果和 title 一致则不用添加
homeUrl 博客的主页地址,自定义域名时需要配置
faviconUrl 页面的 favicon 地址,如果和 avatarUrl 一致则不用添加
email 用于自动提交仓库时用,不添加也可以
startSite 用于页面底部显示网站运行天数
filingNum 用于页面底部显示备案信息
onePageListNum 用于首页每页展示的文章数量
singlePage 自定义独立页面,例如 about 或者 link 等
iconList 用于定义 singlePage 按钮展示的 SVG 图标 (16px),about 和 link 内置无需定义
exlink 用于自定义首页右上角圆形按钮到外部链接功能,按钮图标定义在 iconList 中
commentLabelColor 用于自定义显示评论数量标签的颜色
yearColorList 用于自定义显示不同年份标签的颜色
i18n 用于定义博客语言,目前支持 EN/CN/RU
UTC 用于定义时区
themeMode 用于定义主题模式,默认为 manual,也可选择 fix 详细说明
dayTheme 用于定义亮主题
nightTheme 用于定义暗主题
urlMode 用于定义文章链接生成模式,目前支持 pinyin/issue/ru_translit
style 用于自定义文章页 CSS
script 用于自定义文章页 JavaScript
head 用于自定义文章页 head 内容
allHead 用于自定义所有页面 head 内容
indexStyle 用于自定义首页 CSS
indexScript 用于自定义首页 JavaScript
showPostSource 设置为 1 则在文章页显示 issue 链接按钮,设置为 0 则不显示
rssSplit 设置 RSS 输出的截断符号,默认 sentence 为第一句话,可配置其他特殊符号
bottomText 用于设置文章页面右下角显示的内容
ogImage 用于设置 Open Graph 协议展示的图片
primerCSS 用于设置 primer.css 的 CDN 地址,默认使用南科大
needComment 用于设置是否需要评论功能,1 开启评论,0 关闭

修改发布时间

1
<!-- ##{\"timestamp\":1490764800}## -->

自定义单篇文章页面的 style 和 script

1
2
<!-- ##{\"style\":\"<style>#postBody{font-size:20px}</style>\"}## -->
<!-- ##{\"script\":\"<script async src='//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js'></script>\"}## -->

多种自定义参数

1
<!-- ##{\"script\":\"<script async src='//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js'></script>\",\"style\":\"<style>#postBody{font-size:20px}</style>\",\"timestamp\":1490764800}## -->

自定义全局文章页面的 style 和 script,config.json 文件添加

1
2
\"style\":\"<style>#postBody{font-size:20px}</style>\",
\"script\":\"<script async src='//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js'></script>\",

置顶文章

Pin issue,手动全局生成一次即可。

删除文章

Close issue 或者 Delete issue,手动全局生成一次即可。

四、进阶

Github Issues Blog List

Github Issues 博客项目(不完全统计)

内容来源

@rena.to/github-blog

AllanChain/blog

babilema

ginb

gitblog

gitblog

Gmeek

Issue-Blog-With-Github-Action

qiwihui/blog

Sep0lkit/git-issues-blog

ttop5/issue-blog

whyour/issue-blog

xizhibei/blog

Github Issues Blog Gitblog

用过 Jekyll、Hexo,捣鼓了一阵子,都没能长期使用持续写作,现在转头使用 GIthub Issue 搭建的博客系统。后者有两个优点,一来省去了生成、部署的操作;二来解决了图床的需求。只要仓库存在,Issues 可以一直保留,改天也可以换个相似的博客系统。

内容来源

项目地址和安装指南

yihong0618/gitblog
这个博客开源了

安装方法

1. 克隆仓库

1
git clone git@github.com:yihong0618/gitblog.git

2. 生成tokens

个人首页,Settings -> Developer Settings -> Personal access tokens -> Tokens (classic) -> Generate New Tokens -> Generate new yokens (classic)

或直达链接:
settings/tokens

<img src="https://cdn.jsdelivr.net/gh/shenjuexiao/shenjuexiao@main/Github_png/Gitblog_Settings_Developer-Settings_Personal-access-tokens.png\“ />

3. 仓库 actions

仓库首页,Settings -> Security and quality -> Secrets and variables -> Actions -> New repository secret

<img src="https://cdn.jsdelivr.net/gh/shenjuexiao/shenjuexiao@main/Gitblog_Settings_Security-and-quality_Secrets-and-variables.png\“ />

直接使用 G_T,就不用修改文件: .github\\workflows\\generate_readme.yml 中的 ${{ secrets.G_T }} 字段。

1
2
3
- name: Generate new md
run: |
python main.py ${{ secrets.G_T }} ${{ github.repository }} --issue_number '${{ github.event.issue.number }}'

4. 修改generate_readme.yml

.github/workflows/generate_readme.yml
新版不需要。

其他问题

原作者后续更新了 *.yml 文件,有些参数不需要修改了。
后续更新补充。

Hello World 2026

Quick Start

Create a new post

1
$ hexo new \"My New Post\"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

Hello World 2015

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in trobuleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new \"My New Post\"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

0%