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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .github/workflows/image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ on:
- "uv.lock"

concurrency:
group: weather-briefing-image-${{ github.ref_type == 'branch' && 'edge' || format('release-{0}', github.ref_name) }}
cancel-in-progress: true
group: weather-briefing-image
cancel-in-progress: false
Comment on lines +18 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Edge rollback via reruns 🐞 Bug ≡ Correctness

With a single global concurrency group and cancel-in-progress: false, the mutable :edge tag is
updated by whichever workflow run finishes last, so a manually rerun (or otherwise delayed) run for
an older ref can execute after newer builds and overwrite :edge with an older digest, effectively
rolling edge backwards even though newer immutable tags (sha/version) remain correct. The current
notes describe serialization as avoiding concurrent edge updates but do not warn about this
last-writer/rerun rollback behavior, which can cause operators to misinterpret the guarantees of the
concurrency policy.
Agent Prompt
## Issue description
The image publishing workflow serializes all runs into one concurrency group and does not cancel in-progress runs, so the mutable `:edge` tag follows last-writer semantics: whichever run completes last updates `edge`. This allows manual reruns (or other delayed executions) of older tag/master refs to execute after newer builds and overwrite `:edge` with an older digest, effectively rolling `edge` backward; the documentation also needs to clearly state this limitation so operators don’t assume serialization implies “newest ref always wins.”

## Issue Context
- The workflow triggers on both `master` pushes and release tag pushes.
- The manifest-publish step tags `:edge` for both tag builds and branch builds.
- With a global concurrency group and `cancel-in-progress: false`, `edge` is determined by the last completed publisher across all executions, including manual reruns (e.g., for diagnostics).
- `docs/notes.md` currently explains the serialization boundary (avoiding concurrent writes) but does not warn that reruns/delayed runs of older refs can still overwrite `edge` after newer runs.

## Fix Focus Areas
- .github/workflows/image.yml[17-19]
- .github/workflows/image.yml[101-134]
- docs/notes.md[9-24]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


env:
WEATHER_BRIEFING_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/weather-briefing

jobs:
weather-briefing:
if: >-
github.ref_type == 'tag' ||
!contains(github.event.head_commit.message, 'Weather-Briefing-Skip-Edge-Image: true')
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Comment on lines +18 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Race condition risking cancelled release builds due to static concurrency group.

GitHub Actions cancels previously pending workflow runs in the same concurrency group when multiple events are queued simultaneously. An atomic push triggers both the tag and master events at the exact same time. If the master event is processed second and queued, it will silently cancel the pending tag event. Since the generated master commit is explicitly designed to skip the build (via the Weather-Briefing-Skip-Edge-Image trailer), this cancellation race will result in the release image not being built at all.

The explicit commit trailer already cleanly prevents the edge tag race by skipping the master build entirely, making the shared concurrency group unnecessary and actively dangerous for atomic pushes.

  • .github/workflows/image.yml#L18-L28: Revert the workflow to use a ref-dependent concurrency group (e.g., group: ${{ github.workflow }}-${{ github.ref }}) to isolate the tag queue from the master queue, ensuring the release tag build is never cancelled by the skipped master build.
  • docs/notes.md#L7-L25: Remove the explanation of the single weather-briefing-image concurrency group. Update the text to rely purely on the commit trailer mechanism to explain how the master edge build conflict is safely bypassed.
📍 Affects 2 files
  • .github/workflows/image.yml#L18-L28 (this comment)
  • docs/notes.md#L7-L25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/image.yml around lines 18 - 28, Replace the static
concurrency group in the weather-briefing workflow at
.github/workflows/image.yml:18-28 with a ref-dependent group such as
github.workflow plus github.ref, preserving the existing cancellation behavior
and trailer-based job skip. Remove the single-group concurrency explanation from
docs/notes.md:7-25 and describe the master edge-build protection solely through
the Weather-Briefing-Skip-Edge-Image commit trailer.

runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -120,7 +123,11 @@ jobs:

tags=(--tag "${REGISTRY_IMAGE}:${COMMIT_TAG}")
if [[ -n "${RELEASE_VERSION}" ]]; then
tags+=(--tag "${REGISTRY_IMAGE}:${RELEASE_VERSION}" --tag "${REGISTRY_IMAGE}:latest")
tags+=(
--tag "${REGISTRY_IMAGE}:${RELEASE_VERSION}"
--tag "${REGISTRY_IMAGE}:latest"
--tag "${REGISTRY_IMAGE}:edge"
)
else
tags+=(--tag "${REGISTRY_IMAGE}:edge")
fi
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ jobs:
echo "Development version update produced no changes"
exit 1
fi
git commit -m "chore: start ${NEXT_DEVELOPMENT_VERSION} development"
git commit \
-m "chore: start ${NEXT_DEVELOPMENT_VERSION} development" \
-m "Weather-Briefing-Skip-Edge-Image: true"

git push --atomic origin HEAD:master "refs/tags/${WEATHER_BRIEFING_VERSION}"
2 changes: 2 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,6 @@ release workflow 的手动输入是 GitHub Actions `choice`,只允许 major、

工作流先同步 `pyproject.toml`、包内版本、`uv.lock` 和 README 的稳定部署版本,生成正式版 commit 并让同名 tag 指向该 commit;再只把代码与锁文件推进到下一 patch 的 `-dev` commit。最后 atomic push `master` 与 tag,使远端不会只接收其中一部分。

GitHub 会分别为同一次 atomic push 中的 `master` 和 tag 更新触发 push workflow。自动生成的 `-dev` commit 因此带有专用 commit trailer,使镜像流水线跳过该次 `master` 构建;tag 流水线只构建正式版 commit 一次,并让版本号、`latest` 与 `edge` 标签共同指向该镜像。后续普通 `master` 提交仍照常更新 `edge`。

