更多issue模板
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
name: 申请删除照片墙照片
|
||||
description: 删除照片墙某个专题里的照片
|
||||
title: "photo-delete: "
|
||||
labels:
|
||||
- photo-delete
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
提交后,将进行人工审核,请勿恶意提交,否则禁用账号。
|
||||
如果该专题里已经有相同 `title` 的照片,会删除它。
|
||||
- type: dropdown
|
||||
id: topic
|
||||
attributes:
|
||||
label: topic
|
||||
description: 要删除的照片所属专题。
|
||||
options:
|
||||
- classroom
|
||||
- events
|
||||
- graduation-day
|
||||
- candid
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: title
|
||||
attributes:
|
||||
label: title
|
||||
description: 照片标题;同一专题内标题相同会被视为删除已有照片。
|
||||
placeholder: 班级节目
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,53 @@
|
||||
name: 更新照片墙照片
|
||||
description: 新增或修改照片墙某个专题里的照片
|
||||
title: "photo: "
|
||||
labels:
|
||||
- photo-update
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
提交后,自动化会根据 `topic` 找到对应的 `src/data/photo-topics/<topic>.ts`。
|
||||
如果该专题里已经有相同 `title` 的照片,会更新它;否则会追加一张新照片。
|
||||
- type: dropdown
|
||||
id: topic
|
||||
attributes:
|
||||
label: topic
|
||||
description: 要写入的照片专题。
|
||||
options:
|
||||
- classroom
|
||||
- events
|
||||
- graduation-day
|
||||
- candid
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: title
|
||||
attributes:
|
||||
label: title
|
||||
description: 照片标题;同一专题内标题相同会被视为修改已有照片。
|
||||
placeholder: 班级节目
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: caption
|
||||
attributes:
|
||||
label: caption
|
||||
description: 照片说明。
|
||||
placeholder: 那天大家一起站在灯光下面。
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: image
|
||||
attributes:
|
||||
label: image
|
||||
description: 图片地址,可以是外链,也可以是站内路径,例如 /photos/events/show.jpg。
|
||||
placeholder: https://example.com/photo.jpg
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: cover
|
||||
attributes:
|
||||
label: cover
|
||||
description: 可选。填写后会同时更新该专题封面;留空则不改封面。
|
||||
placeholder: https://example.com/cover.jpg
|
||||
@@ -0,0 +1,64 @@
|
||||
name: 网站建议
|
||||
description: 对网站首页、同学资料、照片墙、内容文案或交互体验提出建议
|
||||
title: "suggestion: "
|
||||
labels:
|
||||
- site-suggestion
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢你愿意帮我们把网站做得更好。
|
||||
请尽量说明建议涉及的位置、你希望改成什么样,以及这样修改的原因。
|
||||
- type: dropdown
|
||||
id: section
|
||||
attributes:
|
||||
label: 建议涉及的网站部分
|
||||
description: 请选择最接近的部分;如果不确定,可以选择「其他」。
|
||||
options:
|
||||
- 首页
|
||||
- 如今的我们
|
||||
- 照片墙
|
||||
- 班级故事
|
||||
- 网站文案
|
||||
- 页面布局
|
||||
- 移动端体验
|
||||
- 交互功能
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: current
|
||||
attributes:
|
||||
label: 当前情况
|
||||
description: 目前你看到的问题、觉得不方便的地方,或想改进的位置。
|
||||
placeholder: 例如:照片墙某个专题的入口不太明显,手机上需要滑很久才能找到。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: 建议内容
|
||||
description: 你希望网站如何调整?可以写具体文案、布局、功能或展示方式。
|
||||
placeholder: 例如:希望在首页增加照片墙入口,并按「教室 / 活动 / 毕业日」展示几个快捷入口。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reason
|
||||
attributes:
|
||||
label: 建议原因
|
||||
description: 为什么你觉得这个建议值得做?它会帮助谁,或解决什么问题?
|
||||
placeholder: 例如:这样第一次访问网站的同学可以更快找到照片,也更容易回忆起不同阶段的活动。
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: page
|
||||
attributes:
|
||||
label: 相关页面或链接
|
||||
description: 可选。填写具体页面地址、截图链接或相关位置说明。
|
||||
placeholder: /photos 或 首页顶部
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 可选。可以补充截图说明、参考网站、更多背景或其他想法。
|
||||
placeholder: 其他想补充的内容。
|
||||
@@ -0,0 +1,71 @@
|
||||
name: Review Issues Without Template
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
label-without-template:
|
||||
if: ${{ !github.event.issue.pull_request }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add review label when no template is used
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const reviewLabel = "needs-review";
|
||||
const templateLabels = new Set([
|
||||
"people-update",
|
||||
"photo-update",
|
||||
"site-suggestion",
|
||||
]);
|
||||
|
||||
const issue = context.payload.issue;
|
||||
const labels = issue.labels.map((label) => label.name);
|
||||
const body = issue.body || "";
|
||||
const title = issue.title || "";
|
||||
|
||||
const hasTemplateLabel = labels.some((label) => templateLabels.has(label));
|
||||
const hasTemplateTitle =
|
||||
title.startsWith("people:") ||
|
||||
title.startsWith("photo:") ||
|
||||
title.startsWith("suggestion:");
|
||||
const hasTemplateFields =
|
||||
body.includes("### slug") ||
|
||||
body.includes("### topic") ||
|
||||
body.includes("### 建议涉及的网站部分");
|
||||
|
||||
if (hasTemplateLabel || hasTemplateTitle || hasTemplateFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: reviewLabel,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: reviewLabel,
|
||||
color: "fbca04",
|
||||
description: "Issue did not use a repository template and needs manual review.",
|
||||
});
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: [reviewLabel],
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Update Photo Data
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- labeled
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-photo:
|
||||
if: contains(github.event.issue.labels.*.name, 'photo-update') || startsWith(github.event.issue.title, 'photo:')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Update photo topic data
|
||||
env:
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
run: node scripts/update-photo-topic-from-issue.mjs
|
||||
|
||||
- name: Create pull request
|
||||
id: create-pr
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
commit-message: "chore: update photo data from issue #${{ github.event.issue.number }}"
|
||||
title: "更新照片墙:#${{ github.event.issue.number }}"
|
||||
body: |
|
||||
由 #${{ github.event.issue.number }} 自动生成。
|
||||
|
||||
这个 PR 会根据 issue 表单更新 `src/data/photo-topics/<topic>.ts`:
|
||||
- 如果同一专题内已有相同 `title`,则修改对应照片
|
||||
- 如果没有相同 `title`,则追加一张新照片
|
||||
- 如果填写了 `cover`,则同步更新专题封面
|
||||
branch: photo-update/issue-${{ github.event.issue.number }}
|
||||
delete-branch: true
|
||||
labels: photo-update
|
||||
|
||||
- name: Comment on issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prUrl = "${{ steps.create-pr.outputs.pull-request-url }}";
|
||||
const body = prUrl
|
||||
? `已根据这个 issue 创建照片更新 PR:${prUrl}\n\n如果内容还要调整,直接编辑 issue 表单即可更新同一个 PR。`
|
||||
: "这个 issue 没有产生新的文件改动,所以没有创建 PR。";
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { relative, resolve } from "node:path";
|
||||
|
||||
const issueBody = process.env.ISSUE_BODY ?? "";
|
||||
const topicRoot = resolve(process.env.PHOTO_TOPIC_ROOT ?? "src/data/photo-topics");
|
||||
|
||||
const fields = ["topic", "title", "caption", "image", "cover"];
|
||||
const requiredFields = ["topic", "title", "caption", "image"];
|
||||
const sectionAliases = new Map(fields.map((field) => [field.toLowerCase(), field]));
|
||||
|
||||
function normalizeValue(value) {
|
||||
const trimmed = value.trim();
|
||||
return trimmed === "_No response_" ? "" : trimmed;
|
||||
}
|
||||
|
||||
function parseIssueForm(body) {
|
||||
const result = {};
|
||||
const sectionPattern = /^###\s+(.+?)\s*$/gm;
|
||||
const matches = [...body.matchAll(sectionPattern)];
|
||||
|
||||
for (let index = 0; index < matches.length; index += 1) {
|
||||
const rawLabel = matches[index][1].trim().toLowerCase();
|
||||
const field = sectionAliases.get(rawLabel);
|
||||
if (!field) continue;
|
||||
|
||||
const start = matches[index].index + matches[index][0].length;
|
||||
const end = index + 1 < matches.length ? matches[index + 1].index : body.length;
|
||||
result[field] = normalizeValue(body.slice(start, end));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function assertValidInput(input) {
|
||||
for (const field of requiredFields) {
|
||||
if (!input[field]) {
|
||||
throw new Error(`Issue form is missing required field: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.topic)) {
|
||||
throw new Error("topic must use lowercase letters, numbers, and single hyphens only.");
|
||||
}
|
||||
}
|
||||
|
||||
function topicPathFromSlug(slug) {
|
||||
return resolve(topicRoot, `${slug}.ts`);
|
||||
}
|
||||
|
||||
function readTopicModule(filePath) {
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const declarationPattern = /export const\s+(\w+)\s*:\s*PhotoTopic\s*=\s*({[\s\S]*?});\s*$/;
|
||||
const match = source.match(declarationPattern);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`Could not find PhotoTopic export in ${relative(process.cwd(), filePath)}`);
|
||||
}
|
||||
|
||||
const [, exportName, objectSource] = match;
|
||||
const topic = Function(`return (${objectSource});`)();
|
||||
return { exportName, topic };
|
||||
}
|
||||
|
||||
function buildPhoto(input, existing = {}) {
|
||||
return {
|
||||
title: input.title || existing.title || "",
|
||||
caption: input.caption || existing.caption || "",
|
||||
image: input.image || existing.image || ""
|
||||
};
|
||||
}
|
||||
|
||||
function quote(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function formatPhoto(photo) {
|
||||
return ` {
|
||||
title: ${quote(photo.title)},
|
||||
caption: ${quote(photo.caption)},
|
||||
image: ${quote(photo.image)}
|
||||
}`;
|
||||
}
|
||||
|
||||
function formatTopicModule(exportName, topic) {
|
||||
return `import type { PhotoTopic } from "../photo-types";
|
||||
|
||||
export const ${exportName}: PhotoTopic = {
|
||||
slug: ${quote(topic.slug)},
|
||||
title: ${quote(topic.title)},
|
||||
text: ${quote(topic.text)},
|
||||
cover: ${quote(topic.cover ?? "")},
|
||||
photos: [
|
||||
${topic.photos.map(formatPhoto).join(",\n")}
|
||||
]
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
const input = parseIssueForm(issueBody);
|
||||
assertValidInput(input);
|
||||
|
||||
const topicPath = topicPathFromSlug(input.topic);
|
||||
const { exportName, topic } = readTopicModule(topicPath);
|
||||
|
||||
if (topic.slug !== input.topic) {
|
||||
throw new Error(`Topic slug mismatch: issue asked for ${input.topic}, file contains ${topic.slug}`);
|
||||
}
|
||||
|
||||
if (input.cover) {
|
||||
topic.cover = input.cover;
|
||||
}
|
||||
|
||||
const existingIndex = topic.photos.findIndex((photo) => photo.title === input.title);
|
||||
if (existingIndex >= 0) {
|
||||
topic.photos[existingIndex] = buildPhoto(input, topic.photos[existingIndex]);
|
||||
console.log(`Updated existing photo "${input.title}" in topic: ${input.topic}`);
|
||||
} else {
|
||||
topic.photos.push(buildPhoto(input));
|
||||
console.log(`Added new photo "${input.title}" to topic: ${input.topic}`);
|
||||
}
|
||||
|
||||
writeFileSync(topicPath, formatTopicModule(exportName, topic), "utf8");
|
||||
console.log(`Wrote ${relative(process.cwd(), topicPath)}`);
|
||||
Reference in New Issue
Block a user