Skip to content

Commit

Permalink
Add job annotations (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
corentinmusard authored Jan 5, 2025
1 parent 266faa4 commit 015e81d
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 20 deletions.
1 change: 1 addition & 0 deletions .github/workflows/private.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
contents: read # To access the private repository
actions: read # To read workflow runs
pull-requests: read # To read PR labels
checks: read # Optional. To read run annotations
steps:
- uses: actions/checkout@v4
- name: Export workflow
Expand Down
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.2.0] - 2025-01-05

### Added

- Add job annotations if available, requires the following permission on private repositories:

```yaml
permissions:
checks: read # Optional. To read run annotations
```
### Fixed
- Add error handling for octokit requests
Expand Down Expand Up @@ -145,7 +156,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support for `https` endpoints (proto over http).
- Update to node 20.x

[unreleased]: https://github.com/corentinmusard/otel-cicd-action/compare/v2.1.0...HEAD
[unreleased]: https://github.com/corentinmusard/otel-cicd-action/compare/v2.2.0...HEAD
[2.2.0]: https://github.com/corentinmusard/otel-cicd-action/compare/v2.1.0...v2.2.0
[2.1.0]: https://github.com/corentinmusard/otel-cicd-action/compare/v2.0.0...v2.1.0
[2.0.0]: https://github.com/corentinmusard/otel-cicd-action/compare/v1.13.2...v2.0.0
[1.13.2]: https://github.com/corentinmusard/otel-cicd-action/compare/v1.13.1...v1.13.2
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ permissions:
contents: read # To access the private repository
actions: read # To read workflow runs
pull-requests: read # To read PR labels
checks: read # Optional. To read run annotations
```

### Adding arbitrary resource attributes
Expand Down
45 changes: 40 additions & 5 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31537,6 +31537,19 @@ async function listJobsForWorkflowRun(context, octokit, runId) {
per_page: 100,
});
}
async function getJobsAnnotations(context, octokit, jobIds) {
const annotations = {};
for (const jobId of jobIds) {
annotations[jobId] = await listAnnotations(context, octokit, jobId);
}
return annotations;
}
async function listAnnotations(context, octokit, checkRunId) {
return await octokit.paginate(octokit.rest.checks.listAnnotations, {
...context.repo,
check_run_id: checkRunId,
});
}
async function getPRsLabels(context, octokit, prNumbers) {
const labels = {};
for (const prNumber of prNumbers) {
Expand Down Expand Up @@ -33934,14 +33947,17 @@ function stepToAttributes(step) {
}

const tracer$1 = trace.getTracer("otel-cicd-action");
async function traceJob(job) {
async function traceJob(job, annotations) {
if (!job.completed_at) {
coreExports.warning(`Job ${job.id} is not completed yet`);
return;
}
const startTime = new Date(job.started_at);
const completedTime = new Date(job.completed_at);
const attributes = jobToAttributes(job);
const attributes = {
...jobToAttributes(job),
...annotationsToAttributes(annotations),
};
await tracer$1.startActiveSpan(job.name, { attributes, startTime }, async (span) => {
const code = job.conclusion === "failure" ? SpanStatusCode.ERROR : SpanStatusCode.OK;
span.setStatus({ code });
Expand Down Expand Up @@ -33997,9 +34013,19 @@ function jobToAttributes(job) {
error: job.conclusion === "failure",
};
}
function annotationsToAttributes(annotations) {
const attributes = {};
for (let i = 0; annotations && i < annotations.length; i++) {
const annotation = annotations[i];
const prefix = `github.job.annotations.${i}`;
attributes[`${prefix}.level`] = annotation.annotation_level ?? undefined;
attributes[`${prefix}.message`] = annotation.message ?? undefined;
}
return attributes;
}

const tracer = trace.getTracer("otel-cicd-action");
async function traceWorkflowRun(workflowRun, jobs, prLabels) {
async function traceWorkflowRun(workflowRun, jobs, jobAnnotations, prLabels) {
const startTime = new Date(workflowRun.run_started_at ?? workflowRun.created_at);
const attributes = workflowRunToAttributes(workflowRun, prLabels);
return await tracer.startActiveSpan(workflowRun.name ?? workflowRun.display_title, { attributes, root: true, startTime }, async (rootSpan) => {
Expand All @@ -34012,7 +34038,7 @@ async function traceWorkflowRun(workflowRun, jobs, prLabels) {
queuedSpan.end(new Date(jobs[0].started_at));
}
for (const job of jobs) {
await traceJob(job);
await traceJob(job, jobAnnotations[job.id]);
}
rootSpan.end(new Date(workflowRun.updated_at));
return rootSpan.spanContext().traceId;
Expand Down Expand Up @@ -86090,6 +86116,15 @@ async function run() {
const workflowRun = await getWorkflowRun(githubExports.context, octokit, runId);
coreExports.info("Get jobs");
const jobs = await listJobsForWorkflowRun(githubExports.context, octokit, runId);
coreExports.info("Get job annotations");
const jobsId = (jobs ?? []).map((job) => job.id);
let jobAnnotations = {};
try {
jobAnnotations = await getJobsAnnotations(githubExports.context, octokit, jobsId);
}
catch (error) {
coreExports.info(`Failed to get job annotations: ${error instanceof Error && error.message}`);
}
coreExports.info("Get PRs labels");
const prNumbers = (workflowRun.pull_requests ?? []).map((pr) => pr.number);
const prLabels = await getPRsLabels(githubExports.context, octokit, prNumbers);
Expand All @@ -86108,7 +86143,7 @@ async function run() {
};
const provider = createTracerProvider(otlpEndpoint, otlpHeaders, attributes);
coreExports.info(`Trace workflow run for ${runId} and export to ${otlpEndpoint}`);
const traceId = await traceWorkflowRun(workflowRun, jobs, prLabels);
const traceId = await traceWorkflowRun(workflowRun, jobs, jobAnnotations, prLabels);
coreExports.setOutput("traceId", traceId);
coreExports.debug(`traceId: ${traceId}`);
coreExports.info("Flush and shutdown tracer provider");
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

20 changes: 15 additions & 5 deletions src/__assets__/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,9 @@
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970679534',
'github.job.workflow_name': 'CI on main',
'github.job.head_branch': 'main',
error: false
error: false,
'github.job.annotations.0.level': 'warning',
'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'
},
status: { code: 1 },
events: [],
Expand Down Expand Up @@ -864,7 +866,9 @@
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970679834',
'github.job.workflow_name': 'CI on main',
'github.job.head_branch': 'main',
error: false
error: false,
'github.job.annotations.0.level': 'warning',
'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'
},
status: { code: 1 },
events: [],
Expand Down Expand Up @@ -2207,7 +2211,9 @@
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970680323',
'github.job.workflow_name': 'CI on main',
'github.job.head_branch': 'main',
error: false
error: false,
'github.job.annotations.0.level': 'warning',
'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'
},
status: { code: 1 },
events: [],
Expand Down Expand Up @@ -2588,7 +2594,9 @@
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970680487',
'github.job.workflow_name': 'CI on main',
'github.job.head_branch': 'main',
error: false
error: false,
'github.job.annotations.0.level': 'warning',
'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'
},
status: { code: 1 },
events: [],
Expand Down Expand Up @@ -3469,7 +3477,9 @@
'github.job.check_run_url': 'https://api.github.com/repos/biomejs/biome/check-runs/34970680756',
'github.job.workflow_name': 'CI on main',
'github.job.head_branch': 'main',
error: false
error: false,
'github.job.annotations.0.level': 'warning',
'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'
},
status: { code: 1 },
events: [],
Expand Down
40 changes: 40 additions & 0 deletions src/__assets__/run.rec

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion src/github.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Context } from "@actions/github/lib/context";
import type { GitHub } from "@actions/github/lib/utils";
import type { components } from "@octokit/openapi-types";

type Octokit = InstanceType<typeof GitHub>;

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

async function getJobsAnnotations(context: Context, octokit: Octokit, jobIds: number[]) {
const annotations: Record<number, components["schemas"]["check-annotation"][]> = {};

for (const jobId of jobIds) {
annotations[jobId] = await listAnnotations(context, octokit, jobId);
}
return annotations;
}

async function listAnnotations(context: Context, octokit: Octokit, checkRunId: number) {
return await octokit.paginate(octokit.rest.checks.listAnnotations, {
...context.repo,
check_run_id: checkRunId,
});
}

async function getPRsLabels(context: Context, octokit: Octokit, prNumbers: number[]) {
const labels: Record<number, string[]> = {};

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

export { getWorkflowRun, listJobsForWorkflowRun, getPRsLabels, type Octokit };
export { getWorkflowRun, listJobsForWorkflowRun, getJobsAnnotations, getPRsLabels, type Octokit };
4 changes: 2 additions & 2 deletions src/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ describe("run", () => {
await run();
await fs.writeFile("src/__assets__/output.txt", output);

expect(core.setOutput).toHaveBeenCalledWith("traceId", "329e58aa53cec7a2beadd2fd0a85c388");
expect(core.setFailed).not.toHaveBeenCalled();
expect(core.setOutput).toHaveBeenCalledWith("traceId", "329e58aa53cec7a2beadd2fd0a85c388");
}, 10000);

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

await run();

expect(core.setOutput).not.toHaveBeenCalled();
expect(core.setFailed).toHaveBeenCalledTimes(1);
expect(core.setFailed).toHaveBeenCalledWith(expect.any(RequestError));
expect(core.setOutput).not.toHaveBeenCalled();
}, 10000);
});
13 changes: 11 additions & 2 deletions src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { context, getOctokit } from "@actions/github";
import type { ResourceAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAMESPACE } from "@opentelemetry/semantic-conventions/incubating";
import { getPRsLabels, getWorkflowRun, listJobsForWorkflowRun } from "./github";
import { getJobsAnnotations, getPRsLabels, getWorkflowRun, listJobsForWorkflowRun } from "./github";
import { traceWorkflowRun } from "./trace/workflow";
import { createTracerProvider, stringToRecord } from "./tracer";

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

core.info("Get job annotations");
const jobsId = (jobs ?? []).map((job) => job.id);
let jobAnnotations = {};
try {
jobAnnotations = await getJobsAnnotations(context, octokit, jobsId);
} catch (error) {
core.info(`Failed to get job annotations: ${error instanceof Error && error.message}`);
}

core.info("Get PRs labels");
const prNumbers = (workflowRun.pull_requests ?? []).map((pr) => pr.number);
const prLabels = await getPRsLabels(context, octokit, prNumbers);
Expand All @@ -43,7 +52,7 @@ async function run() {
const provider = createTracerProvider(otlpEndpoint, otlpHeaders, attributes);

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

core.setOutput("traceId", traceId);
core.debug(`traceId: ${traceId}`);
Expand Down
21 changes: 19 additions & 2 deletions src/trace/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ import { traceStep } from "./step";

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

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

const startTime = new Date(job.started_at);
const completedTime = new Date(job.completed_at);
const attributes = jobToAttributes(job);
const attributes = {
...jobToAttributes(job),
...annotationsToAttributes(annotations),
};

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

function annotationsToAttributes(annotations: components["schemas"]["check-annotation"][] | undefined) {
const attributes: Attributes = {};

for (let i = 0; annotations && i < annotations.length; i++) {
const annotation = annotations[i];
const prefix = `github.job.annotations.${i}`;

attributes[`${prefix}.level`] = annotation.annotation_level ?? undefined;
attributes[`${prefix}.message`] = annotation.message ?? undefined;
}

return attributes;
}

export { traceJob };
3 changes: 2 additions & 1 deletion src/trace/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const tracer = trace.getTracer("otel-cicd-action");
async function traceWorkflowRun(
workflowRun: components["schemas"]["workflow-run"],
jobs: components["schemas"]["job"][],
jobAnnotations: Record<number, components["schemas"]["check-annotation"][]>,
prLabels: Record<number, string[]>,
) {
const startTime = new Date(workflowRun.run_started_at ?? workflowRun.created_at);
Expand All @@ -28,7 +29,7 @@ async function traceWorkflowRun(
}

for (const job of jobs) {
await traceJob(job);
await traceJob(job, jobAnnotations[job.id]);
}

rootSpan.end(new Date(workflowRun.updated_at));
Expand Down

0 comments on commit 015e81d

Please sign in to comment.