开发版 `--version` 以包源码位置的父目录作为预期仓库根,并要求 Git `--show-toplevel` 返回同一目录后才附加 commit SHA 和 dirty 状态。因此从其他 Git 仓库启动 CLI,或把普通安装放在其他仓库的虚拟环境中,都不会误报外部仓库信息。
19 changes: 19 additions & 0 deletions docs/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@

## 审查时应保留的显式边界

### 镜像构建与发布串行边界

release tag 工作流构建的镜像会复用同一套镜像构建流程产出 `edge`。为了避免在 master 与
release tag 两类触发并发时互相覆盖可变 tag(`edge`),`.github/workflows/image.yml` 统一使用单一
`weather-briefing-image` 并发组,并且不主动取消进行中的同组任务,这样 tag 构建与 master 边构建会按序
完成,最终 `edge` 只会被最后完成(而不是并发竞争)的一次更新覆盖。

`weather_briefing/__init__.py` 与 `pyproject.toml` 的版本更新逻辑在 release 流程中引入了 commit trailer
`Weather-Briefing-Skip-Edge-Image: true` 的提交,用于让“先打 release tag,再立即切到 `-dev`
并推送新提交”时跳过一轮与其冲突的 master edge 构建。这样可以减少一次镜像构建,并且确保最终 `edge`
与本次 release tag 对应的制品一致。

该机制的边界:

- 只在 release 工作流的 commit message 中显式设置该 trailer 的提交才会跳过 edge 构建。
- `edge` 继续作为“最近一次可运行制品”的游标标签;`latest` 与 `vX.Y.Z` 标签依然各自表示完整发布语义。
- 若未来需要同时对 `edge` 与别的分支/场景并发发布或允许多路覆盖策略发生变化,需要同步更新
`.github/workflows/image.yml` 的并发策略与这段决策说明。

### 配置与启动边界

DeepSeek 是唯一保留旧环境变量别名的 LLM provider:`DEEPSEEK_MODEL` 后备到 `LLM_MODEL`,`DEEPSEEK_BASE_URL` 后备到 any-llm 使用的 `DEEPSEEK_API_BASE`。这是已投入部署的输入兼容,不是厂商映射;新增 provider 应直接使用 any-llm 的 provider ID 和环境变量,不得把这一分支扩展成通用 provider 配置注册表。
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@

## 运行环境

目标为自有服务器。应用支持 Python 3.11–3.14,以当前最新稳定 Python 作为首选开发和测试版本,并在 CI 中覆盖全部受支持版本;项目元数据不设置未经验证的未来 Python 版本上限。项目使用 uv 原生 `uv_build` 构建后端并提交 `uv.lock`,不引入 PDM 工具链。Distroless Debian 13 镜像使用其系统 Python。应用以内置调度器常驻运行,SQLite 位于外部持久目录或卷中。项目维护单一、非 root、由 `uv.lock` 锁定依赖的 OCI 镜像,不使用 Docker Compose;GitHub Actions 仅用于 CI 和独立镜像发布,不预设生产运行 secrets。每次 `X.Y.Z` Git tag 构建更新同名 OCI 标签、`latest` 和 commit SHA 标签;`master` 构建更新 `edge` 和 commit SHA 标签,不修改 `latest` 或版本标签。发布工作流不接收自由格式版本号,由用户单选 major、minor 或 patch:major 递增并把 minor、patch 归零,minor 递增并把 patch 归零,patch 在正式版上递增、在 `-dev` 版上发布其已声明的目标版本。工作流先创建正式版提交和同名 tag,再创建下一个 patch 的 `-dev` 提交,并将 master 与 tag 原子推送;README 部署示例保留最新正式版。开发版本只在包代码位于 weather-briefing 自身 Git worktree 时通过 `--version` 附加短 commit SHA,有未提交或未跟踪改动时同时标记 dirty;从其他 Git 仓库运行、正式版或无法读取自身 Git 状态时只显示内置版本。
目标为自有服务器。应用支持 Python 3.11–3.14,以当前最新稳定 Python 作为首选开发和测试版本,并在 CI 中覆盖全部受支持版本;项目元数据不设置未经验证的未来 Python 版本上限。项目使用 uv 原生 `uv_build` 构建后端并提交 `uv.lock`,不引入 PDM 工具链。Distroless Debian 13 镜像使用其系统 Python。应用以内置调度器常驻运行,SQLite 位于外部持久目录或卷中。项目维护单一、非 root、由 `uv.lock` 锁定依赖的 OCI 镜像,不使用 Docker Compose;GitHub Actions 仅用于 CI 和独立镜像发布,不预设生产运行 secrets。每次 `X.Y.Z` Git tag 只构建正式版 commit 一次,并让同名 OCI 标签、`latest`、`edge` 和 commit SHA 标签共同指向该镜像;自动生成的下一开发版 `master` commit 跳过重复的 edge 构建,后续普通 `master` 构建继续更新 `edge` 和 commit SHA 标签而不修改 `latest` 或版本标签。发布工作流不接收自由格式版本号,由用户单选 major、minor 或 patch:major 递增并把 minor、patch 归零,minor 递增并把 patch 归零,patch 在正式版上递增、在 `-dev` 版上发布其已声明的目标版本。工作流先创建正式版提交和同名 tag,再创建下一个 patch 的 `-dev` 提交,并将 master 与 tag 原子推送;README 部署示例保留最新正式版。开发版本只在包代码位于 weather-briefing 自身 Git worktree 时通过 `--version` 附加短 commit SHA,有未提交或未跟踪改动时同时标记 dirty;从其他 Git 仓库运行、正式版或无法读取自身 Git 状态时只显示内置版本。