Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ permissions:
contents: read

jobs:
release-policy:
if: github.event_name == 'pull_request' && github.base_ref == 'main'
runs-on: ubuntu-latest
steps:
- name: Require develop as the release source
env:
HEAD_REF: ${{ github.head_ref }}
HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
REPOSITORY: ${{ github.repository }}
run: |
if [ "$HEAD_REF" != "develop" ]; then
echo "Pull requests targeting main must come from develop, got: $HEAD_REF" >&2
exit 1
fi

if [ "$HEAD_REPOSITORY" != "$REPOSITORY" ]; then
echo "The develop -> main release PR must come from this repository." >&2
exit 1
fi

validate:
runs-on: ubuntu-latest
steps:
Expand Down
143 changes: 143 additions & 0 deletions .github/workflows/promote-main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Promote develop to main

on:
workflow_dispatch:
inputs:
release_pr:
description: Open develop -> main release PR number
required: true
type: string

permissions:
contents: write
pull-requests: read

concurrency:
group: promote-main
cancel-in-progress: false

jobs:
promote:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: main
fetch-depth: 0

- name: Fetch release branches
run: git fetch origin main develop --no-tags

- name: Validate release PR and branch state
id: release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PR: ${{ inputs.release_pr }}
REPOSITORY: ${{ github.repository }}
shell: bash
run: |
set -euo pipefail

if ! [[ "$RELEASE_PR" =~ ^[0-9]+$ ]]; then
echo "release_pr must be a pull request number." >&2
exit 1
fi

pr_json="$(gh api "repos/$REPOSITORY/pulls/$RELEASE_PR")"
state="$(jq -r '.state' <<<"$pr_json")"
base_ref="$(jq -r '.base.ref' <<<"$pr_json")"
head_ref="$(jq -r '.head.ref' <<<"$pr_json")"
head_repo="$(jq -r '.head.repo.full_name' <<<"$pr_json")"
head_sha="$(jq -r '.head.sha' <<<"$pr_json")"

if [ "$state" != "open" ]; then
echo "Release PR #$RELEASE_PR must be open." >&2
exit 1
fi

if [ "$base_ref" != "main" ] || [ "$head_ref" != "develop" ]; then
echo "Release PR #$RELEASE_PR must be develop -> main." >&2
exit 1
fi

if [ "$head_repo" != "$REPOSITORY" ]; then
echo "Release PR #$RELEASE_PR must use this repository's develop branch." >&2
exit 1
fi

develop_sha="$(git rev-parse origin/develop)"
main_sha="$(git rev-parse origin/main)"

if [ "$head_sha" != "$develop_sha" ]; then
echo "Release PR head ($head_sha) does not match current develop ($develop_sha)." >&2
echo "Refresh or recreate the release PR before promoting." >&2
exit 1
fi

if ! git merge-base --is-ancestor "$main_sha" "$develop_sha"; then
echo "main is not an ancestor of develop; fast-forward promotion is unsafe." >&2
echo "Reconcile develop with main before retrying." >&2
exit 1
fi

if [ "$main_sha" = "$develop_sha" ]; then
echo "main already points to the current develop commit." >&2
exit 1
fi

echo "develop_sha=$develop_sha" >> "$GITHUB_OUTPUT"
echo "main_sha=$main_sha" >> "$GITHUB_OUTPUT"

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 11.9.0

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm

- name: Checkout release candidate
run: git checkout --detach "${{ steps.release.outputs.develop_sha }}"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Validate release candidate
run: pnpm check:build

- name: Recheck develop and fast-forward main
env:
EXPECTED_DEVELOP_SHA: ${{ steps.release.outputs.develop_sha }}
shell: bash
run: |
set -euo pipefail

git fetch origin main develop --no-tags
current_develop_sha="$(git rev-parse origin/develop)"
current_main_sha="$(git rev-parse origin/main)"

