Skip to content

Commit c188cf8

Browse files
authored
Merge branch 'main' into copilot/send-telemetry-data-file-handling
2 parents 1f5944a + 52f3a07 commit c188cf8

7 files changed

Lines changed: 154 additions & 33 deletions

actions/setup/js/send_otlp_span.cjs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,25 @@ function parseBooleanEnv(value) {
161161
return undefined;
162162
}
163163

164+
/**
165+
* Resolve the job name for conclusion spans.
166+
*
167+
* Normally this comes from INPUT_JOB_NAME (job-name action input), but some
168+
* deployment paths can miss that env var in the post step. In that case, fall
169+
* back to parsing the conclusion span name ("gh-aw.<job>.conclusion").
170+
*
171+
* @param {string} spanName
172+
* @returns {string}
173+
*/
174+
function resolveConclusionJobName(spanName) {
175+
const inputJobName = (process.env.INPUT_JOB_NAME || "").trim();
176+
if (inputJobName) {
177+
return inputJobName;
178+
}
179+
const match = /^gh-aw\.([^.]+)\.conclusion$/.exec(spanName);
180+
return match ? match[1] : "";
181+
}
182+
164183
/**
165184
* Parse setup-time aw_context passed via environment before aw_info.json exists.
166185
*
@@ -2005,7 +2024,7 @@ async function sendJobConclusionSpan(spanName, options = {}) {
20052024
const awmgVersion = (typeof awInfo.awmg_version === "string" ? awInfo.awmg_version : "") || process.env.GH_AW_INFO_AWMG_VERSION || "";
20062025
const bodyModified = typeof awInfo.body_modified === "boolean" ? awInfo.body_modified : parseBooleanEnv(process.env.GH_AW_INFO_BODY_MODIFIED);
20072026
const trackerId = process.env.GH_AW_TRACKER_ID || awInfo.tracker_id || "";
2008-
const jobName = process.env.INPUT_JOB_NAME || "";
2027+
const jobName = resolveConclusionJobName(spanName);
20092028
const jobEmitsOwnTokenUsage = jobName === "agent" || jobName === "detection" || (!!engineId && jobName === engineId);
20102029
const runId = process.env.GITHUB_RUN_ID || "";
20112030
const runAttempt = awInfo.run_attempt || process.env.GITHUB_RUN_ATTEMPT || "1";

actions/setup/js/send_otlp_span.test.cjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3564,6 +3564,23 @@ describe("sendJobConclusionSpan", () => {
35643564
expect(aicAttr.value.doubleValue).toBe(0.125);
35653565
});
35663566

3567+
it("includes gh-aw.aic when INPUT_JOB_NAME is missing but span name is gh-aw.agent.conclusion", async () => {
3568+
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
3569+
vi.stubGlobal("fetch", mockFetch);
3570+
3571+
process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]);
3572+
process.env.GH_AW_AIC = "0.125";
3573+
delete process.env.INPUT_JOB_NAME;
3574+
3575+
await sendJobConclusionSpan("gh-aw.agent.conclusion");
3576+
3577+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
3578+
const span = body.resourceSpans[0].scopeSpans[0].spans[0];
3579+
const aicAttr = span.attributes.find(a => a.key === "gh-aw.aic");
3580+
expect(aicAttr).toBeDefined();
3581+
expect(aicAttr.value.doubleValue).toBe(0.125);
3582+
});
3583+
35673584
it("emits dashboard metrics and aliases on the conclusion span", async () => {
35683585
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
35693586
vi.stubGlobal("fetch", mockFetch);

actions/setup/js/write_large_content_to_file.cjs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ const { generateCompactSchema } = require("./generate_compact_schema.cjs");
1515
function writeLargeContentToFile(content) {
1616
const logsDir = "/tmp/gh-aw/safeoutputs";
1717

18-
// Ensure directory exists
19-
if (!fs.existsSync(logsDir)) {
20-
fs.mkdirSync(logsDir, { recursive: true });
21-
}
18+
fs.mkdirSync(logsDir, { recursive: true });
2219

2320
// Generate SHA256 hash of content
2421
const hash = crypto.createHash("sha256").update(content).digest("hex");
@@ -27,16 +24,11 @@ function writeLargeContentToFile(content) {
2724
const filename = `${hash}.json`;
2825
const filepath = path.join(logsDir, filename);
2926

30-
// Write content to file
3127
fs.writeFileSync(filepath, content, "utf8");
3228

33-
// Generate compact schema description for jq/agent
3429
const description = generateCompactSchema(content);
3530

36-
return {
37-
filename: filename,
38-
description: description,
39-
};
31+
return { filename, description };
4032
}
4133

4234
module.exports = {

actions/setup/js/write_large_content_to_file.test.cjs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,61 @@ describe("writeLargeContentToFile", () => {
114114
const written = fs.readFileSync(filepath, "utf8");
115115
expect(written).toBe(content);
116116
});
117+
118+
it("should handle non-JSON content gracefully", async () => {
119+
const { writeLargeContentToFile } = await import("./write_large_content_to_file.cjs");
120+
121+
const content = "this is plain text, not JSON";
122+
const result = writeLargeContentToFile(content);
123+
124+
expect(result.description).toBe("text content");
125+
const filepath = path.join(testDir, result.filename);
126+
expect(fs.readFileSync(filepath, "utf8")).toBe(content);
127+
});
128+
129+
it("should handle empty object", async () => {
130+
const { writeLargeContentToFile } = await import("./write_large_content_to_file.cjs");
131+
132+
const content = JSON.stringify({});
133+
const result = writeLargeContentToFile(content);
134+
135+
expect(result.description).toBe("{}");
136+
});
137+
138+
it("should handle empty array", async () => {
139+
const { writeLargeContentToFile } = await import("./write_large_content_to_file.cjs");
140+
141+
const content = JSON.stringify([]);
142+
const result = writeLargeContentToFile(content);
143+
144+
expect(result.description).toBe("[]");
145+
});
146+
147+
it("should handle nested object (only top-level keys listed)", async () => {
148+
const { writeLargeContentToFile } = await import("./write_large_content_to_file.cjs");
149+
150+
const content = JSON.stringify({ a: { b: 1 }, c: [1, 2] });
151+
const result = writeLargeContentToFile(content);
152+
153+
expect(result.description).toBe("{a, c}");
154+
});
155+
156+
it("should work when directory already exists", async () => {
157+
const { writeLargeContentToFile } = await import("./write_large_content_to_file.cjs");
158+
159+
fs.mkdirSync(testDir, { recursive: true });
160+
expect(fs.existsSync(testDir)).toBe(true);
161+
162+
const content = JSON.stringify({ already: "there" });
163+
expect(() => writeLargeContentToFile(content)).not.toThrow();
164+
});
165+
166+
it("should handle JSON primitive (number)", async () => {
167+
const { writeLargeContentToFile } = await import("./write_large_content_to_file.cjs");
168+
169+
const content = JSON.stringify(42);
170+
const result = writeLargeContentToFile(content);
171+
172+
expect(result.description).toBe("number");
173+
});
117174
});

pkg/cli/outcome_domain_breakdown.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package cli
22

33
import (
4+
"cmp"
45
"fmt"
5-
"sort"
6+
"slices"
67
"strings"
78

89
"github.com/github/gh-aw/pkg/logger"
@@ -92,11 +93,11 @@ func ComputeDomainBreakdowns(reports []OutcomeReport) []DomainBreakdown {
9293
}
9394

9495
// Sort by total_objective_value descending
95-
sort.Slice(result, func(i, j int) bool {
96-
if result[i].TotalObjectiveValue != result[j].TotalObjectiveValue {
97-
return result[i].TotalObjectiveValue > result[j].TotalObjectiveValue
96+
slices.SortFunc(result, func(a, b DomainBreakdown) int {
97+
if a.TotalObjectiveValue != b.TotalObjectiveValue {
98+
return cmp.Compare(b.TotalObjectiveValue, a.TotalObjectiveValue)
9899
}
99-
return result[i].Label < result[j].Label
100+
return strings.Compare(a.Label, b.Label)
100101
})
101102

102103
domainBreakdownLog.Printf("Computed domain breakdowns: domains=%d, total_attempted=%d", len(result), countTotalAttempted(result))
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//go:build !integration
2+
3+
package cli
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestComputeDomainBreakdowns_SortsByValueThenLabel(t *testing.T) {
13+
reports := []OutcomeReport{
14+
{
15+
Result: OutcomeAccepted,
16+
ObjectiveValue: 20,
17+
ObjectiveLabels: []string{"beta"},
18+
},
19+
{
20+
Result: OutcomeAccepted,
21+
ObjectiveValue: 20,
22+
ObjectiveLabels: []string{"alpha"},
23+
},
24+
{
25+
Result: OutcomeRejected,
26+
ObjectiveLabels: []string{"gamma"},
27+
},
28+
}
29+
30+
breakdowns := ComputeDomainBreakdowns(reports)
31+
require.Len(t, breakdowns, 3)
32+
assert.Equal(t, []string{"alpha", "beta", "gamma"}, []string{breakdowns[0].Label, breakdowns[1].Label, breakdowns[2].Label})
33+
assert.Equal(t, []int{20, 20, 0}, []int{breakdowns[0].TotalObjectiveValue, breakdowns[1].TotalObjectiveValue, breakdowns[2].TotalObjectiveValue})
34+
}

pkg/cli/outcomes_history.go

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package cli
22

33
import (
4+
"cmp"
45
"encoding/json"
56
"fmt"
67
"os"
7-
"sort"
8+
"slices"
89
"strconv"
910
"strings"
1011

@@ -246,32 +247,32 @@ func buildHistoricalObjectiveReport(source string, items []historicalGitHubItem,
246247
})
247248
}
248249

249-
sort.Slice(buckets, func(i, j int) bool {
250-
if buckets[i].ContributedValue != buckets[j].ContributedValue {
251-
return buckets[i].ContributedValue > buckets[j].ContributedValue
250+
slices.SortFunc(buckets, func(a, b historicalObjectiveBucket) int {
251+
if a.ContributedValue != b.ContributedValue {
252+
return cmp.Compare(b.ContributedValue, a.ContributedValue)
252253
}
253-
if buckets[i].Count != buckets[j].Count {
254-
return buckets[i].Count > buckets[j].Count
254+
if a.Count != b.Count {
255+
return cmp.Compare(b.Count, a.Count)
255256
}
256-
if buckets[i].MappedValue != buckets[j].MappedValue {
257-
return buckets[i].MappedValue > buckets[j].MappedValue
257+
if a.MappedValue != b.MappedValue {
258+
return cmp.Compare(b.MappedValue, a.MappedValue)
258259
}
259-
return buckets[i].Label < buckets[j].Label
260+
return strings.Compare(a.Label, b.Label)
260261
})
261262

262-
sort.Slice(rows, func(i, j int) bool {
263-
if rows[i].ObjectiveValue != rows[j].ObjectiveValue {
264-
return rows[i].ObjectiveValue > rows[j].ObjectiveValue
263+
slices.SortFunc(rows, func(a, b historicalObjectiveItem) int {
264+
if a.ObjectiveValue != b.ObjectiveValue {
265+
return cmp.Compare(b.ObjectiveValue, a.ObjectiveValue)
265266
}
266-
leftTime := rows[i].ClosedAt
267+
leftTime := a.ClosedAt
267268
if leftTime == "" {
268-
leftTime = rows[i].MergedAt
269+
leftTime = a.MergedAt
269270
}
270-
rightTime := rows[j].ClosedAt
271+
rightTime := b.ClosedAt
271272
if rightTime == "" {
272-
rightTime = rows[j].MergedAt
273+
rightTime = b.MergedAt
273274
}
274-
return leftTime < rightTime
275+
return strings.Compare(leftTime, rightTime)
275276
})
276277

277278
representative := make([]historicalObjectiveItem, 0, min(len(rows), 15))

0 commit comments

Comments
 (0)