Skip to content

Commit 2844f9d

Browse files
mvicknrclaude
andauthored
chore: Add test coverage, code cleanup (#21)
* chore: Enable reading multiple agent control schemas * chore: Make main more maintainable * chore: Add back in SHA validation * chore: Add test coverage, remove duplicate tests, parameterize tests Co-Authored-By: Claude <noreply@anthropic.com> * chore: Fix small go formatting issues * chore: Documentation updates * chore: Update to agreed-upon agent types * revert: Manual revert of chore: Enable reading multiple agent control schemas --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 11de56c commit 2844f9d

File tree

22 files changed

+1556
-939
lines changed

22 files changed

+1556
-939
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copied during tests from integration-test/agent-flow/
22
.fleetControl/
3+
!integration-test/agent-flow/.fleetControl/
34

45
# Build output
56
agent-metadata-action

CLAUDE.md

Lines changed: 338 additions & 118 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
# Agent Metadata Action
44

5-
A GitHub Action that reads agent configuration metadata from the calling repository. This action parses the `.fleetControl/configurationDefinitions.yml` file and makes the configuration data available in New Relic.
5+
A GitHub Action that reads agent configuration metadata from the calling repository. There are 2 scearios to use this action:
6+
1. An agent release - This action parses the `.fleetControl/configurationDefinitions.yml` file and makes the configuration data and metadata available in New Relic.
7+
2. A docs update for an agent release - This action parses the frontmatter of the docs mdx files and makes the metadata available in New Relic.
68

79
## Installation
810

@@ -25,13 +27,14 @@ This action requires OAuth credentials to authenticate with New Relic services.
2527
These must be passed as action inputs using the `with:` parameter in your workflow.
2628

2729
### Example Workflow For Releasing a New Agent Version
28-
This action automatically checks out your repository at the specified version tag, then reads the `.fleetControl/configurationDefinitions.yml` file and other associated files in `/fleetControl` and saves the agent information in New Relic. The action handles the checkout internally, so you don't need to include a separate `actions/checkout` step. If you do not want to use this action, you can call New Relic directly to add the agent information.
30+
This action automatically checks out your repository at the specified version tag, then reads the `.fleetControl/configurationDefinitions.yml` file and other associated files in `/fleetControl` and saves the agent information in New Relic.
2931

3032
```yaml
3133
name: Process Agent Metadata
3234
on:
33-
push:
34-
branches: [main]
35+
release:
36+
types:
37+
- published
3538
3639
jobs:
3740
read-metadata:
@@ -42,14 +45,13 @@ jobs:
4245
with:
4346
newrelic-client-id: ${{ secrets.OAUTH_CLIENT_ID }}
4447
newrelic-private-key: ${{ secrets.OAUTH_CLIENT_SECRET }}
45-
agent-type: dotnet # Required: The type of agent (e.g., dotnet, java, python)
46-
version: 1.0.0 # Required: will be used to check out appropriate release tag
48+
agent-type: dotnet-agent # Required for agent release workflow: The type of agent (e.g., nodejs-agent, java-agent)
49+
version: 1.0.0 # Required for agent release workflow: will be used to check out appropriate release tag
4750
cache: true # Optional: Enable Go build cache (default: true)
4851
```
4952

50-
### Example Workflow For Updating Docs Metadata on an Existing Agent Version
51-
This action automatically checks out the calling repo's commit, detects the changed release notes and saves the agent metadata in New Relic. The action handles the checkout internally, so you don't need to include a separate `actions/checkout` step. If you do not want to use this action, you can call New Relic directly to add the agent metadata.
52-
53+
### Example Workflow For Updating Docs Metadata for a new/existing Agent Version
54+
This action should be triggered on a push to the main docs branch. It will automatically detect the changed release notes in the push and save the agent metadata in New Relic.
5355

5456
```yaml
5557
name: Process Agent Metadata
@@ -75,30 +77,24 @@ For the agent scenario, the action expects a YAML file at `.fleetControl/configu
7577

7678
```yaml
7779
configurationDefinitions:
78-
- platform: "kubernetes" # or "host" or "all" if there is no distinction
80+
- platform: "KUBERNETESCLUSTER" # or "HOST" or "ALL" if there is no distinction
7981
description: "Description of the configuration"
80-
type: "config-type"
82+
type: "agent-config"
8183
version: "1.0.0" -- config schema version
82-
format: "json" -- format of the agent config file
84+
format: "yml" -- format of the agent config file
8385
schema: "./schemas/config-schema.json"
8486
```
8587

86-
**All fields are required.** The action validates each configuration entry and will fail with a clear error message if any required field is missing (version, platform, description, type, format, schema).
8788

8889
**Dec 2025 - schema temporarily optional until full functionality is ready
8990

9091
**Schema paths must be relative to the `.fleetControl` directory and cannot use directory traversal (`..`) for security.
9192

9293
## Building
9394

94-
To build the action locally:
95-
9695
```bash
9796
# Build the binary
9897
go build -o agent-metadata-action ./cmd/agent-metadata-action
99-
100-
# Run locally
101-
/bin/bash /Users/mvick/IdeaProjects/agent-metadata-action/run_local.sh
10298
```
10399

104100
## Testing

cmd/agent-metadata-action/main.go

Lines changed: 121 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8+
"path/filepath"
89

910
"agent-metadata-action/internal/client"
1011
"agent-metadata-action/internal/config"
@@ -14,7 +15,7 @@ import (
1415

1516
// metadataClient interface for testing
1617
type metadataClient interface {
17-
SendMetadata(ctx context.Context, agentType string, metadata *models.AgentMetadata) error
18+
SendMetadata(ctx context.Context, agentType string, agentVersion string, metadata *models.AgentMetadata) error
1819
}
1920

2021
// createMetadataClientFunc is a variable that holds the function to create a metadata client
@@ -25,114 +26,151 @@ var createMetadataClientFunc = func(baseURL, token string) metadataClient {
2526

2627
func main() {
2728
if err := run(); err != nil {
28-
fmt.Fprintf(os.Stderr, "::error::%v\n", err)
29+
_, _ = fmt.Fprintf(os.Stderr, "::error::%v\n", err)
2930
os.Exit(1)
3031
}
3132
}
3233

3334
func run() error {
34-
workspace := config.GetWorkspace()
35+
// Validate required environment and setup
36+
workspace, token, err := validateEnvironment()
37+
if err != nil {
38+
return err
39+
}
3540

36-
// Workspace is required
41+
// Create metadataClient
42+
ctx := context.Background()
43+
metadataClient := createMetadataClientFunc(config.GetMetadataURL(), token)
44+
45+
// Determine which flow to execute
46+
agentType := config.GetAgentType()
47+
agentVersion := config.GetVersion()
48+
49+
if agentType != "" && agentVersion != "" {
50+
return runAgentFlow(ctx, metadataClient, workspace, agentType, agentVersion)
51+
}
52+
53+
return runDocsFlow(ctx, metadataClient)
54+
}
55+
56+
// validateEnvironment checks required environment variables and workspace
57+
func validateEnvironment() (workspace string, token string, err error) {
58+
workspace = config.GetWorkspace()
3759
if workspace == "" {
38-
return fmt.Errorf("GITHUB_WORKSPACE is required but not set")
60+
return "", "", fmt.Errorf("GITHUB_WORKSPACE is required but not set")
3961
}
4062

41-
// Validate workspace directory exists
4263
if _, err := os.Stat(workspace); err != nil {
43-
return fmt.Errorf("error reading configs: workspace directory does not exist: %s", workspace)
64+
return "", "", fmt.Errorf("workspace directory does not exist: %s", workspace)
4465
}
4566

46-
// Get OAuth token from environment (set by action.yml authentication step)
47-
token := config.GetToken()
67+
token = config.GetToken()
4868
if token == "" {
49-
return fmt.Errorf("NEWRELIC_TOKEN is required but not set")
69+
return "", "", fmt.Errorf("NEWRELIC_TOKEN is required but not set")
5070
}
51-
fmt.Println("::notice::OAuth token loaded from environment")
5271

53-
// Create instrumentation client
54-
ctx := context.Background()
55-
metadataClient := createMetadataClientFunc(config.GetMetadataURL(), token)
72+
fmt.Println("::notice::Environment validated successfully")
73+
return workspace, token, nil
74+
}
5675

57-
agentType := config.GetAgentType()
58-
agentVersion := config.GetVersion()
76+
// runAgentFlow handles the agent repository workflow
77+
func runAgentFlow(ctx context.Context, client metadataClient, workspace, agentType, agentVersion string) error {
78+
fmt.Println("::debug::Running agent repository flow")
5979

60-
if agentType != "" && agentVersion != "" { // Scenario 1: Agent repo flow
61-
fmt.Println("::debug::Agent scenario")
62-
fleetControlPath := workspace + "/.fleetControl"
63-
if _, err := os.Stat(fleetControlPath); err != nil {
64-
return fmt.Errorf("error ./fleetControl folder does not exist: %s", fleetControlPath)
65-
} else {
66-
fmt.Printf("::debug::Reading config from workspace: %s\n", workspace)
67-
68-
configs, err := loader.ReadConfigurationDefinitions(workspace)
69-
if err != nil {
70-
return fmt.Errorf("error reading configs: %w", err)
71-
}
72-
73-
fmt.Println("::notice::Successfully read configs file")
74-
fmt.Printf("::debug::Found %d configs\n", len(configs))
75-
76-
// @todo need to update this to read a list of files for future use
77-
agentControl, err := loader.LoadAndEncodeAgentControl(workspace)
78-
if err != nil {
79-
fmt.Printf("::debug::Unable to read agent control file: %s\n", workspace)
80-
} else {
81-
fmt.Println("::notice::Successfully read agent control file")
82-
}
83-
84-
metadata := loader.LoadMetadataForAgents(agentVersion)
85-
86-
// @todo will need to add agentRequirements here in a future PR
87-
88-
agentMetadata := models.AgentMetadata{
89-
ConfigurationDefinitions: configs,
90-
Metadata: metadata,
91-
AgentControl: agentControl,
92-
}
93-
94-
printJSON("Agent Metadata", agentMetadata)
95-
96-
// Send metadata to instrumentation service
97-
fmt.Println("::debug::Sending metadata to instrumentation service...")
98-
if err := metadataClient.SendMetadata(ctx, agentType, &agentMetadata); err != nil {
99-
return fmt.Errorf("failed to send metadata: %w", err)
100-
}
101-
fmt.Println("::notice::Successfully sent metadata to instrumentation service")
102-
}
103-
} else { // Scenario 2: Docs flow
104-
fmt.Println("::debug::Docs scenario")
105-
106-
metadata, err := loader.LoadMetadataForDocs()
107-
if err != nil {
108-
// warn but don't fail the docs push - this data is useful but not required at this time
109-
fmt.Printf("::warn::Error reading metadata %s \n", err)
110-
} else {
111-
for _, currMetadata := range metadata {
112-
fmt.Printf("::debug::Found metadata for %s %s \n", currMetadata.AgentType, currMetadata.AgentMetadataFromDocs["version"])
113-
printJSON("Docs Metadata", currMetadata.AgentMetadataFromDocs)
114-
115-
currAgentMetadata := models.AgentMetadata{
116-
Metadata: currMetadata.AgentMetadataFromDocs,
117-
}
118-
119-
if err := metadataClient.SendMetadata(ctx, currMetadata.AgentType, &currAgentMetadata); err != nil {
120-
fmt.Printf("::warn::Failed to send docs metadata to instrumentation service for agent type: %s \n", currMetadata.AgentType)
121-
} else {
122-
fmt.Printf("::notice::Successfully sent docs metadata to instrumentation service for agent type: %s \n", currMetadata.AgentType)
123-
}
124-
}
80+
// Check for .fleetControl directory
81+
fleetControlPath := filepath.Join(workspace, config.GetRootFolderForAgentRepo())
82+
if _, err := os.Stat(fleetControlPath); err != nil {
83+
return fmt.Errorf("%s directory does not exist: %s", config.GetRootFolderForAgentRepo(), fleetControlPath)
84+
}
85+
86+
// Load configuration definitions (required)
87+
configs, err := loader.ReadConfigurationDefinitions(workspace)
88+
if err != nil {
89+
return fmt.Errorf("failed to read configuration definitions: %w", err)
90+
}
91+
fmt.Printf("::notice::Loaded %d configuration definitions\n", len(configs))
92+
93+
// Load agent control (optional)
94+
agentControl, err := loader.LoadAndEncodeAgentControl(workspace)
95+
if err != nil {
96+
fmt.Println("::warn::Unable to load agent control files - continuing without them")
97+
} else {
98+
fmt.Printf("::notice::Loaded %d agent control files\n", len(agentControl))
99+
}
100+
101+
// Build metadata
102+
metadata := models.AgentMetadata{
103+
ConfigurationDefinitions: configs,
104+
Metadata: loader.LoadMetadataForAgents(agentVersion),
105+
AgentControlDefinitions: agentControl,
106+
}
107+
108+
printJSON("Agent Metadata", metadata)
109+
110+
// Send to service
111+
if err := client.SendMetadata(ctx, agentType, agentVersion, &metadata); err != nil {
112+
return fmt.Errorf("failed to send metadata for %s: %w", agentType, err)
113+
}
114+
115+
fmt.Printf("::notice::Successfully sent metadata for %s version %s\n", agentType, agentVersion)
116+
return nil
117+
}
118+
119+
// runDocsFlow handles the documentation repository workflow
120+
func runDocsFlow(ctx context.Context, client metadataClient) error {
121+
fmt.Println("::debug::Running documentation flow")
122+
123+
// Load metadata from changed MDX files
124+
metadataList, err := loader.LoadMetadataForDocs()
125+
if err != nil {
126+
return fmt.Errorf("failed to load metadata from docs: %w", err)
127+
}
128+
129+
if len(metadataList) == 0 {
130+
fmt.Println("::notice::No metadata changes detected")
131+
return nil
132+
}
133+
134+
fmt.Printf("::notice::Processing %d metadata entries\n", len(metadataList))
135+
136+
// Send each metadata entry separately
137+
successCount := 0
138+
for _, entry := range metadataList {
139+
if err := sendDocsMetadata(ctx, client, entry); err != nil {
140+
fmt.Printf("::warn::Failed to send metadata for %s: %v\n", entry.AgentType, err)
141+
continue
125142
}
143+
successCount++
144+
}
145+
146+
fmt.Printf("::notice::Successfully sent %d of %d metadata entries\n", successCount, len(metadataList))
147+
return nil
148+
}
149+
150+
// sendDocsMetadata sends a single docs metadata entry to the service
151+
func sendDocsMetadata(ctx context.Context, client metadataClient, entry loader.MetadataForDocs) error {
152+
version, _ := entry.AgentMetadataFromDocs["version"].(string)
153+
154+
metadata := models.AgentMetadata{
155+
Metadata: entry.AgentMetadataFromDocs,
156+
}
157+
158+
printJSON(fmt.Sprintf("Docs Metadata (%s %s)", entry.AgentType, version), entry.AgentMetadataFromDocs)
159+
160+
if err := client.SendMetadata(ctx, entry.AgentType, version, &metadata); err != nil {
161+
return err
126162
}
127163

164+
fmt.Printf("::notice::Sent metadata for %s version %s\n", entry.AgentType, version)
128165
return nil
129166
}
130167

168+
// printJSON marshals data to JSON and prints it with a debug annotation
131169
func printJSON(label string, data any) {
132170
jsonData, err := json.MarshalIndent(data, "", " ")
133171
if err != nil {
134-
fmt.Fprintf(os.Stderr, "::error::Failed to marshal %s: %v\n", label, err)
135-
os.Exit(1)
172+
fmt.Printf("::debug::Failed to marshal %s: %v\n", label, err)
173+
return
136174
}
137175
fmt.Printf("::debug::%s: %s\n", label, string(jsonData))
138176
}

0 commit comments

Comments
 (0)