This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-jcasbin.git


The following commit(s) were added to refs/heads/master by this push:
     new 3ab35f3  build: complete JCasbin 1.100.0 release preparation (#531)
3ab35f3 is described below

commit 3ab35f38b088f32ea3f2285474cc98009945e53d
Author: Emery <[email protected]>
AuthorDate: Mon Sep 14 10:25:22 2026 +0800

    build: complete JCasbin 1.100.0 release preparation (#531)
---
 .github/scripts/test_verify_release.py | 147 +++++++++++++++++++++++++++++++++
 .github/scripts/verify_release.py      | 112 +++++++++++++++++++++++++
 .github/workflows/maven-ci.yml         |   5 +-
 .github/workflows/maven-publish.yml    |   8 ++
 .github/workflows/release.yml          |  38 ++++++---
 RELEASING.md                           |  90 ++++++++++++++++++++
 pom.xml                                |   2 +-
 7 files changed, 388 insertions(+), 14 deletions(-)

diff --git a/.github/scripts/test_verify_release.py 
b/.github/scripts/test_verify_release.py
new file mode 100644
index 0000000..76dda0a
--- /dev/null
+++ b/.github/scripts/test_verify_release.py
@@ -0,0 +1,147 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import hashlib
+import io
+import os
+from pathlib import Path
+import subprocess
+import tarfile
+import tempfile
+import unittest
+from unittest.mock import patch
+import zipfile
+
+import verify_release as release
+
+
+class ReleaseVerificationTest(unittest.TestCase):
+    def setUp(self):
+        self.temp = tempfile.TemporaryDirectory()
+        self.addCleanup(self.temp.cleanup)
+        self.previous = Path.cwd()
+        self.repo = Path(self.temp.name)
+        os.chdir(self.repo)
+        self.addCleanup(os.chdir, self.previous)
+        self.git('init', '-q')
+        Path('pom.xml').write_text('<project 
xmlns="http://maven.apache.org/POM/4.0.0";><version>1.0.0</version></project>')
+        Path('LICENSE').write_text('Test fixture')
+        self.git('add', '.')
+        self.git('-c', 'user.name=Release test', '-c', 
'[email protected]', 'commit', '-qm', 'test: fixture')
+        self.name = 'apache-casbin-jcasbin-1.0.0-incubating-src'
+        self.remote = {release.ASF + 'KEYS': b'test public key'}
+        for extension in ('tar.gz', 'zip'):
+            data = self.git('archive', '--format=' + extension, '--prefix=' + 
self.name + '/', 'HEAD')
+            self.put_archive(extension, data)
+
+    def git(self, *args):
+        return subprocess.check_output(['git', *args], stderr=subprocess.PIPE)
+
+    def put_archive(self, extension, data):
+        filename = self.name + '.' + extension
+        url = release.ASF + 'jcasbin-1.0.0-incubating/' + filename
+        self.remote[url] = data
+        self.remote[url + '.sha512'] = (hashlib.sha512(data).hexdigest() + '  
' + filename + '\n').encode()
+        self.remote[url + '.asc'] = b'test signature'
+
+    def invoke(self, signature_error=None):
+        # No live downloads or signing material: exercise real Git/archive/hash
+        # verification and inject only the external network/GPG boundaries.
+        real_run = subprocess.run
+        def run_command(command, **kwargs):
+            if command[0] == 'gpg':
+                return subprocess.CompletedProcess(command, 0)
+            return real_run(command, **kwargs)
+        with patch.object(release, 'download', 
side_effect=self.remote.__getitem__), \
+             patch.object(release.subprocess, 'run', side_effect=run_command) 
as calls, \
+             patch.object(release, 'verify_signature', 
side_effect=signature_error) as signature:
+            release.verify('1.0.0', Path('dist'))
+        self.assertEqual(sum(call.args[0][0] == 'gpg' for call in 
calls.call_args_list), 1)
+        self.assertEqual(signature.call_count, 2)
+
+    def test_preserves_all_six_promoted_files(self):
+        self.invoke()
+        files = list(Path('dist').iterdir())
+        self.assertEqual(len(files), 6)
+        for file in files:
+            self.assertEqual(file.read_bytes(), self.remote[release.ASF + 
'jcasbin-1.0.0-incubating/' + file.name])
+
+    def test_rejects_missing_signature(self):
+        self.remote.pop(release.ASF + 'jcasbin-1.0.0-incubating/' + self.name 
+ '.tar.gz.asc')
+        with self.assertRaises(KeyError):
+            self.invoke()
+
+    def test_rejects_signature_failure(self):
+        with self.assertRaises(subprocess.CalledProcessError):
+            self.invoke(subprocess.CalledProcessError(1, 'gpg'))
+
+    def test_rejects_corrupt_archive(self):
+        self.remote[release.ASF + 'jcasbin-1.0.0-incubating/' + self.name + 
'.tar.gz'] += b'corrupt'
+        with self.assertRaises(ValueError):
+            self.invoke()
+
+    def test_rejects_different_source_with_valid_checksum(self):
+        Path('LICENSE').write_text('Changed after vote')
+        self.git('add', 'LICENSE')
+        self.git('-c', 'user.name=Release test', '-c', 
'[email protected]', 'commit', '-qm', 'test: changed tree')
+        with self.assertRaises(ValueError):
+            self.invoke()
+
+    def test_rejects_zip_missing_file_with_valid_checksum(self):
+        buf = io.BytesIO()
+        with zipfile.ZipFile(buf, 'w') as archive:
+            archive.writestr(self.name + '/pom.xml', 
Path('pom.xml').read_bytes())
+        self.put_archive('zip', buf.getvalue())
+        with self.assertRaises(ValueError):
+            self.invoke()
+
+    def test_rejects_overwriting_existing_output(self):
+        Path('dist').mkdir()
+        Path('dist/sentinel').write_text('keep')
+        with self.assertRaises(FileExistsError):
+            self.invoke()
+        self.assertEqual(Path('dist/sentinel').read_text(), 'keep')
+
+    def test_rejects_invalid_version_before_network(self):
+        for version in ('1.0.0-rc1', '../1.0.0', '1.0.1'):
+            with self.subTest(version=version), patch.object(release, 
'download') as network:
+                with self.assertRaises(ValueError):
+                    release.verify(version, Path('dist'))
+                network.assert_not_called()
+
+    def test_rejects_bad_checksum_filename(self):
+        with self.assertRaises(ValueError):
+            release.check_checksum(b'x', (hashlib.sha512(b'x').hexdigest() + ' 
 other\n').encode(), 'expected')
+
+    def test_rejects_archive_traversal_and_links(self):
+        for name, kind in (('../escape', tarfile.REGTYPE), ('root/link', 
tarfile.SYMTYPE)):
+            buf = io.BytesIO()
+            with tarfile.open(fileobj=buf, mode='w') as archive:
+                item = tarfile.TarInfo(name)
+                item.type = kind
+                archive.addfile(item)
+            with self.assertRaises(ValueError):
+                release.source_files(buf.getvalue(), 'tar')
+
+    def test_gpg_verification_is_checked_and_uses_isolated_home(self):
+        with patch.object(release.subprocess, 'run') as gpg:
+            release.verify_signature(Path('isolated'), Path('source.asc'), 
Path('source'))
+        gpg.assert_called_once_with(['gpg', '--batch', '--homedir', 
'isolated', '--verify', 'source.asc', 'source'], check=True)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/.github/scripts/verify_release.py 
b/.github/scripts/verify_release.py
new file mode 100644
index 0000000..8795f73
--- /dev/null
+++ b/.github/scripts/verify_release.py
@@ -0,0 +1,112 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Verify promoted ASF source files before GitHub or Maven publication."""
+
+import argparse
+import hashlib
+import io
+from pathlib import Path, PurePosixPath
+import re
+import subprocess
+import tarfile
+import tempfile
+import urllib.request
+import xml.etree.ElementTree as ET
+import zipfile
+
+ASF = 'https://downloads.apache.org/incubator/casbin/'
+
+
+def source_files(data, extension):
+    files = {}
+    def safe_name(name):
+        if '\\' in name or PurePosixPath(name).is_absolute() or '..' in 
PurePosixPath(name).parts:
+            raise ValueError('Unsafe archive path')
+    if extension == 'zip':
+        with zipfile.ZipFile(io.BytesIO(data)) as archive:
+            for item in archive.infolist():
+                safe_name(item.filename)
+                if (item.external_attr >> 16) & 0o170000 == 0o120000:
+                    raise ValueError('ZIP symlink is unsupported')
+                if not item.is_dir():
+                    if item.filename in files:
+                        raise ValueError('Duplicate ZIP member')
+                    files[item.filename] = archive.read(item)
+    else:
+        with tarfile.open(fileobj=io.BytesIO(data), mode='r:*') as archive:
+            for item in archive:
+                safe_name(item.name)
+                if item.isdir():
+                    continue
+                if not item.isfile() or item.name in files:
+                    raise ValueError('Unsupported or duplicate TAR member')
+                files[item.name] = archive.extractfile(item).read()
+    return files
+
+
+def check_checksum(data, checksum, filename):
+    match = re.fullmatch(r'([0-9a-fA-F]{128}) [ *]' + re.escape(filename) + 
r'\r?\n?', checksum.decode('ascii'))
+    if not match or hashlib.sha512(data).hexdigest() != match[1].lower():
+        raise ValueError('Invalid SHA-512 for ' + filename)
+
+
+def download(url):
+    with urllib.request.urlopen(url, timeout=60) as response:
+        return response.read()
+
+
+def verify_signature(home, signature, archive):
+    subprocess.run(['gpg', '--batch', '--homedir', str(home), '--verify', 
str(signature), str(archive)], check=True)
+
+
+def verify(version, output):
+    if not re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+', version):
+        raise ValueError('Expected final version X.Y.Z')
+    pom_version = 
ET.parse('pom.xml').findtext('{http://maven.apache.org/POM/4.0.0}version')
+    if pom_version != version:
+        raise ValueError('POM does not match requested version')
+    commit = subprocess.check_output(['git', 'rev-parse', 'HEAD'], 
text=True).strip()
+    name = 'apache-casbin-jcasbin-' + version + '-incubating-src'
+    expected = source_files(subprocess.check_output(['git', 'archive', 
'--format=tar', '--prefix=' + name + '/', commit]), 'tar')
+    output.mkdir(parents=True, exist_ok=False)
+    url = ASF + 'jcasbin-' + version + '-incubating/'
+    with tempfile.TemporaryDirectory(prefix='jcasbin-public-keys-') as temp:
+        home = Path(temp)
+        home.chmod(0o700)
+        keys = home / 'KEYS'
+        keys.write_bytes(download(ASF + 'KEYS'))
+        subprocess.run(['gpg', '--batch', '--homedir', str(home), '--import', 
str(keys)], check=True)
+        for extension in ('tar.gz', 'zip'):
+            filename = name + '.' + extension
+            archive = output / filename
+            for suffix in ('', '.sha512', '.asc'):
+                (output / (filename + suffix)).write_bytes(download(url + 
filename + suffix))
+            data = archive.read_bytes()
+            check_checksum(data, (output / (filename + 
'.sha512')).read_bytes(), filename)
+            verify_signature(home, output / (filename + '.asc'), archive)
+            if source_files(data, extension) != expected:
+                raise ValueError('Promoted archive does not match final Git 
commit: ' + filename)
+    print('Verified promoted ASF originals for ' + commit)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('version')
+    parser.add_argument('--output', type=Path, default=Path('dist'))
+    args = parser.parse_args()
+    verify(args.version, args.output)
diff --git a/.github/workflows/maven-ci.yml b/.github/workflows/maven-ci.yml
index dedb8c5..cb145d8 100644
--- a/.github/workflows/maven-ci.yml
+++ b/.github/workflows/maven-ci.yml
@@ -41,7 +41,10 @@ jobs:
           java-version: 1.8
 
       - name: Build with Maven
-        run: mvn clean test cobertura:cobertura
+        run: mvn -B -ntp clean verify cobertura:cobertura -Dgpg.skip=true
+
+      - name: Test release verification
+        run: python3 -m unittest discover -s .github/scripts -p 'test_*.py'
 
       - name: Codecov
         uses: codecov/codecov-action@v1
diff --git a/.github/workflows/maven-publish.yml 
b/.github/workflows/maven-publish.yml
index d8c7639..34a5810 100644
--- a/.github/workflows/maven-publish.yml
+++ b/.github/workflows/maven-publish.yml
@@ -38,6 +38,9 @@ jobs:
     name: Publish Maven
     if: ${{ github.repository == 'apache/casbin-jcasbin' }}
     runs-on: ubuntu-latest
+    concurrency:
+      group: maven-${{ github.ref }}
+      cancel-in-progress: false
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -63,6 +66,11 @@ jobs:
               raise SystemExit(f'POM version mismatch: expected {requested}, 
got {version}')
           PY
 
+      - name: Verify promoted ASF release before loading signing credentials
+        env:
+          MAVEN_VERSION: ${{ inputs.maven_version }}
+        run: python3 .github/scripts/verify_release.py "${MAVEN_VERSION}" 
--output verified-release
+
       - name: Set up JDK 1.8
         uses: actions/setup-java@v4
         with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 70e23ed..0e4b544 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,6 +23,8 @@ name: Release
 #
 # The release manager downloads the source package from the release, signs it
 # locally, and uses it for voting. This workflow does not sign anything.
+# Push the final tag only after the voted originals have been promoted to
+# downloads.apache.org/incubator/casbin/jcasbin-X.Y.Z-incubating/.
 
 on:
   push:
@@ -32,6 +34,10 @@ on:
 permissions:
   contents: write
 
+concurrency:
+  group: release-${{ github.ref }}
+  cancel-in-progress: false
+
 jobs:
   prepare:
     name: Prepare Release
@@ -179,6 +185,7 @@ jobs:
           } > RELEASE_NOTES.md
 
       - name: Create source packages
+        if: needs.prepare.outputs.prerelease == 'true'
         env:
           TAG: ${{ needs.prepare.outputs.tag }}
           BASENAME: ${{ needs.prepare.outputs.basename }}
@@ -190,19 +197,26 @@ jobs:
           sha512sum "${BASENAME}.tar.gz" > "${BASENAME}.tar.gz.sha512"
           sha512sum "${BASENAME}.zip" > "${BASENAME}.zip.sha512"
 
+      - name: Verify and reuse promoted ASF source packages
+        if: needs.prepare.outputs.prerelease == 'false'
+        env:
+          VERSION: ${{ needs.prepare.outputs.maven_version }}
+        run: python3 .github/scripts/verify_release.py "${VERSION}"
+
       - name: Create GitHub release
-        uses: softprops/action-gh-release@v2
-        with:
-          tag_name: ${{ needs.prepare.outputs.tag }}
-          name: JCasbin ${{ needs.prepare.outputs.tag }}
-          body_path: RELEASE_NOTES.md
-          draft: false
-          prerelease: ${{ needs.prepare.outputs.prerelease == 'true' }}
-          files: |
-            dist/${{ needs.prepare.outputs.basename }}.tar.gz
-            dist/${{ needs.prepare.outputs.basename }}.tar.gz.sha512
-            dist/${{ needs.prepare.outputs.basename }}.zip
-            dist/${{ needs.prepare.outputs.basename }}.zip.sha512
+        env:
+          GH_TOKEN: ${{ github.token }}
+          TAG: ${{ needs.prepare.outputs.tag }}
+          PRERELEASE: ${{ needs.prepare.outputs.prerelease }}
+        run: |
+          set -euo pipefail
+          FLAGS=()
+          if [ "${PRERELEASE}" = "true" ]; then
+            FLAGS+=(--prerelease --latest=false)
+          fi
+          # Creation fails if this release exists; never replace its assets.
+          gh release create "${TAG}" dist/* --verify-tag \
+            --title "JCasbin ${TAG}" --notes-file RELEASE_NOTES.md 
"${FLAGS[@]}"
 
   publish-maven:
     name: Publish Maven
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..0484bf5
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,90 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Releasing JCasbin
+
+Prepare version, dependency, licensing and workflow changes together in one
+release-preparation PR. Complete the scoped checks before requesting merge.
+Use [BUILDING.md](BUILDING.md) for the full Java 8/Maven build. Release-script
+tests run with `python3 -m unittest discover -s .github/scripts -p 
'test_*.py'`.
+The release workflows require Python 3 and GnuPG, provided by Ubuntu runners.
+
+## Before creating an RC tag
+
+1. Check the latest releases/tags and commit the selected `X.Y.Z` in the POM.
+   Resolve relevant dependency updates and legal/source-archive findings in the
+   same PR. The approved release scope determines which changes are included.
+2. After merge, record the exact merged SHA and verify its build. Check the
+   final source archive with Apache RAT and review LICENSE, NOTICE, DISCLAIMER,
+   third-party material and any exclusions. New merged bytes need new evidence.
+3. Confirm that a maintainer can create the tag, the release manager can sign
+   and write the Casbin ASF distribution directories, and the signing public
+   key is in the official Casbin KEYS file.
+4. The Maven job uses existing repository secrets `GPG_PRIVATE_KEY`,
+   `GPG_PASSPHRASE`, `GPG_KEY_NAME`, `OSSRH_JIRA_USERNAME` and
+   `OSSRH_JIRA_PASSWORD`. The last two names are retained for compatibility:
+   their values must be the Central Publisher Portal user-token username and
+   password with publishing permission for `org.casbin`, not old OSSRH login
+   credentials. Check configuration without disclosing secret values.
+
+## RC and community vote
+
+Create `vX.Y.Z-rcN` at the reviewed merged SHA. The tag workflow creates a
+GitHub prerelease containing:
+
+- `apache-casbin-jcasbin-X.Y.Z-incubating-src.tar.gz` and its `.sha512`;
+- `apache-casbin-jcasbin-X.Y.Z-incubating-src.zip` and its `.sha512`.
+
+Download and verify these exact archives; independently inspect/unpack/build
+them. Sign **both** archives, adding each `.asc`. Stage all six files under
+`https://dist.apache.org/repos/dist/dev/incubator/casbin/jcasbin-X.Y.Z-incubating-rcN/`.
+Verify the public downloads, signatures and checksums, then conduct the
+applicable Casbin/Incubator votes on these immutable files. Never overwrite a
+staged candidate. If candidate bytes must change, use a new RC number.
+
+GitHub refuses creation if that Release already exists. A failed or partial
+run needs its actual uploaded state inspected; do not delete and recreate a
+candidate already used for voting as a retry mechanism.
+
+## Promote and publish after approval
+
+1. Record the successful applicable vote results. Promote the same six files
+   to the ASF release directory `incubator/casbin/jcasbin-X.Y.Z-incubating/`.
+2. Wait until all six files are available at
+   `https://downloads.apache.org/incubator/casbin/jcasbin-X.Y.Z-incubating/`.
+3. Create the final `vX.Y.Z` tag at the same reviewed RC commit. This workflow
+   downloads the promoted ASF originals, verifies SHA-512 and detached
+   signatures against Casbin KEYS, and compares both archive file sets with
+   the tagged Git source. It does not rebuild or replace those source archives.
+4. GitHub Release creation fails if the Release already exists. Maven Central
+   publishing follows only after the final GitHub Release job succeeds.
+   A Maven-only retry is available through **Publish Maven** on the matching
+   final tag. It repeats the ASF-source verification before loading private
+   signing credentials; never dispatch it from master or an RC tag. Check
+   whether the Maven version already exists before retrying a failed publish.
+5. Verify the published distributions, update download links, send the release
+   announcement as authorized, and retain the vote/artifact evidence.
+
+Missing ASF files, mismatched source, bad checksums or invalid signatures stop
+publication. Availability in ASF's release directory is an execution gate;
+the release manager remains responsible for the actual vote and promotion.
+These operations require no additional source PR after the preparation merges.
+
+References: [ASF release 
policy](https://www.apache.org/legal/release-policy.html)
+and [Central Maven 
publishing](https://central.sonatype.org/publish/publish-portal-maven/).
diff --git a/pom.xml b/pom.xml
index f8db737..658ad59 100644
--- a/pom.xml
+++ b/pom.xml
@@ -278,7 +278,7 @@ under the License.
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
-            <version>2.18.1</version>
+            <version>2.18.9</version>
         </dependency>
     </dependencies>
 </project>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to