# Azure Artifacts Maven feed option for BEAT IT Store

This is an **additive option** to the [complete GitHub/Azure Repos → Jenkins → CloudHub 2 walkthrough](/resources/mulesoft/from-scratch-cloudhub/README.md). The existing parent, Mule child, Exchange publication and CloudHub settings remain unchanged. Azure Artifacts stores/serves the reviewed parent POM and, optionally, a byte-identical copy of an existing application candidate.

No feed, token, remote package, Jenkins job or CloudHub application was created while authoring this package. Tests use temporary files and stubbed Maven. An operational helper command with --confirm really reads or publishes to the configured feed and can consume storage. Use only approved accounts, access and capacity.

The standalone Azure ZIP contains this add-on folder only. Extract its azure-artifacts directory into the existing from-scratch-cloudhub starter root beside pom.xml and scripts. For a fresh start, use the combined starter ZIP, which already includes it. Do not run application-copy helpers from a detached add-on without the original reviewed project/candidate.

## 1. Source control, binary repositories and deployment

| Service | Stores | Enforcing boundary |
| --- | --- | --- |
| GitHub or Azure Repos | Source commits, POMs, tests, Jenkinsfile | Repository identity and branch/job policy |
| Azure Artifacts | Versioned Maven POMs and optional application archive copies | Feed identity, project/feed permissions, coordinates |
| Anypoint Exchange | Parent custom asset and Mule release required by this CH2 path | Business group, Exchange permissions, asset lifecycle |
| CloudHub 2.0 | Deployed application and environment configuration | Scoped deployment identity, target, capacity, approval |

Azure Repos access does not create or authorize an Artifacts feed. Source can remain canonical in GitHub while binary packages use Azure Artifacts. An Azure upload is not a CloudHub deployment; promotion to a feed view is not runtime deployment approval.

Keep the original canonical remote, Git review, supported tools, Jenkins setup and CloudHub path. Do not replace the child's Exchange distributionManagement or point cloudhub2Deployment at an Azure feed. The chosen strategy is dual storage:

1. Review/configure the original parent.
2. Publish its exact release to Exchange using the original parent-only profile.
3. Copy the same POM to Azure with the neutral file publisher here.
4. Verify a separate consumer resolves those exact parent bytes from Azure with an empty cache.
5. Build/test/publish the Mule child to Exchange through the original release lifecycle.
6. Optionally copy the existing candidate JAR and original child POM to Azure without rebuilding.
7. Check an independent Azure download against the existing candidate digest.
8. Continue original Exchange comparison, CloudHub approvals, deployment and smoke checks.

Never publish different parent bytes under the same GAV in different repositories. Parent and child versions remain independent. A changed parent requires a new reviewed parent release and updated child reference. The helper does not activate the parent Exchange profile or modify either POM.

## 2. Create a private feed and copy its scope-specific URL

Start in the approved Azure DevOps Services organization/project. Confirm permission, storage policy and allowed use before creating a feed. This guide does not authorize subscription upgrades.

1. Open project → Artifacts. If absent, ask the project owner about access/feature availability rather than assuming repository access includes packages.
2. Select Create Feed, enter an approved name such as beatit-maven-training and choose the required visibility. Use an approved private project/permission design for private packages.
3. Choose project or organization scope deliberately. Their endpoint shapes differ.
4. Leave upstream sources off for this first narrow exercise unless the owner requires public-package caching and reviews its provenance/retention implications.
5. Create the feed. Open feed settings/gear → Permissions → Add users/groups. Grant the intended publisher Feed Publisher (Contributor); restore-only users need Feed Reader. The token owner's underlying permissions still apply.
6. Review Views permissions too: a view may expose packages beyond the audience implied by direct feed membership.
7. Select Connect to feed → Maven and copy the exact endpoint, including required project segment and encoding.

```text
Project-scoped:
https://pkgs.dev.azure.com/ORGANIZATION/PROJECT/_packaging/FEED/maven/v1

Organization-scoped:
https://pkgs.dev.azure.com/ORGANIZATION/_packaging/FEED/maven/v1
```

Do not remove PROJECT from a project-scoped URL to make an error disappear. This helper supports Azure DevOps Services endpoints above, not on-premises Azure DevOps Server variants.

From the starter repository root, choose one:

```bash
cp azure-artifacts/feed.project.example.json azure-artifacts/feed.json
```

