diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..28c3301 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,34 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Etc/UTC + labels: + - dependencies + - github-actions + commit-message: + prefix: "ci" + + # Watches the upstream Apache Jackrabbit coordinate declared (tracker-only) + # in pom.xml's . When Apache cuts a new release, + # Dependabot bumps the upstream.jackrabbit.version property; the + # bump-on-dependabot.yml workflow then re-vendors sources.jar, runs + # OpenRewrite, and updates the project version, committing back to the + # Dependabot branch. + - package-ecosystem: maven + directory: / + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Etc/UTC + labels: + - dependencies + - upstream-bump + commit-message: + prefix: "upstream" + open-pull-requests-limit: 1 diff --git a/.github/scripts/bump-upstream.sh b/.github/scripts/bump-upstream.sh new file mode 100755 index 0000000..4cf4caa --- /dev/null +++ b/.github/scripts/bump-upstream.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# Re-vendor upstream Apache Jackrabbit WebDAV sources and re-apply the +# Jakarta EE 10 transform. +# +# Invoked from .github/workflows/bump-on-dependabot.yml after Dependabot +# opens a PR bumping the upstream.jackrabbit.version property in pom.xml. +# Reads the new upstream version straight from the pom property, downloads +# the matching sources.jar from Maven Central, replaces src/main/{java, +# resources}, runs OpenRewrite, and updates the project version. Caller is +# responsible for committing and pushing. +# +# Idempotent: if the project version already matches the upstream property +# (i.e. nothing left to bump), exits 0 without modifying any files. + +set -euo pipefail + +UPSTREAM_VERSION="$(mvn -q -B -ntp help:evaluate -Dexpression=upstream.jackrabbit.version -DforceStdout)" +CURRENT_PROJECT_VERSION="$(mvn -q -B -ntp help:evaluate -Dexpression=project.version -DforceStdout)" +EXPECTED_PROJECT_VERSION="${UPSTREAM_VERSION}-jakarta-ee10" + +echo "Upstream version from pom.xml property: ${UPSTREAM_VERSION}" +echo "Current project.version: ${CURRENT_PROJECT_VERSION}" +echo "Target project.version: ${EXPECTED_PROJECT_VERSION}" + +if [[ "${CURRENT_PROJECT_VERSION}" == "${EXPECTED_PROJECT_VERSION}" ]]; then + echo "Project version already matches upstream property — nothing to do." + exit 0 +fi + +URL="https://repo1.maven.org/maven2/org/apache/jackrabbit/jackrabbit-webdav/${UPSTREAM_VERSION}/jackrabbit-webdav-${UPSTREAM_VERSION}-sources.jar" +echo "Downloading ${URL}" +curl -fsSL -o sources.jar.new "${URL}" +mv sources.jar.new sources.jar + +workspace="$PWD" +rm -rf src/main/java src/main/resources +mkdir -p src/main/java src/main/resources + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +unzip -q sources.jar -d "$tmp" +rm -rf "$tmp/META-INF" + +# .java sources → src/main/java; everything else (e.g. .properties) → +# src/main/resources. Upstream sources.jar contains both intermixed. +(cd "$tmp" && find . -type f -name '*.java' | while read -r f; do + dest="$workspace/src/main/java/${f#./}" + mkdir -p "$(dirname "$dest")" + cp "$f" "$dest" + done) +(cd "$tmp" && find . -type f ! -name '*.java' | while read -r f; do + dest="$workspace/src/main/resources/${f#./}" + mkdir -p "$(dirname "$dest")" + cp "$f" "$dest" + done) + +echo "Re-applying Jakarta EE 10 transform via OpenRewrite" +mvn -B -ntp rewrite:run + +echo "Setting project version to ${EXPECTED_PROJECT_VERSION}" +mvn -B -ntp versions:set -DnewVersion="${EXPECTED_PROJECT_VERSION}" -DgenerateBackupPoms=false + +echo "Bump complete: jackrabbit-webdav ${UPSTREAM_VERSION} → ${EXPECTED_PROJECT_VERSION}" diff --git a/.github/workflows/bump-on-dependabot.yml b/.github/workflows/bump-on-dependabot.yml new file mode 100644 index 0000000..78c0e5d --- /dev/null +++ b/.github/workflows/bump-on-dependabot.yml @@ -0,0 +1,73 @@ +name: Apply upstream bump on Dependabot PR + +# Replaces the prior cron-driven check-upstream.yml: Dependabot handles the +# polling (see .github/dependabot.yml's maven ecosystem entry, which watches +# the tracker-only coordinate in pom.xml). When +# Dependabot opens a PR bumping the upstream.jackrabbit.version property, +# this workflow does the irreducible follow-up work that Dependabot itself +# can't do: re-vendor sources.jar, re-apply the OpenRewrite Jakarta EE 10 +# transform, and update the project version — all committed back onto the +# Dependabot branch so ci.yml's smoke test gates merge. + +on: + pull_request_target: + types: [opened, synchronize] + +permissions: + contents: write + pull-requests: write + +jobs: + apply-bump: + # Three guards (per Joe's review): + # 1. Author is Dependabot — bot-opened PRs only + # 2. Label is upstream-bump — distinguishes the Maven ecosystem update + # from the github-actions one (which has no follow-up work) + # 3. PR head is in this repo — Dependabot branches are always in-repo; + # a head from a fork means a malicious actor is impersonating the + # label or actor check, so refuse to run + if: >- + github.actor == 'dependabot[bot]' + && contains(github.event.pull_request.labels.*.name, 'upstream-bump') + && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + + steps: + - name: Checkout PR head + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.ref }} + # Use the workflow token so the follow-up commit can be pushed back + # to the Dependabot branch. + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + cache: maven + + - name: Configure git identity + run: | + git config user.name 'dependabot[bot]' + git config user.email '49699333+dependabot[bot]@users.noreply.github.com' + + # The bump script lives at .github/scripts/bump-upstream.sh (checked in, + # never read from the PR's pom.xml). Dependabot's manifest-only PR diffs + # don't touch this path, so in practice we're always executing the + # main-branch script — but the three guards above (bot author, label, + # same-repo head) are what actually keep arbitrary code from running + # under the pull_request_target token. + - name: Run upstream bump script + run: ./.github/scripts/bump-upstream.sh + + - name: Commit and push if changed + run: | + if [[ -z "$(git status --porcelain)" ]]; then + echo "No changes produced by bump script; nothing to commit." + exit 0 + fi + git add -A + git commit -m "Re-vendor sources and re-apply Jakarta EE 10 transform" + git push diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ef7cdb6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: Build + smoke test + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + cache: maven + + - name: Build and run smoke test + run: mvn -B -ntp verify diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..5e35e06 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,41 @@ +name: Publish to GitHub Packages + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + ref: + description: 'Ref (branch, tag, or SHA) to publish from' + required: false + default: 'main' + +jobs: + publish: + name: Maven deploy + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.ref || github.ref }} + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + cache: maven + server-id: github + server-username: GITHUB_ACTOR + server-password: GITHUB_TOKEN + + - name: Deploy + run: mvn -B -ntp -DskipTests deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md new file mode 100644 index 0000000..97d01a8 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# jackrabbit-webdav-jakarta + +A thin fork of [Apache Jackrabbit WebDAV](https://jackrabbit.apache.org/) with the +`javax.servlet` → `jakarta.servlet` namespace transform applied so [eXist-db 7.0](https://github.com/eXist-db/exist) +(Jetty 12, Jakarta Servlet 6.0) can link against it. + +This artifact exists only until Apache publishes a Jakarta-Servlet-native release of +`jackrabbit-webdav` upstream, at which point this repo will be archived and the +eXist-db dependency redirected to the upstream coordinate. + +## Coordinates + +```xml + + org.exist-db.thirdparty.org.apache.jackrabbit + jackrabbit-webdav + 2.22.3-jakarta-ee10 + +``` + +Published to GitHub Packages: + +## Versioning + +`-jakarta-ee10` + +- `2.22.3-jakarta-ee10` → Apache Jackrabbit `2.22.3` + this repo's Jakarta EE 10 transform +- Snapshots use `-jakarta-ee10-SNAPSHOT` + +The version string makes the upstream provenance auditable at a glance — both the +upstream tag and the Jakarta profile are encoded in the version, with no hidden +metadata in classifiers or qualifiers. + +## How the transform works + +Upstream Apache Jackrabbit sources are vendored at `src/main/java/`, pre-transformed +to `jakarta.servlet.*`. The canonical pre-transform source archive is the +`sources.jar` at the repo root (extracted from Maven Central). The transform is +applied via the [OpenRewrite Maven plugin](https://docs.openrewrite.org/) using +the `org.openrewrite.java.migrate.jakarta.JakartaEE10` recipe — but the rewrite +runs **at upstream-bump time**, not at every build: + +```sh +# After extracting a new sources.jar over src/main/java/: +mvn rewrite:run +``` + +The transformed result is committed to the repo, so day-to-day builds are plain +`mvn compile` against already-jakarta source. There are no manual patches, no +`sed` scripts, and no hand-edited source files — the only diff against upstream +is whatever OpenRewrite produces. + +## How upstream tracking works + +Dependabot handles the polling. A tracker-only `` entry +in `pom.xml` declares `org.apache.jackrabbit:jackrabbit-webdav` at +`${upstream.jackrabbit.version}`, with no corresponding `` entry — +so the coordinate stays off the compile classpath but is visible to Dependabot's +Maven manifest scan ([`.github/dependabot.yml`](.github/dependabot.yml)). + +When Apache cuts a new `jackrabbit-webdav` release, Dependabot opens a PR +labelled `upstream-bump` that bumps the `upstream.jackrabbit.version` property. +That PR fires [`bump-on-dependabot.yml`](.github/workflows/bump-on-dependabot.yml), +which does the work Dependabot can't: + +1. Reads the new upstream version from the bumped property +2. Downloads the matching `*-sources.jar` from Maven Central, replacing the + `sources.jar` at the repo root +3. Re-extracts source into `src/main/java/` (`.java` files) and + `src/main/resources/` (everything else) +4. Runs `mvn rewrite:run` to re-apply the Jakarta EE 10 transform +5. Bumps the project `` to `-jakarta-ee10` +6. Commits and pushes back onto the Dependabot branch + +The smoke test ([`ci.yml`](.github/workflows/ci.yml)) then re-runs against the +follow-up commit and gates merge: if the new upstream version still cleanly +transforms and links against Jakarta Servlet 6.0, the bump is safe to merge. + +## How to cut a release + +1. Land all bump / fix PRs on `main` +2. Tag the release commit: + ```sh + git tag v2.22.3-jakarta-ee10 + git push origin v2.22.3-jakarta-ee10 + ``` +3. The publish workflow ([`publish.yml`](.github/workflows/publish.yml)) fires on + `v*` tag push and deploys to GitHub Packages +4. Confirm the artifact resolves at + `https://maven.pkg.github.com/eXist-db/jackrabbit-webdav-jakarta` + +`workflow_dispatch` is also wired up if you need to publish a SNAPSHOT manually. + +## Consuming from a local Maven build + +GitHub Packages requires authentication for read access. Add to `~/.m2/settings.xml`: + +```xml + + + github-jackrabbit-webdav-jakarta + YOUR_GITHUB_USERNAME + YOUR_PAT_WITH_read:packages + + +``` + +The same PAT works across every `github-*` server id the eXist-db org publishes +(`github`, `github-xqts-runner`, and this one) — one PAT, multiple `` +blocks. The repository declaration lives in `exist-parent/pom.xml` in +[eXist-db/exist](https://github.com/eXist-db/exist). + +## License / attribution + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0), +inherited from upstream. This repository is a derivative work of +[apache/jackrabbit](https://github.com/apache/jackrabbit); all credit for the +WebDAV implementation belongs to the Apache Jackrabbit project. The only +modifications applied here are the OpenRewrite namespace transforms required for +Jakarta EE 10 compatibility. diff --git a/pom.xml b/pom.xml index 9aa2641..8ede534 100644 --- a/pom.xml +++ b/pom.xml @@ -26,8 +26,31 @@ UTF-8 17 + + 2.22.3 + + + + + org.apache.jackrabbit + jackrabbit-webdav + ${upstream.jackrabbit.version} + + + + org.osgi @@ -61,8 +84,34 @@ jcl-over-slf4j 2.0.17 + + + org.junit.jupiter + junit-jupiter + 5.11.4 + test + + + org.mockito + mockito-core + 5.14.2 + test + + + + github + GitHub Packages - eXist-db/jackrabbit-webdav-jakarta + https://maven.pkg.github.com/eXist-db/jackrabbit-webdav-jakarta + + + github + GitHub Packages - eXist-db/jackrabbit-webdav-jakarta + https://maven.pkg.github.com/eXist-db/jackrabbit-webdav-jakarta + + + @@ -73,6 +122,11 @@ 17 + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + org.openrewrite.maven rewrite-maven-plugin diff --git a/src/test/java/org/exist/jackrabbit/webdav/JakartaTransformSmokeTest.java b/src/test/java/org/exist/jackrabbit/webdav/JakartaTransformSmokeTest.java new file mode 100644 index 0000000..e0526b6 --- /dev/null +++ b/src/test/java/org/exist/jackrabbit/webdav/JakartaTransformSmokeTest.java @@ -0,0 +1,65 @@ +package org.exist.jackrabbit.webdav; + +import jakarta.servlet.http.HttpServletRequest; +import org.apache.jackrabbit.webdav.WebdavRequestImpl; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Litmus test that the OpenRewrite Jakarta EE 10 transform produced a linkable + * artifact: a class that consumes {@code jakarta.servlet} types and contains no + * surviving {@code javax/servlet} references in its bytecode. + */ +class JakartaTransformSmokeTest { + + @Test + void webdavRequestImplConsumesJakartaServletApi() { + Constructor[] ctors = WebdavRequestImpl.class.getConstructors(); + boolean foundJakartaCtor = false; + for (Constructor ctor : ctors) { + for (Class param : ctor.getParameterTypes()) { + if (param.getName().startsWith("jakarta.servlet.")) { + foundJakartaCtor = true; + } + assertFalse(param.getName().startsWith("javax.servlet."), + "Constructor still references javax.servlet: " + ctor); + } + } + assertTrue(foundJakartaCtor, + "Expected at least one WebdavRequestImpl constructor to accept a jakarta.servlet.* type"); + } + + @Test + void webdavRequestImplConstructsAgainstJakartaServletStub() { + HttpServletRequest req = mock(HttpServletRequest.class); + when(req.getHeader("Host")).thenReturn("localhost"); + when(req.getScheme()).thenReturn("http"); + when(req.getContextPath()).thenReturn(""); + + WebdavRequestImpl webdavRequest = new WebdavRequestImpl(req, null); + assertNotNull(webdavRequest); + } + + @Test + void compiledBytecodeContainsNoJavaxServletReferences() throws IOException { + String resource = "org/apache/jackrabbit/webdav/WebdavRequestImpl.class"; + try (InputStream in = getClass().getClassLoader().getResourceAsStream(resource)) { + assertNotNull(in, "Could not locate " + resource + " on the test classpath"); + byte[] bytes = in.readAllBytes(); + String asLatin1 = new String(bytes, java.nio.charset.StandardCharsets.ISO_8859_1); + assertFalse(asLatin1.contains("javax/servlet"), + "Compiled bytecode still references javax/servlet — OpenRewrite transform incomplete"); + assertTrue(asLatin1.contains("jakarta/servlet"), + "Compiled bytecode missing expected jakarta/servlet references"); + } + } +}