Skip to content

ci: replace the long-dead Travis config with a GitHub Actions gate - #349

Merged
Episkey-G merged 3 commits into
masterfrom
ci/add-pr-gate
Aug 19, 2026
Merged

ci: replace the long-dead Travis config with a GitHub Actions gate#349
Episkey-G merged 3 commits into
masterfrom
ci/add-pr-gate

Conversation

@Episkey-G

Copy link
Copy Markdown
Collaborator

What

  • new .github/workflows/ci.ymlci-syntax / lint / test, aggregated by ci-gate
  • new .github/workflows/compat.yml — Python 3.8 → 3.14, scheduled only
  • new scripts/ci-syntax.sh + scripts/dup-check.py — the two gates
  • Makefile: ci-syntax target; test-cov also emits coverage.xml
  • deleted .travis.yml — it only ever tested 3.5/3.6/3.7 and has not run in years

The two gates

  • G1 python -m compileall -q ucloud — the code can actually be imported
  • G2 scripts/dup-check.py — duplicate top-level defs, duplicate methods in a class, duplicate imports

G2 exists because compileall exits 0 on duplicates (later definition silently wins, which is legal Python, not a syntax error). That is exactly how a generated client can lose a method: two APIs whose names differ only in case both become one snake_case method, and the surviving one is the wrong one.

Only this PR's files block

ci-syntax scans the whole tree but only fails on files changed in this PR; hits in untouched files are printed as warnings and don't affect the exit code.

Without this, one pre-existing defect anywhere in ucloud/services/ would red-flag every product's codegen PR forever. That isn't hypothetical — it has been observed: a release of UHost + UAI-Modelverse, which did not regenerate uphost at all, was still flagged by a stale finding in uphost/client.py.