if [ "$current_develop_sha" != "$EXPECTED_DEVELOP_SHA" ]; then
echo "develop changed while the release was being validated; aborting." >&2
exit 1
fi

if ! git merge-base --is-ancestor "$current_main_sha" "$EXPECTED_DEVELOP_SHA"; then
echo "main changed and is no longer an ancestor of the validated develop commit; aborting." >&2
exit 1
fi

git push origin "$EXPECTED_DEVELOP_SHA:refs/heads/main"

- name: Publish summary
run: |
{
echo "## Promotion complete"
echo
echo "- release PR: #${{ inputs.release_pr }}"
echo "- main: \`${{ steps.release.outputs.main_sha }}\` -> \`${{ steps.release.outputs.develop_sha }}\`"
echo "- method: fast-forward"
} >> "$GITHUB_STEP_SUMMARY"
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,25 +81,30 @@ For application behavior, runtime errors, feature requests, or security issues,

## Branches and Publishing

This repository uses the following publishing flow:
`main` is the stable/default branch and `develop` is the integration and preview branch. The repository keeps a linear history: `main` is always equal to, or an ancestor of, `develop`.

```text
feature branch
develop
GitHub Pages development preview
main
production documentation site
│ Pull Request (rebase or squash)
develop ── development preview
│ develop -> main release PR (review only)
│ CI + preview review
Promote develop to main workflow
│ fast-forward
main ── production documentation
```

- `develop`: receives documentation changes and publishes the development preview;
- `main`: contains reviewed documentation ready for production;
- `develop`: receives reviewed documentation changes and publishes the development preview;
- `main`: points to the latest released commit and publishes the production documentation;
- feature branches: contain changes for a specific project, topic, or documentation batch.

Documentation changes should normally merge into `develop` first. After reviewing the preview, they can be promoted to `main`.
Normal changes should go through a Pull Request into `develop`. After the preview is accepted, open a `develop -> main` release PR to review the exact production diff. Do not merge that release PR with GitHub's merge buttons. Instead, run **Actions → Promote develop to main**, enter the release PR number, and let the workflow validate and fast-forward `main` to the reviewed `develop` commit.

The promotion workflow aborts if the release PR does not target `main` from `develop`, if `develop` changes during validation, or if `main` is no longer an ancestor of `develop`.

## Local Development

Expand Down
29 changes: 17 additions & 12 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,25 +79,30 @@ docs/.vitepress/navigation.ts

## 分支与发布

本仓库采用以下分支流程:
`main` 是稳定/默认分支,`develop` 是集成与预览分支。仓库保持线性历史:`main` 始终与 `develop` 相同,或是 `develop` 的祖先提交。

```text
功能分支
develop
GitHub Pages 开发预览
main
正式文档站
│ Pull Request(rebase 或 squash)
develop ── 开发预览
│ develop -> main 发布 PR(仅用于审阅)
│ CI + 预览确认
Promote develop to main workflow
│ fast-forward
main ── 正式文档站
```

* `develop`:接收文档变更并生成开发预览
* `main`:保存已经确认并准备正式发布的文档
* `develop`:接收已经审阅的文档变更并生成开发预览
* `main`:指向最近一次正式发布的提交并发布正式文档
* 功能分支:用于编写单个项目、主题或批次的文档变更。

文档变更应优先合并到 `develop`,确认预览效果后再同步到 `main`。
普通文档变更应通过 Pull Request 合入 `develop`。确认开发预览后,创建 `develop -> main` 发布 PR,用于查看正式发布的完整差异。该发布 PR **不要使用 GitHub 的 Merge / Rebase / Squash 按钮合并**,而是在 **Actions → Promote develop to main** 中输入发布 PR 编号,由工作流完成校验并将 `main` fast-forward 到已经审阅的 `develop` 提交。

如果发布 PR 不是 `develop -> main`、校验期间 `develop` 发生变化,或者 `main` 已不再是 `develop` 的祖先,发布工作流会直接终止。

## 本地开发

Expand Down
Loading