From e73df50afde1807fbb813bdccc9f25da1be4464f Mon Sep 17 00:00:00 2001 From: Gu-ZT Date: Tue, 10 Nov 2026 16:31:35 +0800 Subject: [PATCH] =?UTF-8?q?ci(release):=20=E4=BD=BF=E7=94=A8=E8=BE=93?= =?UTF-8?q?=E5=85=A5=20tag=20=E8=A6=86=E7=9B=96=E5=A4=9A=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 必填无 v 前缀的 tag 并覆盖构建与 Central 部署版本 - 按输入后缀决定发布类型和升版规则,保持 Beta 开发快照 - 校验重跑 tag 一致性,并从锁定开发版本安全创建升版 PR --- .github/release/artifacts.py | 4 +- .github/release/central.py | 4 +- .github/release/common.py | 10 ++--- .github/release/release.py | 14 +++---- .github/release/test_release.py | 52 ++++++++++++++++++------- .github/release/test_release_options.py | 39 ++++++++++++++----- .github/workflows/release.yml | 7 +++- RELEASING.md | 14 +++---- 8 files changed, 96 insertions(+), 48 deletions(-) diff --git a/.github/release/artifacts.py b/.github/release/artifacts.py index 7fa5cfae2..03de95eab 100644 --- a/.github/release/artifacts.py +++ b/.github/release/artifacts.py @@ -11,7 +11,7 @@ import zipfile from pathlib import Path -from common import digest, mod_version, require, staging_digest, summary +from common import digest, release_type, require, staging_digest, summary GROUP = "dev.anvilcraft.lib" @@ -31,7 +31,7 @@ def verify_mod(jar, expected_id, version): def collect(source, line, sha, version, dest): actual_sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip() require(actual_sha == sha, f"Expected {sha} for {line}, found {actual_sha}") - require(mod_version((source / "gradle.properties").read_bytes()) == version, "Version drift") + release_type(version) game = tomllib.loads((source / "gradle/libs.versions.toml").read_text())["versions"]["minecraft"] modules = json.loads((source / ".github/modules.json").read_text()) main = f"anvillib-neoforge-{line}" diff --git a/.github/release/central.py b/.github/release/central.py index 5da81a3c8..63e732e81 100644 --- a/.github/release/central.py +++ b/.github/release/central.py @@ -25,7 +25,8 @@ def portal(path): def deploy(tag, line, source, staging): - target = tag_manifest(tag)["targets"][line] + plan = tag_manifest(tag) + target = plan["targets"][line] sha = target["sha"] context = f"release/{tag}/{line}/central" previous = commit_status(sha, context) @@ -49,6 +50,7 @@ def deploy(tag, line, source, staging): # JReleaser 1.23 logs the receipt immediately, before waiting for validation. # Save it here so a cancelled Gradle process can be resumed by deployment ID. with subprocess.Popen(["bash", "gradlew", f":anvillib-neoforge-{line}:jreleaserDeploy", + f"-Pmod_version={plan['version']}", f"-PanvillibCentralStaging={Path(staging).resolve()}", "--git-root-search", "--no-configuration-cache", "--console=plain"], cwd=source, env=dict(os.environ, JRELEASER_MAVENCENTRAL_STAGE="UPLOAD"), diff --git a/.github/release/common.py b/.github/release/common.py index fb7894fe3..7f08b8435 100644 --- a/.github/release/common.py +++ b/.github/release/common.py @@ -64,9 +64,7 @@ def release_type(version): def mod_version(properties): matches = VERSION_LINE.findall(properties) require(len(matches) == 1, "Expected one mod_version property") - version = matches[0][1].decode() - release_type(version) - return version + return matches[0][1].decode() def digest(path, algorithm="sha256"): @@ -106,7 +104,7 @@ def artifact(name): def tag_manifest(tag): - require(re.fullmatch("v" + VERSION, tag), f"Invalid release tag: {tag}") + require(re.fullmatch(VERSION, tag), f"Invalid release tag: {tag}") ref = github(f"git/ref/tags/{tag}", missing=True) if ref is None: return None @@ -114,13 +112,13 @@ def tag_manifest(tag): obj = github("git/tags/" + ref["object"]["sha"]) manifest = json.loads(obj["message"].strip().split("\n\n", 1)[1]) require(manifest["tag"] == tag and manifest["repo"] == repo() - and manifest["version"] == tag[1:] and re.fullmatch(r"[0-9]+", manifest["run"]) + and manifest["version"] == tag and re.fullmatch(r"[0-9]+", manifest["run"]) and set(manifest["targets"]) == set(LINES) and obj["object"]["sha"] == manifest["targets"][ANCHOR]["sha"], "Conflicting release tag") for target in manifest["targets"].values(): require(re.fullmatch(r"[0-9]+(?:\.[0-9]+)+", target["game"]) and re.fullmatch(r"[0-9a-f]{40}", target["sha"]), "Invalid Minecraft version or source SHA") - stem = f"anvillib-neoforge-{target['game']}-{tag[1:]}" + stem = f"anvillib-neoforge-{target['game']}-{tag}" require(target["jar"] == stem + ".jar" and set(target["files"]) == {stem + suffix for suffix in (".jar", "-sources.jar", "-javadoc.jar")} and all(re.fullmatch(r"[0-9a-f]{64}", value) for value in target["files"].values()), diff --git a/.github/release/release.py b/.github/release/release.py index f2fba3df7..591909016 100644 --- a/.github/release/release.py +++ b/.github/release/release.py @@ -15,29 +15,28 @@ def prepare(): require(os.environ["GITHUB_REF"] == f"refs/heads/dev/{ANCHOR}", "Run Create Multi-Version Releases from dev/26.1") + tag = os.environ.get("RELEASE_TAG", "") + release_type(tag) plan_file = Path("release-plan.json") saved = artifact("source-plan") if saved: gh("run", "download", os.environ["GITHUB_RUN_ID"], "-n", "source-plan") plan = json.loads(plan_file.read_text()) + require(plan["tag"] == tag, "Input tag differs from the original release plan") else: refs = github("git/matching-refs/heads/dev/") heads = {ref["ref"]: ref["object"]["sha"] for ref in refs} targets = {} - versions = {} for line in LINES: sha = heads[f"refs/heads/dev/{line}"] if line == ANCHOR: require(sha == os.environ["GITHUB_SHA"], "Default branch advanced before preflight; dispatch again") props = contents("gradle.properties", sha) - versions[line] = mod_version(props) java = re.search(rb"(?m)^java_version=(\d+)\s*$", props).group(1).decode() # Catch an incomplete rollout before scheduling seven builds. contents("gradle/scripts/release.gradle", sha) targets[line] = {"sha": sha, "java": java} - require(len(set(versions.values())) == 1, f"Branches disagree on mod_version: {versions}") - version = versions[ANCHOR] - plan = {"repo": repo(), "tag": "v" + version, "version": version, + plan = {"repo": repo(), "tag": tag, "version": tag, "run": os.environ["GITHUB_RUN_ID"], "targets": targets} if os.environ.get("GENERATE_RELEASE_NOTES") == "true": plan.update(generate_notes(plan, ANCHOR)) @@ -164,7 +163,8 @@ def bump(tag, line): if current == next_version: summary(f"{base} is already at {next_version}.") return - require(current == version, f"{base} is at {current}; expected {version}") + expected = mod_version(contents("gradle.properties", plan["targets"][line]["sha"])) + require(current == expected, f"{base} is at {current}; expected {expected} from the locked source") title = f"chore(release): 将版本推进到 {next_version} [skip ci]" owner = repo().split("/")[0] prior = github(f"pulls?state=all&base={quote(base, safe='')}&head={quote(owner + ':' + head, safe='')}") @@ -178,7 +178,7 @@ def bump(tag, line): and [f["filename"] for f in commit["files"]] == ["gradle.properties"], f"Unexpected content on {head}") parent = contents("gradle.properties", commit["parents"][0]["sha"]) - require(mod_version(parent) == version and contents("gradle.properties", ref["object"]["sha"]) == + require(mod_version(parent) == expected and contents("gradle.properties", ref["object"]["sha"]) == VERSION_LINE.sub(lambda m: m[1] + next_version.encode() + m[3], parent), f"Conflicting {head}") else: updated = VERSION_LINE.sub(lambda m: m[1] + next_version.encode() + m[3], props) diff --git a/.github/release/test_release.py b/.github/release/test_release.py index 9013e0274..1eb28e3ab 100644 --- a/.github/release/test_release.py +++ b/.github/release/test_release.py @@ -13,7 +13,7 @@ import release from common import LINES, digest, mod_version, staging_digest -TAG = "v2.0.0" +TAG = "2.0.0" PLAN = {"tag": TAG, "version": "2.0.0", "targets": { line: {"sha": f"sha-{line}", "game": line, "jar": f"{line}.jar", "files": {f"{line}.jar": line}} for line in LINES}} @@ -28,19 +28,23 @@ def setUp(self): for module in (release, central, mod_sites): self.enterContext(patch.object(module, "summary")) - def test_preflight_rejects_a_branch_with_a_different_version(self): + def test_preflight_uses_input_tag_despite_different_development_versions(self): refs = [{"ref": f"refs/heads/dev/{line}", "object": {"sha": line}} for line in LINES] def content(path, sha): if path != "gradle.properties": return b"" - version = "2.0.1" if sha == "26.2" else "2.0.0" - return f"java_version=25\nmod_version={version}\n".encode() - with patch.dict(os.environ, {"GITHUB_REF": "refs/heads/dev/26.1", "GITHUB_SHA": "26.1"}), \ + return f"java_version=25\nmod_version=development-{sha}\n".encode() + tag = "3.2.0+pre-release.13" + with tempfile.TemporaryDirectory() as tmp, chdir(tmp), \ + patch.dict(os.environ, {"GITHUB_REF": "refs/heads/dev/26.1", "GITHUB_SHA": "26.1", + "RELEASE_TAG": tag, "DRY_RUN": "true", "GENERATE_RELEASE_NOTES": "false"}), \ patch.object(release, "artifact", return_value=None), patch.object(release, "github", return_value=refs), \ patch.object(release, "contents", side_effect=content), patch.object(release, "output") as output: - with self.assertRaisesRegex(ValueError, "Branches disagree"): - release.prepare() - output.assert_not_called() + release.prepare() + plan = json.loads(Path("release-plan.json").read_text()) + self.assertEqual((plan["tag"], plan["version"]), (tag, tag)) + self.assertEqual(set(plan["targets"]), set(LINES)) + self.assertEqual(dict(c.args for c in output.call_args_list)["tag"], tag) def test_staging_digest_covers_extra_and_modified_files(self): with tempfile.TemporaryDirectory() as tmp: @@ -116,6 +120,26 @@ def upload(_release, _upload, _tag, path): release.seal() self.assertEqual((len(tags), len(drafts), len(uploads), len(set(uploads))), (1, 1, 7, 7)) + def test_central_passes_the_release_version_to_gradle(self): + receipt = "bce8fb04-e144-4d32-a5b9-7616a564ef19" + version = "2.0.0+pre-release.13" + plan = dict(PLAN, version=version, tag=version, + targets={"26.1": {"sha": "sha-26.1", "staging_sha256": "checksum"}}) + credentials = {"JRELEASER_" + key: "test" for key in ( + "MAVENCENTRAL_USERNAME", "MAVENCENTRAL_PASSWORD", "GPG_SECRET_KEY", "GPG_PUBLIC_KEY", "GPG_PASSPHRASE")} + with tempfile.TemporaryDirectory() as folder, patch.dict(os.environ, credentials), \ + patch.object(central, "tag_manifest", return_value=plan), \ + patch.object(central, "commit_status", return_value=None), \ + patch.object(central, "staging_digest", return_value="checksum"), \ + patch.object(central, "portal", return_value=b'{"deploymentState": "PUBLISHED"}'), \ + patch.object(central, "record") as record, patch.object(central.subprocess, "Popen") as gradle: + process = gradle.return_value.__enter__.return_value + process.stdout = [f"uploaded as deployment {receipt}\n"] + process.wait.return_value = 0 + central.deploy(version, "26.1", folder, "built/staging") + self.assertIn("-Pmod_version=" + version, gradle.call_args.args[0]) + record.assert_called_with("sha-26.1", f"release/{version}/26.1/central", "success", "deployment " + receipt) + def test_central_continues_from_receipt_after_cancellation(self): receipt = "bce8fb04-e144-4d32-a5b9-7616a564ef19" statuses = [json.dumps({"deploymentState": state}).encode() @@ -163,8 +187,8 @@ def test_uncertain_upload_requires_reconciliation(self): upload.assert_not_called() def test_bump_preserves_bytes_and_reuses_single_commit_and_pr(self): - original = b"# Config\r\nmod_version=2.0.0\r\njava_version=25\r\n" - files = {"base": original} + original = b"# Config\r\nmod_version=1.6.0+snapshot.42\r\njava_version=25\r\n" + files = {"base": original, "sha-26.1": original} head, prs, payloads = [], [], [] def github(path, data=None, **kwargs): if data is not None: @@ -198,13 +222,13 @@ def github(path, data=None, **kwargs): patch.object(release, "github", side_effect=github): release.bump(TAG, "26.1") release.bump(TAG, "26.1") - self.assertEqual(files["bump"], original.replace(b"2.0.0", b"2.0.1")) + self.assertEqual(files["bump"], original.replace(b"1.6.0+snapshot.42", b"2.0.1")) self.assertEqual(len(payloads), 4) self.assertEqual(prs[0]["title"], next(d["message"] for key, d in payloads if key == "git/commits")) self.assertIn("[skip ci]", prs[0]["title"]) files["base"] = files["bump"] release.bump(TAG, "26.1") - files["base"] = original.replace(b"2.0.0", b"2.1.0") + files["base"] = original.replace(b"1.6.0+snapshot.42", b"2.1.0") with self.assertRaisesRegex(ValueError, "expected"): release.bump(TAG, "26.1") files["base"] = original @@ -213,8 +237,8 @@ def github(path, data=None, **kwargs): release.bump(TAG, "26.1") self.assertEqual(len(payloads), 4) - def test_invalid_release_versions(self): - for raw in (b"", b"mod_version=2.0", b"mod_version=02.0.0", b"mod_version=2.0.0+snapshot.5", + def test_requires_one_nonempty_development_version(self): + for raw in (b"", b"mod_version=", b"mod_version= ", b"mod_version=2.0.0\nmod_version=2.0.1"): with self.assertRaises(ValueError): mod_version(raw) diff --git a/.github/release/test_release_options.py b/.github/release/test_release_options.py index f22a4ce21..3ded46a81 100644 --- a/.github/release/test_release_options.py +++ b/.github/release/test_release_options.py @@ -42,10 +42,10 @@ def test_exact_version_formats_and_types(self): with self.subTest(version=version): self.assertEqual(common.mod_version(f"mod_version={version}\r\n".encode()), version) self.assertEqual(common.release_type(version), kind) - for version in ("2.0.0-beta.1", "2.0.0+alpha", "2.0.0+rc.01", "2.0.0+beta.-1", + for version in ("2.0", "02.0.0", "2.0.0-beta.1", "2.0.0+alpha", "2.0.0+rc.01", "2.0.0+beta.-1", "2.0.0+snapshot.5", "2.0.0+unknown.1", "2.0.0+build.1+rc.2", "2.0.0+hotfix.1/other"): with self.subTest(invalid=version), self.assertRaises(ValueError): - common.mod_version(f"mod_version={version}".encode()) + common.release_type(version) def test_prepare_freezes_native_notes_and_bump_policy(self): refs = [{"ref": f"refs/heads/dev/{line}", "object": {"sha": f"sha-{line}"}} for line in common.LINES] @@ -53,13 +53,13 @@ def test_prepare_freezes_native_notes_and_bump_policy(self): for version, _, bump in CASES: for enabled in (False, True): def content(path, _sha): - return f"mod_version={version}\njava_version=25\n".encode() if path == "gradle.properties" else b"" + return b"mod_version=1.6.0\njava_version=25\n" if path == "gradle.properties" else b"" def api(path, data=None): if path == "git/matching-refs/heads/dev/": return refs self.fail(path) with self.subTest(version=version, notes=enabled), tempfile.TemporaryDirectory() as tmp, chdir(tmp), \ - patch.dict(os.environ, {"GENERATE_RELEASE_NOTES": str(enabled).lower(), "DRY_RUN": "true"}), \ + patch.dict(os.environ, {"GENERATE_RELEASE_NOTES": str(enabled).lower(), "DRY_RUN": "true", "RELEASE_TAG": version}), \ patch.object(release, "artifact", side_effect=[None, {"id": 1}]), \ patch.object(release, "contents", side_effect=content), \ patch.object(release, "github", side_effect=api) as github, \ @@ -77,9 +77,28 @@ def api(path, data=None): self.assertEqual(plan.get("previous_tag_name"), "v1.9.0" if enabled else None) self.assertEqual(json.loads(Path("release-plan.json").read_text()), plan) + def test_invalid_tag_is_rejected_before_preflight(self): + for tag in ("", "v2.0.0", "2.0", "2.0.0-beta.1", "2.0.0+snapshot.5", "2.0.0\n", "$(example)"): + with self.subTest(tag=tag), patch.dict(os.environ, {"RELEASE_TAG": tag}), \ + patch.object(release, "artifact") as artifact, patch.object(release, "github") as github: + with self.assertRaises(ValueError): + release.prepare() + artifact.assert_not_called() + github.assert_not_called() + + def test_rerun_cannot_relabel_a_frozen_plan(self): + with tempfile.TemporaryDirectory() as tmp, chdir(tmp), \ + patch.dict(os.environ, {"RELEASE_TAG": "2.0.1"}), \ + patch.object(release, "artifact", return_value={"id": 1}), patch.object(release, "gh"), \ + patch.object(release, "github") as github: + Path("release-plan.json").write_text(json.dumps(PLAN)) + with self.assertRaisesRegex(ValueError, "tag differs"): + release.prepare() + github.assert_not_called() + def test_qualified_tags_preserve_version_and_original_notes(self): for version, _, _ in CASES: - tag = "v" + version + tag = version targets = {} for line in common.LINES: stem = f"anvillib-neoforge-{line}-{version}" @@ -97,7 +116,7 @@ def test_qualified_tags_preserve_version_and_original_notes(self): def test_seal_preserves_generated_notes_when_reusing_a_draft(self): native = "## What's Changed\n* PR #42 by @contributor\n" - plan = dict(PLAN, tag="v2.0.0+beta.1", version="2.0.0+beta.1", release_notes=native) + plan = dict(PLAN, tag="2.0.0+beta.1", version="2.0.0+beta.1", release_notes=native) drafts, tags = [], [] def api(path, data=None, **kwargs): if path == "git/tags": @@ -136,7 +155,7 @@ def test_publication_type_and_latest_follow_the_version(self): patch.object(release, "release_by_tag", return_value=draft), \ patch.object(release, "commit_status", return_value={"state": "success"}), \ patch.object(release, "github", return_value={"html_url": "example"}) as github: - release.publish("v" + version) + release.publish(version) github.assert_called_once_with("releases/42", { "draft": False, "prerelease": prerelease, "make_latest": "false" if prerelease else "true", }, method="PATCH") @@ -148,10 +167,10 @@ def test_prerelease_waits_for_central_and_rejects_type_drift(self): patch.object(release, "commit_status", return_value={"state": "pending"}), \ patch.object(release, "github") as github: with self.assertRaisesRegex(ValueError, "Central has not published"): - release.publish("v" + plan["version"]) + release.publish(plan["version"]) lookup.return_value = dict(DRAFT, prerelease=False) with self.assertRaisesRegex(ValueError, "Conflicting release type"): - release.publish("v" + plan["version"]) + release.publish(plan["version"]) github.assert_not_called() def test_all_qualified_versions_skip_bump_without_writing(self): @@ -161,7 +180,7 @@ def test_all_qualified_versions_skip_bump_without_writing(self): with self.subTest(version=version), patch.object(release, "tag_manifest", return_value=dict(PLAN, version=version)), \ patch.object(release, "release_by_tag", return_value={"draft": False}), \ patch.object(release, "github") as github, patch.object(release, "contents") as contents: - release.bump("v" + version, "26.1") + release.bump(version, "26.1") github.assert_not_called() contents.assert_not_called() diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1d4b6399..74e4c6fe5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,10 @@ name: Create Multi-Version Releases on: workflow_dispatch: inputs: + tag: + description: 发布 tag 与构建版本,不带 v(例如 2.0.0+pre-release.13) + type: string + required: true generate_release_notes: description: Include GitHub-generated release notes from dev/26.1 type: boolean @@ -21,6 +25,7 @@ env: CI_BUILD: 'false' PR_BUILD: 'false' NOT_DEV: 'false' + RELEASE_TAG: ${{ inputs.tag }} DRY_RUN: ${{ inputs.dry_run }} GENERATE_RELEASE_NOTES: ${{ inputs.generate_release_notes }} @@ -83,7 +88,7 @@ jobs: - name: Stage and verify local publications if: steps.cache.outputs.built != 'True' run: | - (cd source && bash gradlew -PanvillibRelease stageRelease --no-configuration-cache) + (cd source && bash gradlew -PanvillibRelease "-Pmod_version=$RELEASE_VERSION" stageRelease --no-configuration-cache) python3 control/.github/release/artifacts.py source "$LINE" "$SOURCE_SHA" "$RELEASE_VERSION" source/build/release/dist - uses: actions/upload-artifact@v7 if: steps.cache.outputs.built != 'True' diff --git a/RELEASING.md b/RELEASING.md index 144820ff9..b9d9fc885 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,6 +1,6 @@ # AnvilLib 发布 -在默认分支 `dev/26.1` 打开 **Actions → Create Multi-Version Releases → Run workflow**。勾选 `dry_run` 可先构建七个版本并查看文件校验表,取消勾选后执行发布。同一时间只运行一次发布或演练。 +在默认分支 `dev/26.1` 打开 **Actions → Create Multi-Version Releases → Run workflow**,在必填的 `tag` 中输入本次版本,例如 `2.0.0+pre-release.13`,不加 `v` 前缀。勾选 `dry_run` 可先构建七个版本并查看文件校验表,取消勾选后执行发布。同一时间只运行一次发布或演练。 可选的 `generate_release_notes` 默认关闭。开启后直接调用 GitHub 的 Generate release notes API,以本次锁定的 `dev/26.1` SHA 为目标,显式指定上一 tag 生成合并 PR、贡献者及完整变更链接。正文采用 GitHub 的返回内容,并附上七份构件的源码和校验表。仓库的 `.github/release.yml` 或 `.github/release.yaml` 配置同样生效。结果保存在原运行的 `source-plan` 和 tag 注释中,重跑复用该内容,`dry_run` 摘要也会展示它。 @@ -8,9 +8,9 @@ ## 版本与构件 -七条开发线为 `1.21.1`、`1.21.4`、`1.21.8`、`1.21.11`、`26.1`、`26.2`、`26.3`。预检读取所有分支头,要求根 `mod_version` 完全相同。版本类型按以下项目约定自动识别,后缀使用 `+`,`N` 为无前导零的非负整数。 +七条开发线为 `1.21.1`、`1.21.4`、`1.21.8`、`1.21.11`、`26.1`、`26.2`、`26.3`。预检锁定所有分支头,发布版本只取手动输入的 `tag`。构建和 Central 部署通过 `-Pmod_version=` 覆盖 Gradle 属性,不修改源码中的 `gradle.properties`,也不要求各分支的开发版本一致。版本类型按以下项目约定自动识别,后缀使用 `+`,`N` 为无前导零的非负整数。 -| mod_version | GitHub 类型 | Modrinth / CurseForge 类型 | 发布成功后升 patch | +| 输入 tag | GitHub 类型 | Modrinth / CurseForge 类型 | 发布成功后升 patch | | --- | --- | --- | --- | | `X.Y.Z+alpha.N` | Pre-release | Alpha | 否 | | `X.Y.Z+beta.N` | Pre-release | Beta | 否 | @@ -20,11 +20,11 @@ | `X.Y.Z+hotfix.N` | Release | Release | 否 | | `X.Y.Z` | Release | Release | 是 | -例如发布 `2.0.0+beta.1` 前先将七条分支的版本同步到该值,生成的 tag 为 `v2.0.0+beta.1`,Maven 坐标和 JAR 内版本保留完整后缀。后续发 `2.0.0+rc.1` 或 `2.0.0` 时,再将七条分支同步到对应版本。所有类型都等待七个 Maven Central 部署达到 `PUBLISHED` 后公开 GitHub Release。Pre-release 使用独立 tag,GitHub 的 Latest 标记由正式版维护。 +例如输入 `2.0.0+beta.1`,生成的 tag 就是 `2.0.0+beta.1`,Maven 坐标和 JAR 内版本使用该完整值。后续发布 `2.0.0+rc.1` 或 `2.0.0` 时,在输入框填写对应 tag 即可。所有类型都等待七个 Maven Central 部署达到 `PUBLISHED` 后公开 GitHub Release。Pre-release 使用独立 tag,GitHub 的 Latest 标记由正式版维护。 每条分支用自己的 Gradle Wrapper 和 JDK 从锁定的 SHA 构建。`stageRelease` 收集该分支模块清单中的全部发布单元、sync 注解处理器和主模块。校验覆盖 POM 的内部依赖、主 JAR 的模组版本、每个内嵌 slim JAR 的字节及 Maven 附件。 -带注解 tag `v` 指向锁定的默认分支提交。运行编号、七个源码 SHA、平台项目 ID 和供下载文件的 SHA-256 都在 tag 注释中。GitHub Release 展示七个主 JAR 及对应的源码链接。Minecraft 文件名取自实际游戏版本,例如 `dev/26.1` 构建的文件名中使用 `26.1.2`。 +带注解 tag `` 指向锁定的默认分支提交。运行编号、七个源码 SHA、平台项目 ID 和供下载文件的 SHA-256 都在 tag 注释中。GitHub Release 展示七个主 JAR 及对应的源码链接。Minecraft 文件名取自实际游戏版本,例如 `dev/26.1` 构建的文件名中使用 `26.1.2`。 总控自动运行 Modrinth、CurseForge、Maven 和 ReleaseFile 上传,不提供平台选择。Maven 或 ReleaseFile 失败会使整体运行失败并保留草稿;两站独立运行,失败只记录警告和回执,公开 Release 与升版任务不等待两站。 @@ -34,7 +34,7 @@ | GitHub | 七个附件的摘要与 tag 清单一致,七个 Central 成功回执已记录 | 在原运行续传缺失附件或重试公开草稿 | | Modrinth、CurseForge | 各自接收各游戏版本的主包和 sources、javadoc 附件 | 对应任务给出警告,不重试;GitHub 公开和升版 PR 独立执行 | -纯 `X.Y.Z` 的 Release 公开后,工作流对七条开发分支分别创建一个升至下一 patch 的 PR。标题和单一提交消息都包含 `[skip ci]`。按仓库现有规则审核并保留默认 squash 标题后合并。重跑会复用升版分支与 PR。待七条分支版本一致后再发下一版。普通开发推送继续发布内部 Maven 快照以及两站的 Beta 构建。 +纯 `X.Y.Z` 的 Release 公开后,工作流对七条开发分支分别创建一个升至下一 patch 的 PR。标题和单一提交消息都包含 `[skip ci]`。按仓库现有规则审核并保留默认 squash 标题后合并。下一 patch 按输入 tag 计算,并从分支最新提交创建 PR;仅当根版本仍等于锁定源码中的开发版本时才修改,若已是目标版本则记为完成,其他版本报告冲突。重跑会复用升版分支与 PR。普通开发推送继续发布内部 Maven 快照以及两站的 Beta 构建。 ## 开发 CI @@ -46,7 +46,7 @@ PR 使用 `+pr.N` 版本,运行服务端启动与关闭测试,并为子模 - 正式流程中断时优先重跑原运行。已完成的构建通过原运行的 Actions artifact 复用。已存在的 tag、草稿附件和远端回执须与本次版本吻合。可在 tag 的注释中找到原运行编号。 - 两站上传各执行一次。原运行重跑时复用成功回执,已有失败或未决记录不会再次上传。Modrinth 可核对远端文件后补全成功回执。 -- 发布进度记录在每条开发线的源码提交状态中。状态名形如 `release/v2.0.0/26.1/central` 或 `release/v2.0.0/26.1/curseforge/sources`。每条记录链接到产生它的 Actions 运行。CurseForge 为主文件和两个子文件分别记录回执,Modrinth 可从主文件的 SHA-512 找回已接收的版本。 +- 发布进度记录在每条开发线的源码提交状态中。状态名形如 `release/2.0.0/26.1/central` 或 `release/2.0.0/26.1/curseforge/sources`。每条记录链接到产生它的 Actions 运行。CurseForge 为主文件和两个子文件分别记录回执,Modrinth 可从主文件的 SHA-512 找回已接收的版本。 - Central 日志输出 deployment ID 时会立即记录,重跑继续查询同一 ID。Central 明确拒绝部署时,摘要或错误日志会给出原因。若上传请求已发出却没有回执,先在 Central Portal 或 CurseForge 作者后台核对项目、版本、文件名和文件哈希。确认后可用下方命令补录状态,再重跑原任务。 ```sh