diff --git a/.github/workflows/agentApmTests.yml b/.github/workflows/agentApmTests.yml new file mode 100644 index 000000000..3a5d96ff9 --- /dev/null +++ b/.github/workflows/agentApmTests.yml @@ -0,0 +1,90 @@ +name: Agent APM Tests +on: + workflow_call: + workflow_dispatch: + +jobs: + Agent-APM-Tests: + name: agent-apm ${{ matrix.os.name }} + strategy: + fail-fast: false + matrix: + os: + - name: ubuntu + version: 24.04 + - name: windows + version: 2022 + - name: macos + version: 14 + runs-on: ${{ matrix.os.name }}-${{ matrix.os.version }} + steps: + - name: Skip macOS - JGC-413 + if: matrix.os.name == 'macos' + run: | + echo "::warning::JGC-413 - Skip until artifactory bootstrap in osx is fixed" + exit 0 + + - name: Checkout code + if: matrix.os.name != 'macos' + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + allow-unsafe-pr-checkout: true + + - name: Setup FastCI + if: matrix.os.name != 'macos' + uses: jfrog-fastci/fastci@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + fastci_otel_token: ${{ secrets.FASTCI_TOKEN }} + + - name: Setup Go with cache + if: matrix.os.name != 'macos' + uses: jfrog/.github/actions/install-go-with-cache@main + + # install.sh only supports Darwin and Linux, so Windows uses install.ps1. + - name: Install APM CLI + if: matrix.os.name != 'macos' && matrix.os.name != 'windows' + shell: bash + run: | + set -euo pipefail + # Install Microsoft APM CLI (>= 0.1.0) required by jf agent apm. + if ! command -v apm >/dev/null 2>&1; then + curl -fsSL https://raw.githubusercontent.com/microsoft/apm/main/install.sh | bash + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "$HOME/bin" >> "$GITHUB_PATH" + fi + + - name: Install APM CLI on Windows + if: matrix.os.name == 'windows' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $installRoot = Join-Path $env:LOCALAPPDATA 'Programs\apm' + $binDir = Join-Path $installRoot 'bin' + $env:APM_INSTALL_DIR = $binDir + Invoke-RestMethod https://raw.githubusercontent.com/microsoft/apm/main/install.ps1 | Invoke-Expression + # The shim lives in bin; apm.exe lives in current. Expose both so + # lookups succeed with and without PATHEXT resolution. + $binDir | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 + Join-Path $installRoot 'current' | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 + + # PATH additions only apply to later steps, so verify here. + - name: Verify APM CLI + if: matrix.os.name != 'macos' + shell: bash + run: apm --version + + - name: Install local Artifactory + if: matrix.os.name != 'macos' + uses: jfrog/.github/actions/install-local-artifactory@main + with: + RTLIC: ${{ secrets.RTLIC }} + # No VERSION pin: releases.jfrog.io keeps only a subset of patch + # releases, so pinning breaks once that version is pruned. Latest + # supports agentpackages repositories. + RT_CONNECTION_TIMEOUT_SECONDS: ${{ env.RT_CONNECTION_TIMEOUT_SECONDS || '1200' }} + + - name: Run agent APM tests + if: matrix.os.name != 'macos' + run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.agentApm diff --git a/.github/workflows/build-gate.yml b/.github/workflows/build-gate.yml index 662f4e021..f29a321ce 100644 --- a/.github/workflows/build-gate.yml +++ b/.github/workflows/build-gate.yml @@ -41,6 +41,10 @@ jobs: needs: gate uses: ./.github/workflows/agentPluginsTests.yml secrets: inherit + agent-apm: + needs: gate + uses: ./.github/workflows/agentApmTests.yml + secrets: inherit access: needs: gate # OIDC suite: caller must grant id-token so the reusable workflow can request it. @@ -173,6 +177,7 @@ jobs: - gate - frogbot - agent-plugins + - agent-apm - access - artifactory - conan diff --git a/agent_apm_test.go b/agent_apm_test.go new file mode 100644 index 000000000..4d530f0a2 --- /dev/null +++ b/agent_apm_test.go @@ -0,0 +1,678 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + biutils "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/generic" + artUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + coreBuild "github.com/jfrog/jfrog-cli-core/v2/common/build" + "github.com/jfrog/jfrog-cli-core/v2/common/spec" + coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/jfrog/jfrog-cli/inttestutils" + "github.com/jfrog/jfrog-cli/utils/tests" +) + +const ( + apmTestOwner = "jfrog" +) + +// --------------------------------------------------------------------------- +// Init / cleanup +// --------------------------------------------------------------------------- + +func InitAgentApmTests() { + initArtifactoryCli() + cleanUpOldRepositories() + tests.AddTimestampToGlobalVars() + createRequiredRepos() +} + +func CleanAgentApmTests() { + deleteCreatedRepos() +} + +func initAgentApmTest(t *testing.T) { + if !*tests.TestAgentApm { + t.Skip("Skipping Agent APM test. To run Agent APM tests add the '-test.agentApm=true' option.") + } + requireApmBinary(t) + createJfrogHomeConfig(t, false) + require.True(t, isRepoExist(tests.AgentApmLocalRepo), "agent apm local repo does not exist: "+tests.AgentApmLocalRepo) +} + +func cleanAgentApmTest() { + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.AgentApmBuildName, artHttpDetails) + tests.CleanFileSystem() +} + +func requireApmBinary(t *testing.T) { + t.Helper() + if _, err := exec.LookPath("apm"); err != nil { + t.Skip("apm CLI is not on PATH; install Agent Package Manager >= 0.1.0 to run these tests") + } +} + +// runAgentApmCmd executes `jf agent apm ` in dir (empty = current dir). +func runAgentApmCmd(t *testing.T, dir string, args ...string) error { + t.Helper() + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + if dir != "" { + wd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(dir)) + t.Cleanup(func() { _ = os.Chdir(wd) }) + } + return jfrogCli.Exec(append([]string{"agent", "apm"}, args...)...) +} + +func runSetupAgentApm(t *testing.T) { + t.Helper() + // Isolate HOME so jf setup agent-apm writes ~/.apm/config.json under a temp tree. + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) // Windows + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + require.NoError(t, jfrogCli.Exec( + "setup", "agent-apm", + "--repo="+tests.AgentApmLocalRepo, + ), "jf setup agent-apm must succeed") +} + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +func agentApmFixtureSrc(name string) string { + return filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "agent_apm", name) +} + +// copyApmFixture copies a testdata/agent_apm/ project into a fresh temp dir +// and injects a registries: block pointing at the test Artifactory repo. +func copyApmFixture(t *testing.T, fixtureName string) string { + t.Helper() + src := agentApmFixtureSrc(fixtureName) + dst, cleanup := coretests.CreateTempDirWithCallbackAndAssert(t) + t.Cleanup(cleanup) + + // CopyDir copies contents of src into dst, so we need to copy the children, + // not the directory itself. Read the source directory and copy each item. + entries, err := os.ReadDir(src) + require.NoError(t, err, "failed to read fixture source: %s", src) + for _, entry := range entries { + srcItem := filepath.Join(src, entry.Name()) + dstItem := filepath.Join(dst, entry.Name()) + if entry.IsDir() { + require.NoError(t, biutils.CopyDir(srcItem, dstItem, true, nil)) + } else { + data, err := os.ReadFile(srcItem) // #nosec G304 -- test fixture + require.NoError(t, err) + require.NoError(t, os.WriteFile(dstItem, data, 0644)) // #nosec G306 -- test fixture + } + } + + injectApmRegistry(t, dst) + return dst +} + +func injectApmRegistry(t *testing.T, projectDir string) { + t.Helper() + manifestPath := filepath.Join(projectDir, "apm.yml") + data, err := os.ReadFile(manifestPath) // #nosec G304 -- test fixture path + require.NoError(t, err) + + var doc map[string]any + require.NoError(t, yaml.Unmarshal(data, &doc)) + + artURL := strings.TrimRight(*tests.JfrogUrl, "/") + if !strings.HasSuffix(artURL, "/artifactory") { + artURL += "/artifactory" + } + registryURL := fmt.Sprintf("%s/api/agentpackages/%s/", artURL, tests.AgentApmLocalRepo) + doc["registries"] = map[string]any{ + tests.AgentApmLocalRepo: map[string]any{ + "url": registryURL, + }, + "default": tests.AgentApmLocalRepo, + } + + // Inject unique version suffix to avoid immutability conflicts when tests run in parallel or repeated. + // Extract test name and use it as part of the version prerelease (e.g., 1.0.0-TestName). + if version, ok := doc["version"].(string); ok { + testName := strings.TrimPrefix(t.Name(), "main.") + uniqueVersion := fmt.Sprintf("%s-%s", version, testName) + doc["version"] = uniqueVersion + } + + // Rewrite fixture deps that still reference the demo owner "uday/" to the test owner. + if deps, ok := doc["dependencies"].(map[string]any); ok { + if apmDeps, ok := deps["apm"].([]any); ok { + for i, dep := range apmDeps { + if s, ok := dep.(string); ok { + apmDeps[i] = strings.Replace(s, "uday/", apmTestOwner+"/", 1) + } + } + deps["apm"] = apmDeps + } + } + + out, err := yaml.Marshal(doc) + require.NoError(t, err) + require.NoError(t, os.WriteFile(manifestPath, out, 0o644)) // #nosec G306 -- test fixture +} + +func packageRef(name string) string { + return apmTestOwner + "/" + name +} + +func apmArtifactPath(repo, owner, name, version string) string { + return fmt.Sprintf("%s/%s/%s/%s-%s.zip", repo, owner, name, name, version) +} + +func assertApmPackageExists(t *testing.T, owner, name, version string) { + t.Helper() + sm, err := artUtils.CreateServiceManager(serverDetails, -1, 0, false) + require.NoError(t, err) + path := apmArtifactPath(tests.AgentApmLocalRepo, owner, name, version) + _, err = sm.GetItemProps(path) + require.NoError(t, err, "APM package should exist at %s", path) +} + +func publishApmFixture(t *testing.T, fixtureName, packageName, version string, extraArgs ...string) (string, string) { + t.Helper() + dir := copyApmFixture(t, fixtureName) + + // Read the injected apm.yml to get the actual version (includes test-name suffix) + manifestPath := filepath.Join(dir, "apm.yml") + data, err := os.ReadFile(manifestPath) // #nosec G304 -- test fixture path + require.NoError(t, err) + var doc map[string]any + require.NoError(t, yaml.Unmarshal(data, &doc)) + actualVersion, ok := doc["version"].(string) + if !ok { + actualVersion = version + } + + args := append([]string{packageRef(packageName)}, extraArgs...) + require.NoError(t, runAgentApmCmd(t, dir, append([]string{"publish"}, args...)...), + "publish %s@%s", packageName, actualVersion) + assertApmPackageExists(t, apmTestOwner, packageName, actualVersion) + return dir, actualVersion +} + +// --------------------------------------------------------------------------- +// Config & Setup (P0 #1, #2) +// --------------------------------------------------------------------------- + +// TestAgentApmSetup configures ~/.apm/config.json via jf setup agent-apm (scenario #1). +func TestAgentApmSetup(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + + runSetupAgentApm(t) + + configPath := filepath.Join(os.Getenv("HOME"), ".apm", "config.json") + require.FileExists(t, configPath, "jf setup agent-apm should create ~/.apm/config.json") + data, err := os.ReadFile(configPath) // #nosec G304 -- path under isolated HOME + require.NoError(t, err) + assert.Contains(t, string(data), tests.AgentApmLocalRepo, + "config should reference the agentpackages repo") + + // Idempotent second run. + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + require.NoError(t, jfrogCli.Exec("setup", "agent-apm", "--repo="+tests.AgentApmLocalRepo)) +} + +// TestAgentApmInstallUsesManifestRegistry publishes a package then installs a consumer that +// discovers the registry from apm.yml registries: (scenario #2) without relying solely on setup. +func TestAgentApmInstallUsesManifestRegistry(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + _, _ = publishApmFixture(t, "pkg-base-v2", "pkg-base", "1.1.0") + + consumerDir := copyApmFixture(t, "pkg-consumer") + require.NoError(t, runAgentApmCmd(t, consumerDir, "install", "--yes")) + require.FileExists(t, filepath.Join(consumerDir, "apm.lock.yaml"), + "install should write apm.lock.yaml using the registries: block") +} + +// --------------------------------------------------------------------------- +// Publish & Build Info (P0 #3–#9, #12) +// --------------------------------------------------------------------------- + +// TestAgentApmPublish uploads a package and verifies Artifactory layout owner/name/name-ver.zip (#3,#4). +func TestAgentApmPublish(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + _, _ = publishApmFixture(t, "pkg-base", "pkg-base", "1.0.0") +} + +// TestAgentApmPublishWithBuildInfo captures build-info on publish and publishes it (#3,#5,#6,#12). +func TestAgentApmPublishWithBuildInfo(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + buildNumber := t.Name() + t.Cleanup(func() { _ = coreBuild.RemoveBuildDir(tests.AgentApmBuildName, buildNumber, "") }) + + dir := copyApmFixture(t, "pkg-base") + require.NoError(t, runAgentApmCmd(t, dir, + "publish", packageRef("pkg-base"), + "--build-name="+tests.AgentApmBuildName, + "--build-number="+buildNumber, + )) + require.NoError(t, artifactoryCli.Exec("bp", tests.AgentApmBuildName, buildNumber)) + + published, found, err := tests.GetBuildInfo(serverDetails, tests.AgentApmBuildName, buildNumber) + require.NoError(t, err) + require.True(t, found, "build info must be retrievable after jf rt bp") + require.NotEmpty(t, published.BuildInfo.Modules) + require.NotEmpty(t, published.BuildInfo.Modules[0].Artifacts, + "published zip should appear as a build-info artifact") + assert.NotEmpty(t, published.BuildInfo.Modules[0].Artifacts[0].Sha256, + "artifact sha256 must be present (scenario #6/#18/#19)") +} + +// TestAgentApmNoBuildInfoWithoutFlags verifies publish without BI flags creates no local build (#25 inverse). +func TestAgentApmNoBuildInfoWithoutFlags(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + dir := copyApmFixture(t, "pkg-base") + require.NoError(t, runAgentApmCmd(t, dir, "publish", packageRef("pkg-base"))) + + localBuilds, err := coreBuild.GetGeneratedBuildsInfo(tests.AgentApmBuildName, "1", "") + require.NoError(t, err) + assert.Empty(t, localBuilds, "no local build info without --build-name/--build-number") +} + +// TestAgentApmPublishBuildNameWithoutNumber requires both build flags together (#25). +func TestAgentApmPublishBuildNameWithoutNumber(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + dir := copyApmFixture(t, "pkg-base") + err := runAgentApmCmd(t, dir, + "publish", packageRef("pkg-base"), + "--build-name="+tests.AgentApmBuildName, + ) + require.Error(t, err, "--build-name without --build-number must fail") +} + +// TestAgentApmModuleOverride verifies --module overrides the build module id (#26). +func TestAgentApmModuleOverride(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + buildNumber := t.Name() + t.Cleanup(func() { _ = coreBuild.RemoveBuildDir(tests.AgentApmBuildName, buildNumber, "") }) + customModule := "my-custom-apm-module" + + dir := copyApmFixture(t, "pkg-base") + require.NoError(t, runAgentApmCmd(t, dir, + "publish", packageRef("pkg-base"), + "--build-name="+tests.AgentApmBuildName, + "--build-number="+buildNumber, + "--module="+customModule, + )) + require.NoError(t, artifactoryCli.Exec("bp", tests.AgentApmBuildName, buildNumber)) + + published, found, err := tests.GetBuildInfo(serverDetails, tests.AgentApmBuildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + require.NotEmpty(t, published.BuildInfo.Modules) + assert.Equal(t, customModule, published.BuildInfo.Modules[0].Id) +} + +// TestAgentApmBuildPropertiesOnArtifact verifies build.* properties after publish (#8). +func TestAgentApmBuildPropertiesOnArtifact(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + buildNumber := t.Name() + t.Cleanup(func() { _ = coreBuild.RemoveBuildDir(tests.AgentApmBuildName, buildNumber, "") }) + + dir := copyApmFixture(t, "pkg-base") + require.NoError(t, runAgentApmCmd(t, dir, + "publish", packageRef("pkg-base"), + "--build-name="+tests.AgentApmBuildName, + "--build-number="+buildNumber, + )) + require.NoError(t, artifactoryCli.Exec("bp", tests.AgentApmBuildName, buildNumber)) + + sm, err := artUtils.CreateServiceManager(serverDetails, -1, 0, false) + require.NoError(t, err) + + // Read the actual version from apm.yml (which includes test-name suffix) + manifestPath := filepath.Join(dir, "apm.yml") + data, err := os.ReadFile(manifestPath) // #nosec G304 -- test fixture path + require.NoError(t, err) + var doc map[string]any + require.NoError(t, yaml.Unmarshal(data, &doc)) + actualVersion, ok := doc["version"].(string) + if !ok { + actualVersion = "1.0.0" + } + + props, err := sm.GetItemProps(apmArtifactPath(tests.AgentApmLocalRepo, apmTestOwner, "pkg-base", actualVersion)) + require.NoError(t, err) + require.NotNil(t, props) + assert.Contains(t, props.Properties, "build.name") + assert.Contains(t, props.Properties, "build.number") + assert.Contains(t, props.Properties, "build.timestamp") +} + +// TestAgentApmBuildInfoFromEnvVars uses JFROG_CLI_BUILD_* without CLI flags (#25 env path). +func TestAgentApmBuildInfoFromEnvVars(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + envBuildName := tests.AgentApmBuildName + "-env" + envBuildNumber := "42" + t.Setenv("JFROG_CLI_BUILD_NAME", envBuildName) + t.Setenv("JFROG_CLI_BUILD_NUMBER", envBuildNumber) + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, envBuildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, envBuildName, artHttpDetails) + + dir := copyApmFixture(t, "pkg-base") + require.NoError(t, runAgentApmCmd(t, dir, "publish", packageRef("pkg-base"))) + require.NoError(t, artifactoryCli.Exec("bp", envBuildName, envBuildNumber)) + + _, found, err := tests.GetBuildInfo(serverDetails, envBuildName, envBuildNumber) + require.NoError(t, err) + assert.True(t, found, "build info should be captured from JFROG_CLI_BUILD_* env vars") +} + +// TestAgentApmChecksumStoredByArtifactory verifies Artifactory stores sha256 on the zip (#19). +func TestAgentApmChecksumStoredByArtifactory(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + _, publishedVersion := publishApmFixture(t, "pkg-base", "pkg-base", "1.0.0") + + path := apmArtifactPath(tests.AgentApmLocalRepo, apmTestOwner, "pkg-base", publishedVersion) + searchSpec := spec.NewBuilder().Pattern(path).BuildSpec() + searchCmd := generic.NewSearchCommand() + searchCmd.SetServerDetails(serverDetails).SetSpec(searchSpec) + reader, err := searchCmd.Search() + require.NoError(t, err) + defer func() { _ = reader.Close() }() + item := new(artUtils.SearchResult) + require.NoError(t, reader.NextRecord(item)) + assert.NotEmpty(t, item.Sha256, "Artifactory must store sha256 for the published zip") +} + +// --------------------------------------------------------------------------- +// Install & Resolve (P0 #13, #15; P1 #14, #16) +// --------------------------------------------------------------------------- + +// TestAgentApmInstallWithBuildInfo installs deps and captures build-info (#7,#13,#18). +func TestAgentApmInstallWithBuildInfo(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + _, _ = publishApmFixture(t, "pkg-base-v2", "pkg-base", "1.1.0") + + buildNumber := t.Name() + t.Cleanup(func() { _ = coreBuild.RemoveBuildDir(tests.AgentApmBuildName, buildNumber, "") }) + + consumerDir := copyApmFixture(t, "pkg-consumer") + require.NoError(t, runAgentApmCmd(t, consumerDir, + "install", "--yes", + "--build-name="+tests.AgentApmBuildName, + "--build-number="+buildNumber, + )) + require.FileExists(t, filepath.Join(consumerDir, "apm.lock.yaml")) + + require.NoError(t, artifactoryCli.Exec("bp", tests.AgentApmBuildName, buildNumber)) + published, found, err := tests.GetBuildInfo(serverDetails, tests.AgentApmBuildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + require.NotEmpty(t, published.BuildInfo.Modules) + require.NotEmpty(t, published.BuildInfo.Modules[0].Dependencies, + "install build-info should record registry dependencies") + for _, dep := range published.BuildInfo.Modules[0].Dependencies { + assert.NotEmpty(t, dep.Sha256, "dependency %s must have sha256", dep.Id) + } +} + +// TestAgentApmInstallNotFound returns an error for an unknown package (#15). +func TestAgentApmInstallNotFound(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + dir := copyApmFixture(t, "pkg-nodeps") + err := runAgentApmCmd(t, dir, "install", "jfrog/nonexistent-apm-pkg-xyzzy", "--yes") + assert.Error(t, err, "install of unknown package should fail") +} + +// TestAgentApmInstallFrozenFailsWithoutLockfile (#14). +func TestAgentApmInstallFrozenFailsWithoutLockfile(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + dir := copyApmFixture(t, "pkg-consumer") + // Native apm flags after -- escape. + err := runAgentApmCmd(t, dir, "install", "--", "--frozen") + assert.Error(t, err, "install --frozen without lockfile should fail") +} + +// TestAgentApmUpdateWithBuildInfo runs update and captures BI (#16). +func TestAgentApmUpdateWithBuildInfo(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + publishApmFixture(t, "pkg-base-v2", "pkg-base", "1.1.0") + + consumerDir := copyApmFixture(t, "pkg-consumer") + require.NoError(t, runAgentApmCmd(t, consumerDir, "install", "--yes")) + + buildNumber := t.Name() + t.Cleanup(func() { _ = coreBuild.RemoveBuildDir(tests.AgentApmBuildName, buildNumber, "") }) + require.NoError(t, runAgentApmCmd(t, consumerDir, + "update", "--yes", + "--build-name="+tests.AgentApmBuildName, + "--build-number="+buildNumber, + )) + require.NoError(t, artifactoryCli.Exec("bp", tests.AgentApmBuildName, buildNumber)) + _, found, err := tests.GetBuildInfo(serverDetails, tests.AgentApmBuildName, buildNumber) + require.NoError(t, err) + assert.True(t, found) +} + +// --------------------------------------------------------------------------- +// Flag validation & Auth (P0 #23, #24, #36; P1 #32, #28) +// --------------------------------------------------------------------------- + +// TestAgentApmPublishMissingPackageArg (#23). +func TestAgentApmPublishMissingPackageArg(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + dir := copyApmFixture(t, "pkg-base") + err := runAgentApmCmd(t, dir, "publish") + assert.Error(t, err, "publish without package/org should fail") +} + +// TestAgentApmNoRegistryConfigured fails clearly when no registry is available (#24/#36). +func TestAgentApmNoRegistryConfigured(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + + // Isolated HOME without setup, and fixture without registries injection. + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + + src := agentApmFixtureSrc("pkg-noregistry") + dst, cleanup := coretests.CreateTempDirWithCallbackAndAssert(t) + t.Cleanup(cleanup) + require.NoError(t, biutils.CopyDir(src, dst, true, nil)) + + err := runAgentApmCmd(t, dst, "publish", packageRef("pkg-noregistry")) + require.Error(t, err, "publish without registry should fail before/during apm auth") + lower := strings.ToLower(err.Error()) + assert.True(t, + strings.Contains(lower, "registry") || strings.Contains(lower, "setup") || strings.Contains(lower, "auth"), + "error should mention registry/setup/auth, got: %s", err.Error()) +} + +// TestAgentApmNativeFlagEscape verifies -- passes native apm flags (#28). +func TestAgentApmNativeFlagEscape(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + dir := copyApmFixture(t, "pkg-nodeps") + // --dry-run after -- should reach apm without being interpreted by jf. + err := runAgentApmCmd(t, dir, "install", "--", "--dry-run") + // Zero-dep project: dry-run may succeed; either way it must not be a jf flag parse error. + if err != nil { + assert.NotContains(t, strings.ToLower(err.Error()), "unknown flag", + "native --dry-run after -- must not be rejected as a jf flag") + } +} + +// TestAgentApmPassthroughLock smoke-tests passthrough `jf agent apm lock`. +func TestAgentApmPassthroughLock(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + _, _ = publishApmFixture(t, "pkg-base-v2", "pkg-base", "1.1.0") + consumerDir := copyApmFixture(t, "pkg-consumer") + require.NoError(t, runAgentApmCmd(t, consumerDir, "install", "--yes")) + + // lock is intentionally passthrough-only (no build-info). + err := runAgentApmCmd(t, consumerDir, "lock") + // lock may be a no-op / succeed depending on apm version; just ensure the command is routed. + if err != nil { + assert.NotContains(t, strings.ToLower(err.Error()), "unknown command", + "jf agent apm lock must be recognized as passthrough") + } +} + +// --------------------------------------------------------------------------- +// Round-trip & CI (P1 #40, #50, #53) +// --------------------------------------------------------------------------- + +// TestAgentApmRoundTrip publish then install and verify lockfile (#40). +func TestAgentApmRoundTrip(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + _, _ = publishApmFixture(t, "pkg-base-v2", "pkg-base", "1.1.0") + consumerDir := copyApmFixture(t, "pkg-consumer") + require.NoError(t, runAgentApmCmd(t, consumerDir, "install", "--yes")) + + lockData, err := os.ReadFile(filepath.Join(consumerDir, "apm.lock.yaml")) // #nosec G304 + require.NoError(t, err) + assert.Contains(t, string(lockData), "pkg-base") + assert.Contains(t, string(lockData), "sha256:") +} + +// TestAgentApmCIPipeline install(BI) → publish(BI) → bp (#50 simplified). +func TestAgentApmCIPipeline(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + runSetupAgentApm(t) + + t.Setenv("CI", "true") + buildNumber := t.Name() + t.Cleanup(func() { _ = coreBuild.RemoveBuildDir(tests.AgentApmBuildName, buildNumber, "") }) + + baseDir := copyApmFixture(t, "pkg-base") + require.NoError(t, runAgentApmCmd(t, baseDir, + "publish", packageRef("pkg-base"), + "--build-name="+tests.AgentApmBuildName, + "--build-number="+buildNumber, + )) + require.NoError(t, artifactoryCli.Exec("bp", tests.AgentApmBuildName, buildNumber)) + _, found, err := tests.GetBuildInfo(serverDetails, tests.AgentApmBuildName, buildNumber) + require.NoError(t, err) + assert.True(t, found) +} + +// TestAgentApmArtifactoryUnreachable returns a clear error (#53). +func TestAgentApmArtifactoryUnreachable(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + // Point default server at a closed port. + createJfrogHomeConfig(t, false) + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog config", "") + _ = jfrogCli.Exec("rm", "default", "--quiet") + require.NoError(t, coretests.NewJfrogCli(execMain, "jfrog config", + "--access-token=dummy").Exec( + "add", "default", "--interactive=false", + "--url=http://127.0.0.1:1/", + )) + + dir := copyApmFixture(t, "pkg-base") + err := runAgentApmCmd(t, dir, "publish", packageRef("pkg-base")) + assert.Error(t, err, "unreachable Artifactory must not succeed silently") +} + +// --------------------------------------------------------------------------- +// Proxy / TLS (P1 #49; P2 #56/#57) — skip unless env configured +// --------------------------------------------------------------------------- + +func TestAgentApmWithProxy(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + if os.Getenv("HTTPS_PROXY") == "" && os.Getenv("HTTP_PROXY") == "" { + t.Skip("HTTPS_PROXY/HTTP_PROXY not set") + } + runSetupAgentApm(t) + _, _ = publishApmFixture(t, "pkg-base", "pkg-base", "1.0.0") +} + +func TestAgentApmNoProxy(t *testing.T) { + initAgentApmTest(t) + defer cleanAgentApmTest() + if os.Getenv("HTTPS_PROXY") == "" && os.Getenv("HTTP_PROXY") == "" { + t.Skip("proxy env not set") + } + t.Setenv("NO_PROXY", "*") + runSetupAgentApm(t) + _, _ = publishApmFixture(t, "pkg-base", "pkg-base", "1.0.0") +} + +// TestAgentApmCoverageSummary documents plan coverage (scenarios from APM_TEST_PLAN_FINAL.md). +func TestAgentApmCoverageSummary(t *testing.T) { + t.Log(`Agent APM e2e coverage mapped from APM_TEST_PLAN_FINAL.md: +P0 implemented: #1 setup, #2 manifest registry, #3/#4 publish+layout, #5/#6/#12 BI publish/retrieve, +#8 build props, #13/#18 install BI+checksums, #15 not-found, #19 RT sha256, #23 missing arg, +#24/#36 no registry, #25 build flags. +P1 implemented: #14 frozen, #16 update BI, #26 module, #28 flag escape, #40 round-trip, #50 CI pipeline, #53 unreachable. +Deferred (need Xray/RB/policy/CI VCS): #9-11, #27, #29-30, #31-35, #37-39, #41-48, #51-52, #54-57.`) +} diff --git a/artifactory/cli_test.go b/artifactory/cli_test.go index cab47779e..59a98cf38 100644 --- a/artifactory/cli_test.go +++ b/artifactory/cli_test.go @@ -318,10 +318,10 @@ func TestPrintTransferFilesResponse_Table(t *testing.T) { func TestPrintTransferFilesResponse_Table_FailureStatus(t *testing.T) { result := transferfilescore.TransferFilesResult{ - TotalRepositories: 2, - TotalFiles: 100, - TransferredFiles: 50, - TransferFailures: 50, + TotalRepositories: 2, + TotalFiles: 100, + TransferredFiles: 50, + TransferFailures: 50, } var buf bytes.Buffer originalErr := assert.AnError diff --git a/config/cli.go b/config/cli.go index 5477d86ee..b60100529 100644 --- a/config/cli.go +++ b/config/cli.go @@ -13,10 +13,10 @@ import ( "github.com/jfrog/jfrog-client-go/auth/cert" + commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils" "github.com/jfrog/jfrog-cli-core/v2/common/commands" coreformat "github.com/jfrog/jfrog-cli-core/v2/common/format" corecommon "github.com/jfrog/jfrog-cli-core/v2/docs/common" - commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils" coreconfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli/docs/config/add" diff --git a/general/token/cli_test.go b/general/token/cli_test.go index 13a9b627f..b41722326 100644 --- a/general/token/cli_test.go +++ b/general/token/cli_test.go @@ -181,4 +181,3 @@ func TestPrintOidcTokenResponse_UnsupportedFormat(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "unsupported format") } - diff --git a/go.mod b/go.mod index e353b5807..f9e39566b 100644 --- a/go.mod +++ b/go.mod @@ -21,8 +21,8 @@ require ( github.com/jfrog/build-info-go v1.13.1-0.20260713073853-4f3044bf0940 github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-application v1.0.2-0.20260713081138-6df6041db819 - github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0 - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728105909-591d4872e483 + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260729114809-a410267ad52a + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260729114723-9b6c8e2151dd github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260618135203-4d2bdd4ee35f github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab github.com/jfrog/jfrog-cli-security v1.31.3 @@ -252,3 +252,7 @@ require ( //replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.54.2-0.20251007084958-5eeaa42c31a6 // replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260723100012-d9e9c3412cb2 + +// Local development (RTECO-1649 / RTECO-1648 APM worktrees) — keep commented for CI: +// replace github.com/jfrog/jfrog-cli-artifactory => ../jfrog-cli-artifactory/.worktrees/task-RTECO-1648/task/RTECO-1648/rteco-1648-jf-agent-apm-implementation-document +// replace github.com/jfrog/jfrog-cli-core/v2 => ../jfrog-cli-core/.worktrees/task-RTECO-1648/task/RTECO-1648/rteco-1648-jf-agent-apm-implementation-document diff --git a/go.sum b/go.sum index 6ca3d9c23..1fdeca4d1 100644 --- a/go.sum +++ b/go.sum @@ -406,10 +406,10 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260713081138-6df6041db819 h1:nbhpYN6Sb+oPJWpEHAaNbnwJJ5R2/g/TDnpCKjC/mRA= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260713081138-6df6041db819/go.mod h1:p8yLtbmCxxQucIbLZKnWu0F+EDtj6NLXbRQCEK/nb6o= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0 h1:DVEYAyGJDn9ywGyBSa/MC3oWZsGC2DmPihv++/EDsJs= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0/go.mod h1:1vxzqW7jHBSuTNqO2vxEnhbniwq4dj5wveDuCMJX7Yo= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728105909-591d4872e483 h1:0edZnVaV7I1F/vmRb+ZudSwvk4I1RUhAgo+jqL4b0Z8= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728105909-591d4872e483/go.mod h1:MygQx8pekgPCXyXnejIAVG9S4ImGcDFmcfRPUug/0d0= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260729114809-a410267ad52a h1:QP8NO30SSZU7kTpeWzxVkVPOYiHF0PsMOwjiDrUK16s= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260729114809-a410267ad52a/go.mod h1:0wkGWPFLJWnN/KDUI6zz0Q4/6CHEfH3OS3dmmn8phqU= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260729114723-9b6c8e2151dd h1:s/2tImmmwUVBMtlnA6GUtn43C92UYKn1D20BWmw0Ack= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260729114723-9b6c8e2151dd/go.mod h1:MygQx8pekgPCXyXnejIAVG9S4ImGcDFmcfRPUug/0d0= github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260618135203-4d2bdd4ee35f h1:MV4BATdkEoUYJmdPDvaB9EBb8JQZg28n/K4X7dcmyAY= github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260618135203-4d2bdd4ee35f/go.mod h1:t2luv7YHtrKe/Yf1xLZgLOkkiPtk1DsKj0OLXL2GwYo= github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab h1:Zn/qB8LYhSu82YDtbqXwErN1RPHTHe/a3gQY6Ti/OBE= diff --git a/main_test.go b/main_test.go index 2b713da71..b652d07d6 100644 --- a/main_test.go +++ b/main_test.go @@ -18,9 +18,9 @@ import ( commandUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" "github.com/jfrog/jfrog-cli-core/v2/common/commands" "github.com/jfrog/jfrog-cli-core/v2/common/format" - corecommon "github.com/jfrog/jfrog-cli-core/v2/docs/common" "github.com/jfrog/jfrog-cli-core/v2/common/project" "github.com/jfrog/jfrog-cli-core/v2/common/spec" + corecommon "github.com/jfrog/jfrog-cli-core/v2/docs/common" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/utils/log" @@ -92,6 +92,9 @@ func setupIntegrationTests() { if *tests.TestAgentPlugins { InitAgentPluginsTests() } + if *tests.TestAgentApm { + InitAgentApmTests() + } if *tests.TestAccess { InitAccessTests() } @@ -134,6 +137,9 @@ func tearDownIntegrationTests() { if *tests.TestAgentPlugins { CleanAgentPluginsTests() } + if *tests.TestAgentApm { + CleanAgentApmTests() + } if *tests.TestTransfer { CleanTransferTests() } diff --git a/missioncontrol/cli_test.go b/missioncontrol/cli_test.go index d3214c0a1..b768e20aa 100644 --- a/missioncontrol/cli_test.go +++ b/missioncontrol/cli_test.go @@ -120,4 +120,3 @@ func TestPrintLicenseAcquireResponse_UnsupportedFormat(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "unsupported format") } - diff --git a/pipelines/cli.go b/pipelines/cli.go index 4d19fee63..904e376d8 100644 --- a/pipelines/cli.go +++ b/pipelines/cli.go @@ -8,11 +8,11 @@ import ( "os" "text/tabwriter" + commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils" "github.com/jfrog/jfrog-cli-core/v2/common/commands" coreformat "github.com/jfrog/jfrog-cli-core/v2/common/format" corecommon "github.com/jfrog/jfrog-cli-core/v2/docs/common" pipelines "github.com/jfrog/jfrog-cli-core/v2/pipelines/commands" - commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils" coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli/docs/common" @@ -22,10 +22,10 @@ import ( "github.com/jfrog/jfrog-cli/docs/pipelines/trigger" "github.com/jfrog/jfrog-cli/docs/pipelines/version" "github.com/jfrog/jfrog-cli/utils/cliutils" - clientUtils "github.com/jfrog/jfrog-client-go/utils" - clientlog "github.com/jfrog/jfrog-client-go/utils/log" plservices "github.com/jfrog/jfrog-client-go/pipelines/services" + clientUtils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" + clientlog "github.com/jfrog/jfrog-client-go/utils/log" "github.com/urfave/cli" ) diff --git a/pipelines/cli_test.go b/pipelines/cli_test.go index a37a23fa1..541ff784e 100644 --- a/pipelines/cli_test.go +++ b/pipelines/cli_test.go @@ -111,4 +111,3 @@ func TestPrintSyncStatusResponse_UnsupportedFormat(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "unsupported format") } - diff --git a/testdata/agent_apm/pkg-base-v2/.apm/skills/hello/SKILL.md b/testdata/agent_apm/pkg-base-v2/.apm/skills/hello/SKILL.md new file mode 100644 index 000000000..f01fe3f57 --- /dev/null +++ b/testdata/agent_apm/pkg-base-v2/.apm/skills/hello/SKILL.md @@ -0,0 +1,9 @@ +--- +name: hello +description: A minimal test skill published for jf agent apm end-to-end testing. +--- + +# Hello Skill + +Says hello. This skill exists purely to give the pkg-base test package real +content to publish for the jf agent apm / bi apm end-to-end test. diff --git a/testdata/agent_apm/pkg-base-v2/apm.yml b/testdata/agent_apm/pkg-base-v2/apm.yml new file mode 100644 index 000000000..29f2964cc --- /dev/null +++ b/testdata/agent_apm/pkg-base-v2/apm.yml @@ -0,0 +1,16 @@ +name: pkg-base +version: 1.1.0 +description: APM project for pkg-base +author: Uday +license: MIT +# Which agent platforms to deploy to. +# Resolution order: --target flag > this field > auto-detect from filesystem. +# Accepted values: vscode, agents, copilot, claude, cursor, opencode, codex, +# gemini, antigravity, windsurf, kiro, agent-skills, all +targets: +- claude +dependencies: + apm: [] + mcp: [] +includes: auto +scripts: {} diff --git a/testdata/agent_apm/pkg-base/.apm/skills/hello/SKILL.md b/testdata/agent_apm/pkg-base/.apm/skills/hello/SKILL.md new file mode 100644 index 000000000..f01fe3f57 --- /dev/null +++ b/testdata/agent_apm/pkg-base/.apm/skills/hello/SKILL.md @@ -0,0 +1,9 @@ +--- +name: hello +description: A minimal test skill published for jf agent apm end-to-end testing. +--- + +# Hello Skill + +Says hello. This skill exists purely to give the pkg-base test package real +content to publish for the jf agent apm / bi apm end-to-end test. diff --git a/testdata/agent_apm/pkg-base/apm.yml b/testdata/agent_apm/pkg-base/apm.yml new file mode 100644 index 000000000..aae13fb83 --- /dev/null +++ b/testdata/agent_apm/pkg-base/apm.yml @@ -0,0 +1,16 @@ +name: pkg-base +version: 1.0.0 +description: APM project for pkg-base +author: Uday +license: MIT +# Which agent platforms to deploy to. +# Resolution order: --target flag > this field > auto-detect from filesystem. +# Accepted values: vscode, agents, copilot, claude, cursor, opencode, codex, +# gemini, antigravity, windsurf, kiro, agent-skills, all +targets: +- claude +dependencies: + apm: [] + mcp: [] +includes: auto +scripts: {} diff --git a/testdata/agent_apm/pkg-consumer/.apm/skills/consumer-skill/SKILL.md b/testdata/agent_apm/pkg-consumer/.apm/skills/consumer-skill/SKILL.md new file mode 100644 index 000000000..434f811dd --- /dev/null +++ b/testdata/agent_apm/pkg-consumer/.apm/skills/consumer-skill/SKILL.md @@ -0,0 +1,9 @@ +--- +name: consumer-skill +description: A test skill that depends on pkg-base, published for jf agent apm transitive-dependency testing. +--- + +# Consumer Skill + +Depends on uday/pkg-base. Exists to test transitive dependency resolution +through the jf agent apm / bi apm pipeline. diff --git a/testdata/agent_apm/pkg-consumer/.claude/skills/consumer-skill/SKILL.md b/testdata/agent_apm/pkg-consumer/.claude/skills/consumer-skill/SKILL.md new file mode 100644 index 000000000..434f811dd --- /dev/null +++ b/testdata/agent_apm/pkg-consumer/.claude/skills/consumer-skill/SKILL.md @@ -0,0 +1,9 @@ +--- +name: consumer-skill +description: A test skill that depends on pkg-base, published for jf agent apm transitive-dependency testing. +--- + +# Consumer Skill + +Depends on uday/pkg-base. Exists to test transitive dependency resolution +through the jf agent apm / bi apm pipeline. diff --git a/testdata/agent_apm/pkg-consumer/.claude/skills/hello/SKILL.md b/testdata/agent_apm/pkg-consumer/.claude/skills/hello/SKILL.md new file mode 100644 index 000000000..f01fe3f57 --- /dev/null +++ b/testdata/agent_apm/pkg-consumer/.claude/skills/hello/SKILL.md @@ -0,0 +1,9 @@ +--- +name: hello +description: A minimal test skill published for jf agent apm end-to-end testing. +--- + +# Hello Skill + +Says hello. This skill exists purely to give the pkg-base test package real +content to publish for the jf agent apm / bi apm end-to-end test. diff --git a/testdata/agent_apm/pkg-consumer/.gitignore b/testdata/agent_apm/pkg-consumer/.gitignore new file mode 100644 index 000000000..6390e0f39 --- /dev/null +++ b/testdata/agent_apm/pkg-consumer/.gitignore @@ -0,0 +1,3 @@ + +# APM dependencies +apm_modules/ diff --git a/testdata/agent_apm/pkg-consumer/apm.yml b/testdata/agent_apm/pkg-consumer/apm.yml new file mode 100644 index 000000000..bb805e737 --- /dev/null +++ b/testdata/agent_apm/pkg-consumer/apm.yml @@ -0,0 +1,13 @@ +name: pkg-consumer +version: 1.0.0 +description: APM project for pkg-consumer +author: Uday +license: MIT +targets: +- claude +dependencies: + apm: + - uday/pkg-base#1.1.0 + mcp: [] +includes: auto +scripts: {} diff --git a/testdata/agent_apm/pkg-nodeps/apm.yml b/testdata/agent_apm/pkg-nodeps/apm.yml new file mode 100644 index 000000000..73cd6ebcb --- /dev/null +++ b/testdata/agent_apm/pkg-nodeps/apm.yml @@ -0,0 +1,15 @@ +name: pkg-nodeps +version: 1.0.0 +description: APM project for pkg-nodeps +author: Uday +# Which agent platforms to deploy to. +# Resolution order: --target flag > this field > auto-detect from filesystem. +# Accepted values: vscode, agents, copilot, claude, cursor, opencode, codex, +# gemini, antigravity, windsurf, kiro, agent-skills, all +targets: +- claude +dependencies: + apm: [] + mcp: [] +includes: auto +scripts: {} diff --git a/testdata/agent_apm/pkg-noregistry/.claude/skills/hello/SKILL.md b/testdata/agent_apm/pkg-noregistry/.claude/skills/hello/SKILL.md new file mode 100644 index 000000000..f01fe3f57 --- /dev/null +++ b/testdata/agent_apm/pkg-noregistry/.claude/skills/hello/SKILL.md @@ -0,0 +1,9 @@ +--- +name: hello +description: A minimal test skill published for jf agent apm end-to-end testing. +--- + +# Hello Skill + +Says hello. This skill exists purely to give the pkg-base test package real +content to publish for the jf agent apm / bi apm end-to-end test. diff --git a/testdata/agent_apm/pkg-noregistry/.gitignore b/testdata/agent_apm/pkg-noregistry/.gitignore new file mode 100644 index 000000000..6390e0f39 --- /dev/null +++ b/testdata/agent_apm/pkg-noregistry/.gitignore @@ -0,0 +1,3 @@ + +# APM dependencies +apm_modules/ diff --git a/testdata/agent_apm/pkg-noregistry/apm.yml b/testdata/agent_apm/pkg-noregistry/apm.yml new file mode 100644 index 000000000..bdd436178 --- /dev/null +++ b/testdata/agent_apm/pkg-noregistry/apm.yml @@ -0,0 +1,12 @@ +name: pkg-noregistry +version: 1.0.0 +description: APM project for pkg-noregistry +author: Uday +targets: +- claude +dependencies: + apm: + - uday/pkg-base#1.0.0 + mcp: [] +includes: auto +scripts: {} diff --git a/testdata/agent_apm_local_repository_config.json b/testdata/agent_apm_local_repository_config.json new file mode 100644 index 000000000..1a8653424 --- /dev/null +++ b/testdata/agent_apm_local_repository_config.json @@ -0,0 +1,5 @@ +{ + "key": "${AGENT_APM_LOCAL_REPO}", + "rclass": "local", + "packageType": "agentpackages" +} diff --git a/utils/tests/consts.go b/utils/tests/consts.go index 31fdb9704..bbf1efb49 100644 --- a/utils/tests/consts.go +++ b/utils/tests/consts.go @@ -118,6 +118,7 @@ const ( UvRemoteRepositoryConfig = "uv_remote_repository_config.json" UvVirtualRepositoryConfig = "uv_virtual_repository_config.json" AgentPluginsLocalRepositoryConfig = "agent_plugins_local_repository_config.json" + AgentApmLocalRepositoryConfig = "agent_apm_local_repository_config.json" ConanLocalRepositoryConfig = "conan_local_repository_config.json" ConanRemoteRepositoryConfig = "conan_remote_repository_config.json" ConanVirtualRepositoryConfig = "conan_virtual_repository_config.json" @@ -230,6 +231,7 @@ var ( UvRemoteRepo = "cli-uv-remote" UvVirtualRepo = "cli-uv-virtual" AgentPluginsLocalRepo = "cli-agent-plugins-local" + AgentApmLocalRepo = "cli-agent-apm-local" ConanLocalRepo = "cli-conan-local" ConanRemoteRepo = "cli-conan-remote" ConanVirtualRepo = "cli-conan-virtual" @@ -271,6 +273,7 @@ var ( PoetryBuildName = "cli-poetry-build" UvBuildName = "cli-uv-build" AgentPluginsBuildName = "cli-agent-plugins-build" + AgentApmBuildName = "cli-agent-apm-build" NixBuildName = "cli-nix-build" AlpineBuildName = "cli-alpine-build" ConanBuildName = "cli-conan-build" diff --git a/utils/tests/container.go b/utils/tests/container.go index c6f3de6c0..e85683e29 100644 --- a/utils/tests/container.go +++ b/utils/tests/container.go @@ -3,9 +3,9 @@ package tests import ( "context" "fmt" + "github.com/jfrog/jfrog-client-go/utils/log" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/mount" - "github.com/jfrog/jfrog-client-go/utils/log" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" "io" diff --git a/utils/tests/utils.go b/utils/tests/utils.go index c3de68a9d..f0abcbe13 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -73,6 +73,7 @@ var ( TestNix *bool TestAlpine *bool TestAgentPlugins *bool + TestAgentApm *bool TestConan *bool TestHelm *bool TestHuggingFace *bool @@ -142,6 +143,7 @@ func init() { TestNix = flag.Bool("test.nix", false, "Test Nix") TestAlpine = flag.Bool("test.alpine", false, "Test Alpine APK") TestAgentPlugins = flag.Bool("test.agentPlugins", false, "Test Agent Plugins") + TestAgentApm = flag.Bool("test.agentApm", false, "Test Agent APM (Agent Package Manager)") TestConan = flag.Bool("test.conan", false, "Test Conan") TestHelm = flag.Bool("test.helm", false, "Test Helm") TestHuggingFace = flag.Bool("test.huggingface", false, "Test HuggingFace") @@ -325,6 +327,7 @@ var reposConfigMap = map[*string]string{ &UvRemoteRepo: UvRemoteRepositoryConfig, &UvVirtualRepo: UvVirtualRepositoryConfig, &AgentPluginsLocalRepo: AgentPluginsLocalRepositoryConfig, + &AgentApmLocalRepo: AgentApmLocalRepositoryConfig, &NixLocalRepo: NixLocalRepositoryConfig, &NixRemoteRepo: NixRemoteRepositoryConfig, &NixVirtualRepo: NixVirtualRepositoryConfig, @@ -405,6 +408,7 @@ func GetNonVirtualRepositories() map[*string]string { TestNix: {&NixLocalRepo, &NixRemoteRepo}, TestAlpine: {&AlpineLocalRepo, &AlpineRemoteRepo}, TestAgentPlugins: {&AgentPluginsLocalRepo}, + TestAgentApm: {&AgentApmLocalRepo}, TestConan: {&ConanLocalRepo, &ConanRemoteRepo}, TestHelm: {&HelmLocalRepo}, TestHuggingFace: {&HuggingFaceLocalRepo}, @@ -439,6 +443,7 @@ func GetVirtualRepositories() map[*string]string { TestNix: {&NixVirtualRepo}, TestAlpine: {&AlpineVirtualRepo}, TestAgentPlugins: {}, + TestAgentApm: {}, TestConan: {&ConanVirtualRepo}, TestHelm: {}, TestHuggingFace: {}, @@ -484,6 +489,7 @@ func GetBuildNames() []string { TestNix: {&NixBuildName}, TestAlpine: {&AlpineBuildName}, TestAgentPlugins: {&AgentPluginsBuildName}, + TestAgentApm: {&AgentApmBuildName}, TestConan: {&ConanBuildName}, TestHelm: {&HelmBuildName}, TestHuggingFace: {&HuggingFaceBuildName}, @@ -548,6 +554,7 @@ func getSubstitutionMap() map[string]string { "${UV_REMOTE_REPO}": UvRemoteRepo, "${UV_VIRTUAL_REPO}": UvVirtualRepo, "${AGENT_PLUGINS_LOCAL_REPO}": AgentPluginsLocalRepo, + "${AGENT_APM_LOCAL_REPO}": AgentApmLocalRepo, "${NIX_LOCAL_REPO}": NixLocalRepo, "${NIX_REMOTE_REPO}": NixRemoteRepo, "${NIX_VIRTUAL_REPO}": NixVirtualRepo, @@ -628,6 +635,7 @@ func AddTimestampToGlobalVars() { UvRemoteRepo += uniqueSuffix UvVirtualRepo += uniqueSuffix AgentPluginsLocalRepo += uniqueSuffix + AgentApmLocalRepo += uniqueSuffix NixLocalRepo += uniqueSuffix NixRemoteRepo += uniqueSuffix NixVirtualRepo += uniqueSuffix @@ -666,6 +674,7 @@ func AddTimestampToGlobalVars() { PipenvBuildName += uniqueSuffix PoetryBuildName += uniqueSuffix AgentPluginsBuildName += uniqueSuffix + AgentApmBuildName += uniqueSuffix UvBuildName += uniqueSuffix NixBuildName += uniqueSuffix AlpineBuildName += uniqueSuffix