diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 74470cc4..ecec3c76 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,10 +8,9 @@ jobs: fail-fast: false matrix: version: - - { jdk: 21, idea: 2025.1 } - - { jdk: 21, idea: 2025.2 } - - { jdk: 21, idea: 2025.3 } - { jdk: 21, idea: 2026.1 } + - { jdk: 25, idea: 2026.2 } + - { jdk: 25, idea: 2026.3 } - { jdk: 25, idea: LATEST-EAP-SNAPSHOT } name: 'IDEA ${{ matrix.version.idea }}' env: @@ -32,7 +31,7 @@ jobs: - name: 'Generate coverage report' run: ./gradlew --warning-mode=all jacocoTestReport - name: 'Upload coverage to Codecov' - if: github.repository == 'mapstruct/mapstruct-idea' && matrix.version.idea == '2025.1' + if: github.repository == 'mapstruct/mapstruct-idea' && matrix.version.idea == '2026.1' uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: fail_ci_if_error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..80ea040d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,131 @@ +name: Release +run-name: "Release ${{ inputs.version || 'from current snapshot version' }}" + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 1.9.2). Defaults to pluginVersion in gradle.properties without -SNAPSHOT' + required: false + next: + description: 'Next development version without -SNAPSHOT (e.g. 1.9.3). Defaults to the release version with the patch incremented' + required: false + +permissions: + contents: read + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + name: 'Release' + if: github.repository == 'mapstruct/mapstruct-idea' + runs-on: ubuntu-latest + environment: release + permissions: + contents: write + id-token: write + attestations: write + steps: + - name: 'Checkout' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: main + - name: 'Set up JDK' + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + distribution: 'zulu' + java-version: 21 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + with: + cache-read-only: true + - name: 'Determine versions' + env: + RELEASE_VERSION: ${{ inputs.version }} + NEXT_BASE_VERSION: ${{ inputs.next }} + run: | + semver='^[0-9]+\.[0-9]+\.[0-9]+$' + if [ -z "$RELEASE_VERSION" ]; then + current=$(sed -n 's/^pluginVersion = //p' gradle.properties) + RELEASE_VERSION="${current%-SNAPSHOT}" + echo "Deduced release version $RELEASE_VERSION from pluginVersion $current" + fi + [[ "$RELEASE_VERSION" =~ $semver ]] || { echo "Invalid release version: $RELEASE_VERSION"; exit 1; } + if [ -z "$NEXT_BASE_VERSION" ]; then + IFS=. read -r major minor patch <<< "$RELEASE_VERSION" + NEXT_BASE_VERSION="$major.$minor.$((patch + 1))" + echo "Deduced next development version $NEXT_BASE_VERSION-SNAPSHOT" + fi + [[ "$NEXT_BASE_VERSION" =~ $semver ]] || { echo "Invalid next version: $NEXT_BASE_VERSION"; exit 1; } + if [ -n "$(git ls-remote --tags origin "refs/tags/$RELEASE_VERSION")" ]; then + echo "Tag $RELEASE_VERSION already exists" + exit 1 + fi + echo "RELEASE_VERSION=$RELEASE_VERSION" >> "$GITHUB_ENV" + echo "NEXT_BASE_VERSION=$NEXT_BASE_VERSION" >> "$GITHUB_ENV" + echo "### Releasing $RELEASE_VERSION (next: $NEXT_BASE_VERSION-SNAPSHOT)" >> "$GITHUB_STEP_SUMMARY" + - name: 'Prepare release notes' + run: | + # Drop the sections of NEXT_RELEASE_CHANGELOG.md that have no entries + awk ' + /^### / { heading = $0; next } + NF { + if (heading != "") { if (printed) print ""; print heading; print ""; heading = ""; printed = 1 } + print + } + ' NEXT_RELEASE_CHANGELOG.md > "$RUNNER_TEMP/release-notes.md" + if ! grep -q '^\* ' "$RUNNER_TEMP/release-notes.md"; then + echo "NEXT_RELEASE_CHANGELOG.md has no entries, add the release notes before releasing" + exit 1 + fi + cat "$RUNNER_TEMP/release-notes.md" >> "$GITHUB_STEP_SUMMARY" + - name: 'Configure git' + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + - name: 'Set release version' + run: | + sed -i "s/^pluginVersion = .*/pluginVersion = $RELEASE_VERSION/" gradle.properties + git commit -am "Release $RELEASE_VERSION" + git tag -a "$RELEASE_VERSION" -m "$RELEASE_VERSION" + - name: 'Build' + # Tests run in CI, only build the plugin distribution here so no unverified dependencies run in this job + run: ./gradlew --warning-mode=all buildPlugin + - name: 'Sign plugin' + env: + CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} + run: | + if [ -z "$CERTIFICATE_CHAIN" ] || [ -z "$PRIVATE_KEY" ]; then + echo "CERTIFICATE_CHAIN and PRIVATE_KEY secrets are required to sign the plugin" + exit 1 + fi + ./gradlew --warning-mode=all signPlugin + ./gradlew --warning-mode=all verifyPluginSignature + echo "PLUGIN_ZIP=$(ls build/distributions/*-signed.zip)" >> "$GITHUB_ENV" + - name: 'Attest build provenance' + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: ${{ env.PLUGIN_ZIP }} + - name: 'Publish to JetBrains Marketplace' + env: + JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }} + CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} + run: ./gradlew --warning-mode=all publishPlugin + - name: 'Set next development version' + run: | + sed -i "s/^pluginVersion = .*/pluginVersion = $NEXT_BASE_VERSION-SNAPSHOT/" gradle.properties + printf '### Features\n\n### Enhancements\n\n### Bugs\n\n### Documentation\n\n### Build\n' > NEXT_RELEASE_CHANGELOG.md + git commit -am "Prepare next development version" + - name: 'Push changes' + run: git push --atomic origin main "refs/tags/$RELEASE_VERSION" + - name: 'Create GitHub release' + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "$RELEASE_VERSION" "$PLUGIN_ZIP" --title "$RELEASE_VERSION" --verify-tag --notes-file "$RUNNER_TEMP/release-notes.md" diff --git a/NEXT_RELEASE_CHANGELOG.md b/NEXT_RELEASE_CHANGELOG.md new file mode 100644 index 00000000..350eb283 --- /dev/null +++ b/NEXT_RELEASE_CHANGELOG.md @@ -0,0 +1,26 @@ +### Features + +### Enhancements + +* Support generics in `@Mapping` source and target references (#83) +* Do not report unknown source properties when a `Map` is used as the source parameter (#243) + +### Bugs + +* Completion in `java(...)` expressions does not work for record targets (#245) +* Kotlin references fail with `NoClassDefFoundError` in IntelliJ IDEA 2026.3 (#281) +* `componentModel` completion shows unrelated suggestions (#275) + +### Documentation + +### Build + +* Release the plugin through a GitHub Actions workflow +* Sign the plugin and attest the build provenance of the released plugin +* Verify the dependencies of the plugin distribution and the Gradle plugins +* Test against IntelliJ IDEA 2026.1, 2026.2 and 2026.3 +* Update Gradle to 9.8.2 + +### Minimum Supported IntelliJ + +* 2026.1 diff --git a/build.gradle b/build.gradle index 2bf82c79..0c458913 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,6 @@ java { } repositories { - mavenLocal() mavenCentral() intellijPlatform { defaultRepositories() @@ -40,10 +39,18 @@ intellijPlatform { projectName = 'MapStruct-Intellij-Plugin' pluginConfiguration { ideaVersion { - sinceBuild = "251" + sinceBuild = "261" untilBuild = provider { null } as Provider } } + signing { + certificateChain = providers.environmentVariable('CERTIFICATE_CHAIN') + privateKey = providers.environmentVariable('PRIVATE_KEY') + password = providers.environmentVariable('PRIVATE_KEY_PASSWORD') + } + publishing { + token = providers.environmentVariable('JETBRAINS_MARKETPLACE_TOKEN') + } } // Simple function to load change-notes.html and description.html into valid text for plugin.xml @@ -105,6 +112,8 @@ dependencies { bundledPlugin( 'com.intellij.java' ) bundledPlugin( 'org.jetbrains.kotlin' ) + zipSigner( '0.1.45' ) + testFramework( TestFrameworkType.Platform.INSTANCE ) testFramework( TestFrameworkType.Plugin.Java.INSTANCE ) } @@ -152,3 +161,19 @@ test { exceptionFormat = 'full' } } + +// Dependency verification (gradle/verification-metadata.xml) only covers what ends up in the plugin distribution +// and what runs while signing and publishing the plugin. +// The Gradle plugins are always verified. Everything else (tests, checkstyle, jacoco) only runs in CI, +// and depends on the IDEA version that is being tested against. +def verifiedConfigurations = [ + 'runtimeClasspath', + 'intellijPlatformRuntimeClasspath', + 'intellijPlatformSandboxRuntimeClasspath', + 'marketplaceZipSigner', +] +configurations.configureEach { + if (!verifiedConfigurations.contains(name)) { + resolutionStrategy.disableDependencyVerification() + } +} diff --git a/change-notes.html b/change-notes.html index 722f1a30..f0871aba 100644 --- a/change-notes.html +++ b/change-notes.html @@ -1,4 +1,13 @@ +

1.10.0

+

1.9.1