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]