Use feed.organization.example.json instead for organization scope. Edit the two non-secret fields. The following shape is synthetic, not a real account:

```json
{
  "organization": "beatit-example",
  "feedUrl": "https://pkgs.dev.azure.com/beatit-example/Training/_packaging/beatit-maven-training/maven/v1"
}
```

No token, userinfo/password URL, query token or extra credential field belongs in this JSON. The helper rejects placeholders, unexpected keys, a mismatched organization, an unapproved host, HTTP and malformed scope paths.

Expected checkpoint: the feed and access are reviewed, and feed.json matches Connect to feed. No package has been published.

## 3. Authentication and matching Maven server IDs

Microsoft's current Maven feed instructions document PAT-backed settings. This standalone Jenkins example uses that supported fallback: an organization-scoped, expiring token stored in Jenkins, never a literal POM value.

A publisher needs approved Packaging Read & write scope plus Feed Publisher permission. Restore-only credentials use Packaging Read plus Feed Reader. Do not add Code scope merely because both services are in Azure. SCM checkout has its own credential.

Prefer your organization's verified Microsoft Entra/workload-identity integration where available. Microsoft documents passwordless Azure Pipelines service connections for feed access, but Azure Pipelines MavenAuthenticate is not a Jenkins step or a token automatically available to Jenkins. This package does not implement an Entra token broker, claim a Pipelines job token exists in Jenkins, or substitute an Entra token into a PAT field without validating that client flow. An organization that forbids PATs must adopt/test its approved alternative rather than weakening policy for this example.

Create two Secret text entries in the protected Jenkins folder:

- azure-artifacts-publisher: approved scoped publishing token.
- azure-artifacts-reader: separate least-privilege restore token.

Keep scm-read, Exchange and CloudHub credentials separate. Token expiration/rotation and owner access must be managed under organization policy. Tokens do not grant rights their owner lacks.

The settings.azure.template.xml server ID beatit-azure-feed matches both its active resolver repository and the helper's repositoryId. It need not equal the feed's display name, but the chosen IDs must match exactly.

```xml
<server>
  <id>beatit-azure-feed</id>
  <username>${env.AZURE_ARTIFACTS_ORGANIZATION}</username>
  <password>${env.AZURE_ARTIFACTS_TOKEN}</password>
</server>
```

The helper supplies the non-secret organization/feed URL to Maven from feed.json. Jenkins binds only the token at runtime. No token is passed through -D arguments. Avoid Maven -X, printenv and effective-settings with live credentials.

For an approved local session, read the token without echoing it or putting a literal in history:

```bash
read -r -s -p "Azure Artifacts scoped token: " AZURE_ARTIFACTS_TOKEN
export AZURE_ARTIFACTS_TOKEN
```

Clear it after the operation:

```bash
unset AZURE_ARTIFACTS_TOKEN
```

Expected: source/settings retain environment references. A local variable's existence is not proof of valid remote authentication.

## 4. Inspect templates and run offline checks

The original Store POMs are not rewritten. The standalone tools/pom.xml is a neutral packaging-pom project with no Store parent, Mule extension or Exchange plugin. A fully qualified Apache Maven Deploy Plugin 3.2.0 deploy-file goal copies explicit files there instead of running the application's build lifecycle. Do not add that plugin to the original Mule Exchange configuration to force a second destination.

consumer.template.xml is a neutral parent consumer. The helper fills the original parent GAV, leaves relativePath empty and supplies the Azure bootstrap repository. A non-recursive command with a newly created empty cache prevents the nearby POM/warm local cache from masquerading as an Azure download.

Run without credentials or network:

```bash
python3 azure-artifacts/feed.py check-templates
python3 azure-artifacts/test_feed.py
```

Expected: template checks and ten local tests pass. They check scope URLs, unsafe configuration, server-ID matching, no-confirm/no-call guards, unchanged original POMs, fresh-cache parent-byte verification and application-copy/hash behavior using a stubbed Maven process. They do not run Maven, authenticate or publish.

Finish original parent configuration first: literal owning business-group UUID, immutable version and supported runtime value. The helper refuses remaining parent placeholders and snapshot coordinates. Prepare the consumer locally:

```bash
python3 azure-artifacts/feed.py prepare --config azure-artifacts/feed.json
```

Expected: .private/azure-parent-check/pom.xml contains the exact reviewed GAV and feed URL with no token. Inspect it. Creating this file establishes no remote resolution.

