Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions .github/scripts/publish-maven-repo.sh
Original file line number Diff line number Diff line change
@@ -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
<repositories>
<repository>
<id>tob1as864</id>
<url>https://raw.githubusercontent.com/Tob1as864/maven-repo/main</url>
</repository>
</repositories>
```

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
89 changes: 89 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ target/
*.class
.idea/
*.iml
.maven-repo/
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,103 @@ document puts the ReqIF elements into the default namespace
(`<REQ-IF xmlns="...">`) or into a prefixed one (`<rif:REQ-IF xmlns:rif="...">`).
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
<repositories>
<repository>
<id>tob1as864</id>
<url>https://raw.githubusercontent.com/Tob1as864/maven-repo/main</url>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>de.uni_stuttgart.ils</groupId>
<artifactId>reqif4j</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
```

The repository `<id>` 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
`<server>` 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
<snapshots><enabled>true</enabled></snapshots>
```

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+):

Expand Down
Loading
Loading