This commit is contained in:
commit
23964f7695
24 files changed
+1126
No files matched your search
@@ -0,0 +1,54 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore: [main]
|
||||
tags-ignore: ['**']
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout public source without a credential
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
GIT_TERMINAL_PROMPT: '0'
|
||||
run: |
|
||||
printf '%s' "$SOURCE_SHA" | grep -Eq '^[a-f0-9]{40}$'
|
||||
git init .
|
||||
git remote add origin https://git.garvis.dev/dmg/purpur-carry.git
|
||||
git config --local credential.helper ''
|
||||
git config --local http.extraHeader ''
|
||||
git config --local http.followRedirects false
|
||||
git fetch --force --tags origin '+refs/heads/*:refs/remotes/origin/*'
|
||||
git fetch origin "$SOURCE_SHA"
|
||||
git checkout --detach "$SOURCE_SHA"
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 25
|
||||
cache: gradle
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Validate conventional commits
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
npx -y -p @commitlint/cli -p @commitlint/config-conventional \
|
||||
commitlint --from "$BASE_SHA" --to "$HEAD_SHA" \
|
||||
--extends @commitlint/config-conventional
|
||||
- name: Build and test
|
||||
run: ./gradlew clean check jar
|
||||
- name: Upload development artifact
|
||||
uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3
|
||||
with:
|
||||
name: purpur-carry-${{ github.sha }}
|
||||
path: build/libs/purpur-carry-*.jar
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,114 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: purpur-carry-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout public source without a credential
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
GIT_TERMINAL_PROMPT: '0'
|
||||
run: |
|
||||
printf '%s' "$SOURCE_SHA" | grep -Eq '^[a-f0-9]{40}$'
|
||||
git init .
|
||||
git remote add origin https://git.garvis.dev/dmg/purpur-carry.git
|
||||
git config --local credential.helper ''
|
||||
git config --local http.extraHeader ''
|
||||
git config --local http.followRedirects false
|
||||
git fetch --force --tags origin '+refs/heads/*:refs/remotes/origin/*'
|
||||
git fetch origin "$SOURCE_SHA"
|
||||
git checkout -B main "$SOURCE_SHA"
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 25
|
||||
cache: gradle
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Build and test before release
|
||||
run: ./gradlew clean check jar
|
||||
- name: Capture previous tag
|
||||
id: previous
|
||||
run: |
|
||||
previous=$(git describe --tags --abbrev=0 2>/dev/null || true)
|
||||
printf 'tag=%s\n' "$previous" >> "$GITHUB_OUTPUT"
|
||||
- name: Compute and push semantic release tag
|
||||
env:
|
||||
RELEASE_CREDENTIAL: ${{ secrets.RELEASE_TOKEN }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
LC_ALL: C
|
||||
run: |
|
||||
set +x
|
||||
test "$REPOSITORY" = dmg/purpur-carry
|
||||
test -n "$RELEASE_CREDENTIAL" || { printf '%s\n' 'Missing RELEASE_TOKEN.' >&2; exit 1; }
|
||||
unset GIT_CREDENTIALS GH_TOKEN GITHUB_TOKEN GL_TOKEN GITLAB_TOKEN
|
||||
unset BB_TOKEN BITBUCKET_TOKEN BB_TOKEN_BASIC_AUTH BITBUCKET_TOKEN_BASIC_AUTH
|
||||
unset DEBUG NODE_DEBUG GIT_TRACE GIT_TRACE_CURL GIT_CURL_VERBOSE GIT_TRACE_PACKET
|
||||
unset GIT_TRACE_SETUP GIT_TRACE2 GIT_TRACE2_EVENT GIT_TRACE2_PERF
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
git config --local credential.helper ''
|
||||
git config --local --add credential.helper "$GITHUB_WORKSPACE/scripts/git-credential.sh"
|
||||
git config --local credential.useHttpPath true
|
||||
git config --local http.followRedirects false
|
||||
npx -y \
|
||||
-p semantic-release@24.2.9 \
|
||||
-p @semantic-release/commit-analyzer@13.0.1 \
|
||||
-p @semantic-release/release-notes-generator@14.1.0 \
|
||||
semantic-release --branches main \
|
||||
--repository-url https://git.garvis.dev/dmg/purpur-carry.git \
|
||||
--plugins @semantic-release/commit-analyzer,@semantic-release/release-notes-generator
|
||||
- name: Resolve newly created exact-source release
|
||||
id: release
|
||||
env:
|
||||
PREVIOUS_TAG: ${{ steps.previous.outputs.tag }}
|
||||
EXPECTED_COMMIT: ${{ github.sha }}
|
||||
run: |
|
||||
current=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
|
||||
if [ -n "$current" ] && [ "$current" != "$PREVIOUS_TAG" ]; then
|
||||
test "$(git rev-parse "${current}^{commit}")" = "$EXPECTED_COMMIT"
|
||||
printf 'created=true\nversion=%s\n' "${current#v}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
printf 'created=false\n' >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Verify versioned plugin and release tooling
|
||||
if: steps.release.outputs.created == 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
run: ./gradlew clean check jar -PreleaseVersion="$VERSION"
|
||||
- name: Upload release workflow artifact
|
||||
if: steps.release.outputs.created == 'true'
|
||||
uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3
|
||||
with:
|
||||
name: purpur-carry-${{ steps.release.outputs.version }}
|
||||
path: build/libs/purpur-carry-${{ steps.release.outputs.version }}.jar
|
||||
if-no-files-found: error
|
||||
- name: Publish verified JAR through a draft release
|
||||
if: steps.release.outputs.created == 'true'
|
||||
env:
|
||||
RELEASE_CREDENTIAL: ${{ secrets.RELEASE_TOKEN }}
|
||||
RELEASE_COMMIT: ${{ github.sha }}
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
PREVIOUS_TAG: ${{ steps.previous.outputs.tag }}
|
||||
run: |
|
||||
set +x
|
||||
if [ -n "$PREVIOUS_TAG" ]; then
|
||||
RELEASE_NOTES=$(git log --pretty='format:- %s (%h)' "${PREVIOUS_TAG}..HEAD")
|
||||
else
|
||||
RELEASE_NOTES=$(git log --pretty='format:- %s (%h)' HEAD)
|
||||
fi
|
||||
export RELEASE_NOTES
|
||||
node build/release-tools/publish-release.js
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.gradle/
|
||||
build/
|
||||
out/
|
||||
.idea/
|
||||
*.iml
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
node_modules/
|
||||
@@ -0,0 +1,9 @@
|
||||
# purpur-carry agent entrypoint
|
||||
|
||||
The canonical stories, engineering guidance, and **all process documents** are in the private [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md).
|
||||
|
||||
Before work, read the sibling `../somc-okf/index.md`, `../somc-okf/processes/index.md`, `../somc-okf/projects/purpur-carry/index.md`, `engineering.md` in that project section, and relevant `../somc-okf/user-stories/purpur-carry/` stories. Also follow the parent workspace `AGENTS.md` when present.
|
||||
|
||||
For standalone checkouts, start at the [project page](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-carry/index.md) and [shared process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md). Obtain wiki access before feature work; do not recreate a local knowledge bundle. Source builds do not require private wiki access.
|
||||
|
||||
Development follows the [development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md): approved stories, failing tests, passing implementation, verification, then source/wiki commit and push. GitOps changes are committed locally **without pushing**; only [Do release](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/do-release.md) authorizes a reviewed GitOps push.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Purpur Carry
|
||||
|
||||
Purpur Carry is a Purpur 26.2 plugin for native animal and Villager carrying on SoMC. The initial build/release enabler intentionally packages no gameplay; the approved behavior is specified in the [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/purpur-carry/index.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
- Purpur 26.2 build 2618
|
||||
- Java 25 or newer
|
||||
- Node.js 20 or newer for release-tool verification
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
./gradlew clean check jar
|
||||
```
|
||||
|
||||
The command compiles with fatal warnings, runs the Java distribution tests and TypeScript release-tool tests, then writes `build/libs/purpur-carry-<version>.jar`. Purpur is compile-only and is not bundled.
|
||||
|
||||
## Current behavior
|
||||
|
||||
The enabler loads as `PurpurCarry` but registers no command, permission, listener, configuration, persistence, or gameplay behavior. [US-001](https://git.garvis.dev/dmg/somc-okf/src/branch/main/user-stories/purpur-carry/us-001-carry-animals-and-villagers.md) owns `/carry`, `purpurcarry.use`, ordinary-Lead interaction, native passenger carrying, release, and cleanup.
|
||||
|
||||
## Releases
|
||||
|
||||
Gitea Actions verifies the exact public source revision. Pull requests validate conventional commits; main-branch conventional commits drive semantic releases. Publication uses the repository-scoped `RELEASE_TOKEN` only through an in-memory Git credential helper and authenticated request headers inside the release process, never command arguments. A release is published only after the exact versioned JAR has passed the complete verification suite and uploaded successfully through a draft transaction.
|
||||
|
||||
A source release is not installation or deployment approval. GitOps preparation and production release use the separately gated workspace runbooks.
|
||||
@@ -0,0 +1,57 @@
|
||||
plugins {
|
||||
java
|
||||
}
|
||||
|
||||
group = "games.dmg"
|
||||
version = providers.gradleProperty("releaseVersion")
|
||||
.orElse("0.1.0-SNAPSHOT")
|
||||
.get()
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://repo.purpurmc.org/snapshots/")
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.compilerArgs.addAll(listOf("-Xlint:all,-deprecation,-removal", "-Werror"))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||
|
||||
testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
testImplementation("org.yaml:snakeyaml:2.4")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
dependsOn(tasks.jar)
|
||||
systemProperty("distribution.jar", tasks.jar.get().archiveFile.get().asFile.absolutePath)
|
||||
systemProperty("distribution.version", project.version.toString())
|
||||
}
|
||||
|
||||
val installReleaseTools = tasks.register<Exec>("installReleaseTools") {
|
||||
commandLine("npm", "ci", "--ignore-scripts", "--no-audit", "--no-fund")
|
||||
}
|
||||
val releaseToolCheck = tasks.register<Exec>("releaseToolCheck") {
|
||||
dependsOn(installReleaseTools)
|
||||
commandLine("npm", "test")
|
||||
}
|
||||
tasks.check { dependsOn(releaseToolCheck) }
|
||||
|
||||
val pluginVersion = version
|
||||
|
||||
tasks.processResources {
|
||||
filesMatching("plugin.yml") {
|
||||
expand("version" to pluginVersion)
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
Generated
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "purpur-carry-release-tools",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "purpur-carry-release-tools",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "20.19.9",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
|
||||
"integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "purpur-carry-release-tools",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": { "node": ">=20" },
|
||||
"scripts": {
|
||||
"test": "rm -rf build/release-tools && tsc && node --test build/release-tools/*.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.19.9",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
# Git consumes this protocol over a pipe; never run interactively with a real credential.
|
||||
set +x
|
||||
[ "${1-}" = get ] || exit 0
|
||||
protocol= host= path= username=
|
||||
while IFS= read -r line && [ -n "$line" ]; do
|
||||
case "$line" in
|
||||
protocol=*) protocol=${line#protocol=} ;;
|
||||
host=*) host=${line#host=} ;;
|
||||
path=*) path=${line#path=} ;;
|
||||
username=*) username=${line#username=} ;;
|
||||
esac
|
||||
done
|
||||
[ "$protocol" = https ] || exit 1
|
||||
[ "$host" = git.garvis.dev ] || exit 1
|
||||
[ "$path" = dmg/purpur-carry.git ] || exit 1
|
||||
[ -z "$username" ] || [ "$username" = dmg ] || exit 1
|
||||
[ -n "${RELEASE_CREDENTIAL-}" ] || exit 1
|
||||
LC_ALL=C
|
||||
export LC_ALL
|
||||
case "$RELEASE_CREDENTIAL" in *[![:graph:]]*) exit 1 ;; esac
|
||||
printf '%s\n' 'username=dmg'
|
||||
printf 'password=%s\n\n' "$RELEASE_CREDENTIAL"
|
||||
@@ -0,0 +1,69 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { test } from "node:test";
|
||||
|
||||
const fixture = "synthetic-git-protocol-fixture";
|
||||
const helper = resolve("scripts/git-credential.sh");
|
||||
const request = "protocol=https\nhost=git.garvis.dev\npath=dmg/purpur-carry.git\n\n";
|
||||
const environment = {
|
||||
PATH: process.env.PATH,
|
||||
RELEASE_CREDENTIAL: fixture,
|
||||
LC_ALL: "C",
|
||||
GIT_ASKPASS: "echo",
|
||||
GIT_TERMINAL_PROMPT: "0",
|
||||
GIT_CONFIG_NOSYSTEM: "1",
|
||||
GIT_CONFIG_GLOBAL: "/dev/null",
|
||||
};
|
||||
|
||||
test("real Git obtains only the scoped in-memory credential", () => {
|
||||
const child = spawnSync("git", [
|
||||
"-c", "credential.helper=",
|
||||
"-c", `credential.helper=${helper}`,
|
||||
"-c", "credential.useHttpPath=true",
|
||||
"credential", "fill",
|
||||
], { encoding: "utf8", input: request, env: environment });
|
||||
assert.equal(child.status, 0);
|
||||
assert.ok(child.stdout.split("\n").includes("username=dmg"));
|
||||
assert.ok(child.stdout.split("\n").includes(`password=${fixture}`));
|
||||
assert.equal(child.stderr, "");
|
||||
});
|
||||
|
||||
for (const input of [
|
||||
request.replace("https", "http"),
|
||||
request.replace("git.garvis.dev", "evil.invalid"),
|
||||
request.replace("purpur-carry.git", "another-project.git"),
|
||||
request.replace("\n\n", "\nusername=other\n\n"),
|
||||
]) {
|
||||
test(`refuses mismatched credential scope: ${JSON.stringify(input)}`, () => {
|
||||
const child = spawnSync("/bin/sh", [helper, "get"], {
|
||||
encoding: "utf8", input, env: environment,
|
||||
});
|
||||
assert.notEqual(child.status, 0);
|
||||
assert.equal(child.stdout, "");
|
||||
assert.equal(child.stderr, "");
|
||||
});
|
||||
}
|
||||
|
||||
for (const action of ["store", "erase"]) {
|
||||
test(`ignores ${action} without persistence`, () => {
|
||||
const child = spawnSync("/bin/sh", [helper, action], {
|
||||
encoding: "utf8", input: request, env: environment,
|
||||
});
|
||||
assert.equal(child.status, 0);
|
||||
assert.equal(child.stdout, "");
|
||||
assert.equal(child.stderr, "");
|
||||
});
|
||||
}
|
||||
|
||||
for (const value of ["", "bad\npassword=injected", "trailing-space "]) {
|
||||
test(`rejects malformed credential input of length ${value.length}`, () => {
|
||||
const child = spawnSync("/bin/sh", [helper, "get"], {
|
||||
encoding: "utf8", input: request,
|
||||
env: { ...environment, RELEASE_CREDENTIAL: value },
|
||||
});
|
||||
assert.notEqual(child.status, 0);
|
||||
assert.equal(child.stdout, "");
|
||||
assert.equal(child.stderr, "");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { test } from "node:test";
|
||||
|
||||
for (const environment of [
|
||||
{ VERSION: "1.2.3", RELEASE_COMMIT: "a".repeat(40) },
|
||||
{
|
||||
VERSION: "../../outside",
|
||||
RELEASE_CREDENTIAL: "synthetic-cli-fixture",
|
||||
RELEASE_COMMIT: "a".repeat(40),
|
||||
},
|
||||
]) {
|
||||
test(`CLI fails closed without echoing environment: ${Object.keys(environment)}`, () => {
|
||||
const child = spawnSync(process.execPath, ["build/release-tools/publish-release.js"], {
|
||||
encoding: "utf8", env: environment,
|
||||
});
|
||||
assert.equal(child.status, 1);
|
||||
assert.equal(child.stdout, "");
|
||||
assert.equal(child.stderr,
|
||||
"Release publication failed; inspect the release state before retrying.\n");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { publishRelease } from "./release-client.js";
|
||||
|
||||
try {
|
||||
const version = process.env.VERSION ?? "";
|
||||
const credential = process.env.RELEASE_CREDENTIAL ?? "";
|
||||
const commit = process.env.RELEASE_COMMIT ?? "";
|
||||
if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version)
|
||||
|| !credential || !/^[a-f0-9]{40}$/.test(commit)) {
|
||||
throw new Error("Invalid input");
|
||||
}
|
||||
const jar = await readFile(`build/libs/purpur-carry-${version}.jar`);
|
||||
await publishRelease({
|
||||
credential,
|
||||
version,
|
||||
commit,
|
||||
jar,
|
||||
notes: process.env.RELEASE_NOTES ?? "See the source history for changes.",
|
||||
}, fetch);
|
||||
console.log(`Published purpur-carry v${version}.`);
|
||||
} catch {
|
||||
console.error("Release publication failed; inspect the release state before retrying.");
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { publishRelease, type ReleaseInput } from "./release-client.js";
|
||||
|
||||
const input: ReleaseInput = {
|
||||
credential: "synthetic-test-value-not-a-real-credential",
|
||||
version: "1.2.3",
|
||||
commit: "a".repeat(40),
|
||||
notes: "Verified changes",
|
||||
jar: new Uint8Array([0x50, 0x4b, 0x03, 0x04]),
|
||||
};
|
||||
|
||||
function success(init: RequestInit): Response {
|
||||
return Response.json({
|
||||
id: 42,
|
||||
draft: init.method !== "PATCH",
|
||||
tag_name: "v1.2.3",
|
||||
name: "purpur-carry-1.2.3.jar",
|
||||
size: input.jar.length,
|
||||
});
|
||||
}
|
||||
|
||||
test("publishes exact bytes through a fixed-repository draft without redirects", async () => {
|
||||
const calls: { url: string; init: RequestInit }[] = [];
|
||||
const id = await publishRelease(input, async (url, init) => {
|
||||
calls.push({ url, init });
|
||||
return success(init);
|
||||
});
|
||||
|
||||
assert.equal(id, 42);
|
||||
assert.equal(calls.length, 3);
|
||||
assert.equal(calls[0]!.url, "https://git.garvis.dev/api/v1/repos/dmg/purpur-carry/releases");
|
||||
assert.deepEqual(JSON.parse(String(calls[0]!.init.body)), {
|
||||
tag_name: "v1.2.3",
|
||||
target_commitish: input.commit,
|
||||
name: "v1.2.3",
|
||||
body: input.notes,
|
||||
draft: true,
|
||||
prerelease: false,
|
||||
});
|
||||
assert.equal(calls[1]!.url,
|
||||
"https://git.garvis.dev/api/v1/repos/dmg/purpur-carry/releases/42/assets?name=purpur-carry-1.2.3.jar");
|
||||
assert.deepEqual(new Uint8Array(await new Response(calls[1]!.init.body).arrayBuffer()), input.jar);
|
||||
assert.equal(calls[2]!.url, "https://git.garvis.dev/api/v1/repos/dmg/purpur-carry/releases/42");
|
||||
assert.deepEqual(JSON.parse(String(calls[2]!.init.body)), { draft: false });
|
||||
for (const call of calls) {
|
||||
assert.equal(call.init.redirect, "error");
|
||||
assert.equal(new Headers(call.init.headers).get("authorization"), `token ${input.credential}`);
|
||||
assert.ok(!call.url.includes(input.credential));
|
||||
}
|
||||
});
|
||||
|
||||
for (const failureStep of [1, 2, 3]) {
|
||||
for (const status of [302, 401, 409, 500]) {
|
||||
test(`stops at step ${failureStep} on HTTP ${status} without leaking API bodies`, async () => {
|
||||
let calls = 0;
|
||||
await assert.rejects(publishRelease(input, async (_url, init) => {
|
||||
calls++;
|
||||
if (calls === failureStep) return new Response(input.credential, { status });
|
||||
return success(init);
|
||||
}), (error: unknown) => {
|
||||
assert.ok(error instanceof Error);
|
||||
assert.ok(error.message.includes(String(status)));
|
||||
assert.ok(!error.message.includes(input.credential));
|
||||
return true;
|
||||
});
|
||||
assert.equal(calls, failureStep);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test("sanitizes transport errors", async () => {
|
||||
await assert.rejects(publishRelease(input, async () => {
|
||||
throw new Error(`diagnostic containing ${input.credential}`);
|
||||
}), { message: "Release API request failed." });
|
||||
});
|
||||
|
||||
for (const invalid of [
|
||||
{ credential: "" },
|
||||
{ credential: "header\r\ninjection" },
|
||||
{ version: "../../latest" },
|
||||
{ version: "1.0.0-SNAPSHOT" },
|
||||
{ commit: "main" },
|
||||
{ jar: new Uint8Array() },
|
||||
]) {
|
||||
test(`rejects invalid ${Object.keys(invalid)[0]} before network access`, async () => {
|
||||
let calls = 0;
|
||||
await assert.rejects(publishRelease({ ...input, ...invalid }, async () => {
|
||||
calls++;
|
||||
return Response.json({ id: 42 });
|
||||
}), { message: "Invalid release input." });
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
export interface ReleaseInput {
|
||||
credential: string;
|
||||
version: string;
|
||||
commit: string;
|
||||
notes: string;
|
||||
jar: Uint8Array;
|
||||
}
|
||||
|
||||
export type Request = (url: string, init: RequestInit) => Promise<Response>;
|
||||
|
||||
const releases = "https://git.garvis.dev/api/v1/repos/dmg/purpur-carry/releases";
|
||||
|
||||
export async function publishRelease(input: ReleaseInput, request: Request): Promise<number> {
|
||||
if (!/^[\x21-\x7e]+$/.test(input.credential)
|
||||
|| !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(input.version)
|
||||
|| !/^[a-f0-9]{40}$/.test(input.commit)
|
||||
|| input.jar.length === 0) {
|
||||
throw new Error("Invalid release input.");
|
||||
}
|
||||
const send: Request = async (url, init) => {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await request(url, { ...init, signal: AbortSignal.timeout(60_000) });
|
||||
} catch {
|
||||
throw new Error("Release API request failed.");
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Release API request failed (HTTP ${response.status}).`);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
const response = await send(releases, {
|
||||
method: "POST",
|
||||
redirect: "error",
|
||||
headers: {
|
||||
authorization: `token ${input.credential}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tag_name: `v${input.version}`,
|
||||
target_commitish: input.commit,
|
||||
name: `v${input.version}`,
|
||||
body: input.notes,
|
||||
draft: true,
|
||||
prerelease: false,
|
||||
}),
|
||||
});
|
||||
const release = await readObject(response, "Invalid draft release response.");
|
||||
if (typeof release.id !== "number" || !Number.isSafeInteger(release.id) || release.id <= 0
|
||||
|| release.draft !== true || release.tag_name !== `v${input.version}`) {
|
||||
throw new Error("Invalid draft release response.");
|
||||
}
|
||||
const assetName = `purpur-carry-${input.version}.jar`;
|
||||
const assetResponse = await send(`${releases}/${release.id}/assets?name=${assetName}`, {
|
||||
method: "POST",
|
||||
redirect: "error",
|
||||
headers: {
|
||||
authorization: `token ${input.credential}`,
|
||||
"content-type": "application/octet-stream",
|
||||
},
|
||||
body: new Uint8Array(input.jar).buffer,
|
||||
});
|
||||
const asset = await readObject(assetResponse, "Invalid release asset response.");
|
||||
if (asset.name !== assetName || asset.size !== input.jar.length) {
|
||||
throw new Error("Invalid release asset response.");
|
||||
}
|
||||
const publishedResponse = await send(`${releases}/${release.id}`, {
|
||||
method: "PATCH",
|
||||
redirect: "error",
|
||||
headers: {
|
||||
authorization: `token ${input.credential}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ draft: false }),
|
||||
});
|
||||
const published = await readObject(
|
||||
publishedResponse, "Invalid published release response.");
|
||||
if (published.id !== release.id || published.draft !== false
|
||||
|| published.tag_name !== `v${input.version}`) {
|
||||
throw new Error("Invalid published release response.");
|
||||
}
|
||||
return release.id;
|
||||
}
|
||||
|
||||
async function readObject(
|
||||
response: Response,
|
||||
message: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
let value: unknown;
|
||||
try {
|
||||
value = await response.json();
|
||||
} catch {
|
||||
throw new Error(message);
|
||||
}
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(message);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
}
|
||||
|
||||
rootProject.name = "purpur-carry"
|
||||
@@ -0,0 +1,10 @@
|
||||
package games.dmg.purpurcarry;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class PurpurCarryPlugin extends JavaPlugin {
|
||||
@Override
|
||||
public void onEnable() {
|
||||
getLogger().info("Purpur Carry enabled.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
name: PurpurCarry
|
||||
version: ${version}
|
||||
main: games.dmg.purpurcarry.PurpurCarryPlugin
|
||||
api-version: "1.20"
|
||||
description: Native animal and Villager carrying for SoMC.
|
||||
author: dmg.games
|
||||
@@ -0,0 +1,47 @@
|
||||
package games.dmg.purpurcarry;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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 java.io.DataInputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarFile;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
final class PurpurCarryDistributionTest {
|
||||
@Test
|
||||
void packagesTheApprovedLoadableJava25PluginIdentity() throws Exception {
|
||||
Path artifact = Path.of(System.getProperty("distribution.jar"));
|
||||
String version = System.getProperty("distribution.version");
|
||||
assertEquals("purpur-carry-" + version + ".jar", artifact.getFileName().toString());
|
||||
try (var jar = new JarFile(artifact.toFile())) {
|
||||
var descriptor = jar.getJarEntry("plugin.yml");
|
||||
assertNotNull(descriptor);
|
||||
Map<?, ?> metadata = new Yaml().load(jar.getInputStream(descriptor));
|
||||
assertEquals("PurpurCarry", metadata.get("name"));
|
||||
assertEquals(version, metadata.get("version"));
|
||||
assertEquals("games.dmg.purpurcarry.PurpurCarryPlugin", metadata.get("main"));
|
||||
assertEquals("1.20", metadata.get("api-version"));
|
||||
assertFalse(metadata.containsKey("commands"));
|
||||
assertFalse(metadata.containsKey("permissions"));
|
||||
Class<?> entrypoint = Class.forName((String) metadata.get("main"));
|
||||
assertTrue(JavaPlugin.class.isAssignableFrom(entrypoint));
|
||||
var bytecode = jar.getJarEntry("games/dmg/purpurcarry/PurpurCarryPlugin.class");
|
||||
assertNotNull(bytecode);
|
||||
try (var input = new DataInputStream(jar.getInputStream(bytecode))) {
|
||||
assertEquals(0xCAFEBABE, input.readInt());
|
||||
input.readUnsignedShort();
|
||||
assertEquals(69, input.readUnsignedShort());
|
||||
}
|
||||
assertFalse(jar.stream().anyMatch(entry ->
|
||||
entry.getName().startsWith("org/bukkit/")
|
||||
|| entry.getName().startsWith("org/purpurmc/")
|
||||
|| entry.getName().endsWith("Test.class")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package games.dmg.purpurcarry;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class WorkflowContractTest {
|
||||
@Test
|
||||
void ciBuildsAndUploadsACommitAddressedDevelopmentJar() throws Exception {
|
||||
String workflow = Files.readString(Path.of(".gitea/workflows/ci.yml"));
|
||||
|
||||
assertTrue(workflow.contains("pull_request:"));
|
||||
assertTrue(workflow.contains("java-version: 25"));
|
||||
assertTrue(workflow.contains("./gradlew clean check jar"));
|
||||
assertTrue(workflow.contains("commitlint"));
|
||||
assertTrue(workflow.contains("purpur-carry-${{ github.sha }}"));
|
||||
assertTrue(workflow.contains("build/libs/purpur-carry-*.jar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseVerifiesExactPublicSourceAndPublishesWithoutCredentialArguments() throws Exception {
|
||||
String workflow = Files.readString(Path.of(".gitea/workflows/release.yml"));
|
||||
|
||||
assertTrue(workflow.contains("SOURCE_SHA: ${{ github.sha }}"));
|
||||
assertTrue(workflow.contains("https://git.garvis.dev/dmg/purpur-carry.git"));
|
||||
assertTrue(workflow.contains("./gradlew clean check jar"));
|
||||
assertTrue(workflow.contains("semantic-release@24.2.9"));
|
||||
assertTrue(workflow.contains("git-credential.sh"));
|
||||
assertTrue(workflow.contains("RELEASE_CREDENTIAL: ${{ secrets.RELEASE_TOKEN }}"));
|
||||
assertTrue(workflow.contains("RELEASE_COMMIT: ${{ github.sha }}"));
|
||||
assertTrue(workflow.contains("node build/release-tools/publish-release.js"));
|
||||
assertTrue(workflow.contains("build/libs/purpur-carry-${{ steps.release.outputs.version }}.jar"));
|
||||
assertFalse(workflow.contains("Authorization: token"));
|
||||
assertFalse(workflow.contains("GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}"));
|
||||
assertFalse(workflow.contains("token: ${{ secrets.RELEASE_TOKEN }}"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"outDir": "build/release-tools",
|
||||
"rootDir": "scripts",
|
||||
"types": ["node"],
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"noEmitOnError": true
|
||||
},
|
||||
"include": ["scripts/*.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user