diff --git a/.fernignore b/.fernignore index 084a8ebb..528a3cc2 100644 --- a/.fernignore +++ b/.fernignore @@ -1 +1,58 @@ -# Specify files that shouldn't be modified by Fern +# Files Fern must not modify or delete when regenerating this repo. +# +# Fern prunes anything it did not generate, and it reads this file from the pull request's +# base branch — so an entry only protects a file once it is on main. +# +# publish-main.yml is hand-written: it releases com.whop.api:whop-java when a merge to main +# lands a gradle.properties version that is not yet published, and it is meant to be the +# only publish path. It has two targets behind one set of guards — GitHub Packages +# (whopio-internal, authenticated with the run's own GITHUB_TOKEN, no namespace to +# register) and the Sonatype Central Portal (public, blocked on DNS verification and a +# signing key). RELEASE_TARGET sets the default, a workflow_dispatch input overrides it, +# and every pull request rehearses both so neither path rots while the other is the live +# one. +# +# stamp-version.py is that workflow's version stamp: gradle.properties, every source file +# carrying X-Fern-SDK-Version, and .fern/metadata.json. It is a file rather than a script +# inlined into one job because both publish paths build the same commit and have to stamp +# it identically. +# +# gradle.properties is the version of record and the release trigger. The Fern Java +# generator writes no version and no group into build.gradle at all — `gradle properties` +# on the delivered tree reports `version: unspecified`, an empty group and an artifact name +# taken from the checkout directory — so without this file the project has no Maven +# coordinate. Gradle reads `group` and `version` from gradle.properties natively, which +# keeps the coordinate out of the generated build.gradle and lets Fern keep owning the +# dependency list there. +# +# release.gradle is the Gradle init script that adds the MavenPublication: the POM (name, +# description, url, licenses, developers, scm) that Maven Central requires, the whop-java +# archive name, a repository that stages the bundle to build/bundle instead of uploading +# it, and the GitHub Packages repository the internal release publishes to. Applying it +# with -I rather than editing build.gradle means a regeneration cannot silently drop it +# and Fern keeps ownership of the build file — including the four `api` dependencies, +# which are the reason a real POM and not a bare jar is what has to reach consumers. +# +# ci.yml is hand-written for a security reason, not a stylistic one. The Fern Java +# generator's GithubWorkflowGenerator emits a tag-gated `publish` job — `if: +# github.event_name == 'push' && contains(github.ref, 'refs/tags/')`, running `./gradlew +# publish` or `./gradlew sonatypeCentralUpload` with MAVEN_USERNAME, MAVEN_PASSWORD, +# MAVEN_SIGNATURE_KID, MAVEN_SIGNATURE_SECRET_KEY and MAVEN_SIGNATURE_PASSWORD in the same +# environment as Gradle's dependency resolution — whenever the generator is configured +# with a Maven registry. The java-release group in the monorepo's sdks/fern/generators.yml +# has no maven output today, so the ci.yml Fern delivered in whopsdk-java#1 is compile+test +# only. Adding that output is the obvious next step for anyone wiring Maven publishing +# through Fern, and it would introduce a second unreviewed publish path that also fires on +# the tag publish-main.yml creates. Because Fern reads this file from the base branch, the +# entry has to exist before that regeneration, not after it. Ours is the generated file +# with the publish job that cannot appear, modernised off actions/setup-java@v1. +# +# LICENSE: Fern does not carry over a LICENSE, and the POM this repo publishes declares +# Apache-2.0 — the same license the python-release and ruby-release groups pass to Fern. +# Without this entry the first regeneration would delete the file the POM points at. +.github/workflows/ci.yml +.github/workflows/publish-main.yml +.github/release.gradle +.github/stamp-version.py +gradle.properties +LICENSE diff --git a/.github/release.gradle b/.github/release.gradle new file mode 100644 index 00000000..6260c185 --- /dev/null +++ b/.github/release.gradle @@ -0,0 +1,67 @@ +gradle.projectsLoaded { + gradle.rootProject { root -> + root.afterEvaluate { + root.base.archivesName = 'whop-java' + + root.publishing { + publications { + maven(MavenPublication) { + groupId = root.group + artifactId = 'whop-java' + version = root.version + from root.components.java + + pom { + name = 'Whop Java SDK' + description = 'The official Java SDK for the Whop API.' + url = 'https://github.com/whopio/whopsdk-java' + licenses { + license { + name = 'Apache License, Version 2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'whop' + name = 'Whop' + email = 'support@whop.com' + organization = 'Whop' + organizationUrl = 'https://whop.com' + } + } + scm { + url = 'https://github.com/whopio/whopsdk-java' + connection = 'scm:git:https://github.com/whopio/whopsdk-java.git' + developerConnection = 'scm:git:ssh://git@github.com/whopio/whopsdk-java.git' + } + } + } + } + + repositories { + maven { + name = 'bundle' + url = root.layout.buildDirectory.dir('bundle') + } + + // Same publication, same POM, same coordinate as Central — only the + // transport differs, so a consumer who later moves to Central changes + // a repository URL and not a dependency line. Credentials are read + // from the environment rather than a property so that the empty + // default keeps `-I` usable for bundle staging, where the task that + // needs them is never invoked; Gradle only demands them when + // publishMavenPublicationToGitHubPackagesRepository actually runs. + maven { + name = 'gitHubPackages' + url = System.getenv('GITHUB_PACKAGES_URL') ?: 'https://maven.pkg.github.com/whopio/whopsdk-java' + credentials { + username = System.getenv('GITHUB_ACTOR') ?: '' + password = System.getenv('GITHUB_TOKEN') ?: '' + } + } + } + } + } + } +} diff --git a/.github/stamp-version.py b/.github/stamp-version.py new file mode 100644 index 00000000..31123c1e --- /dev/null +++ b/.github/stamp-version.py @@ -0,0 +1,101 @@ +"""Stamp one version across every file in the tree that reports one. + +Two jobs publish the same commit to two registries, so the stamp has to be a single +artifact both can run rather than a script inlined into one of them. It is deliberately +fail-loud: Fern owns every file it touches, so a rename or a reshaped header should stop +a release rather than ship a jar that reports the generator's constant on the wire. +""" + +import json +import pathlib +import re +import sys + +HEADER = "X-Fern-SDK-Version" + + +def fail(message): + raise SystemExit("::error::" + message) + + +def stamp_version_file(path, version): + text = path.read_text() + text, count = re.subn( + r"(?m)^([ \t]*version[ \t]*=[ \t]*).*$", r"\g<1>" + version, text, count=1 + ) + if count == 0: + fail("no version property found in " + str(path)) + path.write_text(text) + print("stamped " + str(path)) + + +def stamp_sources(version): + carriers = [ + path + for path in pathlib.Path("src/main").rglob("*.java") + if HEADER in path.read_text() + ] + + if not carriers: + fail( + "nothing under src/main carries " + + HEADER + + "; the published jar would report the generator's constant" + ) + + for path in carriers: + source = path.read_text() + source, hits = re.subn( + r'("' + HEADER + r'"\s*,\s*")[^"]*', r"\g<1>" + version, source + ) + if hits == 0: + fail( + str(path) + + " names " + + HEADER + + " but the rewrite matched nothing; the header shape changed" + ) + path.write_text(source) + print("stamped " + str(path)) + + +def stamp_metadata(path, version): + if not path.exists(): + return + metadata = json.loads(path.read_text()) + for key in ("requestedVersion", "sdkVersion"): + if key in metadata: + metadata[key] = version + path.write_text(json.dumps(metadata, indent=2) + "\n") + print("stamped " + str(path)) + + +def assert_no_stale_version(version): + semver = re.compile(r"\d+\.\d+\.\d+") + stale = [] + for path in pathlib.Path("src/main").rglob("*.java"): + for number, line in enumerate(path.read_text().splitlines(), 1): + if HEADER in line and semver.search(line) and version not in line: + stale.append(str(path) + ":" + str(number) + ": " + line.strip()) + + if stale: + fail( + "the source tree still reports a version other than " + + version + + ":\n" + + "\n".join(stale) + ) + + +def main(): + version, version_file, metadata_file = sys.argv[1:4] + + stamp_version_file(pathlib.Path(version_file), version) + stamp_sources(version) + stamp_metadata(pathlib.Path(metadata_file), version) + assert_no_stale_version(version) + print("every version stamp reports " + version) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 190eed59..95a9404d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,42 +1,56 @@ -name: ci +name: CI -on: [push] +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + JAVA_VERSION: "17" jobs: compile: runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - - name: Set up Java - id: setup-jre - uses: actions/setup-java@v1 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 with: - java-version: "11" - architecture: x64 + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb + with: + validate-wrappers: true - name: Compile - run: ./gradlew compileJava + run: ./gradlew --no-daemon compileJava test: - needs: [ compile ] + needs: [compile] runs-on: ubuntu-latest steps: - - name: Checkout repo - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} - - name: Set up Java - id: setup-jre - uses: actions/setup-java@v1 + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb with: - java-version: "11" - architecture: x64 + validate-wrappers: true - name: Test - run: ./gradlew test + run: ./gradlew --no-daemon test diff --git a/.github/workflows/publish-main.yml b/.github/workflows/publish-main.yml new file mode 100644 index 00000000..b26f9fb7 --- /dev/null +++ b/.github/workflows/publish-main.yml @@ -0,0 +1,1012 @@ +name: Release + +on: + push: + branches: [main] + paths: ["gradle.properties"] + pull_request: + workflow_dispatch: + inputs: + dry_run: + description: "Build, sign with a throwaway key and verify the bundle without uploading" + type: boolean + default: true + target: + description: "Which registry to release to" + type: choice + default: default + options: + - default + - github-packages + - maven-central + - both + +permissions: + contents: read + +concurrency: + group: publish-whop-java-${{ github.ref }} + cancel-in-progress: false + +env: + RELEASE_TARGET: github-packages + GROUP_ID: com.whop.api + ARTIFACT_ID: whop-java + PLACEHOLDER_VERSION: 0.0.0 + GROUP_PATH: com/whop/api + VERSION_FILE: gradle.properties + METADATA_FILE: .fern/metadata.json + STAMP_SCRIPT: .github/stamp-version.py + RESOURCE_DIR: src/main/java/com/whop/api/resources + MIN_RESOURCE_MODULES: "60" + INIT_SCRIPT: .github/release.gradle + BUNDLE_DIR: build/bundle + JAVA_VERSION: "17" + CENTRAL_BASE: https://repo1.maven.org/maven2 + PORTAL_BASE: https://central.sonatype.com/api/v1/publisher + PUBLISHING_TYPE: AUTOMATIC + PACKAGES_API: https://api.github.com/orgs/whopio/packages/maven + PACKAGES_BASE: https://maven.pkg.github.com/whopio/whopsdk-java + +jobs: + decide: + if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + outputs: + release: ${{ steps.gate.outputs.release }} + dry: ${{ steps.gate.outputs.dry }} + version: ${{ steps.gate.outputs.version }} + target: ${{ steps.gate.outputs.target }} + central: ${{ steps.gate.outputs.central }} + packages: ${{ steps.gate.outputs.packages }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + fetch-depth: 2 + persist-credentials: false + + - name: Write the version-regression guard + run: | + set -euo pipefail + cat > "${RUNNER_TEMP}/version_guard.py" <<'PY' + import re + import sys + + + def parse(raw): + match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(?:[-.]([0-9A-Za-z.-]+))?", raw) + if match is None: + raise SystemExit("unparseable version: " + raw) + core = tuple(int(part) for part in match.group(1, 2, 3)) + return core, match.group(4) + + + def rank(pre): + if pre is None: + return (1,) + parts = [] + for chunk in pre.split("."): + if chunk.isdigit(): + parts.append((0, int(chunk), "")) + else: + parts.append((1, 0, chunk)) + return (0, parts) + + + candidate_core, candidate_pre = parse(sys.argv[1]) + latest_core, latest_pre = parse(sys.argv[2]) + + if (candidate_core, rank(candidate_pre)) < (latest_core, rank(latest_pre)): + raise SystemExit(sys.argv[1] + " is behind " + sys.argv[2]) + + print(sys.argv[1] + " is at or ahead of " + sys.argv[2]) + PY + + - name: Self-test the version-regression guard + run: | + set -euo pipefail + failures=0 + + check() { + if python3 "${RUNNER_TEMP}/version_guard.py" "$1" "$2" >/dev/null 2>&1; then + actual=allow + else + actual=block + fi + + if [ "$actual" = "$3" ]; then + echo "ok: $1 vs $2 -> $actual" + else + echo "::error::version guard: $1 vs $2 expected $3, got $actual" + failures=$((failures + 1)) + fi + } + + check 1.0.0 1.0.0 allow + check 1.0.1 1.0.0 allow + check 1.0.0 1.0.1 block + check 0.0.10 0.0.9 allow + check 0.0.9 0.0.10 block + check 1.2.0 1.10.0 block + check 1.10.0 1.2.0 allow + check 2.0.0 1.99.99 allow + check 1.0.0-rc.1 0.9.9 allow + check 1.0.0-rc.1 1.0.0 block + check 1.0.0 1.0.0-rc.1 allow + check 1.0.0-rc.2 1.0.0-rc.1 allow + check 1.0.0-rc.1 1.0.0-rc.2 block + + [ "$failures" -eq 0 ] + + - name: Decide whether to release + id: gate + env: + EVENT: ${{ github.event_name }} + DRY_RUN: ${{ github.event.inputs.dry_run }} + TARGET_INPUT: ${{ github.event.inputs.target }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + emit() { echo "$1=$2" >> "$GITHUB_OUTPUT"; } + + if [ "$EVENT" = "pull_request" ] || [ "$DRY_RUN" = "true" ]; then + dry=true + else + dry=false + fi + emit dry "$dry" + + if [ "$EVENT" = "pull_request" ]; then + target=both + elif [ -n "${TARGET_INPUT:-}" ] && [ "$TARGET_INPUT" != "default" ]; then + target="$TARGET_INPUT" + else + target="$RELEASE_TARGET" + fi + + case "$target" in + github-packages) want_central=false; want_packages=true ;; + maven-central) want_central=true; want_packages=false ;; + both) want_central=true; want_packages=true ;; + *) + echo "::error::'${target}' is not a release target. Use github-packages, maven-central or both." + exit 1 + ;; + esac + + emit target "$target" + echo "event=$EVENT dry=$dry target=$target ref=$GITHUB_REF" + + refuse() { + if [ "$dry" = "true" ]; then + echo "::warning::[dry] a real release would stop here: $*" + return 0 + fi + echo "::error::$*" + exit 1 + } + + if [ ! -f "$VERSION_FILE" ]; then + echo "::notice::${VERSION_FILE} does not exist — this is not a tree this workflow can release." + emit release false + emit version "" + exit 0 + fi + + version=$(sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*//p' "$VERSION_FILE" | head -1 | tr -d '[:space:]') + + if [ -z "$version" ]; then + echo "::error::${VERSION_FILE} has no version property." + exit 1 + fi + + echo "${VERSION_FILE} version: $version" + emit version "$version" + + if ! printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$'; then + echo "::error::'$version' is not a version this workflow will publish." + exit 1 + fi + + if printf '%s' "$version" | grep -qiE 'SNAPSHOT'; then + echo "::error::'$version' is a snapshot; the Central Portal release path does not accept snapshots." + exit 1 + fi + + declared_group=$(sed -n 's/^[[:space:]]*group[[:space:]]*=[[:space:]]*//p' "$VERSION_FILE" | head -1 | tr -d '[:space:]') + if [ "$declared_group" != "$GROUP_ID" ]; then + echo "::error::${VERSION_FILE} declares group '${declared_group}' but this workflow publishes '${GROUP_ID}'." + exit 1 + fi + + if [ "$version" = "$PLACEHOLDER_VERSION" ]; then + if [ "$dry" = "true" ]; then + echo "::notice::[dry] ${PLACEHOLDER_VERSION} is the unreleased placeholder and a real release would stop here. Continuing so this run still exercises the build, the stamps, the bundle and the signing." + else + echo "::notice::${VERSION_FILE} is still at the ${PLACEHOLDER_VERSION} placeholder; there is nothing to release. The first release is a deliberate one-line pull request that sets a real version — Maven Central never lets it be replaced." + emit release false + exit 0 + fi + fi + + if [ "$EVENT" = "pull_request" ]; then + enforce_diff=false + else + enforce_diff=true + fi + + if git rev-parse --verify --quiet HEAD^ >/dev/null; then + changed=$(git diff --name-only HEAD^ HEAD) + body=$(git diff -U0 HEAD^ HEAD -- "$VERSION_FILE" | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' || true) + + if [ -z "$body" ]; then + other=0 + else + other=$(printf '%s\n' "$body" | grep -cvE '^[+-][[:space:]]*version[[:space:]]*=' || true) + fi + + if [ "$changed" = "$VERSION_FILE" ] && [ "$other" -eq 0 ]; then + echo "HEAD changes ${VERSION_FILE}'s version property and nothing else." + else + summary="a release must change ${VERSION_FILE} and nothing but its version property. Changed: $(printf '%s' "$changed" | tr '\n' ' ')" + if [ "$enforce_diff" = "true" ]; then + refuse "$summary" + else + echo "::notice::[$EVENT] this is not a release commit, so the diff is informational: ${summary}" + fi + fi + else + echo "::notice::no HEAD^ to diff against; the only-version-changed check has nothing to compare." + fi + + guard_against() { + if guard=$(python3 "${RUNNER_TEMP}/version_guard.py" "$version" "$2" 2>&1); then + echo "$guard" + else + refuse "${VERSION_FILE} is at ${version} but $1 already has $2. This branch was generated before that release; rebase or regenerate it before merging. ($guard)" + fi + } + + central_published=unknown + if [ "$want_central" = "true" ]; then + metadata_url="${CENTRAL_BASE}/${GROUP_PATH}/${ARTIFACT_ID}/maven-metadata.xml" + metadata_status=$(curl -sS -o "${RUNNER_TEMP}/maven-metadata.xml" -w '%{http_code}' "$metadata_url" || echo 000) + echo "Maven Central returned HTTP ${metadata_status} for ${metadata_url}" + + latest="" + case "$metadata_status" in + 200) + latest=$(python3 -c 'import re,sys; text=open(sys.argv[1]).read(); m=re.search(r"([^<]+)", text) or re.search(r"([^<]+)", text); print(m.group(1) if m else "")' "${RUNNER_TEMP}/maven-metadata.xml") + if [ -z "$latest" ]; then + refuse "maven-metadata.xml for ${GROUP_ID}:${ARTIFACT_ID} carries neither nor ." + fi + ;; + 404) + echo "::warning::${GROUP_ID}:${ARTIFACT_ID} has never been published to Maven Central. Releasing ${version} claims the coordinate permanently — Maven Central does not allow deleting or replacing a released version." + ;; + *) + refuse "Maven Central returned HTTP ${metadata_status} for maven-metadata.xml; refusing to guess the published version." + ;; + esac + + if [ -n "$latest" ]; then + echo "Latest on Maven Central: $latest" + guard_against "Maven Central" "$latest" + fi + + pom_url="${CENTRAL_BASE}/${GROUP_PATH}/${ARTIFACT_ID}/${version}/${ARTIFACT_ID}-${version}.pom" + pom_status=$(curl -sS -o /dev/null -w '%{http_code}' -I "$pom_url" || echo 000) + echo "Maven Central returned HTTP ${pom_status} for ${pom_url}" + + case "$pom_status" in + 200) central_published=yes ;; + 404) central_published=no ;; + *) + refuse "Maven Central returned HTTP ${pom_status} for ${ARTIFACT_ID} ${version}; refusing to guess whether it is published." + ;; + esac + fi + + packages_published=unknown + if [ "$want_packages" = "true" ]; then + : > "${RUNNER_TEMP}/package_versions.txt" + page=1 + package_status=200 + + while :; do + versions_url="${PACKAGES_API}/${GROUP_ID}.${ARTIFACT_ID}/versions?per_page=100&page=${page}" + package_status=$(curl -sS -o "${RUNNER_TEMP}/versions.json" -w '%{http_code}' \ + --header "Accept: application/vnd.github+json" \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + --header "Authorization: Bearer ${GITHUB_TOKEN}" \ + "$versions_url" || echo 000) + echo "GitHub Packages returned HTTP ${package_status} for ${versions_url}" + + if [ "$package_status" != "200" ]; then + break + fi + + count=$(python3 - "${RUNNER_TEMP}/versions.json" "${RUNNER_TEMP}/package_versions.txt" <<'PY' + import json + import sys + + page = json.load(open(sys.argv[1])) + with open(sys.argv[2], "a") as sink: + for item in page: + sink.write(item["name"] + "\n") + print(len(page)) + PY + ) + + if [ "$count" -lt 100 ]; then + break + fi + page=$((page + 1)) + done + + case "$package_status" in + 200) + published_versions=$(sort -u "${RUNNER_TEMP}/package_versions.txt") + echo "Published on GitHub Packages:" + printf '%s\n' "$published_versions" + + if printf '%s\n' "$published_versions" | grep -Fxq "$version"; then + packages_published=yes + else + packages_published=no + fi + + while IFS= read -r published_version; do + [ -n "$published_version" ] || continue + guard_against "GitHub Packages" "$published_version" + done <<< "$published_versions" + ;; + 404) + packages_published=no + echo "::warning::${GROUP_ID}:${ARTIFACT_ID} has never been published to GitHub Packages. A 404 here is the registry saying the package does not exist — an unauthorized read answers 403 with a scope message, and this step refuses on anything that is not 200 or 404." + ;; + *) + refuse "the GitHub Packages API returned HTTP ${package_status} for ${GROUP_ID}.${ARTIFACT_ID}; refusing to guess whether ${version} is published." + ;; + esac + fi + + if [ "$dry" = "true" ]; then + echo "Dry run: building, staging and verifying the bundle for ${target}. Nothing is uploaded." + emit release true + emit central "$want_central" + emit packages "$want_packages" + exit 0 + fi + + release_central=false + release_packages=false + + if [ "$want_central" = "true" ]; then + if [ "$central_published" = "yes" ]; then + echo "::notice::${GROUP_ID}:${ARTIFACT_ID}:${version} is already on Maven Central; nothing to publish there." + else + release_central=true + fi + fi + + if [ "$want_packages" = "true" ]; then + if [ "$packages_published" = "yes" ]; then + echo "::notice::${GROUP_ID}:${ARTIFACT_ID}:${version} is already on GitHub Packages; nothing to publish there." + else + release_packages=true + fi + fi + + emit central "$release_central" + emit packages "$release_packages" + + if [ "$release_central" = "false" ] && [ "$release_packages" = "false" ]; then + emit release false + exit 0 + fi + + emit release true + + build: + needs: decide + if: needs.decide.outputs.release == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - name: Refuse to build a tree Fern did not fill in + run: | + set -euo pipefail + + for required in build.gradle settings.gradle "$INIT_SCRIPT" "$STAMP_SCRIPT" "$VERSION_FILE"; do + if [ ! -f "$required" ]; then + echo "::error::${required} is missing; this tree cannot produce a release bundle." + exit 1 + fi + done + + count=$(find "$RESOURCE_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l) + echo "${RESOURCE_DIR}: ${count} resource packages" + if [ "$count" -lt "$MIN_RESOURCE_MODULES" ]; then + echo "::error::only ${count} resource packages, expected at least ${MIN_RESOURCE_MODULES}. Fern emits an empty SDK and exits 0 when it cannot parse the spec, and a Maven Central release cannot be recalled." + exit 1 + fi + + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb + with: + validate-wrappers: true + + - name: Sync the version stamps + env: + VERSION: ${{ needs.decide.outputs.version }} + run: python3 "$STAMP_SCRIPT" "$VERSION" "$VERSION_FILE" "$METADATA_FILE" + + - name: Test + run: ./gradlew --no-daemon test + + - name: Stage the Maven bundle + run: ./gradlew --no-daemon -I "$INIT_SCRIPT" publishMavenPublicationToBundleRepository + + - name: Verify the staged bundle + env: + VERSION: ${{ needs.decide.outputs.version }} + run: | + set -euo pipefail + + dir="${BUNDLE_DIR}/${GROUP_PATH}/${ARTIFACT_ID}/${VERSION}" + if [ ! -d "$dir" ]; then + echo "::error::${dir} does not exist; Gradle staged the wrong coordinate." + find "$BUNDLE_DIR" -maxdepth 6 -type d || true + exit 1 + fi + + find "$BUNDLE_DIR" -name 'maven-metadata.xml*' -delete + + for suffix in ".jar" "-sources.jar" "-javadoc.jar" ".pom"; do + file="${dir}/${ARTIFACT_ID}-${VERSION}${suffix}" + if [ ! -s "$file" ]; then + echo "::error::${file} is missing or empty. Maven Central requires the main, sources and javadoc jars plus a POM." + ls -l "$dir" || true + exit 1 + fi + echo "present: $(ls -l "$file")" + done + + pom="${dir}/${ARTIFACT_ID}-${VERSION}.pom" + python3 - "$pom" "$GROUP_ID" "$ARTIFACT_ID" "$VERSION" build.gradle <<'PY' + import re + import sys + import xml.etree.ElementTree as ET + + pom, group, artifact, version, build_file = sys.argv[1:6] + ns = {"m": "http://maven.apache.org/POM/4.0.0"} + root = ET.parse(pom).getroot() + + + def text(tag, node=root): + found = node.find("m:" + tag, ns) + return None if found is None else (found.text or "").strip() + + + problems = [] + for tag, expected in (("groupId", group), ("artifactId", artifact), ("version", version)): + if text(tag) != expected: + problems.append(tag + " is " + repr(text(tag)) + ", expected " + repr(expected)) + + for tag in ("name", "description", "url"): + if not text(tag): + problems.append(tag + " is empty; Maven Central rejects the deployment") + + for tag in ("licenses", "developers", "scm"): + if root.find("m:" + tag, ns) is None: + problems.append(tag + " is absent; Maven Central rejects the deployment") + + declared = {} + for node in root.findall("m:dependencies/m:dependency", ns): + key = text("groupId", node) + ":" + text("artifactId", node) + declared[key] = (text("version", node), text("scope", node)) + + print("POM dependencies:") + for key in sorted(declared): + print(" " + key + ":" + str(declared[key][0]) + " (" + str(declared[key][1]) + ")") + + expected_api = re.findall( + r"""(?m)^\s*api\s+['"]([^'":]+):([^'":]+):([^'"]+)['"]""", + open(build_file).read(), + ) + if not expected_api: + problems.append(build_file + " declares no api dependencies; the parser no longer matches") + + for dep_group, dep_artifact, dep_version in expected_api: + key = dep_group + ":" + dep_artifact + if key not in declared: + problems.append( + key + + " is an api dependency in " + + build_file + + " but is absent from the POM; consumers would not resolve it" + ) + continue + found_version, scope = declared[key] + if found_version != dep_version: + problems.append(key + " is " + str(found_version) + " in the POM, " + dep_version + " in " + build_file) + if scope != "compile": + problems.append( + key + + " is scope " + + str(scope) + + " in the POM; an api dependency has to reach the consumer's compile classpath" + ) + + if problems: + raise SystemExit("::error::" + "; ".join(problems)) + + print( + "POM declares " + + group + ":" + artifact + ":" + version + + " with the metadata Central requires and all " + + str(len(expected_api)) + + " api dependencies at compile scope" + ) + PY + + unzip -p "${dir}/${ARTIFACT_ID}-${VERSION}-sources.jar" com/whop/api/core/ClientOptions.java > "${RUNNER_TEMP}/ClientOptions.java" + if ! grep -q "\"X-Fern-SDK-Version\", \"${VERSION}\"" "${RUNNER_TEMP}/ClientOptions.java"; then + echo "::error::the sources jar does not report ${VERSION} on the wire." + grep -n 'X-Fern-SDK-Version' "${RUNNER_TEMP}/ClientOptions.java" || true + exit 1 + fi + + rm -rf "${RUNNER_TEMP}/jar" + mkdir -p "${RUNNER_TEMP}/jar" + unzip -q "${dir}/${ARTIFACT_ID}-${VERSION}.jar" -d "${RUNNER_TEMP}/jar" + + carriers=$(grep -rlaF 'X-Fern-SDK-Version' "${RUNNER_TEMP}/jar" || true) + if [ -z "$carriers" ]; then + echo "::error::no class in the main jar names X-Fern-SDK-Version. The header moved, and the stamp step is no longer stamping what ships." + exit 1 + fi + + echo "compiled carriers of X-Fern-SDK-Version:" + printf '%s\n' "$carriers" + + while IFS= read -r carrier; do + if ! grep -qaF "$VERSION" "$carrier"; then + echo "::error::${carrier} names X-Fern-SDK-Version but does not carry ${VERSION}; the published jar would report a different version on the wire." + exit 1 + fi + done <<< "$carriers" + + echo "bundle verified:" + find "$BUNDLE_DIR" -type f | sort + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: bundle + path: ${{ env.BUNDLE_DIR }}/ + if-no-files-found: error + retention-days: 7 + + publish: + needs: [decide, build] + if: needs.decide.outputs.central == 'true' + runs-on: ubuntu-latest + environment: ${{ needs.decide.outputs.dry == 'true' && 'dry-run' || 'maven-central' }} + permissions: + contents: read + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: bundle + path: bundle/ + + - name: Refuse a dry run that can see the release credentials + env: + DRY: ${{ needs.decide.outputs.dry }} + HAS_PORTAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME != '' }} + HAS_PORTAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD != '' }} + HAS_SIGNING_KEY: ${{ secrets.MAVEN_SIGNING_KEY != '' }} + run: | + set -euo pipefail + + echo "dry=${DRY} portal_username=${HAS_PORTAL_USERNAME} portal_password=${HAS_PORTAL_PASSWORD} signing_key=${HAS_SIGNING_KEY}" + + if [ "$DRY" = "true" ]; then + if [ "$HAS_PORTAL_USERNAME" = "true" ] || [ "$HAS_PORTAL_PASSWORD" = "true" ] || [ "$HAS_SIGNING_KEY" = "true" ]; then + echo "::error::a dry run can read the release credentials. They must exist only as environment secrets on the maven-central environment, never as repository or organization secrets — repository secrets are handed to same-repo pull request runs, so anyone who can open a pull request could exfiltrate them." + exit 1 + fi + echo "no release credential is reachable from this dry run." + exit 0 + fi + + missing="" + [ "$HAS_PORTAL_USERNAME" = "true" ] || missing="${missing} MAVEN_CENTRAL_USERNAME" + [ "$HAS_PORTAL_PASSWORD" = "true" ] || missing="${missing} MAVEN_CENTRAL_PASSWORD" + [ "$HAS_SIGNING_KEY" = "true" ] || missing="${missing} MAVEN_SIGNING_KEY" + + if [ -n "$missing" ]; then + echo "::error::the maven-central environment is missing:${missing}" + exit 1 + fi + + echo "every release credential is present." + + - name: Match the artifact against the checksums the build job staged + run: | + set -euo pipefail + + checked=0 + while IFS= read -r checksum; do + artifact="${checksum%.sha1}" + if [ ! -f "$artifact" ]; then + echo "::error::${checksum} has no ${artifact} beside it." + exit 1 + fi + expected=$(tr -d '[:space:]' < "$checksum") + actual=$(sha1sum "$artifact" | cut -d' ' -f1) + if [ "$expected" != "$actual" ]; then + echo "::error::${artifact} is sha1:${actual} but the build job staged sha1:${expected}. Refusing to sign bytes the build job never verified." + exit 1 + fi + checked=$((checked + 1)) + done < <(find bundle -name '*.sha1') + + if [ "$checked" -eq 0 ]; then + echo "::error::the downloaded bundle carries no checksums." + exit 1 + fi + echo "${checked} artifacts match the checksums the build job staged" + + - name: Sign the bundle + env: + DRY: ${{ needs.decide.outputs.dry }} + VERSION: ${{ needs.decide.outputs.version }} + SIGNING_KEY: ${{ secrets.MAVEN_SIGNING_KEY }} + SIGNING_PASSPHRASE: ${{ secrets.MAVEN_SIGNING_PASSPHRASE }} + run: | + set -euo pipefail + + GNUPGHOME=$(mktemp -d) + chmod 700 "$GNUPGHOME" + export GNUPGHOME + + if [ "$DRY" = "true" ]; then + echo "::warning::signing with a throwaway key generated in this runner. A real release signs with Whop's Maven Central key, and that substitution is the one thing this rehearsal cannot cover." + gpg --batch --pinentry-mode loopback --passphrase "" \ + --quick-generate-key "Whop Dry Run " rsa3072 sign 0 + SIGNING_PASSPHRASE="" + else + printf '%s\n' "$SIGNING_KEY" | gpg --batch --import + fi + + fingerprint=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/ {print $10; exit}') + if [ -z "$fingerprint" ]; then + echo "::error::no secret key is available to sign with." + exit 1 + fi + echo "signing with ${fingerprint}" + + signed=0 + while IFS= read -r file; do + gpg --batch --yes --pinentry-mode loopback --passphrase "$SIGNING_PASSPHRASE" \ + --local-user "$fingerprint" --armor --detach-sign --output "${file}.asc" "$file" + signed=$((signed + 1)) + done < <(find bundle -type f ! -name '*.asc' ! -name '*.md5' ! -name '*.sha1' ! -name '*.sha256' ! -name '*.sha512') + + if [ "$signed" -eq 0 ]; then + echo "::error::there was nothing in the bundle to sign." + exit 1 + fi + echo "signed ${signed} artifacts" + + while IFS= read -r signature; do + if ! gpg --batch --verify "$signature" "${signature%.asc}" >/dev/null 2>&1; then + echo "::error::${signature} does not verify against ${signature%.asc}" + exit 1 + fi + done < <(find bundle -name '*.asc') + echo "every signature verifies" + + for suffix in ".jar" "-sources.jar" "-javadoc.jar" ".pom"; do + required="bundle/${GROUP_PATH}/${ARTIFACT_ID}/${VERSION}/${ARTIFACT_ID}-${VERSION}${suffix}.asc" + if [ ! -s "$required" ]; then + echo "::error::${required} is missing. Maven Central rejects a deployment whose main, sources or javadoc jar or POM is unsigned." + exit 1 + fi + done + + (cd bundle && zip -qr "${RUNNER_TEMP}/central-bundle.zip" .) + echo "bundle: $(ls -l "${RUNNER_TEMP}/central-bundle.zip")" + unzip -Z1 "${RUNNER_TEMP}/central-bundle.zip" | sort + + - name: Upload the bundle to the Sonatype Central Portal + if: needs.decide.outputs.dry == 'false' + env: + VERSION: ${{ needs.decide.outputs.version }} + PORTAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + PORTAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + run: | + set -euo pipefail + + auth=$(printf '%s:%s' "$PORTAL_USERNAME" "$PORTAL_PASSWORD" | base64 -w0) + + status=$(curl -sS -o "${RUNNER_TEMP}/upload.txt" -w '%{http_code}' \ + --request POST \ + --header "Authorization: Bearer ${auth}" \ + --form "bundle=@${RUNNER_TEMP}/central-bundle.zip" \ + "${PORTAL_BASE}/upload?name=${ARTIFACT_ID}-${VERSION}&publishingType=${PUBLISHING_TYPE}" || echo 000) + + if [ "$status" != "201" ]; then + echo "::error::the Central Portal returned HTTP ${status} for the upload." + cat "${RUNNER_TEMP}/upload.txt" || true + exit 1 + fi + + deployment=$(tr -d '[:space:]' < "${RUNNER_TEMP}/upload.txt") + echo "deployment=${deployment}" + echo "DEPLOYMENT_ID=${deployment}" >> "$GITHUB_ENV" + + terminal="" + for attempt in $(seq 1 120); do + code=$(curl -sS -o "${RUNNER_TEMP}/status.json" -w '%{http_code}' \ + --request POST \ + --header "Authorization: Bearer ${auth}" \ + "${PORTAL_BASE}/status?id=${deployment}" || echo 000) + + if [ "$code" != "200" ]; then + echo "::warning::status poll ${attempt} returned HTTP ${code}" + sleep 15 + continue + fi + + state=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("deploymentState",""))' "${RUNNER_TEMP}/status.json") + echo "poll ${attempt}: ${state}" + + case "$state" in + PUBLISHED) + terminal=PUBLISHED + break + ;; + FAILED) + terminal=FAILED + break + ;; + VALIDATED) + if [ "$PUBLISHING_TYPE" = "USER_MANAGED" ]; then + terminal=VALIDATED + break + fi + ;; + esac + + sleep 15 + done + + cat "${RUNNER_TEMP}/status.json" || true + + case "$terminal" in + PUBLISHED) + echo "::notice::${GROUP_ID}:${ARTIFACT_ID}:${VERSION} is published." + ;; + VALIDATED) + echo "::notice::deployment ${deployment} validated and is waiting for a human to release it at https://central.sonatype.com/publishing/deployments" + ;; + FAILED) + echo "::error::deployment ${deployment} failed validation. Dropping it so the version can be retried." + curl -sS -o /dev/null -w 'drop http=%{http_code}\n' \ + --request DELETE \ + --header "Authorization: Bearer ${auth}" \ + "${PORTAL_BASE}/deployment/${deployment}" || true + exit 1 + ;; + *) + echo "::error::deployment ${deployment} did not reach a terminal state. It is left in place — resolve it at https://central.sonatype.com/publishing/deployments before re-running, because a second upload of the same version will not supersede it." + exit 1 + ;; + esac + + publish-github-packages: + needs: [decide, build] + if: needs.decide.outputs.packages == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: bundle + path: bundle/ + + - name: Refuse a run that can see the Maven Central credentials + env: + HAS_PORTAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME != '' }} + HAS_PORTAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD != '' }} + HAS_SIGNING_KEY: ${{ secrets.MAVEN_SIGNING_KEY != '' }} + run: | + set -euo pipefail + + echo "portal_username=${HAS_PORTAL_USERNAME} portal_password=${HAS_PORTAL_PASSWORD} signing_key=${HAS_SIGNING_KEY}" + + if [ "$HAS_PORTAL_USERNAME" = "true" ] || [ "$HAS_PORTAL_PASSWORD" = "true" ] || [ "$HAS_SIGNING_KEY" = "true" ]; then + echo "::error::this job can read the Maven Central release credentials. They belong to the maven-central environment and nothing else — this job joins no environment, so seeing them means they were created as repository or organization secrets, which GitHub hands to same-repo pull request runs." + exit 1 + fi + + echo "no Maven Central credential is reachable from this job." + + - name: Report the coordinate and the artifact set the build job verified + env: + VERSION: ${{ needs.decide.outputs.version }} + run: | + set -euo pipefail + + dir="bundle/${GROUP_PATH}/${ARTIFACT_ID}/${VERSION}" + echo "coordinate: ${GROUP_ID}:${ARTIFACT_ID}:${VERSION}" + echo "repository: ${PACKAGES_BASE}" + echo "artifacts:" + find "$dir" -type f ! -name '*.md5' ! -name '*.sha1' ! -name '*.sha256' ! -name '*.sha512' | sort + echo "POM:" + cat "${dir}/${ARTIFACT_ID}-${VERSION}.pom" + + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb + with: + validate-wrappers: true + + - name: Sync the version stamps + env: + VERSION: ${{ needs.decide.outputs.version }} + run: python3 "$STAMP_SCRIPT" "$VERSION" "$VERSION_FILE" "$METADATA_FILE" + + - name: Match the POM against the one the build job verified + env: + VERSION: ${{ needs.decide.outputs.version }} + run: | + set -euo pipefail + + ./gradlew --no-daemon -I "$INIT_SCRIPT" generatePomFileForMavenPublication + + staged="bundle/${GROUP_PATH}/${ARTIFACT_ID}/${VERSION}/${ARTIFACT_ID}-${VERSION}.pom" + if ! diff -u "$staged" build/publications/maven/pom-default.xml; then + echo "::error::this job would publish a POM the build job never verified. The build job asserts the coordinate, the Central metadata and every api dependency against the generated build.gradle; a POM that differs here means the two jobs did not see the same tree." + exit 1 + fi + + echo "the POM this job will publish is the one the build job verified" + + - name: Rehearse the upload + if: needs.decide.outputs.dry == 'true' + env: + GITHUB_PACKAGES_URL: ${{ env.PACKAGES_BASE }} + run: | + set -euo pipefail + echo "::warning::dry run — resolving the publish task graph without uploading. The HTTP transport to ${PACKAGES_BASE} is the only part of this path a dry run cannot cover." + ./gradlew --no-daemon -I "$INIT_SCRIPT" publishMavenPublicationToGitHubPackagesRepository --dry-run + + - name: Publish to GitHub Packages + if: needs.decide.outputs.dry == 'false' + env: + GITHUB_PACKAGES_URL: ${{ env.PACKAGES_BASE }} + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew --no-daemon -I "$INIT_SCRIPT" publishMavenPublicationToGitHubPackagesRepository + + - name: Verify what the registry now serves + if: needs.decide.outputs.dry == 'false' + env: + VERSION: ${{ needs.decide.outputs.version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + pom_url="${PACKAGES_BASE}/${GROUP_PATH}/${ARTIFACT_ID}/${VERSION}/${ARTIFACT_ID}-${VERSION}.pom" + status=$(curl -sSL -o "${RUNNER_TEMP}/published.pom" -w '%{http_code}' \ + --header "Authorization: Bearer ${GITHUB_TOKEN}" "$pom_url" || echo 000) + echo "GitHub Packages returned HTTP ${status} for ${pom_url}" + + if [ "$status" != "200" ]; then + echo "::error::${GROUP_ID}:${ARTIFACT_ID}:${VERSION} is not resolvable from ${PACKAGES_BASE} after the upload." + exit 1 + fi + + staged="bundle/${GROUP_PATH}/${ARTIFACT_ID}/${VERSION}/${ARTIFACT_ID}-${VERSION}.pom" + if ! diff -u "$staged" "${RUNNER_TEMP}/published.pom"; then + echo "::error::the POM the registry serves is not the POM the build job verified." + exit 1 + fi + + echo "::notice::${GROUP_ID}:${ARTIFACT_ID}:${VERSION} resolves from ${PACKAGES_BASE} and serves the POM the build job verified." + + tag: + needs: [decide, build, publish, publish-github-packages] + if: always() && needs.decide.outputs.dry == 'false' && needs.decide.outputs.version != '' + runs-on: ubuntu-latest + permissions: + contents: write + packages: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + + - name: Tag the published version + env: + VERSION: ${{ needs.decide.outputs.version }} + TARGET: ${{ needs.decide.outputs.target }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + central_pom="${CENTRAL_BASE}/${GROUP_PATH}/${ARTIFACT_ID}/${VERSION}/${ARTIFACT_ID}-${VERSION}.pom" + packages_pom="${PACKAGES_BASE}/${GROUP_PATH}/${ARTIFACT_ID}/${VERSION}/${ARTIFACT_ID}-${VERSION}.pom" + + resolvable() { + local status + if [ "$TARGET" != "maven-central" ]; then + status=$(curl -sSL -o /dev/null -w '%{http_code}' \ + --header "Authorization: Bearer ${GITHUB_TOKEN}" "$packages_pom" || echo 000) + echo " GitHub Packages: HTTP ${status}" + if [ "$status" = "200" ]; then + return 0 + fi + fi + + if [ "$TARGET" != "github-packages" ]; then + status=$(curl -sS -o /dev/null -w '%{http_code}' -I "$central_pom" || echo 000) + echo " Maven Central: HTTP ${status}" + if [ "$status" = "200" ]; then + return 0 + fi + fi + + return 1 + } + + found=no + for attempt in $(seq 1 20); do + echo "attempt ${attempt}:" + if resolvable; then + found=yes + break + fi + sleep 30 + done + + if [ "$found" != "yes" ]; then + echo "::notice::${GROUP_ID}:${ARTIFACT_ID}:${VERSION} is not resolvable from any registry this run targeted; nothing to tag. Re-run this workflow once it is — this job is idempotent." + exit 0 + fi + + if git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" >/dev/null 2>&1; then + echo "v${VERSION} is already tagged." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "v${VERSION}" -m "${ARTIFACT_ID} ${VERSION}" + + git push origin "refs/tags/v${VERSION}" || \ + git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" >/dev/null diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..c58bc52d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +group=com.whop.api +version=0.0.0 +org.gradle.jvmargs=-Xmx4g