diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..75c6749 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,132 @@ +name: Development CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: rawmark-development + cancel-in-progress: false + +jobs: + check-and-package: + runs-on: ubuntu-latest + timeout-minutes: 55 + env: + CGO_ENABLED: '0' + steps: + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache: false + - name: Install frontend dependencies + run: | + corepack enable + corepack prepare pnpm@12.11.2 --activate + pnpm -C web install --frozen-lockfile --registry=https://registry.npmjs.org + - name: Check and build frontend + run: | + pnpm -C web lint:check + pnpm -C web typecheck + pnpm -C web test + pnpm -C web build + pnpm -C web build:demo + - name: Check backend + run: | + go vet -tags dev ./... + go test ./internal/... + go test -tags dev ./web ./cmd/rawmark + go vet ./... + go test ./web ./cmd/rawmark + - name: Test filesystem permissions as an unprivileged user + run: | + for package in vault watch; do + go test -c -o "/tmp/rawmark-$package.test" "./internal/$package" + (cd "internal/$package" && runuser -u node -- "/tmp/rawmark-$package.test" -test.v) + done + - name: Build platform packages and smoke-test Linux amd64 + run: | + mkdir -p package/licenses downloads + cp LICENSE package/ + cp web/LICENSE package/licenses/vue3-starter-MIT.txt + cp web/src/common/appearance/fonts/*-OFL.txt package/licenses/ + cp web/src/common/appearance/fonts/maple/LICENSE.txt package/licenses/Maple-Mono-OFL.txt + for target in linux/amd64 linux/arm64 windows/amd64 darwin/arm64 darwin/amd64; do + export GOOS="${target%/*}" GOARCH="${target#*/}" + extension='' + if [ "$GOOS" = windows ]; then extension='.exe'; fi + rm -f package/rawmark package/rawmark.exe + go vet ./... + go build -trimpath -ldflags="-s -w -X main.releaseVersion=$(cat VERSION)" -o "package/rawmark$extension" ./cmd/rawmark + if [ "$target" = linux/amd64 ]; then + python3 scripts/smoke-release.py package/rawmark + fi + python3 scripts/package-release.py "$GOOS" "$GOARCH" + done + - name: Check upload sizes and generate checksums + run: | + python3 - <<'PY' + import hashlib + from pathlib import Path + + archives = sorted(Path('downloads').glob('rawmark_*')) + if len(archives) != 5: + raise SystemExit(f'Expected five platform packages, found {len(archives)}') + checksums = [] + for archive in archives: + size = archive.stat().st_size + print(f'{archive.name}: {size:,} bytes') + if size >= 90_000_000: + raise SystemExit(f'{archive.name} exceeds the 90 MB upload budget (Cloudflare limit: 100 MB)') + checksums.append(f'{hashlib.file_digest(archive.open("rb"), "sha256").hexdigest()} {archive.name}\n') + Path('downloads/SHA256SUMS').write_text(''.join(checksums)) + PY + - uses: https://github.com/actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: linux-amd64 + path: downloads/*_linux_amd64.tar.gz + compression-level: 0 + if-no-files-found: error + retention-days: 7 + - uses: https://github.com/actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: linux-arm64 + path: downloads/*_linux_arm64.tar.gz + compression-level: 0 + if-no-files-found: error + retention-days: 7 + - uses: https://github.com/actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: windows-amd64 + path: downloads/*_windows_amd64.zip + compression-level: 0 + if-no-files-found: error + retention-days: 7 + - uses: https://github.com/actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: darwin-arm64 + path: downloads/*_darwin_arm64.tar.gz + compression-level: 0 + if-no-files-found: error + retention-days: 7 + - uses: https://github.com/actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: darwin-amd64 + path: downloads/*_darwin_amd64.tar.gz + compression-level: 0 + if-no-files-found: error + retention-days: 7 + - uses: https://github.com/actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: checksums + path: downloads/SHA256SUMS + if-no-files-found: error + retention-days: 7 diff --git a/docs/development.md b/docs/development.md index a498b98..bc3df50 100644 --- a/docs/development.md +++ b/docs/development.md @@ -108,11 +108,21 @@ Account language reuses the settings store's serial writes, server settings, and Backend `internal/i18n/` preserves message IDs and parameters. Errors render at the HTTP boundary according to `Accept-Language`, retaining `errors.Is/As` and original diagnostics; logs keep their existing language. Existing index warnings are reanalyzed from source notes only when needed for translation, refreshing the entry's complete metadata and revision without changing the index format. Stale entries for deleted or moved notes are skipped; other read errors fail explicitly. Add both languages and interpolation parameters for new messages and run the corresponding dictionary-completeness, settings-sync, editor, and API tests. +## Development CI and release promotion + +Gitea is the development remote (`origin`); GitHub is the publication remote (`github`). Normal `git push origin main` runs `.gitea/workflows/ci.yml`. There is no automatic mirror to GitHub and no release or deployment step in development CI. The private runner executes one job at a time; keep its capacity at one (and never above two for this project). + +The Linux runner checks lint, types, frontend tests, production/demo builds, Go vet and tests, filesystem permissions as an unprivileged user, and the Linux amd64 binary lifecycle with disposable data. It cross-compiles and vets Linux amd64/arm64, Windows amd64, and macOS arm64/amd64. Cross-compilation does **not** validate native behavior on Windows, macOS, or Linux arm64; the GitHub release workflow retains native tests. + +Each platform archive is uploaded separately, with checksums and seven-day retention. CI rejects archives of 90,000,000 bytes or more before upload, leaving room below the Cloudflare 100 MB request limit. Do not combine all platforms into one artifact or commit build output. Git's HTTP limit applies to the complete push request too, not just individual files: keep pushes small and do not add large binaries to history. The self-hosted runner connects to Gitea over its configured private Docker network. + +To publish, select a reviewed commit that passed Gitea CI. Update the version and release notes first and validate the resulting commit on Gitea again. After explicit release approval, push that exact commit to GitHub and tag it there; do not publish the moving tip of a newer, unverified branch. GitHub release Actions currently remains manually disabled following the withdrawn releases; enable it only for an explicitly approved release. Production deployment is separately authorized. + ## Version releases and the public demo `VERSION` is the release-version source. Docker builds and release binaries inject it into `--version`; Vite uses it for the settings display. The existing content-derived PWA build hash remains independent. -For a release, update `VERSION`, `.github/release-notes.md`, and the paired installation examples; run the required checks, review and commit the changes, push `main`, then push the matching `vX.Y.Z` tag. `.github/workflows/release.yml` is the only release workflow: ordinary branch pushes do not run it, and publishing its Release does not start another workflow. Release runs are globally serialized, with at most two native matrix jobs at once. Jobs check the project, test native Linux amd64/arm64, Windows amd64 and macOS amd64/arm64 binaries, package those same binaries into a multi-platform image, deploy the demo, and publish the Release and stable image tags. Rerun failed jobs in the existing run when necessary; do not move published tags or overwrite release assets. +For a release, update `VERSION`, `.github/release-notes.md`, and the paired installation examples; run the required checks, review and commit the changes, and pass Gitea CI. After release approval, push the selected `main` commit to the `github` remote, then push its matching `vX.Y.Z` tag to `github`. `.github/workflows/release.yml` is the only release workflow: ordinary branch pushes do not run it, and publishing its Release does not start another workflow. Release runs are globally serialized, with at most two native matrix jobs at once. Jobs check the project, test native Linux amd64/arm64, Windows amd64 and macOS amd64/arm64 binaries, package those same binaries into a multi-platform image, deploy the demo, and publish the Release and stable image tags. Rerun failed jobs in the existing run when necessary; do not move published tags or overwrite release assets. GitHub Pages must use the GitHub Actions publishing source, with the `github-pages` environment allowing release tags. The first GHCR package must be made public once in its package settings; verify anonymous pulls before announcing the release. Runtime production deployment is separate from publishing a version. diff --git a/docs/development.zh-CN.md b/docs/development.zh-CN.md index ea43d78..6c442b3 100644 --- a/docs/development.zh-CN.md +++ b/docs/development.zh-CN.md @@ -108,11 +108,21 @@ python3 scripts/prepare-maple-font.py /path/to/MapleMonoNormalNL-NF-CN.zip 后端 `internal/i18n/` 保存消息 ID 与参数,错误在 HTTP 边界根据 `Accept-Language` 渲染,并保留 `errors.Is/As` 及原始诊断;日志遵循原有语言。既有索引警告只在需要时重新分析源笔记生成译文,同时更新该条目的完整元数据和版本,不改索引格式;已删除或移动的过期条目跳过,其他读取错误明确返回。新增文案同时补充中英字典与参数,运行字典完整性、设置同步、编辑器和 API 的相应测试。 +## 开发 CI 与发布晋级 + +Gitea 为开发远端(`origin`),GitHub 为发布远端(`github`)。日常 `git push origin main` 触发 `.gitea/workflows/ci.yml`;不配置 GitHub 自动镜像,开发 CI 不发布版本或部署服务。私有 runner 每次执行一个 job,保持容量为一(本项目最多不能超过两个)。 + +Linux runner 执行 lint、类型检查、前端测试、正式与 Demo 构建、Go vet 和测试、普通用户文件权限测试,以及使用临时数据的 Linux amd64 程序生命周期测试;交叉编译并 vet Linux amd64/arm64、Windows amd64、macOS arm64/amd64。交叉编译**不代表**已验证 Windows、macOS 或 Linux arm64 的原生行为,GitHub 发布流程仍保留原生测试。 + +每个平台独立上传压缩包,附校验和,保留七天。上传前拒绝大于等于 90,000,000 字节的包,为 Cloudflare 的 100 MB 请求限制预留余量;不将五平台合成一个产物,不将构建输出提交到 Git。Git HTTP 限制同样针对整个 push 请求,而非只针对单个文件:保持小批量推送,不将大二进制文件写入历史。自托管 runner 通过已配置的 Docker 私有网络连接 Gitea。 + +发布时选择已 review 且通过 Gitea CI 的 commit。先完成版本与发布说明调整,再在 Gitea 验证最终提交。获得明确发布授权后,将该提交同步至 GitHub,并为该提交推送版本标签,不能直接发布后来移动且未验证的分支顶端。此前版本撤销后,GitHub Release Actions 仍处于手动禁用状态;仅在获准发布时恢复。生产部署另行授权。 + ## 版本发布与公开 Demo `VERSION` 是发布版本的唯一来源。Docker 构建和发布二进制将其写入 `--version`,Vite 用于设置页面展示;既有 PWA 内容哈希保持独立。 -发布时更新 `VERSION`、`.github/release-notes.md` 和双语安装示例,完成检查、review 和提交后 push `main`,再推送对应的 `vX.Y.Z` 标签。`.github/workflows/release.yml` 是唯一发布工作流:普通分支推送不触发,创建 Release 也不启动第二个工作流。发布运行全局串行,原生平台矩阵最多同时执行两个 job。同一次运行完成检查、Linux amd64/arm64、Windows amd64 与 macOS amd64/arm64 原生程序测试、以相同程序封装双架构镜像、Demo 部署及 Release 和稳定镜像标签发布。失败时在原 run 中重跑失败 job,不移动已发布标签或覆盖发布附件。 +发布时更新 `VERSION`、`.github/release-notes.md` 和双语安装示例,完成检查、review、提交并通过 Gitea CI。明确获准发布后,将选定的 `main` 提交推送至 `github` 远端,再将该提交对应的 `vX.Y.Z` 标签推送至 `github`。`.github/workflows/release.yml` 是唯一发布工作流:普通分支推送不触发,创建 Release 也不启动第二个工作流。发布运行全局串行,原生平台矩阵最多同时执行两个 job。同一次运行完成检查、Linux amd64/arm64、Windows amd64 与 macOS amd64/arm64 原生程序测试、以相同程序封装双架构镜像、Demo 部署及 Release 和稳定镜像标签发布。失败时在原 run 中重跑失败 job,不移动已发布标签或覆盖发布附件。 GitHub Pages 必须选择 GitHub Actions 作为发布来源,`github-pages` 环境需要允许版本标签部署。首次 GHCR 镜像需在包设置中将可见性改为 Public,公告前验证匿名拉取。生产实例部署与版本发布是不同操作。