## 5. Publish the original parent POM to Azure

Complete the original parent Exchange publication first if using this Store → CloudHub 2 path. That separate operation remains:

```bash
mvn -B -N -s PROTECTED_EXCHANGE_SETTINGS_PATH -Ppublish-parent-exchange deploy
```

Use its original protected Exchange credential, not the Azure token. Do not activate that profile for the Azure file-copy operation. A feed-only parent experiment is possible without CloudHub, but it does not satisfy the original Exchange parent/application prerequisites.

From the starter root, under approved Azure publish access:

```bash
python3 azure-artifacts/feed.py publish-parent --config azure-artifacts/feed.json --settings azure-artifacts/settings.azure.template.xml --confirm
```

The helper invokes a command with this shape; the script supplies actual paths and the configured feed URL:

```text
mvn -B -s SETTINGS_PATH -f azure-artifacts/tools/pom.xml
  org.apache.maven.plugins:maven-deploy-plugin:3.2.0:deploy-file
  -DrepositoryId=beatit-azure-feed
  -Durl=APPROVED_FEED_URL
  -Dfile=ORIGINAL_PARENT_POM
  -DpomFile=ORIGINAL_PARENT_POM
  -Dpackaging=pom
  -DgeneratePom=false
```

This is a file publication, not mvn deploy on the application. It sends the original POM and its coordinates rather than manufacturing a second parent with different repository/profile text. The helper checks that the source file is unchanged after the process returns. It does not build the child or send a CloudHub request.

Expected after actual authorized success: the private feed lists the parent group/artifact/version, and Maven reports successful publication. Retain the reviewed source commit, parent GAV, source-POM hash, feed identity and sanitized Maven outcome. A local subprocess returning successfully is not a substitute for inspecting the package and later verifying its bytes.

For a version conflict, first inspect whether the exact release is already present. Do not delete/recreate packages to force a different POM under the same version. Version reuse/deletion rules are repository policy, not a way to make immutable release identity optional. Publish a new reviewed parent version when content changes.

## 6. Resolve that parent from an empty Maven cache

Switch to the restore-only credential before read verification. This helper uses a unique cache per run and an empty relativePath in its generated consumer:

```bash
python3 azure-artifacts/feed.py resolve-parent --config azure-artifacts/feed.json --settings azure-artifacts/settings.azure.template.xml --confirm
```

It prepares .private/azure-parent-check/pom.xml, chooses a newly created .private/azure-parent-check/fresh-m2-* directory, and invokes Maven non-recursively to produce an effective POM. It then compares the cached downloaded parent POM's SHA-256 with the reviewed original parent. Missing or different bytes fail the operation. Generated effective POM/cache files stay in .private, not in the package or Git.

Expected: remote parent resolution succeeds and the exact-byte check passes. A wrong server ID, expired token, feed URL or parent GAV must fail rather than fall back to the nearby original parent.

**Important limit:** this proves a separate consumer can obtain the parent from the selected feed only when actually run successfully. A normal full Store checkout still has ../pom.xml and may legitimately use that local parent. Do not report that every Store build fetched from Azure merely because this neutral consumer did.

Maven uses repositories/bootstrap settings to find a parent before it can inherit parent configuration. distributionManagement specifies publication destination and cannot by itself solve missing-parent resolution. The settings active profile supplies the Azure repository; the generated consumer also documents it explicitly. Server credentials are matched by ID, not by a similar-looking display label.

For a changed-condition exercise, use an allowed test identity without Feed Reader or a nonexistent test parent version. Predict and retain a resolution failure, then restore the reviewed values. Do not broaden privileges to make a negative test pass.

## 7. Optionally archive an existing application candidate in Azure

Keep the original release path: the child is built, MUnit-tested and published to Exchange once; its manifest records exact package/GAV/control-file hashes. CloudHub deployment still requires Exchange. This optional Azure copy adds a binary repository record; it does not replace Exchange or grant deployment permission.

After the candidate exists and its original controls verify, bind the Azure publisher and run:

```bash
python3 scripts/candidate.py verify
python3 azure-artifacts/feed.py publish-app --config azure-artifacts/feed.json --settings azure-artifacts/settings.azure.template.xml --confirm
```

