diff --git a/index.ts b/index.ts index 9e69168..3744df6 100644 --- a/index.ts +++ b/index.ts @@ -1,11 +1,16 @@ import * as core from '@actions/core' -import {context, getCurrentDeployment, getCurrentJob, getInput, PermissionError, run} from './lib/actions.js' +import {context, getCurrentDeployment, getInput, PermissionError, run} from './lib/actions.js' +import { getCurrentJob } from 'lib/jobs.js' // see https://github.com/actions/toolkit for more GitHub actions libraries import {fileURLToPath} from 'url' import * as process from 'node:process' import {sleep} from "./lib/common.js"; import * as github from "@actions/github"; +if(!process.env.GITHUB_API_URL && process.env.GITHUB_SERVER_URL) { + process.env.GITHUB_API_URL = `${process.env.GITHUB_SERVER_URL.replace(/\/$/, '')}/api/v3` +} + export const action = run(async () => { const inputs = { @@ -26,7 +31,12 @@ export const action = run(async () => { core.setOutput('run_html_url', runHtmlUrl) core.exportVariable('GITHUB_RUN_HTML_URL', runHtmlUrl) - await getCurrentJob(octokit).then((job) => { + await getCurrentJob(octokit, context).then((job) => { + if (job === undefined) { + core.warning('Current job not found for runner: ' + context.runnerName) + return null + } + if (core.isDebug()) { core.debug(JSON.stringify(job)); } diff --git a/lib/actions.ts b/lib/actions.ts index e08cde7..75d7ec8 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -8,9 +8,8 @@ import process from 'node:process'; import {_throw} from './common.js'; import YAML from "yaml"; import fs from "node:fs"; +import {EnhancedContext} from './common.js'; -// cache of getCurrentJob result -let _currentJob: Awaited> // cache of getCurrentJob result let _currentDeployment: Awaited> @@ -187,69 +186,8 @@ export class PermissionError extends Error { } } -// --- Enhanced GitHub Action Context -------------------------------------------------- - -class EnhancedContext extends Context { - - get repository() { - return `${this.repo.owner}/${this.repo.repo}`; - } - - get jobCheckRunId() { - return parseInt(process.env.JOB_CHECK_RUN_ID - // WORKAROUND until https://github.com/actions/runner/pull/4053 is merged and released - ?? process.env.INPUT__JOB_CHECK_RUN_ID - ?? _throw(new Error('Missing environment variable: JOB_CHECK_RUN_ID'))); - } - - get workflowRef() { - return process.env.GITHUB_WORKFLOW_REF - ?? _throw(new Error('Missing environment variable: GITHUB_WORKFLOW_REF')); - } - - get workflowSha() { - return process.env.GITHUB_WORKFLOW_SHA - ?? _throw(new Error('Missing environment variable: GITHUB_WORKFLOW_SHA')); - } - - get runHtmlUrl() { - return process.env.GITHUB_RUN_HTML_URL ?? `${this.serverUrl}/${this.repository}` + `/actions/runs/${this.runId}` + - (this.runAttempt ? `/attempts/${this.runAttempt}` : ''); - } - - get runnerName() { - return process.env.RUNNER_NAME - ?? _throw(new Error('Missing environment variable: RUNNER_NAME')); - } - - get runnerTempDir() { - return process.env.RUNNER_TEMP - ?? _throw(new Error('Missing environment variable: RUNNER_TEMP')); - } -} - export const context = new EnhancedContext(); -/** - * Get the current job from the workflow run - * @returns the current job - */ -export async function getCurrentJob(octokit: InstanceType): Promise { - if (_currentJob) return _currentJob - - const currentJob = await octokit.rest.actions.getJobForWorkflowRun({ - ...context.repo, - job_id: context.jobCheckRunId, - }).catch((error) => { - if (error.status === 403) { - throwPermissionError({scope: 'actions', permission: 'read'}, error) - } - throw error - }).then((res) => res.data) - - return _currentJob = currentJob -} - /** * Get the current deployment from the workflow run * @returns the current deployment or undefined diff --git a/lib/common.ts b/lib/common.ts index d54789a..0cb5edd 100644 --- a/lib/common.ts +++ b/lib/common.ts @@ -1,5 +1,6 @@ import {z} from "zod"; import YAML from "yaml"; +import {Context} from '@actions/github/lib/context'; export type JsonLiteral = string | number | boolean | null // eslint-disable-next-line @typescript-eslint/consistent-type-definitions @@ -59,3 +60,48 @@ export function getFlatValues(values: unknown): unknown[] { export function _throw(error: unknown): never { throw error } + +// --- Enhanced GitHub Action Context -------------------------------------------------- + +export class EnhancedContext extends Context { + + get repository() { + return `${this.repo.owner}/${this.repo.repo}`; + } + + get isGithubEnterprise() { + return process.env.GITHUB_API_URL != 'https://api.github.com' + } + + get jobCheckRunId() { + return parseInt(process.env.JOB_CHECK_RUN_ID + // WORKAROUND until https://github.com/actions/runner/pull/4053 is merged and released + ?? process.env.INPUT__JOB_CHECK_RUN_ID + ?? _throw(new Error('Missing environment variable: JOB_CHECK_RUN_ID'))); + } + + get workflowRef() { + return process.env.GITHUB_WORKFLOW_REF + ?? _throw(new Error('Missing environment variable: GITHUB_WORKFLOW_REF')); + } + + get workflowSha() { + return process.env.GITHUB_WORKFLOW_SHA + ?? _throw(new Error('Missing environment variable: GITHUB_WORKFLOW_SHA')); + } + + get runHtmlUrl() { + return process.env.GITHUB_RUN_HTML_URL ?? `${this.serverUrl}/${this.repository}` + `/actions/runs/${this.runId}` + + (this.runAttempt ? `/attempts/${this.runAttempt}` : ''); + } + + get runnerName() { + return process.env.RUNNER_NAME + ?? _throw(new Error('Missing environment variable: RUNNER_NAME')); + } + + get runnerTempDir() { + return process.env.RUNNER_TEMP + ?? _throw(new Error('Missing environment variable: RUNNER_TEMP')); + } +} diff --git a/lib/jobs.ts b/lib/jobs.ts new file mode 100644 index 0000000..c3fd554 --- /dev/null +++ b/lib/jobs.ts @@ -0,0 +1,71 @@ +import { GitHub } from "@actions/github/lib/utils"; +import { context, throwPermissionError } from "./actions"; +import { EnhancedContext } from "./common"; +import * as core from '@actions/core' +import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods"; + +// type alias for job object returned by octokit.rest.actions.getJobForWorkflowRun +type Job = RestEndpointMethodTypes["actions"]["getJobForWorkflowRun"]["response"]["data"]; + +// cache of getCurrentJob result +let _currentJob: Job | undefined = undefined + +/** + * Get the job for the current runner from the workflow run using Octokit REST API. + * Equivalent to: + * jobs=`curl .../actions/runs/${run_id}/jobs` + * job=$(echo $jobs | jq -r '.jobs[] | select(.runner_name=="${{ runner.name }}")') + * @param octokit - An authenticated Octokit instance + * @param context - The enhanced GitHub Actions context + * @returns The job for the current runner, or undefined if not found + */ +export async function getCurrentJobByRunner( + octokit: InstanceType, + context: EnhancedContext +): Promise { + const runId = context.runId; + const runnerName = context.runnerName; + const { owner, repo } = context.repo; + const jobsResponse = await octokit.rest.actions.listJobsForWorkflowRun({ + owner: owner, + repo: repo, + run_id: runId, + per_page: 100, + }); + const job: Job | undefined = jobsResponse.data.jobs.find(j => j.runner_name === runnerName); + return job; +} + +/** + * Get the job object for the current runner from the workflow run using Octokit REST API. + * @param octokit - An authenticated Octokit instance + * @param context - The enhanced GitHub Actions context + * @returns The job object for the current runner + */ +export async function getCurrentJob(octokit: InstanceType, context: EnhancedContext +) : Promise{ + // Return cached job if available + if (_currentJob != undefined) return _currentJob + + // For GitHub Enterprise we need a workaround + if (context.isGithubEnterprise) { + core.warning( + 'Running in GitHub Enterprise environment, we currently need to use a workaround until ' + + 'this functionality is natively supported: https://github.blog/changelog/2025-11-13-github-actions-oidc-token-claims-now-include-check_run_id/' + ) + const currentJob = await getCurrentJobByRunner(octokit, context) + return _currentJob = currentJob + } + + // For GitHub.com we can directly get the job by using the check_run_id + const currentJob = await octokit.rest.actions.getJobForWorkflowRun({ + ...context.repo, + job_id: context.jobCheckRunId, + }).catch((error) => { + if (error.status === 403) { + throwPermissionError({scope: 'actions', permission: 'read'}, error) + } + throw error + }).then((res) => res.data) + return _currentJob = currentJob +} diff --git a/package-lock.json b/package-lock.json index dce3ce1..b5e1b07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "actions--context", + "name": "csalerno-actions--context", "lockfileVersion": 3, "requires": true, "packages": {