This is an automated email from the ASF dual-hosted git repository. jdaugherty pushed a commit to branch build-hygiene-from-core in repository https://gitbox.apache.org/repos/asf/grails-intellij-plugin.git
commit 3ce3e0f2e8b6b157162436107397b6cca6cd0685 Author: James Daugherty <[email protected]> AuthorDate: Sat Jul 18 22:05:27 2026 -0400 Port build-hygiene tooling from grails-core Add a build-logic included build with Kotlin convention plugins for build reproducibility, JaCoCo code coverage, and Sonatype OSS Index vulnerability scanning, applied to the root project and every module. - reproducible archives (preserveFileTimestamps=false, reproducibleFileOrder, fixed permissions) so the plugin ZIP and module jars are byte-stable - JaCoCo per-module + root jacoco-report-aggregation (testCodeCoverageReport), CC-safe under this repo's org.gradle.configuration-cache=true - ossIndexAudit with an OssIndexAuditThrottle build service and fail-fast when SONATYPE_GUIDE_TOKEN is absent - CI: coverage.yml (Codecov), vulnerability-scan.yml (OSS Index), codeql.yml - codecov.yml and .github/dependabot.yml (gradle, /build-logic, github-actions) - RELEASE.md documenting the release pipeline + reproducibility verification - etc/bin/test-reproducible-build.sh and etc/bin/Dockerfile for repro testing - .gitattributes pinning LF; enable GitHub issues in .asf.yaml --- .asf.yaml | 3 + .gitattributes | 8 ++ .github/dependabot.yml | 43 +++++++ .github/workflows/codeql.yml | 55 +++++++++ .github/workflows/coverage.yml | 60 ++++++++++ .github/workflows/vulnerability-scan.yml | 85 ++++++++++++++ .gitignore | 4 + RELEASE.md | 126 +++++++++++++++++++++ build-logic/build.gradle.kts | 29 +++++ build-logic/settings.gradle.kts | 26 +++++ .../src/main/kotlin/OssIndexAuditThrottle.kt | 35 ++++++ .../main/kotlin/grails-intellij.jacoco.gradle.kts | 46 ++++++++ .../kotlin/grails-intellij.reproducible.gradle.kts | 34 ++++++ .../grails-intellij.vulnerability-scan.gradle.kts | 66 +++++++++++ build.gradle.kts | 35 ++++++ codecov.yml | 37 ++++++ copyright/build.gradle.kts | 3 + coverage/build.gradle.kts | 3 + etc/bin/Dockerfile | 48 ++++++++ etc/bin/test-reproducible-build.sh | 84 ++++++++++++++ gradle-tooling/build.gradle.kts | 3 + gradle.properties | 3 + grails-compiler-patch/build.gradle.kts | 3 + grails-rt/build.gradle.kts | 3 + hibernate/build.gradle.kts | 3 + i18n/build.gradle.kts | 3 + jps-plugin/build.gradle.kts | 3 + langInjection/build.gradle.kts | 3 + maven/build.gradle.kts | 3 + settings.gradle.kts | 4 + testFramework/build.gradle.kts | 3 + 31 files changed, 864 insertions(+) diff --git a/.asf.yaml b/.asf.yaml index 60fbd2d..79c7763 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -32,6 +32,9 @@ github: prevent_self_review: false description: "Grails plugin for IntelliJ IDEA" homepage: https://grails.apache.org/ + # enable the GitHub Issues tracker for this repository + features: + issues: true rulesets: - name: "Default Branch Protection" type: branch diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..45d9ad4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Files that must keep LF line endings on every platform. The reproducible-build +# Docker image (etc/bin/Dockerfile) refuses shell scripts with CRLF shebangs, and +# Gradle wrapper/property parsing expects LF. Pin them so a checkout under +# core.autocrlf=true cannot corrupt the build. +*.sh text eol=lf +gradlew text eol=lf +Dockerfile text eol=lf +*.properties text eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..24cdef9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,43 @@ +# 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. + +version: 2 +updates: + # Main build (root Gradle project + all modules) + - package-ecosystem: gradle + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: main + labels: + - "type: dependency upgrade" + # build-logic included build (convention plugins + their tool dependencies) + - package-ecosystem: gradle + directory: "/build-logic" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: main + labels: + - "type: dependency upgrade" + # GitHub Actions used by the workflows + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + target-branch: main + labels: + - "type: dependency upgrade" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..84b35b1 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,55 @@ +# 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. + +name: "CodeQL" +on: + push: + branches: + - main + pull_request: + branches: + - main +# queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + # The plugin is Kotlin + Java; the CodeQL 'java' pack covers both JVM languages. + language: [ 'java' ] + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: "☕️ Setup JDK" + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: liberica # .sdkmanrc pins librca = Liberica + java-version: 25 + - name: "🔎 Initialize CodeQL" + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + with: + languages: ${{ matrix.language }} + build-mode: none + - name: "🔬 Perform CodeQL Analysis" + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..5b68590 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,60 @@ +# 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. + +name: "Coverage" +on: + push: + branches: + - main + pull_request: + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +jobs: + coverage: + name: "Coverage" + runs-on: ubuntu-latest + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: "📝 Read JDK version from .sdkmanrc" + id: sdkmanrc + run: echo "java=$(grep -E '^java=' .sdkmanrc | cut -d= -f2 | sed 's/-librca$//')" >> "$GITHUB_OUTPUT" + - name: "☕️ Setup JDK" + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: liberica # .sdkmanrc pins librca = Liberica + java-version: ${{ steps.sdkmanrc.outputs.java }} + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) + - name: "🌡️ Run tests with coverage" + run: ./gradlew testCodeCoverageReport --continue --stacktrace + - name: "📤 Upload coverage artifact" + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-report + path: build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml + if-no-files-found: warn + - name: "📊 Upload coverage to Codecov" + continue-on-error: true + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml + verbose: true diff --git a/.github/workflows/vulnerability-scan.yml b/.github/workflows/vulnerability-scan.yml new file mode 100644 index 0000000..5a47483 --- /dev/null +++ b/.github/workflows/vulnerability-scan.yml @@ -0,0 +1,85 @@ +# 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. + +name: "Vulnerability Scan" +on: + push: + branches: + - main + schedule: + # Run every Monday at 03:00 UTC + - cron: '0 3 * * 1' + workflow_dispatch: +# Do not scan concurrently; OSS Index has per-account rate limits +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false +jobs: + scan: + name: "OSS Index Scan" + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: "📝 Read JDK version from .sdkmanrc" + id: sdkmanrc + run: echo "java=$(grep -E '^java=' .sdkmanrc | cut -d= -f2 | sed 's/-librca$//')" >> "$GITHUB_OUTPUT" + - name: "☕️ Setup JDK" + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: liberica # .sdkmanrc pins librca = Liberica + java-version: ${{ steps.sdkmanrc.outputs.java }} + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) + - name: "🔍 Run OSS Index Vulnerability Scan" + env: + SONATYPE_GUIDE_USERNAME: ${{ secrets.SONATYPE_GUIDE_USERNAME }} + SONATYPE_GUIDE_TOKEN: ${{ secrets.SONATYPE_GUIDE_TOKEN }} + continue-on-error: true + id: scan + run: ./gradlew ossIndexAudit --continue --info 2>&1 | tee /tmp/ossindex-scan.log; exit ${PIPESTATUS[0]} + - name: "📋 Publish Vulnerability Summary" + if: always() + run: | + echo "## 🔍 OSS Index Vulnerability Scan" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.scan.outcome }}" = "success" ]; then + echo "✅ No vulnerabilities found." >> $GITHUB_STEP_SUMMARY + else + echo "❌ Vulnerabilities detected." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + awk ' + BEGIN { in_section=0; in_vuln=0 } + { gsub(/\033\[[0-9;]*m/, "") } + /^##\[ossIndexAudit:begin\]/ { in_section=1; next } + /^##\[ossIndexAudit:end\]/ { in_section=0; in_vuln=0; next } + !in_section { next } + /^\[[0-9]+\/[0-9]+\] - pkg:maven\// { + sub(/^\[[0-9]+\/[0-9]+\] - /, "") + if (!seen_coord[$0]++) { print ""; print } + next + } + /^ Vulnerability Title:/ { in_vuln=1; block=$0 "\n"; cve_id=""; next } + in_vuln && /^ CVE:/ { match($0,/CVE-[0-9-]+/); if (RSTART) cve_id=substr($0,RSTART,RLENGTH); block=block $0 "\n"; next } + in_vuln && /^ Reference:/ { block=block $0 "\n"; if (cve_id && !seen_cve[cve_id]++) printf "%s",block; in_vuln=0; next } + in_vuln { block=block $0 "\n" } + ' /tmp/ossindex-scan.log >> $GITHUB_STEP_SUMMARY \ + || echo "(no scan output captured — check the full log)" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi diff --git a/.gitignore b/.gitignore index 91d7666..d3586d2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ gradle-bootstrap/gradlew.bat gradle-bootstrap/gradle /.claude *.hprof +# reproducible-build test output (etc/bin/test-reproducible-build.sh) +/first.txt +/second.txt +etc/bin/results diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..ea0aded --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,126 @@ +<!-- +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. +--> + +# Apache Grails IntelliJ Plugin — Release Process + +This document describes how a version of the Apache Grails IntelliJ plugin is released. + +**ASF framing.** The official release voted on by the PMC is the **signed source +distribution** published to `dist.apache.org`. The plugin ZIP on the JetBrains +Marketplace is a *convenience binary* built from that same tagged source. Both must be +reproducible from the tagged commit (see [Verifying a Reproducible Build](#verifying-a-reproducible-build)). + +The pipeline is encoded in [`.github/workflows/release.yml`](.github/workflows/release.yml) +and is triggered when a **GitHub Release is published** for a tag (`vX.Y.Z`). It runs in +four stages. + +## Prerequisites + +- The release tag (`vX.Y.Z`) points at the exact commit to be released. +- Marketplace signing / publishing secrets are configured: `CERTIFICATE_CHAIN`, + `PRIVATE_KEY`, `PRIVATE_KEY_PASSWORD`, `PUBLISH_TOKEN`. +- ASF signing + dist secrets are configured: `GRAILS_GPG_KEY`, `GPG_KEY_ID`, + `SVC_DIST_GRAILS_USERNAME`, `SVC_DIST_GRAILS_PASSWORD`. +- The release environment reviewers are set in [`.asf.yaml`](.asf.yaml) (GitHub does not + allow private groups as approvers, so approvers are listed individually). +- The JDK is pinned by [`.sdkmanrc`](.sdkmanrc); CI reads the version from it so local and + CI builds match. See [INSTALL](INSTALL) for local build setup. + +## 1. Build & stage the plugin ZIP (`publish` job) + +1. Establish `VERSION` from the tag (`${TAG#v}`). +2. `./gradlew check buildPlugin -Pversion=${VERSION}` — full build, tests, and Plugin ZIP. +3. `./gradlew signPlugin` — JetBrains Marketplace signature. +4. Rename to `apache-grails-intellij-plugin-${VERSION}-bin.zip`. +5. GPG-detached-sign (`.asc`) and checksum (`.sha512`). +6. Upload the ZIP, `.asc`, and `.sha512` to the GitHub Release. + +## 2. Create the source distribution (`source` job) + +1. Check out the tag, then remove files that must not ship in a source release + (`.git`, `.github`, `.asf.yaml`, `gradlew`/`gradlew.bat`, `gradle/wrapper`). +2. Zip to `apache-grails-intellij-plugin-${VERSION}-src.zip`. +3. GPG-detached-sign (`.asc`) and checksum (`.sha512`). +4. Upload all three to the GitHub Release. + +## 3. Stage to dist.apache.org & start the vote (`upload` job) + +1. Ensure `dist/dev/grails/INTELLIJ` exists in the ASF dist SVN. +2. Upload the source + binary distributions (with signatures and checksums) under + `dist/dev/grails/INTELLIJ/${VERSION}/`. +3. **MANUAL:** start a `[VOTE]` thread for the release on `[email protected]`. + +Reviewers should verify signatures, checksums, the RAT license audit, and a +**reproducible build** before voting (next section). + +## 4. Publish the release (`release` job — post-vote, environment-gated) + +After the vote succeeds (the `release` GitHub environment requires the reviewers in +`.asf.yaml`): + +1. **MANUAL:** confirm the vote result on `[email protected]`. +2. **MANUAL:** promote the staged artifacts from `dist/dev` to `dist/release` via + [`.github/scripts/releaseDistributions.sh`](.github/scripts/releaseDistributions.sh). +3. `./gradlew publishPlugin -Pversion=${VERSION}` — publish the convenience binary to the + JetBrains Marketplace. +4. **MANUAL:** update the [ASF release reporter](https://reporter.apache.org/) and send + the `[RESULT][VOTE]` + `[ANNOUNCE]` emails. + +## Verifying a Reproducible Build + +A vote is a good time to confirm the artifacts can be rebuilt bit-for-bit from the tag. +Reproducibility is enforced by the `grails-intellij.reproducible` convention plugin +(`build-logic/`), which pins archive timestamps, file order, and entry permissions across +every module and the plugin ZIP. + +### On the same machine + +From a clean checkout of the tagged commit: + +```bash +etc/bin/test-reproducible-build.sh +``` + +The script pins `SOURCE_DATE_EPOCH` to the last commit time, builds `buildPlugin` twice +from a clean tree (`--rerun-tasks --no-build-cache`), and compares the SHA-256 of the +plugin ZIP and every module jar. It prints `((none) ✅ build is reproducible)` on success +and lists any differing artifacts on failure. + +> ⚠️ The script runs `git clean -xdf` between builds, which deletes **all** untracked +> files. Run it in a throwaway checkout or after stashing your work. + +### In a container (build-environment parity) + +To rule out host-OS differences, reproduce inside a container that resembles the CI Linux +environment ([`etc/bin/Dockerfile`](etc/bin/Dockerfile), pinned to the same Liberica JDK +as `.sdkmanrc`): + +```bash +docker build -t grails-ij:testing -f etc/bin/Dockerfile . +docker run -it --rm -v "$(pwd):/home/builder/project" grails-ij:testing bash +# inside the container: +etc/bin/test-reproducible-build.sh +``` + +### Common reproducibility gotchas + +- **Archive timestamps / order** — handled by the reproducible convention plugin + (`preserveFileTimestamps=false`, `reproducibleFileOrder=true`, fixed permissions). +- **`SOURCE_DATE_EPOCH`** — most tools honor it for embedded dates; the script sets it + from the last git commit so successive builds agree. +- **JDK version** — a different JDK can change bytecode. Always build with the JDK pinned + in `.sdkmanrc`; the Docker image installs exactly that version. diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000..2ef3348 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * 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. + */ +plugins { + `kotlin-dsl` +} + +// The convention plugins here are consumed by the root build and every module. The +// vulnerability-scan plugin applies the Sonatype OSS Index plugin and references its +// extension type, so that plugin must be on the build-logic compile classpath. +dependencies { + // Dependabot (gradle ecosystem, /build-logic) keeps this version current. + implementation("org.sonatype.gradle.plugins:scan-gradle-plugin:3.1.6") +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..8334e44 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,26 @@ +/* + * 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. + */ +dependencyResolutionManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/OssIndexAuditThrottle.kt b/build-logic/src/main/kotlin/OssIndexAuditThrottle.kt new file mode 100644 index 0000000..4f8619f --- /dev/null +++ b/build-logic/src/main/kotlin/OssIndexAuditThrottle.kt @@ -0,0 +1,35 @@ +/* + * 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. + */ +package org.apache.grails.intellij.buildlogic + +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +/** + * Shared build service used solely as a concurrency gate for `ossIndexAudit` tasks. + * + * The Sonatype OSS Index Gradle plugin backs every audit task with a single shared + * on-disk response cache guarded by a file lock. When Gradle runs audit tasks in parallel, + * they contend for that lock and fail with `OverlappingFileLockException`, surfaced by the + * plugin as the opaque "Could not audit the project: null". + * + * Registered with `maxParallelUsages = 1`, this service forces Gradle to run at most one + * audit task at a time while leaving parallel execution enabled for the rest of the build. + */ +abstract class OssIndexAuditThrottle : BuildService<BuildServiceParameters.None> diff --git a/build-logic/src/main/kotlin/grails-intellij.jacoco.gradle.kts b/build-logic/src/main/kotlin/grails-intellij.jacoco.gradle.kts new file mode 100644 index 0000000..9011919 --- /dev/null +++ b/build-logic/src/main/kotlin/grails-intellij.jacoco.gradle.kts @@ -0,0 +1,46 @@ +/* + * 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 org.gradle.testing.jacoco.tasks.JacocoReport + +// Per-project JaCoCo conventions. Ported from grails-core's GrailsJacocoPlugin (per-module +// portion only). The cross-project aggregate is produced by the root build via Gradle's +// built-in `jacoco-report-aggregation` plugin, which is safe under the configuration cache +// (this repo has org.gradle.configuration-cache=true, unlike grails-core). + +plugins { + jacoco +} + +jacoco { + // keep in sync with `jacocoVersion` in gradle.properties + toolVersion = (project.findProperty("jacocoVersion") ?: "0.8.14").toString() +} + +tasks.withType<Test>().configureEach { + finalizedBy(tasks.withType<JacocoReport>()) +} + +tasks.withType<JacocoReport>().configureEach { + dependsOn(tasks.withType<Test>()) + reports { + xml.required.set(true) + html.required.set(true) + csv.required.set(false) + } +} diff --git a/build-logic/src/main/kotlin/grails-intellij.reproducible.gradle.kts b/build-logic/src/main/kotlin/grails-intellij.reproducible.gradle.kts new file mode 100644 index 0000000..2644224 --- /dev/null +++ b/build-logic/src/main/kotlin/grails-intellij.reproducible.gradle.kts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +// Reproducible-build conventions. Ported from grails-core's CompilePlugin.configureReproducible. +// Applied to the root project and every module so that the plugin ZIP +// (build/distributions/*.zip) and all composed/module jars are byte-for-byte stable. +// +// preserveFileTimestamps = false pins every archive entry to a constant timestamp, so +// SOURCE_DATE_EPOCH is not required for archive reproducibility; reproducibleFileOrder +// removes the only other common source of non-determinism (filesystem iteration order). +// Fixed permissions guarantee the same entry metadata regardless of the host umask. + +tasks.withType<AbstractArchiveTask>().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + filePermissions { unix("0644") } + dirPermissions { unix("0755") } +} diff --git a/build-logic/src/main/kotlin/grails-intellij.vulnerability-scan.gradle.kts b/build-logic/src/main/kotlin/grails-intellij.vulnerability-scan.gradle.kts new file mode 100644 index 0000000..d4d8205 --- /dev/null +++ b/build-logic/src/main/kotlin/grails-intellij.vulnerability-scan.gradle.kts @@ -0,0 +1,66 @@ +/* + * 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 org.apache.grails.intellij.buildlogic.OssIndexAuditThrottle +import org.sonatype.gradle.plugins.scan.ossindex.OssIndexPluginExtension + +// Sonatype OSS Index / Guide vulnerability scanning. Ported from grails-core's +// VulnerabilityScanPlugin. Credentials come from the environment: +// SONATYPE_GUIDE_USERNAME - Guide account email / user token username +// SONATYPE_GUIDE_TOKEN - Guide Personal Access Token (PAT) +// The `ossIndexAudit` task fails fast when SONATYPE_GUIDE_TOKEN is absent. + +plugins { + id("org.sonatype.gradle.plugins.scan") +} + +// The OSS Index plugin guards its shared on-disk response cache with a file lock, so audit +// tasks cannot run concurrently without hitting OverlappingFileLockException. Gate every +// audit task on a single-usage build service to serialize them while leaving parallel +// execution enabled for the rest of the build. +val throttle = gradle.sharedServices.registerIfAbsent("ossIndexAuditThrottle", OssIndexAuditThrottle::class.java) { + maxParallelUsages.set(1) +} + +configure<OssIndexPluginExtension> { + username = System.getenv("SONATYPE_GUIDE_USERNAME") ?: "" + password = System.getenv("SONATYPE_GUIDE_TOKEN") ?: "" + // Exemptions are limited to vulnerabilities with NO available upstream fix. Everything + // with a published fix must be bumped instead. Each entry must name the CVE and the + // removal condition, and be revisited whenever a fix ships. Empty until a real, + // unfixable CVE is encountered. + excludeCoordinates = emptySet() +} + +tasks.named("ossIndexAudit") { + group = "verification" + description = "Scans ${project.name} runtime dependencies for known vulnerabilities via Sonatype Guide" + usesService(throttle) + doFirst { + if (System.getenv("SONATYPE_GUIDE_TOKEN").isNullOrEmpty()) { + throw GradleException( + "SONATYPE_GUIDE_TOKEN environment variable is not set. " + + "A Sonatype Guide Personal Access Token is required to run the vulnerability scan." + ) + } + println("##[ossIndexAudit:begin] ${project.path}") + } + doLast { + println("##[ossIndexAudit:end] ${project.path}") + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 4c8be96..b656489 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import org.gradle.testing.jacoco.tasks.JacocoReport import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask @@ -23,6 +24,11 @@ plugins { id("org.jetbrains.kotlin.jvm") version "2.3.0" id("org.jetbrains.intellij.platform") version "2.18.1" id("org.nosphere.apache.rat") version "0.8.1" + // build-hygiene conventions (build-logic included build) + id("jacoco-report-aggregation") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") } group = "org.apache.grails.intellij" @@ -87,6 +93,30 @@ dependencies { // JPS/build runtimeOnly(project(":grails-compiler-patch")) runtimeOnly(project(":jps-plugin")) + + // Aggregate JaCoCo coverage from every module into the root testCodeCoverageReport. + jacocoAggregation(project(":copyright")) + jacocoAggregation(project(":coverage")) + jacocoAggregation(project(":gradle-tooling")) + jacocoAggregation(project(":grails-compiler-patch")) + jacocoAggregation(project(":grails-rt")) + jacocoAggregation(project(":hibernate")) + jacocoAggregation(project(":i18n")) + jacocoAggregation(project(":jps-plugin")) + jacocoAggregation(project(":langInjection")) + jacocoAggregation(project(":maven")) + jacocoAggregation(project(":testFramework")) +} + +// jacoco-report-aggregation auto-registers testCodeCoverageReport from the default `test` +// suite, producing build/reports/jacoco/testCodeCoverageReport/... ; ensure the XML variant +// is emitted for Codecov upload. +tasks.named<JacocoReport>("testCodeCoverageReport") { + reports { + xml.required.set(true) + html.required.set(true) + csv.required.set(false) + } } kotlin { @@ -210,6 +240,11 @@ tasks.rat { "out/**", // IntelliJ Platform Gradle Plugin local cache (generated Ivy descriptors, git-ignored) ".intellijPlatform/**", + // build-logic included build: generated output and local caches (sources are headered) + "build-logic/build/**", + "build-logic/.gradle/**", + // reproducible-build test output (git-ignored, regenerated by etc/bin/test-reproducible-build.sh) + "etc/bin/results/**", // generated code (JFlex lexers; headers are injected by the generator config) "gen/**", // test fixtures: content *is* the test input (mock Grails projects/JARs); diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..44ee4d7 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,37 @@ +# 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. + +codecov: + require_ci_to_pass: yes + +comment: + layout: "reach, diff, flags, files" + behavior: default + require_changes: false + require_base: no + require_head: yes + +coverage: + precision: 4 + round: nearest + status: + patch: + default: + target: auto + informational: true + project: + default: + target: auto + informational: true diff --git a/copyright/build.gradle.kts b/copyright/build.gradle.kts index 3700f32..8408920 100644 --- a/copyright/build.gradle.kts +++ b/copyright/build.gradle.kts @@ -18,6 +18,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.intellij.platform.module") } diff --git a/coverage/build.gradle.kts b/coverage/build.gradle.kts index 572a710..d4eda8d 100644 --- a/coverage/build.gradle.kts +++ b/coverage/build.gradle.kts @@ -18,6 +18,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.kotlin.jvm") version "2.3.0" id("org.jetbrains.intellij.platform.module") } diff --git a/etc/bin/Dockerfile b/etc/bin/Dockerfile new file mode 100644 index 0000000..8b8cd13 --- /dev/null +++ b/etc/bin/Dockerfile @@ -0,0 +1,48 @@ +# 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. + +# A container that resembles the GitHub Actions Linux build environment, used to test +# reproducible builds independently of the host OS. Mount the project directory into the +# container so builds run in the container but artifacts can be compared on the host. +# +# Build & run from the project root: +# docker build -t grails-ij:testing -f etc/bin/Dockerfile . +# docker run -it --rm -v "$(pwd):/home/builder/project" grails-ij:testing bash +# Then, inside the container: +# cd project && etc/bin/test-reproducible-build.sh +# +# Keep the JDK version synced with .sdkmanrc (java=<version>-librca). +FROM bellsoft/liberica-openjdk-debian:25.0.3 + +USER root +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl unzip coreutils git gpg sudo psmisc locales \ + && rm -rf /var/lib/apt/lists/* + +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ + && dpkg-reconfigure --frontend=noninteractive locales \ + && update-locale LANG=en_US.UTF-8 + +RUN useradd --create-home --home-dir /home/builder builder \ + && echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +USER builder +WORKDIR /home/builder/project + +ENV CI=true +ENV LANG=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 + +CMD ["/bin/bash"] diff --git a/etc/bin/test-reproducible-build.sh b/etc/bin/test-reproducible-build.sh new file mode 100755 index 0000000..628889a --- /dev/null +++ b/etc/bin/test-reproducible-build.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# +# 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. +# +# Verifies that the Grails IntelliJ plugin builds reproducibly: it builds the plugin +# ZIP (and every module jar) twice from a clean tree with SOURCE_DATE_EPOCH pinned, then +# compares the SHA-256 of each produced artifact. Any artifact whose hash differs between +# the two builds is printed and the script exits non-zero. +# +# WARNING: this runs `git clean -xdf`, which deletes ALL untracked files (build output, +# .idea, IDE caches, etc.). Run it only in a throwaway checkout or after stashing work. +# +# Usage: run from anywhere; the script locates the project root relative to itself. +# etc/bin/test-reproducible-build.sh +set -euo pipefail + +export SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" +PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/../.." &> /dev/null && pwd)" +cd "${PROJECT_ROOT}" + +# Portable SHA-256: prefer GNU coreutils, fall back to macOS `shasum`. +if command -v sha256sum &> /dev/null; then + sha() { sha256sum "$@"; } +else + sha() { shasum -a 256 "$@"; } +fi + +# Hash the release artifacts (plugin ZIP) and every module jar, sorted by path so the +# output is order-stable across runs. +hash_artifacts() { + { + find build/distributions -type f -name '*.zip' 2> /dev/null || true + find . -type f -path '*/build/libs/*.jar' 2> /dev/null || true + } | sort | while IFS= read -r f; do + [ -f "$f" ] && sha "$f" + done +} + +build_once() { + git clean -xdf + ./gradlew buildPlugin --rerun-tasks --no-build-cache --no-daemon +} + +echo "==> First build (SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH})" +build_once +FIRST="$(hash_artifacts)" + +echo "==> Second build" +build_once +SECOND="$(hash_artifacts)" + +printf '%s\n' "$FIRST" > first.txt +printf '%s\n' "$SECOND" > second.txt + +echo "" +echo "Differing artifacts:" +# comm on the second (hash) field: lines present in only one build indicate a hash mismatch. +DIFF="$(comm -3 <(printf '%s\n' "$FIRST" | sort) <(printf '%s\n' "$SECOND" | sort) \ + | awk '{print $2}' | sed 's|^[[:space:]]*||' | grep -v '^$' | sort -u || true)" + +if [ -z "$DIFF" ]; then + echo " (none) ✅ build is reproducible" + exit 0 +else + printf ' %s\n' "$DIFF" + echo "" + echo "❌ build is NOT reproducible — see first.txt / second.txt for the full hash lists" + exit 1 +fi diff --git a/gradle-tooling/build.gradle.kts b/gradle-tooling/build.gradle.kts index 05786b0..f51f633 100644 --- a/gradle-tooling/build.gradle.kts +++ b/gradle-tooling/build.gradle.kts @@ -17,6 +17,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.intellij.platform.module") } diff --git a/gradle.properties b/gradle.properties index 4b6cf1a..473db41 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,9 @@ version=262.0.0 kotlin.stdlib.default.dependency=false +# Build-hygiene tool versions +jacocoVersion=0.8.14 + # https://plugins.jetbrains.com/docs/intellij/testing-faq.html#how-to-test-a-jvm-language # set path to local intellij-community project test.idea.home.path=/Users/yann/idea-ultimate/ultimate/community diff --git a/grails-compiler-patch/build.gradle.kts b/grails-compiler-patch/build.gradle.kts index dc323c3..896fa65 100644 --- a/grails-compiler-patch/build.gradle.kts +++ b/grails-compiler-patch/build.gradle.kts @@ -17,6 +17,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.intellij.platform.module") } diff --git a/grails-rt/build.gradle.kts b/grails-rt/build.gradle.kts index 925a933..da5ee88 100644 --- a/grails-rt/build.gradle.kts +++ b/grails-rt/build.gradle.kts @@ -15,6 +15,9 @@ */ plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") } group = "org.apache.grails.intellij.rt" diff --git a/hibernate/build.gradle.kts b/hibernate/build.gradle.kts index 6faff4a..2e88bf9 100644 --- a/hibernate/build.gradle.kts +++ b/hibernate/build.gradle.kts @@ -17,6 +17,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.intellij.platform.module") } diff --git a/i18n/build.gradle.kts b/i18n/build.gradle.kts index d134dac..99961d9 100644 --- a/i18n/build.gradle.kts +++ b/i18n/build.gradle.kts @@ -17,6 +17,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.kotlin.jvm") version "2.3.0" id("org.jetbrains.intellij.platform.module") } diff --git a/jps-plugin/build.gradle.kts b/jps-plugin/build.gradle.kts index 0ae7944..cee000e 100644 --- a/jps-plugin/build.gradle.kts +++ b/jps-plugin/build.gradle.kts @@ -17,6 +17,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.intellij.platform.module") } diff --git a/langInjection/build.gradle.kts b/langInjection/build.gradle.kts index e74c2d1..c84fbac 100644 --- a/langInjection/build.gradle.kts +++ b/langInjection/build.gradle.kts @@ -17,6 +17,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.intellij.platform.module") } diff --git a/maven/build.gradle.kts b/maven/build.gradle.kts index 4b47727..2e1300a 100644 --- a/maven/build.gradle.kts +++ b/maven/build.gradle.kts @@ -17,6 +17,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.kotlin.jvm") version "2.3.0" id("org.jetbrains.intellij.platform.module") } diff --git a/settings.gradle.kts b/settings.gradle.kts index caefd5a..e62f760 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +pluginManagement { + includeBuild("build-logic") +} + rootProject.name = "Grails" include("copyright") diff --git a/testFramework/build.gradle.kts b/testFramework/build.gradle.kts index f965f64..0822f0b 100644 --- a/testFramework/build.gradle.kts +++ b/testFramework/build.gradle.kts @@ -20,6 +20,9 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") + id("grails-intellij.reproducible") + id("grails-intellij.jacoco") + id("grails-intellij.vulnerability-scan") id("org.jetbrains.kotlin.jvm") version "2.3.0" id("org.jetbrains.intellij.platform.module") }
