diff --git a/.buddy/cd.yml b/.buddy/cd.yml index aefa14ea..713abcf0 100644 --- a/.buddy/cd.yml +++ b/.buddy/cd.yml @@ -61,9 +61,9 @@ - "echo \"Creating tag v${SDK_VERSION}...\"" - "git tag -a \"v${SDK_VERSION}\" -m \"CI Generated Tag\"" - "" - - "# Publish to Maven" - - echo "Publishing to Maven repository..." - - ./gradlew publish closeAndReleaseStagingRepository + - "# Publish to Maven Central via Sonatype Central Portal" + - echo "Publishing to Sonatype Central..." + - ./gradlew uploadToSonatypeCentral - "if [ $? -ne 0 ]; then" - ' echo "Failed to publish to Maven repository."' - " git tag -d \"v${SDK_VERSION}\" # Remove tag if publishing fails" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c0333f43 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,258 @@ +# Contributing to Smartcar Java SDK + +Thank you for your interest in contributing to the Smartcar Java SDK! This document provides guidelines and information for developers working on this project. + +## Development Setup + +### Prerequisites +- **Java**: Java 8+ for development, Java 17+ recommended for CI/CD compatibility +- **Gradle**: Project uses Gradle 7.6.4 (use included wrapper `./gradlew`) +- **Git**: For version control + +### Getting Started +1. Fork and clone the repository +2. Build the project to ensure everything works: + ```bash + ./gradlew build + ``` + +### Project Structure +``` +src/ +├── main/java/com/smartcar/sdk/ # Main SDK code +├── test/java/com/smartcar/sdk/ # Unit tests +└── integration/java/com/smartcar/sdk/ # Integration tests +``` + +### Running Tests +```bash +# Unit tests +./gradlew test + +# Integration tests (requires API credentials) +./gradlew integration + +# All tests +./gradlew check + +# Test coverage report +./gradlew jacocoTestReport +``` + +### Code Style +- Follow existing code conventions +- Use meaningful variable and method names +- Include appropriate JavaDoc comments for public APIs +- Maintain Java 8 compatibility + +## Maven Central Publishing + +This project publishes to Maven Central using the **Sonatype Central Portal** (modern replacement for OSSRH Legacy). + +### Publishing Architecture + +#### Automated CI/CD Pipeline (`.buddy/cd.yml`) +Our CI/CD pipeline automatically handles releases when code is pushed to `master`: + +1. **Version Detection**: Extracts version from `gradle.properties` +2. **Duplicate Check**: Ensures the version tag doesn't already exist +3. **Change Detection**: Verifies there are changes since the last tag +4. **Signing**: Uses PGP key from environment variables +5. **Upload**: Runs `./gradlew uploadToSonatypeCentral` +6. **Tagging**: Creates git tag and pushes to GitHub + +#### Custom Publishing Task (`build.gradle`) +The `uploadToSonatypeCentral` task handles the complex Maven Central requirements: + +- **Bundle Creation**: Creates proper Maven directory structure +- **Artifact Collection**: Gathers JAR, sources, javadoc, POM, and module files +- **Signature Generation**: Signs all artifacts with PGP +- **Checksum Generation**: Creates MD5 and SHA1 checksums +- **Bundle Upload**: Submits to Sonatype Central Portal via REST API + +### Required Environment Variables + +#### For CI/CD: +```bash +# PGP Signing (base64 encoded private key) +ORG_GRADLE_PROJECT_signingKey_encoded="base64-encoded-private-key" +ORG_GRADLE_PROJECT_signingPassword="your-pgp-key-password" + +# Sonatype Central Portal credentials +ORG_GRADLE_PROJECT_sonatypeUsername="your-username" +ORG_GRADLE_PROJECT_sonatypePassword="your-password" + +# GitHub token for tagging +GH_TOKEN="your-github-token" +``` + +#### For Local Development: +```bash +# PGP Signing (raw private key) +ORG_GRADLE_PROJECT_signingKey="$(cat ~/.gnupg/private-key.asc)" +ORG_GRADLE_PROJECT_signingPassword="your-pgp-key-password" + +# Sonatype credentials (same as CI) +ORG_GRADLE_PROJECT_sonatypeUsername="your-username" +ORG_GRADLE_PROJECT_sonatypePassword="your-password" +``` + +### PGP Key Setup + +#### Generating a New PGP Key +```bash +# Generate RSA 4096-bit key +gpg --full-generate-key +# Choose: RSA and RSA, 4096 bits, no expiration +# Use real name and email for Smartcar organization + +# List keys to get key ID +gpg --list-secret-keys --keyid-format LONG + +# Export private key for CI (base64 encoded) +gpg --armor --export-secret-keys YOUR_KEY_ID | base64 -w 0 +``` + +#### Key Distribution +```bash +# Upload to public keyservers +gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID +gpg --keyserver keys.openpgp.org --send-keys YOUR_KEY_ID +``` + +### Local Testing +To test the publishing process locally: + +```bash +# 1. Set up environment variables (see above) + +# 2. Build and sign artifacts +./gradlew build signMainPublication + +# 3. Test upload (will create bundle and upload) +./gradlew uploadToSonatypeCentral + +# 4. Check Sonatype Central Portal +# Visit: https://central.sonatype.com +# Review uploaded artifacts and click "Publish" when ready +``` + +### Publishing Workflow Details + +#### Phase 1: Automated Upload +- CI pipeline uploads signed artifacts to Sonatype Central +- Validation occurs automatically (signatures, checksums, required files) +- Upload ID is returned for tracking + +#### Phase 2: Manual Publishing +- **Human Review**: Visit [Sonatype Central Portal](https://central.sonatype.com) +- **Validation Check**: Verify all artifacts are present and properly signed +- **Publish Action**: Click "Publish" button to release to Maven Central +- **Propagation**: Artifacts appear on Maven Central within 10-30 minutes + +#### Why Manual Publishing? +Sonatype Central Portal requires manual approval for security reasons, unlike the old OSSRH system which could auto-release. This provides: +- Final quality control before public release +- Protection against accidental releases +- Audit trail for all publications + +### Troubleshooting + +#### Common Issues: + +**Missing Signatures:** +```bash +# Ensure PGP key is properly configured +gpg --list-secret-keys + +# Verify signing works +echo "test" | gpg --clearsign +``` + +**Bundle Validation Errors:** +- Check that all required files are included: JAR, sources, javadoc, POM, module +- Verify each artifact has corresponding .asc, .md5, .sha1 files +- Ensure proper Maven directory structure + +**Authentication Issues:** +- Verify Sonatype Central credentials (not OSSRH credentials) +- Check token hasn't expired +- Ensure username/password are correctly set + +**Upload Failures:** +```bash +# Test credentials +curl -u "$USERNAME:$PASSWORD" https://central.sonatype.com/api/v1/publisher/status + +# Check bundle structure +unzip -l build/sonatype-bundle.zip +``` + +### Migration Notes + +This project was migrated from **OSSRH Legacy** to **Sonatype Central Portal** in 2025: + +#### Key Changes: +- **Publishing URL**: `s01.oss.sonatype.org` → `central.sonatype.com` +- **Credentials**: New Sonatype Central account required +- **Process**: Automated staging/release → Upload + manual publish +- **Bundle Format**: Custom zip bundle instead of individual file uploads + +#### Benefits: +- Faster publishing (10-30 min vs 2+ hours) +- Better security with manual approval +- Improved validation and error reporting +- Modern REST API instead of legacy Nexus + +## Release Process + +### Version Management +- Version is defined in `gradle.properties` as `libVersion` +- Follow semantic versioning (MAJOR.MINOR.PATCH) +- Update version in `gradle.properties` before creating release + +### Creating a Release +1. **Update Version**: Modify `libVersion` in `gradle.properties` +2. **Test Locally**: Run full test suite and validate changes +3. **Commit Changes**: Push version bump to `master` branch +4. **Automated Pipeline**: CI will automatically build, sign, and upload +5. **Manual Publish**: Visit Sonatype Central Portal and click "Publish" +6. **Verify Release**: Check Maven Central after 30 minutes + +### Emergency Procedures +If a release needs to be withdrawn or has critical issues: +- Contact Sonatype support (releases cannot be deleted from Maven Central) +- Prepare hotfix release with incremented version +- Update documentation and notify users + +## Dependencies and Compatibility + +### Current Dependencies +- **Gson**: JSON serialization/deserialization +- **OkHttp**: HTTP client for API communication +- **Selenium**: WebDriver for integration testing +- **TestNG**: Testing framework +- **PowerMock**: Mocking framework for tests + +### Java Compatibility +- **Source**: Java 8 (maintained for broad compatibility) +- **Target**: Java 8 bytecode +- **CI/CD**: Java 17+ (for modern build tools) +- **Testing**: Selenium 4.x (last version supporting Java 8) + +### Upgrade Considerations +When updating dependencies: +- Maintain Java 8 compatibility +- Test integration test suite thoroughly +- Check for CVE vulnerabilities with `./gradlew dependencyCheck` +- Update build.gradle and verify signing still works + +## Questions and Support + +For questions about: +- **SDK Usage**: Check [Smartcar Developer Documentation](https://smartcar.com/docs) +- **Development**: Create GitHub issue or reach out to maintainers +- **Publishing Issues**: Check Sonatype Central Portal or contact support + +## License +This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. diff --git a/README.md b/README.md index 00143397..451c0edb 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,53 @@ Smartcar aims to support the SDK on all LTS Java releases (and Java 8) until the In accordance with the Semantic Versioning specification, the addition of support for new Java releases would result in a MINOR version bump and the removal of support for Java releases would result in a MAJOR version bump. +## Development and Publishing + +### Development Setup +This project uses Gradle 7.6.4 and requires Java 8+ for development (Java 17+ for CI/CD). + +```bash +# Build the project +./gradlew build + +# Run tests +./gradlew test + +# Run integration tests +./gradlew integration +``` + +### Publishing to Maven Central +This SDK is published to Maven Central using the **Sonatype Central Portal** (the modern replacement for OSSRH). The publishing process is automated through our CI/CD pipeline but requires manual approval. + +#### Publishing Workflow: +1. **Automated Upload**: When code is pushed to `master`, our CI pipeline automatically: + - Builds and signs the artifacts using PGP + - Uploads to Sonatype Central Portal for validation + - Creates a git tag for the release + +2. **Manual Publishing**: After upload validation succeeds: + - Visit the [Sonatype Central Portal](https://central.sonatype.com) + - Review the uploaded artifacts + - Click "Publish" to release to Maven Central + - Artifacts sync to Maven Central within 10-30 minutes + +#### Local Publishing (Development) +For testing the publishing process locally: + +```bash +# Ensure signing keys are configured (see CONTRIBUTING.md) +export ORG_GRADLE_PROJECT_signingKey="$(cat private-key.asc)" +export ORG_GRADLE_PROJECT_signingPassword="your-key-password" +export ORG_GRADLE_PROJECT_sonatypeUsername="your-sonatype-username" +export ORG_GRADLE_PROJECT_sonatypePassword="your-sonatype-password" + +# Upload to Sonatype Central +./gradlew uploadToSonatypeCentral +``` + +For detailed development and contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md). + [1]: https://tools.ietf.org/html/rfc6749#section-1.3.1 [2]: https://tools.ietf.org/html/rfc6749 diff --git a/README.mdt b/README.mdt index 38856513..c0e1803a 100644 --- a/README.mdt +++ b/README.mdt @@ -125,6 +125,53 @@ Smartcar aims to support the SDK on all LTS Java releases (and Java 8) until the In accordance with the Semantic Versioning specification, the addition of support for new Java releases would result in a MINOR version bump and the removal of support for Java releases would result in a MAJOR version bump. +## Development and Publishing + +### Development Setup +This project uses Gradle 7.6.4 and requires Java 8+ for development (Java 17+ for CI/CD). + +```bash +# Build the project +./gradlew build + +# Run tests +./gradlew test + +# Run integration tests +./gradlew integration +``` + +### Publishing to Maven Central +This SDK is published to Maven Central using the **Sonatype Central Portal** (the modern replacement for OSSRH). The publishing process is automated through our CI/CD pipeline but requires manual approval. + +#### Publishing Workflow: +1. **Automated Upload**: When code is pushed to `master`, our CI pipeline automatically: + - Builds and signs the artifacts using PGP + - Uploads to Sonatype Central Portal for validation + - Creates a git tag for the release + +2. **Manual Publishing**: After upload validation succeeds: + - Visit the [Sonatype Central Portal](https://central.sonatype.com) + - Review the uploaded artifacts + - Click "Publish" to release to Maven Central + - Artifacts sync to Maven Central within 10-30 minutes + +#### Local Publishing (Development) +For testing the publishing process locally: + +```bash +# Ensure signing keys are configured (see CONTRIBUTING.md) +export ORG_GRADLE_PROJECT_signingKey="\$(cat private-key.asc)" +export ORG_GRADLE_PROJECT_signingPassword="your-key-password" +export ORG_GRADLE_PROJECT_sonatypeUsername="your-sonatype-username" +export ORG_GRADLE_PROJECT_sonatypePassword="your-sonatype-password" + +# Upload to Sonatype Central +./gradlew uploadToSonatypeCentral +``` + +For detailed development and contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md). + [1]: https://tools.ietf.org/html/rfc6749#section-1.3.1 [2]: https://tools.ietf.org/html/rfc6749 diff --git a/build.gradle b/build.gradle index 0b81f0ed..ae502900 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,6 @@ plugins { id 'signing' id 'com.adarshr.test-logger' version '2.1.1' - id 'io.github.gradle-nexus.publish-plugin' version '1.0.0' id 'org.unbroken-dome.test-sets' version '4.0.0' } @@ -220,22 +219,7 @@ publishing { } -/** - * Requires the following environment variables to be set: - * - ORG_GRADLE_PROJECT_sonatypeUsername - * - ORG_GRADLE_PROJECT_sonatypePassword - * - * @see https://github.com/gradle-nexus/publish-plugin - */ -nexusPublishing { - repositories { - sonatype { - nexusUrl.set uri('https://s01.oss.sonatype.org/service/local/') - snapshotRepositoryUrl.set uri('https://s01.oss.sonatype.org/content/repositories/snapshots/') - stagingProfileId = '117eb260e0ad59' - } - } -} + /** * Requires the following environment variables to be set: @@ -248,11 +232,138 @@ signing { def signingKey = findProperty("signingKey") def signingPassword = findProperty("signingPassword") - // Only require signing if both key and password are available + // Always set up signing but only require it if keys are available required { signingKey != null && signingPassword != null } if (signingKey != null && signingPassword != null) { useInMemoryPgpKeys signingKey, signingPassword - sign publishing.publications.main + } + + sign publishing.publications.main +} + +/** + * Custom task to upload artifacts to Sonatype Central + * Requires the following environment variables to be set: + * - ORG_GRADLE_PROJECT_sonatypeUsername (Central Portal username) + * - ORG_GRADLE_PROJECT_sonatypePassword (Central Portal password) + */ +task uploadToSonatypeCentral { + group = 'publishing' + description = 'Uploads signed artifacts to Sonatype Central Portal' + + dependsOn 'signMainPublication' + + doLast { + def username = findProperty("sonatypeUsername") + def password = findProperty("sonatypePassword") + + if (!username || !password) { + throw new GradleException("Missing Sonatype Central credentials. Set ORG_GRADLE_PROJECT_sonatypeUsername and ORG_GRADLE_PROJECT_sonatypePassword") + } + + // Get the published artifacts from local repository + def localRepo = "${System.getProperty('user.home')}/.m2/repository" + def groupPath = libGroup.replace('.', '/') + def artifactPath = "${localRepo}/${groupPath}/${libName}/${libVersion}" + + println "Preparing upload bundle for ${libGroup}:${libName}:${libVersion}" + + // Create a temporary directory for the bundle with proper Maven structure + def bundleDir = file("${buildDir}/sonatype-bundle") + def mavenDir = file("${bundleDir}/${groupPath}/${libName}/${libVersion}") + mavenDir.mkdirs() + + // Copy all artifacts to proper Maven directory structure + copy { + from artifactPath + into mavenDir + include '*.jar', '*.pom', '*.module', '*.asc', '*.md5', '*.sha1' + } + + // Generate missing checksums and signatures if needed + def artifactFiles = fileTree(mavenDir).matching { + include '*.jar', '*.pom', '*.module' + exclude '*.asc', '*.md5', '*.sha1' + } + + artifactFiles.each { file -> + def fileName = file.name + + // Generate MD5 checksum + def md5File = new File(mavenDir, "${fileName}.md5") + if (!md5File.exists()) { + def md5Hash = java.security.MessageDigest.getInstance("MD5") + .digest(file.bytes) + .encodeHex() + .toString() + md5File.text = md5Hash + println "Generated MD5 for ${fileName}: ${md5Hash}" + } + + // Generate SHA1 checksum + def sha1File = new File(mavenDir, "${fileName}.sha1") + if (!sha1File.exists()) { + def sha1Hash = java.security.MessageDigest.getInstance("SHA-1") + .digest(file.bytes) + .encodeHex() + .toString() + sha1File.text = sha1Hash + println "Generated SHA1 for ${fileName}: ${sha1Hash}" + } + + // Check for signature + def sigFile = new File(mavenDir, "${fileName}.asc") + if (!sigFile.exists()) { + println "Warning: Missing signature for ${fileName}" + } + } + + // List all files in the bundle + println "Bundle contents:" + fileTree(bundleDir).each { file -> + def relativePath = bundleDir.toPath().relativize(file.toPath()).toString() + println " ${relativePath}" + } + + // Create the upload bundle (zip file) + def bundleFile = file("${buildDir}/sonatype-bundle.zip") + ant.zip(destfile: bundleFile) { + fileset(dir: bundleDir) + } + + println "Created bundle: ${bundleFile.absolutePath} (${bundleFile.length()} bytes)" + + // Upload to Sonatype Central using curl + def curlCommand = [ + 'curl', '-X', 'POST', + '--header', 'accept: application/json', + '--form', "bundle=@${bundleFile.absolutePath}", + '--user', "${username}:${password}", + '--silent', '--show-error', + 'https://central.sonatype.com/api/v1/publisher/upload' + ] + + println "Uploading to Sonatype Central..." + def process = curlCommand.execute() + + def outputStream = new ByteArrayOutputStream() + def errorStream = new ByteArrayOutputStream() + + process.consumeProcessOutput(outputStream, errorStream) + process.waitFor() + + def output = outputStream.toString() + def error = errorStream.toString() + + if (process.exitValue() == 0) { + println "Upload successful!" + println "Response: ${output}" + } else { + println "Upload failed!" + println "Error: ${error}" + println "Output: ${output}" + throw new GradleException("Failed to upload to Sonatype Central") + } } }