diff --git a/.github/scripts/publish-maven-repo.sh b/.github/scripts/publish-maven-repo.sh new file mode 100755 index 0000000..efbab65 --- /dev/null +++ b/.github/scripts/publish-maven-repo.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# +# Publishes the Maven artifacts of this project into a separate, public Git +# repository that holds a plain Maven repository layout. That repository is +# served read-only (and token-free) through raw.githubusercontent.com and can +# hold the artifacts of several libraries side by side, because Maven's layout +# already namespaces them by groupId and artifactId. +# +# Usage: +# .github/scripts/publish-maven-repo.sh [version] +# +# Without an argument the version from pom.xml is published as-is (typically a +# SNAPSHOT). With an argument the pom version is set to it for the build only; +# pom.xml is restored afterwards, so the working tree keeps its development +# version and nothing has to be committed back to the source branch. +# +# Environment: +# MAVEN_REPO_REMOTE git remote of the target repository +# (default: git@github.com:Tob1as864/maven-repo.git) +# MAVEN_REPO_BRANCH branch to publish to (default: main) +# CHECKOUT_DIR where to clone it (default: .maven-repo) +# PUSH set to "false" for a local dry run (default: true) + +set -euo pipefail + +REMOTE="${MAVEN_REPO_REMOTE:-git@github.com:Tob1as864/maven-repo.git}" +BRANCH="${MAVEN_REPO_BRANCH:-main}" +CHECKOUT_DIR="${CHECKOUT_DIR:-.maven-repo}" +PUSH="${PUSH:-true}" +RELEASE_VERSION="${1:-}" + +MVN="${MVN:-mvn}" +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +log() { printf '\n==> %s\n' "$*"; } + +# --- clone the target repository --------------------------------------------- +log "Cloning $REMOTE" +rm -rf "$CHECKOUT_DIR" +git clone --quiet --depth 1 "$REMOTE" "$CHECKOUT_DIR" + +REPO_DIR="$(cd "$CHECKOUT_DIR" && pwd)" + +# A freshly created repository has no commits, so HEAD is unborn and no branch +# ref exists yet; -B creates the branch in that case and switches to it in all +# others. +if [ "$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD)" != "$BRANCH" ]; then + git -C "$REPO_DIR" checkout -q -B "$BRANCH" +fi + +# --- determine and validate the version to publish --------------------------- +if [ -n "$RELEASE_VERSION" ]; then + log "Setting project version to $RELEASE_VERSION" + POM_BACKUP="$(mktemp)" + cp pom.xml "$POM_BACKUP" + # Restore the development version even when the build below fails. + trap 'cp "$POM_BACKUP" "$REPO_ROOT/pom.xml"; rm -f "$POM_BACKUP"' EXIT + "$MVN" -B --no-transfer-progress versions:set \ + -DnewVersion="$RELEASE_VERSION" -DgenerateBackupPoms=false +fi + +VERSION="$("$MVN" -B -q --no-transfer-progress help:evaluate \ + -Dexpression=project.version -DforceStdout)" +GROUP_ID="$("$MVN" -B -q --no-transfer-progress help:evaluate \ + -Dexpression=project.groupId -DforceStdout)" +ARTIFACT_ID="$("$MVN" -B -q --no-transfer-progress help:evaluate \ + -Dexpression=project.artifactId -DforceStdout)" +ARTIFACT_DIR="$REPO_DIR/$(printf '%s' "$GROUP_ID" | tr '.' '/')/$ARTIFACT_ID/$VERSION" + +case "$VERSION" in + *-SNAPSHOT) ;; + *) + # Released versions are immutable: never silently overwrite one. + if [ -d "$ARTIFACT_DIR" ]; then + echo "ERROR: $GROUP_ID:$ARTIFACT_ID:$VERSION already exists in $REMOTE." >&2 + echo " Bump the version or delete it there first." >&2 + exit 1 + fi + ;; +esac + +# --- build and deploy into the checkout -------------------------------------- +log "Deploying $GROUP_ID:$ARTIFACT_ID:$VERSION into $REPO_DIR" +"$MVN" -B --no-transfer-progress -Prelease clean deploy -Dmaven.repo.dir="$REPO_DIR" + +# --- landing page ------------------------------------------------------------ +# The repository is shared by several libraries, so only write a README when it +# has none yet; an existing one is maintained by hand and must not be clobbered. +if [ ! -e "$REPO_DIR/README.md" ]; then + cat > "$REPO_DIR/README.md" <<'README' +# Maven repository + +This repository holds released Java artifacts in Maven repository layout. Its +contents are **generated** by the release workflows of the individual library +repositories - do not commit here by hand. + +Consume it without any authentication: + +```xml + + + tob1as864 + https://raw.githubusercontent.com/Tob1as864/maven-repo/main + + +``` + +Then declare the library you need as an ordinary dependency. Browse the +directory tree above for the available groupIds, artifacts and versions. +README +fi + +# --- commit and push ---------------------------------------------------------- +git -C "$REPO_DIR" add -A +if git -C "$REPO_DIR" diff --cached --quiet; then + log "No changes to publish" + exit 0 +fi + +git -C "$REPO_DIR" commit -q -m "Publish $GROUP_ID:$ARTIFACT_ID $VERSION" +log "Committed $GROUP_ID:$ARTIFACT_ID $VERSION" + +if [ "$PUSH" != "true" ]; then + log "PUSH=$PUSH - skipping push (dry run)" + exit 0 +fi + +for delay in 2 4 8 16 0; do + if git -C "$REPO_DIR" push -u origin "$BRANCH"; then + log "Pushed to $REMOTE ($BRANCH)" + exit 0 + fi + [ "$delay" -eq 0 ] && break + echo "Push failed, retrying in ${delay}s ..." >&2 + sleep "$delay" +done + +echo "ERROR: could not push to $REMOTE" >&2 +exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e419376 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,89 @@ +name: Publish to Maven repo + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to publish, e.g. 1.2.0. Leave empty to publish the current pom version (SNAPSHOT).' + required: false + default: '' + +# Only the checkout of this repository is needed; writing to the Maven +# repository goes through the deploy key, not through GITHUB_TOKEN. +permissions: + contents: read + +# The job pushes to a shared repository, so never run two of them at once. +concurrency: + group: maven-repo-publish + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: maven + + - name: Determine version + id: version + run: | + if [ "${{ github.event_name }}" = "push" ]; then + # Tag v1.2.0 publishes version 1.2.0. + echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + else + echo "value=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT" + fi + + - name: Set up deploy key for the Maven repository + env: + DEPLOY_KEY: ${{ secrets.MAVEN_REPO_DEPLOY_KEY }} + run: | + if [ -z "$DEPLOY_KEY" ]; then + echo "::error::Secret MAVEN_REPO_DEPLOY_KEY is not set. See the README" \ + "section 'One-time setup of the publishing credentials'." + exit 1 + fi + # A wrong key format fails much later with an opaque SSH error, so + # reject the common mistakes here with an actionable message. + case "$(printf '%s' "$DEPLOY_KEY" | head -n1)" in + '-----BEGIN '*'PRIVATE KEY-----') + ;; + 'PuTTY-User-Key-File'*) + echo "::error::MAVEN_REPO_DEPLOY_KEY holds a PuTTY .ppk key, which OpenSSH" \ + "cannot read. In PuTTYgen use Conversions -> Export OpenSSH key and" \ + "store that file's full contents instead." + exit 1 + ;; + *) + echo "::error::MAVEN_REPO_DEPLOY_KEY is not an OpenSSH private key. It must" \ + "contain the complete key file, starting with a line" \ + "'-----BEGIN OPENSSH PRIVATE KEY-----' - not a single line copied out" \ + "of it." + exit 1 + ;; + esac + mkdir -p ~/.ssh + chmod 700 ~/.ssh + # printf keeps the trailing newline OpenSSH requires; a here-string would not. + printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> ~/.ssh/known_hosts 2>/dev/null + + - name: Configure git + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + + - name: Publish + run: .github/scripts/publish-maven-repo.sh "${{ steps.version.outputs.value }}" diff --git a/.gitignore b/.gitignore index 0ded4a7..a4351e8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ target/ *.class .idea/ *.iml +.maven-repo/ diff --git a/README.md b/README.md index 67874f2..0ea34b2 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,103 @@ document puts the ReqIF elements into the default namespace (``) or into a prefixed one (``). The same holds for the embedded XHTML (`xhtml:div`, `reqif-xhtml:div`, ...). +# Using reqif4j as a dependency + +Released artifacts are published into the separate, public repository +[Tob1as864/maven-repo](https://github.com/Tob1as864/maven-repo), which holds a +plain Maven repository layout and is served over `raw.githubusercontent.com`. +No GitHub token and no `settings.xml` entry is needed. + +Maven: + +```xml + + + tob1as864 + https://raw.githubusercontent.com/Tob1as864/maven-repo/main + + + + + + de.uni_stuttgart.ils + reqif4j + 1.1.0 + + +``` + +The repository `` is just a local name for the declaration — pick any name +that is unique inside your own pom; it is unrelated to the library's +`artifactId`. Its only technical purpose is linking a repository to matching +`` credentials or mirrors in `settings.xml`, neither of which this +repository needs. + +To use a development build, additionally allow snapshots for the repository. +Maven enables them by default, so this is only needed if you switched them off: + +```xml +true +``` + +Gradle: + +```kotlin +repositories { + maven { url = uri("https://raw.githubusercontent.com/Tob1as864/maven-repo/main") } +} + +dependencies { + implementation("de.uni_stuttgart.ils:reqif4j:1.1.0") +} +``` + +Sources and javadoc jars are published alongside every version, so IDEs can +show the API documentation. Note that raw.githubusercontent.com is CDN-cached +for a few minutes, so a freshly published version may not resolve immediately. + +## Publishing a new version + +`.github/workflows/release.yml` builds the artifacts and commits them into the +`maven-repo` repository. It runs when a `v*` tag is pushed (tag `v1.2.0` +publishes version `1.2.0`), or on demand via *Actions -> Publish to Maven repo +-> Run workflow*, where an empty version input publishes the current SNAPSHOT. + +Release versions are immutable: publishing a version that already exists there +fails instead of overwriting it. The pom version is only changed for the build, +so no version bump is committed to this repository. + +The same publish step can be run locally, without pushing: + +``` +PUSH=false .github/scripts/publish-maven-repo.sh 1.2.0 +``` + +### One-time setup of the publishing credentials + +The workflow authenticates against `maven-repo` with an SSH deploy key, which +grants write access to that one repository only: + +1. Create the key pair locally, without a passphrase: + `ssh-keygen -t ed25519 -C "reqif4j release workflow" -f maven-repo-key -N ""` +2. In **Tob1as864/maven-repo** -> *Settings -> Deploy keys -> Add deploy key*: + paste the contents of `maven-repo-key.pub` and tick **Allow write access**. +3. In **this** repository -> *Settings -> Secrets and variables -> Actions -> + New repository secret* (a repository secret, not an environment secret): + name `MAVEN_REPO_DEPLOY_KEY`, value the **complete** contents of the private + key file `maven-repo-key`, from `-----BEGIN OPENSSH PRIVATE KEY-----` through + `-----END OPENSSH PRIVATE KEY-----`. +4. Delete both local key files. + +The secret must hold an OpenSSH private key in its original multi-line form; +the workflow rejects anything else before it starts publishing. PuTTY's own +`.ppk` format does not work - if you generate the key with PuTTYgen, use +*Conversions -> Export OpenSSH key* and store that exported file's contents. +The key must not have a passphrase, because the workflow runs unattended. + +The same deploy key setup is repeated per library that publishes into +`maven-repo`; each library repository gets its own key. + # Build & Test The project builds with Maven (Java 17+): diff --git a/pom.xml b/pom.xml index 41f49f3..d65751f 100644 --- a/pom.xml +++ b/pom.xml @@ -11,13 +11,47 @@ reqif4j Java parser for ReqIF (Requirements Interchange Format) documents + https://github.com/Tob1as864/ReqIF-Parser-Java + + + + GNU General Public License, version 3 + https://www.gnu.org/licenses/gpl-3.0.txt + repo + + + + + scm:git:https://github.com/Tob1as864/ReqIF-Parser-Java.git + scm:git:git@github.com:Tob1as864/ReqIF-Parser-Java.git + https://github.com/Tob1as864/ReqIF-Parser-Java + 17 UTF-8 5.10.2 + + ${project.build.directory}/maven-repo + + + git-maven-repo + Git-hosted Maven repository (maven-repo branch) + file://${maven.repo.dir} + + + git-maven-repo + Git-hosted Maven repository (maven-repo branch) + file://${maven.repo.dir} + + + org.junit.jupiter @@ -34,6 +68,66 @@ maven-surefire-plugin 3.2.5 + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.1 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.1 + + + org.codehaus.mojo + versions-maven-plugin + 2.16.2 + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + none + true + + + + attach-javadocs + + jar + + + + + + + +