Use DEV as the sole personal instance #4
No files matched your search
@@ -1,53 +0,0 @@
|
||||
name: Deploy production manually
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Completed date-hash release version
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: rawmark-production
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
runs-on: docker
|
||||
timeout-minutes: 20
|
||||
container:
|
||||
image: docker:29-cli
|
||||
volumes:
|
||||
- /docker/gitea/main/RawMark:/docker/gitea/main/RawMark
|
||||
- /docker/gitea/mirrors/RawMark:/docker/gitea/mirrors/RawMark
|
||||
env:
|
||||
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
|
||||
GITEA_READ_TOKEN: ${{ gitea.token }}
|
||||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
DOCKER_CONFIG: /tmp/rawmark-anonymous-docker
|
||||
defaults:
|
||||
run:
|
||||
shell: sh
|
||||
steps:
|
||||
- name: Install deployment runtimes
|
||||
run: apk add --no-cache nodejs python3 git docker-cli-compose
|
||||
- uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Verify completed release, back up and deploy only RawMark
|
||||
run: |
|
||||
set -eu
|
||||
mkdir -p "$DOCKER_CONFIG"
|
||||
chmod 700 "$DOCKER_CONFIG"
|
||||
python3 scripts/deploy.py production --directory /docker/gitea/main/RawMark \
|
||||
--version "$RELEASE_VERSION" --client-container "$(hostname)"
|
||||
- name: Remove job configuration
|
||||
if: always()
|
||||
run: rm -rf "$DOCKER_CONFIG"
|
||||
@@ -182,35 +182,3 @@ jobs:
|
||||
run: |
|
||||
python3 scripts/ci-cleanup.py "$CI_RESOURCE_LABEL" --client-container "$(hostname)"
|
||||
rm -rf "$DOCKER_CONFIG"
|
||||
|
||||
deploy:
|
||||
needs: [build, publish]
|
||||
if: always() && needs.build.result == 'success' && (needs.publish.result == 'success' || needs.publish.result == 'skipped')
|
||||
runs-on: docker
|
||||
timeout-minutes: 20
|
||||
container:
|
||||
image: docker:29-cli
|
||||
volumes:
|
||||
- /docker/gitea/mirrors/RawMark:/docker/gitea/mirrors/RawMark
|
||||
- /docker/gitea/main/RawMark:/docker/gitea/main/RawMark
|
||||
env:
|
||||
GITEA_READ_TOKEN: ${{ secrets.RAWMARK_GITEA_TOKEN }}
|
||||
DOCKER_CONFIG: /tmp/rawmark-anonymous-docker
|
||||
steps:
|
||||
- name: Install deployment runtimes
|
||||
run: apk add --no-cache nodejs python3 git docker-cli-compose
|
||||
- uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
ref: ${{ gitea.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Deploy the completed release and finish its reservation
|
||||
run: |
|
||||
set -eu
|
||||
mkdir -p "$DOCKER_CONFIG"
|
||||
python3 scripts/deploy.py production --directory /docker/gitea/main/RawMark \
|
||||
--version "$(python3 -c 'import sys;sys.path.insert(0,"scripts");import release;print(release.selected_state()["version"])')" \
|
||||
--client-container "$(hostname)"
|
||||
- name: Remove temporary Docker configuration
|
||||
if: always()
|
||||
run: rm -rf "$DOCKER_CONFIG"
|
||||
@@ -133,8 +133,8 @@ Gitea 为公开主仓库,GitHub 为单向公开镜像;默认分支均为 mai
|
||||
|
||||
- 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 未处理时拒绝覆盖。已有数据即使无旧容器也必须备份。
|
||||
- DEV 为 `/docker/gitea/dev/RawMark`,容器 `RawMark-dev`,Compose 项目 `rawmark-dev`,监听 `127.0.0.1:21001`,域名 `raw.134333.xyz`,raw-dev 域名停用。DEV 是唯一常驻实例,保存个人真实数据;CI 与冒烟使用隔离合成数据。数据独立;所有部署共用宿主实例 `.deploy.lock`,锁内核对 dev 最新 SHA,pending 未处理时拒绝覆盖。已有数据即使无旧容器也必须备份。
|
||||
- Git 镜像独立于 CI 成败:push/create/delete、每小时对账和手动重试,共用 `/docker/gitea/mirrors/RawMark/.mirror.lock`,仅同步业务分支和 TAG 及完整历史,跟随源端删除。同步代码从 main 读取;`RAWMARK_MIRROR_ENABLED=true` 后启用,凭据统一使用 Gitea 账号级 Secret `ALL_GITHUB_TOKEN`(头像 → 设置 → Actions → 密钥),接入前配置并验证实际权限,仓库级同名项会覆盖账号级值。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 字节预算。
|
||||
- 正式发布从受信 main 手动触发,使用日期哈希注解 TAG、六平台程序包与 Linux 双架构镜像;发布 ID 和候选 SHA 固定,发布记录与 .release.lock 位于持久镜像目录。GitHub 引用同步复用镜像锁,Pages Demo 不切换。双端发布和 GHCR 公开拉取验收后自动部署 MAIN:/docker/gitea/main/RawMark、RawMark-main、rawmark-main,127.0.0.1:21000,raw.134333.xyz。自动/手动/批量启停共用实例 .deploy.lock,旧版本不得覆盖新版;生产数据保留并先备份,不随开发推送更新。
|
||||
- 正式发布从受信 main 手动触发,使用日期哈希注解 TAG、六平台程序包与 Linux 双架构镜像;发布 ID 和候选 SHA 固定,发布记录与 .release.lock 位于持久镜像目录。GitHub 引用同步复用镜像锁,Pages Demo 不切换。双端发布和 GHCR 公开拉取验收后结束发布并标记预约完成,不部署 MAIN、不更新 DEV。DEV 批量管理入口为 `/docker/gitea/dev/manage.sh start|stop`,与 CD 共用实例锁,pending 阻止启动。
|
||||
+7
-9
@@ -162,9 +162,11 @@ The Bearer authentication upgrade requires the frontend and backend to be releas
|
||||
- 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 DEV and formal artifacts
|
||||
|
||||
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:
|
||||
Personal use keeps only `/docker/gitea/dev/RawMark` running: container `RawMark-dev`, Compose project `rawmark-dev`, loopback port 21001 and public entry `https://raw.134333.xyz`. The raw-dev site is retired. DEV holds real private data; CI and isolated smoke tests use synthetic data.
|
||||
|
||||
Copy `deploy/compose.image.yml` to the instance as `compose.yml`, with a private mode-600 `.env` beside it. Preserve existing ownership and never mount repository examples as live data.
|
||||
|
||||
```dotenv
|
||||
RAWMARK_CHANNEL=development
|
||||
@@ -176,12 +178,8 @@ RAWMARK_DATA_DIR=/docker/gitea/dev/RawMark/data
|
||||
RAWMARK_IMAGE=git.134333.xyz/eezd/dev/rawmark@sha256:<verified-digest>
|
||||
```
|
||||
|
||||
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.
|
||||
Dev pushes deploy verified Gitea development digests after checks and isolated smoke tests. Main pushes and PRs only validate. Manual formal releases publish packages and Gitea/GHCR images with GitHub mirrors; they do not create MAIN or update DEV. `/docker/gitea/dev/manage.sh start|stop` manages first-level projects using instance `.deploy.lock` files and rejects starts with unresolved pending deployments.
|
||||
|
||||
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.
|
||||
Deployment stops the service and backs up complete data, including SQLite WAL, configuration and previous-image details before writing `deployment.pending.json`. Successful health checks write `deployment.json` and clear pending. A failed candidate is stopped while its backup and pending record remain; no automatic data restore or program downgrade occurs. Recovery and private-data migration require explicit authorization and compatibility review. Never run two instances sharing writable data.
|
||||
|
||||
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.
|
||||
|
||||
Preparing the production files does not restart the current service or execute a data migration. Its first transition from the old source-build Compose is an explicitly authorized production update. Do not run both Compose configurations simultaneously. Test upgrades and recovery with synthetic data before that transition.
|
||||
|
||||
A new development domain needs DNS, CDN custom-hostname activation and TLS, plus both the origin and regional ingress proxy configuration. Verify public access; a healthy loopback port or DNS record alone is insufficient. Keep resource-ticket paths redacted on every proxy.
|
||||
Domain changes require public, CDN and TLS validation while preserving resource-ticket log redaction. DNS remains user-managed.
|
||||
@@ -162,9 +162,11 @@ Bearer 认证升级需要前后端一起发布。旧 Cookie 页面不能继续
|
||||
- 更新后界面异常先核对前后端是否来自同一构建,再查看浏览器网络请求和应用日志;插件与外观恢复入口见[使用指南](guide.zh-CN.md#插件与界面恢复)。
|
||||
- 笔记读取返回 `Cache-Control: no-store, no-transform`,防止代理改写条件保存使用的强版本 ETag。代理将其改为弱 ETag 可能导致正常编辑返回 HTTP 412;公网入口须保留该响应策略。
|
||||
|
||||
## 开发与生产托管
|
||||
## DEV 托管与正式产物
|
||||
|
||||
仓库 `deploy/compose.image.yml` 是仅拉镜像的模板。开发实例放在 `/docker/gitea/dev/RawMark`,生产实例放在 `/docker/main/rawmark`;将模板复制至对应实例目录并命名为 `compose.yml`,旁边放权限 600 的私有 `.env`。项目名与容器名分别指定。实例不依赖源码工作目录,必须明确填写:
|
||||
个人使用只保留 `/docker/gitea/dev/RawMark` 常驻实例,容器 `RawMark-dev`、Compose 项目 `rawmark-dev`、端口 `127.0.0.1:21001`,入口 `https://raw.134333.xyz`。raw-dev 站点停用。实例持有真实用户数据,CI 和隔离冒烟只使用合成数据。
|
||||
|
||||
将 `deploy/compose.image.yml` 复制为实例 `compose.yml`,旁边放权限 600 的私有 `.env`。不要覆盖已有数据所有权或挂载仓库示例作为真实数据。
|
||||
|
||||
```dotenv
|
||||
RAWMARK_CHANNEL=development
|
||||
@@ -176,12 +178,8 @@ RAWMARK_DATA_DIR=/docker/gitea/dev/RawMark/data
|
||||
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` 所有者,不递归修改现有生产权限,不将仓库示例模板作为运行数据挂载。
|
||||
DEV push 检查及隔离验收通过后自动部署 Gitea 开发镜像;main push 与 PR 只检查。正式 Release 手动发布程序包、Gitea/GHCR 镜像及 GitHub 镜像,不创建 MAIN 或更新 DEV。`/docker/gitea/dev/manage.sh start|stop` 批量管理一级项目,共用实例 `.deploy.lock`,pending 未处理时拒绝启动。
|
||||
|
||||
dev push CI 通过检查和隔离冒烟后自动更新开发;main push 与 PR 仅验证。本次 CI 迁移期间,正式发布及 Gitea **Deploy production manually** 工作流保持禁用,启用或执行须另行规划并授权。已准备的生产工作流要求两端正式发布及回执一致,固定 GHCR 摘要,并在替换前备份数据和配置。备份为私有文件并包含 SQLite WAL;旧镜像和备份继续保留。
|
||||
部署停止服务后备份完整数据(含 SQLite WAL)、配置和旧镜像信息,再写入 `deployment.pending.json`;健康检查通过才写入 `deployment.json` 并清除 pending。候选失败时停止候选、保留备份,不自动降级程序或恢复数据库。恢复和私人数据迁移需明确授权及兼容性核查;不得同时运行共享可写数据的新旧实例。
|
||||
|
||||
替换前记录 `deployment.pending.json`,健康和前端检查通过后记录 `deployment.json`。候选版本访问数据后失败则停止候选,保留备份和 pending 记录,不自动恢复私人数据,也不直接用旧程序打开可能迁移后的数据;恢复须明确授权并核对兼容性。备份在替换前失败时,旧数据未被新程序访问,可安全重新启动旧容器。不做全局 Docker 清理。
|
||||
|
||||
准备生产文件不等于重启当前服务或执行数据迁移。首次从旧源码构建 Compose 切换须明确授权生产更新,不同时运行两套 Compose。先用合成数据演练更新和恢复。
|
||||
|
||||
新开发域名须完成 DNS、CDN 自定义主机名及证书激活,并配置源站和国内入口。必须验证公网访问,不能仅凭回环端口健康或 DNS 有记录认定成功。所有代理都要对资源票据路径脱敏。
|
||||
域名切换须验证公网、CDN 与 TLS,并保留资源票据日志脱敏。DNS 由用户管理。
|
||||
+2
-2
@@ -114,8 +114,8 @@ Gitea is the public source repository; GitHub is a strict one-way Git mirror. Bo
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- DEV lives at `/docker/gitea/dev/RawMark`, container `RawMark-dev`, explicit Compose project `rawmark-dev`, loopback port 21001 and domain `raw.134333.xyz`; raw-dev is retired. DEV is the only persistent instance and holds private user data; CI smoke tests use isolated synthetic data. 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 the account-level `ALL_GITHUB_TOKEN` in Gitea user Settings → Actions → Secrets with repository/workflow write access. DEV images never go to GHCR.
|
||||
- `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.
|
||||
- Formal releases run manually from trusted main, require successful main-push CI, and publish six native packages plus Linux amd64/arm64 images with date/hash versions. Fixed release IDs retain resumable checkpoints under the persistent release lock. After both releases and public GHCR digest pulls pass, MAIN deploys to `/docker/gitea/main/RawMark` as `RawMark-main` (project `rawmark-main`). Keep Pages unchanged.
|
||||
- Formal releases run manually from trusted main, require successful main-push CI, and publish six native packages plus Linux amd64/arm64 images with date/hash versions. Fixed release IDs retain resumable checkpoints under the persistent release lock. After both releases and public GHCR digest pulls pass, the reservation is marked complete without deploying or restarting any instance. Use `/docker/gitea/dev/manage.sh start|stop` for bulk DEV management with instance locks and pending-start protection. Keep Pages unchanged.
|
||||
+6
-26
@@ -14,7 +14,7 @@ import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from release import API, GITEA_DEV_IMAGE, GITHUB_IMAGE, release_receipt, require, run, release_state, save_state
|
||||
from release import API, GITEA_DEV_IMAGE, require, run
|
||||
|
||||
|
||||
def settings(directory, channel):
|
||||
@@ -48,16 +48,14 @@ def save(path, data):
|
||||
|
||||
def deployment(directory, channel, image, commit, version, client_container=None):
|
||||
directory = Path(directory).resolve(strict=True)
|
||||
require(channel == 'development', 'Only the persistent DEV instance supports deployment')
|
||||
cfg = settings(directory, channel)
|
||||
registry = GITEA_DEV_IMAGE if channel == 'development' else GITHUB_IMAGE
|
||||
registry = GITEA_DEV_IMAGE
|
||||
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')
|
||||
# Serializes actual deployments, including manually started jobs and CLI use.
|
||||
with (directory / '.deploy.lock').open('a') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
if channel == 'production' and (directory / 'deployment.json').exists():
|
||||
deployed = json.loads((directory / 'deployment.json').read_text())
|
||||
run('git', 'merge-base', '--is-ancestor', deployed['commit'], commit)
|
||||
require(not (directory / 'deployment.pending.json').exists(),
|
||||
'Resolve the previous pending deployment before attempting another update')
|
||||
run('docker', 'pull', image)
|
||||
@@ -143,33 +141,15 @@ def deployment(directory, channel, image, commit, version, client_container=None
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('channel', choices=('development', 'production'))
|
||||
parser.add_argument('channel', choices=('development',))
|
||||
parser.add_argument('--directory', required=True)
|
||||
parser.add_argument('--image')
|
||||
parser.add_argument('--commit')
|
||||
parser.add_argument('--version', required=True)
|
||||
parser.add_argument('--client-container')
|
||||
args = parser.parse_args()
|
||||
if args.channel == 'production':
|
||||
require(not args.image and not args.commit, 'Production accepts a completed version, not an arbitrary image')
|
||||
gitea = API(os.environ['GITEA_API_URL'], os.environ['GITEA_READ_TOKEN'])
|
||||
github = API('https://api.github.com', '', github=True)
|
||||
build, receipt = release_receipt(gitea, github, args.version)
|
||||
image, commit = GITHUB_IMAGE + '@' + receipt['image_digest'], build['commit']
|
||||
else:
|
||||
image, commit = args.image, args.commit
|
||||
require(image and commit, 'Development requires an image digest and commit')
|
||||
if args.channel == 'production':
|
||||
with release_state() as state:
|
||||
if state and state.get('status') != 'complete':
|
||||
require(state.get('commit') == commit and state.get('version') == args.version,
|
||||
'Another release is pending')
|
||||
deployment(args.directory, args.channel, image, commit, args.version, args.client_container)
|
||||
if state and state.get('commit') == commit:
|
||||
state['status'] = 'complete'
|
||||
save_state(state)
|
||||
else:
|
||||
deployment(args.directory, args.channel, image, commit, args.version, args.client_container)
|
||||
require(args.image and args.commit, 'Development requires an image digest and commit')
|
||||
deployment(args.directory, args.channel, args.image, args.commit, args.version, args.client_container)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+12
-3
@@ -332,8 +332,7 @@ def preflight(version, commit):
|
||||
'GitHub classic PAT needs repository access, workflow and write:packages')
|
||||
current = gitea.release(version)
|
||||
if current and not current['draft']:
|
||||
build, _ = release_receipt(gitea, github, version)
|
||||
require(build['commit'] == commit, 'Published version belongs to another commit')
|
||||
complete_publication(gitea, github, version, commit)
|
||||
output('completed', 'true')
|
||||
output('reuse', 'true')
|
||||
return
|
||||
@@ -386,6 +385,16 @@ def save_state(state):
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def complete_publication(gitea, github, version, commit):
|
||||
"""Finish the matching reservation only after both published receipts validate."""
|
||||
state = selected_state()
|
||||
require(state['version'] == version and state['commit'] == commit, 'Publication candidate mismatch')
|
||||
build, _ = release_receipt(gitea, github, version)
|
||||
require(build['commit'] == commit, 'Published version belongs to another commit')
|
||||
state['status'] = 'complete'
|
||||
save_state(state)
|
||||
|
||||
|
||||
def reserve(dev):
|
||||
require(re.fullmatch(r'[0-9a-f]{40}', dev), 'Expected the accepted dev SHA')
|
||||
with release_state() as current:
|
||||
@@ -651,7 +660,7 @@ def publish():
|
||||
run('docker', 'buildx', 'imagetools', 'create', '--tag', image + ':' + stable_tag, image + '@' + value)
|
||||
require(registry_digest(image, stable_tag) == value, 'Promoted image digest differs')
|
||||
gitea.repo(f'/releases/{release["id"]}', method='PATCH', data={'draft': False})
|
||||
release_receipt(gitea, github, tag)
|
||||
complete_publication(gitea, github, tag, build['commit'])
|
||||
print(f'Published RawMark {tag}: {build["commit"]}, {value}')
|
||||
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ class DeploymentTests(unittest.TestCase):
|
||||
def test_channel_mismatch_cannot_touch_docker(self):
|
||||
with patch.object(deploy, 'run') as docker:
|
||||
with self.assertRaisesRegex(ValueError, 'channel'):
|
||||
deploy.deployment(self.directory, 'production', self.image, SHA, '0.1.1')
|
||||
deploy.settings(self.directory, 'production')
|
||||
docker.assert_not_called()
|
||||
|
||||
def test_unresolved_pending_deployment_cannot_be_overwritten(self):
|
||||
@@ -380,3 +380,52 @@ class MainAdmissionTests(unittest.TestCase):
|
||||
{'head_sha': SHA, 'event': 'push', 'path': 'dev.yml@refs/heads/dev', 'conclusion': 'success'},
|
||||
]}, {'workflow_runs': []}]
|
||||
self.assertFalse(release.successful_main_ci(api, SHA))
|
||||
|
||||
|
||||
class PublicationCompletionTests(unittest.TestCase):
|
||||
def test_success_finishes_reservation_without_deployment(self):
|
||||
state = {'version': 'version', 'commit': SHA, 'status': 'selected'}
|
||||
with patch.object(release, 'selected_state', return_value=state), \
|
||||
patch.object(release, 'release_receipt', return_value=({'commit': SHA}, {})), \
|
||||
patch.object(release, 'save_state') as save:
|
||||
release.complete_publication(None, None, 'version', SHA)
|
||||
self.assertEqual(state['status'], 'complete')
|
||||
save.assert_called_once_with(state)
|
||||
|
||||
def test_partial_publication_remains_pending(self):
|
||||
state = {'version': 'version', 'commit': SHA, 'status': 'selected'}
|
||||
with patch.object(release, 'selected_state', return_value=state), \
|
||||
patch.object(release, 'release_receipt', side_effect=ValueError('Incomplete mirror')), \
|
||||
patch.object(release, 'save_state') as save:
|
||||
with self.assertRaisesRegex(ValueError, 'Incomplete mirror'):
|
||||
release.complete_publication(None, None, 'version', SHA)
|
||||
self.assertEqual(state['status'], 'selected')
|
||||
save.assert_not_called()
|
||||
|
||||
def test_published_retry_repairs_pending_reservation(self):
|
||||
state = {'version': 'version', 'commit': SHA, 'status': 'selected'}
|
||||
gitea, github = Mock(), Mock()
|
||||
gitea.request.return_value = github.request.return_value = {'login': 'eezd'}
|
||||
github.repo.return_value = {'permissions': {'push': True}}
|
||||
github.headers = {'X-OAuth-Scopes': 'repo, workflow, write:packages'}
|
||||
gitea.release.return_value = {'draft': False}
|
||||
with patch.dict(os.environ, {'CI_RUN_ID': '42'}), \
|
||||
patch.object(release, 'selected_state', return_value=state), \
|
||||
patch.object(release, 'clients', return_value=(gitea, github)), \
|
||||
patch.object(release, 'validate_identity'), \
|
||||
patch.object(release, 'release_receipt', return_value=({'commit': SHA}, {})), \
|
||||
patch.object(release, 'save_state'), patch.object(release, 'run') as run, \
|
||||
patch.object(release, 'output') as output:
|
||||
release.preflight('version', SHA)
|
||||
self.assertEqual(state['status'], 'complete')
|
||||
output.assert_any_call('completed', 'true')
|
||||
run.assert_not_called()
|
||||
|
||||
def test_wrong_candidate_cannot_finish_reservation(self):
|
||||
state = {'version': 'version', 'commit': SHA, 'status': 'selected'}
|
||||
with patch.object(release, 'selected_state', return_value=state), \
|
||||
patch.object(release, 'release_receipt', return_value=({'commit': 'c' * 40}, {})), \
|
||||
patch.object(release, 'save_state') as save:
|
||||
with self.assertRaisesRegex(ValueError, 'another commit'):
|
||||
release.complete_publication(None, None, 'version', SHA)
|
||||
save.assert_not_called()
|
||||
Reference in new issue
Block a user