Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ inputs:
description: 'OTLP parent span ID (16-character hexadecimal string) for the setup span. Pass the setup-span-id output of the upstream setup step so job setup spans form a single tree.'
required: false
default: ''
otlp-oidc-token:
description: 'Optional pre-minted OIDC bearer token used for OTLP Authorization headers.'
required: false
default: ''

outputs:
files_copied:
Expand Down
3 changes: 3 additions & 0 deletions setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const safeOutputArtifactClient = getActionInput("SAFE_OUTPUT_ARTIFACT_CLIENT") |
const inputTraceId = getActionInput("TRACE_ID");
const inputParentSpanId = getActionInput("PARENT_SPAN_ID");
const inputJobName = getActionInput("JOB_NAME");
const inputOTLPOIDCToken = getActionInput("OTLP_OIDC_TOKEN");

const result = spawnSync(path.join(__dirname, "setup.sh"), [], {
stdio: "inherit",
Expand All @@ -25,6 +26,7 @@ const result = spawnSync(path.join(__dirname, "setup.sh"), [], {
INPUT_TRACE_ID: inputTraceId,
INPUT_PARENT_SPAN_ID: inputParentSpanId,
INPUT_JOB_NAME: inputJobName,
INPUT_OTLP_OIDC_TOKEN: inputOTLPOIDCToken,
// Tell setup.sh to skip the OTLP span: in action mode index.js sends it
// after setup.sh returns so that the startMs captured here is used.
GH_AW_SKIP_SETUP_OTLP: "1",
Expand Down Expand Up @@ -53,6 +55,7 @@ if (result.status !== 0) {
process.env.INPUT_TRACE_ID = inputTraceId;
process.env.INPUT_PARENT_SPAN_ID = inputParentSpanId;
process.env.INPUT_JOB_NAME = inputJobName;
process.env.INPUT_OTLP_OIDC_TOKEN = inputOTLPOIDCToken;
const { run } = require(path.join(__dirname, "js", "action_setup_otlp.cjs"));
await run();
} catch {
Expand Down
57 changes: 57 additions & 0 deletions setup/js/action_setup_otlp.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,47 @@ function writeEnvLine(filePath, key, value, logLabel, fileLabel) {
console.log(`[otlp] ${logLabel} written to ${fileLabel}`);
}

/**
* @param {string} headers
* @returns {boolean}
*/
function hasAuthorizationHeader(headers) {
return /(^|,)\s*authorization\s*=/i.test(headers);
}

/**
* @param {string} headers
* @param {string} token
* @returns {string}
*/
function mergeAuthorizationHeader(headers, token) {
if (hasAuthorizationHeader(headers)) return headers;
return (headers ? `${headers},` : "") + "Authorization=Bearer " + token;
}

/**
* @param {string} endpointsRaw
* @param {string} token
* @returns {string}
*/
function mergeAuthorizationIntoOTLPEndpoints(endpointsRaw, token) {
if (!endpointsRaw) return endpointsRaw;
let parsed;
try {
parsed = JSON.parse(endpointsRaw);
} catch {
return endpointsRaw;
}
if (!Array.isArray(parsed)) return endpointsRaw;
const updated = parsed.map(entry => {
if (!entry || typeof entry !== "object") return entry;
const currentHeaders = typeof entry.headers === "string" ? entry.headers : "";
const mergedHeaders = mergeAuthorizationHeader(currentHeaders, token);
return { ...entry, headers: mergedHeaders };
});
return JSON.stringify(updated);
}

/**
* Send the OTLP job-setup span and propagate trace context via GITHUB_OUTPUT /
* GITHUB_ENV. Non-fatal: all errors are silently swallowed.
Expand Down Expand Up @@ -86,6 +127,22 @@ async function run() {
process.env.INPUT_PARENT_SPAN_ID = inputParentSpanId;
}

const inputOTLPOIDCToken = getActionInput("OTLP_OIDC_TOKEN");
if (inputOTLPOIDCToken) {
const existingHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS || "";
const mergedHeaders = mergeAuthorizationHeader(existingHeaders, inputOTLPOIDCToken);

process.env.OTEL_EXPORTER_OTLP_HEADERS = mergedHeaders;
writeEnvLine(process.env.GITHUB_ENV, "OTEL_EXPORTER_OTLP_HEADERS", mergedHeaders, "OTEL_EXPORTER_OTLP_HEADERS", "GITHUB_ENV");

const existingEndpoints = process.env.GH_AW_OTLP_ENDPOINTS || "";
const mergedEndpoints = mergeAuthorizationIntoOTLPEndpoints(existingEndpoints, inputOTLPOIDCToken);
if (mergedEndpoints && mergedEndpoints !== existingEndpoints) {
process.env.GH_AW_OTLP_ENDPOINTS = mergedEndpoints;
writeEnvLine(process.env.GITHUB_ENV, "GH_AW_OTLP_ENDPOINTS", mergedEndpoints, "GH_AW_OTLP_ENDPOINTS", "GITHUB_ENV");
}
}

if (!endpoints) {
console.log("[otlp] GH_AW_OTLP_ENDPOINTS not set, skipping setup span");
} else {
Expand Down
22 changes: 20 additions & 2 deletions setup/js/add_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -825,9 +825,17 @@ async function main(config = {}) {
return recordComment(comment, isDiscussion);
} catch (error) {
const errorMessage = getErrorMessage(error);
const normalizedErrorMessage = errorMessage.toLowerCase();
// Known GitHub lock-related message fragments observed from REST/GraphQL comment APIs.
const lockPhrases = ["issue is locked", "conversation is locked", "resource is locked", "resource locked"];
const hasKnownLockPhrase = lockPhrases.some(phrase => normalizedErrorMessage.includes(phrase));

// Check if this is a 404 error (discussion/issue was deleted or wrong type)
const is404 = error?.status === 404 || errorMessage.includes("404") || errorMessage.toLowerCase().includes("not found");
const is404 = error?.status === 404 || errorMessage.includes("404") || normalizedErrorMessage.includes("not found");
const isHttp423Locked = error?.status === 423;
const isHttp403WithLockedMessage = error?.status === 403 && normalizedErrorMessage.includes("locked");
const isLockedByKnownMessageWithoutStatus = error?.status == null && hasKnownLockPhrase;
const isLocked = isHttp423Locked || isHttp403WithLockedMessage || isLockedByKnownMessageWithoutStatus;

// If 404 and item_number was explicitly provided and we tried as issue/PR,
// retry as a discussion (the user may have provided a discussion number)
Expand Down Expand Up @@ -879,7 +887,17 @@ async function main(config = {}) {
};
}

// For non-404 errors, fail as before
if (isLocked) {
// Treat locked targets as warnings - locked PRs/issues are a valid repository state
core.warning(`Target is locked, skipping comment: ${errorMessage}`);
return {
success: true,
warning: `Target is locked: ${errorMessage}`,
skipped: true,
};
}

// For all other errors, propagate the failure
core.error(`Failed to add comment: ${errorMessage}`);
return {
success: false,
Expand Down
24 changes: 17 additions & 7 deletions setup/js/add_labels.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const { tryEnforceArrayLimit } = require("./limit_enforcement_helpers.cjs");
const { logStagedPreviewInfo } = require("./staged_preview.cjs");
const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs");
const { resolveSafeOutputIssueTarget } = require("./temporary_id.cjs");
const { attachExecutionState, fetchIssueState, normalizeLabelNames } = require("./safe_output_execution_metadata.cjs");
const { MAX_LABELS } = require("./constants.cjs");
const { createCountGatedHandler } = require("./handler_scaffold.cjs");
const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs");
Expand Down Expand Up @@ -178,7 +179,8 @@ const main = createCountGatedHandler({
}

try {
await withRetry(
const beforeState = await fetchIssueState(githubClient, repoParts, itemNumber);
const { data: labels } = await withRetry(
() =>
githubClient.rest.issues.addLabels({
owner: repoParts.owner,
Expand All @@ -191,12 +193,20 @@ const main = createCountGatedHandler({
);

core.info(`Successfully added ${uniqueLabels.length} labels to ${contextType} #${itemNumber} in ${itemRepo}`);
return {
success: true,
number: itemNumber,
labelsAdded: uniqueLabels,
contextType,
};
return attachExecutionState(
{
success: true,
number: itemNumber,
repo: itemRepo,
labelsAdded: uniqueLabels,
contextType,
},
beforeState,
{
...beforeState,
labels: normalizeLabelNames(labels),
}
);
} catch (error) {
const errorMessage = getErrorMessage(error);
core.error(`Failed to add labels: ${errorMessage}`);
Expand Down
8 changes: 7 additions & 1 deletion setup/js/add_reaction.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ async function main() {

/**
* Resolve the REST API endpoint for non-discussion events.
* Returns null for discussion/discussion_comment/unsupported events (handled separately).
* Returns null for discussion/discussion_comment/pull_request_review/unsupported events (handled separately).
* @param {string} eventName
* @param {string} owner
* @param {string} repo
* @param {Record<string, any>} payload
* @returns {string | null}
*/
function resolveRestEndpoint(eventName, owner, repo, payload) {
Expand Down Expand Up @@ -108,6 +109,10 @@ function resolveRestEndpoint(eventName, owner, repo, payload) {
return `/repos/${owner}/${repo}/pulls/comments/${reviewCommentId}/reactions`;
}

case "pull_request_review":
// Reactions are not supported on review objects; skip silently.
return null;

default:
return null;
}
Expand All @@ -126,6 +131,7 @@ function isRestReactionEvent(eventName) {
* @param {string} eventName
* @param {string} owner
* @param {string} repo
* @param {Record<string, any>} payload
* @param {string} reaction
*/
async function handleGraphQLOrUnknownEvent(eventName, owner, repo, payload, reaction) {
Expand Down
Loading
Loading