The file-copy goal runs in the neutral tools project, using the recorded deployable JAR and original child POM. It explicitly uses JAR extension/packaging and classifier mule-application so the artifact identity stays unambiguous. It does not invoke clean, package, the application deploy lifecycle or CloudHub. The candidate is verified again afterward.

Expected: the Azure feed contains the application GAV, POM and classified JAR. Parent and app are separate package versions. A feed view such as @Release is a package-visibility/promotional convention; it does not authorize the CloudHub production stage.

To verify an independent read round trip, switch to the reader token:

```bash
python3 azure-artifacts/feed.py restore-app --config azure-artifacts/feed.json --settings azure-artifacts/settings.azure.template.xml --confirm
```

Despite its short action name, this is **round-trip verification of an existing candidate**, not disaster recovery with a missing source JAR. It first requires the original candidate, manifest and reviewed control files locally. It downloads through a fresh cache into .private/azure-restored and compares the copied classified JAR to the recorded digest. A pre-existing output is rejected so stale files cannot masquerade as a new download. Use a new disposable practice workspace for another clean round trip, or explicitly archive/remove only that generated verification output under your handling policy.

This does not prove Exchange and Azure contain the same release until their respective comparisons have actually passed. Never regenerate a manifest around arbitrary downloaded bytes to turn a mismatch into success. The original Exchange check and CloudHub deployment script remain the deployment gate.

## 8. Add the optional Jenkins stage without mixing credentials

Keep the original protected main-only job and dedicated trusted agent. Canonical source can be GitHub or Azure Repos; the Azure feed credential grants no SCM or CloudHub authority. All POM/helper/Jenkinsfile changes must go through the same review process before credentialed builds.

1. In the protected folder's Credentials page, create Secret text IDs azure-artifacts-publisher and azure-artifacts-reader with the approved separate scopes.
2. Review azure-artifacts/feed.json as non-secret target configuration.
3. Read Jenkinsfile.fragment. It is a stage fragment, not a complete replacement Jenkinsfile. Insert its optional archive stage **after candidate creation/verification** and before the relevant deployment approval if repository archiving is a release requirement.
4. Review/commit the resulting Jenkinsfile before producing a candidate. The original manifest hashes that file; inserting a stage into an already-recorded candidate's control files deliberately causes a mismatch.
5. Keep the original Exchange build/publication stages intact. Do not put the Azure publisher token at pipeline-global environment scope.
6. Run a separately authorized parent publication when a new parent version is approved. Do not republish every existing parent in each application release.
7. For feed read/round-trip checks, bind only azure-artifacts-reader using the same environment variable name for that short operation.

The fragment uses:

```groovy
withCredentials([string(credentialsId: 'azure-artifacts-publisher', variable: 'AZURE_ARTIFACTS_TOKEN')]) {
  sh '''
    set +x
    set -eu
    python3 azure-artifacts/feed.py publish-app --config azure-artifacts/feed.json --settings azure-artifacts/settings.azure.template.xml --confirm
  '''
}
```

A matching read-stage example is:

```groovy
withCredentials([string(credentialsId: 'azure-artifacts-reader', variable: 'AZURE_ARTIFACTS_TOKEN')]) {
  sh '''
    set +x
    set -eu
    python3 azure-artifacts/feed.py resolve-parent --config azure-artifacts/feed.json --settings azure-artifacts/settings.azure.template.xml --confirm
  '''
}
```

Use the original agent's supported Maven/JDK versions. Neither masking nor withCredentials makes malicious build code safe. Do not run untrusted pull requests on the credentialed job/agent; same-account processes can inspect environment values. Feed Reader is narrower than Publisher, but it still exposes private artifacts and must not be handed to arbitrary fork code. A scheduling label is not an isolation boundary.

Jenkins does not automatically have Azure Pipelines' project Build Service identity or System.AccessToken. Grant permission to the actual identity whose supported credential this job uses. Do not copy Azure Pipelines YAML authentication tasks into Jenkins and claim they executed.

If Azure archiving is required by your release policy, a failed Azure copy should block the relevant promotion. If it is informational, document that explicitly through a reviewed policy; do not quietly swallow errors. The provided fragment propagates process failure.

## 9. Continue to CloudHub 2 using the original gate

After any required Azure checks, use the original candidate verification, authenticated Exchange byte check, sandbox approval, existing-artifact deployment and Postman smoke suite. The CloudHub connected app still needs its own reviewed Anypoint scopes and environment access.

