Compare commits
5
Commits
b65c680595
...
67b8e64d6a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67b8e64d6a | ||
|
|
4607da1a04 | ||
|
|
afb959f02a | ||
|
|
469af15045 | ||
|
|
da4b1e3b18 |
No files matched your search
@@ -2,8 +2,10 @@ name: Development CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [dev]
|
||||||
pull_request:
|
|
||||||
|
env:
|
||||||
|
RAWMARK_BUILD_CHANNEL: development
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -28,7 +30,7 @@ jobs:
|
|||||||
cache: false
|
cache: false
|
||||||
- name: Check CI behavior and project
|
- name: Check CI behavior and project
|
||||||
run: |
|
run: |
|
||||||
python3 -m unittest discover -s scripts/tests -v
|
env -u RAWMARK_BUILD_CHANNEL python3 -m unittest discover -s scripts/tests -v
|
||||||
sh scripts/ci-check.sh development
|
sh scripts/ci-check.sh development
|
||||||
- name: Package and smoke-test Linux amd64
|
- name: Package and smoke-test Linux amd64
|
||||||
run: sh scripts/ci-package.sh development
|
run: sh scripts/ci-package.sh development
|
||||||
@@ -50,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
image:
|
image:
|
||||||
needs: check-and-package
|
needs: check-and-package
|
||||||
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
|
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/dev'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
@@ -85,7 +87,7 @@ jobs:
|
|||||||
python3 scripts/release.py prepare-image
|
python3 scripts/release.py prepare-image
|
||||||
docker build --provenance=false --build-arg TARGETARCH=amd64 --tag "$TEMP_IMAGE" \
|
docker build --provenance=false --build-arg TARGETARCH=amd64 --tag "$TEMP_IMAGE" \
|
||||||
--label "org.opencontainers.image.revision=${{ gitea.sha }}" \
|
--label "org.opencontainers.image.revision=${{ gitea.sha }}" \
|
||||||
--label "org.opencontainers.image.version=$(cat VERSION)" \
|
--label "org.opencontainers.image.version=$(python3 scripts/release.py version)" \
|
||||||
-f deploy/Dockerfile.release image
|
-f deploy/Dockerfile.release image
|
||||||
python3 scripts/smoke-docker.py "$TEMP_IMAGE" --client-container "$(hostname)" \
|
python3 scripts/smoke-docker.py "$TEMP_IMAGE" --client-container "$(hostname)" \
|
||||||
--resource-label "$CI_RESOURCE_LABEL"
|
--resource-label "$CI_RESOURCE_LABEL"
|
||||||
@@ -106,13 +108,13 @@ jobs:
|
|||||||
|
|
||||||
deploy-development:
|
deploy-development:
|
||||||
needs: image
|
needs: image
|
||||||
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
|
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/dev'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
container:
|
container:
|
||||||
image: docker:29-cli
|
image: docker:29-cli
|
||||||
volumes:
|
volumes:
|
||||||
- /docker/gitea/dev/rawmark:/docker/gitea/dev/rawmark
|
- /docker/gitea/dev/RawMark:/docker/gitea/dev/RawMark
|
||||||
env:
|
env:
|
||||||
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
|
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
|
||||||
GITEA_RELEASE_TOKEN: ${{ secrets.RAWMARK_GITEA_TOKEN }}
|
GITEA_RELEASE_TOKEN: ${{ secrets.RAWMARK_GITEA_TOKEN }}
|
||||||
@@ -135,7 +137,7 @@ jobs:
|
|||||||
mkdir -p "$DOCKER_CONFIG"
|
mkdir -p "$DOCKER_CONFIG"
|
||||||
chmod 700 "$DOCKER_CONFIG"
|
chmod 700 "$DOCKER_CONFIG"
|
||||||
printf '%s' "$GITEA_RELEASE_TOKEN" | docker login git.134333.xyz --username eezd --password-stdin
|
printf '%s' "$GITEA_RELEASE_TOKEN" | docker login git.134333.xyz --username eezd --password-stdin
|
||||||
python3 scripts/deploy.py development --directory /docker/gitea/dev/rawmark \
|
python3 scripts/deploy.py development --directory /docker/gitea/dev/RawMark \
|
||||||
--image "$DEPLOY_IMAGE" --commit "$DEPLOY_COMMIT" --version "$DEPLOY_VERSION" \
|
--image "$DEPLOY_IMAGE" --commit "$DEPLOY_COMMIT" --version "$DEPLOY_VERSION" \
|
||||||
--client-container "$(hostname)"
|
--client-container "$(hostname)"
|
||||||
- name: Remove job credentials
|
- name: Remove job credentials
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
name: Main CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
RAWMARK_BUILD_CHANNEL: development
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: rawmark-main
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-and-package:
|
||||||
|
if: gitea.event_name != 'pull_request' || gitea.event.pull_request.head.repo.full_name == gitea.repository
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 55
|
||||||
|
env:
|
||||||
|
CI_RUN_ID: ${{ gitea.run_id }}
|
||||||
|
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: Check CI behavior and project
|
||||||
|
run: |
|
||||||
|
env -u RAWMARK_BUILD_CHANNEL python3 -m unittest discover -s scripts/tests -v
|
||||||
|
sh scripts/ci-check.sh development
|
||||||
|
- name: Package and smoke-test Linux amd64
|
||||||
|
run: sh scripts/ci-package.sh development
|
||||||
|
- 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: checksums
|
||||||
|
path: |
|
||||||
|
downloads/SHA256SUMS
|
||||||
|
downloads/build.json
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
image:
|
||||||
|
needs: check-and-package
|
||||||
|
runs-on: docker
|
||||||
|
timeout-minutes: 20
|
||||||
|
env:
|
||||||
|
CI_RUN_ID: ${{ gitea.run_id }}
|
||||||
|
CI_RUN_ATTEMPT: ${{ gitea.run_attempt }}
|
||||||
|
CI_RESOURCE_LABEL: io.rawmark.ci.run=dev-${{ gitea.run_id }}-${{ gitea.run_attempt }}
|
||||||
|
TEMP_IMAGE: rawmark-ci:${{ gitea.sha }}-${{ gitea.run_id }}-${{ gitea.run_attempt }}
|
||||||
|
DOCKER_CONFIG: /tmp/rawmark-docker-auth
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: sh
|
||||||
|
steps:
|
||||||
|
- name: Install CI runtimes
|
||||||
|
run: apk add --no-cache nodejs python3
|
||||||
|
- uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: https://github.com/actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||||
|
with:
|
||||||
|
name: linux-amd64
|
||||||
|
path: downloads
|
||||||
|
- uses: https://github.com/actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||||
|
with:
|
||||||
|
name: checksums
|
||||||
|
path: downloads
|
||||||
|
- name: Verify, build and smoke-test the exact binary
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
python3 scripts/release.py prepare-image
|
||||||
|
docker build --provenance=false --build-arg TARGETARCH=amd64 --tag "$TEMP_IMAGE" \
|
||||||
|
--label "org.opencontainers.image.revision=${{ gitea.sha }}" \
|
||||||
|
--label "org.opencontainers.image.version=$(python3 scripts/release.py version)" \
|
||||||
|
-f deploy/Dockerfile.release image
|
||||||
|
python3 scripts/smoke-docker.py "$TEMP_IMAGE" --client-container "$(hostname)" \
|
||||||
|
--resource-label "$CI_RESOURCE_LABEL"
|
||||||
|
- name: Clean this attempt's resources
|
||||||
|
if: always()
|
||||||
|
run: python3 scripts/ci-cleanup.py "$CI_RESOURCE_LABEL" --client-container "$(hostname)" --image "$TEMP_IMAGE"
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: GitHub Git mirror
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['**']
|
||||||
|
tags: ['**']
|
||||||
|
create:
|
||||||
|
delete:
|
||||||
|
schedule:
|
||||||
|
- cron: '17 * * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
concurrency:
|
||||||
|
group: rawmark-github-mirror
|
||||||
|
cancel-in-progress: false
|
||||||
|
jobs:
|
||||||
|
mirror:
|
||||||
|
if: vars.RAWMARK_MIRROR_ENABLED == 'true'
|
||||||
|
runs-on: docker
|
||||||
|
timeout-minutes: 15
|
||||||
|
container:
|
||||||
|
image: docker:29-cli
|
||||||
|
volumes:
|
||||||
|
- /docker/gitea/mirrors/RawMark:/docker/gitea/mirrors/RawMark
|
||||||
|
steps:
|
||||||
|
- name: Install mirror runtimes
|
||||||
|
run: apk add --no-cache nodejs python3 git git-lfs
|
||||||
|
- uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Reconcile current source references
|
||||||
|
env:
|
||||||
|
GITHUB_MIRROR_TOKEN: ${{ secrets.RAWMARK_GITHUB_MIRROR_TOKEN }}
|
||||||
|
run: python3 scripts/mirror.py --directory /docker/gitea/mirrors/RawMark
|
||||||
@@ -67,10 +67,14 @@
|
|||||||
|
|
||||||
## 文档与交接
|
## 文档与交接
|
||||||
|
|
||||||
- 文档按任务、行为变化或项目约定维护;注释解释意图与约束,项目指引只记录已确认的事实,不额外生成无关文档或备份。
|
- 文档按任务、行为变化或项目约定按需维护;注释解释意图与约束,项目指引只记录已确认的事实,不额外生成无关文档或备份。
|
||||||
- 仅在已授权执行且交接信息无法从代码、Git 历史或已有文档恢复时,创建或更新 `CONTEXT.md`;简单修改无需记录。
|
- `CONTEXT.md` 是跨会话交接快照,而非变更日志;仅在已授权执行且关键信息无法从代码、Git 历史或已有文档恢复时创建或更新,简单修改无需记录。
|
||||||
- 交接内容原位更新,保留有效目标、决策及理由、未完成事项和风险,标注未验证信息,删除过时状态;当前状态不超过 100 行,全文不超过 500 行。
|
- 新会话接续任务时,按需读取 `CONTEXT.md` 及其引用的关键文件,并结合代码和 Git 状态核实,不将历史记录直接视为当前事实。
|
||||||
- 保留 `CONTEXT.md` 原有跟踪策略:本地专用文件继续忽略,不擅自停止跟踪已纳入版本控制的文件。
|
- 交接内容保留当前目标与进度、关键决策及理由、已否决方案及原因、用户约束、环境注意事项、未解决风险和下一步;已完成里程碑仅保留必要摘要,不重复记录实现细节和变更历史。
|
||||||
|
- 内容应准确、自包含;必要的路径、命令、报错和配置值保留原文,无法核实的信息标注 `[未核实]`,不得记录密码、密钥或 Token。
|
||||||
|
- 目标、进度、决策或风险发生重要变化时,按需原位更新;及时清理过时状态、已完成事项和已解决风险,避免内容重复或相互矛盾。
|
||||||
|
- 当前状态不超过 100 行,全文不超过 500 行;超限时优先压缩历史记录,长期有效的项目规则归入 `AGENTS.md`,详细设计归入现有文档。
|
||||||
|
- 保留 `CONTEXT.md` 原有 Git 跟踪策略:本地专用文件继续忽略,不擅自停止跟踪已纳入版本控制的文件。
|
||||||
|
|
||||||
## 项目定位与工具链
|
## 项目定位与工具链
|
||||||
|
|
||||||
@@ -125,6 +129,12 @@ pnpm -C web build
|
|||||||
|
|
||||||
## CI/CD
|
## CI/CD
|
||||||
|
|
||||||
Gitea 为主仓库和构建执行方;日常 main push 仅构建 Linux amd64,通过隔离验收后发布开发镜像并更新独立开发实例。开发实例目录为 `/docker/gitea/dev/rawmark`,开发应用容器名为 `rawmark-dev`,生产实例目录为 `/docker/main/rawmark`。正式发布只从 Gitea 手动入口选择已通过开发 CI 的提交,构建五平台包及 Linux 双架构镜像,同步同一版本至 GitHub/GHCR;GitHub 不执行项目构建,仅允许 Pages 平台部署预构建 Demo。生产仅手动选择完整发布版本并固定 GHCR 摘要,不能随开发推送更新。
|
Gitea 为公开主仓库,GitHub 为单向公开镜像;默认分支均为 main,日常开发使用 dev。
|
||||||
|
|
||||||
runner 保持容量一,最多两个任务同时运行;当前宿主 Docker 执行器只接收受信代码。临时资源按任务标识清理,不全局 prune,不删除部署容器。CI 行为测试为 `python3 -m unittest discover -s scripts/tests -v`;项目检查与打包入口为 `sh scripts/ci-check.sh development|release`、`sh scripts/ci-package.sh development|release`,仅在 CI 工具链容器执行。正式版本、凭据、上传限制、部署目录与回退步骤见双语开发和部署文档。
|
- dev push:容器内检查、Linux amd64 打包与隔离冒烟,通过后发布 `git.134333.xyz/eezd/dev/rawmark:YYYYMMDD-<8位SHA>`,按摘要部署 DEV。日期取提交时间 UTC,完整 SHA 保留在回执;同名异提交拒绝,已验证镜像重试复用。
|
||||||
|
- main push:独立检查实际合并提交、Linux amd64 构建及容器冒烟,不发布镜像、不部署。仓库内受信 PR 只检查;外部 fork PR 不自动进入共享 Docker runner,不注入发布凭据。
|
||||||
|
- DEV 为 `/docker/gitea/dev/RawMark`,容器 `RawMark-dev`,Compose 项目 `rawmark-dev`,监听 `127.0.0.1:21001`,域名 `raw-dev.134333.xyz`。数据独立;所有部署共用宿主实例 `.deploy.lock`,锁内核对 dev 最新 SHA,pending 未处理时拒绝覆盖。已有数据即使无旧容器也必须备份。
|
||||||
|
- Git 镜像独立于 CI 成败:push/create/delete、每小时对账和手动重试,共用 `/docker/gitea/mirrors/RawMark/.mirror.lock`,仅同步业务分支和 TAG 及完整历史,跟随源端删除。同步代码从 main 读取;`RAWMARK_MIRROR_ENABLED=true` 后启用,凭据为 `RAWMARK_GITHUB_MIRROR_TOKEN`。DEV 镜像不上传 GHCR。main 切换前保持该变量关闭。
|
||||||
|
- CI 凭据 `RAWMARK_GITEA_TOKEN` 需要仓库和 Packages 写权限。runner 保持容量一、`sweep: false`,只运行受信代码;工具链安装在任务容器。资源按任务标签清理,禁止全局 prune。
|
||||||
|
- 开发构建通过 `RAWMARK_BUILD_CHANNEL=development` 派生版本,前端与二进制一致;正式 `VERSION` 不因开发构建改变。开发包为 `RawMark_<版本>_linux_amd64.tar.gz`;历史正式产物不改名。安装包及单次传输保持既有 90,000,000 字节预算。
|
||||||
|
- 正式发布与生产入口暂时禁用,后续由 gitea-release 适配,不能绕过 Git 镜像锁直接写 GitHub 分支。生产保留现有目录和数据,不随开发推送更新;GitHub 旧 Release workflow 保持禁用,Pages Demo 不切换。
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
name: ${RAWMARK_PROJECT:?Set RAWMARK_PROJECT}
|
name: ${RAWMARK_PROJECT:?Set RAWMARK_PROJECT}
|
||||||
services:
|
services:
|
||||||
rawmark:
|
rawmark:
|
||||||
container_name: ${RAWMARK_PROJECT:?Set RAWMARK_PROJECT}
|
container_name: ${RAWMARK_CONTAINER_NAME:?Set RAWMARK_CONTAINER_NAME}
|
||||||
image: ${RAWMARK_IMAGE:?Set a verified registry image digest}
|
image: ${RAWMARK_IMAGE:?Set a verified registry image digest}
|
||||||
ports:
|
ports:
|
||||||
- "${RAWMARK_BIND:-127.0.0.1}:${RAWMARK_PORT:?Set RAWMARK_PORT}:8080"
|
- "${RAWMARK_BIND:-127.0.0.1}:${RAWMARK_PORT:?Set RAWMARK_PORT}:8080"
|
||||||
|
|||||||
+7
-5
@@ -160,23 +160,25 @@ The Bearer authentication upgrade requires the frontend and backend to be releas
|
|||||||
- Resetting settings retains the account, resources, plugin storage, and local tabs.
|
- Resetting settings retains the account, resources, plugin storage, and local tabs.
|
||||||
- Logs live in `logs/`, with three rotated files retained, each up to 10 MiB. Settings can export logs and a version summary; this does not reconstruct historical logs.
|
- Logs live in `logs/`, with three rotated files retained, each up to 10 MiB. Settings can export logs and a version summary; this does not reconstruct historical logs.
|
||||||
- If the interface breaks after an update, first check that the frontend and backend come from the same build, then inspect browser network requests and application logs. See the [user guide](guide.md#plugins-and-interface-recovery) for plugin and appearance recovery entry points.
|
- If the interface breaks after an update, first check that the frontend and backend come from the same build, then inspect browser network requests and application logs. See the [user guide](guide.md#plugins-and-interface-recovery) for plugin and appearance recovery entry points.
|
||||||
|
- Note reads send `Cache-Control: no-store, no-transform` so proxies preserve the strong revision ETag used by conditional saves. A proxy rewriting it to a weak ETag can cause otherwise valid edits to return HTTP 412; preserve this response policy through the public ingress.
|
||||||
|
|
||||||
## Managed development and production
|
## Managed development and production
|
||||||
|
|
||||||
The checked-in `deploy/compose.image.yml` is an image-only template. Development lives in `/docker/gitea/dev/rawmark`; production lives in `/docker/main/rawmark`. Copy the template to the instance directory as `compose.yml`, with a private mode-600 `.env` beside it. The container name follows `RAWMARK_PROJECT`, so the development container is `rawmark-dev`. These instances do not depend on a source checkout. Set all required values explicitly:
|
The checked-in `deploy/compose.image.yml` is an image-only template. Development lives in `/docker/gitea/dev/RawMark`; production lives in `/docker/main/rawmark`. Copy the template to the instance directory as `compose.yml`, with a private mode-600 `.env` beside it. Set the project and container names independently. These instances do not depend on a source checkout. Set all required values explicitly:
|
||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
RAWMARK_CHANNEL=development
|
RAWMARK_CHANNEL=development
|
||||||
RAWMARK_PROJECT=rawmark-dev
|
RAWMARK_PROJECT=rawmark-dev
|
||||||
|
RAWMARK_CONTAINER_NAME=RawMark-dev
|
||||||
RAWMARK_BIND=127.0.0.1
|
RAWMARK_BIND=127.0.0.1
|
||||||
RAWMARK_PORT=21001
|
RAWMARK_PORT=21001
|
||||||
RAWMARK_DATA_DIR=/docker/gitea/dev/rawmark/data
|
RAWMARK_DATA_DIR=/docker/gitea/dev/RawMark/data
|
||||||
RAWMARK_IMAGE=git.134333.xyz/eezd/rawmark@sha256:<verified-digest>
|
RAWMARK_IMAGE=git.134333.xyz/eezd/dev/rawmark@sha256:<verified-digest>
|
||||||
```
|
```
|
||||||
|
|
||||||
For production, use channel `production`, project `rawmark`, port 21000, `/docker/main/rawmark/data`, and `ghcr.io/eezd/rawmark@sha256:<verified-digest>`. Preserve the current binding during migration. Create only a new development data directory with owner `65532:65532`; never recursively change existing production ownership or mount the repository example template as live storage.
|
For production, use channel `production`, project `rawmark`, the existing container name, port 21000, `/docker/main/rawmark/data`, and `ghcr.io/eezd/rawmark@sha256:<verified-digest>`. Preserve the current binding during migration. Create only a new development data directory with owner `65532:65532`; never recursively change existing production ownership or mount the repository example template as live storage.
|
||||||
|
|
||||||
Main-push CI automatically updates development. The **Deploy production manually** Gitea workflow only accepts a version whose Gitea and GitHub releases are formal and contain identical build/release receipts and matching tags. It fixes the GHCR digest, checks current mounts and competing writers, stops only this project's old service, and backs up the whole data directory plus configuration before replacement. Backups are private and include SQLite WAL files; old images and backups are retained.
|
Dev-push CI automatically updates development after checks and isolated smoke tests pass. Main pushes and PRs only validate. Formal release and **Deploy production manually** Gitea workflows remain disabled during this CI migration; enabling or executing them requires a separate release plan and authorization. The prepared production workflow requires matching formal Gitea/GitHub releases and receipts, pins the GHCR digest, and backs up data and configuration before replacement. Backups are private and include SQLite WAL files; old images and backups are retained.
|
||||||
|
|
||||||
The job records `deployment.pending.json` before replacement and `deployment.json` after health/frontend checks. If the candidate fails after accessing data, it stops the candidate and retains the backup and pending record for diagnosis; it does not automatically restore private data or run an older program against possibly migrated storage. Recovery requires explicit authorization and compatibility review. If backup fails before replacement, the untouched old service can be restarted safely. No global Docker cleanup is performed.
|
The job records `deployment.pending.json` before replacement and `deployment.json` after health/frontend checks. If the candidate fails after accessing data, it stops the candidate and retains the backup and pending record for diagnosis; it does not automatically restore private data or run an older program against possibly migrated storage. Recovery requires explicit authorization and compatibility review. If backup fails before replacement, the untouched old service can be restarted safely. No global Docker cleanup is performed.
|
||||||
|
|
||||||
|
|||||||
@@ -160,23 +160,25 @@ Bearer 认证升级需要前后端一起发布。旧 Cookie 页面不能继续
|
|||||||
- 重置设置保留账户、资源、插件存储和本机标签页。
|
- 重置设置保留账户、资源、插件存储和本机标签页。
|
||||||
- 日志位于 `logs/`,轮转保留三份,每份至多 10 MiB;可在设置中导出日志及版本摘要。该功能不补录历史日志。
|
- 日志位于 `logs/`,轮转保留三份,每份至多 10 MiB;可在设置中导出日志及版本摘要。该功能不补录历史日志。
|
||||||
- 更新后界面异常先核对前后端是否来自同一构建,再查看浏览器网络请求和应用日志;插件与外观恢复入口见[使用指南](guide.zh-CN.md#插件与界面恢复)。
|
- 更新后界面异常先核对前后端是否来自同一构建,再查看浏览器网络请求和应用日志;插件与外观恢复入口见[使用指南](guide.zh-CN.md#插件与界面恢复)。
|
||||||
|
- 笔记读取返回 `Cache-Control: no-store, no-transform`,防止代理改写条件保存使用的强版本 ETag。代理将其改为弱 ETag 可能导致正常编辑返回 HTTP 412;公网入口须保留该响应策略。
|
||||||
|
|
||||||
## 开发与生产托管
|
## 开发与生产托管
|
||||||
|
|
||||||
仓库 `deploy/compose.image.yml` 是仅拉镜像的模板。开发实例放在 `/docker/gitea/dev/rawmark`,生产实例放在 `/docker/main/rawmark`;将模板复制至对应实例目录并命名为 `compose.yml`,旁边放权限 600 的私有 `.env`。容器名跟随 `RAWMARK_PROJECT`,开发容器为 `rawmark-dev`。实例不依赖源码工作目录,必须明确填写:
|
仓库 `deploy/compose.image.yml` 是仅拉镜像的模板。开发实例放在 `/docker/gitea/dev/RawMark`,生产实例放在 `/docker/main/rawmark`;将模板复制至对应实例目录并命名为 `compose.yml`,旁边放权限 600 的私有 `.env`。项目名与容器名分别指定。实例不依赖源码工作目录,必须明确填写:
|
||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
RAWMARK_CHANNEL=development
|
RAWMARK_CHANNEL=development
|
||||||
RAWMARK_PROJECT=rawmark-dev
|
RAWMARK_PROJECT=rawmark-dev
|
||||||
|
RAWMARK_CONTAINER_NAME=RawMark-dev
|
||||||
RAWMARK_BIND=127.0.0.1
|
RAWMARK_BIND=127.0.0.1
|
||||||
RAWMARK_PORT=21001
|
RAWMARK_PORT=21001
|
||||||
RAWMARK_DATA_DIR=/docker/gitea/dev/rawmark/data
|
RAWMARK_DATA_DIR=/docker/gitea/dev/RawMark/data
|
||||||
RAWMARK_IMAGE=git.134333.xyz/eezd/rawmark@sha256:<已验证摘要>
|
RAWMARK_IMAGE=git.134333.xyz/eezd/dev/rawmark@sha256:<已验证摘要>
|
||||||
```
|
```
|
||||||
|
|
||||||
生产使用 channel `production`、project `rawmark`、端口 21000、`/docker/main/rawmark/data` 和 `ghcr.io/eezd/rawmark@sha256:<已验证摘要>`;迁移时保留当前监听地址。仅为新的开发目录设置 `65532:65532` 所有者,不递归修改现有生产权限,不将仓库示例模板作为运行数据挂载。
|
生产使用 channel `production`、project `rawmark`、现有容器名、端口 21000、`/docker/main/rawmark/data` 和 `ghcr.io/eezd/rawmark@sha256:<已验证摘要>`;迁移时保留当前监听地址。仅为新的开发目录设置 `65532:65532` 所有者,不递归修改现有生产权限,不将仓库示例模板作为运行数据挂载。
|
||||||
|
|
||||||
main push CI 自动更新开发。Gitea **Deploy production manually** 工作流只接受两端均正式发布、构建/发布回执相同且标签一致的版本。它固定 GHCR 摘要,检查原挂载与并发写入实例,仅停止本项目旧服务,备份完整数据目录和配置后替换。备份为私有文件并包含 SQLite WAL;旧镜像和备份继续保留。
|
dev push CI 通过检查和隔离冒烟后自动更新开发;main push 与 PR 仅验证。本次 CI 迁移期间,正式发布及 Gitea **Deploy production manually** 工作流保持禁用,启用或执行须另行规划并授权。已准备的生产工作流要求两端正式发布及回执一致,固定 GHCR 摘要,并在替换前备份数据和配置。备份为私有文件并包含 SQLite WAL;旧镜像和备份继续保留。
|
||||||
|
|
||||||
替换前记录 `deployment.pending.json`,健康和前端检查通过后记录 `deployment.json`。候选版本访问数据后失败则停止候选,保留备份和 pending 记录,不自动恢复私人数据,也不直接用旧程序打开可能迁移后的数据;恢复须明确授权并核对兼容性。备份在替换前失败时,旧数据未被新程序访问,可安全重新启动旧容器。不做全局 Docker 清理。
|
替换前记录 `deployment.pending.json`,健康和前端检查通过后记录 `deployment.json`。候选版本访问数据后失败则停止候选,保留备份和 pending 记录,不自动恢复私人数据,也不直接用旧程序打开可能迁移后的数据;恢复须明确授权并核对兼容性。备份在替换前失败时,旧数据未被新程序访问,可安全重新启动旧容器。不做全局 Docker 清理。
|
||||||
|
|
||||||
|
|||||||
+8
-41
@@ -110,45 +110,12 @@ Backend `internal/i18n/` preserves message IDs and parameters. Errors render at
|
|||||||
|
|
||||||
## Gitea CI/CD
|
## Gitea CI/CD
|
||||||
|
|
||||||
Gitea is the primary repository and build executor (`origin`). GitHub (`github`) receives explicitly published versions; there is no continuous mirror. All project compilation and checks run in CI toolchain containers, without installing Node or Go on the deployment host. The runner has capacity one. Together with the allowed Pages platform deployment, no more than two jobs run at once.
|
Gitea is the public source repository; GitHub is a strict one-way Git mirror. Both use main as the default branch and dev for daily development.
|
||||||
|
|
||||||
### Development
|
- A dev push checks, packages and smoke-tests Linux amd64 in CI containers, publishes only `git.134333.xyz/eezd/dev/rawmark:YYYYMMDD-<8-char-SHA>`, then deploys its verified digest. The date comes from the commit timestamp in UTC. Receipts retain the full SHA; conflicting versions fail and verified images are reused on retry.
|
||||||
|
- A main push independently checks and smoke-tests the actual merged commit without publishing images or deploying. Trusted same-repository PRs only check; external fork PRs do not automatically enter the shared Docker runner or receive publication credentials.
|
||||||
A main push runs `.gitea/workflows/ci.yml`: project checks, CI behavior tests, Linux amd64 packaging, and native lifecycle smoke tests. A dependent Docker job verifies that run's archive and checks the non-root image with synthetic data, including restart/replacement persistence and server-side logout. Only then does it publish `git.134333.xyz/eezd/rawmark:dev-<full-SHA>` and deploy its digest to the independent development instance. PRs only run checks and packaging; they receive no publication credentials and do not deploy. The host-Docker runner is for trusted code, not an untrusted-code sandbox; external PRs require maintainer review before execution.
|
- DEV lives at `/docker/gitea/dev/RawMark`, container `RawMark-dev`, explicit Compose project `rawmark-dev`, loopback port 21001 and domain `raw-dev.134333.xyz`. All deployment entrypoints share the instance `.deploy.lock`; stale dev commits are skipped and unresolved pending deployments block updates. Existing data is backed up even when no old container exists.
|
||||||
|
- Git mirroring runs independently on push/create/delete, hourly reconciliation and manual retry, sharing `/docker/gitea/mirrors/RawMark/.mirror.lock`. Only business branches, tags and complete history are mirrored, including deletions. Control code comes from main. Enable `RAWMARK_MIRROR_ENABLED=true` after the main cutover and configure `RAWMARK_GITHUB_MIRROR_TOKEN` with repository/workflow write access. DEV images never go to GHCR.
|
||||||
Development uses `raw-dev.134333.xyz`, loopback port 21001, and `/docker/gitea/dev/rawmark/data`. Production retains its own data under `/docker/main/rawmark` and port 21000. Before updating development, the deployment lock rechecks main so an old run cannot overwrite a newer deployment.
|
- `RAWMARK_GITEA_TOKEN` requires repository and Packages write access. Keep runner capacity one and sweep disabled; only trusted code runs on this Docker-socket runner. Install toolchains inside CI containers and clean only resources labeled for the current attempt.
|
||||||
|
- `RAWMARK_BUILD_CHANNEL=development` derives a consistent frontend/binary version without editing the formal VERSION file. Development archives use `RawMark_<version>_linux_amd64.tar.gz`; historical release artifacts retain their names. Keep the existing 90,000,000-byte transfer budget.
|
||||||
`sh scripts/ci-check.sh development` and `sh scripts/ci-package.sh development` are the CI entry points. The `release` argument enables the Demo and five-platform packaging. Run `python3 -m unittest discover -s scripts/tests -v` for artifact and deployment behavior tests. Do not rebuild these artifacts on the deployment host.
|
- Formal release and production workflows are temporarily disabled pending gitea-release adaptation; future publication must share Git mirror serialization. Production data and paths remain unchanged. Keep the old GitHub Release workflow disabled and preserve the current Pages Demo.
|
||||||
|
|
||||||
### Formal releases
|
|
||||||
|
|
||||||
Update `VERSION` and `.github/release-notes.md`, review the commit and pass main-push development CI. In Gitea's **Release** workflow, select main and enter the exact full commit SHA and matching `X.Y.Z` version. Publishing is an explicit action, not a tag-push side effect.
|
|
||||||
|
|
||||||
The same workflow performs checks and packages Linux amd64/arm64, Windows amd64, and Darwin amd64/arm64. Only Linux amd64 is runtime-tested on the available runner. Cross-builds and vet do not replace native Windows, macOS or ARM64 acceptance; the earlier Windows native test failure has not been reverified. Archives include licenses and `SHA256SUMS`. Linux binaries from these exact archives are packaged into one dual-architecture image and copied unchanged to Gitea Registry and GHCR.
|
|
||||||
|
|
||||||
The workflow stages a Gitea draft and immutable `build.json` after artifact uploads. `image.json` records the tested candidate; `release.json` connects the build, image digest and Demo commit. Both final releases contain matching files. On interruption, rerun failed jobs or dispatch the same version/commit: existing receipts and original artifacts are reused. If an original artifact has expired, recovery fails explicitly rather than rebuilding different content. Conflicting tags or attachments fail; published versions are not overwritten. GitHub main advances by normal fast-forward only.
|
|
||||||
|
|
||||||
GitHub publication and Pages validation complete before the Gitea release becomes formal. Cross-service publication is not atomic: a failed run can leave a GitHub release or candidate images visible while Gitea remains a draft. Such a version is not production-eligible. Stable image tags advance only after artifact and public-image checks.
|
|
||||||
|
|
||||||
Each installation archive and Demo artifact must be smaller than 90,000,000 bytes. Upload platforms separately. Check the source Git pack and image blobs too: the Cloudflare 100 MB limit applies to requests, not only individual tracked files. The combined OCI file is local CI scratch space, never a release upload.
|
|
||||||
|
|
||||||
### Credentials and runner
|
|
||||||
|
|
||||||
Configure these repository Actions Secrets in Gitea:
|
|
||||||
|
|
||||||
| Secret | Required access |
|
|
||||||
| --- | --- |
|
|
||||||
| `RAWMARK_GITEA_TOKEN` | Gitea repository releases and Packages write access |
|
|
||||||
| `RAWMARK_GITHUB_TOKEN` | GitHub classic PAT with repository access, `workflow` for workflow-file changes, and `write:packages` |
|
|
||||||
|
|
||||||
Gitea rejects Secret names beginning with `GITEA_` or `GITHUB_`. Tokens do not belong in Git or job output. Job Docker credentials use temporary private directories. The manual production job uses a read-only Gitea job token and anonymous public GitHub/GHCR reads, not the publication PAT.
|
|
||||||
|
|
||||||
The first GHCR package is private by default. Make it public in its package settings and rerun the failed publication job; anonymous pulling is required before completion. The workflow does not claim a repository token automatically includes Packages access.
|
|
||||||
|
|
||||||
The existing runner uses `container.sweep: false` so deployed services survive job completion. Smoke resources carry a run/attempt label and the always-cleanup step removes only those resources. Deployment mounts are allowlisted to the two instance directories. Do not use global Docker prune or delete historical packages. Check cancellation cleanup as well as successful runs.
|
|
||||||
|
|
||||||
### Static Demo
|
|
||||||
|
|
||||||
`pnpm -C web build:demo` generates `web/dist-demo/` with base `/RawMark/`. It contains page-local example edits, no private API, authentication, persistent workspace or Service Worker. Gitea copies this prebuilt output to GitHub's `gh-pages` branch with `.nojekyll`; Pages uses that branch as its publishing source. GitHub still runs its platform Pages deployment, the only allowed GitHub job. It does not compile RawMark. The old GitHub project release workflow is removed.
|
|
||||||
|
|
||||||
Runtime production updates are a separate manual action; see [deployment](deployment.md#managed-development-and-production).
|
|
||||||
@@ -110,45 +110,12 @@ python3 scripts/prepare-maple-font.py /path/to/MapleMonoNormalNL-NF-CN.zip
|
|||||||
|
|
||||||
## Gitea CI/CD
|
## Gitea CI/CD
|
||||||
|
|
||||||
Gitea 是主仓库和构建执行方(`origin`);GitHub(`github`)只接收明确发布的版本,不持续镜像。项目检查和编译全部在 CI 工具链容器执行,不向部署宿主安装 Node 或 Go。runner 容量保持一;加上允许的 Pages 平台部署,同时执行的任务不超过两个。
|
Gitea 为公开主仓库,GitHub 为单向公开镜像;默认分支均为 main,日常开发使用 dev。
|
||||||
|
|
||||||
### 开发流程
|
- dev push:容器内检查、Linux amd64 打包与隔离冒烟,通过后发布 `git.134333.xyz/eezd/dev/rawmark:YYYYMMDD-<8位SHA>`,按摘要部署 DEV。日期取提交时间 UTC,完整 SHA 保留在回执;同名异提交拒绝,已验证镜像重试复用。
|
||||||
|
- main push:独立检查实际合并提交、Linux amd64 构建及容器冒烟,不发布镜像、不部署。仓库内受信 PR 只检查;外部 fork PR 不自动进入共享 Docker runner,不注入发布凭据。
|
||||||
main push 触发 `.gitea/workflows/ci.yml`,执行项目检查、CI 行为测试、Linux amd64 打包和原生生命周期冒烟。后续 Docker job 校验同一运行的包,使用合成数据验证非 root 镜像、重启及重建容器持久化、服务端注销;通过后才发布 `git.134333.xyz/eezd/rawmark:dev-<完整SHA>` 并按摘要更新独立开发实例。PR 仅检查和打包,不注入发布凭据、不部署。宿主 Docker runner 只运行受信代码,并非不可信代码沙箱;外部 PR 运行前须由维护者审查。
|
- DEV 为 `/docker/gitea/dev/RawMark`,容器 `RawMark-dev`,Compose 项目 `rawmark-dev`,监听 `127.0.0.1:21001`,域名 `raw-dev.134333.xyz`。数据独立;所有部署共用宿主实例 `.deploy.lock`,锁内核对 dev 最新 SHA,pending 未处理时拒绝覆盖。已有数据即使无旧容器也必须备份。
|
||||||
|
- Git 镜像独立于 CI 成败:push/create/delete、每小时对账和手动重试,共用 `/docker/gitea/mirrors/RawMark/.mirror.lock`,仅同步业务分支和 TAG 及完整历史,跟随源端删除。同步代码从 main 读取;`RAWMARK_MIRROR_ENABLED=true` 后启用,凭据为 `RAWMARK_GITHUB_MIRROR_TOKEN`。DEV 镜像不上传 GHCR。main 切换前保持该变量关闭。
|
||||||
开发使用 `raw-dev.134333.xyz`、回环端口 21001 和 `/docker/gitea/dev/rawmark/data`;生产保留 `/docker/main/rawmark` 下的独立数据和 21000 端口。部署锁内再次核对 main,旧运行不能覆盖新部署。
|
- CI 凭据 `RAWMARK_GITEA_TOKEN` 需要仓库和 Packages 写权限。runner 保持容量一、`sweep: false`,只运行受信代码;工具链安装在任务容器。资源按任务标签清理,禁止全局 prune。
|
||||||
|
- 开发构建通过 `RAWMARK_BUILD_CHANNEL=development` 派生版本,前端与二进制一致;正式 `VERSION` 不因开发构建改变。开发包为 `RawMark_<版本>_linux_amd64.tar.gz`;历史正式产物不改名。安装包及单次传输保持既有 90,000,000 字节预算。
|
||||||
CI 入口是 `sh scripts/ci-check.sh development` 和 `sh scripts/ci-package.sh development`;参数换为 `release` 才构建 Demo 和五平台程序。`python3 -m unittest discover -s scripts/tests -v` 验证产物与部署行为。部署宿主不重新编译这些产物。
|
- 正式发布与生产入口暂时禁用,后续由 gitea-release 适配,不能绕过 Git 镜像锁直接写 GitHub 分支。生产保留现有目录和数据,不随开发推送更新;GitHub 旧 Release workflow 保持禁用,Pages Demo 不切换。
|
||||||
|
|
||||||
### 正式发布
|
|
||||||
|
|
||||||
先更新 `VERSION` 和 `.github/release-notes.md`,review 提交并通过 main push 开发 CI。进入 Gitea 的 **Release** 工作流,选择 main,输入完整提交 SHA 和对应的 `X.Y.Z` 版本。手动操作才发布,推标签不会自动启动发布。
|
|
||||||
|
|
||||||
同一工作流完成检查并打包 Linux amd64/arm64、Windows amd64、Darwin amd64/arm64。现有 runner 只对 Linux amd64 执行原生运行验收;交叉构建和 vet 不代替 Windows、macOS、ARM64 原生验收,先前 Windows 原生测试失败尚未复验。程序包附许可证和 `SHA256SUMS`。双架构镜像复用这些包中的 Linux 二进制,原样同步到 Gitea Registry 和 GHCR。
|
|
||||||
|
|
||||||
完成产物上传后,工作流准备 Gitea 草稿和不可变的 `build.json`;`image.json` 记录已验证候选镜像,`release.json` 关联构建、镜像摘要和 Demo 提交。两端最终发布文件一致。中断后重跑失败 job 或重新选择相同版本和提交,复用已有回执与原始产物;原产物过期则明确失败,不重新构建另一份内容。标签或附件冲突报错,已发布版本不覆盖。GitHub main 只正常快进。
|
|
||||||
|
|
||||||
GitHub 发布和 Pages 验证完成后,Gitea 才成为正式 Release。跨服务发布不具原子性:失败时可能已有 GitHub Release 或候选镜像,但 Gitea 仍为草稿,该版本不能进入生产。稳定镜像标签只在产物和公开镜像验证后更新。
|
|
||||||
|
|
||||||
每个安装包及 Demo 产物小于 90,000,000 字节,按平台分别上传。同时核对源码 Git pack 和镜像 blob:Cloudflare 100 MB 限制针对请求,而非只针对单个入库文件。合并 OCI 文件只留在 CI 临时空间,不作为发布附件上传。
|
|
||||||
|
|
||||||
### 凭据与 runner
|
|
||||||
|
|
||||||
在 Gitea 仓库 Actions Secrets 中配置:
|
|
||||||
|
|
||||||
| Secret | 所需权限 |
|
|
||||||
| --- | --- |
|
|
||||||
| `RAWMARK_GITEA_TOKEN` | Gitea 仓库 Release 与 Packages 写入 |
|
|
||||||
| `RAWMARK_GITHUB_TOKEN` | GitHub classic PAT,仓库权限、修改工作流文件所需的 `workflow`、`write:packages` |
|
|
||||||
|
|
||||||
Gitea 禁止 Secret 名以 `GITEA_` 或 `GITHUB_` 开头。令牌不进入 Git 或日志;job 的 Docker 凭据使用私有临时目录。手动生产 job 使用只读 Gitea job token 和匿名 GitHub/GHCR 读取,不获取发布 PAT。
|
|
||||||
|
|
||||||
首次创建的 GHCR 包默认私有,需要在包设置中设为公开后重跑失败的发布 job;匿名拉取通过后才能完成发布。不假定仓库凭据天然有 Packages 权限。
|
|
||||||
|
|
||||||
现有 runner 设置 `container.sweep: false`,避免任务结束后清除已部署服务。冒烟资源带 run/attempt 标签,由 always 清理步骤仅回收本次资源;部署挂载仅允许两个实例目录。不执行全局 Docker prune,不删除历史软件包;成功、失败和取消后均核对清理结果。
|
|
||||||
|
|
||||||
### 静态 Demo
|
|
||||||
|
|
||||||
`pnpm -C web build:demo` 生成 `web/dist-demo/`,基础路径为 `/RawMark/`。示例编辑仅在当前页面有效,不使用私有 API、身份、持久化工作区或 Service Worker。Gitea 将预构建产物和 `.nojekyll` 同步至 GitHub `gh-pages`,Pages 从此分支发布。GitHub 仍会执行平台 Pages 部署,这是唯一允许的 GitHub job,不编译 RawMark;旧 GitHub 项目发布工作流已移除。
|
|
||||||
|
|
||||||
生产更新为独立手动操作,见[部署文档](deployment.zh-CN.md#开发与生产托管)。
|
|
||||||
+2
-1
@@ -327,7 +327,8 @@ func (s *Server) readFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
||||||
w.Header().Set("ETag", `"`+rev+`"`)
|
w.Header().Set("ETag", `"`+rev+`"`)
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
// Preserve the strong revision ETag through compression proxies for If-Match writes.
|
||||||
|
w.Header().Set("Cache-Control", "no-store, no-transform")
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,23 @@ type fixture struct {
|
|||||||
admin *http.Client
|
admin *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoteRevisionSurvivesProxyBoundary(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
f.write("proxy.md", "original")
|
||||||
|
res := f.do(f.admin, "GET", "/api/v1/files/proxy.md", "", nil)
|
||||||
|
defer res.Body.Close()
|
||||||
|
etag := res.Header.Get("ETag")
|
||||||
|
if res.StatusCode != http.StatusOK || res.Header.Get("Cache-Control") != "no-store, no-transform" || !strings.HasPrefix(etag, `"`) {
|
||||||
|
t.Fatalf("笔记响应必须保留强版本标识并禁止代理转换: %d %v", res.StatusCode, res.Header)
|
||||||
|
}
|
||||||
|
if code := f.status(f.admin, "PUT", "/api/v1/files/proxy.md", "updated", map[string]string{"If-Match": etag}); code != http.StatusOK {
|
||||||
|
t.Fatalf("读取的版本标识应允许条件保存: %d", code)
|
||||||
|
}
|
||||||
|
if code := f.status(f.admin, "PUT", "/api/v1/files/proxy.md", "stale", map[string]string{"If-Match": etag}); code != http.StatusPreconditionFailed {
|
||||||
|
t.Fatalf("过期版本必须仍拒绝保存: %d", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMoveBroadcastsWhenAssetRewriteReplacesFileIdentity(t *testing.T) {
|
func TestMoveBroadcastsWhenAssetRewriteReplacesFileIdentity(t *testing.T) {
|
||||||
f := newFixture(t)
|
f := newFixture(t)
|
||||||
f.write("from.md", "")
|
f.write("from.md", "")
|
||||||
@@ -274,7 +291,11 @@ func TestAPIResponsesAreNotCached(t *testing.T) {
|
|||||||
// Firefox once cached identity and tree responses offline, incorrectly opening an unloadable workspace.
|
// Firefox once cached identity and tree responses offline, incorrectly opening an unloadable workspace.
|
||||||
for _, path := range []string{"/api/v1/auth/state", "/api/v1/tree", "/api/v1/files/笔记.md", "/api/v1/raw/笔记.md"} {
|
for _, path := range []string{"/api/v1/auth/state", "/api/v1/tree", "/api/v1/files/笔记.md", "/api/v1/raw/笔记.md"} {
|
||||||
res := f.do(f.admin, "GET", path, "", nil)
|
res := f.do(f.admin, "GET", path, "", nil)
|
||||||
if res.StatusCode != http.StatusOK || res.Header.Get("Cache-Control") != "no-store" {
|
want := "no-store"
|
||||||
|
if strings.HasPrefix(path, "/api/v1/files/") {
|
||||||
|
want = "no-store, no-transform"
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK || res.Header.Get("Cache-Control") != want {
|
||||||
t.Errorf("%s:状态 %d,缓存策略 %q", path, res.StatusCode, res.Header.Get("Cache-Control"))
|
t.Errorf("%s:状态 %d,缓存策略 %q", path, res.StatusCode, res.Header.Get("Cache-Control"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ set -eu
|
|||||||
|
|
||||||
mode="${1:?Expected development or release}"
|
mode="${1:?Expected development or release}"
|
||||||
case "$mode" in development|release) ;; *) exit 2 ;; esac
|
case "$mode" in development|release) ;; *) exit 2 ;; esac
|
||||||
|
if [ "$mode" = development ]; then export RAWMARK_BUILD_CHANNEL=development; fi
|
||||||
|
export RAWMARK_BUILD_VERSION="$(python3 scripts/release.py version)"
|
||||||
export CGO_ENABLED=0
|
export CGO_ENABLED=0
|
||||||
corepack enable
|
corepack enable
|
||||||
corepack prepare [email protected] --activate
|
corepack prepare [email protected] --activate
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ case "$mode" in
|
|||||||
release) targets='linux/amd64 linux/arm64 windows/amd64 darwin/amd64 darwin/arm64' ;;
|
release) targets='linux/amd64 linux/arm64 windows/amd64 darwin/amd64 darwin/arm64' ;;
|
||||||
*) exit 2 ;;
|
*) exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
if [ "$mode" = development ]; then export RAWMARK_BUILD_CHANNEL=development; fi
|
||||||
|
export RAWMARK_BUILD_VERSION="$(python3 scripts/release.py version)"
|
||||||
export CGO_ENABLED=0
|
export CGO_ENABLED=0
|
||||||
mkdir -p package/licenses downloads
|
mkdir -p package/licenses downloads
|
||||||
cp LICENSE package/
|
cp LICENSE package/
|
||||||
@@ -19,7 +21,7 @@ for target in $targets; do
|
|||||||
if [ "$GOOS" = windows ]; then extension='.exe'; fi
|
if [ "$GOOS" = windows ]; then extension='.exe'; fi
|
||||||
rm -f package/rawmark package/rawmark.exe
|
rm -f package/rawmark package/rawmark.exe
|
||||||
go vet ./...
|
go vet ./...
|
||||||
go build -trimpath -ldflags="-s -w -X main.releaseVersion=$(cat VERSION)" \
|
go build -trimpath -ldflags="-s -w -X main.releaseVersion=$(python3 scripts/release.py version)" \
|
||||||
-o "package/rawmark$extension" ./cmd/rawmark
|
-o "package/rawmark$extension" ./cmd/rawmark
|
||||||
if [ "$target" = linux/amd64 ]; then python3 scripts/smoke-release.py package/rawmark; fi
|
if [ "$target" = linux/amd64 ]; then python3 scripts/smoke-release.py package/rawmark; fi
|
||||||
python3 scripts/package-release.py "$GOOS" "$GOARCH"
|
python3 scripts/package-release.py "$GOOS" "$GOARCH"
|
||||||
|
|||||||
+6
-5
@@ -14,7 +14,7 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from release import API, GITEA_IMAGE, GITHUB_IMAGE, release_receipt, require, run
|
from release import API, GITEA_DEV_IMAGE, GITHUB_IMAGE, release_receipt, require, run
|
||||||
|
|
||||||
|
|
||||||
def settings(directory, channel):
|
def settings(directory, channel):
|
||||||
@@ -49,7 +49,7 @@ def save(path, data):
|
|||||||
def deployment(directory, channel, image, commit, version, client_container=None):
|
def deployment(directory, channel, image, commit, version, client_container=None):
|
||||||
directory = Path(directory).resolve(strict=True)
|
directory = Path(directory).resolve(strict=True)
|
||||||
cfg = settings(directory, channel)
|
cfg = settings(directory, channel)
|
||||||
registry = GITEA_IMAGE if channel == 'development' else GITHUB_IMAGE
|
registry = GITEA_DEV_IMAGE if channel == 'development' else GITHUB_IMAGE
|
||||||
require(re.fullmatch(re.escape(registry) + r'@sha256:[0-9a-f]{64}', image), 'Expected an immutable registry digest')
|
require(re.fullmatch(re.escape(registry) + r'@sha256:[0-9a-f]{64}', image), 'Expected an immutable registry digest')
|
||||||
require(re.fullmatch(r'[0-9a-f]{40}', commit), 'Expected a full commit SHA')
|
require(re.fullmatch(r'[0-9a-f]{40}', commit), 'Expected a full commit SHA')
|
||||||
# Serializes actual deployments, including manually started jobs and CLI use.
|
# Serializes actual deployments, including manually started jobs and CLI use.
|
||||||
@@ -67,8 +67,8 @@ def deployment(directory, channel, image, commit, version, client_container=None
|
|||||||
'Executable version mismatch')
|
'Executable version mismatch')
|
||||||
if channel == 'development':
|
if channel == 'development':
|
||||||
api = API(os.environ['GITEA_API_URL'], os.environ['GITEA_RELEASE_TOKEN'])
|
api = API(os.environ['GITEA_API_URL'], os.environ['GITEA_RELEASE_TOKEN'])
|
||||||
if api.repo('/branches/main')['commit']['id'] != commit:
|
if api.repo('/branches/dev')['commit']['id'] != commit:
|
||||||
print('Skipped development deployment: main has advanced')
|
print('Skipped development deployment: dev has advanced')
|
||||||
return
|
return
|
||||||
identifiers = run('docker', 'ps', '-aq', '--filter', 'label=com.docker.compose.project=' + cfg['RAWMARK_PROJECT'],
|
identifiers = run('docker', 'ps', '-aq', '--filter', 'label=com.docker.compose.project=' + cfg['RAWMARK_PROJECT'],
|
||||||
'--filter', 'label=com.docker.compose.service=rawmark').split()
|
'--filter', 'label=com.docker.compose.service=rawmark').split()
|
||||||
@@ -87,6 +87,7 @@ def deployment(directory, channel, image, commit, version, client_container=None
|
|||||||
backup = None
|
backup = None
|
||||||
if previous:
|
if previous:
|
||||||
run('docker', 'stop', '--time', '15', previous['Id'])
|
run('docker', 'stop', '--time', '15', previous['Id'])
|
||||||
|
if any((directory / 'data').iterdir()):
|
||||||
try:
|
try:
|
||||||
backups = directory / 'backups'
|
backups = directory / 'backups'
|
||||||
backups.mkdir(mode=0o700, exist_ok=True)
|
backups.mkdir(mode=0o700, exist_ok=True)
|
||||||
@@ -101,7 +102,7 @@ def deployment(directory, channel, image, commit, version, client_container=None
|
|||||||
archive.add(directory / name, arcname=name)
|
archive.add(directory / name, arcname=name)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
# No new executable has touched the data; restarting the old container is safe here.
|
# No new executable has touched the data; restarting the old container is safe here.
|
||||||
if previous['State']['Running']:
|
if previous and previous['State']['Running']:
|
||||||
run('docker', 'start', previous['Id'])
|
run('docker', 'start', previous['Id'])
|
||||||
raise
|
raise
|
||||||
pending = {'image': image, 'commit': commit, 'version': version, 'channel': channel,
|
pending = {'image': image, 'commit': commit, 'version': version, 'channel': channel,
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Mirror only business Git references under one persistent repository lock."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import fcntl
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
from release import API, git_env, require
|
||||||
|
|
||||||
|
|
||||||
|
def git(directory, *args, env=None):
|
||||||
|
return subprocess.check_output(['git', '-C', str(directory), *args], env=env, text=True).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def references(directory, remote=None, env=None):
|
||||||
|
command = ['ls-remote', remote, 'refs/heads/*', 'refs/tags/*'] if remote else [
|
||||||
|
'for-each-ref', '--format=%(objectname)\t%(refname)', 'refs/heads/', 'refs/tags/']
|
||||||
|
output = git(directory, *command, env=env)
|
||||||
|
return {ref: sha for line in output.splitlines() for sha, ref in [line.split()] if not ref.endswith('^{}')}
|
||||||
|
|
||||||
|
|
||||||
|
def synchronize(directory, source, target, environment):
|
||||||
|
directory = Path(directory)
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
with (directory / '.mirror.lock').open('a') as lock:
|
||||||
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||||
|
repo = directory / 'repository.git'
|
||||||
|
if not repo.exists():
|
||||||
|
subprocess.run(['git', 'init', '--bare', str(repo)], check=True, stdout=subprocess.DEVNULL)
|
||||||
|
for _ in range(3):
|
||||||
|
git(repo, 'fetch', '--prune', '--no-tags', source,
|
||||||
|
'+refs/heads/*:refs/heads/*', '+refs/tags/*:refs/tags/*', env=environment)
|
||||||
|
snapshot = references(repo)
|
||||||
|
require('refs/heads/main' in snapshot, 'Source main is missing; refusing destructive synchronization')
|
||||||
|
# Fetch every LFS object before updating destination references when LFS is present.
|
||||||
|
if git(repo, 'log', '--all', '--format=', '--name-only', '--', '.gitattributes', '**/.gitattributes').strip():
|
||||||
|
subprocess.run(['git', '-C', str(repo), 'lfs', 'fetch', '--all', source], env=environment, check=True)
|
||||||
|
subprocess.run(['git', '-C', str(repo), 'lfs', 'push', '--all', target], env=environment, check=True)
|
||||||
|
git(repo, 'push', '--atomic', '--prune', target,
|
||||||
|
'+refs/heads/*:refs/heads/*', '+refs/tags/*:refs/tags/*', env=environment)
|
||||||
|
require(references(repo, target, environment) == snapshot, 'Destination references differ')
|
||||||
|
if references(repo, source, environment) == snapshot:
|
||||||
|
receipt = {'source': source, 'target': target, 'refs': snapshot, 'verified_at': int(time.time())}
|
||||||
|
(directory / 'last-sync.json').write_text(json.dumps(receipt, indent=2) + '\n')
|
||||||
|
return receipt
|
||||||
|
raise RuntimeError('Source kept changing; retry synchronization')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--directory', required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
api = API('https://api.github.com', os.environ['GITHUB_MIRROR_TOKEN'], github=True)
|
||||||
|
synchronize(args.directory, 'https://git.134333.xyz/eezd/RawMark.git',
|
||||||
|
'https://github.com/eezd/RawMark.git', git_env(api))
|
||||||
|
print('GitHub mirror verified')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -7,8 +7,12 @@ import tarfile
|
|||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
system, arch = sys.argv[1:]
|
system, arch = sys.argv[1:]
|
||||||
version = Path('VERSION').read_text().strip()
|
from release import build_version
|
||||||
name = f'rawmark_{version}_{system}_{arch}'
|
|
||||||
|
version = build_version()
|
||||||
|
from release import archive_name
|
||||||
|
|
||||||
|
name = archive_name(version, system + '/' + arch).removesuffix('.zip').removesuffix('.tar.gz')
|
||||||
package = Path('package')
|
package = Path('package')
|
||||||
files = sorted(p for p in package.rglob('*') if p.is_file())
|
files = sorted(p for p in package.rglob('*') if p.is_file())
|
||||||
if system == 'windows':
|
if system == 'windows':
|
||||||
|
|||||||
+38
-11
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
@@ -22,6 +23,7 @@ import zipfile
|
|||||||
|
|
||||||
REPOSITORY = 'eezd/RawMark'
|
REPOSITORY = 'eezd/RawMark'
|
||||||
GITEA_IMAGE = 'git.134333.xyz/eezd/rawmark'
|
GITEA_IMAGE = 'git.134333.xyz/eezd/rawmark'
|
||||||
|
GITEA_DEV_IMAGE = 'git.134333.xyz/eezd/dev/rawmark'
|
||||||
GITHUB_IMAGE = 'ghcr.io/eezd/rawmark'
|
GITHUB_IMAGE = 'ghcr.io/eezd/rawmark'
|
||||||
PLATFORMS = ('linux/amd64', 'linux/arm64', 'windows/amd64', 'darwin/amd64', 'darwin/arm64')
|
PLATFORMS = ('linux/amd64', 'linux/arm64', 'windows/amd64', 'darwin/amd64', 'darwin/arm64')
|
||||||
UPLOAD_LIMIT = 90_000_000
|
UPLOAD_LIMIT = 90_000_000
|
||||||
@@ -52,13 +54,23 @@ def output(name, value):
|
|||||||
target.write(f'{name}={value}\n')
|
target.write(f'{name}={value}\n')
|
||||||
|
|
||||||
|
|
||||||
|
def build_version():
|
||||||
|
if os.environ.get('RAWMARK_BUILD_CHANNEL') != 'development':
|
||||||
|
return Path('VERSION').read_text().strip()
|
||||||
|
commit = run('git', 'rev-parse', 'HEAD')
|
||||||
|
timestamp = int(run('git', 'show', '-s', '--format=%ct', commit))
|
||||||
|
date = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).strftime('%Y%m%d')
|
||||||
|
return date + '-' + commit[:8]
|
||||||
|
|
||||||
|
|
||||||
def archive_name(version, platform):
|
def archive_name(version, platform):
|
||||||
extension = '.zip' if platform.startswith('windows/') else '.tar.gz'
|
extension = '.zip' if platform.startswith('windows/') else '.tar.gz'
|
||||||
return f'rawmark_{version}_{platform.replace("/", "_")}{extension}'
|
prefix = 'RawMark' if re.fullmatch(r'[0-9]{8}-[0-9a-f]{8}', version) else 'rawmark'
|
||||||
|
return f'{prefix}_{version}_{platform.replace("/", "_")}{extension}'
|
||||||
|
|
||||||
|
|
||||||
def bundle(mode):
|
def bundle(mode):
|
||||||
version = Path('VERSION').read_text().strip()
|
version = build_version()
|
||||||
commit = run('git', 'rev-parse', 'HEAD')
|
commit = run('git', 'rev-parse', 'HEAD')
|
||||||
platforms = PLATFORMS if mode == 'release' else PLATFORMS[:1]
|
platforms = PLATFORMS if mode == 'release' else PLATFORMS[:1]
|
||||||
files = {}
|
files = {}
|
||||||
@@ -91,7 +103,7 @@ def bundle(mode):
|
|||||||
|
|
||||||
def validate_build(build):
|
def validate_build(build):
|
||||||
require(build['schema'] == 1, 'Unsupported build receipt')
|
require(build['schema'] == 1, 'Unsupported build receipt')
|
||||||
require(build['version'] == Path('VERSION').read_text().strip(), 'Build version mismatch')
|
require(build['version'] == build_version(), 'Build version mismatch')
|
||||||
require(build['commit'] == run('git', 'rev-parse', 'HEAD'), 'Build commit mismatch')
|
require(build['commit'] == run('git', 'rev-parse', 'HEAD'), 'Build commit mismatch')
|
||||||
require(build['mode'] in ('development', 'release'), 'Unknown build mode')
|
require(build['mode'] in ('development', 'release'), 'Unknown build mode')
|
||||||
expected = PLATFORMS if build['mode'] == 'release' else PLATFORMS[:1]
|
expected = PLATFORMS if build['mode'] == 'release' else PLATFORMS[:1]
|
||||||
@@ -423,7 +435,19 @@ def promote_development(local_image):
|
|||||||
token = os.environ['GITEA_RELEASE_TOKEN']
|
token = os.environ['GITEA_RELEASE_TOKEN']
|
||||||
subprocess.run(['docker', 'login', 'git.134333.xyz', '--username', 'eezd', '--password-stdin'],
|
subprocess.run(['docker', 'login', 'git.134333.xyz', '--username', 'eezd', '--password-stdin'],
|
||||||
input=token, text=True, check=True)
|
input=token, text=True, check=True)
|
||||||
candidate = GITEA_IMAGE + ':build-' + build['commit'] + '-' + os.environ['CI_RUN_ID'] + '-' + os.environ['CI_RUN_ATTEMPT']
|
existing = registry_digest(GITEA_DEV_IMAGE, build['version'])
|
||||||
|
if existing:
|
||||||
|
reference = GITEA_DEV_IMAGE + '@' + existing
|
||||||
|
run('docker', 'pull', reference)
|
||||||
|
info = json.loads(run('docker', 'image', 'inspect', reference))[0]
|
||||||
|
labels = info['Config'].get('Labels') or {}
|
||||||
|
require(labels.get('org.opencontainers.image.revision') == build['commit']
|
||||||
|
and labels.get('org.opencontainers.image.version') == build['version'],
|
||||||
|
'Existing development version belongs to another commit')
|
||||||
|
output('image', reference)
|
||||||
|
output('version', build['version'])
|
||||||
|
return
|
||||||
|
candidate = GITEA_DEV_IMAGE + ':build-' + build['commit'] + '-' + os.environ['CI_RUN_ID'] + '-' + os.environ['CI_RUN_ATTEMPT']
|
||||||
# Uncompressed layer sizes provide a conservative bound before any CDN upload.
|
# Uncompressed layer sizes provide a conservative bound before any CDN upload.
|
||||||
with tempfile.TemporaryFile() as target:
|
with tempfile.TemporaryFile() as target:
|
||||||
subprocess.run(['docker', 'image', 'save', local_image], stdout=target, check=True)
|
subprocess.run(['docker', 'image', 'save', local_image], stdout=target, check=True)
|
||||||
@@ -434,15 +458,15 @@ def promote_development(local_image):
|
|||||||
run('docker', 'tag', local_image, candidate)
|
run('docker', 'tag', local_image, candidate)
|
||||||
try:
|
try:
|
||||||
run('docker', 'push', candidate)
|
run('docker', 'push', candidate)
|
||||||
value = registry_digest(GITEA_IMAGE, candidate.rsplit(':', 1)[1])
|
value = registry_digest(GITEA_DEV_IMAGE, candidate.rsplit(':', 1)[1])
|
||||||
tag = 'dev-' + build['commit']
|
tag = build['version']
|
||||||
existing = registry_digest(GITEA_IMAGE, tag)
|
existing = registry_digest(GITEA_DEV_IMAGE, tag)
|
||||||
require(existing in (None, value), 'Immutable development tag already has different content')
|
require(existing in (None, value), 'Immutable development tag already has different content')
|
||||||
if not existing:
|
if not existing:
|
||||||
run('docker', 'buildx', 'imagetools', 'create', '--prefer-index=false', '--tag',
|
run('docker', 'buildx', 'imagetools', 'create', '--prefer-index=false', '--tag',
|
||||||
GITEA_IMAGE + ':' + tag, GITEA_IMAGE + '@' + value)
|
GITEA_DEV_IMAGE + ':' + tag, GITEA_DEV_IMAGE + '@' + value)
|
||||||
require(registry_digest(GITEA_IMAGE, tag) == value, 'Development tag digest changed')
|
require(registry_digest(GITEA_DEV_IMAGE, tag) == value, 'Development tag digest changed')
|
||||||
output('image', GITEA_IMAGE + '@' + value)
|
output('image', GITEA_DEV_IMAGE + '@' + value)
|
||||||
output('version', build['version'])
|
output('version', build['version'])
|
||||||
finally:
|
finally:
|
||||||
run('docker', 'image', 'rm', candidate)
|
run('docker', 'image', 'rm', candidate)
|
||||||
@@ -610,6 +634,7 @@ def main():
|
|||||||
sub = parser.add_subparsers(dest='command', required=True)
|
sub = parser.add_subparsers(dest='command', required=True)
|
||||||
sub.add_parser('bundle').add_argument('mode', choices=('development', 'release'))
|
sub.add_parser('bundle').add_argument('mode', choices=('development', 'release'))
|
||||||
sub.add_parser('prepare-image')
|
sub.add_parser('prepare-image')
|
||||||
|
sub.add_parser('version')
|
||||||
pre = sub.add_parser('preflight')
|
pre = sub.add_parser('preflight')
|
||||||
pre.add_argument('version')
|
pre.add_argument('version')
|
||||||
pre.add_argument('commit')
|
pre.add_argument('commit')
|
||||||
@@ -617,7 +642,9 @@ def main():
|
|||||||
sub.add_parser('publish')
|
sub.add_parser('publish')
|
||||||
sub.add_parser('promote-development').add_argument('image')
|
sub.add_parser('promote-development').add_argument('image')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.command == 'bundle':
|
if args.command == 'version':
|
||||||
|
print(build_version())
|
||||||
|
elif args.command == 'bundle':
|
||||||
bundle(args.mode)
|
bundle(args.mode)
|
||||||
elif args.command == 'prepare-image':
|
elif args.command == 'prepare-image':
|
||||||
prepare_image()
|
prepare_image()
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ parser.add_argument('image')
|
|||||||
parser.add_argument('--client-container', help='CI job container to connect to the test network')
|
parser.add_argument('--client-container', help='CI job container to connect to the test network')
|
||||||
parser.add_argument('--resource-label', help='Label temporary resources for cancellation cleanup')
|
parser.add_argument('--resource-label', help='Label temporary resources for cancellation cleanup')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
version = Path('VERSION').read_text().strip()
|
from release import build_version
|
||||||
|
|
||||||
|
version = build_version()
|
||||||
name = 'rawmark-ci-' + secrets.token_hex(8)
|
name = 'rawmark-ci-' + secrets.token_hex(8)
|
||||||
labels = ('--label', args.resource_label) if args.resource_label else ()
|
labels = ('--label', args.resource_label) if args.resource_label else ()
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ import urllib.request
|
|||||||
|
|
||||||
|
|
||||||
binary = str(Path(sys.argv[1]).resolve())
|
binary = str(Path(sys.argv[1]).resolve())
|
||||||
version = Path('VERSION').read_text().strip()
|
from release import build_version
|
||||||
|
|
||||||
|
version = build_version()
|
||||||
assert subprocess.check_output([binary, '--version'], text=True).strip() == f'RawMark {version}'
|
assert subprocess.check_output([binary, '--version'], text=True).strip() == f'RawMark {version}'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ class DeploymentTests(unittest.TestCase):
|
|||||||
(self.directory / '.env').write_text(
|
(self.directory / '.env').write_text(
|
||||||
f'RAWMARK_CHANNEL=development\nRAWMARK_PROJECT=rawmark-ci-deploy\n'
|
f'RAWMARK_CHANNEL=development\nRAWMARK_PROJECT=rawmark-ci-deploy\n'
|
||||||
f'RAWMARK_BIND=127.0.0.1\nRAWMARK_PORT=21899\nRAWMARK_DATA_DIR={self.directory}/data\n')
|
f'RAWMARK_BIND=127.0.0.1\nRAWMARK_PORT=21899\nRAWMARK_DATA_DIR={self.directory}/data\n')
|
||||||
self.image = release.GITEA_IMAGE + '@' + DIGEST
|
self.image = release.GITEA_DEV_IMAGE + '@' + DIGEST
|
||||||
self.previous = {'Id': 'old-id', 'Image': 'old-image', 'State': {'Running': True},
|
self.previous = {'Id': 'old-id', 'Image': 'old-image', 'State': {'Running': True},
|
||||||
'Mounts': [{'Destination': '/data', 'Type': 'bind', 'Source': str(self.directory / 'data')}]}
|
'Mounts': [{'Destination': '/data', 'Type': 'bind', 'Source': str(self.directory / 'data')}]}
|
||||||
self.image_info = {'Id': 'new-image', 'Config': {'User': '65532:65532', 'Labels': {
|
self.image_info = {'Id': 'new-image', 'Config': {'User': '65532:65532', 'Labels': {
|
||||||
@@ -249,7 +249,7 @@ class DeploymentTests(unittest.TestCase):
|
|||||||
deploy.deployment(self.directory, 'development', self.image, SHA, '0.1.1')
|
deploy.deployment(self.directory, 'development', self.image, SHA, '0.1.1')
|
||||||
docker.assert_not_called()
|
docker.assert_not_called()
|
||||||
|
|
||||||
def test_stale_main_skips_without_stopping_existing_service(self):
|
def test_stale_dev_skips_without_stopping_existing_service(self):
|
||||||
with patch.object(deploy, 'API') as api, patch.object(deploy, 'run', side_effect=self.command):
|
with patch.object(deploy, 'API') as api, patch.object(deploy, 'run', side_effect=self.command):
|
||||||
api.return_value.repo.return_value = {'commit': {'id': 'c' * 40}}
|
api.return_value.repo.return_value = {'commit': {'id': 'c' * 40}}
|
||||||
deploy.deployment(self.directory, 'development', self.image, SHA, '0.1.1')
|
deploy.deployment(self.directory, 'development', self.image, SHA, '0.1.1')
|
||||||
@@ -278,6 +278,21 @@ class DeploymentTests(unittest.TestCase):
|
|||||||
self.assertLess(stop, start)
|
self.assertLess(stop, start)
|
||||||
self.assertIn('RAWMARK_IMAGE=' + self.image, (self.directory / '.env').read_text())
|
self.assertIn('RAWMARK_IMAGE=' + self.image, (self.directory / '.env').read_text())
|
||||||
|
|
||||||
|
def test_existing_data_is_backed_up_without_a_container(self):
|
||||||
|
def without_container(*args, **kwargs):
|
||||||
|
if args[:2] == ('docker', 'ps'):
|
||||||
|
return ''
|
||||||
|
return self.command(*args, **kwargs)
|
||||||
|
opener = Mock()
|
||||||
|
opener.open.side_effect = [contextlib.nullcontext(io.BytesIO(b'<html>RawMark</html>')),
|
||||||
|
contextlib.nullcontext(io.BytesIO(b'{"code":0}'))]
|
||||||
|
with patch.object(deploy, 'run', side_effect=without_container), patch.object(
|
||||||
|
deploy.urllib.request, 'build_opener', return_value=opener):
|
||||||
|
deploy.deployment(self.directory, 'development', self.image, SHA, '0.1.1')
|
||||||
|
receipt = json.loads((self.directory / 'deployment.json').read_text())
|
||||||
|
self.assertTrue(Path(receipt['backup']).exists())
|
||||||
|
self.assertIsNone(receipt['previous_image'])
|
||||||
|
|
||||||
def test_failed_replacement_keeps_backup_and_does_not_downgrade_data(self):
|
def test_failed_replacement_keeps_backup_and_does_not_downgrade_data(self):
|
||||||
def failure(*args, **kwargs):
|
def failure(*args, **kwargs):
|
||||||
if args[:2] == ('docker', 'compose') and 'up' in args:
|
if args[:2] == ('docker', 'compose') and 'up' in args:
|
||||||
@@ -293,6 +308,15 @@ class DeploymentTests(unittest.TestCase):
|
|||||||
self.assertNotIn('RAWMARK_IMAGE=', (self.directory / '.env').read_text())
|
self.assertNotIn('RAWMARK_IMAGE=', (self.directory / '.env').read_text())
|
||||||
|
|
||||||
|
|
||||||
|
class DevelopmentVersionTests(unittest.TestCase):
|
||||||
|
def test_version_uses_utc_commit_date_and_exact_eight_character_hash(self):
|
||||||
|
with patch.dict(os.environ, {'RAWMARK_BUILD_CHANNEL': 'development'}), patch.object(
|
||||||
|
release, 'run', side_effect=[SHA, '0']):
|
||||||
|
self.assertEqual(release.build_version(), '19700101-aaaaaaaa')
|
||||||
|
self.assertEqual(release.archive_name('19700101-aaaaaaaa', 'linux/amd64'),
|
||||||
|
'RawMark_19700101-aaaaaaaa_linux_amd64.tar.gz')
|
||||||
|
|
||||||
|
|
||||||
class CleanupTests(unittest.TestCase):
|
class CleanupTests(unittest.TestCase):
|
||||||
def test_cleanup_rejects_broad_filter(self):
|
def test_cleanup_rejects_broad_filter(self):
|
||||||
with patch('subprocess.check_output') as docker:
|
with patch('subprocess.check_output') as docker:
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Verify strict mirroring with isolated local repositories."""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
from mirror import git, references, synchronize
|
||||||
|
|
||||||
|
|
||||||
|
class MirrorTests(unittest.TestCase):
|
||||||
|
def test_full_history_tags_deletions_and_failed_source(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
source, target = root / 'source', root / 'target.git'
|
||||||
|
subprocess.run(['git', 'init', '-b', 'main', str(source)], check=True, capture_output=True)
|
||||||
|
subprocess.run(['git', 'init', '--bare', str(target)], check=True, capture_output=True)
|
||||||
|
(source / 'note.md').write_text('synthetic note')
|
||||||
|
git(source, 'add', '.')
|
||||||
|
git(source, '-c', 'user.name=Test', '-c', '[email protected]', 'commit', '-m', 'test')
|
||||||
|
git(source, 'branch', 'dev')
|
||||||
|
git(source, '-c', 'user.name=Test', '-c', '[email protected]', 'tag', '-a', 'test-tag', '-m', 'test')
|
||||||
|
state = root / 'state'
|
||||||
|
synchronize(state, str(source), str(target), os.environ.copy())
|
||||||
|
self.assertEqual(references(source), references(target))
|
||||||
|
git(source, 'branch', '-D', 'dev')
|
||||||
|
git(source, 'tag', '-d', 'test-tag')
|
||||||
|
git(target, 'branch', 'target-only', 'main')
|
||||||
|
synchronize(state, str(source), str(target), os.environ.copy())
|
||||||
|
self.assertEqual(references(source), references(target))
|
||||||
|
before = references(target)
|
||||||
|
with self.assertRaises(subprocess.CalledProcessError):
|
||||||
|
synchronize(state, str(root / 'missing'), str(target), os.environ.copy())
|
||||||
|
self.assertEqual(before, references(target))
|
||||||
+1
-1
@@ -8,7 +8,7 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
|||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
import { defineConfig } from 'vitest/config'
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
const releaseVersion = readFileSync(new URL('../VERSION', import.meta.url), 'utf8').trim()
|
const releaseVersion = process.env.RAWMARK_BUILD_VERSION ?? readFileSync(new URL('../VERSION', import.meta.url), 'utf8').trim()
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
define: { __RAWMARK_RELEASE__: JSON.stringify(releaseVersion) },
|
define: { __RAWMARK_RELEASE__: JSON.stringify(releaseVersion) },
|
||||||
|
|||||||
Reference in new issue
Block a user