Skip to content

[CI] Support package discovery in nested directories - WIP#18495

Draft
mrodm wants to merge 17 commits intoelastic:mainfrom
mrodm:allow-multiple-directories
Draft

[CI] Support package discovery in nested directories - WIP#18495
mrodm wants to merge 17 commits intoelastic:mainfrom
mrodm:allow-multiple-directories

Conversation

@mrodm
Copy link
Copy Markdown
Collaborator

@mrodm mrodm commented Apr 17, 2026

Summary

This PR centralises package discovery logic into a new citools.ListPackages function and uses it across packagenames and codeowners, replacing the previously duplicated filepath.WalkDir implementations. As a side effect, both checks now support packages organised in nested directory structures (e.g. packages/category/my_package), not just flat packages/my_package layouts.

Proposed commit

WHAT:

Allow packages to be placed in nested subdirectories under packages/
(e.g. packages/<category>/<name>) instead of being restricted to the flat
packages/<name> layout. Previously, codeowners
discovery logic would silently ignore any package not sitting directly under
packages/, meaning nested packages would bypass ownership validation entirely.

WHY:

As the number of integrations grows, being able to organise packages into
category subdirectories becomes necessary to keep the repository manageable.
The existing tooling hard-coded a single-level walk, blocking this. This change
makes nested directory layouts a supported and validated structure across all
CI tooling, so teams can start grouping packages without breaking checks.

Changes

dev/citools

  • Add ListPackages(dir string) ([]string, error) — walks a directory recursively, returns sorted paths of all valid packages found. A package is considered valid when its manifest.yml contains non-empty format_version, name, type (one of integration, input, content) and version fields.
  • Extend packageManifest struct with FormatVersion, Type and Version fields, and add an IsValid() method, mirroring the logic previously only present in dev/packagenames.
  • Expose readPackageManifest as ReadPackageManifest so it can be reused from other packages.

dev/packagenames

  • Remove manifest.go and its local packageManifest type; replace all usages with citools.ReadPackageManifest.
  • Replace the internal walkPackagePaths function with a call to citools.ListPackages.

dev/codeowners

  • Replace the inline filepath.WalkDir loop in validatePackages with a call to citools.ListPackages, removing the duplicate discovery logic.
  • Update testdata manifests with the required format_version, name, type and version fields.
  • Add a nested_packages testdata fixture with packages at two directory levels (top-level and under a category/ subdirectory, one with data streams).
  • Add five new TestValidatePackages cases covering: explicit per-package owners, missing owner for a nested package, category-level inherited ownership, valid per-stream ownership in a nested package, and missing stream owner in a nested package.

magefile.go

  • Add a ListPackages mage target that prints all package paths under the packages/ directory, one per line.

