Skip to content

Commit 015e81d

Browse files
Add job annotations (#43)
1 parent 266faa4 commit 015e81d

File tree

12 files changed

+163
-20
lines changed

12 files changed

+163
-20
lines changed

.github/workflows/private.yml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
contents: read # To access the private repository
1616
actions: read # To read workflow runs
1717
pull-requests: read # To read PR labels
18+
checks: read # Optional. To read run annotations
1819
steps:
1920
- uses: actions/checkout@v4
2021
- name: Export workflow

CHANGELOG.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.2.0] - 2025-01-05
11+
12+
### Added
13+
14+
- Add job annotations if available, requires the following permission on private repositories:
15+
16+
```yaml
17+
permissions:
18+
checks: read # Optional. To read run annotations
19+
```
20+
1021
### Fixed
1122
1223
- Add error handling for octokit requests
@@ -145,7 +156,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
145156
- Support for `https` endpoints (proto over http).
146157
- Update to node 20.x
147158

148-
[unreleased]: https://github.com/corentinmusard/otel-cicd-action/compare/v2.1.0...HEAD
159+
[unreleased]: https://github.com/corentinmusard/otel-cicd-action/compare/v2.2.0...HEAD
160+
[2.2.0]: https://github.com/corentinmusard/otel-cicd-action/compare/v2.1.0...v2.2.0
149161
[2.1.0]: https://github.com/corentinmusard/otel-cicd-action/compare/v2.0.0...v2.1.0
150162
[2.0.0]: https://github.com/corentinmusard/otel-cicd-action/compare/v1.13.2...v2.0.0
151163
[1.13.2]: https://github.com/corentinmusard/otel-cicd-action/compare/v1.13.1...v1.13.2

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ permissions:
9292
contents: read # To access the private repository
9393
actions: read # To read workflow runs
9494
pull-requests: read # To read PR labels
95+
checks: read # Optional. To read run annotations
9596
```
9697

9798
### Adding arbitrary resource attributes

dist/index.js

+40-5
Original file line numberDiff line numberDiff line change
@@ -31537,6 +31537,19 @@ async function listJobsForWorkflowRun(context, octokit, runId) {
3153731537
per_page: 100,
3153831538
});
3153931539
}
31540+
async function getJobsAnnotations(context, octokit, jobIds) {
31541+
const annotations = {};
31542+
for (const jobId of jobIds) {
31543+
annotations[jobId] = await listAnnotations(context, octokit, jobId);
31544+
}
31545+
return annotations;
31546+
}
31547+
async function listAnnotations(context, octokit, checkRunId) {
31548+
return await octokit.paginate(octokit.rest.checks.listAnnotations, {
31549+
...context.repo,
31550+
check_run_id: checkRunId,
31551+
});
31552+
}
3154031553
async function getPRsLabels(context, octokit, prNumbers) {
3154131554
const labels = {};
3154231555
for (const prNumber of prNumbers) {
@@ -33934,14 +33947,17 @@ function stepToAttributes(step) {
3393433947
}
3393533948

3393633949
const tracer$1 = trace.getTracer("otel-cicd-action");
33937-
async function traceJob(job) {
33950+
async function traceJob(job, annotations) {
3393833951
if (!job.completed_at) {
3393933952
coreExports.warning(`Job ${job.id} is not completed yet`);
3394033953
return;
3394133954
}
3394233955
const startTime = new Date(job.started_at);
3394333956
const completedTime = new Date(job.completed_at);
33944-
const attributes = jobToAttributes(job);
33957+
const attributes = {
33958+
...jobToAttributes(job),
33959+
...annotationsToAttributes(annotations),
33960+
};
3394533961
await tracer$1.startActiveSpan(job.name, { attributes, startTime }, async (span) => {
3394633962
const code = job.conclusion === "failure" ? SpanStatusCode.ERROR : SpanStatusCode.OK;
3394733963
span.setStatus({ code });
@@ -33997,9 +34013,19 @@ function jobToAttributes(job) {
3399734013
error: job.conclusion === "failure",
3399834014
};
3399934015
}
34016+
function annotationsToAttributes(annotations) {
34017+
const attributes = {};
34018+
for (let i = 0; annotations && i < annotations.length; i++) {
34019+
const annotation = annotations[i];
34020+
const prefix = `github.job.annotations.${i}`;
34021+
attributes[`${prefix}.level`] = annotation.annotation_level ?? undefined;
34022+
attributes[`${prefix}.message`] = annotation.message ?? undefined;
34023+
}
34024+
return attributes;
34025+
}
3400034026

3400134027
const tracer = trace.getTracer("otel-cicd-action");
34002-
async function traceWorkflowRun(workflowRun, jobs, prLabels) {
34028+
async function traceWorkflowRun(workflowRun, jobs, jobAnnotations, prLabels) {
3400334029
const startTime = new Date(workflowRun.run_started_at ?? workflowRun.created_at);
3400434030
const attributes = workflowRunToAttributes(workflowRun, prLabels);
3400534031
return await tracer.startActiveSpan(workflowRun.name ?? workflowRun.display_title, { attributes, root: true, startTime }, async (rootSpan) => {
@@ -34012,7 +34038,7 @@ async function traceWorkflowRun(workflowRun, jobs, prLabels) {
3401234038
queuedSpan.end(new Date(jobs[0].started_at));
3401334039
}
3401434040
for (const job of jobs) {
34015-
await traceJob(job);
34041+
await traceJob(job, jobAnnotations[job.id]);
3401634042
}
3401734043
rootSpan.end(new Date(workflowRun.updated_at));
3401834044
return rootSpan.spanContext().traceId;
@@ -86090,6 +86116,15 @@ async function run() {
8609086116
const workflowRun = await getWorkflowRun(githubExports.context, octokit, runId);
8609186117
coreExports.info("Get jobs");
8609286118
const jobs = await listJobsForWorkflowRun(githubExports.context, octokit, runId);
86119+
coreExports.info("Get job annotations");
86120+
const jobsId = (jobs ?? []).map((job) => job.id);
86121+
let jobAnnotations = {};
86122+
try {
86123+
jobAnnotations = await getJobsAnnotations(githubExports.context, octokit, jobsId);
86124+
}
86125+
catch (error) {
86126+
coreExports.info(`Failed to get job annotations: ${error instanceof Error && error.message}`);
86127+
}
8609386128
coreExports.info("Get PRs labels");
8609486129
const prNumbers = (workflowRun.pull_requests ?? []).map((pr) => pr.number);
8609586130
const prLabels = await getPRsLabels(githubExports.context, octokit, prNumbers);
@@ -86108,7 +86143,7 @@ async function run() {
8610886143
};
8610986144
const provider = createTracerProvider(otlpEndpoint, otlpHeaders, attributes);
8611086145
coreExports.info(`Trace workflow run for ${runId} and export to ${otlpEndpoint}`);
86111-
const traceId = await traceWorkflowRun(workflowRun, jobs, prLabels);
86146+
const traceId = await traceWorkflowRun(workflowRun, jobs, jobAnnotations, prLabels);
8611286147
coreExports.setOutput("traceId", traceId);
8611386148
coreExports.debug(`traceId: ${traceId}`);
8611486149
coreExports.info("Flush and shutdown tracer provider");

dist/index.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/__assets__/output.txt

+15-5
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,9 @@
444444
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970679534',
445445
'github.job.workflow_name': 'CI on main',
446446
'github.job.head_branch': 'main',
447-
error: false
447+
error: false,
448+
'github.job.annotations.0.level': 'warning',
449+
'github.job.annotations.0.message': 'ubuntu-latest pipelines will use ubuntu-24.04 soon. For more details, see https://github.com/actions/runner-images/issues/10636'
448450
},
449451
status: { code: 1 },
450452
events: [],
@@ -864,7 +866,9 @@
864866
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970679834',
865867
'github.job.workflow_name': 'CI on main',
866868
'github.job.head_branch': 'main',
867-
error: false
869+
error: false,
870+
'github.job.annotations.0.level': 'warning',
871+
'github.job.annotations.0.message': 'ubuntu-latest pipelines will use ubuntu-24.04 soon. For more details, see https://github.com/actions/runner-images/issues/10636'
868872
},
869873
status: { code: 1 },
870874
events: [],
@@ -2207,7 +2211,9 @@
22072211
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970680323',
22082212
'github.job.workflow_name': 'CI on main',
22092213
'github.job.head_branch': 'main',
2210-
error: false
2214+
error: false,
2215+
'github.job.annotations.0.level': 'warning',
2216+
'github.job.annotations.0.message': 'ubuntu-latest pipelines will use ubuntu-24.04 soon. For more details, see https://github.com/actions/runner-images/issues/10636'
22112217
},
22122218
status: { code: 1 },
22132219
events: [],
@@ -2588,7 +2594,9 @@
25882594
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970680487',
25892595
'github.job.workflow_name': 'CI on main',
25902596
'github.job.head_branch': 'main',
2591-
error: false
2597+
error: false,
2598+
'github.job.annotations.0.level': 'warning',
2599+
'github.job.annotations.0.message': 'ubuntu-latest pipelines will use ubuntu-24.04 soon. For more details, see https://github.com/actions/runner-images/issues/10636'
25922600
},
25932601
status: { code: 1 },
25942602
events: [],
@@ -3469,7 +3477,9 @@
34693477
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970680756',
34703478
'github.job.workflow_name': 'CI on main',
34713479
'github.job.head_branch': 'main',
3472-
error: false
3480+
error: false,
3481+
'github.job.annotations.0.level': 'warning',
3482+
'github.job.annotations.0.message': 'ubuntu-latest pipelines will use ubuntu-24.04 soon. For more details, see https://github.com/actions/runner-images/issues/10636'
34733483
},
34743484
status: { code: 1 },
34753485
events: [],

src/__assets__/run.rec

+40
Large diffs are not rendered by default.

src/github.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Context } from "@actions/github/lib/context";
22
import type { GitHub } from "@actions/github/lib/utils";
3+
import type { components } from "@octokit/openapi-types";
34

45
type Octokit = InstanceType<typeof GitHub>;
56

@@ -20,6 +21,22 @@ async function listJobsForWorkflowRun(context: Context, octokit: Octokit, runId:
2021
});
2122
}
2223

24+
async function getJobsAnnotations(context: Context, octokit: Octokit, jobIds: number[]) {
25+
const annotations: Record<number, components["schemas"]["check-annotation"][]> = {};
26+
27+
for (const jobId of jobIds) {
28+
annotations[jobId] = await listAnnotations(context, octokit, jobId);
29+
}
30+
return annotations;
31+
}
32+
33+
async function listAnnotations(context: Context, octokit: Octokit, checkRunId: number) {
34+
return await octokit.paginate(octokit.rest.checks.listAnnotations, {
35+
...context.repo,
36+
check_run_id: checkRunId,
37+
});
38+
}
39+
2340
async function getPRsLabels(context: Context, octokit: Octokit, prNumbers: number[]) {
2441
const labels: Record<number, string[]> = {};
2542

@@ -40,4 +57,4 @@ async function listLabelsOnIssue(context: Context, octokit: Octokit, prNumber: n
4057
);
4158
}
4259

43-
export { getWorkflowRun, listJobsForWorkflowRun, getPRsLabels, type Octokit };
60+
export { getWorkflowRun, listJobsForWorkflowRun, getJobsAnnotations, getPRsLabels, type Octokit };

src/runner.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ describe("run", () => {
7373
await run();
7474
await fs.writeFile("src/__assets__/output.txt", output);
7575

76-
expect(core.setOutput).toHaveBeenCalledWith("traceId", "329e58aa53cec7a2beadd2fd0a85c388");
7776
expect(core.setFailed).not.toHaveBeenCalled();
77+
expect(core.setOutput).toHaveBeenCalledWith("traceId", "329e58aa53cec7a2beadd2fd0a85c388");
7878
}, 10000);
7979

8080
it("should fail", async () => {
@@ -83,8 +83,8 @@ describe("run", () => {
8383

8484
await run();
8585

86-
expect(core.setOutput).not.toHaveBeenCalled();
8786
expect(core.setFailed).toHaveBeenCalledTimes(1);
8887
expect(core.setFailed).toHaveBeenCalledWith(expect.any(RequestError));
88+
expect(core.setOutput).not.toHaveBeenCalled();
8989
}, 10000);
9090
});

src/runner.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { context, getOctokit } from "@actions/github";
33
import type { ResourceAttributes } from "@opentelemetry/resources";
44
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
55
import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAMESPACE } from "@opentelemetry/semantic-conventions/incubating";
6-
import { getPRsLabels, getWorkflowRun, listJobsForWorkflowRun } from "./github";
6+
import { getJobsAnnotations, getPRsLabels, getWorkflowRun, listJobsForWorkflowRun } from "./github";
77
import { traceWorkflowRun } from "./trace/workflow";
88
import { createTracerProvider, stringToRecord } from "./tracer";
99

@@ -23,6 +23,15 @@ async function run() {
2323
core.info("Get jobs");
2424
const jobs = await listJobsForWorkflowRun(context, octokit, runId);
2525

26+
core.info("Get job annotations");
27+
const jobsId = (jobs ?? []).map((job) => job.id);
28+
let jobAnnotations = {};
29+
try {
30+
jobAnnotations = await getJobsAnnotations(context, octokit, jobsId);
31+
} catch (error) {
32+
core.info(`Failed to get job annotations: ${error instanceof Error && error.message}`);
33+
}
34+
2635
core.info("Get PRs labels");
2736
const prNumbers = (workflowRun.pull_requests ?? []).map((pr) => pr.number);
2837
const prLabels = await getPRsLabels(context, octokit, prNumbers);
@@ -43,7 +52,7 @@ async function run() {
4352
const provider = createTracerProvider(otlpEndpoint, otlpHeaders, attributes);
4453

4554
core.info(`Trace workflow run for ${runId} and export to ${otlpEndpoint}`);
46-
const traceId = await traceWorkflowRun(workflowRun, jobs, prLabels);
55+
const traceId = await traceWorkflowRun(workflowRun, jobs, jobAnnotations, prLabels);
4756

4857
core.setOutput("traceId", traceId);
4958
core.debug(`traceId: ${traceId}`);

src/trace/job.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@ import { traceStep } from "./step";
1414

1515
const tracer = trace.getTracer("otel-cicd-action");
1616

17-
async function traceJob(job: components["schemas"]["job"]) {
17+
async function traceJob(job: components["schemas"]["job"], annotations?: components["schemas"]["check-annotation"][]) {
1818
if (!job.completed_at) {
1919
core.warning(`Job ${job.id} is not completed yet`);
2020
return;
2121
}
2222

2323
const startTime = new Date(job.started_at);
2424
const completedTime = new Date(job.completed_at);
25-
const attributes = jobToAttributes(job);
25+
const attributes = {
26+
...jobToAttributes(job),
27+
...annotationsToAttributes(annotations),
28+
};
2629

2730
await tracer.startActiveSpan(job.name, { attributes, startTime }, async (span) => {
2831
const code = job.conclusion === "failure" ? SpanStatusCode.ERROR : SpanStatusCode.OK;
@@ -82,4 +85,18 @@ function jobToAttributes(job: components["schemas"]["job"]): Attributes {
8285
};
8386
}
8487

88+
function annotationsToAttributes(annotations: components["schemas"]["check-annotation"][] | undefined) {
89+
const attributes: Attributes = {};
90+
91+
for (let i = 0; annotations && i < annotations.length; i++) {
92+
const annotation = annotations[i];
93+
const prefix = `github.job.annotations.${i}`;
94+
95+
attributes[`${prefix}.level`] = annotation.annotation_level ?? undefined;
96+
attributes[`${prefix}.message`] = annotation.message ?? undefined;
97+
}
98+
99+
return attributes;
100+
}
101+
85102
export { traceJob };

src/trace/workflow.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const tracer = trace.getTracer("otel-cicd-action");
88
async function traceWorkflowRun(
99
workflowRun: components["schemas"]["workflow-run"],
1010
jobs: components["schemas"]["job"][],
11+
jobAnnotations: Record<number, components["schemas"]["check-annotation"][]>,
1112
prLabels: Record<number, string[]>,
1213
) {
1314
const startTime = new Date(workflowRun.run_started_at ?? workflowRun.created_at);
@@ -28,7 +29,7 @@ async function traceWorkflowRun(
2829
}
2930

3031
for (const job of jobs) {
31-
await traceJob(job);
32+
await traceJob(job, jobAnnotations[job.id]);
3233
}
3334

3435
rootSpan.end(new Date(workflowRun.updated_at));

0 commit comments

Comments
 (0)