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
42 changes: 33 additions & 9 deletions .github/workflows/image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ on:
push:
branches:
- "master"
tags:
- "[0-9]+.[0-9]+.[0-9]+"
paths:
- ".github/workflows/image.yml"
- "weather_briefing/**"
- "Dockerfile"
- "pyproject.toml"
- "uv.lock"
workflow_dispatch:
merge_group:

concurrency:
group: weather-briefing-image-${{ github.ref_type == 'branch' && 'edge' || format('release-{0}', github.ref_name) }}
cancel-in-progress: true
Comment thread
IceCodeNew marked this conversation as resolved.

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

jobs:
weather-briefing:
Expand Down Expand Up @@ -95,20 +98,41 @@ jobs:
- name: Create manifest list and push
working-directory: /tmp/weather-briefing-digests/
env:
COMMIT_TAG: sha-${{ github.sha }}
REGISTRY_IMAGE: ${{ env.WEATHER_BRIEFING_IMAGE }}
WEATHER_BRIEFING_VERSION: ${{ env.WEATHER_BRIEFING_VERSION }}
RELEASE_VERSION: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
# shellcheck disable=SC2046
docker buildx imagetools create --tag "${REGISTRY_IMAGE}:latest" --tag "${REGISTRY_IMAGE}:${WEATHER_BRIEFING_VERSION}" \
$(printf "${REGISTRY_IMAGE}@sha256:%s " *)
set -euo pipefail
shopt -s nullglob
digest_files=(*)
if (( ${#digest_files[@]} == 0 )); then
echo "No platform digest artifacts were downloaded"
exit 1
fi
sources=()
for digest_file in "${digest_files[@]}"; do
if ! [[ "${digest_file}" =~ ^[a-f0-9]{64}$ ]]; then
echo "Unexpected digest artifact filename: ${digest_file}"
exit 1
fi
sources+=("${REGISTRY_IMAGE}@sha256:${digest_file}")
done

tags=(--tag "${REGISTRY_IMAGE}:${COMMIT_TAG}")
if [[ -n "${RELEASE_VERSION}" ]]; then
tags+=(--tag "${REGISTRY_IMAGE}:${RELEASE_VERSION}" --tag "${REGISTRY_IMAGE}:latest")
else
tags+=(--tag "${REGISTRY_IMAGE}:edge")
fi
docker buildx imagetools create "${tags[@]}" "${sources[@]}"
- name: Inspect image
env:
REGISTRY_IMAGE: ${{ env.WEATHER_BRIEFING_IMAGE }}:latest
REGISTRY_IMAGE: ${{ env.WEATHER_BRIEFING_IMAGE }}:sha-${{ github.sha }}
run: |
docker buildx imagetools inspect "${REGISTRY_IMAGE}" \
| grep -Po '[^ \r\n\t\v]+@sha256:[a-z0-9]+' | xargs -r -t -n 1 docker buildx imagetools inspect --raw
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: "${{ env.WEATHER_BRIEFING_IMAGE }}:latest"
image-ref: "${{ env.WEATHER_BRIEFING_IMAGE }}:sha-${{ github.sha }}"
format: "table"
15 changes: 6 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:

jobs:
release:
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-24.04
permissions:
contents: write
Expand Down Expand Up @@ -51,9 +52,6 @@ jobs:
init_path = pathlib.Path('weather_briefing/__init__.py')
init_path.write_text(init_path.read_text().replace(f'__version__ = \"{prev}\"', f'__version__ = \"{version}\"'))

image_path = pathlib.Path('.github/workflows/image.yml')
image_path.write_text(image_path.read_text().replace(f'WEATHER_BRIEFING_VERSION: \"{prev}\"', f'WEATHER_BRIEFING_VERSION: \"{version}\"'))

readme_path = pathlib.Path('README.md')
readme_version = f'WEATHER_BRIEFING_VERSION=\"{prev}\"'
readme_content = readme_path.read_text()
Expand All @@ -70,18 +68,17 @@ jobs:
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${RELEASE_GH_PAT}@github.com/${GITHUB_REPOSITORY}.git"

git add pyproject.toml weather_briefing/__init__.py .github/workflows/image.yml README.md uv.lock
git add pyproject.toml weather_briefing/__init__.py README.md uv.lock
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: bump version to ${WEATHER_BRIEFING_VERSION}"
fi

if git rev-parse "refs/tags/${WEATHER_BRIEFING_VERSION}" >/dev/null 2>&1; then
echo "Tag ${WEATHER_BRIEFING_VERSION} already exists, skipping"
else
git tag -a "${WEATHER_BRIEFING_VERSION}" -m "Release ${WEATHER_BRIEFING_VERSION}"
git push origin "${WEATHER_BRIEFING_VERSION}"
echo "Refusing to reuse existing release tag ${WEATHER_BRIEFING_VERSION}"
exit 1
fi

git push origin HEAD:master
git tag -a "${WEATHER_BRIEFING_VERSION}" -m "Release ${WEATHER_BRIEFING_VERSION}"
git push --atomic origin HEAD:master "refs/tags/${WEATHER_BRIEFING_VERSION}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
IceCodeNew marked this conversation as resolved.
2 changes: 2 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,5 @@ Docker `run --env-file` 接受的是 `KEY=value` 列表,不按 shell 语义解
## 镜像与构建上下文

项目使用 uv 原生 `uv_build` 构建后端。Dockerfile 使用多阶段构建:先从官方 Distroless uv 镜像取得 uv/uvx,再复制到 Debian 13 Distroless Python nonroot 镜像,并直接以该镜像按 `uv.lock` 创建生产虚拟环境;最终阶段基于 digest 固定的 `gcr.io/distroless/python3-debian13`,从独立 assets 镜像加入 Bash 与 Toybox,并只复制运行环境和应用。builder 与 runtime 使用相同 Debian 版本及系统 Python,并通过镜像探针验证;最终进程以无特权用户运行。`.dockerignore` 不继承 `.gitignore`,因此使用独立白名单,只让 Dockerfile 实际需要的项目元数据、锁文件和包源码进入 BuildKit 上下文;`.env`、Git 历史、测试和文档不会发送给 builder。

镜像工作流实现 [requirements.md](requirements.md#运行环境) 定义的标签通道,并用一次 manifest 创建命令同时更新当前事件对应的全部标签。
10 changes: 10 additions & 0 deletions docs/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ Nominatim 限速使用 `await asyncio.sleep()`,它只挂起当前协程并把

最新值和窗口基线是累计变化判断的最低证据,因此二者在摘要后仍无法容纳时不能静默处理;应用继续生成简报,同时发送按 source 与内容指纹去重的运维告警。这样接受一次简报可能缺少部分历史比较的降级,但不因可选历史输入阻断当前天气与预警投递。最近变化节点的丢弃不告警,避免正常预算裁剪造成噪声。若告警长期出现,应先让对应 adapter 提供更紧凑的领域摘要;只有确定性摘要无法满足实际质量时,才评估缓存式、可观测且与主调用隔离的 LLM 压缩。

## OCI 标签作为发布通道

镜像标签通道的规范以[运行环境要求](requirements.md#运行环境)为准。`master` 事件更新 `edge` 与 commit SHA 标签,版本 tag 事件更新同名版本、`latest` 与 commit SHA 标签;manifest 步骤不预查 registry,也不主动阻止标签被覆盖。

release 工作流的单一 job 只在手动选择 `master` 时运行,并把生成的版本提交与同名 Git tag atomic push 到 `master`。镜像工作流不提供手动发布或 merge queue 入口;Renovate 更新依赖 branch automerge 后直接触发 `master` 镜像构建,未合并的 Renovate PR 不构建镜像。

连续的 `master` 事件共用可取消的构建组,使旧请求尽早停止,只有最新的 master 请求继续更新 `edge`。每个版本 tag 使用独立的可取消构建组,避免后续 master 请求或其他版本取消正式发布;同一版本的重复事件只保留最新一次。不能让所有发布共用一个 concurrency group,因为 GitHub 只保留一个 pending run,短时间多个版本可能使中间版本未经构建即被替换。

master run 可能在平台 digest 已推送、manifest 尚未创建时被取消,从而暂时留下无标签 digest。当前接受由 registry 清理这类中间产物,以换取快速淘汰过时构建;若 registry 存储持续增长、清理策略不足或产生额外费用,应把取消边界移到 registry 写入之前,或合并构建与发布阶段。

## 异步编排中的同步本地持久化

### 当前选择
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,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。
目标为自有服务器。应用支持 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` 或版本标签。