chore: plan Notion bridge update with exact DB IDs and property schema
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
1af4fd8411
commit
248ef71aaf
|
|
@ -0,0 +1,66 @@
|
|||
name: 铸渊 · Bridge E · GitHub Changes → Notion
|
||||
|
||||
# 管道 E:main 分支有 push 或 PR 创建/关闭 → 变更摘要同步到 Notion 变更日志数据库
|
||||
# 依赖 Secrets:
|
||||
# NOTION_TOKEN Notion 集成 token(见仓库 Settings → Secrets → Actions)
|
||||
# NOTION_CHANGES_DB_ID 「GitHub 变更日志」Notion 数据库 ID
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, closed]
|
||||
|
||||
jobs:
|
||||
bridge-changes-to-notion:
|
||||
name: 📡 Bridge E · GitHub Changes → Notion
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2 # 获取前一次 commit 以便 diff
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
# ── commit push 分支 ──────────────────────────────────
|
||||
- name: 📡 同步 commit 变更到 Notion
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
|
||||
NOTION_CHANGES_DB_ID: ${{ secrets.NOTION_CHANGES_DB_ID }}
|
||||
EVENT_TYPE: commit
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||
COMMITTER: ${{ github.event.head_commit.author.username || github.actor }}
|
||||
COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }}
|
||||
CHANGED_FILES: ${{ join(github.event.commits[0].added, ',') }}
|
||||
run: |
|
||||
# 从 git diff 提取完整变更文件列表(更完整)
|
||||
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | tr '\n' ',' | sed 's/,$//')
|
||||
export CHANGED_FILES="${CHANGED:-$CHANGED_FILES}"
|
||||
node scripts/notion-bridge.js changes
|
||||
|
||||
# ── PR 分支 ───────────────────────────────────────────
|
||||
- name: 📡 同步 PR 变更到 Notion
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
|
||||
NOTION_CHANGES_DB_ID: ${{ secrets.NOTION_CHANGES_DB_ID }}
|
||||
EVENT_TYPE: pr
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_ACTION: ${{ github.event.action }}
|
||||
PR_MERGED: ${{ github.event.pull_request.merged }}
|
||||
COMMITTER: ${{ github.event.pull_request.user.login }}
|
||||
COMMIT_TIMESTAMP: ${{ github.event.pull_request.updated_at }}
|
||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
COMMIT_MSG: ${{ github.event.pull_request.title }}
|
||||
run: node scripts/notion-bridge.js changes
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
name: 铸渊 · Bridge A · SYSLOG → Notion
|
||||
|
||||
# 管道 A:syslog-inbox/ 有新文件 push → 同步到 Notion 数据库
|
||||
# 依赖 Secrets:
|
||||
# NOTION_TOKEN Notion 集成 token(见仓库 Settings → Secrets → Actions)
|
||||
# NOTION_SYSLOG_DB_ID 「GitHub SYSLOG 收件箱」Notion 数据库 ID
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'syslog-inbox/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
bridge-syslog-to-notion:
|
||||
name: 📥 Bridge A · SYSLOG → Notion
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: 🔗 同步 syslog-inbox 到 Notion
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
|
||||
NOTION_SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }}
|
||||
run: node scripts/notion-bridge.js syslog
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
// scripts/notion-bridge.js
|
||||
// 铸渊 → Notion 数据桥(共用模块)
|
||||
//
|
||||
// 管道 A (syslog): node scripts/notion-bridge.js syslog
|
||||
// 管道 E (changes): node scripts/notion-bridge.js changes
|
||||
//
|
||||
// 必需环境变量:
|
||||
// NOTION_TOKEN GitHub Secret: NOTION_TOKEN
|
||||
// NOTION_SYSLOG_DB_ID 管道A · 「GitHub SYSLOG 收件箱」数据库 ID
|
||||
// NOTION_CHANGES_DB_ID 管道E · 「GitHub 变更日志」数据库 ID
|
||||
//
|
||||
// 所有其他上下文通过环境变量传入(见下方各管道章节)
|
||||
|
||||
'use strict';
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// Notion API 基础调用
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
const NOTION_VERSION = '2022-06-28';
|
||||
const NOTION_API = 'api.notion.com';
|
||||
const NOTION_RICH_TEXT_MAX = 2000; // Notion rich_text content length limit per block
|
||||
const NOTION_TITLE_MAX = 120; // Practical limit for Notion page title
|
||||
|
||||
/**
|
||||
* 向 Notion API 发起 HTTPS POST 请求
|
||||
* @param {string} endpoint - e.g. '/v1/pages'
|
||||
* @param {object} body - JSON body
|
||||
* @param {string} token - Bearer token
|
||||
* @returns {Promise<object>} - Notion API 返回值
|
||||
*/
|
||||
function notionPost(endpoint, body, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify(body);
|
||||
const opts = {
|
||||
hostname: NOTION_API,
|
||||
port: 443,
|
||||
path: endpoint,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json',
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(opts, res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(parsed);
|
||||
} else {
|
||||
reject(new Error(`Notion API ${res.statusCode}: ${parsed.message || data}`));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error(`Notion API parse error: ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Notion rich_text 块
|
||||
* @param {string} content
|
||||
*/
|
||||
function richText(content) {
|
||||
const MAX = 2000; // Notion 单个 rich_text 内容上限
|
||||
return [{ type: 'text', text: { content: String(content || '').slice(0, MAX) } }];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Notion paragraph block
|
||||
*/
|
||||
function paragraph(content) {
|
||||
const MAX = 2000;
|
||||
const text = String(content || '').slice(0, MAX);
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
paragraph: { rich_text: richText(text) },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Notion heading_3 block
|
||||
*/
|
||||
function heading3(content) {
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'heading_3',
|
||||
heading_3: { rich_text: richText(String(content || '')) },
|
||||
};
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 管道 A · SYSLOG 收件箱同步到 Notion
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 将单条 syslog entry 写入 Notion 数据库
|
||||
* @param {string} dbId - 「GitHub SYSLOG 收件箱」database_id
|
||||
* @param {object} entry - syslog JSON 内容
|
||||
* @param {string} filename - 源文件名(用于溯源)
|
||||
* @param {string} token - Notion token
|
||||
*/
|
||||
async function createSyslogRecord(dbId, entry, filename, token) {
|
||||
const title = entry.title || filename;
|
||||
const devId = entry.from || entry.dev_id || '未知';
|
||||
const ts = entry.timestamp || new Date().toISOString();
|
||||
const content = JSON.stringify(entry, null, 2);
|
||||
|
||||
const body = {
|
||||
parent: { database_id: dbId },
|
||||
properties: {
|
||||
// 「名称」是 Notion 数据库的默认标题列,始终存在
|
||||
'名称': { title: richText(title) },
|
||||
// 以下属性若不存在会被 Notion 忽略,不影响创建
|
||||
'DEV编号': { rich_text: richText(devId) },
|
||||
'来源文件': { rich_text: richText(filename) },
|
||||
'接收时间': { date: { start: ts } },
|
||||
'处理状态': { select: { name: '待处理' } },
|
||||
},
|
||||
children: [
|
||||
heading3('📋 SYSLOG 原始内容'),
|
||||
paragraph(content),
|
||||
],
|
||||
};
|
||||
|
||||
return notionPost('/v1/pages', body, token);
|
||||
}
|
||||
|
||||
async function runPipelineA() {
|
||||
const token = process.env.NOTION_TOKEN;
|
||||
const dbId = process.env.NOTION_SYSLOG_DB_ID;
|
||||
|
||||
if (!token || !dbId) {
|
||||
console.log('⚠️ 管道A: 缺少 NOTION_TOKEN 或 NOTION_SYSLOG_DB_ID,跳过 Notion 同步');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const INBOX_DIR = 'syslog-inbox';
|
||||
if (!fs.existsSync(INBOX_DIR)) {
|
||||
console.log('📭 syslog-inbox/ 目录不存在,跳过');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(INBOX_DIR)
|
||||
.filter(f => (f.endsWith('.json') || f.endsWith('.md') || f.endsWith('.txt')) && f !== '.gitkeep');
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('📭 syslog-inbox/ 无待处理条目,跳过 Notion 同步');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`📥 管道A: 发现 ${files.length} 条 syslog,开始同步到 Notion…`);
|
||||
|
||||
let ok = 0, failed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(INBOX_DIR, file);
|
||||
let entry;
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(fullPath, 'utf8');
|
||||
entry = file.endsWith('.json') ? JSON.parse(raw) : { title: file, content: raw };
|
||||
} catch (e) {
|
||||
console.error(`❌ 读取 ${file} 失败: ${e.message}`);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const page = await createSyslogRecord(dbId, entry, file, token);
|
||||
console.log(` ✅ ${file} → Notion page: ${page.id}`);
|
||||
ok++;
|
||||
} catch (e) {
|
||||
console.error(` ❌ ${file} → Notion 失败: ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ 管道A Notion 同步完成 · 成功 ${ok} 条 · 失败 ${failed} 条`);
|
||||
if (failed > 0) process.exit(1);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 管道 E · GitHub 变更日志同步到 Notion
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 将单次 commit / PR 写入 Notion 变更日志数据库
|
||||
* 环境变量(由 workflow 注入):
|
||||
* EVENT_TYPE commit | pr
|
||||
* COMMIT_SHA 提交 SHA (commit)
|
||||
* COMMIT_MSG commit message
|
||||
* COMMITTER 提交者用户名
|
||||
* COMMIT_TIMESTAMP 提交时间 ISO
|
||||
* CHANGED_FILES 逗号分隔的变更文件路径
|
||||
* PR_NUMBER PR 编号 (pr)
|
||||
* PR_TITLE PR 标题 (pr)
|
||||
* PR_ACTION opened | closed (pr)
|
||||
* PR_MERGED true | false (pr)
|
||||
*/
|
||||
async function runPipelineE() {
|
||||
const token = process.env.NOTION_TOKEN;
|
||||
const dbId = process.env.NOTION_CHANGES_DB_ID;
|
||||
|
||||
if (!token || !dbId) {
|
||||
console.log('⚠️ 管道E: 缺少 NOTION_TOKEN 或 NOTION_CHANGES_DB_ID,跳过 Notion 同步');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const eventType = process.env.EVENT_TYPE || 'commit';
|
||||
const now = process.env.COMMIT_TIMESTAMP || new Date().toISOString();
|
||||
|
||||
let title, type, committer, changedFiles, description;
|
||||
|
||||
if (eventType === 'pr') {
|
||||
const action = process.env.PR_ACTION || 'opened';
|
||||
const merged = process.env.PR_MERGED === 'true';
|
||||
const prNum = process.env.PR_NUMBER || '';
|
||||
const prTitle = process.env.PR_TITLE || '(无标题)';
|
||||
const finalAct = merged ? 'merged' : action;
|
||||
|
||||
title = `PR #${prNum}: ${prTitle}`;
|
||||
type = 'PR';
|
||||
committer = process.env.COMMITTER || '未知';
|
||||
changedFiles = process.env.CHANGED_FILES || '';
|
||||
description = `PR #${prNum} ${finalAct} by ${committer}`;
|
||||
} else {
|
||||
const sha = (process.env.COMMIT_SHA || '').slice(0, 7);
|
||||
const msg = process.env.COMMIT_MSG || '(无 commit message)';
|
||||
title = msg.split('\n')[0].slice(0, 120); // 取第一行
|
||||
type = 'commit';
|
||||
committer = process.env.COMMITTER || '未知';
|
||||
changedFiles = process.env.CHANGED_FILES || '';
|
||||
description = `${sha}: ${title}`;
|
||||
}
|
||||
|
||||
console.log(`📡 管道E: 同步变更记录到 Notion: ${title}`);
|
||||
|
||||
const body = {
|
||||
parent: { database_id: dbId },
|
||||
properties: {
|
||||
'名称': { title: richText(title) },
|
||||
'提交者': { rich_text: richText(committer) },
|
||||
'类型': { select: { name: type } },
|
||||
'时间': { date: { start: now } },
|
||||
'变更文件': { rich_text: richText(changedFiles) },
|
||||
},
|
||||
children: [
|
||||
heading3('📝 变更详情'),
|
||||
paragraph(description),
|
||||
...(changedFiles ? [
|
||||
heading3('📁 变更文件列表'),
|
||||
paragraph(changedFiles.split(',').join('\n')),
|
||||
] : []),
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
const page = await notionPost('/v1/pages', body, token);
|
||||
console.log(`✅ 管道E 变更记录已写入 Notion: ${page.id}`);
|
||||
} catch (e) {
|
||||
console.error(`❌ 管道E 写入 Notion 失败: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 入口
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
const mode = process.argv[2];
|
||||
|
||||
if (mode === 'syslog') {
|
||||
runPipelineA().catch(e => { console.error(e); process.exit(1); });
|
||||
} else if (mode === 'changes') {
|
||||
runPipelineE().catch(e => { console.error(e); process.exit(1); });
|
||||
} else {
|
||||
console.error('用法: node scripts/notion-bridge.js [syslog|changes]');
|
||||
process.exit(1);
|
||||
}
|
||||
Loading…
Reference in New Issue