.buildkite/scripts/common.sh

  • Add should_test_package helper function that encapsulates the pushd/is_pr_affected/popdpattern along with fatal-error handling, replacing near-identical blocks that were duplicated intest_integrations_with_serverless.shandtrigger_integrations_in_parallel.sh`.

.buildkite/scripts/build_packages.sh

  • Replace the find-based flat directory loop with list_all_directories (backed by mage listPackages) so packages in nested subdirectories are discovered correctly.
  • Replace with_go with with_mage (which calls with_go internally) so mage is available when list_all_directories runs.

Backport pipeline

  • Remove PACKAGE_FOLDER_NAME as a required input parameter from pipeline.backport.yml and backport_branch.sh. The package path is now resolved automatically by a new get_package_path function that looks up the package whose manifest.yml name field matches the given PACKAGE_NAME, using mage listPackages to iterate all discovered packages.
  • Update docs/extend/developer-workflow-support-old-package.md to reflect the removed parameter.

Checklist

  • I have reviewed tips for building integrations and this pull request is aligned with them.
  • I have verified that all data streams collect metrics or logs.
  • I have added an entry to my package's changelog.yml file.
  • I have verified that Kibana version constraints are current according to guidelines.
  • I have verified that any added dashboard complies with Kibana's Dashboard good practices

Author's Checklist

  • mage check passes locally with no errors
  • Verified that mage ListPackages correctly lists all packages,
    including any nested under a subdirectory
  • Confirmed that the package build and publish pipeline handles packages
    in nested directories correctly
  • Tested the package discovery and validation logic against the Serverless CI pipeline
  • Remove test/debug changes applied to list_all_directories.
  • Remove test/debug changes applied to .buildkite/scripts/build_packages.sh
  • Update backport script to take into account packages in nested directories correctly

How to test this PR locally

Related issues


This PR was drafted with the assistance of Claude (claude-sonnet-4-6).

jsoriano and others added 7 commits April 16, 2026 19:20
- Add ListPackages function to dev/citools/packages.go that walks a
  directory and returns sorted paths of all valid packages found.
- Extend packageManifest struct with FormatVersion, Type and Version
  fields, and add IsValid() method mirroring dev/packagenames logic.
- Refactor dev/packagenames to delegate package discovery to
  citools.ListPackages, removing the now-redundant walkPackagePaths.

Generated with Claude (claude-sonnet-4-6)
- Rename readPackageManifest to ReadPackageManifest so it can be used
  from other packages.
- Remove dev/packagenames/manifest.go and its local packageManifest
  type, replacing all usages with citools.ReadPackageManifest.
- Update all internal callers (kibana.go, logsdb.go, subscription.go,
  packages.go) to use the renamed function.

Generated with Claude (claude-sonnet-4-6)
- Replace the inline WalkDir loop in validatePackages with a call to
  citools.ListPackages, removing duplicated package-discovery logic.
- Update testdata manifests (devexp, package_1) with the required
  format_version, name, type and version fields so they are recognised
  as valid packages by citools.ListPackages/IsValid.

Generated with Claude (claude-sonnet-4-6)
- Add nested_packages testdata with packages at two directory levels:
  one at the top level and two under a category/ subdirectory, one of
  which has data streams.
- Add five new TestValidatePackages cases covering: all packages with
  explicit owners, a missing owner for a nested package, category-level
  ownership inherited by nested packages, valid per-stream ownership in
  a nested package, and missing stream owner in a nested package.

Generated with Claude (claude-sonnet-4-6)
Add a ListPackages target that calls citools.ListPackages to print all
package paths found under the packages directory, one per line.

Generated with Claude (claude-sonnet-4-6)
@mrodm mrodm self-assigned this Apr 17, 2026
Comment thread dev/codeowners/codeowners.go
Comment on lines 831 to 833
list_all_directories() {
find . -maxdepth 1 -mindepth 1 -type d | xargs -I {} basename {} | sort
mage listPackages | grep -E '^packages/(elastic_package_registry|nginx)$'
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical scripts/common.sh:831

list_all_directories() now hardcodes a grep filter that returns only two package names (elastic_package_registry and nginx). Since callers trigger_integrations_in_parallel.sh and test_integrations_with_serverless.sh iterate over this output to drive CI pipelines, all other packages are silently excluded from testing and publishing. If this restriction is intentional, consider documenting why only these two packages should run; otherwise, the filter should be removed to restore full package coverage.

 list_all_directories() {
-    mage listPackages | grep -E '^packages/(elastic_package_registry|nginx)$'
+    mage listPackages
 }
🤖 Copy this AI Prompt to have your agent fix this:
In file .buildkite/scripts/common.sh around lines 831-833:

`list_all_directories()` now hardcodes a grep filter that returns only two package names (`elastic_package_registry` and `nginx`). Since callers `trigger_integrations_in_parallel.sh` and `test_integrations_with_serverless.sh` iterate over this output to drive CI pipelines, all other packages are silently excluded from testing and publishing. If this restriction is intentional, consider documenting why only these two packages should run; otherwise, the filter should be removed to restore full package coverage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be removed before merging the PR. This grep is there to test just a subset of packages instead of the ~400 packages.

mrodm added 3 commits April 17, 2026 13:11
- Add -d "${WORKSPACE}" to the mage call in list_all_directories so it
  always finds magefile.go regardless of the working directory when invoked.
- Remove pushd packages / popd wrappers in trigger_integrations_in_parallel.sh
  and test_integrations_with_serverless.sh since list_all_directories now
  returns full paths (e.g. packages/nginx) relative to the repo root.
- Fix process_package: the parameter rename from package to package_path was
  incomplete — pushd, teardown calls, and log lines still referenced the
  old ${package} variable (now unbound). Use ${package_path} for directory
  operations and ${package_name} (from package_name_manifest) for labels.
- Update test_one_package.sh to derive parent_path and package_name from
  the received package_path instead of assuming a packages/ prefix.
- Consistently rename local variables from package to package_name across
  check_package, build_zip_package, teardown_*_package, run_tests_package,
  and related helpers.

Generated with Claude (claude-sonnet-4-6)
- Export manifestFileName as ManifestFileName in dev/citools/packages.go
  so it can be reused across packages.
- Remove the duplicate manifestFileName constant from dev/packagenames
  and replace its usage with citools.ManifestFileName.
- Replace the hardcoded "manifest.yml" string in dev/codeowners with
  citools.ManifestFileName.

Generated with Claude (claude-sonnet-4-6)
Generated with Claude (claude-sonnet-4-6)
Comment thread .buildkite/scripts/trigger_integrations_in_parallel.sh
Comment thread .buildkite/scripts/common.sh
Comment thread .buildkite/scripts/trigger_integrations_in_parallel.sh
@mrodm mrodm force-pushed the allow-multiple-directories branch from 17332f9 to e2454f2 Compare April 17, 2026 12:00
Comment thread .buildkite/scripts/trigger_integrations_in_parallel.sh
Extract the pushd/is_pr_affected/popd pattern with error handling
into a reusable should_test_package function, to be used from
test_integrations_with_serverless.sh and trigger_integrations_in_parallel.sh.

Generated with the assistance of Claude Sonnet 4.6
@elastic-vault-github-plugin-prod
Copy link
Copy Markdown

🚀 Benchmarks report

To see the full report comment with /test benchmark fullreport

Replace find-based flat directory loop with list_all_directories so that
packages organised in subdirectories are discovered correctly.
Also replace `cat manifest.yml | yq` with direct `yq manifest.yml` calls.

Generated with the assistance of Claude Sonnet 4.6
Comment on lines 159 to +161

buildkite-agent pipeline upload "${PIPELINE_FILE}"
cat "${PIPELINE_FILE}"
# buildkite-agent pipeline upload "${PIPELINE_FILE}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical scripts/build_packages.sh:159

The buildkite-agent pipeline upload command is replaced with cat, so the pipeline that signs and publishes packages is never uploaded to Buildkite. Built packages will remain in artifacts but never reach the package registry.

 cat "${PIPELINE_FILE}"
-# buildkite-agent pipeline upload "${PIPELINE_FILE}"
+buildkite-agent pipeline upload "${PIPELINE_FILE}"
🤖 Copy this AI Prompt to have your agent fix this:
In file .buildkite/scripts/build_packages.sh around lines 159-161:

The `buildkite-agent pipeline upload` command is replaced with `cat`, so the pipeline that signs and publishes packages is never uploaded to Buildkite. Built packages will remain in artifacts but never reach the package registry.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change to allow debugging the script safely. This will be reverted before merging the PR.

@mrodm mrodm force-pushed the allow-multiple-directories branch from c280270 to 71e903e Compare April 17, 2026 14:50
Comment on lines 88 to 91
if skipPublishing ; then
echo "packageStoragePublish: not the main branch or a backport branch, nothing will be published"
exit 0
# exit 0
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium scripts/build_packages.sh:88

When skipPublishing returns true, the script prints "nothing will be published" but continues executing instead of exiting. This causes the script to proceed through expensive build steps (tool installation, package building, artifact copying, pipeline generation) on branches that should skip publishing entirely, wasting resources and potentially causing side effects.

if skipPublishing ; then
    echo "packageStoragePublish: not the main branch or a backport branch, nothing will be published"
-    # exit 0
+    exit 0
 fi
🤖 Copy this AI Prompt to have your agent fix this:
In file .buildkite/scripts/build_packages.sh around lines 88-91:

When `skipPublishing` returns true, the script prints "nothing will be published" but continues executing instead of exiting. This causes the script to proceed through expensive build steps (tool installation, package building, artifact copying, pipeline generation) on branches that should skip publishing entirely, wasting resources and potentially causing side effects.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes related to debugging. It will be reverted before merging.

SIGNING_STEP_KEY: "sign-service"
ARTIFACTS_FOLDER: "packageArtifacts"
DRY_RUN: "${DRY_RUN}"
DRY_RUN: "true"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium scripts/build_packages.sh:150

DRY_RUN is hardcoded to "true" on line 150, so the trigger_publish_packages.sh step always runs in dry-run mode regardless of the DRY_RUN environment variable value. Packages will never actually be published when the pipeline intends to run for real. Consider reverting to "${DRY_RUN}" to respect the environment variable.

Suggested change
DRY_RUN: "true"
DRY_RUN: "${DRY_RUN}"
🤖 Copy this AI Prompt to have your agent fix this:
In file .buildkite/scripts/build_packages.sh around line 150:

`DRY_RUN` is hardcoded to `"true"` on line 150, so the `trigger_publish_packages.sh` step always runs in dry-run mode regardless of the `DRY_RUN` environment variable value. Packages will never actually be published when the pipeline intends to run for real. Consider reverting to `"${DRY_RUN}"` to respect the environment variable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes related to debugging. It will be reverted before merging.

Replace with_go with with_mage so that mage is available when
list_all_directories is called. with_mage already calls with_go
internally so Go setup is preserved.

Generated with the assistance of Claude Sonnet 4.6
Comment on lines 87 to 92

if skipPublishing ; then
echo "packageStoragePublish: not the main branch or a backport branch, nothing will be published"
exit 0
# exit 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium scripts/build_packages.sh:87

When skipPublishing returns true, the script prints "nothing will be published" but continues executing instead of exiting. It proceeds to build packages, copy artifacts, and generate the signing/publishing pipeline YAML, wasting CI resources on non-main/non-backport branches. If the DRY_RUN or buildkite-agent pipeline upload guards are re-enabled later, packages could be signed and published from arbitrary feature branches.

@@ -88,5 +88,5 @@
 if skipPublishing ; then
     echo "packageStoragePublish: not the main branch or a backport branch, nothing will be published"
-    # exit 0
+    exit 0
 fi
 
 add_bin_path
🤖 Copy this AI Prompt to have your agent fix this:
In file .buildkite/scripts/build_packages.sh around lines 87-92:

When `skipPublishing` returns true, the script prints "nothing will be published" but continues executing instead of exiting. It proceeds to build packages, copy artifacts, and generate the signing/publishing pipeline YAML, wasting CI resources on non-main/non-backport branches. If the `DRY_RUN` or `buildkite-agent pipeline upload` guards are re-enabled later, packages could be signed and published from arbitrary feature branches.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes related to debugging. It will be reverted before merging.

Add a termination check for ownerDir == "/" to prevent an infinite loop
when filepath.Dir reaches the filesystem root on non-Windows systems,
where filepath.Dir("/") returns "/" instead of ".".

Generated with the assistance of Claude Sonnet 4.6
return nil
}

func (codeowners *githubOwners) findOwnerForFile(path string) ([]string, bool) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low codeowners/codeowners.go:203

The loop in findOwnerForFile terminates when ownerDir becomes . or / before checking the map for a root-level owner entry (key /). Files in subdirectories never match a root CODEOWNERS entry because the final parent directory is never searched.

🤖 Copy this AI Prompt to have your agent fix this:
In file dev/codeowners/codeowners.go around line 203:

The loop in `findOwnerForFile` terminates when `ownerDir` becomes `.` or `/` **before** checking the map for a root-level owner entry (key `/`). Files in subdirectories never match a root CODEOWNERS entry because the final parent directory is never searched.

Evidence trail:
dev/codeowners/codeowners.go lines 203-216 at REVIEWED_COMMIT: The function `findOwnerForFile` shows the loop structure where the break condition `if ownerDir == "." || ownerDir == "/"` is checked AFTER updating `ownerDir` but BEFORE the next iteration's map lookup. This means when `ownerDir` becomes `.` (for relative paths) or `/` (for absolute paths), the loop breaks without checking the map for the root-level owner entry.

Comment on lines +208 to +210
if filepath.IsAbs(path) {
path = strings.TrimPrefix(path, string(filepath.Separator))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low codeowners/codeowners.go:208

On Windows, findOwnerForFile with a path like C:\foo\bar produces incorrect lookup keys. filepath.IsAbs returns true, but strings.TrimPrefix with filepath.Separator (\) fails to trim C from C:\foo\bar, leaving the path unchanged. The subsequent loop then searches for keys like /C:/foo which will never match the expected codeowners entries (which use repository-relative paths). Consider using filepath.ToSlash on the full path and removing a leading / instead of relying on platform-specific separator behavior.

-	if filepath.IsAbs(path) {
-		path = strings.TrimPrefix(path, string(filepath.Separator))
-	}
+	path = filepath.ToSlash(path)
+	if strings.HasPrefix(path, "/") {
+		path = strings.TrimPrefix(path, "/")
+	}
🤖 Copy this AI Prompt to have your agent fix this:
In file dev/codeowners/codeowners.go around lines 208-210:

On Windows, `findOwnerForFile` with a path like `C:\foo\bar` produces incorrect lookup keys. `filepath.IsAbs` returns true, but `strings.TrimPrefix` with `filepath.Separator` (`\`) fails to trim `C` from `C:\foo\bar`, leaving the path unchanged. The subsequent loop then searches for keys like `/C:/foo` which will never match the expected codeowners entries (which use repository-relative paths). Consider using `filepath.ToSlash` on the full path and removing a leading `/` instead of relying on platform-specific separator behavior.

Evidence trail:
dev/codeowners/codeowners.go lines 203-222 at REVIEWED_COMMIT: The `findOwnerForFile` function at line 208-210 uses `strings.TrimPrefix(path, string(filepath.Separator))` to handle absolute paths. On Windows, `filepath.Separator` is `\`, but Windows absolute paths like `C:\foo\bar` start with a drive letter, not a separator. Go's `strings.TrimPrefix` only trims if the string starts with the prefix, so nothing is trimmed. Line 213 then constructs lookup keys with `/C:/foo` format which won't match repository-relative codeowners entries.

@elasticmachine
Copy link
Copy Markdown

💚 Build Succeeded

History

cc @mrodm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for an additional level of directories under packages/

3 participants