diff --git a/.github/actions/core-cicd/deployment/deploy-bunny-maven-s3/README.md b/.github/actions/core-cicd/deployment/deploy-bunny-maven-s3/README.md new file mode 100644 index 000000000000..af7a2c7e4b0d --- /dev/null +++ b/.github/actions/core-cicd/deployment/deploy-bunny-maven-s3/README.md @@ -0,0 +1,48 @@ +# Deploy Maven Artifacts to S3 + +Publishes the locally-installed dotCMS Maven artifacts to the BunnyCDN +S3-compatible repository. This replaces the former `deploy-jfrog` (Artifactory) +action. + +The action restores the `maven-repo` artifact produced by the build phase, +resolves the project version, and delegates the upload to +[`.github/scripts/publish-to-s3/publish.sh`](../../../../scripts/publish-to-s3/README.md). +The script preserves the `com/dotcms//` layout, writes +`.sha1`/`.md5` checksums, and regenerates the `maven-metadata.xml` files that +Artifactory used to create. + +## Inputs + +| Input | Required | Default | Description | +| --- | --- | --- | --- | +| `version` | no | project version | Version to publish. Set it when the restored `maven-repo` artifact was built from a different ref than the checked-out POM (e.g. releases). | +| `modules` | no | all modules for the version | Comma-separated artifactIds to restrict the publish to. | +| `exclude-ext` | no | `repositories,excludeext` | Extra file extensions to skip. | +| `dry-run` | no | `false` | Print what would be uploaded without writing. | +| `bucket` | no | `MAVEN_BUNNY_RW_USERNAME` | S3 bucket / Bunny storage zone. | +| `prefix` | no | `libs-release` | Key prefix inside the bucket. | +| `endpoint` | no | `https://ny-s3.storage.bunnycdn.com` | S3 endpoint URL. | +| `region` | no | `ny` | S3 signing region. | +| `access-key-id` | **yes** | — | S3 access key id (Bunny storage-zone name). | +| `secret-access-key` | **yes** | — | S3 secret access key (Bunny storage-zone password). | +| `checksums` | no | `true` | Upload `.sha1`/`.md5` checksums. | +| `github-token` | **yes** | — | Token used to download the `maven-repo` artifact. | +| `artifact-run-id` | no | `${{ github.run_id }}` | Run id that holds the `maven-repo` artifact. | + +## Example + +```yaml +- name: Deploy Maven artifacts + uses: ./.github/actions/core-cicd/deployment/deploy-bunny-maven-s3 + with: + access-key-id: ${{ secrets.MAVEN_BUNNY_RW_USERNAME }} + secret-access-key: ${{ secrets.MAVEN_BUNNY_RW_PASSWORD }} + github-token: ${{ secrets.GITHUB_TOKEN }} + artifact-run-id: ${{ inputs.artifact-run-id }} +``` + +Public URL for the above with the defaults: + +``` +https://dotcms-repo.b-cdn.net/libs-release/com/dotcms///-.jar +``` diff --git a/.github/actions/core-cicd/deployment/deploy-bunny-maven-s3/action.yml b/.github/actions/core-cicd/deployment/deploy-bunny-maven-s3/action.yml new file mode 100644 index 000000000000..37c163b628c4 --- /dev/null +++ b/.github/actions/core-cicd/deployment/deploy-bunny-maven-s3/action.yml @@ -0,0 +1,113 @@ +name: 'Deploy Maven Artifacts to Bunny S3' +description: | + Publish the locally-installed dotCMS Maven artifacts to the BunnyCDN + S3-compatible storage zone. This replaces the former Artifactory + (deploy-jfrog) deployment. + + The action restores the `maven-repo` artifact produced by the build phase, + resolves the project version, then delegates to + `.github/scripts/publish-to-s3/publish.sh maven`, which uploads every + com/dotcms module for that version and regenerates maven-metadata.xml. + +inputs: + version: + description: 'Version of the artifacts to deploy. Defaults to the project version in the checked-out POM.' + required: false + modules: + description: 'Comma-separated artifactIds to publish. Defaults to every com/dotcms module with a directory for the version.' + required: false + exclude-ext: + description: 'Comma-separated file extensions to skip.' + required: false + default: 'repositories,excludeext' + dry-run: + description: 'Enable dry-run mode (no writes).' + required: false + bucket: + description: 'S3 bucket / Bunny storage zone. Defaults to MAVEN_BUNNY_RW_USERNAME.' + required: false + prefix: + description: 'Key prefix inside the bucket.' + required: false + default: 'libs-release' + endpoint: + description: 'S3 endpoint URL.' + required: false + default: 'https://ny-s3.storage.bunnycdn.com' + region: + description: 'S3 signing region.' + required: false + default: 'ny' + access-key-id: + description: 'S3 access key id (Bunny storage-zone name).' + required: true + secret-access-key: + description: 'S3 secret access key (Bunny storage-zone password).' + required: true + checksums: + description: 'Upload .sha1/.md5 checksums alongside the artifacts.' + required: false + default: 'true' + github-token: + description: 'GitHub Token' + required: true + artifact-run-id: + default: ${{ github.run_id }} + description: 'The run id of the core artifacts' + +runs: + using: "composite" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: ./.github/actions/core-cicd/maven-job + with: + github-token: ${{ inputs.github-token }} + stage-name: "Deploy Artifacts Validate" + maven-args: "validate" # No build needed; only restore the maven-repo artifact + artifacts-from: ${{ inputs.artifact-run-id }} + + - name: 'Extract project version' + if: ${{ inputs.version == '' || inputs.version == null }} + id: maven-version + shell: bash + run: | + echo "::group::Extract project version" + version=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "::notice::version: $version" + echo "version=$version" >> $GITHUB_OUTPUT + echo "::endgroup::" + + - name: 'Deploy artifacts to S3' + shell: bash + env: + MAVEN_BUNNY_RW_USERNAME: ${{ inputs.access-key-id }} + MAVEN_BUNNY_RW_PASSWORD: ${{ inputs.secret-access-key }} + MAVEN_S3_ENDPOINT: ${{ inputs.endpoint }} + MAVEN_S3_REGION: ${{ inputs.region }} + MAVEN_S3_BUCKET: ${{ inputs.bucket }} + MAVEN_S3_PREFIX: ${{ inputs.prefix }} + VERSION: ${{ inputs.version || steps.maven-version.outputs.version }} + PRJ_MODULES: ${{ inputs.modules }} + EXCLUDE_EXT: ${{ inputs.exclude-ext }} + DRY_RUN_MODE: ${{ inputs.dry-run }} + CHECKSUMS: ${{ inputs.checksums }} + run: | + echo "::group::Deploy Artifacts to S3" + args=(maven --version "$VERSION" --exclude-ext "$EXCLUDE_EXT") + + if [[ -n "${PRJ_MODULES:-}" ]]; then + args+=(--modules "$PRJ_MODULES") + fi + if [[ -n "${MAVEN_S3_BUCKET:-}" ]]; then + args+=(--bucket "$MAVEN_S3_BUCKET") + fi + if [[ "${DRY_RUN_MODE:-false}" == "true" ]]; then + args+=(--dry-run) + fi + if [[ "${CHECKSUMS:-true}" != "true" ]]; then + args+=(--no-checksums) + fi + + ./.github/scripts/publish-to-s3/publish.sh "${args[@]}" + echo "::endgroup::" \ No newline at end of file diff --git a/.github/actions/core-cicd/deployment/deploy-jfrog/README.md b/.github/actions/core-cicd/deployment/deploy-jfrog/README.md deleted file mode 100644 index ef001e49b9de..000000000000 --- a/.github/actions/core-cicd/deployment/deploy-jfrog/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Deploy Artifact to Artifactory GitHub Action - -This GitHub Action is used to deploy the dotCMS artifacts to Artifactory. It includes steps to extract project modules, version, and repository details from the POM file and deploy artifacts to the specified Artifactory repository. - -## Inputs - -- **modules**: (Optional) Comma-separated list of modules to deploy. If not provided, it will be extracted from the project POM file. -- **exclude-ext**: (Optional) Comma-separated list of extensions to be excluded. -- **dry-run**: (Optional) Enable dry-run mode. If it sets `true` the action will not deploy artifacts to Artifactory. -- **version**: (Optional) Version of the artifacts to deploy. If not provided, it will be extracted from the project POM file. -- **artifactory-repository**: (Optional) Artifactory Repository. If not provided, it will be extracted from the project POM file. -- **artifactory-url**: (Optional) Artifactory URL. If not provided, it will be extracted from the project POM file. -- **artifactory-access-token**: (Optional) Artifactory Access Token. It takes precedence over `artifactory-username` and - `artifactory-password`. -- **artifactory-username**: (Optional) Artifactory username. -- **artifactory-password**: (Optional) Artifactory password. -- **github-token**: (Required) GitHub Token. - -## Outputs - -This action does not produce explicit outputs but uses extracted values within the workflow. - -## Example Usage - -```yaml -name: Deploy Artifact to Artifactory - -on: - push: - branches: - - main - -jobs: - deploy: - runs-on: ubuntu-${{ vars.UBUNTU_RUNNER_VERSION || '24.04' }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Deploy Artifact - uses: ./.github/actions/core-cicd/deployment/deploy-jfrog - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - artifactory-access-token: ${{ secrets.ARTIFACTORY_ACCESS_TOKEN }} diff --git a/.github/actions/core-cicd/deployment/deploy-jfrog/action.yml b/.github/actions/core-cicd/deployment/deploy-jfrog/action.yml deleted file mode 100644 index f3df12c28dfa..000000000000 --- a/.github/actions/core-cicd/deployment/deploy-jfrog/action.yml +++ /dev/null @@ -1,218 +0,0 @@ -name: 'Deploy Artifact to Artifactory' -description: 'Deploy the dotCMS artifacts to Artifactory' -inputs: - modules: - description: 'Comma-separated list of modules to deploy, if not provided, will be extracted from the project POM file.' - required: false - exclude-ext: - description: 'Comma-separated list of extensions to be excluded.' - required: false - dry-run: - description: 'Enable dry-run mode' - required: false - version: - description: 'Version of the artifacts to deploy, if not provided, will be extracted from the project POM file.' - required: false - artifactory-repository: - description: 'Artifactory Repository, if not provided, will be extracted from the project POM file.' - required: false - artifactory-url: - description: 'Artifactory URL, if not provided, will be extracted from the project POM file.' - required: false - artifactory-access-token: - description: 'Artifactory Access Token. It takes precedence over artifactory-username and artifactory-password.' - required: false - artifactory-username: - description: 'Artifactory username' - required: false - artifactory-password: - description: 'Artifactory password' - required: false - github-token: - description: 'GitHub Token' - required: true - artifact-run-id: - default: ${{ github.run_id }} - description: 'The run id of the core artifacts' - -runs: - using: "composite" - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - uses: ./.github/actions/core-cicd/maven-job - with: - github-token: ${{ inputs.github-token }} - stage-name: "Deploy Artifacts Validate" - maven-args: "validate" # We don't need to build just get the repo and use validate to check everything exists - artifacts-from: ${{ inputs.artifact-run-id }} - - - uses: jfrog/setup-jfrog-cli@v4 - - - name: 'Extract project modules' - if: ${{ inputs.modules == '' || inputs.modules == null }} - id: maven-modules - run: | - echo "::group::Extract project modules" - POM_PATH="${{ inputs.pom-path }}" - artifact_ids="" - - # Function to extract artifact ID from POM file - extract_artifact_id() { - mvn -f "$1" help:evaluate -Dexpression=project.artifactId -q -DforceStdout 2>/dev/null || true - } - - # Function to get submodules from POM file - get_submodules() { - mvn -f "$1" help:evaluate -Dexpression=project.modules -q -DforceStdout 2>/dev/null | sed 's/<[^>]*>//g' | tr -d '\n' | tr -s ' ' | tr ' ' ',' || true - } - - # Function to process each module and its submodules - process_module() { - - local module_path="$1" - local module_pom="$module_path/pom.xml" - - if [ -f "$module_pom" ]; then - artifactId=$(extract_artifact_id "$module_pom") - - if [[ -z "$artifactId" || "$artifactId" == *"[ERROR]"* ]]; then - artifact_ids+="$module_path," - else - artifact_ids+="$artifactId," - fi - - submodules=$(get_submodules "$module_pom") - if [[ -z "$submodules" || "$submodules" == *"[ERROR]"* ]]; then - return - fi - - submodules=$(echo $submodules | sed 's/^,//' || true) - - # Process each module and submodule recursively - for submodule in ${submodules//,/ }; do - process_module "$module_path/$submodule" - done - else - artifact_ids+="$module_path," - fi - } - - modules=$(mvn -f "$POM_PATH" help:evaluate -Dexpression=project.modules -q -DforceStdout 2>/dev/null | sed 's/<[^>]*>//g' | tr -d '\n' | tr -s ' ' | tr ' ' ',' || true) - modules=$(echo $modules | sed 's/^,//' || true) - - for module in ${modules//,/ }; do - process_module "$module" - done - - artifact_ids=$(echo $artifact_ids | sed 's/,$/,dotcms-root/' || true) - echo "::notice::modules: $artifact_ids" - echo "artifact-ids=${artifact_ids}" >> $GITHUB_OUTPUT - echo "::endgroup::" - shell: bash - - - name: 'Extract project version' - if: ${{ inputs.version == '' || inputs.version == null }} - id: maven-version - run: | - echo "::group::Extract project version" - version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - echo "::notice::version: $version" - echo "version=$version" >> $GITHUB_OUTPUT - echo "::endgroup::" - shell: bash - - - name: 'Extract project repo' - if: ${{ inputs.artifactory-repository == '' || inputs.artifactory-repository == null }} - id: maven-artifact-repository - env: - VERSION: ${{ inputs.version || steps.maven-version.outputs.version }} - run: | - echo "::group::Extract artifact repository" - echo "" - - POM_FILE="pom.xml" - - if [[ ! -f "$POM_FILE" ]]; then - echo "$POM_FILE file is missing." - exit 1 - fi - - # Determine the node type based on the version - if [[ "$VERSION" =~ [Ss][Nn][Aa][Pp][Ss][Hh][Oo][Tt] ]]; then - NODE_TYPE="snapshotRepository" - else - NODE_TYPE="repository" - fi - - echo "NODE_TYPE=$NODE_TYPE" - - # Extract the repository URL from the POM file - REPO=$(mvn -f "$POM_FILE" -N help:evaluate -Dexpression=project.distributionManagement.$NODE_TYPE.url -q -DforceStdout 2>/dev/null) - - if [[ "$REPO" =~ ^(https://[^/]+/[^/]+)/(.+)$ ]]; then - SERVER_URL="${BASH_REMATCH[1]}" - REPO_NAME="${BASH_REMATCH[2]}" - - echo "url=$SERVER_URL" >> $GITHUB_OUTPUT - echo "repo=$REPO_NAME" >> $GITHUB_OUTPUT - else - echo "Invalid REPO format" - exit 1 - fi - - echo "::notice::ARTIFACTORY_URL=$SERVER_URL" - echo "::notice::ARTIFACTORY_REPO=$REPO_NAME" - - echo "::endgroup::" - shell: bash - - - name: 'JFrog CLI context' - env: - ARTIFACTORY_URL: ${{ inputs.artifactory-url || steps.maven-artifact-repository.outputs.url }} - ARTIFACTORY_ACCESS_TOKEN: ${{ inputs.artifactory-access-token }} - run: | - echo "::group::JFrog CLI context" - jf rt ping --url=$ARTIFACTORY_URL --access-token=$ARTIFACTORY_ACCESS_TOKEN - echo "::endgroup::" - shell: bash - - - name: 'Deploy artifacts' - env: - ARTIFACTORY_ACCESS_TOKEN: ${{ inputs.artifactory-access-token }} - ARTIFACTORY_URL: ${{ inputs.artifactory-url || steps.maven-artifact-repository.outputs.url }} - ARTIFACTORY_USERNAME: ${{ inputs.artifactory-username }} - ARTIFACTORY_PASSWORD: ${{ inputs.artifactory-password }} - ARTIFACTORY_REPO: ${{ inputs.artifactory-repository || steps.maven-artifact-repository.outputs.repo }} - VERSION: ${{ inputs.version || steps.maven-version.outputs.version }} - PRJ_MODULES: ${{ inputs.modules || steps.maven-modules.outputs.artifact-ids }} - EXCLUDE_EXT: ${{ inputs.exclude-ext || 'repositories,excludeext' }} - DRY_RUN_MODE: ${{ inputs.dry-run }} - run: | - echo "::group::Deploy Artifacts" - - MAVEN_DIR=${HOME}/.m2/repository - cd $MAVEN_DIR - - # Replace ',' with '|' - MODULES=$(echo $PRJ_MODULES | sed 's/,/|/g') - # Replace '.' with '\.' - ESCAPED_VERSION=$(echo $VERSION | sed 's/\./\\./g') - # Replace ',' with '|' - EXCLUDE=$(echo $EXCLUDE_EXT | sed 's/,/|/g') - - if [[ $DRY_RUN_MODE == true ]]; then - DRY_RUN='--dry-run' - fi - - # Deploy the artifacts using JFrog CLI with appropriate credentials and options - if [[ -n "$ARTIFACTORY_ACCESS_TOKEN" ]]; then - jf rt u "com/dotcms/(${MODULES})/${ESCAPED_VERSION}" $ARTIFACTORY_REPO --url=$ARTIFACTORY_URL --access-token=$ARTIFACTORY_ACCESS_TOKEN --flat=false --exclusions ".*\.(${EXCLUDE})$" --recursive --regexp $DRY_RUN - elif [[ -n "$ARTIFACTORY_USERNAME" && -n "$ARTIFACTORY_PASSWORD" ]]; then - jf rt u "com/dotcms/(${MODULES})/${ESCAPED_VERSION}" $ARTIFACTORY_REPO --url=$ARTIFACTORY_URL --user=$ARTIFACTORY_USERNAME --password=$ARTIFACTORY_PASSWORD --flat=false --exclusions ".*\.(${EXCLUDE})$" --recursive --regexp $DRY_RUN - else - echo "Credentials not provided." - exit 1 - fi - echo "::endgroup::" - shell: bash diff --git a/.github/scripts/publish-to-s3/README.md b/.github/scripts/publish-to-s3/README.md new file mode 100644 index 000000000000..7258e121101b --- /dev/null +++ b/.github/scripts/publish-to-s3/README.md @@ -0,0 +1,86 @@ +# publish-to-s3 + +Helper that publishes dotCMS build artifacts to the BunnyCDN S3-compatible +storage zone. It replaces the former Artifactory (`repo.dotcms.com`) +deployments, so every CI/CD publish path goes through one place instead of +duplicating endpoint/credential wiring in each workflow. + +## Usage + +```bash +# Publish one version of every com/dotcms module installed in ~/.m2/repository +.github/scripts/publish-to-s3/publish.sh maven --version 26.09.14-01 + +# Publish a single file to an explicit key +.github/scripts/publish-to-s3/publish.sh file \ + --source ./starter/20260910.zip \ + --key com/dotcms/starter/20260910/starter-20260910.zip + +# Preview without writing +.github/scripts/publish-to-s3/publish.sh maven --version 26.09.14-01 --dry-run +``` + +Both modes publish `.sha1`/`.md5` sidecars beside the artifacts so consumers +never hit `Checksum validation failed, no checksums available`; pass +`--no-checksums` to skip that. `maven` also (re)generates `maven-metadata.xml` +and its sidecars. + +## Configuration + +CLI flags take precedence over these environment variables: + +| Variable | Default | Purpose | +| --- | --- | --- | +| `MAVEN_BUNNY_RW_USERNAME` | — | Bunny storage-zone name / S3 access key id | +| `MAVEN_BUNNY_RW_PASSWORD` | — | Bunny storage-zone password / S3 secret key | +| `MAVEN_S3_BUCKET` | `$MAVEN_BUNNY_RW_USERNAME` | Bucket (storage zone) | +| `MAVEN_S3_PREFIX` | `libs-release` | Key prefix inside the bucket | +| `MAVEN_S3_ENDPOINT` | `https://ny-s3.storage.bunnycdn.com` | S3 endpoint | +| `MAVEN_S3_REGION` | `ny` | S3 signing region | +| `MAVEN_S3_PUBLIC_URL` | `https://dotcms-repo.b-cdn.net` | Public CDN base used for printed URLs | +| `MAVEN_REPO_DIR` | `$HOME/.m2/repository` | Local Maven repository | +| `MAVEN_S3_EXCLUDE_EXT` | `repositories,excludeext` | Extra extensions to skip | +| `MAVEN_S3_UPDATE_METADATA` | `true` | Regenerate `maven-metadata.xml` | +| `MAVEN_S3_CHECKSUMS` | `true` | Upload `.sha1`/`.md5` checksums | +| `MAVEN_S3_ALLOW_SNAPSHOTS` | `false` | Publish `-SNAPSHOT` versions | + +BunnyCDN convention: the S3 access key id **is** the storage-zone name, so the +bucket defaults to `MAVEN_BUNNY_RW_USERNAME`. + +## What `maven` does + +1. Walks `$MAVEN_REPO_DIR/com/dotcms/*/` and uploads each subtree to + `s3://$BUCKET/$PREFIX/com/dotcms///`. S3 has no real + folders; nested key prefixes are created implicitly, so the layout matches + the old Artifactory `libs-release` layout. +2. Uploads `.sha1`/`.md5` checksums for the primary artifacts (`.pom`, `.jar`, + `.zip`, `.war`, `.aar`, `.module`). +3. Regenerates `com/dotcms//maven-metadata.xml` from the versions + already present in the bucket plus the one just uploaded. Artifactory used to + do this automatically and consumers (for example the dotCLI action) read it, + so a plain file copy is not enough. + +Step 3 is best-effort: a metadata failure logs a warning but does not fail a +publish whose artifacts are already in place. + +### Snapshots + +`maven` **refuses to publish `-SNAPSHOT` versions** by default (logs a warning +and exits 0). dotCMS does not consume shared snapshots, and publishing one to +`libs-release` would make the artifact-level `maven-metadata.xml` `` a +snapshot, breaking the release lookup the CLI action performs. Pass +`--allow-snapshots` to override. + +## Resulting URLs + +With the defaults, an artifact published for version `26.09.14-01` is served at: + +``` +https://dotcms-repo.b-cdn.net/libs-release/com/dotcms/dotcms-core/26.09.14-01/dotcms-core-26.09.14-01.jar +``` + +> **Prefix note:** the request that introduced this migration said the repo +> starts under `/libs-releases`, but the example URLs (and the retired +> Artifactory `libs-release` repo) use `/libs-release`. The script defaults to +> `libs-release`; set `MAVEN_S3_PREFIX` to override if the storage zone really +> uses the plural form. diff --git a/.github/scripts/publish-to-s3/publish.sh b/.github/scripts/publish-to-s3/publish.sh new file mode 100755 index 000000000000..b33a8be69f53 --- /dev/null +++ b/.github/scripts/publish-to-s3/publish.sh @@ -0,0 +1,431 @@ +#!/usr/bin/env bash +# +# publish-to-s3/publish.sh +# +# Publishes build artifacts to the dotCMS BunnyCDN S3-compatible storage zone. +# This replaces the Artifactory deployments (repo.dotcms.com). +# +# Two sub-commands: +# +# maven Publish one version of every com/dotcms module found in a local +# Maven repository (~/.m2/repository), preserving the +# groupId/artifactId/version layout, and (re)generate the +# maven-metadata.xml files Artifactory used to create. +# +# file Upload a single file to an explicit key. Used for artifacts that do +# not live in a Maven repository (e.g. the starter zips). +# +# BunnyCDN convention: the S3 access key id IS the storage-zone name, so when +# MAVEN_S3_BUCKET is not set it defaults to MAVEN_BUNNY_RW_USERNAME. +# +# Environment (all optional, CLI flags take precedence): +# MAVEN_BUNNY_RW_USERNAME Bunny storage-zone name / S3 access key id +# MAVEN_BUNNY_RW_PASSWORD Bunny storage-zone password / S3 secret key +# MAVEN_S3_BUCKET Bucket (storage zone). Defaults to the username. +# MAVEN_S3_PREFIX Key prefix. Default: libs-release +# MAVEN_S3_ENDPOINT Default: https://ny-s3.storage.bunnycdn.com +# MAVEN_S3_REGION Default: ny +# MAVEN_S3_PUBLIC_URL Public CDN base, used for printed download URLs. +# Default: https://dotcms-repo.b-cdn.net +# MAVEN_REPO_DIR Default: $HOME/.m2/repository +# MAVEN_S3_EXCLUDE_EXT Extra extensions to skip. Default: repositories,excludeext +# MAVEN_S3_UPDATE_METADATA Default: true +# MAVEN_S3_CHECKSUMS Default: true +# MAVEN_S3_ALLOW_SNAPSHOTS Default: false (shared snapshots are not consumed) +# +# Exit codes: 0 success, 1 error. + +set -euo pipefail + +S3_ENDPOINT="${MAVEN_S3_ENDPOINT:-https://ny-s3.storage.bunnycdn.com}" +S3_REGION="${MAVEN_S3_REGION:-ny}" +S3_PREFIX="${MAVEN_S3_PREFIX:-libs-release}" +S3_BUCKET="${MAVEN_S3_BUCKET:-${MAVEN_BUNNY_RW_USERNAME:-}}" +S3_PUBLIC_URL="${MAVEN_S3_PUBLIC_URL:-https://dotcms-repo.b-cdn.net}" +MAVEN_REPO_DIR="${MAVEN_REPO_DIR:-$HOME/.m2/repository}" +EXCLUDE_EXT="${MAVEN_S3_EXCLUDE_EXT:-repositories,excludeext}" +UPDATE_METADATA="${MAVEN_S3_UPDATE_METADATA:-true}" +CHECKSUMS="${MAVEN_S3_CHECKSUMS:-true}" +ALLOW_SNAPSHOTS="${MAVEN_S3_ALLOW_SNAPSHOTS:-false}" + +# Resolve S3 credentials. Org secrets are exposed under the MAVEN_BUNNY_* names; +# the AWS CLI reads the standard AWS_* variables. These must be exported so the +# `aws` child process inherits them. +# +# Bunny credentials are authoritative for this repository. AWS credentials may +# already be in the environment (the release job configures AWS creds for the +# Javadoc upload before this runs), so override them rather than only filling in +# blanks, and drop any inherited session token or Bunny rejects the request. +if [[ -n "${MAVEN_BUNNY_RW_USERNAME:-}" ]]; then + AWS_ACCESS_KEY_ID="$MAVEN_BUNNY_RW_USERNAME" +fi +if [[ -n "${MAVEN_BUNNY_RW_PASSWORD:-}" ]]; then + AWS_SECRET_ACCESS_KEY="$MAVEN_BUNNY_RW_PASSWORD" +fi +unset AWS_SESSION_TOKEN AWS_SECURITY_TOKEN +export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY +export AWS_DEFAULT_REGION="$S3_REGION" + +log() { printf '::notice::%s\n' "$*"; } +warn() { printf '::warning::%s\n' "$*" >&2; } +die() { printf '::error::%s\n' "$*" >&2; exit 1; } + +# Temp dirs are cleaned up once, on exit. A per-function RETURN trap would fire +# on every function return (including the aws_s3 helper) and delete the staging +# directory before it has been uploaded. +CLEANUP_DIRS=() +cleanup() { + local d + for d in ${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}; do + [[ -n "$d" ]] && rm -rf "$d" + done +} +trap cleanup EXIT + +usage() { + cat <<'EOF' +Usage: + publish.sh maven --version [options] + publish.sh file --source --key [options] + +Common options: + --bucket S3 bucket / storage zone (default: $MAVEN_BUNNY_RW_USERNAME) + --prefix Key prefix (default: libs-release) + --endpoint S3 endpoint (default: https://ny-s3.storage.bunnycdn.com) + --region S3 signing region (default: ny) + --dry-run Print what would be uploaded; do not write + --no-metadata (maven) Do not (re)generate maven-metadata.xml + --no-checksums Do not upload .sha1/.md5 checksums + --allow-snapshots (maven) Publish a -SNAPSHOT version (default: refuse) + +maven options: + --version Version subtree to publish (required) + --repo-dir Local Maven repository (default: $HOME/.m2/repository) + --modules Restrict to these artifactIds (default: every com/dotcms module + that has a directory for ) + --exclude-ext Extra file extensions to skip (default: repositories,excludeext) + +file options: + --source File to upload (required) + --key Destination key relative to the prefix (required) +EOF +} + +# Thin wrapper so every call shares the endpoint, region and credentials. +aws_s3() { + aws --endpoint-url "$S3_ENDPOINT" --region "$S3_REGION" s3 "$@" +} + +require_credentials() { + if [[ "${DRY_RUN:-false}" == "true" ]]; then + # Dry-runs are useful standalone (no secrets), but aws still needs a bucket + # to build a valid s3:// destination, so fall back to a placeholder. + : "${S3_BUCKET:=dry-run-bucket}" + return 0 + fi + [[ -n "$S3_BUCKET" ]] || die "Bucket is empty. Set MAVEN_S3_BUCKET or MAVEN_BUNNY_RW_USERNAME." + [[ -n "${AWS_ACCESS_KEY_ID:-}" ]] || die "Missing MAVEN_BUNNY_RW_USERNAME / AWS_ACCESS_KEY_ID." + [[ -n "${AWS_SECRET_ACCESS_KEY:-}" ]] || die "Missing MAVEN_BUNNY_RW_PASSWORD / AWS_SECRET_ACCESS_KEY." +} + +# Emits "key=value" lines to $GITHUB_OUTPUT when running inside Actions. +emit_output() { + local name="$1" value="$2" + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + printf '%s=%s\n' "$name" "$value" >> "$GITHUB_OUTPUT" + fi +} + +# --------------------------------------------------------------------------- +# maven +# --------------------------------------------------------------------------- + +# extra_args are bash-glob exclusions derived from --exclude-ext. +build_exclude_args() { + EXCLUDE_ARGS=( + --exclude "*.lastUpdated" + --exclude "resolver-status.properties" + --exclude "maven-metadata-local.xml" + --exclude "*.sha1" + --exclude "*.md5" + --exclude "*.sha256" + ) + if [[ -n "$EXCLUDE_EXT" ]]; then + local ext + IFS=',' read -ra _exts <<< "$EXCLUDE_EXT" + for ext in "${_exts[@]}"; do + [[ -n "$ext" ]] && EXCLUDE_ARGS+=(--exclude "*.$ext") + done + fi +} + +# Uploads .sha1/.md5 checksums for the primary artifacts of one version dir. +upload_checksums() { + local src_dir="$1" dest="$2" tmp rel + tmp="$(mktemp -d)" + CLEANUP_DIRS+=("$tmp") + + while IFS= read -r -d '' rel; do + mkdir -p "$tmp/$(dirname "$rel")" + sha1sum "$src_dir/$rel" | awk '{print $1}' > "$tmp/$rel.sha1" + md5sum "$src_dir/$rel" | awk '{print $1}' > "$tmp/$rel.md5" + done < <(cd "$src_dir" && find . -type f \ + \( -name '*.pom' -o -name '*.jar' -o -name '*.zip' \ + -o -name '*.war' -o -name '*.aar' -o -name '*.module' \) -print0) + + local args=(--recursive --no-progress) + if [[ "${DRY_RUN:-false}" == "true" ]]; then + args+=(--dryrun) + fi + aws_s3 cp "$tmp" "$dest" "${args[@]}" +} + +# Rebuilds com/dotcms//maven-metadata.xml from the versions that +# already exist in the bucket plus the version just uploaded. Best-effort: a +# failure here must not fail the publish, because the artifacts themselves are +# already in place. +update_artifact_metadata() { + local artifact="$1" version="$2" ts + ts="$(date -u +%Y%m%d%H%M%S)" + local base="$S3_PREFIX/com/dotcms/$artifact" + local tmp versions latest release v plain listing + + tmp="$(mktemp -d)" + CLEANUP_DIRS+=("$tmp") + + # `aws s3 ls` prints directory entries as `PRE /` and file entries as + # `