Do not change these facts:

- The Mule application's release must exist in Exchange for the original CloudHub 2 plugin path.
- Parent-POM Exchange publication remains separate; an Azure copy does not make it an Exchange asset.
- A token that writes Azure packages cannot deploy CloudHub.
- Publication to two repositories is not evidence of a healthy application.
- Rebuilding for the second repository defeats the same-bytes copy claim; the helper copies the existing artifact instead.
- Application rollback does not undo business effects, and a feed-view change is not rollback.

The supplied tests demonstrate argument/guard/hash behavior under controlled local conditions. Maven plugin resolution, private-feed authentication, upload/download behavior, upstream policies, Jenkins credential binding and CloudHub operation remain unexecuted until your authorized environment supplies actual evidence.

## 10. Troubleshooting and acceptance evidence

| Symptom | Check | Avoid |
| --- | --- | --- |
| Artifacts menu/feed unavailable | Feature access, project membership, organization/feed policy | Assuming Azure Repos access grants package access |
| 401 | Token expiry/type, organization, matching settings server ID | Printing the token or passing it through -D |
| 403 | Actual token owner's project/feed role and Packaging scope | Granting all scopes as a guess |
| 404 | Project-vs-organization URL, feed and exact GAV | Removing URL segments blindly |
| Parent resolves only locally | Empty relativePath and new unique cache in neutral check | Calling a warm-cache build remote-resolution proof |
| Parent bytes differ | Source GAV/content and published release | Regenerating expected output or overwriting immutable release |
| Application classifier differs | jar:mule-application identity and original child POM | Selecting a source/lightweight archive |
| Azure copy succeeds, CH2 fails | Exchange prerequisites and independent deployment scopes | Replacing CH2 target with a feed URL |
| Existing round-trip output rejected | Prior generated verification file | Counting stale output as a fresh download |
| Jenkins has no System.AccessToken | Jenkins-specific credential integration | Treating an Azure Pipelines task as Jenkins functionality |
| Maven/plugin failure | Supported toolchain, neutral tool project and official goal parameters | Running application lifecycle just to copy bytes |

For a completed authorized exercise, retain the reviewed feed URL/scope, permission decisions without secret values, parent/app GAVs, source and candidate hashes, actual sanitized Maven outcomes, parent-byte comparison and optional app round-trip comparison. Keep negative-case evidence and reviewer corrections too. If you ran offline only, submit the ten local test results and a labeled unexecuted plan instead of claiming remote publication.

## Public primary references

- [Microsoft: connect Maven to an Azure feed](https://learn.microsoft.com/en-us/azure/devops/artifacts/maven/project-setup-maven?view=azure-devops) and [Maven package quickstart](https://learn.microsoft.com/en-us/azure/devops/artifacts/get-started-maven?view=azure-devops).
- [Microsoft: feed permissions](https://learn.microsoft.com/en-us/azure/devops/artifacts/feeds/feed-permissions?view=azure-devops), [feed concepts/scopes](https://learn.microsoft.com/en-us/azure/devops/artifacts/concepts/feeds?view=azure-devops) and [feed views](https://learn.microsoft.com/en-us/azure/devops/artifacts/feeds/views?view=azure-devops).
- [Microsoft: PAT policy and management](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops), [Azure Pipelines Entra service connections](https://learn.microsoft.com/en-us/azure/devops/pipelines/library/add-devops-entra-service-connection?view=azure-devops) and [MavenAuthenticate task boundary](https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/maven-authenticate-v0?view=azure-pipelines).
- [Apache Maven deploy-file goal](https://maven.apache.org/plugins/maven-deploy-plugin/deploy-file-mojo.html), [file-deployment usage](https://maven.apache.org/plugins/maven-deploy-plugin/usage.html) and [dependency copy](https://maven.apache.org/plugins/maven-dependency-plugin/copy-mojo.html).
- [MuleSoft: Exchange publication and parent POM assets](https://docs.mulesoft.com/exchange/to-publish-assets-maven) and [CloudHub 2 deployment prerequisites](https://docs.mulesoft.com/mule-runtime/latest/deploy-to-cloudhub-2).
- [Jenkins: Jenkinsfile and credential handling](https://www.jenkins.io/doc/book/pipeline/jenkinsfile/).

These public references were checked for the platform boundaries used here. No Mimeo guide text, vendor exercise, answer or solution was copied.


