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
194 changes: 171 additions & 23 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ on:
required: true
type: string
push:
branches:
- main
paths:
- packages/automas_hsr/pyproject.toml
- packages/automas_script_hsr/pyproject.toml
- packages/automas_hsr_adapter_sra/pyproject.toml
- packages/automas_hsr_adapter_m7a/pyproject.toml
tags:
- "automas-hsr-v*"
- "automas-script-hsr-v*"
Expand All @@ -27,17 +34,21 @@ permissions:
contents: read

concurrency:
group: publish-${{ github.ref }}-${{ inputs.package || 'tag' }}
group: pypi-publish
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
outputs:
package: ${{ steps.release.outputs.package }}
environment: ${{ steps.release.outputs.environment }}
version: ${{ steps.release.outputs.version }}
artifact_name: ${{ steps.release.outputs.artifact_name }}
script_selected: ${{ steps.release.outputs.script_selected }}
script_artifact: ${{ steps.release.outputs.script_artifact }}
sra_selected: ${{ steps.release.outputs.sra_selected }}
sra_artifact: ${{ steps.release.outputs.sra_artifact }}
m7a_selected: ${{ steps.release.outputs.m7a_selected }}
m7a_artifact: ${{ steps.release.outputs.m7a_artifact }}
meta_selected: ${{ steps.release.outputs.meta_selected }}
meta_artifact: ${{ steps.release.outputs.meta_artifact }}
steps:
- uses: actions/checkout@v6
with:
Expand All @@ -50,54 +61,191 @@ jobs:
enable-cache: true
- run: uv sync --locked --all-packages --group dev
- id: release
name: Resolve and validate release target
name: Resolve version changes and release order
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_REF: ${{ github.ref }}
BEFORE_SHA: ${{ github.event.before }}
INPUT_PACKAGE: ${{ inputs.package }}
INPUT_VERSION: ${{ inputs.version }}
run: >-
uv run python scripts/release.py
--event-name "$EVENT_NAME"
--ref "$RELEASE_REF"
--before-sha "$BEFORE_SHA"
--package "$INPUT_PACKAGE"
--version "$INPUT_VERSION"
- name: Verify tag commit belongs to main
if: github.event_name == 'push'
if: startsWith(github.ref, 'refs/tags/')
run: git merge-base --is-ancestor "$GITHUB_SHA" origin/main
- run: uv run python -m unittest discover -s tests -v
- name: Build selected distribution
env:
PACKAGE_DIR: ${{ steps.release.outputs.package_dir }}
run: uv run python -m build "$PACKAGE_DIR" --outdir dist
- run: uv run twine check dist/*
- name: Metadata-only smoke test for the selected wheel
env:
PACKAGE: ${{ steps.release.outputs.package }}
- name: Run repository tests
if: steps.release.outputs.has_targets == 'true'
run: uv run python -m unittest discover -s tests -v

- name: Build automas-script-hsr
if: steps.release.outputs.script_selected == 'true'
run: >-
uv run python -m build packages/automas_script_hsr
--outdir dist/script
- name: Check automas-script-hsr metadata
if: steps.release.outputs.script_selected == 'true'
run: uv run twine check dist/script/*
- name: Smoke test automas-script-hsr wheel
if: steps.release.outputs.script_selected == 'true'
run: >-
uv run python scripts/smoke_wheels.py dist/script
--mode metadata-only
--expected-package automas-script-hsr
- uses: actions/upload-artifact@v4
if: steps.release.outputs.script_selected == 'true'
with:
name: ${{ steps.release.outputs.script_artifact }}
path: dist/script/*
if-no-files-found: error
retention-days: 7

- name: Build automas-hsr-adapter-sra
if: steps.release.outputs.sra_selected == 'true'
run: >-
uv run python -m build packages/automas_hsr_adapter_sra
--outdir dist/sra
- name: Check automas-hsr-adapter-sra metadata
if: steps.release.outputs.sra_selected == 'true'
run: uv run twine check dist/sra/*
- name: Smoke test automas-hsr-adapter-sra wheel
if: steps.release.outputs.sra_selected == 'true'
run: >-
uv run python scripts/smoke_wheels.py dist/sra
--mode metadata-only
--expected-package automas-hsr-adapter-sra
- uses: actions/upload-artifact@v4
if: steps.release.outputs.sra_selected == 'true'
with:
name: ${{ steps.release.outputs.sra_artifact }}
path: dist/sra/*
if-no-files-found: error
retention-days: 7

- name: Build automas-hsr-adapter-m7a
if: steps.release.outputs.m7a_selected == 'true'
run: >-
uv run python -m build packages/automas_hsr_adapter_m7a
--outdir dist/m7a
- name: Check automas-hsr-adapter-m7a metadata
if: steps.release.outputs.m7a_selected == 'true'
run: uv run twine check dist/m7a/*
- name: Smoke test automas-hsr-adapter-m7a wheel
if: steps.release.outputs.m7a_selected == 'true'
run: >-
uv run python scripts/smoke_wheels.py dist
uv run python scripts/smoke_wheels.py dist/m7a
--mode metadata-only
--expected-package "$PACKAGE"
--expected-package automas-hsr-adapter-m7a
- uses: actions/upload-artifact@v4
if: steps.release.outputs.m7a_selected == 'true'
with:
name: ${{ steps.release.outputs.artifact_name }}
path: dist/*
name: ${{ steps.release.outputs.m7a_artifact }}
path: dist/m7a/*
if-no-files-found: error
retention-days: 7

publish:
- name: Build automas-hsr
if: steps.release.outputs.meta_selected == 'true'
run: >-
uv run python -m build packages/automas_hsr
--outdir dist/meta
- name: Check automas-hsr metadata
if: steps.release.outputs.meta_selected == 'true'
run: uv run twine check dist/meta/*
- name: Smoke test automas-hsr wheel
if: steps.release.outputs.meta_selected == 'true'
run: >-
uv run python scripts/smoke_wheels.py dist/meta
--mode metadata-only
--expected-package automas-hsr
- uses: actions/upload-artifact@v4
if: steps.release.outputs.meta_selected == 'true'
with:
name: ${{ steps.release.outputs.meta_artifact }}
path: dist/meta/*
if-no-files-found: error
retention-days: 7

publish_script:
name: Publish automas-script-hsr
needs: build
if: needs.build.outputs.script_selected == 'true'
runs-on: ubuntu-latest
environment:
name: pypi-script-hsr
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.script_artifact }}
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
skip-existing: true

publish_sra:
name: Publish automas-hsr-adapter-sra
needs: [build, publish_script]
if: ${{ always() && needs.build.result == 'success' && needs.build.outputs.sra_selected == 'true' && (needs.publish_script.result == 'success' || needs.publish_script.result == 'skipped') }}
runs-on: ubuntu-latest
environment:
name: pypi-adapter-sra
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.sra_artifact }}
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
skip-existing: true

publish_m7a:
name: Publish automas-hsr-adapter-m7a
needs: [build, publish_script, publish_sra]
if: ${{ always() && needs.build.result == 'success' && needs.build.outputs.m7a_selected == 'true' && (needs.publish_script.result == 'success' || needs.publish_script.result == 'skipped') && (needs.publish_sra.result == 'success' || needs.publish_sra.result == 'skipped') }}
runs-on: ubuntu-latest
environment:
name: pypi-adapter-m7a
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.m7a_artifact }}
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
skip-existing: true

publish_meta:
name: Publish automas-hsr
needs: [build, publish_script, publish_sra, publish_m7a]
if: ${{ always() && needs.build.result == 'success' && needs.build.outputs.meta_selected == 'true' && (needs.publish_script.result == 'success' || needs.publish_script.result == 'skipped') && (needs.publish_sra.result == 'success' || needs.publish_sra.result == 'skipped') && (needs.publish_m7a.result == 'success' || needs.publish_m7a.result == 'skipped') }}
runs-on: ubuntu-latest
environment:
name: ${{ needs.build.outputs.environment }}
name: pypi-hsr
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.artifact_name }}
name: ${{ needs.build.outputs.meta_artifact }}
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
skip-existing: true
29 changes: 29 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,32 @@
- 修改包的代码或发行契约时,同步更新对应测试、版本和内部依赖下限。
- 提交前运行 `uv run python -m unittest discover -s tests -v` 和 `uv run python scripts/build_all.py`。
- 不提交 `dist/`、`build/`、`*.egg-info`、虚拟环境或 Python 缓存。

## 版本号与 PyPI 自动发布

本仓库的 `.github/workflows/publish.yml` 会监视 `main` 上四个 distribution 的
`pyproject.toml`。一旦某个 `[project].version` 相比 push 前的提交发生变化,该包就会
自动测试、构建,并通过 PyPI Trusted Publishing 上传;修改其他字段但不修改版本号
不会发布。

版本文件与 distribution 的对应关系:

- `packages/automas_script_hsr/pyproject.toml` → `automas-script-hsr`
- `packages/automas_hsr_adapter_sra/pyproject.toml` → `automas-hsr-adapter-sra`
- `packages/automas_hsr_adapter_m7a/pyproject.toml` → `automas-hsr-adapter-m7a`
- `packages/automas_hsr/pyproject.toml` → `automas-hsr`

Agent 修改版本时必须遵守:

- 修改版本号等同于安排该版本在合入 `main` 后正式发布;动手前应明确告知维护者这一副作用。
- PyPI 版本和发行文件不可覆盖。不得复用已经发布的版本号,也不得通过降版本来覆盖旧包。
- 同一提交可以提升多个包的版本。工作流会只运行一次全仓测试和构建,再固定按
`automas-script-hsr` → SRA adapter → M7A adapter → `automas-hsr` 的顺序发布。
- 核心包版本变化时,同步检查两个 adapter 的核心依赖下限;发行契约变化时按需提升
adapter 和聚合包版本。不要为了“保持一致”无条件提升没有变化的 distribution。
- 自动发布全局串行,失败不会越过前序包继续发布;修复后重新运行原 workflow 即可。
上传步骤允许跳过同版本的既有文件,仅用于部分成功后的安全重跑,不代表允许复用版本。
- `workflow_dispatch` 和包级 tag 仍作为人工恢复入口。手动发布的版本必须与对应
`pyproject.toml` 完全一致;tag 必须指向 `main` 历史中的提交。
- GitHub environment 与 PyPI Publisher 名称属于发布契约,不得随意改名:
`pypi-script-hsr`、`pypi-adapter-sra`、`pypi-adapter-m7a`、`pypi-hsr`。
24 changes: 18 additions & 6 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
- `pypi-adapter-sra`
- `pypi-adapter-m7a`

每个 environment 应配置 required reviewers,并限制部署来源:允许 `main` 用于首次
手动发布,允许对应的包版本 tag 用于后续自动发布。仓库和 environment 都不保存
PyPI 用户名、密码或长期 token。
每个 environment 应限制部署来源:允许 `main` 用于版本变更自动发布和手动恢复,
允许对应的包版本 tag 用于 tag 恢复发布。若配置 required reviewers,自动发布会在
上传前等待人工审批。仓库和 environment 都不保存 PyPI 用户名、密码或长期 token。

## PyPI Trusted Publishing

在 PyPI 的 Publishing 页面为三个项目分别创建 pending publisher:
在 PyPI 的 Publishing 页面为四个项目分别创建 pending publisher:

| PyPI 项目 | Owner | Repository | Workflow | Environment |
| --- | --- | --- | --- | --- |
Expand Down Expand Up @@ -60,9 +60,21 @@ environment 名必须与表格完全一致。
PyPI。随后,同一份已验证 artifact 才交给发布 job。手动发布若不在 `main`,或填写
版本与项目版本不一致,会在获取 PyPI OIDC 权限前失败。

## 后续 tag 发布
## 版本变更自动发布

后续版本使用包级 tag,tag 中版本必须与对应 `pyproject.toml` 一致:
首次发布完成后,默认发布入口是合入 `main` 的版本号变化。工作流比较 push 前后的四个
`pyproject.toml`;只有 `[project].version` 变化的 distribution 会进入发布计划。
同一提交可以修改多个包,工作流会先统一测试、构建,再固定按核心包、SRA adapter、
M7A adapter、聚合包的顺序上传。没有版本变化时不会上传。

PyPI 版本不可覆盖。必须先设置未使用的新版本号,再合入 `main`;不要用重新运行来替代
升版本。工作流允许跳过已经存在的同版本文件,仅用于前面部分包已成功、后面包失败时
安全重跑整条流程。

## 手动与 tag 恢复发布

`workflow_dispatch` 保留用于单包人工恢复。包级 tag 也会触发同一套验证和上传;tag 中
版本必须与对应 `pyproject.toml` 一致:

```text
automas-hsr-v0.1.1
Expand Down
Loading
Loading