fixed # 修复部分工作流BUG

This commit is contained in:
2026-10-27 18:43:42 +08:00
parent d05b2e5765
commit c6b504924e
16 changed files with 181 additions and 33 deletions
+3 -3
View File
@@ -6,7 +6,7 @@ on:
workflow_dispatch:
inputs:
publish:
description: Sync main branch in NoneBot repository
description: Sync NoneBot repository and trigger PyPI version release
default: false
type: boolean
permissions:
@@ -102,9 +102,9 @@ jobs:
with:
name: nonebot-preview
path: incoming
- name: Sync validated NoneBot repository
- name: Sync NoneBot repository and trigger version release
env:
FARM_RELEASE_TOKEN: ${{ secrets.FARM_RELEASE_TOKEN }}
run: >-
python scripts/publish_nonebot.py
incoming/nonebot_plugin_farm-*.zip --publish
incoming/nonebot_plugin_farm-*.zip --publish --release-tag
+1
View File
@@ -6,6 +6,7 @@
| 文档 | 用途 |
|---|---|
| [nonebot-release.md](nonebot-release.md) | NoneBot 同步、自动版本标签、PyPI发布及失败重跑 |
| [catalogue-updates.md](catalogue-updates.md) | Service 作物维护、发布、启动更新、缓存与兼容回滚 |
| [23-seasonal-events-and-rust-service.md](23-seasonal-events-and-rust-service.md) | 节日活动交互备选、Rust 发布中心、同步、资产事务、过期清理与上线规划(草案) |
| [09-foundation.md](09-foundation.md) | 已实现的第一批核心、API、测试与限制 |
+34
View File
@@ -0,0 +1,34 @@
# NoneBot 发布与版本更新
主仓库 `version.txt` 是版本来源。NoneBot preview 工作流通过测试并生成插件后,
在主仓库 `main` 推送或手动勾选 `publish` 时,执行以下操作:
1. 校验生成包及来源工作区状态。
2. 同步 `Shu-Ying/nonebot_plugin_farm` 的 `main` 分支。
3. 在本次同步提交上创建 `v<version.txt>` 标签。
4. 标签触发独立仓库的“🚀 发布到 PyPI”工作流,构建、校验并上传发行包。
PR及未勾选publish的手动运行只构建测试,不同步、不打标签。脚本未传
`--publish` 时仍是离线预演;`--release-tag` 明确开启自动标签步骤。
标签创建成功只表示触发条件已满足,不表示下游PyPI发布或商店检测已成功。
## 凭据与发布前提
- 主仓库受保护环境中继续使用 `FARM_RELEASE_TOKEN`,必须是可写目标仓库
内容及工作流的PAT或GitHub App token;不能替换为普通 `GITHUB_TOKEN`。
- NoneBot独立仓库需允许Actions运行,并已配置有效的 `PYPI_TOKEN`。
- 目标仓库标签规则也必须允许该发布身份创建 `v*` 标签。
- 发布新内容前递增主仓库 `version.txt`;不要在发布仓库手工改生成的版本文件。
## 重跑和失败处理
已有同版本标签时,比较标签中生成清单的版本、包身份和全部文件摘要;内容
一致则跳过同步和重复触发。只有来源提交信息变化而插件文件相同也会跳过。
若内容不同则拒绝,并提示升级 `version.txt`;脚本不删除或移动旧标签。
同步main成功但创建标签失败时,工作流会报错,保留已同步代码;修复权限等
原因后重跑。若标签已创建而下游PyPI上传失败,应在独立仓库重新运行失败的
发布任务;上游重跑不会再次创建同名标签。PyPI已占用的版本不能靠移动标签
覆盖,应使用新版本。
本地测试使用模拟GitHub请求,不会创建远端标签或上传PyPI。
+1 -2
View File
@@ -3,7 +3,6 @@
from nonebot.plugin import PluginMetadata
from zhenxun.configs.utils import Command, PluginExtraData, RegisterConfig
from ._version import __version__
from .runtime import COMMANDS, HELP
__plugin_meta__ = PluginMetadata(
@@ -12,7 +11,7 @@ __plugin_meta__ = PluginMetadata(
homepage="https://github.com/Shu-Ying/zhenxun_plugin_farm",
supported_adapters={"~onebot.v11", "~qq"},
extra=PluginExtraData(
author="Shu-Ying", version=__version__, menu_type="群内小游戏",
author="Shu-Ying", version="__FARM_VERSION__", menu_type="群内小游戏",
commands=[Command(command=name) for name in COMMANDS],
configs=[RegisterConfig(
key="农场图片质量", value=75, default_value=75, type=int,
+2 -2
View File
@@ -49,9 +49,9 @@
---
## 如何安装
在Astrbot的插件商店下载即可
方法一(推荐):在小真寻后台的插件商店下载即可<br>
方法二:下载源码放在小真寻`plugin`目录下
`在安装后,每次启动均会向`[farm_service](https://github.com/Shu-Ying/farm_service)`仓库进行下载资源,默认使用Github进行下载,可以在设置中切换为Gitea镜像`
---
+3 -16
View File
@@ -50,24 +50,11 @@
## 如何安装
使用真寻分发仓库的完整生成内容或Actions中的真寻预览包,不能直接
复制主仓库的integrations目录。先停止机器人,备份代码与数据,停用
会注册同名命令的旧插件,再在宿主环境安装本插件requirements.txt。
Pillow要求>=10.4,<11,不强制升级宿主到12。安装后手工启动并检查日志。
方法一(推荐):在小真寻后台的插件商店下载即可
默认数据目录为真寻DATA_PATH下的farm_db,新库为farm.sqlite3,日志为
farm-preview.log;旧farm.db不得直接改名覆盖。资源源默认Github,可在
网页配置选择Gitea/farm_service;服务地址独立保留,修改资源源后重载。
图片质量默认75、整数1~95,仅“我的农场 高清”支持高清且质量至少85。
资源或字体下载失败时文字业务仍可用,详细原因见日志。
方法二:下载源码放在小真寻`plugin`目录下
升级前停机备份完整代码与数据目录;安装器拒绝覆盖,先移走旧目录。
回退需匹配代码和数据库备份,不让旧代码强行打开新schema;恢复旧数据
会丢失备份后进度。卸载时停止加载并移走插件代码,保留数据供恢复。
开发者构建、包验证、安全安装、配置、备份卸载与Secrets详见
[当前维护说明](https://github.com/Shu-Ying/main_plugin_farm/blob/main/docs/zhenxun-preview-current.md)。
本地离线验证不代表真实真寻宿主、公开资源或远端发布已验证。
`在安装后,每次启动均会向`[farm_service](https://github.com/Shu-Ying/farm_service)`仓库进行下载资源,默认使用Github进行下载,可以在设置中切换为Gitea镜像`
---
+3 -3
View File
@@ -31,12 +31,12 @@ jobs:
raise SystemExit('Tag differs from version.txt')
PY
- run: python -m pip install build twine
- run: python -m build --sdist --wheel --outdir ../packages
- run: python -m twine check ../packages/*
- run: python -m build --sdist --wheel --outdir dist
- run: python -m twine check dist/*
- uses: actions/upload-artifact@v7
with:
name: pypi-distributions
path: ../packages/*
path: dist/*
if-no-files-found: error
publish:
needs: build
+2 -2
View File
@@ -18,9 +18,9 @@ jobs:
python-version: '3.10'
- run: python verify_package.py --directory .
- run: python -m pip install build
- run: python -m build --sdist --wheel --outdir ../packages
- run: python -m build --sdist --wheel --outdir dist
- uses: actions/upload-artifact@v7
with:
name: nonebot-distributions
path: ../packages/*
path: dist/*
if-no-files-found: error
+7
View File
@@ -54,6 +54,13 @@ def build(root, output, *, help_image=None):
"requirements.txt", "config_migration.py",
):
files[name] = (integration / name).read_bytes()
# 真寻市场静态读取版本字段,生成时写入字面量而非运行时变量。
marker = b'"__FARM_VERSION__"'
if files['__init__.py'].count(marker) != 1:
raise ValueError('Zhenxun entry requires one version placeholder')
files['__init__.py'] = files['__init__.py'].replace(
marker, json.dumps(version).encode('ascii'),
)
files["runtime.py"] = (
root / "integrations/shared/runtime.py"
).read_bytes()
+17
View File
@@ -97,6 +97,8 @@ def main():
token = sync["steps"][-1]["env"]["FARM_RELEASE_TOKEN"]
if token != "${{ secrets.FARM_RELEASE_TOKEN }}":
raise ValueError("NoneBot must reuse FARM_RELEASE_TOKEN")
if '--release-tag' not in sync['steps'][-1]['run'].split():
raise ValueError('NoneBot sync must create its version release tag')
generated = yaml.load((
root / "release/nonebot/verify.yml"
).read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
@@ -113,6 +115,21 @@ def main():
"github.repository == 'Shu-Ying/nonebot_plugin_farm'"
)):
raise ValueError("NoneBot PyPI boundary differs")
for workflow, job in ((generated, 'verify'), (publishing, 'build')):
steps = workflow['jobs'][job]['steps']
if not any(step.get('run') == (
'python -m build --sdist --wheel --outdir dist'
) for step in steps):
raise ValueError('NoneBot distributions must build inside dist')
upload = next(step for step in steps
if step.get('uses', '').startswith(
'actions/upload-artifact@'))
if (upload['with']['path'] != 'dist/*'
or upload['with'].get('if-no-files-found') != 'error'):
raise ValueError('NoneBot upload path must be dist/*')
if not any(step.get('run') == 'python -m twine check dist/*'
for step in publishing['jobs']['build']['steps']):
raise ValueError('NoneBot distribution check must use dist/*')
steps = publishing["jobs"]["publish"]["steps"]
if steps[-1]["with"]["password"] != "${{ secrets.PYPI_TOKEN }}":
raise ValueError("Keep the existing PyPI secret")
+34 -4
View File
@@ -4,6 +4,7 @@ import argparse
import base64
import json
import os
import re
from urllib.error import HTTPError
from urllib.request import Request, urlopen
@@ -37,13 +38,19 @@ def github(method, path, payload, token, allow_missing=False):
) from None
def publish(package, confirmed=False, request=None):
def publish(package, confirmed=False, request=None, *, release_tag=False):
files = read_package(package)
manifest = verify_files(files)
if not re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z0-9.-]+)?',
manifest['version']):
raise ValueError('Invalid release version in version.txt')
message = commit_message(manifest)
result = {"repository": REPOSITORY, "branch": BRANCH,
"version": manifest["version"], "message": message,
"published": False}
tag = 'v' + manifest['version']
if release_tag:
result.update(tag=tag, tag_created=False)
if not confirmed:
return result
if manifest["source"]["dirty"]:
@@ -53,10 +60,23 @@ def publish(package, confirmed=False, request=None):
raise ValueError("Configure FARM_RELEASE_TOKEN in a protected job")
call = request or (lambda method, path, data: github(
method, path, data, token,
allow_missing=path.startswith(("git/ref/heads/", "contents/")),
allow_missing=path.startswith((
"git/ref/heads/", "git/ref/tags/", "contents/",
)),
))
if not call("GET", "", None):
raise ValueError(f"Target repository is not accessible: {REPOSITORY}")
if release_tag and call('GET', f'git/ref/tags/{tag}', None):
owned = call('GET', f'contents/FARM-MANIFEST.json?ref={tag}', None)
if owned is None or owned.get('encoding') != 'base64':
raise ValueError('Existing release is not managed; bump version.txt')
previous = json.loads(base64.b64decode(owned['content']))
if (previous.get('target') != 'nonebot'
or previous.get('plugin') != manifest['plugin']
or previous.get('version') != manifest['version']
or previous.get('files') != manifest['files']):
raise ValueError('Release tag has different files; bump version.txt')
return {**result, 'tag_reused': True}
ref = call("GET", f"git/ref/heads/{BRANCH}", None)
parent = ref["object"]["sha"] if ref else None
if parent:
@@ -92,15 +112,25 @@ def publish(package, confirmed=False, request=None):
call("POST", "git/refs", {
"ref": f"refs/heads/{BRANCH}", "sha": commit["sha"],
})
return {**result, "published": True, "commit": commit["sha"]}
result.update(published=True, commit=commit['sha'])
if release_tag:
# 仅创建不可变标签;失败保留已同步main,重跑时重新检查,不移动旧标签。
call('POST', 'git/refs', {
'ref': f'refs/tags/{tag}', 'sha': commit['sha'],
})
result['tag_created'] = True
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package")
parser.add_argument("--publish", action="store_true")
parser.add_argument('--release-tag', action='store_true',
help='同步后创建v版本标签,触发目标仓库PyPI发布')
args = parser.parse_args()
print(json.dumps(publish(args.package, args.publish), ensure_ascii=False))
print(json.dumps(publish(args.package, args.publish,
release_tag=args.release_tag), ensure_ascii=False))
if __name__ == "__main__":
+11
View File
@@ -1,6 +1,7 @@
"""独立验证生成的真寻包;仅标准库,不导入或启动插件。"""
import argparse
import ast
import hashlib
import json
from pathlib import Path, PurePosixPath
@@ -45,6 +46,16 @@ def verify_files(files):
raise ValueError('Resource ZIP must not be in plugin repository')
if not required <= set(files):
raise ValueError("Incomplete plugin")
tree = ast.parse(files['__init__.py'])
versions = [
keyword.value for node in ast.walk(tree)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
and node.func.id == 'PluginExtraData'
for keyword in node.keywords if keyword.arg == 'version'
]
if (len(versions) != 1 or not isinstance(versions[0], ast.Constant)
or versions[0].value != manifest['version']):
raise ValueError('PluginExtraData requires matching literal version')
return manifest
+1
View File
@@ -154,6 +154,7 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(plugin.runtime.data_dir,
(self.root / 'existing-custom-data').resolve())
self.assertIsNone(plugin.runtime.font_path)
self.assertEqual(plugin.runtime.resource_source, 'Github')
# 使用真实开发素材预置缓存,模拟宿主测试不访问网络。
manifest = json.loads((ROOT /
'src/farm_render/data/asset-manifest.json').read_bytes())
+56
View File
@@ -165,3 +165,59 @@ class NoneBotPackageTests(unittest.TestCase):
with self.assertRaises((ValueError, RuntimeError)):
publish(bundle, True, request)
self.assertFalse(any(method == "PATCH" for method, _, _ in calls))
def test_release_tag_is_created_after_main_sync(self):
bundle, manifest = self.clean_package()
calls = []
def request(method, path, data):
calls.append((method, path, data))
if path == '':
return {'default_branch': 'main'}
if path.startswith('git/ref/tags/'):
return None
if path.startswith('git/ref/heads/'):
return {'object': {'sha': 'existing'}}
if path.startswith('contents/'):
return None
return {'sha': 'generated'}
with patch.dict('os.environ', {'FARM_RELEASE_TOKEN': 'test'}):
result = publish(bundle, True, request, release_tag=True)
self.assertTrue(result['tag_created'])
self.assertEqual(calls[-1], ('POST', 'git/refs', {
'ref': 'refs/tags/v' + manifest['version'], 'sha': 'generated',
}))
self.assertLess(next(i for i, call in enumerate(calls)
if call[0] == 'PATCH'), len(calls) - 1)
def test_existing_release_tag_is_reused_or_conflict_blocks_all_writes(self):
bundle, manifest = self.clean_package()
for matching in (True, False):
calls = []
previous = json.loads(json.dumps(manifest))
if not matching:
previous['files']['README.md'] = '0' * 64
def request(method, path, data):
calls.append((method, path, data))
if path == '':
return {'default_branch': 'main'}
if path.startswith('git/ref/tags/'):
return {'object': {'sha': 'released'}}
if path.startswith('contents/'):
return {'encoding': 'base64', 'content': base64.b64encode(
json.dumps(previous).encode(),
).decode()}
self.fail('Unexpected request: ' + path)
with patch.dict('os.environ', {'FARM_RELEASE_TOKEN': 'test'}):
if matching:
result = publish(bundle, True, request, release_tag=True)
self.assertFalse(result['tag_created'])
else:
with self.assertRaisesRegex(ValueError, 'version.txt'):
publish(bundle, True, request, release_tag=True)
self.assertTrue(all(method == 'GET' for method, _, _ in calls))
def test_release_tag_dry_run_does_not_access_remote(self):
result = publish(self.bundle, release_tag=True,
request=lambda *args: self.fail('Network in dry run'))
self.assertEqual(result['tag'], 'v' + result['version'])
self.assertFalse(result['tag_created'])
+5
View File
@@ -157,6 +157,11 @@ class ZhenxunPackageTests(unittest.TestCase):
self.assertEqual(archive.read(
"zhenxun_plugin_farm_preview/_version.py",
), b'__version__ = "1.2.3-preview.4"\n')
entry = archive.read(
'zhenxun_plugin_farm_preview/__init__.py',
).decode()
self.assertIn('version="1.2.3-preview.4"', entry)
self.assertNotIn('__version__', entry)
def clean_package(self):
from verify_zhenxun import read_package, MANIFEST, ROOT_NAME
+1 -1
View File
@@ -1 +1 @@
2.0.4
2.0.5