From 5cb50f830075f21d8abbd6f7745ad57ef68ef881 Mon Sep 17 00:00:00 2001 From: wangkang1995 <1226426603@qq.com> Date: Sun, 5 Jul 2026 19:09:01 +0800 Subject: [PATCH 1/2] [work-enter] [home-window] [codex] Normalize signing key id before Central publish --- .github/scripts/normalize_signing_key_id.py | 94 +-- .../scripts/normalize_signing_key_id_test.py | 74 +- .../publish_plugin_central_workflow_test.py | 220 +++--- .github/workflows/publish-plugin-central.yml | 686 +++++++++--------- .github/workflows/publish-plugin-pr-check.yml | 268 +++---- 5 files changed, 671 insertions(+), 671 deletions(-) diff --git a/.github/scripts/normalize_signing_key_id.py b/.github/scripts/normalize_signing_key_id.py index 5a8f703..faec2e1 100644 --- a/.github/scripts/normalize_signing_key_id.py +++ b/.github/scripts/normalize_signing_key_id.py @@ -1,47 +1,47 @@ -import re -import sys - - -SHORT_OR_LONG_KEY_ID = re.compile(r"^(0x)?[0-9A-Fa-f]{8,16}$") -FINGERPRINT = re.compile(r"^[0-9A-Fa-f]{40}$") - - -def normalize_signing_key_id(value): - key_id = value.strip() - if not key_id: - return "" - if SHORT_OR_LONG_KEY_ID.fullmatch(key_id): - return key_id - if FINGERPRINT.fullmatch(key_id): - return key_id[-16:] - return "" - - -def shell_exports(value): - key_id = normalize_signing_key_id(value) - if not key_id: - return "\n".join( - ( - "unset SIGNING_KEY_ID", - "unset SIGNING_IN_MEMORY_KEY_ID", - "unset ORG_GRADLE_PROJECT_signingInMemoryKeyId", - ) - ) - - escaped = key_id.replace("'", "'\"'\"'") - return "\n".join( - ( - f"export SIGNING_KEY_ID='{escaped}'", - f"export SIGNING_IN_MEMORY_KEY_ID='{escaped}'", - f"export ORG_GRADLE_PROJECT_signingInMemoryKeyId='{escaped}'", - ) - ) - - -def main(): - value = sys.argv[1] if len(sys.argv) > 1 else "" - print(shell_exports(value)) - - -if __name__ == "__main__": - main() +import re +import sys + + +SHORT_OR_LONG_KEY_ID = re.compile(r"^(0x)?[0-9A-Fa-f]{8,16}$") +FINGERPRINT = re.compile(r"^[0-9A-Fa-f]{40}$") + + +def normalize_signing_key_id(value): + key_id = value.strip() + if not key_id: + return "" + if SHORT_OR_LONG_KEY_ID.fullmatch(key_id): + return key_id + if FINGERPRINT.fullmatch(key_id): + return key_id[-16:] + return "" + + +def shell_exports(value): + key_id = normalize_signing_key_id(value) + if not key_id: + return "\n".join( + ( + "unset SIGNING_KEY_ID", + "unset SIGNING_IN_MEMORY_KEY_ID", + "unset ORG_GRADLE_PROJECT_signingInMemoryKeyId", + ) + ) + + escaped = key_id.replace("'", "'\"'\"'") + return "\n".join( + ( + f"export SIGNING_KEY_ID='{escaped}'", + f"export SIGNING_IN_MEMORY_KEY_ID='{escaped}'", + f"export ORG_GRADLE_PROJECT_signingInMemoryKeyId='{escaped}'", + ) + ) + + +def main(): + value = sys.argv[1] if len(sys.argv) > 1 else "" + print(shell_exports(value)) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/normalize_signing_key_id_test.py b/.github/scripts/normalize_signing_key_id_test.py index 959d09d..788f187 100644 --- a/.github/scripts/normalize_signing_key_id_test.py +++ b/.github/scripts/normalize_signing_key_id_test.py @@ -1,37 +1,37 @@ -import unittest - -from normalize_signing_key_id import normalize_signing_key_id, shell_exports - - -class NormalizeSigningKeyIdTest(unittest.TestCase): - def test_keeps_short_and_long_hex_key_ids(self): - self.assertEqual(normalize_signing_key_id("00B5050F"), "00B5050F") - self.assertEqual(normalize_signing_key_id("0x00B5050F"), "0x00B5050F") - self.assertEqual(normalize_signing_key_id("2BA16C9B594CE0E6"), "2BA16C9B594CE0E6") - - def test_converts_fingerprint_to_long_key_id(self): - self.assertEqual( - normalize_signing_key_id("672F6476219EAB8C55C127A32BA16C9B594CE0E6"), - "2BA16C9B594CE0E6", - ) - - def test_rejects_non_hex_values(self): - self.assertEqual(normalize_signing_key_id("wk1995 <1226426603@qq.com>"), "") - self.assertEqual(normalize_signing_key_id("not-a-key-id"), "") - - def test_shell_exports_clear_all_gradle_key_id_aliases_for_invalid_value(self): - output = shell_exports("not-a-key-id") - - self.assertIn("unset SIGNING_KEY_ID", output) - self.assertIn("unset SIGNING_IN_MEMORY_KEY_ID", output) - self.assertIn("unset ORG_GRADLE_PROJECT_signingInMemoryKeyId", output) - - def test_shell_exports_sets_gradle_alias_for_fingerprint(self): - output = shell_exports("672F6476219EAB8C55C127A32BA16C9B594CE0E6") - - self.assertIn("export SIGNING_KEY_ID='2BA16C9B594CE0E6'", output) - self.assertIn("export ORG_GRADLE_PROJECT_signingInMemoryKeyId='2BA16C9B594CE0E6'", output) - - -if __name__ == "__main__": - unittest.main() +import unittest + +from normalize_signing_key_id import normalize_signing_key_id, shell_exports + + +class NormalizeSigningKeyIdTest(unittest.TestCase): + def test_keeps_short_and_long_hex_key_ids(self): + self.assertEqual(normalize_signing_key_id("00B5050F"), "00B5050F") + self.assertEqual(normalize_signing_key_id("0x00B5050F"), "0x00B5050F") + self.assertEqual(normalize_signing_key_id("2BA16C9B594CE0E6"), "2BA16C9B594CE0E6") + + def test_converts_fingerprint_to_long_key_id(self): + self.assertEqual( + normalize_signing_key_id("672F6476219EAB8C55C127A32BA16C9B594CE0E6"), + "2BA16C9B594CE0E6", + ) + + def test_rejects_non_hex_values(self): + self.assertEqual(normalize_signing_key_id("wk1995 <1226426603@qq.com>"), "") + self.assertEqual(normalize_signing_key_id("not-a-key-id"), "") + + def test_shell_exports_clear_all_gradle_key_id_aliases_for_invalid_value(self): + output = shell_exports("not-a-key-id") + + self.assertIn("unset SIGNING_KEY_ID", output) + self.assertIn("unset SIGNING_IN_MEMORY_KEY_ID", output) + self.assertIn("unset ORG_GRADLE_PROJECT_signingInMemoryKeyId", output) + + def test_shell_exports_sets_gradle_alias_for_fingerprint(self): + output = shell_exports("672F6476219EAB8C55C127A32BA16C9B594CE0E6") + + self.assertIn("export SIGNING_KEY_ID='2BA16C9B594CE0E6'", output) + self.assertIn("export ORG_GRADLE_PROJECT_signingInMemoryKeyId='2BA16C9B594CE0E6'", output) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/publish_plugin_central_workflow_test.py b/.github/scripts/publish_plugin_central_workflow_test.py index 3d451db..5251126 100644 --- a/.github/scripts/publish_plugin_central_workflow_test.py +++ b/.github/scripts/publish_plugin_central_workflow_test.py @@ -1,110 +1,110 @@ -import re -import unittest -from pathlib import Path - - -WORKFLOWS = Path(__file__).resolve().parents[1] / "workflows" -CENTRAL_WORKFLOW = WORKFLOWS / "publish-plugin-central.yml" -PR_CHECK_WORKFLOW = WORKFLOWS / "publish-plugin-pr-check.yml" -SECRET_ENV_NAMES = ( - "CENTRAL_USERNAME", - "CENTRAL_PASSWORD", - "GPG_KEY_CONTENTS", - "SIGNING_KEY_ID", - "SIGNING_PASSWORD", -) - - -def workflow_text(path=CENTRAL_WORKFLOW): - return path.read_text(encoding="utf-8") - - -def step_block(name, path=CENTRAL_WORKFLOW): - text = workflow_text(path) - match = re.search( - rf"(?ms)^ - name: {re.escape(name)}\n(?P.*?)(?=^ - name: |\Z)", - text, - ) - if match is None: - raise AssertionError(f"Missing workflow step: {name}") - return match.group("body") - - -class PublishPluginCentralWorkflowTest(unittest.TestCase): - def test_local_metadata_validation_does_not_inherit_publish_secrets(self): - text = workflow_text() - job_header, _, _ = text.partition(" steps:") - validation = step_block("Validate local publication metadata") - - for name in SECRET_ENV_NAMES: - self.assertNotIn(f"{name}:", job_header) - self.assertNotIn(f"{name}:", validation) - - def test_central_publish_steps_receive_required_publish_env(self): - central_publish = step_block("Publish to Central staging") - central_upload = step_block("Create Central Portal deployment") - - for name in SECRET_ENV_NAMES: - self.assertIn(f"{name}:", central_publish) - - for name in ("CENTRAL_NAMESPACE", "CENTRAL_PUBLISHING_TYPE", "CENTRAL_USERNAME", "CENTRAL_PASSWORD"): - self.assertIn(f"{name}:", central_upload) - - def test_central_publish_falls_back_when_signing_key_id_is_invalid(self): - central_publish = step_block("Publish to Central staging") - - self.assertIn("SIGNING_KEY_ID", central_publish) - self.assertIn("normalize_signing_key_id.py", central_publish) - self.assertIn("original_signing_key_id", central_publish) - self.assertIn("falling back to infer it from GPG_KEY_CONTENTS", central_publish) - self.assertIn("normalized to a Gradle-compatible long key id", central_publish) - - def test_central_publish_is_gated_by_pre_publish_vs_main_version(self): - release = step_block("Resolve release version") - check_tag = step_block("Check release tag") - publish = step_block("Publish to Central staging") - merge = step_block("Merge pre_publish to main") - - self.assertIn("MAIN_VERSION=", release) - self.assertIn("publish_required=true", release) - self.assertIn("publish_required=false", release) - self.assertIn("must not be lower than main version", release) - - publish_condition = "steps.release.outputs.publish_required == 'true'" - self.assertIn(publish_condition, check_tag) - self.assertIn(publish_condition, publish) - self.assertIn("steps.release.outputs.publish_required", merge) - self.assertIn("== \"true\"", merge) - self.assertIn("does not require Central publish; merging directly", merge) - - -class PublishPluginPrCheckWorkflowTest(unittest.TestCase): - def test_version_bump_only_runs_when_plugin_base_changed(self): - detect_changes = step_block("Detect plugin_base changes", PR_CHECK_WORKFLOW) - ensure_version = step_block("Ensure publish plugin version", PR_CHECK_WORKFLOW) - sync_readme = step_block("Sync README publish plugin version", PR_CHECK_WORKFLOW) - commit_bump = step_block("Commit version bump", PR_CHECK_WORKFLOW) - - self.assertIn("git diff --quiet", detect_changes) - self.assertIn("FETCH_HEAD...HEAD", detect_changes) - self.assertIn("plugin_base/", detect_changes) - self.assertIn("changed=true", detect_changes) - self.assertIn("changed=false", detect_changes) - - expected_condition = "steps.plugin_base_changes.outputs.changed == 'true'" - self.assertIn(expected_condition, ensure_version) - self.assertIn(expected_condition, sync_readme) - self.assertIn(expected_condition, commit_bump) - - def test_workflow_only_changes_do_not_need_bot_version_commit(self): - text = workflow_text(PR_CHECK_WORKFLOW) - - self.assertIn("id: plugin_base_changes", text) - self.assertRegex( - text, - r"(?s)- name: Commit version bump.*if:.*steps\.plugin_base_changes\.outputs\.changed == 'true'", - ) - - -if __name__ == "__main__": - unittest.main() +import re +import unittest +from pathlib import Path + + +WORKFLOWS = Path(__file__).resolve().parents[1] / "workflows" +CENTRAL_WORKFLOW = WORKFLOWS / "publish-plugin-central.yml" +PR_CHECK_WORKFLOW = WORKFLOWS / "publish-plugin-pr-check.yml" +SECRET_ENV_NAMES = ( + "CENTRAL_USERNAME", + "CENTRAL_PASSWORD", + "GPG_KEY_CONTENTS", + "SIGNING_KEY_ID", + "SIGNING_PASSWORD", +) + + +def workflow_text(path=CENTRAL_WORKFLOW): + return path.read_text(encoding="utf-8") + + +def step_block(name, path=CENTRAL_WORKFLOW): + text = workflow_text(path) + match = re.search( + rf"(?ms)^ - name: {re.escape(name)}\n(?P.*?)(?=^ - name: |\Z)", + text, + ) + if match is None: + raise AssertionError(f"Missing workflow step: {name}") + return match.group("body") + + +class PublishPluginCentralWorkflowTest(unittest.TestCase): + def test_local_metadata_validation_does_not_inherit_publish_secrets(self): + text = workflow_text() + job_header, _, _ = text.partition(" steps:") + validation = step_block("Validate local publication metadata") + + for name in SECRET_ENV_NAMES: + self.assertNotIn(f"{name}:", job_header) + self.assertNotIn(f"{name}:", validation) + + def test_central_publish_steps_receive_required_publish_env(self): + central_publish = step_block("Publish to Central staging") + central_upload = step_block("Create Central Portal deployment") + + for name in SECRET_ENV_NAMES: + self.assertIn(f"{name}:", central_publish) + + for name in ("CENTRAL_NAMESPACE", "CENTRAL_PUBLISHING_TYPE", "CENTRAL_USERNAME", "CENTRAL_PASSWORD"): + self.assertIn(f"{name}:", central_upload) + + def test_central_publish_falls_back_when_signing_key_id_is_invalid(self): + central_publish = step_block("Publish to Central staging") + + self.assertIn("SIGNING_KEY_ID", central_publish) + self.assertIn("normalize_signing_key_id.py", central_publish) + self.assertIn("original_signing_key_id", central_publish) + self.assertIn("falling back to infer it from GPG_KEY_CONTENTS", central_publish) + self.assertIn("normalized to a Gradle-compatible long key id", central_publish) + + def test_central_publish_is_gated_by_pre_publish_vs_main_version(self): + release = step_block("Resolve release version") + check_tag = step_block("Check release tag") + publish = step_block("Publish to Central staging") + merge = step_block("Merge pre_publish to main") + + self.assertIn("MAIN_VERSION=", release) + self.assertIn("publish_required=true", release) + self.assertIn("publish_required=false", release) + self.assertIn("must not be lower than main version", release) + + publish_condition = "steps.release.outputs.publish_required == 'true'" + self.assertIn(publish_condition, check_tag) + self.assertIn(publish_condition, publish) + self.assertIn("steps.release.outputs.publish_required", merge) + self.assertIn("== \"true\"", merge) + self.assertIn("does not require Central publish; merging directly", merge) + + +class PublishPluginPrCheckWorkflowTest(unittest.TestCase): + def test_version_bump_only_runs_when_plugin_base_changed(self): + detect_changes = step_block("Detect plugin_base changes", PR_CHECK_WORKFLOW) + ensure_version = step_block("Ensure publish plugin version", PR_CHECK_WORKFLOW) + sync_readme = step_block("Sync README publish plugin version", PR_CHECK_WORKFLOW) + commit_bump = step_block("Commit version bump", PR_CHECK_WORKFLOW) + + self.assertIn("git diff --quiet", detect_changes) + self.assertIn("FETCH_HEAD...HEAD", detect_changes) + self.assertIn("plugin_base/", detect_changes) + self.assertIn("changed=true", detect_changes) + self.assertIn("changed=false", detect_changes) + + expected_condition = "steps.plugin_base_changes.outputs.changed == 'true'" + self.assertIn(expected_condition, ensure_version) + self.assertIn(expected_condition, sync_readme) + self.assertIn(expected_condition, commit_bump) + + def test_workflow_only_changes_do_not_need_bot_version_commit(self): + text = workflow_text(PR_CHECK_WORKFLOW) + + self.assertIn("id: plugin_base_changes", text) + self.assertRegex( + text, + r"(?s)- name: Commit version bump.*if:.*steps\.plugin_base_changes\.outputs\.changed == 'true'", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/publish-plugin-central.yml b/.github/workflows/publish-plugin-central.yml index 09e3248..3479aba 100644 --- a/.github/workflows/publish-plugin-central.yml +++ b/.github/workflows/publish-plugin-central.yml @@ -1,343 +1,343 @@ -name: Publish Plugin To Central - -on: - push: - branches: - - pre_publish - workflow_dispatch: - inputs: - namespace: - description: "Sonatype Central namespace, for example cn.entertech" - required: true - default: "cn.entertech" - type: string - publishing_type: - description: "Central publishing type" - required: true - default: "automatic" - type: choice - options: - - automatic - - user_managed - -permissions: - contents: write - -concurrency: - group: publish-plugin-central-${{ github.ref }} - cancel-in-progress: true - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Resolve release version - id: release - shell: bash - run: | - set -euo pipefail - - if [[ "$GITHUB_REF" != "refs/heads/pre_publish" ]]; then - echo "::error::PublishPlugin Central release must run from pre_publish. Current ref: $GITHUB_REF" - exit 1 - fi - - git fetch --no-tags origin main - - python3 .github/scripts/ensure_publish_version.py \ - --head-file plugin_base/build.gradle.kts \ - --normalize-only \ - --write - - release_version="$(python3 - <<'PY' - import re - import sys - from pathlib import Path - - content = Path("plugin_base/build.gradle.kts").read_text(encoding="utf-8") - match = re.search(r"(?m)^\s*version\s*=\s*['\"]([^'\"]+)['\"]", content) - if not match: - print("::error::Cannot find plugin_base version", file=sys.stderr) - sys.exit(1) - - version = match.group(1) - if not re.fullmatch(r"\d+\.\d+\.\d+", version): - print(f"::error::Publish plugin version '{version}' must use digits.digits.digits format", file=sys.stderr) - sys.exit(1) - - print(version) - PY - )" - main_file="$RUNNER_TEMP/main-plugin-build.gradle" - if git show origin/main:plugin_base/build.gradle.kts > "$main_file" 2>/dev/null; then - : - elif git show origin/main:plugin_base/build.gradle > "$main_file" 2>/dev/null; then - : - else - echo "::error::Cannot find main plugin_base build file" - exit 1 - fi - main_version="$(MAIN_FILE="$main_file" python3 - <<'PY' - import os - import re - import sys - from pathlib import Path - - content = Path(os.environ["MAIN_FILE"]).read_text(encoding="utf-8") - match = re.search(r"(?m)^\s*version\s*=\s*['\"]([^'\"]+)['\"]", content) - if not match: - print("::error::Cannot find main plugin_base version", file=sys.stderr) - sys.exit(1) - - version = match.group(1) - if not re.fullmatch(r"\d+\.\d+\.\d+", version): - print(f"::error::Main plugin version '{version}' must use digits.digits.digits format", file=sys.stderr) - sys.exit(1) - - print(version) - PY - )" - comparison="$(RELEASE_VERSION="$release_version" MAIN_VERSION="$main_version" python3 - <<'PY' - import os - - def parse(value): - return tuple(int(part) for part in value.split(".")) - - release = parse(os.environ["RELEASE_VERSION"]) - main = parse(os.environ["MAIN_VERSION"]) - if release < main: - print("lower") - elif release == main: - print("equal") - else: - print("greater") - PY - )" - if [[ "$comparison" == "lower" ]]; then - echo "::error::pre_publish version ${release_version} must not be lower than main version ${main_version}. Refusing to merge." - exit 1 - fi - - publish_required=false - if [[ "$comparison" == "greater" ]]; then - publish_required=true - fi - - release_tag="v${release_version}" - { - echo "version=${release_version}" - echo "main_version=${main_version}" - echo "tag=${release_tag}" - echo "publish_required=${publish_required}" - } >> "$GITHUB_OUTPUT" - { - echo "RELEASE_VERSION=${release_version}" - echo "MAIN_VERSION=${main_version}" - echo "RELEASE_TAG=${release_tag}" - echo "PUBLISH_REQUIRED=${publish_required}" - } >> "$GITHUB_ENV" - if [[ "$publish_required" == "true" ]]; then - echo "Preparing PublishPlugin ${release_version} release as ${release_tag}; main is ${main_version}." - else - echo "PublishPlugin ${release_version} equals main version ${main_version}; Central publish is not required." - fi - - python3 .github/scripts/sync_readme_publish_version.py --version "$release_version" - if ! git diff --quiet -- plugin_base/build.gradle.kts README.md; then - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add plugin_base/build.gradle.kts README.md - git commit -m "[codex] Prepare PublishPlugin ${release_version} release [skip ci]" - fi - - - name: Preflight main merge - id: merge_preflight - shell: bash - run: | - set -euo pipefail - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - release_commit="$(git rev-parse HEAD)" - git fetch origin main pre_publish --tags - - git checkout -B __preflight_main_merge origin/main - if ! git merge --no-commit --no-ff "$release_commit"; then - git status --short || true - git merge --abort || true - echo "::error::pre_publish cannot be preflight-merged into main. Check the merge output above; if conflicts are listed, merge main into pre_publish and resolve them before publishing." - exit 1 - fi - - git merge --abort || true - git checkout --detach "$release_commit" - echo "pre_publish can be merged into main without conflicts." - - - name: Check release tag - id: release_tag - if: steps.release.outputs.publish_required == 'true' - shell: bash - run: | - set -euo pipefail - - git fetch --tags origin - if git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null; then - tag_commit="$(git rev-list -n 1 "${RELEASE_TAG}")" - head_commit="$(git rev-parse HEAD)" - if [[ "$tag_commit" == "$head_commit" ]]; then - echo "exists=true" >> "$GITHUB_OUTPUT" - echo "::notice::${RELEASE_TAG} already exists on this commit; skipping Central publish." - exit 0 - fi - - echo "::error::${RELEASE_TAG} already exists on ${tag_commit}, but HEAD is ${head_commit}. Bump plugin_base version before releasing." - exit 1 - fi - - echo "exists=false" >> "$GITHUB_OUTPUT" - - - name: Set up JDK - if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' - uses: actions/setup-java@v4 - with: - distribution: zulu - java-version: "17" - - - name: Setup Gradle - if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' - uses: gradle/actions/setup-gradle@v4 - - - name: Validate local publication metadata - if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' - run: | - ./gradlew :plugin_base:build --stacktrace - ./gradlew :plugin_base:publishToMavenLocal --stacktrace - python3 .github/scripts/validate_publish_plugin_publications.py - - - name: Publish to Central staging - if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' - env: - CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} - SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} - SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - run: | - original_signing_key_id="${SIGNING_KEY_ID:-}" - eval "$(python3 .github/scripts/normalize_signing_key_id.py "$original_signing_key_id")" - if [[ -n "$original_signing_key_id" && -z "${SIGNING_KEY_ID:-}" ]]; then - echo "::warning::SIGNING_KEY_ID is not a valid PGP key id; falling back to infer it from GPG_KEY_CONTENTS." - elif [[ -n "$original_signing_key_id" && "$original_signing_key_id" != "${SIGNING_KEY_ID:-}" ]]; then - echo "::notice::SIGNING_KEY_ID was normalized to a Gradle-compatible long key id." - fi - - ./gradlew :plugin_base:publishAllPublicationsToCentralStagingRepository \ - -PcentralPublish=true \ - --no-daemon \ - --stacktrace - - - name: Create Central Portal deployment - if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' - env: - CENTRAL_NAMESPACE: ${{ github.event.inputs.namespace || 'cn.entertech' }} - CENTRAL_PUBLISHING_TYPE: ${{ github.event.inputs.publishing_type || 'automatic' }} - CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - run: | - python3 - <<'PY' - import base64 - import os - import sys - import urllib.error - import urllib.parse - import urllib.request - - namespace = os.environ["CENTRAL_NAMESPACE"].strip() - publishing_type = os.environ["CENTRAL_PUBLISHING_TYPE"].strip() - username = os.environ["CENTRAL_USERNAME"].strip() - password = os.environ["CENTRAL_PASSWORD"].strip() - - if not namespace: - print("::error::CENTRAL_NAMESPACE is required", file=sys.stderr) - sys.exit(1) - if not username or not password: - print("::error::MAVEN_CENTRAL_USERNAME and MAVEN_CENTRAL_PASSWORD secrets are required", file=sys.stderr) - sys.exit(1) - - token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii") - url = ( - "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/" - f"{urllib.parse.quote(namespace)}?publishing_type={urllib.parse.quote(publishing_type)}" - ) - request = urllib.request.Request(url, method="POST") - request.add_header("Authorization", f"Bearer {token}") - - try: - with urllib.request.urlopen(request, timeout=60) as response: - body = response.read().decode("utf-8", errors="replace") - except urllib.error.HTTPError as error: - body = error.read().decode("utf-8", errors="replace") - print(f"::error::Central manual upload failed: HTTP {error.code} {body}", file=sys.stderr) - sys.exit(1) - - if body: - print(body) - print("Central Portal deployments: https://central.sonatype.com/publishing/deployments") - PY - - - name: Sync README release version - if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' - shell: bash - run: | - set -euo pipefail - - python3 .github/scripts/sync_readme_publish_version.py --version "$RELEASE_VERSION" - if git diff --quiet -- plugin_base/build.gradle.kts README.md; then - echo "README.md already documents PublishPlugin ${RELEASE_VERSION}" - else - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add plugin_base/build.gradle.kts README.md - git commit -m "[codex] Update README publish plugin version to ${RELEASE_VERSION} [skip ci]" - fi - - git push origin "HEAD:pre_publish" - - - name: Tag successful release - if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' - shell: bash - run: | - set -euo pipefail - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag -a "$RELEASE_TAG" -m "Release PublishPlugin ${RELEASE_VERSION}" - git push origin "$RELEASE_TAG" - - - name: Merge pre_publish to main - shell: bash - run: | - set -euo pipefail - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - git fetch origin main pre_publish --tags - if [[ "${{ steps.release.outputs.publish_required }}" == "true" ]] && ! git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null; then - echo "::error::${RELEASE_TAG} does not exist; refusing to update main." - exit 1 - fi - if [[ "${{ steps.release.outputs.publish_required }}" != "true" ]]; then - echo "pre_publish version ${RELEASE_VERSION} equals main version ${MAIN_VERSION}; does not require Central publish; merging directly." - fi - - git checkout -B main origin/main - git merge --no-ff --no-edit origin/pre_publish - git push origin main +name: Publish Plugin To Central + +on: + push: + branches: + - pre_publish + workflow_dispatch: + inputs: + namespace: + description: "Sonatype Central namespace, for example cn.entertech" + required: true + default: "cn.entertech" + type: string + publishing_type: + description: "Central publishing type" + required: true + default: "automatic" + type: choice + options: + - automatic + - user_managed + +permissions: + contents: write + +concurrency: + group: publish-plugin-central-${{ github.ref }} + cancel-in-progress: true + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve release version + id: release + shell: bash + run: | + set -euo pipefail + + if [[ "$GITHUB_REF" != "refs/heads/pre_publish" ]]; then + echo "::error::PublishPlugin Central release must run from pre_publish. Current ref: $GITHUB_REF" + exit 1 + fi + + git fetch --no-tags origin main + + python3 .github/scripts/ensure_publish_version.py \ + --head-file plugin_base/build.gradle.kts \ + --normalize-only \ + --write + + release_version="$(python3 - <<'PY' + import re + import sys + from pathlib import Path + + content = Path("plugin_base/build.gradle.kts").read_text(encoding="utf-8") + match = re.search(r"(?m)^\s*version\s*=\s*['\"]([^'\"]+)['\"]", content) + if not match: + print("::error::Cannot find plugin_base version", file=sys.stderr) + sys.exit(1) + + version = match.group(1) + if not re.fullmatch(r"\d+\.\d+\.\d+", version): + print(f"::error::Publish plugin version '{version}' must use digits.digits.digits format", file=sys.stderr) + sys.exit(1) + + print(version) + PY + )" + main_file="$RUNNER_TEMP/main-plugin-build.gradle" + if git show origin/main:plugin_base/build.gradle.kts > "$main_file" 2>/dev/null; then + : + elif git show origin/main:plugin_base/build.gradle > "$main_file" 2>/dev/null; then + : + else + echo "::error::Cannot find main plugin_base build file" + exit 1 + fi + main_version="$(MAIN_FILE="$main_file" python3 - <<'PY' + import os + import re + import sys + from pathlib import Path + + content = Path(os.environ["MAIN_FILE"]).read_text(encoding="utf-8") + match = re.search(r"(?m)^\s*version\s*=\s*['\"]([^'\"]+)['\"]", content) + if not match: + print("::error::Cannot find main plugin_base version", file=sys.stderr) + sys.exit(1) + + version = match.group(1) + if not re.fullmatch(r"\d+\.\d+\.\d+", version): + print(f"::error::Main plugin version '{version}' must use digits.digits.digits format", file=sys.stderr) + sys.exit(1) + + print(version) + PY + )" + comparison="$(RELEASE_VERSION="$release_version" MAIN_VERSION="$main_version" python3 - <<'PY' + import os + + def parse(value): + return tuple(int(part) for part in value.split(".")) + + release = parse(os.environ["RELEASE_VERSION"]) + main = parse(os.environ["MAIN_VERSION"]) + if release < main: + print("lower") + elif release == main: + print("equal") + else: + print("greater") + PY + )" + if [[ "$comparison" == "lower" ]]; then + echo "::error::pre_publish version ${release_version} must not be lower than main version ${main_version}. Refusing to merge." + exit 1 + fi + + publish_required=false + if [[ "$comparison" == "greater" ]]; then + publish_required=true + fi + + release_tag="v${release_version}" + { + echo "version=${release_version}" + echo "main_version=${main_version}" + echo "tag=${release_tag}" + echo "publish_required=${publish_required}" + } >> "$GITHUB_OUTPUT" + { + echo "RELEASE_VERSION=${release_version}" + echo "MAIN_VERSION=${main_version}" + echo "RELEASE_TAG=${release_tag}" + echo "PUBLISH_REQUIRED=${publish_required}" + } >> "$GITHUB_ENV" + if [[ "$publish_required" == "true" ]]; then + echo "Preparing PublishPlugin ${release_version} release as ${release_tag}; main is ${main_version}." + else + echo "PublishPlugin ${release_version} equals main version ${main_version}; Central publish is not required." + fi + + python3 .github/scripts/sync_readme_publish_version.py --version "$release_version" + if ! git diff --quiet -- plugin_base/build.gradle.kts README.md; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add plugin_base/build.gradle.kts README.md + git commit -m "[codex] Prepare PublishPlugin ${release_version} release [skip ci]" + fi + + - name: Preflight main merge + id: merge_preflight + shell: bash + run: | + set -euo pipefail + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + release_commit="$(git rev-parse HEAD)" + git fetch origin main pre_publish --tags + + git checkout -B __preflight_main_merge origin/main + if ! git merge --no-commit --no-ff "$release_commit"; then + git status --short || true + git merge --abort || true + echo "::error::pre_publish cannot be preflight-merged into main. Check the merge output above; if conflicts are listed, merge main into pre_publish and resolve them before publishing." + exit 1 + fi + + git merge --abort || true + git checkout --detach "$release_commit" + echo "pre_publish can be merged into main without conflicts." + + - name: Check release tag + id: release_tag + if: steps.release.outputs.publish_required == 'true' + shell: bash + run: | + set -euo pipefail + + git fetch --tags origin + if git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null; then + tag_commit="$(git rev-list -n 1 "${RELEASE_TAG}")" + head_commit="$(git rev-parse HEAD)" + if [[ "$tag_commit" == "$head_commit" ]]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "::notice::${RELEASE_TAG} already exists on this commit; skipping Central publish." + exit 0 + fi + + echo "::error::${RELEASE_TAG} already exists on ${tag_commit}, but HEAD is ${head_commit}. Bump plugin_base version before releasing." + exit 1 + fi + + echo "exists=false" >> "$GITHUB_OUTPUT" + + - name: Set up JDK + if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: "17" + + - name: Setup Gradle + if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' + uses: gradle/actions/setup-gradle@v4 + + - name: Validate local publication metadata + if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' + run: | + ./gradlew :plugin_base:build --stacktrace + ./gradlew :plugin_base:publishToMavenLocal --stacktrace + python3 .github/scripts/validate_publish_plugin_publications.py + + - name: Publish to Central staging + if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' + env: + CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} + SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + run: | + original_signing_key_id="${SIGNING_KEY_ID:-}" + eval "$(python3 .github/scripts/normalize_signing_key_id.py "$original_signing_key_id")" + if [[ -n "$original_signing_key_id" && -z "${SIGNING_KEY_ID:-}" ]]; then + echo "::warning::SIGNING_KEY_ID is not a valid PGP key id; falling back to infer it from GPG_KEY_CONTENTS." + elif [[ -n "$original_signing_key_id" && "$original_signing_key_id" != "${SIGNING_KEY_ID:-}" ]]; then + echo "::notice::SIGNING_KEY_ID was normalized to a Gradle-compatible long key id." + fi + + ./gradlew :plugin_base:publishAllPublicationsToCentralStagingRepository \ + -PcentralPublish=true \ + --no-daemon \ + --stacktrace + + - name: Create Central Portal deployment + if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' + env: + CENTRAL_NAMESPACE: ${{ github.event.inputs.namespace || 'cn.entertech' }} + CENTRAL_PUBLISHING_TYPE: ${{ github.event.inputs.publishing_type || 'automatic' }} + CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + run: | + python3 - <<'PY' + import base64 + import os + import sys + import urllib.error + import urllib.parse + import urllib.request + + namespace = os.environ["CENTRAL_NAMESPACE"].strip() + publishing_type = os.environ["CENTRAL_PUBLISHING_TYPE"].strip() + username = os.environ["CENTRAL_USERNAME"].strip() + password = os.environ["CENTRAL_PASSWORD"].strip() + + if not namespace: + print("::error::CENTRAL_NAMESPACE is required", file=sys.stderr) + sys.exit(1) + if not username or not password: + print("::error::MAVEN_CENTRAL_USERNAME and MAVEN_CENTRAL_PASSWORD secrets are required", file=sys.stderr) + sys.exit(1) + + token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii") + url = ( + "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/" + f"{urllib.parse.quote(namespace)}?publishing_type={urllib.parse.quote(publishing_type)}" + ) + request = urllib.request.Request(url, method="POST") + request.add_header("Authorization", f"Bearer {token}") + + try: + with urllib.request.urlopen(request, timeout=60) as response: + body = response.read().decode("utf-8", errors="replace") + except urllib.error.HTTPError as error: + body = error.read().decode("utf-8", errors="replace") + print(f"::error::Central manual upload failed: HTTP {error.code} {body}", file=sys.stderr) + sys.exit(1) + + if body: + print(body) + print("Central Portal deployments: https://central.sonatype.com/publishing/deployments") + PY + + - name: Sync README release version + if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' + shell: bash + run: | + set -euo pipefail + + python3 .github/scripts/sync_readme_publish_version.py --version "$RELEASE_VERSION" + if git diff --quiet -- plugin_base/build.gradle.kts README.md; then + echo "README.md already documents PublishPlugin ${RELEASE_VERSION}" + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add plugin_base/build.gradle.kts README.md + git commit -m "[codex] Update README publish plugin version to ${RELEASE_VERSION} [skip ci]" + fi + + git push origin "HEAD:pre_publish" + + - name: Tag successful release + if: steps.release.outputs.publish_required == 'true' && steps.release_tag.outputs.exists != 'true' + shell: bash + run: | + set -euo pipefail + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "$RELEASE_TAG" -m "Release PublishPlugin ${RELEASE_VERSION}" + git push origin "$RELEASE_TAG" + + - name: Merge pre_publish to main + shell: bash + run: | + set -euo pipefail + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git fetch origin main pre_publish --tags + if [[ "${{ steps.release.outputs.publish_required }}" == "true" ]] && ! git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null; then + echo "::error::${RELEASE_TAG} does not exist; refusing to update main." + exit 1 + fi + if [[ "${{ steps.release.outputs.publish_required }}" != "true" ]]; then + echo "pre_publish version ${RELEASE_VERSION} equals main version ${MAIN_VERSION}; does not require Central publish; merging directly." + fi + + git checkout -B main origin/main + git merge --no-ff --no-edit origin/pre_publish + git push origin main diff --git a/.github/workflows/publish-plugin-pr-check.yml b/.github/workflows/publish-plugin-pr-check.yml index 98c0348..278f378 100644 --- a/.github/workflows/publish-plugin-pr-check.yml +++ b/.github/workflows/publish-plugin-pr-check.yml @@ -1,134 +1,134 @@ -name: Publish Plugin PR Check - -on: - pull_request: - branches: - - pre_publish - types: - - opened - - synchronize - - reopened - - ready_for_review - -permissions: - contents: write - pull-requests: read - -concurrency: - group: publish-plugin-pr-check-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - validate: - runs-on: ubuntu-latest - - steps: - - name: Checkout PR branch - uses: actions/checkout@v4 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Load base publish version - shell: bash - run: | - set -euo pipefail - git remote add base "https://github.com/${{ github.repository }}.git" 2>/dev/null || \ - git remote set-url base "https://github.com/${{ github.repository }}.git" - git fetch --no-tags base "${{ github.base_ref }}" - - base_file="$RUNNER_TEMP/base-plugin-build.gradle" - if git show FETCH_HEAD:plugin_base/build.gradle.kts > "$base_file" 2>/dev/null; then - echo "BASE_FILE=$base_file" >> "$GITHUB_ENV" - elif git show FETCH_HEAD:plugin_base/build.gradle > "$base_file" 2>/dev/null; then - echo "BASE_FILE=$base_file" >> "$GITHUB_ENV" - else - echo "::error::Cannot find plugin_base build file on ${{ github.base_ref }}" - exit 1 - fi - - if [[ -f plugin_base/build.gradle.kts ]]; then - echo "HEAD_FILE=plugin_base/build.gradle.kts" >> "$GITHUB_ENV" - elif [[ -f plugin_base/build.gradle ]]; then - echo "HEAD_FILE=plugin_base/build.gradle" >> "$GITHUB_ENV" - else - echo "::error::Cannot find plugin_base build file on PR branch" - exit 1 - fi - - - name: Detect plugin_base changes - id: plugin_base_changes - shell: bash - run: | - set -euo pipefail - - if git diff --quiet FETCH_HEAD...HEAD -- plugin_base/; then - echo "changed=false" >> "$GITHUB_OUTPUT" - echo "plugin_base is unchanged; skipping publish plugin version bump." - else - echo "changed=true" >> "$GITHUB_OUTPUT" - echo "plugin_base changed; publish plugin version bump is required." - fi - - - name: Ensure publish plugin version - id: version - if: steps.plugin_base_changes.outputs.changed == 'true' - run: | - python3 .github/scripts/ensure_publish_version.py \ - --head-file "$HEAD_FILE" \ - --base-file "$BASE_FILE" \ - --write - - - name: Sync README publish plugin version - id: readme_version - if: steps.plugin_base_changes.outputs.changed == 'true' - shell: bash - run: | - set -euo pipefail - - python3 .github/scripts/sync_readme_publish_version.py - if git diff --quiet -- README.md; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Commit version bump - if: steps.plugin_base_changes.outputs.changed == 'true' && (steps.version.outputs.changed == 'true' || steps.readme_version.outputs.changed == 'true') - shell: bash - run: | - set -euo pipefail - if [[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then - echo "::error::Publish plugin version or README version needs automatic updates. Update them manually for fork PRs." - exit 1 - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add "$HEAD_FILE" README.md - git commit -m "[codex] Bump publish plugin version to ${{ steps.version.outputs.version }}" - git push origin "HEAD:${{ github.event.pull_request.head.ref }}" - - - name: Set up JDK - uses: actions/setup-java@v4 - with: - distribution: zulu - java-version: "17" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Test version helper - run: | - python3 .github/scripts/ensure_publish_version_test.py - python3 .github/scripts/normalize_signing_key_id_test.py - python3 .github/scripts/sync_readme_publish_version_test.py - - - name: Run required Gradle tasks - run: | - ./gradlew :plugin_base:build --stacktrace - ./gradlew :plugin_base:publishToMavenLocal --stacktrace - python3 .github/scripts/validate_publish_plugin_publications.py - python3 .github/scripts/sync_readme_publish_version.py --check +name: Publish Plugin PR Check + +on: + pull_request: + branches: + - pre_publish + types: + - opened + - synchronize + - reopened + - ready_for_review + +permissions: + contents: write + pull-requests: read + +concurrency: + group: publish-plugin-pr-check-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Load base publish version + shell: bash + run: | + set -euo pipefail + git remote add base "https://github.com/${{ github.repository }}.git" 2>/dev/null || \ + git remote set-url base "https://github.com/${{ github.repository }}.git" + git fetch --no-tags base "${{ github.base_ref }}" + + base_file="$RUNNER_TEMP/base-plugin-build.gradle" + if git show FETCH_HEAD:plugin_base/build.gradle.kts > "$base_file" 2>/dev/null; then + echo "BASE_FILE=$base_file" >> "$GITHUB_ENV" + elif git show FETCH_HEAD:plugin_base/build.gradle > "$base_file" 2>/dev/null; then + echo "BASE_FILE=$base_file" >> "$GITHUB_ENV" + else + echo "::error::Cannot find plugin_base build file on ${{ github.base_ref }}" + exit 1 + fi + + if [[ -f plugin_base/build.gradle.kts ]]; then + echo "HEAD_FILE=plugin_base/build.gradle.kts" >> "$GITHUB_ENV" + elif [[ -f plugin_base/build.gradle ]]; then + echo "HEAD_FILE=plugin_base/build.gradle" >> "$GITHUB_ENV" + else + echo "::error::Cannot find plugin_base build file on PR branch" + exit 1 + fi + + - name: Detect plugin_base changes + id: plugin_base_changes + shell: bash + run: | + set -euo pipefail + + if git diff --quiet FETCH_HEAD...HEAD -- plugin_base/; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "plugin_base is unchanged; skipping publish plugin version bump." + else + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "plugin_base changed; publish plugin version bump is required." + fi + + - name: Ensure publish plugin version + id: version + if: steps.plugin_base_changes.outputs.changed == 'true' + run: | + python3 .github/scripts/ensure_publish_version.py \ + --head-file "$HEAD_FILE" \ + --base-file "$BASE_FILE" \ + --write + + - name: Sync README publish plugin version + id: readme_version + if: steps.plugin_base_changes.outputs.changed == 'true' + shell: bash + run: | + set -euo pipefail + + python3 .github/scripts/sync_readme_publish_version.py + if git diff --quiet -- README.md; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Commit version bump + if: steps.plugin_base_changes.outputs.changed == 'true' && (steps.version.outputs.changed == 'true' || steps.readme_version.outputs.changed == 'true') + shell: bash + run: | + set -euo pipefail + if [[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then + echo "::error::Publish plugin version or README version needs automatic updates. Update them manually for fork PRs." + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "$HEAD_FILE" README.md + git commit -m "[codex] Bump publish plugin version to ${{ steps.version.outputs.version }}" + git push origin "HEAD:${{ github.event.pull_request.head.ref }}" + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: "17" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Test version helper + run: | + python3 .github/scripts/ensure_publish_version_test.py + python3 .github/scripts/normalize_signing_key_id_test.py + python3 .github/scripts/sync_readme_publish_version_test.py + + - name: Run required Gradle tasks + run: | + ./gradlew :plugin_base:build --stacktrace + ./gradlew :plugin_base:publishToMavenLocal --stacktrace + python3 .github/scripts/validate_publish_plugin_publications.py + python3 .github/scripts/sync_readme_publish_version.py --check From 06102aecde37a686043fff4c1bf5e596eb9535c3 Mon Sep 17 00:00:00 2001 From: wk <1226426603@qq.com> Date: Sun, 5 Jul 2026 19:24:20 +0800 Subject: [PATCH 2/2] [work-enter] [work-enter] [codex] Document optional signing key id --- README.md | 1137 +++++++++++++++++++++++++++-------------------------- 1 file changed, 572 insertions(+), 565 deletions(-) diff --git a/README.md b/README.md index 62a75ac..98fc6aa 100644 --- a/README.md +++ b/README.md @@ -1,345 +1,344 @@ -# PublishPlugin - -`PublishPlugin` 用于给 Android Library 和 Gradle Plugin 模块生成 Maven publication,并提供统一的本地发布、远程发布入口。 - -当前远程发布入口继续复用旧任务名 `PublishLibraryRemoteTask`,实现上默认面向 Sonatype Central Portal;旧的私服字段仍保留,避免已有项目的 `PublishInfo { ... }` 配置编译失败。 - -## 接入插件 - -在根工程 `build.gradle.kts` 中加入插件依赖: - -```kotlin -buildscript { - repositories { - google() - mavenCentral() - mavenLocal() - } - - dependencies { - classpath("cn.entertech.android:publish:1.2.1") - } -} -``` - -在需要发布的 Library 或 Gradle Plugin 模块中应用插件: - -```kotlin -plugins { - id("com.android.library") - id("cn.entertech.publish") -} -``` - -Gradle Plugin 模块继续使用: - -```kotlin -plugins { - `java-gradle-plugin` - id("cn.entertech.publish") -} -``` -或者 在需要打包的library|gradle插件目录下的  **build.gradle** 文件添加下面的插件 -``` -apply plugin: 'custom.android.plugin' -``` - -## 基础配置 - -最小配置: - -```kotlin -PublishInfo { - groupId = "cn.entertech.android" - artifactId = "base" - version = "0.0.1" -} -``` - -旧字段仍兼容: - -```kotlin -PublishInfo { - groupId = "cn.entertech.android" - artifactId = "base" - version = "0.0.1" - - pluginId = "" - implementationClass = "" - - publishUrl = "" - publishUserName = "" - publishPassword = "" -} -``` - -`local.properties` 也可以提供旧私服字段,值不要加引号: - -```properties -publishUrl=https://repo.example.com/releases -publishUserName=username -publishPassword=password -``` - -## PublishInfo 字段说明 - -### 基础坐标字段 - -| 字段 | 作用 | 是否必填 | 说明 | -| --- | --- | --- | --- | -| `groupId` | Maven 坐标的 groupId | 是 | Central 发布时必须落在已验证的 `centralNamespace` 下,例如 namespace 是 `ai.looktech`,则 `groupId` 可以是 `ai.looktech` 或 `ai.looktech.xxx`。 | -| `artifactId` | Maven 坐标的 artifactId | 是 | 普通单组件发布时直接作为 artifactId;多 variant 发布时作为默认值和 base 值,可被 `artifactIdForVariant` 动态覆盖。 | -| `version` | Maven 坐标的 version | 是 | 本地发布时 `-debug` 版本会附带 sources jar;远程 Central 发布禁止使用包含 `debug` 的版本。 | - -### Gradle 插件发布字段 - -只有发布 Gradle Plugin 模块时需要配置这两个字段;普通 Android Library 不需要。 - -| 字段 | 作用 | 是否必填 | 说明 | -| --- | --- | --- | --- | -| `pluginId` | Gradle 插件 ID | 发布 Gradle Plugin 时必填 | 写入 `gradlePlugin { plugins { ... } }` 的插件 ID,例如 `cn.entertech.foo`。 | -| `implementationClass` | Gradle 插件实现类 | 发布 Gradle Plugin 时必填 | 插件入口类全限定名,例如 `cn.entertech.foo.FooPlugin`。 | - -### 旧私服字段 - -这些字段保留是为了兼容旧项目;默认 Central 发布不建议把凭据写在 `build.gradle.kts` 里。 - -| 字段 | 作用 | 生效场景 | 说明 | -| --- | --- | --- | --- | -| `publishUrl` | 自定义 Maven 仓库地址 | `remotePublishMode = "customRepository"` | 旧私服发布地址。默认 Central 模式不会用它作为上传地址。 | -| `publishUserName` | 自定义仓库用户名 | `customRepository`,也作为 Central token username fallback | 优先级低于 `-PcentralUsername`、`CENTRAL_USERNAME`、`-PmavenCentralUsername`、`MAVEN_CENTRAL_USERNAME`。 | -| `publishPassword` | 自定义仓库密码 | `customRepository`,也作为 Central token password fallback | 优先级低于 `-PcentralPassword`、`CENTRAL_PASSWORD`、`-PmavenCentralPassword`、`MAVEN_CENTRAL_PASSWORD`。 | - -### Central 发布字段 - -| 字段 | 作用 | 是否必填 | 默认值/说明 | -| --- | --- | --- | --- | -| `remotePublishMode` | 选择远程发布模式 | 否 | 默认 `central`。可设为 `customRepository` 走旧自定义 Maven 仓库。 | -| `centralNamespace` | Sonatype Central namespace | Central 远程发布必填 | 例如 `ai.looktech`。远程发布前会校验 `groupId` 是否在该 namespace 下。 | -| `centralPublishingType` | Central Portal deployment 发布方式 | 否 | 默认 `user_managed`,上传后在 Portal 手动 Publish;可设为 `automatic`。 | -| `centralRepositoryName` | Gradle repository 名称 | 否 | 默认 `CentralStaging`,会影响 Gradle 任务名,例如 `publishEnterPublishPublicationToCentralStagingRepository`。 | - -### POM 元数据字段 - -Central 远程发布要求 POM 元数据完整;这些字段本地发布可以为空,执行 `PublishLibraryRemoteTask` 时会校验。 - -| 字段 | 作用 | Central 是否必填 | 说明 | -| --- | --- | --- | --- | -| `pomName` | POM `` | 否 | 为空时默认使用当前 artifactId。多 variant 动态 artifactId 场景下,会使用每个 publication 的最终 artifactId 作为 fallback。 | -| `pomDescription` | POM `` | 是 | 简短说明库的用途。 | -| `pomInceptionYear` | POM `` | 否 | 项目开始年份。 | -| `pomUrl` | POM `` | 是 | 项目主页、仓库地址或文档地址。 | - -### License 字段 - -| 字段 | 作用 | 默认值 | -| --- | --- | --- | -| `licenseName` | POM license name | `The Apache License, Version 2.0` | -| `licenseUrl` | POM license URL | `https://www.apache.org/licenses/LICENSE-2.0.txt` | -| `licenseDistribution` | POM license distribution | `repo` | - -### Developer 字段 - -Central 远程发布会校验开发者信息,建议全部配置。 - -| 字段 | 作用 | Central 是否必填 | -| --- | --- | --- | -| `developerId` | 开发者或组织 ID | 是 | -| `developerName` | 开发者或组织名称 | 是 | -| `developerEmail` | 联系邮箱 | 是 | -| `developerOrganization` | 组织名称 | 是 | -| `developerOrganizationUrl` | 组织主页 | 是 | -| `developerUrl` | 开发者主页 | 否 | - -### SCM 字段 - -Central 远程发布要求源码仓库信息。 - -| 字段 | 作用 | 示例 | -| --- | --- | --- | -| `scmUrl` | 仓库浏览地址 | `https://github.com/Entertech/lt-rpc-schema` | -| `scmConnection` | 只读 SCM 地址 | `scm:git:git://github.com/Entertech/lt-rpc-schema.git` | -| `scmDeveloperConnection` | 开发者 SCM 地址 | `scm:git:ssh://git@github.com/Entertech/lt-rpc-schema.git` | - -### 动态 artifactId 与 variant 过滤 - -`artifactIdForVariant { variant -> ... }` 不是普通字段,而是按 Android release variant 计算 artifactId 的回调。 -`skipVariantIf { variant -> ... }` 用于过滤不需要发布的 release variant。 - -| API | 作用 | 生效场景 | -| --- | --- | --- | -| `artifactIdForVariant` | 为每个 variant 返回独立 artifactId | Android Library 存在多个 release component 时 | -| `skipVariantIf` | 返回 `true` 时跳过该 variant,不注册 publication | Android Library 多 release variant 发布时 | -| `variant.name` | 当前 component/variant 名 | 例如 `breathAuthRelease` | -| `variant.buildType` | 当前 build type | 当前插件只发布 `release` component | -| `variant.flavors` | flavor 维度到 flavor 名称的 Map | 例如 `project -> breath`、`authentication -> auth` | -| `variant.flavor("dimension")` | 获取指定维度的 flavor 名称 | 不存在时返回空字符串 | - -## 本地打 AAR - -如果只是想在当前项目里生成一个本地 AAR 文件,不需要执行发布任务,直接使用 Android Gradle Plugin 的 assemble 任务。 - -单一 release 组件: - -```bash -./gradlew :module:assembleRelease -``` - -生成目录: - -```text -module/build/outputs/aar/ -``` - -常见产物名: - -```text -module-release.aar -``` - -如果 Library 有 flavor,需要执行具体 variant 的 assemble 任务: - -```bash -./gradlew :affective_local_sdk:assembleBreathAuthRelease -./gradlew :affective_local_sdk:assembleBreathNoAuthRelease -``` - -也可以一次构建所有 release variant: - -```bash -./gradlew :affective_local_sdk:assembleRelease -``` - -多 flavor AAR 仍输出在模块的 `build/outputs/aar/` 目录下,文件名由 Android Gradle Plugin 按 variant 生成,例如: - -```text -affective_local_sdk-breath-auth-release.aar -affective_local_sdk-breath-noAuth-release.aar -``` - -`assemble...` 只负责生成 AAR 文件,不会写入 `~/.m2`,也不会生成可供其他项目通过 Maven 坐标依赖的本地库。 - -## 发布到本地 Maven - -执行模块下的自定义任务: - -```bash -./gradlew :module:PublishLibraryLocalTask -``` - -或直接使用 Gradle Maven Publish 标准任务: - -```bash -./gradlew :module:publishToMavenLocal -``` - -本地发布的 sources 规则: - -- `version` 以 `-debug` 结尾时,发布 sources jar。 -- 非 debug 版本默认不发布 sources jar。 -- Central 远程发布不受这个规则限制,Central 必须包含 sources 和 javadoc。 - -发布成功后,产物会进入本机 Maven 仓库: - -```text -~/.m2/repository//// -``` - -例如: - -```text -~/.m2/repository/cn/entertech/android/affective-offline-sdk/1.0.0/ -``` - -多 variant 发布时,每个未过滤的 release variant 都会生成独立 artifact 目录,并且目录里应至少包含 `.aar`、`.pom`、`.module`: - -```text -~/.m2/repository/cn/entertech/android/breath-affective-offline-sdk-authentication/1.3.6/ -~/.m2/repository/cn/entertech/android/breath-affective-offline-sdk/1.3.6/ -~/.m2/repository/cn/entertech/android/sdk-affective-offline-sdk-authentication/1.3.6/ -~/.m2/repository/cn/entertech/android/sdk-affective-offline-sdk/1.3.6/ -``` - -验证本地发布不能只看 `BUILD SUCCESSFUL`。如果 Gradle 显示成功但目标 artifact 目录没有 `.aar/.pom/.module`,说明 publication 没有真实执行,应优先检查插件日志和 `publishToMavenLocal --stacktrace` 输出。 - -其他项目使用本地发布产物时,需要在仓库中启用 `mavenLocal()`: - -```kotlin -repositories { - google() - mavenCentral() - mavenLocal() -} -``` - -然后按 Maven 坐标依赖: - -```kotlin -dependencies { - implementation("cn.entertech.android:affective-offline-sdk:1.0.0") -} -``` - -多 variant 动态 artifactId 场景下,依赖最终生成的 artifactId: - -```kotlin -dependencies { - implementation("cn.entertech.android:breath-affective-offline-sdk-authentication:1.0.0") - implementation("cn.entertech.android:breath-affective-offline-sdk:1.0.0") -} -``` - -选择方式: - -| 目标 | 使用命令 | 产物位置 | 适合场景 | -| --- | --- | --- | --- | -| 只要一个 `.aar` 文件 | `assembleRelease` 或具体 `assembleRelease` | `module/build/outputs/aar/` | 手动拷贝、临时检查 AAR 内容 | -| 让其他项目通过 Maven 坐标依赖 | `PublishLibraryLocalTask` 或 `publishToMavenLocal` | `~/.m2/repository/` | 本地联调、验证 POM 依赖、模拟远程发布消费方式 | - -## Central Portal 发布 - -远程发布仍执行旧任务名: - -```bash -./gradlew :module:PublishLibraryRemoteTask -``` - -Central 必需配置示例: - -```kotlin -PublishInfo { - groupId = "ai.looktech.ltrpc.schema" - artifactId = "bt-server" - version = "1.0.0" - - centralNamespace = "ai.looktech" - centralPublishingType = "user_managed" - - pomName = "Looktech RPC BT Server Schema" - pomDescription = "BT Server Schema library for Looktech RPC" - pomInceptionYear = "2025" - pomUrl = "https://github.com/Entertech/lt-rpc-schema" - - developerId = "Entertech" - developerName = "Entertech" - developerEmail = "dev@example.com" - developerOrganization = "Entertech" - developerOrganizationUrl = "https://github.com/Entertech" - developerUrl = "https://github.com/Entertech" - - scmUrl = "https://github.com/Entertech/lt-rpc-schema" - scmConnection = "scm:git:git://github.com/Entertech/lt-rpc-schema.git" - scmDeveloperConnection = "scm:git:ssh://git@github.com/Entertech/lt-rpc-schema.git" -} -``` - -Central token 和签名建议通过环境变量或 Gradle property 注入,不要写入仓库: - -```bash +# PublishPlugin + +`PublishPlugin` 用于给 Android Library 和 Gradle Plugin 模块生成 Maven publication,并提供统一的本地发布、远程发布入口。 + +当前远程发布入口继续复用旧任务名 `PublishLibraryRemoteTask`,实现上默认面向 Sonatype Central Portal;旧的私服字段仍保留,避免已有项目的 `PublishInfo { ... }` 配置编译失败。 + +## 接入插件 + +在根工程 `build.gradle.kts` 中加入插件依赖: + +```kotlin +buildscript { + repositories { + google() + mavenCentral() + mavenLocal() + } + + dependencies { + classpath("cn.entertech.android:publish:1.2.1") + } +} +``` + +在需要发布的 Library 或 Gradle Plugin 模块中应用插件: + +```kotlin +plugins { + id("com.android.library") + id("cn.entertech.publish") +} +``` + +Gradle Plugin 模块继续使用: + +```kotlin +plugins { + `java-gradle-plugin` + id("cn.entertech.publish") +} +``` +或者 在需要打包的library|gradle插件目录下的  **build.gradle** 文件添加下面的插件 +``` +apply plugin: 'custom.android.plugin' +``` + +## 基础配置 + +最小配置: + +```kotlin +PublishInfo { + groupId = "cn.entertech.android" + artifactId = "base" + version = "0.0.1" +} +``` + +旧字段仍兼容: + +```kotlin +PublishInfo { + groupId = "cn.entertech.android" + artifactId = "base" + version = "0.0.1" + + pluginId = "" + implementationClass = "" + + publishUrl = "" + publishUserName = "" + publishPassword = "" +} +``` + +`local.properties` 也可以提供旧私服字段,值不要加引号: + +```properties +publishUrl=https://repo.example.com/releases +publishUserName=username +publishPassword=password +``` + +## PublishInfo 字段说明 + +### 基础坐标字段 + +| 字段 | 作用 | 是否必填 | 说明 | +| --- | --- | --- | --- | +| `groupId` | Maven 坐标的 groupId | 是 | Central 发布时必须落在已验证的 `centralNamespace` 下,例如 namespace 是 `ai.looktech`,则 `groupId` 可以是 `ai.looktech` 或 `ai.looktech.xxx`。 | +| `artifactId` | Maven 坐标的 artifactId | 是 | 普通单组件发布时直接作为 artifactId;多 variant 发布时作为默认值和 base 值,可被 `artifactIdForVariant` 动态覆盖。 | +| `version` | Maven 坐标的 version | 是 | 本地发布时 `-debug` 版本会附带 sources jar;远程 Central 发布禁止使用包含 `debug` 的版本。 | + +### Gradle 插件发布字段 + +只有发布 Gradle Plugin 模块时需要配置这两个字段;普通 Android Library 不需要。 + +| 字段 | 作用 | 是否必填 | 说明 | +| --- | --- | --- | --- | +| `pluginId` | Gradle 插件 ID | 发布 Gradle Plugin 时必填 | 写入 `gradlePlugin { plugins { ... } }` 的插件 ID,例如 `cn.entertech.foo`。 | +| `implementationClass` | Gradle 插件实现类 | 发布 Gradle Plugin 时必填 | 插件入口类全限定名,例如 `cn.entertech.foo.FooPlugin`。 | + +### 旧私服字段 + +这些字段保留是为了兼容旧项目;默认 Central 发布不建议把凭据写在 `build.gradle.kts` 里。 + +| 字段 | 作用 | 生效场景 | 说明 | +| --- | --- | --- | --- | +| `publishUrl` | 自定义 Maven 仓库地址 | `remotePublishMode = "customRepository"` | 旧私服发布地址。默认 Central 模式不会用它作为上传地址。 | +| `publishUserName` | 自定义仓库用户名 | `customRepository`,也作为 Central token username fallback | 优先级低于 `-PcentralUsername`、`CENTRAL_USERNAME`、`-PmavenCentralUsername`、`MAVEN_CENTRAL_USERNAME`。 | +| `publishPassword` | 自定义仓库密码 | `customRepository`,也作为 Central token password fallback | 优先级低于 `-PcentralPassword`、`CENTRAL_PASSWORD`、`-PmavenCentralPassword`、`MAVEN_CENTRAL_PASSWORD`。 | + +### Central 发布字段 + +| 字段 | 作用 | 是否必填 | 默认值/说明 | +| --- | --- | --- | --- | +| `remotePublishMode` | 选择远程发布模式 | 否 | 默认 `central`。可设为 `customRepository` 走旧自定义 Maven 仓库。 | +| `centralNamespace` | Sonatype Central namespace | Central 远程发布必填 | 例如 `ai.looktech`。远程发布前会校验 `groupId` 是否在该 namespace 下。 | +| `centralPublishingType` | Central Portal deployment 发布方式 | 否 | 默认 `user_managed`,上传后在 Portal 手动 Publish;可设为 `automatic`。 | +| `centralRepositoryName` | Gradle repository 名称 | 否 | 默认 `CentralStaging`,会影响 Gradle 任务名,例如 `publishEnterPublishPublicationToCentralStagingRepository`。 | + +### POM 元数据字段 + +Central 远程发布要求 POM 元数据完整;这些字段本地发布可以为空,执行 `PublishLibraryRemoteTask` 时会校验。 + +| 字段 | 作用 | Central 是否必填 | 说明 | +| --- | --- | --- | --- | +| `pomName` | POM `` | 否 | 为空时默认使用当前 artifactId。多 variant 动态 artifactId 场景下,会使用每个 publication 的最终 artifactId 作为 fallback。 | +| `pomDescription` | POM `` | 是 | 简短说明库的用途。 | +| `pomInceptionYear` | POM `` | 否 | 项目开始年份。 | +| `pomUrl` | POM `` | 是 | 项目主页、仓库地址或文档地址。 | + +### License 字段 + +| 字段 | 作用 | 默认值 | +| --- | --- | --- | +| `licenseName` | POM license name | `The Apache License, Version 2.0` | +| `licenseUrl` | POM license URL | `https://www.apache.org/licenses/LICENSE-2.0.txt` | +| `licenseDistribution` | POM license distribution | `repo` | + +### Developer 字段 + +Central 远程发布会校验开发者信息,建议全部配置。 + +| 字段 | 作用 | Central 是否必填 | +| --- | --- | --- | +| `developerId` | 开发者或组织 ID | 是 | +| `developerName` | 开发者或组织名称 | 是 | +| `developerEmail` | 联系邮箱 | 是 | +| `developerOrganization` | 组织名称 | 是 | +| `developerOrganizationUrl` | 组织主页 | 是 | +| `developerUrl` | 开发者主页 | 否 | + +### SCM 字段 + +Central 远程发布要求源码仓库信息。 + +| 字段 | 作用 | 示例 | +| --- | --- | --- | +| `scmUrl` | 仓库浏览地址 | `https://github.com/Entertech/lt-rpc-schema` | +| `scmConnection` | 只读 SCM 地址 | `scm:git:git://github.com/Entertech/lt-rpc-schema.git` | +| `scmDeveloperConnection` | 开发者 SCM 地址 | `scm:git:ssh://git@github.com/Entertech/lt-rpc-schema.git` | + +### 动态 artifactId 与 variant 过滤 + +`artifactIdForVariant { variant -> ... }` 不是普通字段,而是按 Android release variant 计算 artifactId 的回调。 +`skipVariantIf { variant -> ... }` 用于过滤不需要发布的 release variant。 + +| API | 作用 | 生效场景 | +| --- | --- | --- | +| `artifactIdForVariant` | 为每个 variant 返回独立 artifactId | Android Library 存在多个 release component 时 | +| `skipVariantIf` | 返回 `true` 时跳过该 variant,不注册 publication | Android Library 多 release variant 发布时 | +| `variant.name` | 当前 component/variant 名 | 例如 `breathAuthRelease` | +| `variant.buildType` | 当前 build type | 当前插件只发布 `release` component | +| `variant.flavors` | flavor 维度到 flavor 名称的 Map | 例如 `project -> breath`、`authentication -> auth` | +| `variant.flavor("dimension")` | 获取指定维度的 flavor 名称 | 不存在时返回空字符串 | + +## 本地打 AAR + +如果只是想在当前项目里生成一个本地 AAR 文件,不需要执行发布任务,直接使用 Android Gradle Plugin 的 assemble 任务。 + +单一 release 组件: + +```bash +./gradlew :module:assembleRelease +``` + +生成目录: + +```text +module/build/outputs/aar/ +``` + +常见产物名: + +```text +module-release.aar +``` + +如果 Library 有 flavor,需要执行具体 variant 的 assemble 任务: + +```bash +./gradlew :affective_local_sdk:assembleBreathAuthRelease +./gradlew :affective_local_sdk:assembleBreathNoAuthRelease +``` + +也可以一次构建所有 release variant: + +```bash +./gradlew :affective_local_sdk:assembleRelease +``` + +多 flavor AAR 仍输出在模块的 `build/outputs/aar/` 目录下,文件名由 Android Gradle Plugin 按 variant 生成,例如: + +```text +affective_local_sdk-breath-auth-release.aar +affective_local_sdk-breath-noAuth-release.aar +``` + +`assemble...` 只负责生成 AAR 文件,不会写入 `~/.m2`,也不会生成可供其他项目通过 Maven 坐标依赖的本地库。 + +## 发布到本地 Maven + +执行模块下的自定义任务: + +```bash +./gradlew :module:PublishLibraryLocalTask +``` + +或直接使用 Gradle Maven Publish 标准任务: + +```bash +./gradlew :module:publishToMavenLocal +``` + +本地发布的 sources 规则: + +- `version` 以 `-debug` 结尾时,发布 sources jar。 +- 非 debug 版本默认不发布 sources jar。 +- Central 远程发布不受这个规则限制,Central 必须包含 sources 和 javadoc。 + +发布成功后,产物会进入本机 Maven 仓库: + +```text +~/.m2/repository//// +``` + +例如: + +```text +~/.m2/repository/cn/entertech/android/affective-offline-sdk/1.0.0/ +``` + +多 variant 发布时,每个未过滤的 release variant 都会生成独立 artifact 目录,并且目录里应至少包含 `.aar`、`.pom`、`.module`: + +```text +~/.m2/repository/cn/entertech/android/breath-affective-offline-sdk-authentication/1.3.6/ +~/.m2/repository/cn/entertech/android/breath-affective-offline-sdk/1.3.6/ +~/.m2/repository/cn/entertech/android/sdk-affective-offline-sdk-authentication/1.3.6/ +~/.m2/repository/cn/entertech/android/sdk-affective-offline-sdk/1.3.6/ +``` + +验证本地发布不能只看 `BUILD SUCCESSFUL`。如果 Gradle 显示成功但目标 artifact 目录没有 `.aar/.pom/.module`,说明 publication 没有真实执行,应优先检查插件日志和 `publishToMavenLocal --stacktrace` 输出。 + +其他项目使用本地发布产物时,需要在仓库中启用 `mavenLocal()`: + +```kotlin +repositories { + google() + mavenCentral() + mavenLocal() +} +``` + +然后按 Maven 坐标依赖: + +```kotlin +dependencies { + implementation("cn.entertech.android:affective-offline-sdk:1.0.0") +} +``` + +多 variant 动态 artifactId 场景下,依赖最终生成的 artifactId: + +```kotlin +dependencies { + implementation("cn.entertech.android:breath-affective-offline-sdk-authentication:1.0.0") + implementation("cn.entertech.android:breath-affective-offline-sdk:1.0.0") +} +``` + +选择方式: + +| 目标 | 使用命令 | 产物位置 | 适合场景 | +| --- | --- | --- | --- | +| 只要一个 `.aar` 文件 | `assembleRelease` 或具体 `assembleRelease` | `module/build/outputs/aar/` | 手动拷贝、临时检查 AAR 内容 | +| 让其他项目通过 Maven 坐标依赖 | `PublishLibraryLocalTask` 或 `publishToMavenLocal` | `~/.m2/repository/` | 本地联调、验证 POM 依赖、模拟远程发布消费方式 | + +## Central Portal 发布 + +远程发布仍执行旧任务名: + +```bash +./gradlew :module:PublishLibraryRemoteTask +``` + +Central 必需配置示例: + +```kotlin +PublishInfo { + groupId = "ai.looktech.ltrpc.schema" + artifactId = "bt-server" + version = "1.0.0" + + centralNamespace = "ai.looktech" + centralPublishingType = "user_managed" + + pomName = "Looktech RPC BT Server Schema" + pomDescription = "BT Server Schema library for Looktech RPC" + pomInceptionYear = "2025" + pomUrl = "https://github.com/Entertech/lt-rpc-schema" + + developerId = "Entertech" + developerName = "Entertech" + developerEmail = "dev@example.com" + developerOrganization = "Entertech" + developerOrganizationUrl = "https://github.com/Entertech" + developerUrl = "https://github.com/Entertech" + + scmUrl = "https://github.com/Entertech/lt-rpc-schema" + scmConnection = "scm:git:git://github.com/Entertech/lt-rpc-schema.git" + scmDeveloperConnection = "scm:git:ssh://git@github.com/Entertech/lt-rpc-schema.git" +} +``` + +Central token 和签名建议通过环境变量或 Gradle property 注入,不要写入仓库: + +```bash CENTRAL_USERNAME="$TOKEN_USERNAME" \ CENTRAL_PASSWORD="$TOKEN_PASSWORD" \ ORG_GRADLE_PROJECT_signingInMemoryKey="$GPG_PRIVATE_KEY" \ -ORG_GRADLE_PROJECT_signingInMemoryKeyId="$GPG_KEY_ID" \ ORG_GRADLE_PROJECT_signingInMemoryKeyPassword="$GPG_PASSWORD" \ ./gradlew :module:PublishLibraryRemoteTask \ -PcentralNamespace=ai.looktech \ @@ -347,251 +346,259 @@ ORG_GRADLE_PROJECT_signingInMemoryKeyPassword="$GPG_PASSWORD" \ --stacktrace ``` -支持的 Central 相关字段: - -```kotlin -PublishInfo { - remotePublishMode = "central" // central / customRepository - centralNamespace = "" - centralPublishingType = "user_managed" // user_managed / automatic - centralRepositoryName = "CentralStaging" - - pomName = "" - pomDescription = "" - pomInceptionYear = "" - pomUrl = "" - - licenseName = "The Apache License, Version 2.0" - licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt" - licenseDistribution = "repo" - - developerId = "" - developerName = "" - developerEmail = "" - developerOrganization = "" - developerOrganizationUrl = "" - developerUrl = "" - - scmUrl = "" - scmConnection = "" - scmDeveloperConnection = "" -} -``` - -## 多 Variant 动态 artifactId - -`PublishInfo.artifactId` 是默认 artifactId,也是没有 variant 回调时的 fallback。 - -当 Android Library 存在多个 release variant 时,插件会为每个 release component 创建独立 publication: - -- 无 flavor 的普通 `release` 组件仍使用旧 publication 名:`EnterPublish`。 -- 多 flavor release 组件使用 `EnterPublish`,例如 `BreathAuthReleaseEnterPublish`。 -- `PublishLibraryRemoteTask` 检测到多个 `*EnterPublish` publication 时,会执行 `publishAllPublicationsToRepository`。 - -如果需要按 variant 动态设置 artifactId,使用 `artifactIdForVariant`: - -```kotlin -val baseArtifactId = "affective-offline-sdk" - -PublishInfo { - groupId = "cn.entertech.android" - artifactId = baseArtifactId - version = "1.0.0" - - artifactIdForVariant { variant -> - val productPrefix = "${variant.flavor("project")}-" - val authSuffix = if (variant.flavor("authentication") == "auth") { - "-authentication" - } else { - "" - } - - "$productPrefix$baseArtifactId$authSuffix" - } -} -``` - -对应 Groovy DSL: - -```groovy -def baseArtifactId = 'affective-offline-sdk' - -PublishInfo { - groupId = 'cn.entertech.android' - artifactId = baseArtifactId - version = '1.0.0' - - artifactIdForVariant { variant -> - def productPrefix = "${variant.flavor('project')}-" - def authSuffix = variant.flavor('authentication') == 'auth' ? '-authentication' : '' - return "${productPrefix}${baseArtifactId}${authSuffix}" - } -} -``` - -示例结果: - -| Variant | artifactId | -| --- | --- | -| `breathAuthRelease` | `breath-affective-offline-sdk-authentication` | -| `breathNoAuthRelease` | `breath-affective-offline-sdk` | -| `sdkAuthRelease` | `sdk-affective-offline-sdk-authentication` | -| `sdkNoAuthRelease` | `sdk-affective-offline-sdk` | - -`PublishVariantInfo` 可用字段: - -```kotlin -variant.name // 例如 breathAuthRelease -variant.buildType // 例如 release -variant.flavors // Map -variant.flavor("project") // 例如 breath / sdk -variant.flavor("authentication") // 例如 auth / noAuth -``` - -没有配置 `artifactIdForVariant` 时,所有 publication 使用 `PublishInfo.artifactId`,保持旧项目行为。 - -如果某些 variant 不需要发布,使用 `skipVariantIf`: - -```kotlin -PublishInfo { - groupId = "cn.entertech.android" - artifactId = "affective-offline-sdk" - version = "1.0.0" - - skipVariantIf { variant -> - variant.flavor("project") == "flowtime" - } -} -``` - -也可以只发布指定 product: - -```kotlin -PublishInfo { - groupId = "cn.entertech.android" - artifactId = "affective-offline-sdk" - version = "1.0.0" - - skipVariantIf { variant -> - variant.flavor("project") != "sdk" - } -} -``` - -过滤规则: - -- 未配置 `skipVariantIf` 时发布所有 release variants。 -- 可配置多个 `skipVariantIf`,任意一个返回 `true` 就跳过该 variant。 -- 被跳过的 variant 不会注册 `singleVariant`,不会创建 publication,也不会生成对应 `publish...PublicationToMavenLocal` 任务。 -- 如果所有候选 release variants 都被跳过,插件会让构建失败,避免出现没有实际发布内容的假成功。 - -## CLI 覆盖 - -Central 字段支持命令行覆盖,便于本地和 CI 使用: - -| Gradle property | 用途 | -| --- | --- | -| `-PcentralNamespace=...` | 覆盖 `centralNamespace` | -| `-PcentralPublishingType=...` | 覆盖 `centralPublishingType` | -| `-PcentralUsername=...` | Central token username | -| `-PcentralPassword=...` | Central token password | -| `-PmavenCentralUsername=...` | Central token username fallback | -| `-PmavenCentralPassword=...` | Central token password fallback | -| `-PpomName=...` | 覆盖 `pomName` | -| `-PpomDescription=...` | 覆盖 `pomDescription` | -| `-PpomUrl=...` | 覆盖 `pomUrl` | - -解析优先级: - -```text -Gradle property > 环境变量 > PublishInfo 字段 > local.properties > 默认值 -``` - -## GitHub Actions 发布 - -业务项目不需要在各自仓库里单独维护 GPG 私钥和 Central token。把这些敏感字段配置为 `Entertech` organization secrets 后,业务项目只调用本仓库提供的 reusable workflow,并使用 `secrets: inherit` 继承组织级 secrets。 - -业务项目示例: - -```yaml -name: Publish Maven Central - -on: - workflow_dispatch: - -jobs: - publish: - uses: Entertech/PublishPlugin/.github/workflows/central-publish.yml@main - secrets: inherit - with: - module: ":library" - namespace: "cn.entertech" - publishing_type: "user_managed" -``` - -如果发布时需要临时覆盖版本,可以传入: - -```yaml -with: - module: ":library" - namespace: "cn.entertech" - publishing_type: "user_managed" - version: "1.2.3" -``` - +`ORG_GRADLE_PROJECT_signingInMemoryKeyId` 可以不传。默认建议只传私钥内容和密码,让 Gradle 从 `GPG_PRIVATE_KEY` 中推断 key id;只有确实需要指定 key id 时,才传 8 位或 16 位 hex key id,例如 `00B5050F`、`0x00B5050F` 或 `2BA16C9B594CE0E6`。 + +支持的 Central 相关字段: + +```kotlin +PublishInfo { + remotePublishMode = "central" // central / customRepository + centralNamespace = "" + centralPublishingType = "user_managed" // user_managed / automatic + centralRepositoryName = "CentralStaging" + + pomName = "" + pomDescription = "" + pomInceptionYear = "" + pomUrl = "" + + licenseName = "The Apache License, Version 2.0" + licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt" + licenseDistribution = "repo" + + developerId = "" + developerName = "" + developerEmail = "" + developerOrganization = "" + developerOrganizationUrl = "" + developerUrl = "" + + scmUrl = "" + scmConnection = "" + scmDeveloperConnection = "" +} +``` + +## 多 Variant 动态 artifactId + +`PublishInfo.artifactId` 是默认 artifactId,也是没有 variant 回调时的 fallback。 + +当 Android Library 存在多个 release variant 时,插件会为每个 release component 创建独立 publication: + +- 无 flavor 的普通 `release` 组件仍使用旧 publication 名:`EnterPublish`。 +- 多 flavor release 组件使用 `EnterPublish`,例如 `BreathAuthReleaseEnterPublish`。 +- `PublishLibraryRemoteTask` 检测到多个 `*EnterPublish` publication 时,会执行 `publishAllPublicationsToRepository`。 + +如果需要按 variant 动态设置 artifactId,使用 `artifactIdForVariant`: + +```kotlin +val baseArtifactId = "affective-offline-sdk" + +PublishInfo { + groupId = "cn.entertech.android" + artifactId = baseArtifactId + version = "1.0.0" + + artifactIdForVariant { variant -> + val productPrefix = "${variant.flavor("project")}-" + val authSuffix = if (variant.flavor("authentication") == "auth") { + "-authentication" + } else { + "" + } + + "$productPrefix$baseArtifactId$authSuffix" + } +} +``` + +对应 Groovy DSL: + +```groovy +def baseArtifactId = 'affective-offline-sdk' + +PublishInfo { + groupId = 'cn.entertech.android' + artifactId = baseArtifactId + version = '1.0.0' + + artifactIdForVariant { variant -> + def productPrefix = "${variant.flavor('project')}-" + def authSuffix = variant.flavor('authentication') == 'auth' ? '-authentication' : '' + return "${productPrefix}${baseArtifactId}${authSuffix}" + } +} +``` + +示例结果: + +| Variant | artifactId | +| --- | --- | +| `breathAuthRelease` | `breath-affective-offline-sdk-authentication` | +| `breathNoAuthRelease` | `breath-affective-offline-sdk` | +| `sdkAuthRelease` | `sdk-affective-offline-sdk-authentication` | +| `sdkNoAuthRelease` | `sdk-affective-offline-sdk` | + +`PublishVariantInfo` 可用字段: + +```kotlin +variant.name // 例如 breathAuthRelease +variant.buildType // 例如 release +variant.flavors // Map +variant.flavor("project") // 例如 breath / sdk +variant.flavor("authentication") // 例如 auth / noAuth +``` + +没有配置 `artifactIdForVariant` 时,所有 publication 使用 `PublishInfo.artifactId`,保持旧项目行为。 + +如果某些 variant 不需要发布,使用 `skipVariantIf`: + +```kotlin +PublishInfo { + groupId = "cn.entertech.android" + artifactId = "affective-offline-sdk" + version = "1.0.0" + + skipVariantIf { variant -> + variant.flavor("project") == "flowtime" + } +} +``` + +也可以只发布指定 product: + +```kotlin +PublishInfo { + groupId = "cn.entertech.android" + artifactId = "affective-offline-sdk" + version = "1.0.0" + + skipVariantIf { variant -> + variant.flavor("project") != "sdk" + } +} +``` + +过滤规则: + +- 未配置 `skipVariantIf` 时发布所有 release variants。 +- 可配置多个 `skipVariantIf`,任意一个返回 `true` 就跳过该 variant。 +- 被跳过的 variant 不会注册 `singleVariant`,不会创建 publication,也不会生成对应 `publish...PublicationToMavenLocal` 任务。 +- 如果所有候选 release variants 都被跳过,插件会让构建失败,避免出现没有实际发布内容的假成功。 + +## CLI 覆盖 + +Central 字段支持命令行覆盖,便于本地和 CI 使用: + +| Gradle property | 用途 | +| --- | --- | +| `-PcentralNamespace=...` | 覆盖 `centralNamespace` | +| `-PcentralPublishingType=...` | 覆盖 `centralPublishingType` | +| `-PcentralUsername=...` | Central token username | +| `-PcentralPassword=...` | Central token password | +| `-PmavenCentralUsername=...` | Central token username fallback | +| `-PmavenCentralPassword=...` | Central token password fallback | +| `-PpomName=...` | 覆盖 `pomName` | +| `-PpomDescription=...` | 覆盖 `pomDescription` | +| `-PpomUrl=...` | 覆盖 `pomUrl` | + +解析优先级: + +```text +Gradle property > 环境变量 > PublishInfo 字段 > local.properties > 默认值 +``` + +## GitHub Actions 发布 + +业务项目不需要在各自仓库里单独维护 GPG 私钥和 Central token。把这些敏感字段配置为 `Entertech` organization secrets 后,业务项目只调用本仓库提供的 reusable workflow,并使用 `secrets: inherit` 继承组织级 secrets。 + +业务项目示例: + +```yaml +name: Publish Maven Central + +on: + workflow_dispatch: + +jobs: + publish: + uses: Entertech/PublishPlugin/.github/workflows/central-publish.yml@main + secrets: inherit + with: + module: ":library" + namespace: "cn.entertech" + publishing_type: "user_managed" +``` + +如果发布时需要临时覆盖版本,可以传入: + +```yaml +with: + module: ":library" + namespace: "cn.entertech" + publishing_type: "user_managed" + version: "1.2.3" +``` + `Entertech` organization secrets 需要维护: ```text MAVEN_CENTRAL_USERNAME MAVEN_CENTRAL_PASSWORD GPG_KEY_CONTENTS -SIGNING_KEY_ID SIGNING_PASSWORD ``` -`MAVEN_CENTRAL_USERNAME` 和 `MAVEN_CENTRAL_PASSWORD` 必须使用 Central Portal 生成的 User Token。`GPG_KEY_CONTENTS`、`SIGNING_KEY_ID`、`SIGNING_PASSWORD` 使用公司统一发布 GPG key。业务仓库无需重复创建这些 secrets,但 organization secret 的 repository access 必须授权给调用 workflow 的仓库。 - -## 发布本插件到 Central - -`PublishPlugin` 自身是 Gradle Plugin 模块,不通过自己的 `PublishLibraryRemoteTask` 发布。发布插件本身使用仓库内的 GitHub Actions: +可选 secret: ```text -.github/workflows/publish-plugin-central.yml +SIGNING_KEY_ID ``` -发布流程: - -1. PR 合入预发布分支 `pre_publish` 前,`publish-plugin-pr-check.yml` 校验版本号、构建、本地发布和 publication 元数据,并同步 README 中的插件版本。 -2. PR 合入 `pre_publish` 后,`publish-plugin-central.yml` 自动发布插件到 Central。 -3. 发布版本会自动规范化为 `数字.数字.数字`,例如 `1.2.0-local` 会发布为 `1.2.0`;如果无法识别出三段数字,workflow 失败。 -4. 发布前先预演 `pre_publish` 合入 `main`,如果存在冲突,workflow 直接失败,不发布 Central。 -5. 同一个 `pre_publish` 发布 run 正在执行时,如果又有新的提交 push 到 `pre_publish`,旧 run 会被自动取消。 -6. Central deployment 创建成功后,workflow 再次同步 README 中的插件版本,并把变更提交回 `pre_publish`。 -7. README 同步提交成功后,workflow 创建并推送 `v` tag。 -8. tag 推送成功后,workflow 将当前 `pre_publish` merge 到 `main`。 -9. 如果 Central 发布失败,不会更新 README,不会创建 tag,也不会合入 `main`。 - +`MAVEN_CENTRAL_USERNAME` 和 `MAVEN_CENTRAL_PASSWORD` 必须使用 Central Portal 生成的 User Token。`GPG_KEY_CONTENTS` 和 `SIGNING_PASSWORD` 使用公司统一发布 GPG key。`SIGNING_KEY_ID` 建议留空,让 Gradle 从 `GPG_KEY_CONTENTS` 私钥内容推断;如果配置,必须是 Gradle signing 支持的 PGP key id,例如 `00B5050F`、`0x00B5050F` 或 16 位 long key id。业务仓库无需重复创建这些 secrets,但 organization secret 的 repository access 必须授权给调用 workflow 的仓库。 + +## 发布本插件到 Central + +`PublishPlugin` 自身是 Gradle Plugin 模块,不通过自己的 `PublishLibraryRemoteTask` 发布。发布插件本身使用仓库内的 GitHub Actions: + +```text +.github/workflows/publish-plugin-central.yml +``` + +发布流程: + +1. PR 合入预发布分支 `pre_publish` 前,`publish-plugin-pr-check.yml` 校验版本号、构建、本地发布和 publication 元数据,并同步 README 中的插件版本。 +2. PR 合入 `pre_publish` 后,`publish-plugin-central.yml` 自动发布插件到 Central。 +3. 发布版本会自动规范化为 `数字.数字.数字`,例如 `1.2.0-local` 会发布为 `1.2.0`;如果无法识别出三段数字,workflow 失败。 +4. 发布前先预演 `pre_publish` 合入 `main`,如果存在冲突,workflow 直接失败,不发布 Central。 +5. 同一个 `pre_publish` 发布 run 正在执行时,如果又有新的提交 push 到 `pre_publish`,旧 run 会被自动取消。 +6. Central deployment 创建成功后,workflow 再次同步 README 中的插件版本,并把变更提交回 `pre_publish`。 +7. README 同步提交成功后,workflow 创建并推送 `v` tag。 +8. tag 推送成功后,workflow 将当前 `pre_publish` merge 到 `main`。 +9. 如果 Central 发布失败,不会更新 README,不会创建 tag,也不会合入 `main`。 + 手动触发或自动发布时需要配置这些 secrets: ```text MAVEN_CENTRAL_USERNAME MAVEN_CENTRAL_PASSWORD GPG_KEY_CONTENTS -SIGNING_KEY_ID SIGNING_PASSWORD ``` -发布坐标: - -```text -cn.entertech.android:publish:1.2.1 -cn.entertech.publish:cn.entertech.publish.gradle.plugin:1.2.1 -``` - -其中第二个是 Gradle plugin marker,用于支持: - -```kotlin -plugins { - id("cn.entertech.publish") version "1.2.1" -} -``` +`SIGNING_KEY_ID` 是可选项。为空时发布 workflow 不会传 key id 给 Gradle,Gradle 会从 `GPG_KEY_CONTENTS` 推断;如果填了 40 位 fingerprint,workflow 会自动截取最后 16 位作为 long key id;如果填了邮箱、描述文本等非法值,workflow 会清空该值并回退到从私钥推断,避免 `Could not read PGP secret key`。 + +发布坐标: + +```text +cn.entertech.android:publish:1.2.1 +cn.entertech.publish:cn.entertech.publish.gradle.plugin:1.2.1 +``` + +其中第二个是 Gradle plugin marker,用于支持: + +```kotlin +plugins { + id("cn.entertech.publish") version "1.2.1" +} +```