The changed set comes from git diff --name-only --diff-filter=d "$CI_BASE_SHA...HEAD" -- ucloud (three-dot, so a moving base branch doesn't drag unrelated commits in). CI_BASE_SHA is supplied by the workflow and actions/checkout uses fetch-depth: 0 so the base commit is present. Fail-closed: if the variable is unset, unresolvable, or the diff fails, it falls back to strict whole-tree and prints why. Running make ci-syntax locally with no variable set gives the strict whole-tree result, so local and CI never disagree silently.

No black --check in the gate — deliberate

Formatting is the generator's job (its pipeline runs make fmt), not the gate's, and the two ends cannot be pinned to the same version: the generation pipeline installs black with -U (always latest), so any pin here would drift apart immediately. Measured: 392 files pass under black 24.8.0/24.10.0, while 25.1.0 onward wants 211 of them reformatted (25.x changed docstring edge whitespace).

It also adds nothing to correctness. The historical failure this might be expected to catch — a hyphenated product name backfilled into an invalid Python identifier — is caught by compileall on its own:

def uai-modelverse(self):   →  compileall  exit 1  (SyntaxError)
def  foo( a,b ):            →  compileall  exit 0

Also fixed

tests/test_unit/test_core/test_client.py used collections.Callable, removed in Python 3.10 — the reason make test-cov could not run on any modern interpreter. (Already merged separately as #347.)

Verified locally

Clean venv, Python 3.12.12, pip install -e '.[ci]' exit 0:

case exit
no CI_BASE_SHA (strict whole tree) 1 — one pre-existing duplicate
a commit touching only a clean product 0, with the pre-existing hit still printed as a warning
syntax error injected into a changed file 1
duplicate method injected into a changed file 1
unresolvable CI_BASE_SHA 1, falls back to strict, prints the reason
make lint / make test-cov 0 / 0 (48 passed, 37 skipped)

Expected: ci-gate will be red on merge

One pre-existing duplicate remains — ucloud/services/uphost/client.py:417,499, both named get_phost_disk_upgrade_price. Its root cause was in the data source and has already been fixed upstream, but the file in this repo only gets corrected when UPHost is next regenerated. Until then it is reported as a warning and does not block anything.

test_client_try_import used `isinstance(x, collections.Callable)`.
`collections.Callable` has been an alias for `collections.abc.Callable`
since 3.3 and was removed in 3.10, so the test raises:

    AttributeError: module 'collections' has no attribute 'Callable'

The suite therefore only passes on Python <= 3.9. .travis.yml tests
3.5/3.6/3.7, which is why this was never noticed -- and Travis has not
run on this repo for a long time either.

Uses the builtin `callable()` instead of `collections.abc.Callable`:
same semantics (collections.abc.Callable.__subclasshook__ just checks
for __call__), and it drops the now-unused `collections` import rather
than keeping one around for a single predicate.

Verified on Python 3.11.9:
    before   1 failed, 47 passed, 37 skipped
    after    48 passed, 37 skipped
本仓此前 .github/ 下只有 PR 模板,唯一的 CI 是 .travis.yml(travis-ci.org 已下线,
且只测 3.5/3.6/3.7)——等于 ucloud/services 下全部生成代码从来没有过闸门。
codegen PR 要能自动合并,前提是先有一个「会红」的门禁;但这个门禁必须只对本轮
产物负责,否则它自己就成了自动合并的阻塞源。

- 新增 .github/workflows/ci.yml:触发限 pull_request/push 到 master(不用裸
  [push, pull_request]),显式 permissions: contents: read;ci-syntax / lint /
  test-cov 三个 job 汇总成恒名的 ci-gate(needs + if: always(),任一
  failure/cancelled 即 exit 1),分支保护只需把 ci-gate 配成 required check。
  codecov 上传是 test job 内的 step 且 continue-on-error,不进 ci-gate 的 needs
  ——外部服务抖动不该拦下合并。所有语言命令都经 Makefile target,yml 里不写裸命令。

- 新增 scripts/ci-syntax.sh,Makefile 的 ci-syntax 改为调它。两条闸都只用 stdlib:
  G1 `compileall -q ucloud` 覆盖全部生成代码(现有 lint 显式
  --exclude=ucloud/services,恰好把生成代码排除在外,这层一直是空的)。
  G2 `scripts/dup-check.py` 用 ast 补编译器抓不住的重复声明——python 里重复的
  def/class 不是语法错误,后者静默覆盖前者,compileall 一律 exit 0。

- 判定边界:**全量扫描整棵 ucloud/,但只对本轮变更的文件阻断**,其余命中打印成
  告警、不计入退出码。原因是 G2 在本仓存量就有一处命中——uphost 的
  get_phost_disk_upgrade_price 被生成了两遍,后者是已删除 API 的旧快照(publish
  集合追加式存储,保留了 API 改名前的形态),根因在数据侧、改 SDK 仓库无效。
  整棵树一律阻断的话,发**任何一个**产品(UDisk、UNet……)的 codegen PR 都会被
  这处与本次发布毫无关系的问题判红,mergeable_state 变 unstable,自动合并永久
  失效——那正是这套门禁要解决的问题本身。
  同源决策见 ucloud-sdk-release/scripts/python-fatal-check.sh(2026-07-31),
  已被 pipeline 2507836 实证:那次发的是 UHost+UAI-Modelverse,根本没重新生成
  uphost,那处重复照样被扫了出来。
  变更集取环境变量 CI_BASE_SHA:`git diff --name-only $CI_BASE_SHA...HEAD` 里
  ucloud/ 下的 .py(三点式取 merge-base→HEAD,正好等于本 PR 引入的改动;
  --diff-filter=d 排除删除项)。yml 里 PR 事件给 base.sha、push 事件给 before,
  checkout 配 fetch-depth: 0(浅克隆下 base sha 不在本地对象库,必然解析不出)。
  未设置或解析不出(force push、全零 sha、无共同祖先)→ fail-closed 退回整棵树
  严格判定并打印退回原因。本地不设该变量看到的就是整棵树严格判定,与退回路径
  一致,不存在「本地绿 / CI 红」的分叉。
  dup-check.py 因此加了 --strict-list(仍全量扫描,只有清单内文件的命中才阻断),
  原有的 `dup-check.py <目录>...` 用法不变,本地与退回路径都走它。

- ci-syntax 里**不放** `black --check`,与 go 的 ci-syntax 带 gofmtcheck.sh 不对称,
  是有意为之:生成侧 ucloud-sdk-release 的 python:format job 用
  `pip install -U black` + `make fmt`,永远拿当天最新版格式化后提交;而 black
  每年改一次稳定风格(实测本仓 392 个文件在 24.8.0/24.10.0 下全过,25.1.0 起有
  211 个被判需重排)。校验端无论钉哪个版本,迟早与生成侧偏斜、把每个 codegen PR
  恒判为红——同样是自动合并要解决的问题本身。格式化归生成侧,门禁只管代码能不能
  用:非法标识符这类真缺陷 compileall 已经拦得住(实测 `def uai-modelverse(self):`
  → exit 1,而纯格式的 `def  foo( a,b ):` → exit 0)。理由写在 Makefile 注释里。

- Makefile test-cov 补 --cov-report=term --cov-report=xml:codecov-action 只上传
  现成的报告文件,不像 Travis 时代的 codecov bash uploader 会顺手跑 coverage xml;
  不出 xml 的话 README 上那个 Codecov 徽章就再也不会更新。

- 新增 .github/workflows/compat.yml:只挂 schedule + workflow_dispatch,不挂
  pull_request/push——它问的是「SDK 今天还能跑在哪些 python 上」,与单个 PR 改了
  什么无关,挂到 PR 上只会在每个 codegen PR 上多出一排无关 check、干扰门禁判读。
  矩阵 3.8–3.14、fail-fast: false。下界 3.8 是实测结论,不是 setup.py 声明的 3.5:
  actions/python-versions 对 3.7 最后只出到 3.7.17 且只有 linux-20.04/22.04 产物,
  而 ubuntu-latest 现指向 Ubuntu 24.04,setup-python 根本装不出 3.7;3.8.18 有
  linux-24.04 产物,且本地 3.8.20 上 pip install -e .[ci]、make lint、make test-cov
  均 exit 0(48 passed / 37 skipped)。

- 删 .travis.yml:服务已下线,留着只会让人误以为还有 CI 在跑。

setup.py 不动:CI 不跑 black,[ci] 就不需要它;在 [ci] 里钉 black 版本反而会把
生成侧 python:format 那步的 `pip install -e ".[ci]"` 降级,干扰生成端。

本地实跑(macOS bash 3.2 + python 3.11.9):
  不设 CI_BASE_SHA                       → exit 1,G2 报出 uphost 那处存量
  CI_BASE_SHA=父提交、只改干净的 udisk    → exit 0,uphost 那处降为告警仍打印
  同上但 udisk 注入语法错 / 重复方法      → exit 1(对本轮变更文件仍然阻断)
  CI_BASE_SHA 给不存在的 sha / 全零 sha  → 退回整棵树严格判定,exit 1,打印退回原因
  make lint / make test-cov              → exit 0,未受影响
兼容矩阵是纯巡检,不参与门禁判定,也没人会盯一个非阻断的定时任务。
声明下界还成不成立是另一个问题,要验的时候手工跑一次即可,
不必为它常驻一条每天运行的 workflow。
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@Episkey-G
Episkey-G merged commit 67c2b2e into master Aug 19, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant