diff --git a/.github/actions/mirror-maven-jar/README.md b/.github/actions/mirror-maven-jar/README.md
new file mode 100644
index 0000000..b1e2984
--- /dev/null
+++ b/.github/actions/mirror-maven-jar/README.md
@@ -0,0 +1,124 @@
+# Mirror Public JAR to CodeArtifact
+
+A composite GitHub Action that mirrors a single pre-built public JAR into AWS CodeArtifact, skipping the upload if that version already exists. The JAR can be downloaded from a remote URL or supplied as a local file.
+
+- [How-to guides](#how-to-guides)
+- [Reference](#reference)
+- [Explanation](#explanation)
+
+## How-to guides
+
+### Mirror a public JAR from a URL
+
+`setup-codeartifact` must run earlier in the **same job** to configure AWS
+credentials and Maven `settings.xml`.
+
+```yaml
+jobs:
+ mirror:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write # required for OIDC role assumption
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Authenticate with CodeArtifact
+ uses: OvertureMaps/workflows/.github/actions/setup-codeartifact@main
+ with:
+ aws-role-arn: arn:aws:iam::123456789012:role/codeartifact-publisher
+ codeartifact-domain: overture
+ codeartifact-domain-owner: "123456789012"
+ codeartifact-repository: maven-third-party
+
+ - name: Mirror libphonenumber
+ uses: OvertureMaps/workflows/.github/actions/mirror-maven-jar@main
+ with:
+ group-id: com.googlecode.libphonenumber
+ artifact-id: libphonenumber
+ version: "8.13.48"
+ jar-url: https://repo1.maven.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.48/libphonenumber-8.13.48.jar
+ codeartifact-domain: overture
+ codeartifact-domain-owner: "123456789012"
+ codeartifact-repository: maven-third-party
+ aws-region: us-west-2
+```
+
+
+Mirror a local JAR file
+
+Use `jar-path` instead of `jar-url` for a JAR already on disk:
+
+```yaml
+- name: Mirror local JAR
+ uses: OvertureMaps/workflows/.github/actions/mirror-maven-jar@main
+ with:
+ group-id: com.example
+ artifact-id: my-lib
+ version: "1.0.0"
+ jar-path: ./vendor/my-lib-1.0.0.jar
+ codeartifact-domain: overture
+ codeartifact-domain-owner: "123456789012"
+ codeartifact-repository: maven-third-party
+ aws-region: us-west-2
+```
+
+
+
+> Pin to a commit SHA rather than `@main` for reproducible builds, e.g.
+> `uses: OvertureMaps/workflows/.github/actions/mirror-maven-jar@`.
+
+## Reference
+
+### Inputs
+
+- `group-id` (**required**): Maven groupId (e.g. `com.googlecode.libphonenumber`).
+- `artifact-id` (**required**): Maven artifactId (e.g. `libphonenumber`).
+- `version` (**required**): Maven version to mirror (e.g. `8.13.48`).
+- `jar-url` (optional): Remote URL to download the JAR from. Mutually exclusive with `jar-path`.
+- `jar-path` (optional): Local path to an existing JAR file. Mutually exclusive with `jar-url`.
+- `codeartifact-domain` (**required**): CodeArtifact domain name.
+- `codeartifact-domain-owner` (**required**): AWS account ID that owns the CodeArtifact domain.
+- `codeartifact-repository` (**required**): CodeArtifact repository name.
+- `aws-region` (**required**): AWS region where CodeArtifact is hosted.
+
+### Outputs
+
+This action has no outputs. It either uploads the JAR or skips when the version
+already exists.
+
+### Prerequisites
+
+This action neither authenticates nor installs a toolchain, so the job must
+provide both before calling it:
+
+- **A JDK + Maven on the runner.** The action invokes `mvn deploy:deploy-file`,
+ so `mvn` must be on `PATH` — typically via `actions/setup-java` (with
+ `distribution`/`java-version`), which also provisions Maven. GitHub-hosted
+ runners include Maven by default; minimal or self-hosted runners may not.
+- **`setup-codeartifact` called earlier in the same job.** This action has no
+ internal authentication step — it relies on the AWS credentials and
+ `~/.m2/settings.xml` that `setup-codeartifact` configures.
+
+## Explanation
+
+### Idempotent mirroring
+
+The action first calls `aws codeartifact describe-package-version`. If the
+version already exists, the download and publish steps are skipped, so reruns
+are safe and cheap. Only missing versions are fetched and deployed via
+`mvn deploy:deploy-file` with a generated pom.
+
+### URL vs local file
+
+`jar-url` and `jar-path` are mutually exclusive. With `jar-url`, the JAR is
+downloaded to `_downloaded.jar` and published; with `jar-path`, the existing
+file is published in place. When the version is not yet mirrored, a validation
+step enforces that exactly one of the two is provided — supplying neither or
+both fails fast with a clear error rather than a confusing Maven failure.
+
+### Why it has no auth step
+
+Unlike `publish-maven-to-codeartifact`, this action assumes authentication already
+happened in the same job via `setup-codeartifact`. This lets a single job mirror
+many JARs after one authentication step, avoiding repeated OIDC role assumptions.
diff --git a/.github/actions/mirror-maven-jar/action.yml b/.github/actions/mirror-maven-jar/action.yml
new file mode 100644
index 0000000..7e9754f
--- /dev/null
+++ b/.github/actions/mirror-maven-jar/action.yml
@@ -0,0 +1,112 @@
+name: Mirror Public JAR to CodeArtifact
+description: >
+ Mirrors a single pre-built public JAR into AWS CodeArtifact, skipping if the
+ version already exists. Supports downloading from a remote URL or using a
+ local file.
+ Prerequisite: setup-codeartifact must have been called in the same job to
+ configure AWS credentials and Maven settings.
+
+inputs:
+ group-id:
+ description: Maven groupId (e.g. com.googlecode.libphonenumber)
+ required: true
+ artifact-id:
+ description: Maven artifactId (e.g. libphonenumber)
+ required: true
+ version:
+ description: Maven version to mirror (e.g. 8.13.48)
+ required: true
+ jar-url:
+ description: Remote URL to download the JAR from (mutually exclusive with jar-path)
+ required: false
+ default: ""
+ jar-path:
+ description: Local path to an existing JAR file (mutually exclusive with jar-url)
+ required: false
+ default: ""
+ codeartifact-domain:
+ description: CodeArtifact domain name
+ required: true
+ codeartifact-domain-owner:
+ description: AWS account ID that owns the CodeArtifact domain
+ required: true
+ codeartifact-repository:
+ description: CodeArtifact repository name
+ required: true
+ aws-region:
+ description: AWS region where CodeArtifact is hosted
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ - name: Check if package already exists in CodeArtifact
+ id: check
+ shell: bash
+ env:
+ CODEARTIFACT_DOMAIN: ${{ inputs.codeartifact-domain }}
+ CODEARTIFACT_DOMAIN_OWNER: ${{ inputs.codeartifact-domain-owner }}
+ CODEARTIFACT_REPOSITORY: ${{ inputs.codeartifact-repository }}
+ GROUP_ID: ${{ inputs.group-id }}
+ ARTIFACT_ID: ${{ inputs.artifact-id }}
+ VERSION: ${{ inputs.version }}
+ AWS_REGION: ${{ inputs.aws-region }}
+ run: |
+ if aws codeartifact describe-package-version \
+ --domain "$CODEARTIFACT_DOMAIN" \
+ --domain-owner "$CODEARTIFACT_DOMAIN_OWNER" \
+ --repository "$CODEARTIFACT_REPOSITORY" \
+ --format maven \
+ --namespace "$GROUP_ID" \
+ --package "$ARTIFACT_ID" \
+ --package-version "$VERSION" \
+ --region "$AWS_REGION" > /dev/null 2>&1; then
+ echo "exists=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "exists=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Validate JAR source inputs
+ if: steps.check.outputs.exists == 'false'
+ shell: bash
+ env:
+ JAR_URL: ${{ inputs.jar-url }}
+ JAR_PATH: ${{ inputs.jar-path }}
+ run: |
+ if [ -n "$JAR_URL" ] && [ -n "$JAR_PATH" ]; then
+ echo "Error: provide only one of jar-url or jar-path, not both." >&2
+ exit 1
+ fi
+ if [ -z "$JAR_URL" ] && [ -z "$JAR_PATH" ]; then
+ echo "Error: version is not yet mirrored; provide exactly one of jar-url or jar-path." >&2
+ exit 1
+ fi
+
+ - name: Download JAR from remote URL
+ if: steps.check.outputs.exists == 'false' && inputs.jar-url != ''
+ shell: bash
+ env:
+ JAR_URL: ${{ inputs.jar-url }}
+ run: curl -fsSL -o _downloaded.jar "$JAR_URL"
+
+ - name: Publish JAR to CodeArtifact
+ if: steps.check.outputs.exists == 'false'
+ shell: bash
+ env:
+ CODEARTIFACT_DOMAIN: ${{ inputs.codeartifact-domain }}
+ GROUP_ID: ${{ inputs.group-id }}
+ ARTIFACT_ID: ${{ inputs.artifact-id }}
+ VERSION: ${{ inputs.version }}
+ JAR_FILE: ${{ inputs.jar-path != '' && inputs.jar-path || '_downloaded.jar' }}
+ REPO_URL: "https://${{ inputs.codeartifact-domain }}-${{ inputs.codeartifact-domain-owner }}.d.codeartifact.${{ inputs.aws-region }}.amazonaws.com/maven/${{ inputs.codeartifact-repository }}/"
+ run: |
+ mvn deploy:deploy-file -ntp \
+ -Dfile="$JAR_FILE" \
+ -DgroupId="$GROUP_ID" \
+ -DartifactId="$ARTIFACT_ID" \
+ -Dversion="$VERSION" \
+ -Dpackaging=jar \
+ -DgeneratePom=true \
+ -DrepositoryId="$CODEARTIFACT_DOMAIN" \
+ -Durl="$REPO_URL" \
+ --settings ~/.m2/settings.xml
diff --git a/.github/actions/publish-maven-to-codeartifact/README.md b/.github/actions/publish-maven-to-codeartifact/README.md
new file mode 100644
index 0000000..d1dd31c
--- /dev/null
+++ b/.github/actions/publish-maven-to-codeartifact/README.md
@@ -0,0 +1,124 @@
+# Build and Publish Maven Project to CodeArtifact
+
+A composite GitHub Action that builds a Maven project from source and publishes its artifacts to AWS CodeArtifact. It sets up the JDK, authenticates with CodeArtifact, optionally overrides the version for dev builds, deploys, and prints the shaded JAR manifest.
+
+- [How-to guides](#how-to-guides)
+- [Reference](#reference)
+- [Explanation](#explanation)
+
+## How-to guides
+
+### Publish a release build
+
+```yaml
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write # required for OIDC role assumption
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build and publish to CodeArtifact
+ uses: OvertureMaps/workflows/.github/actions/publish-maven-to-codeartifact@main
+ with:
+ aws-role-arn: arn:aws:iam::123456789012:role/codeartifact-publisher
+ codeartifact-domain: overture
+ codeartifact-domain-owner: "123456789012"
+ codeartifact-repository: maven-releases
+```
+
+
+Publish a dev build with an overridden version
+
+Set `version` to publish a dev-named artifact. The pom version is overridden via
+`mvn versions:set` and the build runs with `-Denv=dev`.
+
+```yaml
+- name: Publish dev build
+ uses: OvertureMaps/workflows/.github/actions/publish-maven-to-codeartifact@main
+ with:
+ aws-role-arn: arn:aws:iam::123456789012:role/codeartifact-publisher
+ codeartifact-domain: overture
+ codeartifact-domain-owner: "123456789012"
+ codeartifact-repository: maven-snapshots
+ version: 1.4.0-dev-${{ github.run_number }}
+```
+
+
+
+> Pin to a commit SHA rather than `@main` for reproducible builds, e.g.
+> `uses: OvertureMaps/workflows/.github/actions/publish-maven-to-codeartifact@`.
+
+## Reference
+
+### Inputs
+
+- `aws-role-arn` (**required**): IAM role ARN to assume via OIDC for CodeArtifact access.
+- `aws-region` (optional): AWS region where CodeArtifact is hosted. Default `us-west-2`.
+- `codeartifact-domain` (**required**): CodeArtifact domain name.
+- `codeartifact-domain-owner` (**required**): AWS account ID that owns the CodeArtifact domain.
+- `codeartifact-repository` (**required**): CodeArtifact repository name.
+- `version` (optional): Artifact version override. When set, the pom version is overridden via `mvn versions:set` and the project is built with dev naming (`-Denv=dev`). When empty (default), the project's release version is published unchanged (`-Denv=release`).
+- `working-directory` (optional): Directory containing the Maven project (its `pom.xml` and `.java-version`). Defaults to the repository root (`.`). Set this to publish a project that lives in a subdirectory.
+- `commit` (optional): Commit SHA recorded as `build.commit` in the JAR manifest. Defaults to the checked-out workspace HEAD (`git rev-parse HEAD`), which matches the tree actually built — unlike `github.sha`, which is the merge-ref SHA on `pull_request` events. Override only if the build tree is not a git checkout.
+
+### Outputs
+
+This action has no outputs. It deploys the project's artifacts to CodeArtifact
+and prints the shaded JAR's `MANIFEST.MF` to the step log.
+
+### Permissions
+
+```yaml
+permissions:
+ id-token: write
+ contents: read
+```
+
+The assumed IAM role must allow `codeartifact:GetAuthorizationToken`,
+`sts:GetServiceBearerToken`, and the publish actions for the target repository.
+
+### Requirements
+
+- A `.java-version` file in the `working-directory` (consumed by `actions/setup-java`).
+- A Maven build that produces a `*-shaded.jar` under `target/`.
+- A git checkout (the default `actions/checkout` state) so `build.commit` can
+ default to the workspace HEAD. Pass the `commit` input if the tree is not a
+ git checkout.
+
+## Explanation
+
+### What it does
+
+The action runs four phases in one job step: JDK setup (Temurin, version from
+`.java-version`, Maven cache), CodeArtifact authentication (delegated to
+`setup-codeartifact`), an optional `mvn versions:set` when `version` is
+supplied, then `mvn clean deploy` with build provenance properties
+(`build.branch`, `build.commit`, `workflow.run_id`, `workflow.run_number`). Tests
+are skipped during deploy; run them in a separate CI step.
+
+### Commit provenance
+
+`build.commit` defaults to the checked-out workspace HEAD (`git rev-parse HEAD`),
+not `github.sha`. On `pull_request` events `github.sha` is the ephemeral
+merge-ref SHA, which diverges from the commit a caller actually checked out
+(e.g. `pull_request.head.sha`). Deriving it from HEAD keeps the manifest's commit
+in lock-step with the built tree regardless of the caller's checkout strategy;
+pass the `commit` input to override.
+
+### Release vs dev publishing
+
+The `version` input switches between two modes. Empty publishes the pom's
+release version unchanged with `-Denv=release`. A non-empty value overrides the
+pom version and builds with `-Denv=dev`, enabling dev-named snapshot publishing
+without editing the pom in source control.
+
+### Self-referential authentication
+
+Inside a composite action, `uses: ./...` resolves against the **caller's**
+checkout, not this repo. So the internal authentication step references
+`OvertureMaps/workflows/.github/actions/setup-codeartifact@main` by full path
+(with a `zizmor: ignore[unpinned-uses]` comment) rather than a `./` relative
+path. The `@main` ref is tightened to a commit SHA in a follow-up once merged.
diff --git a/.github/actions/publish-maven-to-codeartifact/action.yml b/.github/actions/publish-maven-to-codeartifact/action.yml
new file mode 100644
index 0000000..8b951ef
--- /dev/null
+++ b/.github/actions/publish-maven-to-codeartifact/action.yml
@@ -0,0 +1,118 @@
+name: Build and Publish Maven Project to CodeArtifact
+description: >
+ Builds this project from source and publishes its artifacts to AWS CodeArtifact.
+ Handles Java setup and CodeArtifact authentication internally — suitable for
+ drop-in use in any Maven project workflow.
+
+inputs:
+ aws-role-arn:
+ description: IAM role ARN to assume via OIDC for CodeArtifact access
+ required: true
+ aws-region:
+ description: AWS region where CodeArtifact is hosted
+ required: false
+ default: us-west-2
+ codeartifact-domain:
+ description: CodeArtifact domain name
+ required: true
+ codeartifact-domain-owner:
+ description: AWS account ID that owns the CodeArtifact domain
+ required: true
+ codeartifact-repository:
+ description: CodeArtifact repository name
+ required: true
+ version:
+ description: >
+ Optional artifact version. When set, the pom version is overridden via
+ `mvn versions:set` and the project is built with dev naming. When empty,
+ the project's release version is published unchanged.
+ required: false
+ default: ""
+ working-directory:
+ description: >
+ Directory containing the Maven project (its pom.xml and .java-version).
+ Defaults to the repository root. Set this to publish a project that lives
+ in a subdirectory.
+ required: false
+ default: "."
+ commit:
+ description: >
+ Commit SHA recorded as `build.commit` in the JAR manifest. Defaults to the
+ checked-out workspace HEAD (`git rev-parse HEAD`), which matches the tree
+ actually built — unlike `github.sha`, which is the merge-ref SHA on
+ `pull_request` events. Override only if the build tree is not a git
+ checkout.
+ required: false
+ default: ""
+
+runs:
+ using: "composite"
+ steps:
+ - name: Set up JDK
+ uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
+ with:
+ distribution: "temurin"
+ java-version-file: "${{ inputs.working-directory }}/.java-version"
+ cache: maven
+ cache-dependency-path: "${{ inputs.working-directory }}/pom.xml"
+
+ - name: Authenticate with CodeArtifact
+ uses: OvertureMaps/workflows/.github/actions/setup-codeartifact@main # zizmor: ignore[unpinned-uses] -- self-referential; SHA updated in follow-up commit
+ with:
+ aws-role-arn: ${{ inputs.aws-role-arn }}
+ aws-region: ${{ inputs.aws-region }}
+ codeartifact-domain: ${{ inputs.codeartifact-domain }}
+ codeartifact-domain-owner: ${{ inputs.codeartifact-domain-owner }}
+ codeartifact-repository: ${{ inputs.codeartifact-repository }}
+
+ - name: Set dev version
+ if: ${{ inputs.version != '' }}
+ shell: bash
+ working-directory: ${{ inputs.working-directory }}
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ mvn versions:set -ntp \
+ -DnewVersion="$VERSION" \
+ -DgenerateBackupPoms=false \
+ --settings ~/.m2/settings.xml
+
+ - name: Build and Deploy to CodeArtifact
+ shell: bash
+ working-directory: ${{ inputs.working-directory }}
+ env:
+ CODEARTIFACT_DOMAIN: ${{ inputs.codeartifact-domain }}
+ CODEARTIFACT_REPO_URL: "https://${{ inputs.codeartifact-domain }}-${{ inputs.codeartifact-domain-owner }}.d.codeartifact.${{ inputs.aws-region }}.amazonaws.com/maven/${{ inputs.codeartifact-repository }}/"
+ BUILD_ENV: ${{ inputs.version != '' && 'dev' || 'release' }}
+ BUILD_BRANCH: ${{ github.ref_name }}
+ COMMIT_OVERRIDE: ${{ inputs.commit }}
+ WORKFLOW_RUN_ID: ${{ github.run_id }}
+ WORKFLOW_RUN_NUMBER: ${{ github.run_number }}
+ run: |
+ # Default build.commit to the checked-out HEAD so the manifest matches the
+ # tree actually built. On pull_request, github.sha is the merge-ref SHA.
+ BUILD_COMMIT="$COMMIT_OVERRIDE"
+ if [ -z "$BUILD_COMMIT" ]; then
+ BUILD_COMMIT=$(git rev-parse HEAD)
+ fi
+ mvn clean deploy -ntp \
+ -DaltDeploymentRepository="$CODEARTIFACT_DOMAIN::$CODEARTIFACT_REPO_URL" \
+ -Dbuild.branch="$BUILD_BRANCH" \
+ -Dbuild.commit="$BUILD_COMMIT" \
+ -Dworkflow.run_id="$WORKFLOW_RUN_ID" \
+ -Dworkflow.run_number="$WORKFLOW_RUN_NUMBER" \
+ -Dmaven.test.skip="true" \
+ -Denv="$BUILD_ENV" \
+ --settings ~/.m2/settings.xml
+
+ - name: Print JAR Manifest Entries
+ shell: bash
+ working-directory: ${{ inputs.working-directory }}
+ run: |
+ JAR_FILE=$(find target -name "*-shaded.jar" -print -quit)
+ if [ -z "$JAR_FILE" ]; then
+ echo "Error: No shaded JAR found in target/" >&2
+ exit 1
+ fi
+ echo "Extracting MANIFEST.MF from $JAR_FILE..."
+ unzip -p "$JAR_FILE" META-INF/MANIFEST.MF
diff --git a/.github/actions/setup-codeartifact/README.md b/.github/actions/setup-codeartifact/README.md
new file mode 100644
index 0000000..4649f36
--- /dev/null
+++ b/.github/actions/setup-codeartifact/README.md
@@ -0,0 +1,110 @@
+# Authenticate with AWS CodeArtifact
+
+A composite GitHub Action that assumes an IAM role via OIDC, acquires an AWS CodeArtifact authorization token, and writes a Maven `settings.xml` so subsequent `mvn` commands can resolve and deploy artifacts against CodeArtifact.
+
+- [How-to guides](#how-to-guides)
+- [Reference](#reference)
+- [Explanation](#explanation)
+
+## How-to guides
+
+### Authenticate before a Maven step
+
+```yaml
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write # required for OIDC role assumption
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Authenticate with CodeArtifact
+ uses: OvertureMaps/workflows/.github/actions/setup-codeartifact@main
+ with:
+ aws-role-arn: arn:aws:iam::123456789012:role/codeartifact-publisher
+ codeartifact-domain: overture
+ codeartifact-domain-owner: "123456789012"
+ codeartifact-repository: maven-releases
+
+ - name: Resolve and deploy
+ run: mvn deploy --settings ~/.m2/settings.xml
+```
+
+> Pin to a commit SHA rather than `@main` for reproducible builds, e.g.
+> `uses: OvertureMaps/workflows/.github/actions/setup-codeartifact@`.
+
+## Reference
+
+### Inputs
+
+- `aws-role-arn` (**required**): IAM role ARN to assume via OIDC.
+- `aws-region` (optional): AWS region where CodeArtifact is hosted. Default `us-west-2`.
+- `codeartifact-domain` (**required**): CodeArtifact domain name.
+- `codeartifact-domain-owner` (**required**): AWS account ID that owns the CodeArtifact domain.
+- `codeartifact-repository` (**required**): CodeArtifact repository name.
+
+### Outputs
+
+The action's primary effect is environmental: it acquires a CodeArtifact
+authorization token (masked, passed internally via a step output rather than
+`$GITHUB_ENV`) and writes `~/.m2/settings.xml`. It also echoes the CodeArtifact
+metadata back as outputs so later steps can pipe from a single source of truth
+instead of re-specifying it:
+
+- `codeartifact-domain` — the domain name.
+- `codeartifact-domain-owner` — the owning AWS account ID.
+- `codeartifact-repository` — the repository name.
+- `aws-region` — the AWS region.
+- `repository-url` — the fully-composed Maven repository URL
+ (`https://-.d.codeartifact..amazonaws.com/maven//`).
+
+The authorization token is intentionally **not** exposed as an output — it lives
+only in `settings.xml`.
+
+```yaml
+- name: Authenticate with CodeArtifact
+ id: ca
+ uses: OvertureMaps/workflows/.github/actions/setup-codeartifact@main
+ with:
+ aws-role-arn: arn:aws:iam::123456789012:role/codeartifact-publisher
+ codeartifact-domain: overture
+ codeartifact-domain-owner: "123456789012"
+ codeartifact-repository: maven-releases
+
+- name: Deploy with the piped URL
+ run: mvn deploy -DaltDeploymentRepository="overture::${{ steps.ca.outputs.repository-url }}" --settings ~/.m2/settings.xml
+```
+
+### Permissions
+
+The job must grant OIDC token issuance so the role can be assumed:
+
+```yaml
+permissions:
+ id-token: write
+ contents: read
+```
+
+AWS permissions are governed by the assumed IAM role, which must allow
+`codeartifact:GetAuthorizationToken`, `sts:GetServiceBearerToken`, and the
+read/write CodeArtifact actions needed by downstream Maven steps.
+
+## Explanation
+
+### Why a dedicated auth step
+
+CodeArtifact authorization tokens are short-lived and must be regenerated per
+run. Centralizing OIDC role assumption, token acquisition, and `settings.xml`
+generation in one step keeps credential handling auditable and lets any Maven
+step in the same job resolve or deploy artifacts without bespoke setup.
+
+### The token boundary
+
+The authorization token is masked in logs and passed from the token step to the
+settings step via a step output (not `$GITHUB_ENV`), then embedded into the
+generated `~/.m2/settings.xml`. Keeping it out of `$GITHUB_ENV` means it is not
+exposed as an environment variable to later steps — but `settings.xml` itself is
+readable by any subsequent step in the job, so still treat the runner as trusted
+for the duration of the job and call this action only in jobs you control.
diff --git a/.github/actions/setup-codeartifact/action.yml b/.github/actions/setup-codeartifact/action.yml
new file mode 100644
index 0000000..32010f7
--- /dev/null
+++ b/.github/actions/setup-codeartifact/action.yml
@@ -0,0 +1,86 @@
+name: Authenticate with AWS CodeArtifact
+description: >
+ Assumes an IAM role via OIDC, acquires a CodeArtifact authorization token,
+ and writes Maven settings.xml so subsequent mvn commands can resolve and
+ deploy artifacts. Must be called before any Maven step that touches CodeArtifact.
+
+inputs:
+ aws-role-arn:
+ description: IAM role ARN to assume via OIDC
+ required: true
+ aws-region:
+ description: AWS region where CodeArtifact is hosted
+ required: false
+ default: us-west-2
+ codeartifact-domain:
+ description: CodeArtifact domain name
+ required: true
+ codeartifact-domain-owner:
+ description: AWS account ID that owns the CodeArtifact domain
+ required: true
+ codeartifact-repository:
+ description: CodeArtifact repository name
+ required: true
+
+outputs:
+ codeartifact-domain:
+ description: CodeArtifact domain name (echo of the input, for piping to later steps)
+ value: ${{ inputs.codeartifact-domain }}
+ codeartifact-domain-owner:
+ description: AWS account ID that owns the CodeArtifact domain (echo of the input)
+ value: ${{ inputs.codeartifact-domain-owner }}
+ codeartifact-repository:
+ description: CodeArtifact repository name (echo of the input)
+ value: ${{ inputs.codeartifact-repository }}
+ aws-region:
+ description: AWS region where CodeArtifact is hosted (echo of the input)
+ value: ${{ inputs.aws-region }}
+ repository-url:
+ description: >
+ Fully-composed CodeArtifact Maven repository URL. Pipe this to a later
+ mvn step (e.g. -DaltDeploymentRepository / -Durl) instead of rebuilding it.
+ value: "https://${{ inputs.codeartifact-domain }}-${{ inputs.codeartifact-domain-owner }}.d.codeartifact.${{ inputs.aws-region }}.amazonaws.com/maven/${{ inputs.codeartifact-repository }}/"
+
+runs:
+ using: "composite"
+ steps:
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
+ with:
+ role-to-assume: ${{ inputs.aws-role-arn }}
+ aws-region: ${{ inputs.aws-region }}
+
+ - name: Generate CodeArtifact Token
+ id: token
+ shell: bash
+ # Token is passed to the next step via a masked step output rather than
+ # $GITHUB_ENV, so it is not exposed as an env var to later steps. It is
+ # still embedded into ~/.m2/settings.xml, so the runner must be trusted.
+ env:
+ CODEARTIFACT_DOMAIN: ${{ inputs.codeartifact-domain }}
+ CODEARTIFACT_DOMAIN_OWNER: ${{ inputs.codeartifact-domain-owner }}
+ AWS_REGION: ${{ inputs.aws-region }}
+ run: |
+ TOKEN=$(aws codeartifact get-authorization-token \
+ --domain "$CODEARTIFACT_DOMAIN" \
+ --domain-owner "$CODEARTIFACT_DOMAIN_OWNER" \
+ --region "$AWS_REGION" \
+ --query authorizationToken \
+ --output text)
+ echo "::add-mask::$TOKEN"
+ echo "token=$TOKEN" >> "$GITHUB_OUTPUT"
+
+ - name: Configure Maven Settings for CodeArtifact
+ uses: whelk-io/maven-settings-xml-action@9dc09b23833fa9aa7f27b63db287951856f3433d # v22 # zizmor: ignore[archived-uses] -- upstream archived but SHA-pinned; canonical settings.xml writer with no maintained drop-in replacement
+ with:
+ repositories: >
+ [{
+ "id": "${{ inputs.codeartifact-domain }}",
+ "url": "https://${{ inputs.codeartifact-domain }}-${{ inputs.codeartifact-domain-owner }}.d.codeartifact.${{ inputs.aws-region }}.amazonaws.com/maven/${{ inputs.codeartifact-repository }}/"
+ }]
+ servers: >
+ [{
+ "id": "${{ inputs.codeartifact-domain }}",
+ "username": "aws",
+ "password": "${{ steps.token.outputs.token }}"
+ }]