839 lines
25 KiB
Markdown
839 lines
25 KiB
Markdown
# Phase 0 技术验证 Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 本地跑通「客户在 Sveltia CMS 改内容 → 提交到自建 Gitea → Gitea Action 自动构建 Astro → ossutil 同步到阿里云 OSS」的完整链路,验证设计文档([../specs/2026-07-23-official-site-cms-design.md](../specs/2026-07-23-official-site-cms-design.md))的技术可行性。
|
||
|
||
**Architecture:** Astro 5(SSG)生成纯静态站;Sveltia CMS(CDN SPA)作为客户可视化后台,经 Gitea OAuth 提交 Markdown 到仓库;本地 Docker Gitea + act_runner 提供 Git 后端 + CI;构建产物由 ossutil 推到阿里云 OSS。内容用 Astro Content Collections + Zod schema 强类型校验。
|
||
|
||
**Tech Stack:** Astro 5 · Sveltia CMS · Gitea(+Gitea Actions / act_runner)· ossutil · 阿里云 OSS · Vitest
|
||
|
||
**前置条件:** 本机已装 Node.js 20+、Docker;后续接真实 OSS 时需阿里云账号 + AK/SK + 已建 bucket。
|
||
|
||
---
|
||
|
||
## Scope
|
||
|
||
本计划**只覆盖 Phase 0 技术验证**(设计文档 §十二 Phase 0)。Phase 1 基座化、Phase 2 首客户上线、Phase 3 Payload 适配器各自后续单独出计划。
|
||
|
||
**验证成功标准(端到端):** 在 `http://localhost:4321/admin/` 用 Sveltia 新增一条新闻 → 提交 → 本地 Gitea 收到 commit → Gitea Action 触发并构建成功 → `dist/` 生成新页面 → ossutil 脚本可把 `dist/` 同步到 OSS(凭据就绪时实推)。
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
```
|
||
official-site-base/
|
||
├── package.json # 依赖:astro, vitest
|
||
├── astro.config.mjs # Astro 配置
|
||
├── tsconfig.json
|
||
├── vitest.config.ts
|
||
├── src/
|
||
│ ├── content.config.ts # Content Collections(news)+ schema 引用
|
||
│ ├── content/
|
||
│ │ ├── schemas.ts # Zod schema(可单独单测)
|
||
│ │ └── news/
|
||
│ │ └── welcome.md # 示例新闻
|
||
│ ├── layouts/BaseLayout.astro
|
||
│ ├── pages/
|
||
│ │ ├── index.astro # 首页
|
||
│ │ └── news/
|
||
│ │ ├── index.astro # 新闻列表
|
||
│ │ └── [slug].astro # 新闻详情
|
||
│ └── styles/global.css # 设计 token(白标入口)
|
||
├── public/
|
||
│ ├── admin/
|
||
│ │ ├── index.html # Sveltia CMS 入口
|
||
│ │ └── config.yml # Sveltia 配置(gitea backend)
|
||
│ └── favicon.svg
|
||
├── tests/content-schema.test.ts # Zod schema 单测
|
||
├── scripts/deploy-oss.sh # ossutil 发布脚本
|
||
├── .gitea/workflows/deploy.yml # Gitea Action
|
||
├── docker/gitea.docker-compose.yml # 本地 Gitea + runner
|
||
├── .env.example # OSS 凭据模板
|
||
└── docs/{specs,plans}/...
|
||
```
|
||
|
||
**职责边界:**
|
||
- `src/content/schemas.ts` —— 纯 Zod schema,与 Astro 解耦,可单测
|
||
- `src/content.config.ts` —— 把 schema 绑到 collection,定义 loader
|
||
- `public/admin/config.yml` —— Sveltia 字段配置,与 schema 字段保持一致(重复但必要:Sveltia 是 YAML、Astro 是 TS)
|
||
- `scripts/deploy-oss.sh` —— 独立发布脚本,本地和 CI 共用(DRY)
|
||
|
||
---
|
||
|
||
## Task 1: 初始化 Astro 项目 + 工具链
|
||
|
||
**Files:**
|
||
- Create: `package.json`, `astro.config.mjs`, `tsconfig.json`, `vitest.config.ts`
|
||
- Create: `src/styles/global.css`, `public/favicon.svg`, `.env.example`
|
||
|
||
- [ ] **Step 1: 初始化 Astro 项目(在 official-site-base 目录内)**
|
||
|
||
```bash
|
||
# 已在 official-site-base 目录,初始化最小 Astro(不加模板)
|
||
npm init -y
|
||
npm install astro@latest
|
||
npm install -D vitest@latest
|
||
```
|
||
|
||
- [ ] **Step 2: 写 package.json scripts**
|
||
|
||
把 `package.json` 的 `scripts` 改为:
|
||
|
||
```json
|
||
{
|
||
"name": "official-site-base",
|
||
"type": "module",
|
||
"version": "0.1.0",
|
||
"scripts": {
|
||
"dev": "astro dev",
|
||
"build": "astro build",
|
||
"preview": "astro preview",
|
||
"test": "vitest run"
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 写 astro.config.mjs**
|
||
|
||
```js
|
||
import { defineConfig } from 'astro/config';
|
||
|
||
export default defineConfig({
|
||
site: 'https://example.com',
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 4: 写 tsconfig.json**
|
||
|
||
```json
|
||
{
|
||
"extends": "astro/tsconfigs/strict",
|
||
"include": [".astro/types.d.ts", "**/*"],
|
||
"exclude": ["dist"]
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: 写 vitest.config.ts**
|
||
|
||
```ts
|
||
import { defineConfig } from 'vitest/config';
|
||
|
||
export default defineConfig({
|
||
test: {
|
||
include: ['tests/**/*.test.ts'],
|
||
},
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 6: 写设计 token 入口 src/styles/global.css**
|
||
|
||
```css
|
||
:root {
|
||
/* 白标入口:换客户时改这里 */
|
||
--color-primary: #007aff;
|
||
--color-text: #1a1a1a;
|
||
--color-bg: #ffffff;
|
||
--font-sans: system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
}
|
||
|
||
html { font-family: var(--font-sans); color: var(--color-text); background: var(--color-bg); }
|
||
body { margin: 0; max-width: 960px; margin-inline: auto; padding: 1.5rem; }
|
||
a { color: var(--color-primary); }
|
||
```
|
||
|
||
- [ ] **Step 7: 写 public/favicon.svg 与 .env.example**
|
||
|
||
`public/favicon.svg`:
|
||
```xml
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#007aff"/><text x="16" y="22" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">O</text></svg>
|
||
```
|
||
|
||
`.env.example`:
|
||
```bash
|
||
# 阿里云 OSS 发布凭据(复制为 .env 后填真值,.env 已被 .gitignore 排除)
|
||
OSS_ACCESS_KEY_ID=
|
||
OSS_ACCESS_KEY_SECRET=
|
||
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
|
||
OSS_BUCKET=official-site-base-demo
|
||
OSS_REGION=cn-hangzhou
|
||
# 本地 Gitea
|
||
GITEA_BASE_URL=http://localhost:3000
|
||
```
|
||
|
||
- [ ] **Step 8: 验证 Astro 能起**
|
||
|
||
Run: `npx astro --version && npm run dev`,浏览器开 `http://localhost:4321`,Ctrl+C 停止。
|
||
Expected: 版本号输出(5.x);dev server 起来无报错(此时还没有页面,404 正常)。
|
||
|
||
- [ ] **Step 9: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "feat: 初始化 Astro 项目与工具链(vitest/设计token/env)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: news Content Collection + Zod schema(TDD)
|
||
|
||
**Files:**
|
||
- Create: `src/content/schemas.ts`, `src/content.config.ts`, `src/content/news/welcome.md`
|
||
- Test: `tests/content-schema.test.ts`
|
||
|
||
- [ ] **Step 1: 写失败测试 tests/content-schema.test.ts**
|
||
|
||
```ts
|
||
import { describe, it, expect } from 'vitest';
|
||
import { newsSchema } from '../src/content/schemas';
|
||
|
||
describe('newsSchema', () => {
|
||
it('accepts valid frontmatter and coerces date', () => {
|
||
const parsed = newsSchema.parse({
|
||
title: '官网基座启动',
|
||
date: '2026-07-23',
|
||
category: '公告',
|
||
});
|
||
expect(parsed.title).toBe('官网基座启动');
|
||
expect(parsed.date).toEqual(new Date('2026-07-23'));
|
||
expect(parsed.category).toBe('公告');
|
||
});
|
||
|
||
it('defaults category to 未分类', () => {
|
||
const parsed = newsSchema.parse({ title: '测试', date: '2026-07-23' });
|
||
expect(parsed.category).toBe('未分类');
|
||
});
|
||
|
||
it('rejects missing title', () => {
|
||
expect(() => newsSchema.parse({ date: '2026-07-23' })).toThrow();
|
||
});
|
||
|
||
it('rejects missing date', () => {
|
||
expect(() => newsSchema.parse({ title: '测试' })).toThrow();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试,确认失败**
|
||
|
||
Run: `npm test`
|
||
Expected: FAIL —— `Cannot find module '../src/content/schemas'`(文件还没建)。
|
||
|
||
- [ ] **Step 3: 写 schema src/content/schemas.ts**
|
||
|
||
```ts
|
||
import { z } from 'astro:content';
|
||
|
||
export const newsSchema = z.object({
|
||
title: z.string(),
|
||
date: z.coerce.date(),
|
||
category: z.string().default('未分类'),
|
||
});
|
||
```
|
||
|
||
> `z` 直接从 `astro:content` 导出(Astro 内置 zod,无需单独装)。
|
||
|
||
- [ ] **Step 4: 写 collection 配置 src/content.config.ts**
|
||
|
||
```ts
|
||
import { defineCollection } from 'astro:content';
|
||
import { glob } from 'astro/loaders';
|
||
import { newsSchema } from './content/schemas';
|
||
|
||
const news = defineCollection({
|
||
loader: glob({ pattern: '**/*.md', base: './src/content/news' }),
|
||
schema: newsSchema,
|
||
});
|
||
|
||
export const collections = { news };
|
||
```
|
||
|
||
- [ ] **Step 5: 写示例内容 src/content/news/welcome.md**
|
||
|
||
```markdown
|
||
---
|
||
title: 官网基座启动
|
||
date: 2026-07-23
|
||
category: 公告
|
||
---
|
||
|
||
这是 Phase 0 技术验证的第一条新闻。内容编辑链路跑通后,客户可在 Sveltia CMS 自助新增。
|
||
```
|
||
|
||
- [ ] **Step 6: 运行测试,确认通过**
|
||
|
||
Run: `npm test`
|
||
Expected: PASS(4 个测试全过)。
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "feat: 新增 news 内容集合与 Zod schema(含单测)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: 首页 + 新闻列表/详情页面
|
||
|
||
**Files:**
|
||
- Create: `src/layouts/BaseLayout.astro`, `src/pages/index.astro`, `src/pages/news/index.astro`, `src/pages/news/[slug].astro`
|
||
|
||
- [ ] **Step 1: 写 BaseLayout src/layouts/BaseLayout.astro**
|
||
|
||
```astro
|
||
---
|
||
import '../styles/global.css';
|
||
interface Props { title: string; }
|
||
const { title } = Astro.props;
|
||
---
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<link rel="icon" href="/favicon.svg" />
|
||
<title>{title}</title>
|
||
</head>
|
||
<body>
|
||
<header><a href="/" style="font-weight:700;font-size:1.25rem;">官网基座</a></header>
|
||
<main><slot /></main>
|
||
<footer style="margin-top:3rem;font-size:.85rem;color:#888;">© 官网基座</footer>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
- [ ] **Step 2: 写首页 src/pages/index.astro**
|
||
|
||
```astro
|
||
---
|
||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||
import { getCollection } from 'astro:content';
|
||
|
||
const news = (await getCollection('news')).sort(
|
||
(a, b) => b.data.date.getTime() - a.data.date.getTime()
|
||
);
|
||
---
|
||
<BaseLayout title="官网基座 · 首页">
|
||
<h1>官网基座</h1>
|
||
<p>Phase 0 技术验证站点。</p>
|
||
<section>
|
||
<h2>最新动态</h2>
|
||
<ul>
|
||
{news.map((post) => (
|
||
<li><a href={`/news/${post.id}/`}>{post.data.title}</a> <small>{post.data.date.toLocaleDateString('zh-CN')}</small></li>
|
||
))}
|
||
</ul>
|
||
<p><a href="/news/">查看全部 →</a></p>
|
||
</section>
|
||
</BaseLayout>
|
||
```
|
||
|
||
- [ ] **Step 3: 写新闻列表 src/pages/news/index.astro**
|
||
|
||
```astro
|
||
---
|
||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||
import { getCollection } from 'astro:content';
|
||
|
||
const news = (await getCollection('news')).sort(
|
||
(a, b) => b.data.date.getTime() - a.data.date.getTime()
|
||
);
|
||
---
|
||
<BaseLayout title="新闻动态">
|
||
<h1>新闻动态</h1>
|
||
<ul>
|
||
{news.map((post) => (
|
||
<li>
|
||
<a href={`/news/${post.id}/`}>{post.data.title}</a>
|
||
<small> · {post.data.category} · {post.data.date.toLocaleDateString('zh-CN')}</small>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</BaseLayout>
|
||
```
|
||
|
||
- [ ] **Step 4: 写新闻详情 src/pages/news/[slug].astro**
|
||
|
||
```astro
|
||
---
|
||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||
import { getCollection, render } from 'astro:content';
|
||
|
||
export async function getStaticPaths() {
|
||
const news = await getCollection('news');
|
||
return news.map((post) => ({
|
||
params: { slug: post.id },
|
||
props: { post },
|
||
}));
|
||
}
|
||
|
||
const { post } = Astro.props;
|
||
const { Content } = await render(post);
|
||
---
|
||
<BaseLayout title={post.data.title}>
|
||
<article>
|
||
<h1>{post.data.title}</h1>
|
||
<p><small>{post.data.category} · {post.data.date.toLocaleDateString('zh-CN')}</small></p>
|
||
<Content />
|
||
</article>
|
||
<p><a href="/news/">← 返回列表</a></p>
|
||
</BaseLayout>
|
||
```
|
||
|
||
- [ ] **Step 5: 构建并验证产物**
|
||
|
||
Run: `npm run build`
|
||
Expected: `dist/index.html`、`dist/news/index.html`、`dist/news/welcome/index.html` 生成,无报错。
|
||
|
||
Run: `npm run preview`,浏览器开 `http://localhost:4321`,点进新闻详情。
|
||
Expected: 首页列出到「官网基座启动」一条;详情页正文渲染。
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "feat: 首页与新闻列表/详情页面"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Sveltia CMS 接入(/admin)
|
||
|
||
**Files:**
|
||
- Create: `public/admin/index.html`, `public/admin/config.yml`
|
||
|
||
- [ ] **Step 1: 写 CMS 入口 public/admin/index.html**
|
||
|
||
```html
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="robots" content="noindex" />
|
||
<title>Sveltia CMS</title>
|
||
</head>
|
||
<body>
|
||
<!-- 注意:不要加 type="module",不要加 CSS link(Sveltia 自带样式) -->
|
||
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
> 国内生产环境可把 `sveltia-cms.js` 自托管到 OSS 或换国内 CDN(unpkg 国内可能慢),Phase 0 先用官方 CDN 验证。
|
||
|
||
- [ ] **Step 2: 写 Sveltia 配置 public/admin/config.yml**
|
||
|
||
```yaml
|
||
# yaml-language-server: $schema=https://unpkg.com/@sveltia/cms/schema/sveltia-cms.json
|
||
|
||
# 本地验证用本地 Gitea(Task 5 起的);生产换真实 Gitea 地址
|
||
backend:
|
||
name: gitea
|
||
repo: lzy/official-site-base
|
||
base_url: http://localhost:3000
|
||
api_root: http://localhost:3000/api/v1
|
||
|
||
media_folder: /public/media
|
||
public_folder: /media
|
||
|
||
collections:
|
||
- name: news
|
||
label: 新闻动态
|
||
label_singular: 新闻
|
||
folder: /src/content/news
|
||
create: true
|
||
slug: '{{slug}}'
|
||
fields:
|
||
- { label: 标题, name: title, widget: string }
|
||
- { label: 日期, name: date, widget: datetime, type: date }
|
||
- { label: 分类, name: category, widget: string, default: 未分类 }
|
||
- { label: 正文, name: body, widget: richtext }
|
||
```
|
||
|
||
> Sveltia 的 `folder` 路径要和 Astro `content.config.ts` 的 `base`(`./src/content/news`)一致;字段要和 `newsSchema` 一一对应(title/date/category)。
|
||
|
||
- [ ] **Step 3: 验证 CMS 界面能加载**
|
||
|
||
Run: `npm run dev`,浏览器开 `http://localhost:4321/admin/`。
|
||
Expected: 看到 Sveltia CMS 登录/界面(此时还没连 Gitea,点登录会报错或重定向失败——属正常,Task 6 接 Gitea 后通)。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "feat: 接入 Sveltia CMS(/admin + gitea backend 配置)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: 本地 Gitea + act_runner(Docker)
|
||
|
||
**Files:**
|
||
- Create: `docker/gitea.docker-compose.yml`
|
||
|
||
- [ ] **Step 1: 写 docker-compose docker/gitea.docker-compose.yml**
|
||
|
||
```yaml
|
||
services:
|
||
gitea:
|
||
image: gitea/gitea:1
|
||
environment:
|
||
- USER_UID=1000
|
||
- USER_GID=1000
|
||
volumes:
|
||
- ./gitea-data:/data
|
||
ports:
|
||
- "3000:3000" # Web
|
||
- "2222:22" # SSH
|
||
restart: unless-stopped
|
||
|
||
runner:
|
||
image: gitea/act_runner:latest
|
||
environment:
|
||
- GITEA_INSTANCE_URL=http://gitea:3000
|
||
- GITEA_RUNNER_REGISTRATION_TOKEN=phase0-local-token
|
||
volumes:
|
||
- ./runner-data:/data
|
||
- /var/run/docker.sock:/var/run/docker.sock
|
||
depends_on:
|
||
- gitea
|
||
restart: unless-stopped
|
||
```
|
||
|
||
- [ ] **Step 2: 启动 Gitea + runner**
|
||
|
||
```bash
|
||
cd docker
|
||
docker compose -f gitea.docker-compose.yml up -d
|
||
docker compose -f gitea.docker-compose.yml ps
|
||
```
|
||
Expected: 两个容器 `Up`。
|
||
|
||
- [ ] **Step 3: 完成 Gitea 首次安装(Web 向导)**
|
||
|
||
浏览器开 `http://localhost:3000`。
|
||
- 数据库选 SQLite(默认)
|
||
- 站点 URL:`http://localhost:3000`
|
||
- 创建管理员账户:用户名 `lzy`,密码自设(记牢)
|
||
- 点「立即安装」
|
||
|
||
Expected: 安装完成,跳转登录页,能登入。
|
||
|
||
- [ ] **Step 4: 注册 runner 到 Gitea**
|
||
|
||
在 Gitea Web:`http://localhost:3000/admin/actions/runners`,确认 `runner` 容器已注册并 `idle`(compose 里 token 写死为 `phase0-local-token`,runner 自动注册)。如未注册,到「Site Administration → Actions → Runners」用 `phase0-local-token` 手动添加。
|
||
|
||
Expected: runners 列表有一条 `idle` 的 runner。
|
||
|
||
- [ ] **Step 5: 把 official-site-base 推到本地 Gitea**
|
||
|
||
```bash
|
||
# 在 official-site-base 目录
|
||
git remote add local http://localhost:3000/lzy/official-site-base.git
|
||
# 先在 Gitea Web 「新建仓库」official-site-base(空仓库,不要勾 README)
|
||
git push -u local main
|
||
```
|
||
Expected: 推送成功,Gitea 上能看到代码。
|
||
|
||
- [ ] **Step 6: Commit docker 配置**
|
||
|
||
```bash
|
||
# 回 official-site-base 目录
|
||
git add docker/gitea.docker-compose.yml
|
||
git commit -m "chore: 本地 Gitea + act_runner docker-compose"
|
||
```
|
||
|
||
> 注意:`docker/gitea-data/` 和 `docker/runner-data/` 不要提交(本地数据),后续加到 .gitignore。
|
||
|
||
- [ ] **Step 7: 补 .gitignore 排除本地数据**
|
||
|
||
在 `.gitignore` 末尾追加:
|
||
```
|
||
# 本地 Gitea/runner 数据
|
||
docker/gitea-data/
|
||
docker/runner-data/
|
||
```
|
||
|
||
```bash
|
||
git add .gitignore
|
||
git commit -m "chore: 忽略本地 Gitea/runner 数据目录"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: Gitea OAuth App + Sveltia 提交链路验证
|
||
|
||
**Files:**
|
||
- Modify: `public/admin/config.yml`(确认 base_url)
|
||
|
||
- [ ] **Step 1: 在 Gitea 创建 OAuth2 应用**
|
||
|
||
Gitea Web:头像 → Settings → Applications → **OAuth2 Applications** → 「Create Application」:
|
||
- Name: `Sveltia CMS`
|
||
- Redirect URI: `http://localhost:4321/admin/`
|
||
- 创建后记录 `Client ID` 和 `Client Secret`
|
||
|
||
Expected: 拿到一对 Client ID / Secret。
|
||
|
||
- [ ] **Step 2: 在 Sveltia config 补 auth 配置**
|
||
|
||
修改 `public/admin/config.yml` 的 backend 段(追加 `app_id` / `client_id`,Sveltia gitea backend 用 OAuth):
|
||
|
||
```yaml
|
||
backend:
|
||
name: gitea
|
||
repo: lzy/official-site-base
|
||
base_url: http://localhost:3000
|
||
api_root: http://localhost:3000/api/v1
|
||
# Gitea OAuth2 App(Step 1 创建)
|
||
app_id: <填 Client ID>
|
||
```
|
||
> `client_secret` 不写进配置(Sveltia gitea backend 走 PKCE/标准 OAuth 流,仅用 client_id + redirect;如 Gitea 要求 secret,参考 Sveltia Backends 文档配 `auth_type`)。Phase 0 若 PKCE 流不通,降级用 `git-gateway` 或本地 token 方式,记录到 spec 风险项。
|
||
|
||
- [ ] **Step 3: 验证 Sveltia 登录 Gitea**
|
||
|
||
Run: `npm run dev`,浏览器开 `http://localhost:4321/admin/`,点登录。
|
||
Expected: 跳转到 Gitea 授权页 → 授权 → 回到 Sveltia 看到「新闻动态」集合,能看到 Task 2 的 welcome.md。
|
||
|
||
- [ ] **Step 4: 新增一条新闻并提交**
|
||
|
||
在 Sveltia:进入「新闻动态」→「新增新闻」→ 填标题/日期/分类/正文 → 保存 → 「Publish」。
|
||
Expected: 提交成功,本地 Gitea 仓库(`http://localhost:3000/lzy/official-site-base`)出现新 commit,`src/content/news/` 多一个 md 文件。
|
||
|
||
- [ ] **Step 5: 拉取并验证内容进入构建**
|
||
|
||
```bash
|
||
git pull local main
|
||
ls src/content/news/
|
||
npm run build
|
||
```
|
||
Expected: 新文件出现;build 产物 `dist/news/<新slug>/index.html` 生成。
|
||
|
||
- [ ] **Step 6: Commit config 调整**
|
||
|
||
```bash
|
||
git add public/admin/config.yml
|
||
git commit -m "feat: Sveltia 接 Gitea OAuth,跑通提交链路"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: Gitea Action 自动构建
|
||
|
||
**Files:**
|
||
- Create: `.gitea/workflows/deploy.yml`
|
||
|
||
- [ ] **Step 1: 写工作流 .gitea/workflows/deploy.yml**
|
||
|
||
```yaml
|
||
name: build-and-deploy
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
build:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '20'
|
||
cache: 'npm'
|
||
|
||
- name: Install deps
|
||
run: npm ci
|
||
|
||
- name: Build
|
||
run: npm run build
|
||
|
||
- name: Upload dist artifact
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: dist
|
||
path: dist/
|
||
|
||
- name: Sync to OSS
|
||
env:
|
||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }}
|
||
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
|
||
run: bash scripts/deploy-oss.sh
|
||
```
|
||
|
||
> Gitea Actions 兼容 GitHub Actions 语法;`secrets` 在 Gitea 仓库 Settings → Actions → Secrets 配。OSS 同步在 Task 8 写脚本;本地验证时若未配 OSS secrets,该 step 会跳过(脚本内判空)。
|
||
|
||
- [ ] **Step 2: 推送触发 Action**
|
||
|
||
```bash
|
||
git add .gitea/workflows/deploy.yml
|
||
git commit -m "ci: Gitea Action 构建并发布到 OSS"
|
||
git push local main
|
||
```
|
||
|
||
- [ ] **Step 3: 验证 Action 执行**
|
||
|
||
Gitea Web:`http://localhost:3000/lzy/official-site-base/actions`。
|
||
Expected: 看到一次 push 触发的运行,`build` 作业绿色,artifact `dist` 上传成功;`Sync to OSS` 步骤因无凭据显示跳过/失败(Task 8 配齐后转绿)。
|
||
|
||
---
|
||
|
||
## Task 8: ossutil 发布脚本
|
||
|
||
**Files:**
|
||
- Create: `scripts/deploy-oss.sh`
|
||
|
||
- [ ] **Step 1: 安装 ossutil(如未装)**
|
||
|
||
```bash
|
||
# 参考 https://help.aliyun.com/document_detail/120075.html
|
||
# macOS/Linux:
|
||
curl -o /usr/local/bin/ossutil https://gosspublic.alicdn.com/ossutil/install.sh && bash /usr/local/bin/ossutil
|
||
ossutil version
|
||
```
|
||
> Windows 下载 `ossutil64.exe` 放进 PATH。Phase 0 本地不强求装;CI 环境在 workflow 里装。
|
||
|
||
- [ ] **Step 2: 写脚本 scripts/deploy-oss.sh**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
# 判空:没配凭据时跳过(Phase 0 本地/未配 OSS 时不阻塞)
|
||
if [ -z "${OSS_ACCESS_KEY_ID:-}" ] || [ -z "${OSS_BUCKET:-}" ]; then
|
||
echo "[deploy-oss] 未配置 OSS 凭据,跳过同步。"
|
||
exit 0
|
||
fi
|
||
|
||
: "${OSS_ACCESS_KEY_SECRET:?需 OSS_ACCESS_KEY_SECRET}"
|
||
: "${OSS_ENDPOINT:?需 OSS_ENDPOINT}"
|
||
: "${OSS_BUCKET:?需 OSS_BUCKET}"
|
||
|
||
DIST_DIR="${1:-dist}"
|
||
[ -d "$DIST_DIR" ] || { echo "[deploy-oss] $DIST_DIR 不存在,请先 build"; exit 1; }
|
||
|
||
echo "[deploy-oss] 同步 $DIST_DIR → oss://$OSS_BUCKET/"
|
||
ossutil sync "$DIST_DIR" "oss://$OSS_BUCKET/" \
|
||
-i "$OSS_ACCESS_KEY_ID" \
|
||
-k "$OSS_ACCESS_KEY_SECRET" \
|
||
-e "$OSS_ENDPOINT" \
|
||
--force --delete
|
||
|
||
echo "[deploy-oss] 刷新 CDN(如配了 CDN_DOMAIN)"
|
||
if [ -n "${CDN_DOMAIN:-}" ]; then
|
||
ossutil cdn refresh --dirs "https://${CDN_DOMAIN}/" \
|
||
-i "$OSS_ACCESS_KEY_ID" -k "$OSS_ACCESS_KEY_SECRET" -e "$OSS_ENDPOINT" || true
|
||
fi
|
||
|
||
echo "[deploy-oss] 完成"
|
||
```
|
||
|
||
- [ ] **Step 3: 本地空跑验证(无凭据应跳过)**
|
||
|
||
```bash
|
||
chmod +x scripts/deploy-oss.sh
|
||
./scripts/deploy-oss.sh
|
||
```
|
||
Expected: 输出 `[deploy-oss] 未配置 OSS 凭据,跳过同步。`,退出码 0。
|
||
|
||
- [ ] **Step 4: 本地真实推送验证(需 OSS 凭据)**
|
||
|
||
在阿里云控制台建 bucket(如 `official-site-base-demo`,公共读),拿 AK/SK:
|
||
```bash
|
||
cp .env.example .env
|
||
# 编辑 .env 填真值
|
||
set -a; source .env; set +a # bash 加载环境变量
|
||
npm run build
|
||
./scripts/deploy-oss.sh
|
||
```
|
||
Expected: 输出同步文件列表;浏览器开 `https://official-site-base-demo.oss-cn-hangzhou.aliyuncs.com/index.html` 能看到首页。
|
||
|
||
- [ ] **Step 5: 配 Gitea Secrets 让 CI 同步**
|
||
|
||
Gitea Web:仓库 Settings → Actions → Secrets,新增:
|
||
- `OSS_ACCESS_KEY_ID`、`OSS_ACCESS_KEY_SECRET`、`OSS_ENDPOINT`、`OSS_BUCKET`
|
||
|
||
再 push 一次:
|
||
```bash
|
||
git add scripts/deploy-oss.sh
|
||
git commit -m "feat: ossutil 发布脚本(本地与 CI 共用)"
|
||
git push local main
|
||
```
|
||
Expected: CI 的 `Sync to OSS` 步骤变绿,OSS 内容更新。
|
||
|
||
---
|
||
|
||
## Task 9: 端到端验证 + README 更新
|
||
|
||
**Files:**
|
||
- Modify: `README.md`
|
||
|
||
- [ ] **Step 1: 端到端验证清单**
|
||
|
||
按下列顺序操作并逐条勾选:
|
||
- [ ] `npm run dev`,开 `http://localhost:4321/admin/`,登录 Gitea
|
||
- [ ] Sveltia 新增一条新闻 → Publish
|
||
- [ ] 本地 Gitea 仓库出现新 commit
|
||
- [ ] `git pull local main`,本地 `src/content/news/` 出现新 md
|
||
- [ ] push 后 Gitea Action 触发并构建成功
|
||
- [ ] OSS 同步成功(如已配凭据)
|
||
- [ ] `npm run build` + preview,首页与详情页展示新内容
|
||
|
||
- [ ] **Step 2: 更新 README.md 状态段**
|
||
|
||
把 README 的「状态」段从:
|
||
```
|
||
📋 设计已确认,待出实施计划(Phase 0 技术验证)。
|
||
```
|
||
改为:
|
||
```
|
||
✅ Phase 0 技术验证已跑通:Sveltia CMS → Gitea → Action → OSS 全链路。
|
||
|
||
## 本地开发
|
||
npm install
|
||
npm run dev # 站点 http://localhost:4321,CMS http://localhost:4321/admin/
|
||
docker compose -f docker/gitea.docker-compose.yml up -d # 起 Gitea
|
||
|
||
## 发布
|
||
npm run build
|
||
./scripts/deploy-oss.sh # 需配 .env 的 OSS 凭据
|
||
```
|
||
|
||
- [ ] **Step 3: 最终提交**
|
||
|
||
```bash
|
||
git add README.md
|
||
git commit -m "docs: Phase 0 验证完成,更新 README 与本地开发说明"
|
||
git push local main
|
||
```
|
||
|
||
---
|
||
|
||
## Self-Review
|
||
|
||
**1. Spec 覆盖检查**(对照 spec §五 内容模型 / §四架构 / §九部署):
|
||
- ✅ news 集合 + Zod schema(Task 2)—— 代表 Content Collections 机制,其余 7 个集合在 Phase 1 基座化时按同模式补
|
||
- ✅ Sveltia CMS /admin 可视化编辑(Task 4)
|
||
- ✅ 自建 Gitea + Actions(Task 5/7)
|
||
- ✅ 阿里云 OSS 发布(Task 8)
|
||
- ✅ 端到端链路验证(Task 9)
|
||
- ⏭ 留待后续计划:jobs/downloads/messages 集合、表单微服务、白标换 token 脚本、CMS 适配器接口(Phase 1-3)
|
||
|
||
**2. 占位符扫描:** Task 6 Step 2 的 `<填 Client ID>` 是执行时填入的真实值(来自 Step 1 创建的 App),非 TODO 占位符;其余步骤代码完整。✅
|
||
|
||
**3. 类型/命名一致性:**
|
||
- `newsSchema` 字段(title/date/category)在 Task 2 schema、Task 4 config.yml、Task 3 页面三处一致 ✅
|
||
- collection 名 `news` 在 content.config.ts、config.yml、页面 getCollection('news') 一致 ✅
|
||
- OSS 环境变量名(OSS_ACCESS_KEY_ID 等)在 .env.example、deploy-oss.sh、deploy.yml secrets 三处一致 ✅
|
||
- Gitea 仓库路径 `lzy/official-site-base` 在 config.yml、push 命令一致 ✅
|
||
|
||
**已知风险(执行时留意,已写入 spec §十一):**
|
||
- Gitea OAuth PKCE 流是否开箱通(Task 6 Step 2 注明了降级方案)
|
||
- `act_runner` 注册 token(compose 写死 `phase0-local-token`,仅本地)
|
||
- unpkg CDN 国内访问速度(生产换自托管)
|