Skip to content

label issue

label issue #3

# This file was automatically generated by gh-aw. DO NOT EDIT.
# To update this file, edit the corresponding .md file and run:
# gh aw compile
# For more information: https://github.com/githubnext/gh-aw/blob/main/.github/instructions/github-agentic-workflows.instructions.md
name: "label issue"
on:
issues:
types:
- opened
- reopened
permissions: {}
concurrency:
group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"
run-name: "label issue"
jobs:
check-membership:
runs-on: ubuntu-latest
outputs:
error_message: ${{ steps.check-membership.outputs.error_message }}
is_team_member: ${{ steps.check-membership.outputs.is_team_member }}
result: ${{ steps.check-membership.outputs.result }}
user_permission: ${{ steps.check-membership.outputs.user_permission }}
steps:
- name: Check team membership for workflow
id: check-membership
uses: actions/github-script@v8
env:
GITHUB_AW_REQUIRED_ROLES: admin,maintainer
with:
script: |
async function main() {
const { eventName } = context;
// skip check for safe events
const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"];
if (safeEvents.includes(eventName)) {
core.info(`✅ Event ${eventName} does not require validation`);
core.setOutput("is_team_member", "true");
core.setOutput("result", "safe_event");
return;
}
const actor = context.actor;
const { owner, repo } = context.repo;
const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES;
const requiredPermissions = requiredPermissionsEnv ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") : [];
if (!requiredPermissions || requiredPermissions.length === 0) {
core.warning("❌ Configuration error: Required permissions not specified. Contact repository administrator.");
core.setOutput("is_team_member", "false");
core.setOutput("result", "config_error");
core.setOutput("error_message", "Configuration error: Required permissions not specified");
return;
}
// Check if the actor has the required repository permissions
try {
core.debug(`Checking if user '${actor}' has required permissions for ${owner}/${repo}`);
core.debug(`Required permissions: ${requiredPermissions.join(", ")}`);
const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({
owner: owner,
repo: repo,
username: actor,
});
const permission = repoPermission.data.permission;
core.debug(`Repository permission level: ${permission}`);
// Check if user has one of the required permission levels
for (const requiredPerm of requiredPermissions) {
if (permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain")) {
core.info(`✅ User has ${permission} access to repository`);
core.setOutput("is_team_member", "true");
core.setOutput("result", "authorized");
core.setOutput("user_permission", permission);
return;
}
}
core.warning(`User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}`);
core.setOutput("is_team_member", "false");
core.setOutput("result", "insufficient_permissions");
core.setOutput("user_permission", permission);
core.setOutput(
"error_message",
`Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}`
);
} catch (repoError) {
const errorMessage = repoError instanceof Error ? repoError.message : String(repoError);
core.warning(`Repository permission check failed: ${errorMessage}`);
core.setOutput("is_team_member", "false");
core.setOutput("result", "api_error");
core.setOutput("error_message", `Repository permission check failed: ${errorMessage}`);
return;
}
}
await main();
activation:
needs: check-membership
if: needs.check-membership.outputs.is_team_member == 'true'
runs-on: ubuntu-latest
steps:
- run: echo "Activation success"
agent:
needs: activation
runs-on: ubuntu-latest
permissions: read-all
env:
GITHUB_AW_SAFE_OUTPUTS: /tmp/safe-outputs/outputs.jsonl
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-labels\":{},\"missing-tool\":{}}"
outputs:
output: ${{ steps.collect_output.outputs.output }}
output_types: ${{ steps.collect_output.outputs.output_types }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install GitHub Copilot CLI
run: npm install -g @github/copilot
- name: Setup Safe Outputs Collector MCP
run: |
mkdir -p /tmp/safe-outputs
cat > /tmp/safe-outputs/config.json << 'EOF'
{"add-labels":{},"missing-tool":{}}
EOF
cat > /tmp/safe-outputs/mcp-server.cjs << 'EOF'
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const encoder = new TextEncoder();
const SERVER_INFO = { name: "safe-outputs-mcp-server", version: "1.0.0" };
const debug = msg => process.stderr.write(`[${SERVER_INFO.name}] ${msg}\n`);
const configEnv = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
let safeOutputsConfigRaw;
if (!configEnv) {
const defaultConfigPath = "/tmp/safe-outputs/config.json";
debug(`GITHUB_AW_SAFE_OUTPUTS_CONFIG not set, attempting to read from default path: ${defaultConfigPath}`);
try {
if (fs.existsSync(defaultConfigPath)) {
debug(`Reading config from file: ${defaultConfigPath}`);
const configFileContent = fs.readFileSync(defaultConfigPath, "utf8");
debug(`Config file content length: ${configFileContent.length} characters`);
debug(`Config file read successfully, attempting to parse JSON`);
safeOutputsConfigRaw = JSON.parse(configFileContent);
debug(`Successfully parsed config from file with ${Object.keys(safeOutputsConfigRaw).length} configuration keys`);
} else {
debug(`Config file does not exist at: ${defaultConfigPath}`);
debug(`Using minimal default configuration`);
safeOutputsConfigRaw = {};
}
} catch (error) {
debug(`Error reading config file: ${error instanceof Error ? error.message : String(error)}`);
debug(`Falling back to empty configuration`);
safeOutputsConfigRaw = {};
}
} else {
debug(`Using GITHUB_AW_SAFE_OUTPUTS_CONFIG from environment variable`);
debug(`Config environment variable length: ${configEnv.length} characters`);
try {
safeOutputsConfigRaw = JSON.parse(configEnv);
debug(`Successfully parsed config from environment: ${JSON.stringify(safeOutputsConfigRaw)}`);
} catch (error) {
debug(`Error parsing config from environment: ${error instanceof Error ? error.message : String(error)}`);
throw new Error(`Failed to parse GITHUB_AW_SAFE_OUTPUTS_CONFIG: ${error instanceof Error ? error.message : String(error)}`);
}
}
const safeOutputsConfig = Object.fromEntries(Object.entries(safeOutputsConfigRaw).map(([k, v]) => [k.replace(/-/g, "_"), v]));
debug(`Final processed config: ${JSON.stringify(safeOutputsConfig)}`);
const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS || "/tmp/safe-outputs/outputs.jsonl";
if (!process.env.GITHUB_AW_SAFE_OUTPUTS) {
debug(`GITHUB_AW_SAFE_OUTPUTS not set, using default: ${outputFile}`);
const outputDir = path.dirname(outputFile);
if (!fs.existsSync(outputDir)) {
debug(`Creating output directory: ${outputDir}`);
fs.mkdirSync(outputDir, { recursive: true });
}
}
function writeMessage(obj) {
const json = JSON.stringify(obj);
debug(`send: ${json}`);
const message = json + "\n";
const bytes = encoder.encode(message);
fs.writeSync(1, bytes);
}
class ReadBuffer {
append(chunk) {
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
}
readMessage() {
if (!this._buffer) {
return null;
}
const index = this._buffer.indexOf("\n");
if (index === -1) {
return null;
}
const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
this._buffer = this._buffer.subarray(index + 1);
if (line.trim() === "") {
return this.readMessage();
}
try {
return JSON.parse(line);
} catch (error) {
throw new Error(`Parse error: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
const readBuffer = new ReadBuffer();
function onData(chunk) {
readBuffer.append(chunk);
processReadBuffer();
}
function processReadBuffer() {
while (true) {
try {
const message = readBuffer.readMessage();
if (!message) {
break;
}
debug(`recv: ${JSON.stringify(message)}`);
handleMessage(message);
} catch (error) {
debug(`Parse error: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
function replyResult(id, result) {
if (id === undefined || id === null) return;
const res = { jsonrpc: "2.0", id, result };
writeMessage(res);
}
function replyError(id, code, message, data) {
if (id === undefined || id === null) {
debug(`Error for notification: ${message}`);
return;
}
const error = { code, message };
if (data !== undefined) {
error.data = data;
}
const res = {
jsonrpc: "2.0",
id,
error,
};
writeMessage(res);
}
function appendSafeOutput(entry) {
if (!outputFile) throw new Error("No output file configured");
entry.type = entry.type.replace(/_/g, "-");
const jsonLine = JSON.stringify(entry) + "\n";
try {
fs.appendFileSync(outputFile, jsonLine);
} catch (error) {
throw new Error(`Failed to write to output file: ${error instanceof Error ? error.message : String(error)}`);
}
}
const defaultHandler = type => args => {
const entry = { ...(args || {}), type };
appendSafeOutput(entry);
return {
content: [
{
type: "text",
text: `success`,
},
],
};
};
const uploadAssetHandler = args => {
const branchName = process.env.GITHUB_AW_ASSETS_BRANCH;
if (!branchName) throw new Error("GITHUB_AW_ASSETS_BRANCH not set");
const { path: filePath } = args;
const absolutePath = path.resolve(filePath);
const workspaceDir = process.env.GITHUB_WORKSPACE || process.cwd();
const tmpDir = "/tmp";
const isInWorkspace = absolutePath.startsWith(path.resolve(workspaceDir));
const isInTmp = absolutePath.startsWith(tmpDir);
if (!isInWorkspace && !isInTmp) {
throw new Error(
`File path must be within workspace directory (${workspaceDir}) or /tmp directory. ` +
`Provided path: ${filePath} (resolved to: ${absolutePath})`
);
}
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const stats = fs.statSync(filePath);
const sizeBytes = stats.size;
const sizeKB = Math.ceil(sizeBytes / 1024);
const maxSizeKB = process.env.GITHUB_AW_ASSETS_MAX_SIZE_KB ? parseInt(process.env.GITHUB_AW_ASSETS_MAX_SIZE_KB, 10) : 10240;
if (sizeKB > maxSizeKB) {
throw new Error(`File size ${sizeKB} KB exceeds maximum allowed size ${maxSizeKB} KB`);
}
const ext = path.extname(filePath).toLowerCase();
const allowedExts = process.env.GITHUB_AW_ASSETS_ALLOWED_EXTS
? process.env.GITHUB_AW_ASSETS_ALLOWED_EXTS.split(",").map(ext => ext.trim())
: [
".png",
".jpg",
".jpeg",
];
if (!allowedExts.includes(ext)) {
throw new Error(`File extension '${ext}' is not allowed. Allowed extensions: ${allowedExts.join(", ")}`);
}
const assetsDir = "/tmp/safe-outputs/assets";
if (!fs.existsSync(assetsDir)) {
fs.mkdirSync(assetsDir, { recursive: true });
}
const fileContent = fs.readFileSync(filePath);
const sha = crypto.createHash("sha256").update(fileContent).digest("hex");
const fileName = path.basename(filePath);
const fileExt = path.extname(fileName).toLowerCase();
const targetPath = path.join(assetsDir, fileName);
fs.copyFileSync(filePath, targetPath);
const targetFileName = (sha + fileExt).toLowerCase();
const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com";
const repo = process.env.GITHUB_REPOSITORY || "owner/repo";
const url = `${githubServer.replace("github.com", "raw.githubusercontent.com")}/${repo}/${branchName}/${targetFileName}`;
const entry = {
type: "upload_asset",
path: filePath,
fileName: fileName,
sha: sha,
size: sizeBytes,
url: url,
targetFileName: targetFileName,
};
appendSafeOutput(entry);
return {
content: [
{
type: "text",
text: url,
},
],
};
};
const normTool = toolName => (toolName ? toolName.replace(/-/g, "_").toLowerCase() : undefined);
const ALL_TOOLS = [
{
name: "create_issue",
description: "Create a new GitHub issue",
inputSchema: {
type: "object",
required: ["title", "body"],
properties: {
title: { type: "string", description: "Issue title" },
body: { type: "string", description: "Issue body/description" },
labels: {
type: "array",
items: { type: "string" },
description: "Issue labels",
},
},
additionalProperties: false,
},
},
{
name: "create_discussion",
description: "Create a new GitHub discussion",
inputSchema: {
type: "object",
required: ["title", "body"],
properties: {
title: { type: "string", description: "Discussion title" },
body: { type: "string", description: "Discussion body/content" },
category: { type: "string", description: "Discussion category" },
},
additionalProperties: false,
},
},
{
name: "add_comment",
description: "Add a comment to a GitHub issue or pull request",
inputSchema: {
type: "object",
required: ["body"],
properties: {
body: { type: "string", description: "Comment body/content" },
issue_number: {
type: "number",
description: "Issue or PR number (optional for current context)",
},
},
additionalProperties: false,
},
},
{
name: "create_pull_request",
description: "Create a new GitHub pull request",
inputSchema: {
type: "object",
required: ["title", "body", "branch"],
properties: {
title: { type: "string", description: "Pull request title" },
body: {
type: "string",
description: "Pull request body/description",
},
branch: {
type: "string",
description: "Required branch name",
},
labels: {
type: "array",
items: { type: "string" },
description: "Optional labels to add to the PR",
},
},
additionalProperties: false,
},
},
{
name: "create_pull_request_review_comment",
description: "Create a review comment on a GitHub pull request",
inputSchema: {
type: "object",
required: ["path", "line", "body"],
properties: {
path: {
type: "string",
description: "File path for the review comment",
},
line: {
type: ["number", "string"],
description: "Line number for the comment",
},
body: { type: "string", description: "Comment body content" },
start_line: {
type: ["number", "string"],
description: "Optional start line for multi-line comments",
},
side: {
type: "string",
enum: ["LEFT", "RIGHT"],
description: "Optional side of the diff: LEFT or RIGHT",
},
},
additionalProperties: false,
},
},
{
name: "create_code_scanning_alert",
description: "Create a code scanning alert. severity MUST be one of 'error', 'warning', 'info', 'note'.",
inputSchema: {
type: "object",
required: ["file", "line", "severity", "message"],
properties: {
file: {
type: "string",
description: "File path where the issue was found",
},
line: {
type: ["number", "string"],
description: "Line number where the issue was found",
},
severity: {
type: "string",
enum: ["error", "warning", "info", "note"],
description:
' Security severity levels follow the industry-standard Common Vulnerability Scoring System (CVSS) that is also used for advisories in the GitHub Advisory Database and must be one of "error", "warning", "info", "note".',
},
message: {
type: "string",
description: "Alert message describing the issue",
},
column: {
type: ["number", "string"],
description: "Optional column number",
},
ruleIdSuffix: {
type: "string",
description: "Optional rule ID suffix for uniqueness",
},
},
additionalProperties: false,
},
},
{
name: "add_labels",
description: "Add labels to a GitHub issue or pull request",
inputSchema: {
type: "object",
required: ["labels"],
properties: {
labels: {
type: "array",
items: { type: "string" },
description: "Labels to add",
},
issue_number: {
type: "number",
description: "Issue or PR number (optional for current context)",
},
},
additionalProperties: false,
},
},
{
name: "update_issue",
description: "Update a GitHub issue",
inputSchema: {
type: "object",
properties: {
status: {
type: "string",
enum: ["open", "closed"],
description: "Optional new issue status",
},
title: { type: "string", description: "Optional new issue title" },
body: { type: "string", description: "Optional new issue body" },
issue_number: {
type: ["number", "string"],
description: "Optional issue number for target '*'",
},
},
additionalProperties: false,
},
},
{
name: "push_to_pull_request_branch",
description: "Push changes to a pull request branch",
inputSchema: {
type: "object",
required: ["branch", "message"],
properties: {
branch: {
type: "string",
description: "The name of the branch to push to, should be the branch name associated with the pull request",
},
message: { type: "string", description: "Commit message" },
pull_request_number: {
type: ["number", "string"],
description: "Optional pull request number for target '*'",
},
},
additionalProperties: false,
},
},
{
name: "upload_asset",
description: "Publish a file as a URL-addressable asset to an orphaned git branch",
inputSchema: {
type: "object",
required: ["path"],
properties: {
path: {
type: "string",
description:
"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.",
},
},
additionalProperties: false,
},
handler: uploadAssetHandler,
},
{
name: "missing_tool",
description: "Report a missing tool or functionality needed to complete tasks",
inputSchema: {
type: "object",
required: ["tool", "reason"],
properties: {
tool: { type: "string", description: "Name of the missing tool" },
reason: { type: "string", description: "Why this tool is needed" },
alternatives: {
type: "string",
description: "Possible alternatives or workarounds",
},
},
additionalProperties: false,
},
},
];
debug(`v${SERVER_INFO.version} ready on stdio`);
debug(` output file: ${outputFile}`);
debug(` config: ${JSON.stringify(safeOutputsConfig)}`);
const TOOLS = {};
ALL_TOOLS.forEach(tool => {
if (Object.keys(safeOutputsConfig).find(config => normTool(config) === tool.name)) {
TOOLS[tool.name] = tool;
}
});
Object.keys(safeOutputsConfig).forEach(configKey => {
const normalizedKey = normTool(configKey);
if (TOOLS[normalizedKey]) {
return;
}
if (!ALL_TOOLS.find(t => t.name === normalizedKey)) {
const jobConfig = safeOutputsConfig[configKey];
const dynamicTool = {
name: normalizedKey,
description: `Custom safe-job: ${configKey}`,
inputSchema: {
type: "object",
properties: {},
additionalProperties: true,
},
handler: args => {
const entry = {
type: normalizedKey,
...args,
};
const entryJSON = JSON.stringify(entry);
fs.appendFileSync(outputFile, entryJSON + "\n");
const outputText =
jobConfig && jobConfig.output
? jobConfig.output
: `Safe-job '${configKey}' executed successfully with arguments: ${JSON.stringify(args)}`;
return {
content: [
{
type: "text",
text: outputText,
},
],
};
},
};
if (jobConfig && jobConfig.inputs) {
dynamicTool.inputSchema.properties = {};
dynamicTool.inputSchema.required = [];
Object.keys(jobConfig.inputs).forEach(inputName => {
const inputDef = jobConfig.inputs[inputName];
const propSchema = {
type: inputDef.type || "string",
description: inputDef.description || `Input parameter: ${inputName}`,
};
if (inputDef.options && Array.isArray(inputDef.options)) {
propSchema.enum = inputDef.options;
}
dynamicTool.inputSchema.properties[inputName] = propSchema;
if (inputDef.required) {
dynamicTool.inputSchema.required.push(inputName);
}
});
}
TOOLS[normalizedKey] = dynamicTool;
}
});
debug(` tools: ${Object.keys(TOOLS).join(", ")}`);
if (!Object.keys(TOOLS).length) throw new Error("No tools enabled in configuration");
function handleMessage(req) {
if (!req || typeof req !== "object") {
debug(`Invalid message: not an object`);
return;
}
if (req.jsonrpc !== "2.0") {
debug(`Invalid message: missing or invalid jsonrpc field`);
return;
}
const { id, method, params } = req;
if (!method || typeof method !== "string") {
replyError(id, -32600, "Invalid Request: method must be a string");
return;
}
try {
if (method === "initialize") {
const clientInfo = params?.clientInfo ?? {};
console.error(`client info:`, clientInfo);
const protocolVersion = params?.protocolVersion ?? undefined;
const result = {
serverInfo: SERVER_INFO,
...(protocolVersion ? { protocolVersion } : {}),
capabilities: {
tools: {},
},
};
replyResult(id, result);
} else if (method === "tools/list") {
const list = [];
Object.values(TOOLS).forEach(tool => {
list.push({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
});
});
replyResult(id, { tools: list });
} else if (method === "tools/call") {
const name = params?.name;
const args = params?.arguments ?? {};
if (!name || typeof name !== "string") {
replyError(id, -32602, "Invalid params: 'name' must be a string");
return;
}
const tool = TOOLS[normTool(name)];
if (!tool) {
replyError(id, -32601, `Tool not found: ${name} (${normTool(name)})`);
return;
}
const handler = tool.handler || defaultHandler(tool.name);
const requiredFields = tool.inputSchema && Array.isArray(tool.inputSchema.required) ? tool.inputSchema.required : [];
if (requiredFields.length) {
const missing = requiredFields.filter(f => {
const value = args[f];
return value === undefined || value === null || (typeof value === "string" && value.trim() === "");
});
if (missing.length) {
replyError(id, -32602, `Invalid arguments: missing or empty ${missing.map(m => `'${m}'`).join(", ")}`);
return;
}
}
const result = handler(args);
const content = result && result.content ? result.content : [];
replyResult(id, { content });
} else if (/^notifications\//.test(method)) {
debug(`ignore ${method}`);
} else {
replyError(id, -32601, `Method not found: ${method}`);
}
} catch (e) {
replyError(id, -32603, "Internal error", {
message: e instanceof Error ? e.message : String(e),
});
}
}
process.stdin.on("data", onData);
process.stdin.on("error", err => debug(`stdin error: ${err}`));
process.stdin.resume();
debug(`listening...`);
EOF
chmod +x /tmp/safe-outputs/mcp-server.cjs
- name: Setup MCPs
env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-labels\":{},\"missing-tool\":{}}"
run: |
mkdir -p /tmp/mcp-config
mkdir -p /home/runner/.copilot
cat > /home/runner/.copilot/mcp-config.json << 'EOF'
{
"mcpServers": {
"github": {
"type": "local",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN=${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}",
"ghcr.io/github/github-mcp-server:sha-09deac4"
],
"tools": ["*"]
},
"safe_outputs": {
"type": "local",
"command": "node",
"args": ["/tmp/safe-outputs/mcp-server.cjs"],
"tools": ["*"],
"env": {
"GITHUB_AW_SAFE_OUTPUTS": "${{ env.GITHUB_AW_SAFE_OUTPUTS }}",
"GITHUB_AW_SAFE_OUTPUTS_CONFIG": ${{ toJSON(env.GITHUB_AW_SAFE_OUTPUTS_CONFIG) }}
}
}
}
}
EOF
echo "-------START MCP CONFIG-----------"
cat /home/runner/.copilot/mcp-config.json
echo "-------END MCP CONFIG-----------"
echo "-------/home/runner/.copilot-----------"
find /home/runner/.copilot
echo "HOME: $HOME"
echo "GITHUB_COPILOT_CLI_MODE: $GITHUB_COPILOT_CLI_MODE"
- name: Create prompt
env:
GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
run: |
mkdir -p $(dirname "$GITHUB_AW_PROMPT")
cat > $GITHUB_AW_PROMPT << 'EOF'
# label issue
label the current issue
EOF
- name: Append safe outputs instructions to prompt
env:
GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt
run: |
cat >> $GITHUB_AW_PROMPT << 'EOF'
---
## Adding Labels to Issues or Pull Requests, Reporting Missing Tools or Functionality
**IMPORTANT**: To do the actions mentioned in the header of this section, use the **safe-outputs** tools, do NOT attempt to use `gh`, do NOT attempt to use the GitHub API. You don't have write access to the GitHub repo.
**Adding Labels to Issues or Pull Requests**
To add labels to an issue or a pull request, use the add-labels tool from the safe-outputs MCP
**Reporting Missing Tools or Functionality**
To report a missing tool use the missing-tool tool from the safe-outputs MCP.
EOF
- name: Print prompt to step summary
env:
GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt
run: |
echo "## Generated Prompt" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '``````markdown' >> $GITHUB_STEP_SUMMARY
cat $GITHUB_AW_PROMPT >> $GITHUB_STEP_SUMMARY
echo '``````' >> $GITHUB_STEP_SUMMARY
- name: Capture agent version
run: |
VERSION_OUTPUT=$(copilot --version 2>&1 || echo "unknown")
# Extract semantic version pattern (e.g., 1.2.3, v1.2.3-beta)
CLEAN_VERSION=$(echo "$VERSION_OUTPUT" | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?' | head -n1 || echo "unknown")
echo "AGENT_VERSION=$CLEAN_VERSION" >> $GITHUB_ENV
echo "Agent version: $VERSION_OUTPUT"
- name: Generate agentic run info
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const awInfo = {
engine_id: "copilot",
engine_name: "GitHub Copilot CLI",
model: "",
version: "",
agent_version: process.env.AGENT_VERSION || "",
workflow_name: "label issue",
experimental: false,
supports_tools_allowlist: true,
supports_http_transport: true,
run_id: context.runId,
run_number: context.runNumber,
run_attempt: process.env.GITHUB_RUN_ATTEMPT,
repository: context.repo.owner + '/' + context.repo.repo,
ref: context.ref,
sha: context.sha,
actor: context.actor,
event_name: context.eventName,
staged: false,
created_at: new Date().toISOString()
};
// Write to /tmp directory to avoid inclusion in PR
const tmpPath = '/tmp/aw_info.json';
fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2));
console.log('Generated aw_info.json at:', tmpPath);
console.log(JSON.stringify(awInfo, null, 2));
// Add agentic workflow run information to step summary
core.summary
.addRaw('## Agentic Run Information\n\n')
.addRaw('```json\n')
.addRaw(JSON.stringify(awInfo, null, 2))
.addRaw('\n```\n')
.write();
- name: Upload agentic run info
if: always()
uses: actions/upload-artifact@v4
with:
name: aw_info.json
path: /tmp/aw_info.json
if-no-files-found: warn
- name: Execute GitHub Copilot CLI
id: agentic_execution
# Copilot CLI tool arguments (sorted):
# --allow-tool github(download_workflow_run_artifact)
# --allow-tool github(get_code_scanning_alert)
# --allow-tool github(get_commit)
# --allow-tool github(get_dependabot_alert)
# --allow-tool github(get_discussion)
# --allow-tool github(get_discussion_comments)
# --allow-tool github(get_file_contents)
# --allow-tool github(get_issue)
# --allow-tool github(get_issue_comments)
# --allow-tool github(get_job_logs)
# --allow-tool github(get_latest_release)
# --allow-tool github(get_me)
# --allow-tool github(get_notification_details)
# --allow-tool github(get_pull_request)
# --allow-tool github(get_pull_request_comments)
# --allow-tool github(get_pull_request_diff)
# --allow-tool github(get_pull_request_files)
# --allow-tool github(get_pull_request_review_comments)
# --allow-tool github(get_pull_request_reviews)
# --allow-tool github(get_pull_request_status)
# --allow-tool github(get_release_by_tag)
# --allow-tool github(get_secret_scanning_alert)
# --allow-tool github(get_tag)
# --allow-tool github(get_workflow_run)
# --allow-tool github(get_workflow_run_logs)
# --allow-tool github(get_workflow_run_usage)
# --allow-tool github(list_branches)
# --allow-tool github(list_code_scanning_alerts)
# --allow-tool github(list_commits)
# --allow-tool github(list_dependabot_alerts)
# --allow-tool github(list_discussion_categories)
# --allow-tool github(list_discussions)
# --allow-tool github(list_issue_types)
# --allow-tool github(list_issues)
# --allow-tool github(list_notifications)
# --allow-tool github(list_pull_requests)
# --allow-tool github(list_releases)
# --allow-tool github(list_secret_scanning_alerts)
# --allow-tool github(list_starred_repositories)
# --allow-tool github(list_sub_issues)
# --allow-tool github(list_tags)
# --allow-tool github(list_workflow_jobs)
# --allow-tool github(list_workflow_run_artifacts)
# --allow-tool github(list_workflow_runs)
# --allow-tool github(list_workflows)
# --allow-tool github(search_code)
# --allow-tool github(search_issues)
# --allow-tool github(search_orgs)
# --allow-tool github(search_pull_requests)
# --allow-tool github(search_repositories)
# --allow-tool github(search_users)
# --allow-tool safe_outputs
timeout-minutes: 5
run: |
set -o pipefail
INSTRUCTION=$(cat /tmp/aw-prompts/prompt.txt)
# Run copilot CLI with log capture
copilot --add-dir /tmp/ --log-level all --log-dir /tmp/.copilot/logs/ --allow-tool 'github(download_workflow_run_artifact)' --allow-tool 'github(get_code_scanning_alert)' --allow-tool 'github(get_commit)' --allow-tool 'github(get_dependabot_alert)' --allow-tool 'github(get_discussion)' --allow-tool 'github(get_discussion_comments)' --allow-tool 'github(get_file_contents)' --allow-tool 'github(get_issue)' --allow-tool 'github(get_issue_comments)' --allow-tool 'github(get_job_logs)' --allow-tool 'github(get_latest_release)' --allow-tool 'github(get_me)' --allow-tool 'github(get_notification_details)' --allow-tool 'github(get_pull_request)' --allow-tool 'github(get_pull_request_comments)' --allow-tool 'github(get_pull_request_diff)' --allow-tool 'github(get_pull_request_files)' --allow-tool 'github(get_pull_request_review_comments)' --allow-tool 'github(get_pull_request_reviews)' --allow-tool 'github(get_pull_request_status)' --allow-tool 'github(get_release_by_tag)' --allow-tool 'github(get_secret_scanning_alert)' --allow-tool 'github(get_tag)' --allow-tool 'github(get_workflow_run)' --allow-tool 'github(get_workflow_run_logs)' --allow-tool 'github(get_workflow_run_usage)' --allow-tool 'github(list_branches)' --allow-tool 'github(list_code_scanning_alerts)' --allow-tool 'github(list_commits)' --allow-tool 'github(list_dependabot_alerts)' --allow-tool 'github(list_discussion_categories)' --allow-tool 'github(list_discussions)' --allow-tool 'github(list_issue_types)' --allow-tool 'github(list_issues)' --allow-tool 'github(list_notifications)' --allow-tool 'github(list_pull_requests)' --allow-tool 'github(list_releases)' --allow-tool 'github(list_secret_scanning_alerts)' --allow-tool 'github(list_starred_repositories)' --allow-tool 'github(list_sub_issues)' --allow-tool 'github(list_tags)' --allow-tool 'github(list_workflow_jobs)' --allow-tool 'github(list_workflow_run_artifacts)' --allow-tool 'github(list_workflow_runs)' --allow-tool 'github(list_workflows)' --allow-tool 'github(search_code)' --allow-tool 'github(search_issues)' --allow-tool 'github(search_orgs)' --allow-tool 'github(search_pull_requests)' --allow-tool 'github(search_repositories)' --allow-tool 'github(search_users)' --allow-tool safe_outputs --prompt "$INSTRUCTION" 2>&1 | tee /tmp/agent-stdio.log
env:
COPILOT_AGENT_RUNNER_TYPE: STANDALONE
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }}
GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}
XDG_CONFIG_HOME: /home/runner
- name: Ensure log file exists
if: always()
run: |
# Ensure log file exists
touch /tmp/agent-stdio.log
# Show last few lines for debugging
echo "=== Last 10 lines of Copilot execution log ==="
tail -10 /tmp/agent-stdio.log || echo "No log content available"
- name: Print Safe Outputs
env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
run: |
echo "## Safe Outputs (JSONL)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '``````json' >> $GITHUB_STEP_SUMMARY
if [ -f ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ]; then
cat ${{ env.GITHUB_AW_SAFE_OUTPUTS }} >> $GITHUB_STEP_SUMMARY
# Ensure there's a newline after the file content if it doesn't end with one
if [ -s ${{ env.GITHUB_AW_SAFE_OUTPUTS }} ] && [ "$(tail -c1 ${{ env.GITHUB_AW_SAFE_OUTPUTS }})" != "" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
fi
else
echo "No agent output file found" >> $GITHUB_STEP_SUMMARY
fi
echo '``````' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
- name: Upload Safe Outputs
if: always()
uses: actions/upload-artifact@v4
with:
name: safe_output.jsonl
path: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
if-no-files-found: warn
- name: Ingest agent output
id: collect_output
uses: actions/github-script@v8
env:
GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}
GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-labels\":{},\"missing-tool\":{}}"
with:
script: |
async function main() {
const fs = require("fs");
function sanitizeContent(content) {
if (!content || typeof content !== "string") {
return "";
}
const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS;
const defaultAllowedDomains = ["github.com", "github.io", "githubusercontent.com", "githubassets.com", "github.dev", "codespaces.new"];
const allowedDomains = allowedDomainsEnv
? allowedDomainsEnv
.split(",")
.map(d => d.trim())
.filter(d => d)
: defaultAllowedDomains;
let sanitized = content;
sanitized = neutralizeMentions(sanitized);
sanitized = removeXmlComments(sanitized);
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
sanitized = sanitizeUrlProtocols(sanitized);
sanitized = sanitizeUrlDomains(sanitized);
const maxLength = 524288;
if (sanitized.length > maxLength) {
sanitized = sanitized.substring(0, maxLength) + "\n[Content truncated due to length]";
}
const lines = sanitized.split("\n");
const maxLines = 65000;
if (lines.length > maxLines) {
sanitized = lines.slice(0, maxLines).join("\n") + "\n[Content truncated due to line count]";
}
sanitized = neutralizeBotTriggers(sanitized);
return sanitized.trim();
function sanitizeUrlDomains(s) {
return s.replace(/\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi, match => {
const urlAfterProtocol = match.slice(8);
const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase();
const isAllowed = allowedDomains.some(allowedDomain => {
const normalizedAllowed = allowedDomain.toLowerCase();
return hostname === normalizedAllowed || hostname.endsWith("." + normalizedAllowed);
});
return isAllowed ? match : "(redacted)";
});
}
function sanitizeUrlProtocols(s) {
return s.replace(/\b(\w+):\/\/[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => {
return protocol.toLowerCase() === "https" ? match : "(redacted)";
});
}
function neutralizeMentions(s) {
return s.replace(
/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
(_m, p1, p2) => `${p1}\`@${p2}\``
);
}
function removeXmlComments(s) {
return s.replace(/<!--[\s\S]*?-->/g, "").replace(/<!--[\s\S]*?--!>/g, "");
}
function neutralizeBotTriggers(s) {
return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi, (match, action, ref) => `\`${action} #${ref}\``);
}
}
function getMaxAllowedForType(itemType, config) {
const itemConfig = config?.[itemType];
if (itemConfig && typeof itemConfig === "object" && "max" in itemConfig && itemConfig.max) {
return itemConfig.max;
}
switch (itemType) {
case "create-issue":
return 1;
case "add-comment":
return 1;
case "create-pull-request":
return 1;
case "create-pull-request-review-comment":
return 1;
case "add-labels":
return 5;
case "update-issue":
return 1;
case "push-to-pull-request-branch":
return 1;
case "create-discussion":
return 1;
case "missing-tool":
return 1000;
case "create-code-scanning-alert":
return 1000;
case "upload-asset":
return 10;
default:
return 1;
}
}
function getMinRequiredForType(itemType, config) {
const itemConfig = config?.[itemType];
if (itemConfig && typeof itemConfig === "object" && "min" in itemConfig && itemConfig.min) {
return itemConfig.min;
}
return 0;
}
function repairJson(jsonStr) {
let repaired = jsonStr.trim();
const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" };
repaired = repaired.replace(/[\u0000-\u001F]/g, ch => {
const c = ch.charCodeAt(0);
return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0");
});
repaired = repaired.replace(/'/g, '"');
repaired = repaired.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":');
repaired = repaired.replace(/"([^"\\]*)"/g, (match, content) => {
if (content.includes("\n") || content.includes("\r") || content.includes("\t")) {
const escaped = content.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
return `"${escaped}"`;
}
return match;
});
repaired = repaired.replace(/"([^"]*)"([^":,}\]]*)"([^"]*)"(\s*[,:}\]])/g, (match, p1, p2, p3, p4) => `"${p1}\\"${p2}\\"${p3}"${p4}`);
repaired = repaired.replace(/(\[\s*(?:"[^"]*"(?:\s*,\s*"[^"]*")*\s*),?)\s*}/g, "$1]");
const openBraces = (repaired.match(/\{/g) || []).length;
const closeBraces = (repaired.match(/\}/g) || []).length;
if (openBraces > closeBraces) {
repaired += "}".repeat(openBraces - closeBraces);
} else if (closeBraces > openBraces) {
repaired = "{".repeat(closeBraces - openBraces) + repaired;
}
const openBrackets = (repaired.match(/\[/g) || []).length;
const closeBrackets = (repaired.match(/\]/g) || []).length;
if (openBrackets > closeBrackets) {
repaired += "]".repeat(openBrackets - closeBrackets);
} else if (closeBrackets > openBrackets) {
repaired = "[".repeat(closeBrackets - openBrackets) + repaired;
}
repaired = repaired.replace(/,(\s*[}\]])/g, "$1");
return repaired;
}
function validatePositiveInteger(value, fieldName, lineNum) {
if (value === undefined || value === null) {
if (fieldName.includes("create-code-scanning-alert 'line'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-code-scanning-alert requires a 'line' field (number or string)`,
};
}
if (fieldName.includes("create-pull-request-review-comment 'line'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-pull-request-review-comment requires a 'line' number`,
};
}
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} is required`,
};
}
if (typeof value !== "number" && typeof value !== "string") {
if (fieldName.includes("create-code-scanning-alert 'line'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-code-scanning-alert requires a 'line' field (number or string)`,
};
}
if (fieldName.includes("create-pull-request-review-comment 'line'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-pull-request-review-comment requires a 'line' number or string field`,
};
}
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be a number or string`,
};
}
const parsed = typeof value === "string" ? parseInt(value, 10) : value;
if (isNaN(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
if (fieldName.includes("create-code-scanning-alert 'line'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-code-scanning-alert 'line' must be a valid positive integer (got: ${value})`,
};
}
if (fieldName.includes("create-pull-request-review-comment 'line'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-pull-request-review-comment 'line' must be a positive integer`,
};
}
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be a positive integer (got: ${value})`,
};
}
return { isValid: true, normalizedValue: parsed };
}
function validateOptionalPositiveInteger(value, fieldName, lineNum) {
if (value === undefined) {
return { isValid: true };
}
if (typeof value !== "number" && typeof value !== "string") {
if (fieldName.includes("create-pull-request-review-comment 'start_line'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-pull-request-review-comment 'start_line' must be a number or string`,
};
}
if (fieldName.includes("create-code-scanning-alert 'column'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-code-scanning-alert 'column' must be a number or string`,
};
}
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be a number or string`,
};
}
const parsed = typeof value === "string" ? parseInt(value, 10) : value;
if (isNaN(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
if (fieldName.includes("create-pull-request-review-comment 'start_line'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-pull-request-review-comment 'start_line' must be a positive integer`,
};
}
if (fieldName.includes("create-code-scanning-alert 'column'")) {
return {
isValid: false,
error: `Line ${lineNum}: create-code-scanning-alert 'column' must be a valid positive integer (got: ${value})`,
};
}
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be a positive integer (got: ${value})`,
};
}
return { isValid: true, normalizedValue: parsed };
}
function validateIssueOrPRNumber(value, fieldName, lineNum) {
if (value === undefined) {
return { isValid: true };
}
if (typeof value !== "number" && typeof value !== "string") {
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be a number or string`,
};
}
return { isValid: true };
}
function validateFieldWithInputSchema(value, fieldName, inputSchema, lineNum) {
if (inputSchema.required && (value === undefined || value === null)) {
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} is required`,
};
}
if (value === undefined || value === null) {
return {
isValid: true,
normalizedValue: inputSchema.default || undefined,
};
}
const inputType = inputSchema.type || "string";
let normalizedValue = value;
switch (inputType) {
case "string":
if (typeof value !== "string") {
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be a string`,
};
}
normalizedValue = sanitizeContent(value);
break;
case "boolean":
if (typeof value !== "boolean") {
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be a boolean`,
};
}
break;
case "number":
if (typeof value !== "number") {
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be a number`,
};
}
break;
case "choice":
if (typeof value !== "string") {
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be a string for choice type`,
};
}
if (inputSchema.options && !inputSchema.options.includes(value)) {
return {
isValid: false,
error: `Line ${lineNum}: ${fieldName} must be one of: ${inputSchema.options.join(", ")}`,
};
}
normalizedValue = sanitizeContent(value);
break;
default:
if (typeof value === "string") {
normalizedValue = sanitizeContent(value);
}
break;
}
return {
isValid: true,
normalizedValue,
};
}
function validateItemWithSafeJobConfig(item, jobConfig, lineNum) {
const errors = [];
const normalizedItem = { ...item };
if (!jobConfig.inputs) {
return {
isValid: true,
errors: [],
normalizedItem: item,
};
}
for (const [fieldName, inputSchema] of Object.entries(jobConfig.inputs)) {
const fieldValue = item[fieldName];
const validation = validateFieldWithInputSchema(fieldValue, fieldName, inputSchema, lineNum);
if (!validation.isValid && validation.error) {
errors.push(validation.error);
} else if (validation.normalizedValue !== undefined) {
normalizedItem[fieldName] = validation.normalizedValue;
}
}
return {
isValid: errors.length === 0,
errors,
normalizedItem,
};
}
function parseJsonWithRepair(jsonStr) {
try {
return JSON.parse(jsonStr);
} catch (originalError) {
try {
const repairedJson = repairJson(jsonStr);
return JSON.parse(repairedJson);
} catch (repairError) {
core.info(`invalid input json: ${jsonStr}`);
const originalMsg = originalError instanceof Error ? originalError.message : String(originalError);
const repairMsg = repairError instanceof Error ? repairError.message : String(repairError);
throw new Error(`JSON parsing failed. Original: ${originalMsg}. After attempted repair: ${repairMsg}`);
}
}
}
const outputFile = process.env.GITHUB_AW_SAFE_OUTPUTS;
const safeOutputsConfig = process.env.GITHUB_AW_SAFE_OUTPUTS_CONFIG;
if (!outputFile) {
core.info("GITHUB_AW_SAFE_OUTPUTS not set, no output to collect");
core.setOutput("output", "");
return;
}
if (!fs.existsSync(outputFile)) {
core.info(`Output file does not exist: ${outputFile}`);
core.setOutput("output", "");
return;
}
const outputContent = fs.readFileSync(outputFile, "utf8");
if (outputContent.trim() === "") {
core.info("Output file is empty");
}
core.info(`Raw output content length: ${outputContent.length}`);
let expectedOutputTypes = {};
if (safeOutputsConfig) {
try {
expectedOutputTypes = JSON.parse(safeOutputsConfig);
core.info(`Expected output types: ${JSON.stringify(Object.keys(expectedOutputTypes))}`);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
core.info(`Warning: Could not parse safe-outputs config: ${errorMsg}`);
}
}
const lines = outputContent.trim().split("\n");
const parsedItems = [];
const errors = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line === "") continue;
try {
const item = parseJsonWithRepair(line);
if (item === undefined) {
errors.push(`Line ${i + 1}: Invalid JSON - JSON parsing failed`);
continue;
}
if (!item.type) {
errors.push(`Line ${i + 1}: Missing required 'type' field`);
continue;
}
const itemType = item.type;
if (!expectedOutputTypes[itemType]) {
errors.push(`Line ${i + 1}: Unexpected output type '${itemType}'. Expected one of: ${Object.keys(expectedOutputTypes).join(", ")}`);
continue;
}
const typeCount = parsedItems.filter(existing => existing.type === itemType).length;
const maxAllowed = getMaxAllowedForType(itemType, expectedOutputTypes);
if (typeCount >= maxAllowed) {
errors.push(`Line ${i + 1}: Too many items of type '${itemType}'. Maximum allowed: ${maxAllowed}.`);
continue;
}
core.info(`Line ${i + 1}: type '${itemType}'`);
switch (itemType) {
case "create-issue":
if (!item.title || typeof item.title !== "string") {
errors.push(`Line ${i + 1}: create_issue requires a 'title' string field`);
continue;
}
if (!item.body || typeof item.body !== "string") {
errors.push(`Line ${i + 1}: create_issue requires a 'body' string field`);
continue;
}
item.title = sanitizeContent(item.title);
item.body = sanitizeContent(item.body);
if (item.labels && Array.isArray(item.labels)) {
item.labels = item.labels.map(label => (typeof label === "string" ? sanitizeContent(label) : label));
}
break;
case "add-comment":
if (!item.body || typeof item.body !== "string") {
errors.push(`Line ${i + 1}: add_comment requires a 'body' string field`);
continue;
}
const issueNumValidation = validateIssueOrPRNumber(item.issue_number, "add_comment 'issue_number'", i + 1);
if (!issueNumValidation.isValid) {
if (issueNumValidation.error) errors.push(issueNumValidation.error);
continue;
}
item.body = sanitizeContent(item.body);
break;
case "create-pull-request":
if (!item.title || typeof item.title !== "string") {
errors.push(`Line ${i + 1}: create_pull_request requires a 'title' string field`);
continue;
}
if (!item.body || typeof item.body !== "string") {
errors.push(`Line ${i + 1}: create_pull_request requires a 'body' string field`);
continue;
}
if (!item.branch || typeof item.branch !== "string") {
errors.push(`Line ${i + 1}: create_pull_request requires a 'branch' string field`);
continue;
}
item.title = sanitizeContent(item.title);
item.body = sanitizeContent(item.body);
item.branch = sanitizeContent(item.branch);
if (item.labels && Array.isArray(item.labels)) {
item.labels = item.labels.map(label => (typeof label === "string" ? sanitizeContent(label) : label));
}
break;
case "add-labels":
if (!item.labels || !Array.isArray(item.labels)) {
errors.push(`Line ${i + 1}: add_labels requires a 'labels' array field`);
continue;
}
if (item.labels.some(label => typeof label !== "string")) {
errors.push(`Line ${i + 1}: add_labels labels array must contain only strings`);
continue;
}
const labelsIssueNumValidation = validateIssueOrPRNumber(item.issue_number, "add-labels 'issue_number'", i + 1);
if (!labelsIssueNumValidation.isValid) {
if (labelsIssueNumValidation.error) errors.push(labelsIssueNumValidation.error);
continue;
}
item.labels = item.labels.map(label => sanitizeContent(label));
break;
case "update-issue":
const hasValidField = item.status !== undefined || item.title !== undefined || item.body !== undefined;
if (!hasValidField) {
errors.push(`Line ${i + 1}: update_issue requires at least one of: 'status', 'title', or 'body' fields`);
continue;
}
if (item.status !== undefined) {
if (typeof item.status !== "string" || (item.status !== "open" && item.status !== "closed")) {
errors.push(`Line ${i + 1}: update_issue 'status' must be 'open' or 'closed'`);
continue;
}
}
if (item.title !== undefined) {
if (typeof item.title !== "string") {
errors.push(`Line ${i + 1}: update-issue 'title' must be a string`);
continue;
}
item.title = sanitizeContent(item.title);
}
if (item.body !== undefined) {
if (typeof item.body !== "string") {
errors.push(`Line ${i + 1}: update-issue 'body' must be a string`);
continue;
}
item.body = sanitizeContent(item.body);
}
const updateIssueNumValidation = validateIssueOrPRNumber(item.issue_number, "update-issue 'issue_number'", i + 1);
if (!updateIssueNumValidation.isValid) {
if (updateIssueNumValidation.error) errors.push(updateIssueNumValidation.error);
continue;
}
break;
case "push-to-pull-request-branch":
if (!item.branch || typeof item.branch !== "string") {
errors.push(`Line ${i + 1}: push_to_pull_request_branch requires a 'branch' string field`);
continue;
}
if (!item.message || typeof item.message !== "string") {
errors.push(`Line ${i + 1}: push_to_pull_request_branch requires a 'message' string field`);
continue;
}
item.branch = sanitizeContent(item.branch);
item.message = sanitizeContent(item.message);
const pushPRNumValidation = validateIssueOrPRNumber(
item.pull_request_number,
"push-to-pull-request-branch 'pull_request_number'",
i + 1
);
if (!pushPRNumValidation.isValid) {
if (pushPRNumValidation.error) errors.push(pushPRNumValidation.error);
continue;
}
break;
case "create-pull-request-review-comment":
if (!item.path || typeof item.path !== "string") {
errors.push(`Line ${i + 1}: create-pull-request-review-comment requires a 'path' string field`);
continue;
}
const lineValidation = validatePositiveInteger(item.line, "create-pull-request-review-comment 'line'", i + 1);
if (!lineValidation.isValid) {
if (lineValidation.error) errors.push(lineValidation.error);
continue;
}
const lineNumber = lineValidation.normalizedValue;
if (!item.body || typeof item.body !== "string") {
errors.push(`Line ${i + 1}: create-pull-request-review-comment requires a 'body' string field`);
continue;
}
item.body = sanitizeContent(item.body);
const startLineValidation = validateOptionalPositiveInteger(
item.start_line,
"create-pull-request-review-comment 'start_line'",
i + 1
);
if (!startLineValidation.isValid) {
if (startLineValidation.error) errors.push(startLineValidation.error);
continue;
}
if (
startLineValidation.normalizedValue !== undefined &&
lineNumber !== undefined &&
startLineValidation.normalizedValue > lineNumber
) {
errors.push(`Line ${i + 1}: create-pull-request-review-comment 'start_line' must be less than or equal to 'line'`);
continue;
}
if (item.side !== undefined) {
if (typeof item.side !== "string" || (item.side !== "LEFT" && item.side !== "RIGHT")) {
errors.push(`Line ${i + 1}: create-pull-request-review-comment 'side' must be 'LEFT' or 'RIGHT'`);
continue;
}
}
break;
case "create-discussion":
if (!item.title || typeof item.title !== "string") {
errors.push(`Line ${i + 1}: create_discussion requires a 'title' string field`);
continue;
}
if (!item.body || typeof item.body !== "string") {
errors.push(`Line ${i + 1}: create_discussion requires a 'body' string field`);
continue;
}
if (item.category !== undefined) {
if (typeof item.category !== "string") {
errors.push(`Line ${i + 1}: create_discussion 'category' must be a string`);
continue;
}
item.category = sanitizeContent(item.category);
}
item.title = sanitizeContent(item.title);
item.body = sanitizeContent(item.body);
break;
case "missing-tool":
if (!item.tool || typeof item.tool !== "string") {
errors.push(`Line ${i + 1}: missing_tool requires a 'tool' string field`);
continue;
}
if (!item.reason || typeof item.reason !== "string") {
errors.push(`Line ${i + 1}: missing_tool requires a 'reason' string field`);
continue;
}
item.tool = sanitizeContent(item.tool);
item.reason = sanitizeContent(item.reason);
if (item.alternatives !== undefined) {
if (typeof item.alternatives !== "string") {
errors.push(`Line ${i + 1}: missing-tool 'alternatives' must be a string`);
continue;
}
item.alternatives = sanitizeContent(item.alternatives);
}
break;
case "upload-asset":
if (!item.path || typeof item.path !== "string") {
errors.push(`Line ${i + 1}: upload_asset requires a 'path' string field`);
continue;
}
break;
case "create-code-scanning-alert":
if (!item.file || typeof item.file !== "string") {
errors.push(`Line ${i + 1}: create-code-scanning-alert requires a 'file' field (string)`);
continue;
}
const alertLineValidation = validatePositiveInteger(item.line, "create-code-scanning-alert 'line'", i + 1);
if (!alertLineValidation.isValid) {
if (alertLineValidation.error) {
errors.push(alertLineValidation.error);
}
continue;
}
if (!item.severity || typeof item.severity !== "string") {
errors.push(`Line ${i + 1}: create-code-scanning-alert requires a 'severity' field (string)`);
continue;
}
if (!item.message || typeof item.message !== "string") {
errors.push(`Line ${i + 1}: create-code-scanning-alert requires a 'message' field (string)`);
continue;
}
const allowedSeverities = ["error", "warning", "info", "note"];
if (!allowedSeverities.includes(item.severity.toLowerCase())) {
errors.push(
`Line ${i + 1}: create-code-scanning-alert 'severity' must be one of: ${allowedSeverities.join(", ")}, got ${item.severity.toLowerCase()}`
);
continue;
}
const columnValidation = validateOptionalPositiveInteger(item.column, "create-code-scanning-alert 'column'", i + 1);
if (!columnValidation.isValid) {
if (columnValidation.error) errors.push(columnValidation.error);
continue;
}
if (item.ruleIdSuffix !== undefined) {
if (typeof item.ruleIdSuffix !== "string") {
errors.push(`Line ${i + 1}: create-code-scanning-alert 'ruleIdSuffix' must be a string`);
continue;
}
if (!/^[a-zA-Z0-9_-]+$/.test(item.ruleIdSuffix.trim())) {
errors.push(
`Line ${i + 1}: create-code-scanning-alert 'ruleIdSuffix' must contain only alphanumeric characters, hyphens, and underscores`
);
continue;
}
}
item.severity = item.severity.toLowerCase();
item.file = sanitizeContent(item.file);
item.severity = sanitizeContent(item.severity);
item.message = sanitizeContent(item.message);
if (item.ruleIdSuffix) {
item.ruleIdSuffix = sanitizeContent(item.ruleIdSuffix);
}
break;
default:
const jobOutputType = expectedOutputTypes[itemType];
if (!jobOutputType) {
errors.push(`Line ${i + 1}: Unknown output type '${itemType}'`);
continue;
}
const safeJobConfig = jobOutputType;
if (safeJobConfig && safeJobConfig.inputs) {
const validation = validateItemWithSafeJobConfig(item, safeJobConfig, i + 1);
if (!validation.isValid) {
errors.push(...validation.errors);
continue;
}
Object.assign(item, validation.normalizedItem);
}
break;
}
core.info(`Line ${i + 1}: Valid ${itemType} item`);
parsedItems.push(item);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
errors.push(`Line ${i + 1}: Invalid JSON - ${errorMsg}`);
}
}
if (errors.length > 0) {
core.warning("Validation errors found:");
errors.forEach(error => core.warning(` - ${error}`));
if (parsedItems.length === 0) {
core.setFailed(errors.map(e => ` - ${e}`).join("\n"));
return;
}
}
for (const itemType of Object.keys(expectedOutputTypes)) {
const minRequired = getMinRequiredForType(itemType, expectedOutputTypes);
if (minRequired > 0) {
const actualCount = parsedItems.filter(item => item.type === itemType).length;
if (actualCount < minRequired) {
errors.push(`Too few items of type '${itemType}'. Minimum required: ${minRequired}, found: ${actualCount}.`);
}
}
}
core.info(`Successfully parsed ${parsedItems.length} valid output items`);
const validatedOutput = {
items: parsedItems,
errors: errors,
};
const agentOutputFile = "/tmp/agent_output.json";
const validatedOutputJson = JSON.stringify(validatedOutput);
try {
fs.mkdirSync("/tmp", { recursive: true });
fs.writeFileSync(agentOutputFile, validatedOutputJson, "utf8");
core.info(`Stored validated output to: ${agentOutputFile}`);
core.exportVariable("GITHUB_AW_AGENT_OUTPUT", agentOutputFile);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
core.error(`Failed to write agent output file: ${errorMsg}`);
}
core.setOutput("output", JSON.stringify(validatedOutput));
core.setOutput("raw_output", outputContent);
const outputTypes = Array.from(new Set(parsedItems.map(item => item.type)));
core.info(`output_types: ${outputTypes.join(", ")}`);
core.setOutput("output_types", outputTypes.join(","));
try {
await core.summary
.addRaw("## Processed Output\n\n")
.addRaw("```json\n")
.addRaw(JSON.stringify(validatedOutput))
.addRaw("\n```\n")
.write();
core.info("Successfully wrote processed output to step summary");
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
core.warning(`Failed to write to step summary: ${errorMsg}`);
}
}
await main();
- name: Upload sanitized agent output
if: always() && env.GITHUB_AW_AGENT_OUTPUT
uses: actions/upload-artifact@v4
with:
name: agent_output.json
path: ${{ env.GITHUB_AW_AGENT_OUTPUT }}
if-no-files-found: warn
- name: Upload engine output files
uses: actions/upload-artifact@v4
with:
name: agent_outputs
path: |
/tmp/.copilot/logs/
if-no-files-found: ignore
- name: Upload MCP logs
if: always()
uses: actions/upload-artifact@v4
with:
name: mcp-logs
path: /tmp/mcp-logs/
if-no-files-found: ignore
- name: Parse agent logs for step summary
if: always()
uses: actions/github-script@v8
env:
GITHUB_AW_AGENT_OUTPUT: /tmp/agent-stdio.log
with:
script: |
function main() {
const fs = require("fs");
try {
const logFile = process.env.AGENT_LOG_FILE;
if (!logFile) {
console.log("No agent log file specified");
return;
}
if (!fs.existsSync(logFile)) {
console.log(`Log file not found: ${logFile}`);
return;
}
const content = fs.readFileSync(logFile, "utf8");
const parsedLog = parseCopilotLog(content);
if (parsedLog) {
core.summary.addRaw(parsedLog).write();
console.log("Copilot log parsed successfully");
} else {
console.log("Failed to parse Copilot log");
}
} catch (error) {
core.setFailed(error.message);
}
}
function parseCopilotLog(logContent) {
try {
const lines = logContent.split("\n");
let markdown = "## 🤖 GitHub Copilot CLI Execution\n\n";
let hasOutput = false;
let inCodeBlock = false;
let currentCodeBlock = "";
let currentLanguage = "";
for (const line of lines) {
if (line.trim().startsWith("```")) {
if (!inCodeBlock) {
inCodeBlock = true;
currentLanguage = line.trim().substring(3);
currentCodeBlock = "";
} else {
inCodeBlock = false;
if (currentCodeBlock.trim()) {
markdown += `\`\`\`${currentLanguage}\n${currentCodeBlock}\`\`\`\n\n`;
hasOutput = true;
}
currentCodeBlock = "";
currentLanguage = "";
}
continue;
}
if (inCodeBlock) {
currentCodeBlock += line + "\n";
continue;
}
if (line.includes("copilot -p") || line.includes("github copilot")) {
markdown += `**Command:** \`${line.trim()}\`\n\n`;
hasOutput = true;
}
if (line.includes("Suggestion:") || line.includes("Response:")) {
markdown += `**${line.trim()}**\n\n`;
hasOutput = true;
}
if (line.toLowerCase().includes("error:")) {
markdown += `❌ **Error:** ${line.trim()}\n\n`;
hasOutput = true;
} else if (line.toLowerCase().includes("warning:")) {
markdown += `⚠️ **Warning:** ${line.trim()}\n\n`;
hasOutput = true;
}
const trimmedLine = line.trim();
if (
trimmedLine &&
!trimmedLine.startsWith("$") &&
!trimmedLine.startsWith("#") &&
!trimmedLine.match(/^\d{4}-\d{2}-\d{2}/) &&
trimmedLine.length > 10
) {
if (
trimmedLine.includes("copilot") ||
trimmedLine.includes("suggestion") ||
trimmedLine.includes("generate") ||
trimmedLine.includes("explain")
) {
markdown += `${trimmedLine}\n\n`;
hasOutput = true;
}
}
}
if (inCodeBlock && currentCodeBlock.trim()) {
markdown += `\`\`\`${currentLanguage}\n${currentCodeBlock}\`\`\`\n\n`;
hasOutput = true;
}
if (!hasOutput) {
markdown += "*No significant output captured from Copilot CLI execution.*\n";
}
return markdown;
} catch (error) {
console.error("Error parsing Copilot log:", error);
return `## 🤖 GitHub Copilot CLI Execution\n\n*Error parsing log: ${error.message}*\n`;
}
}
main();
- name: Upload Agent Stdio
if: always()
uses: actions/upload-artifact@v4
with:
name: agent-stdio.log
path: /tmp/agent-stdio.log
if-no-files-found: warn
- name: Validate agent logs for errors
if: always()
uses: actions/github-script@v8
env:
GITHUB_AW_AGENT_OUTPUT: /tmp/agent-stdio.log
GITHUB_AW_ERROR_PATTERNS: "[{\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(ERROR)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped ERROR messages\"},{\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(WARN|WARNING)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped WARNING messages\"},{\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(CRITICAL|ERROR):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed critical/error messages with timestamp\"},{\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(WARNING):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed warning messages with timestamp\"},{\"pattern\":\"(Error):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic error messages from Copilot CLI or Node.js\"},{\"pattern\":\"npm ERR!\\\\s+(.+)\",\"level_group\":0,\"message_group\":1,\"description\":\"NPM error messages during Copilot CLI installation or execution\"},{\"pattern\":\"(Warning):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic warning messages from Copilot CLI\"},{\"pattern\":\"(Fatal error):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Fatal error messages from Copilot CLI\"},{\"pattern\":\"copilot:\\\\s+(error):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Copilot CLI command-level error messages\"},{\"pattern\":\"access denied.*only authorized.*can trigger.*workflow\",\"level_group\":0,\"message_group\":0,\"description\":\"Permission denied - workflow access restriction\"},{\"pattern\":\"access denied.*user.*not authorized\",\"level_group\":0,\"message_group\":0,\"description\":\"Permission denied - user not authorized\"},{\"pattern\":\"repository permission check failed\",\"level_group\":0,\"message_group\":0,\"description\":\"Repository permission check failure\"},{\"pattern\":\"configuration error.*required permissions not specified\",\"level_group\":0,\"message_group\":0,\"description\":\"Configuration error - missing permissions\"},{\"pattern\":\"permission.*denied\",\"level_group\":0,\"message_group\":0,\"description\":\"Generic permission denied error\"},{\"pattern\":\"unauthorized\",\"level_group\":0,\"message_group\":0,\"description\":\"Unauthorized access error\"},{\"pattern\":\"forbidden\",\"level_group\":0,\"message_group\":0,\"description\":\"Forbidden access error\"},{\"pattern\":\"access.*restricted\",\"level_group\":0,\"message_group\":0,\"description\":\"Access restricted error\"},{\"pattern\":\"insufficient.*permission\",\"level_group\":0,\"message_group\":0,\"description\":\"Insufficient permissions error\"},{\"pattern\":\"authentication failed\",\"level_group\":0,\"message_group\":0,\"description\":\"Authentication failure with Copilot CLI\"},{\"pattern\":\"token.*invalid\",\"level_group\":0,\"message_group\":0,\"description\":\"Invalid token error with Copilot CLI\"},{\"pattern\":\"not authorized.*copilot\",\"level_group\":0,\"message_group\":0,\"description\":\"Not authorized for Copilot CLI access\"},{\"pattern\":\"command not found:\\\\s*(.+)\",\"level_group\":0,\"message_group\":1,\"description\":\"Shell command not found error\"},{\"pattern\":\"(.+):\\\\s*command not found\",\"level_group\":0,\"message_group\":1,\"description\":\"Shell command not found error (alternate format)\"},{\"pattern\":\"sh:\\\\s*\\\\d+:\\\\s*(.+):\\\\s*not found\",\"level_group\":0,\"message_group\":1,\"description\":\"Shell command not found error (sh format)\"},{\"pattern\":\"bash:\\\\s*(.+):\\\\s*command not found\",\"level_group\":0,\"message_group\":1,\"description\":\"Bash command not found error\"},{\"pattern\":\"permission denied and could not request permission\",\"level_group\":0,\"message_group\":0,\"description\":\"Copilot CLI permission denied error\"},{\"pattern\":\"✗\\\\s+(.+)\",\"level_group\":0,\"message_group\":1,\"description\":\"Copilot CLI failed command indicator\"},{\"pattern\":\"Error:\\\\s*Cannot find module\\\\s*'(.+)'\",\"level_group\":0,\"message_group\":1,\"description\":\"Node.js module not found error\"},{\"pattern\":\"sh:\\\\s*\\\\d+:\\\\s*(.+):\\\\s*Permission denied\",\"level_group\":0,\"message_group\":1,\"description\":\"Shell permission denied error\"}]"
with:
script: |
function main() {
const fs = require("fs");
try {
const logFile = process.env.GITHUB_AW_AGENT_OUTPUT;
if (!logFile) {
throw new Error("GITHUB_AW_AGENT_OUTPUT environment variable is required");
}
if (!fs.existsSync(logFile)) {
throw new Error(`Log file not found: ${logFile}`);
}
const patterns = getErrorPatternsFromEnv();
if (patterns.length === 0) {
throw new Error("GITHUB_AW_ERROR_PATTERNS environment variable is required and must contain at least one pattern");
}
const content = fs.readFileSync(logFile, "utf8");
const hasErrors = validateErrors(content, patterns);
if (hasErrors) {
core.setFailed("Errors detected in agent logs - failing workflow step");
} else {
core.info("Error validation completed successfully");
}
} catch (error) {
console.debug(error);
core.setFailed(`Error validating log: ${error instanceof Error ? error.message : String(error)}`);
}
}
function getErrorPatternsFromEnv() {
const patternsEnv = process.env.GITHUB_AW_ERROR_PATTERNS;
if (!patternsEnv) {
throw new Error("GITHUB_AW_ERROR_PATTERNS environment variable is required");
}
try {
const patterns = JSON.parse(patternsEnv);
if (!Array.isArray(patterns)) {
throw new Error("GITHUB_AW_ERROR_PATTERNS must be a JSON array");
}
return patterns;
} catch (e) {
throw new Error(`Failed to parse GITHUB_AW_ERROR_PATTERNS as JSON: ${e instanceof Error ? e.message : String(e)}`);
}
}
function validateErrors(logContent, patterns) {
const lines = logContent.split("\n");
let hasErrors = false;
for (const pattern of patterns) {
let regex;
try {
regex = new RegExp(pattern.pattern, "g");
} catch (e) {
core.error(`invalid error regex pattern: ${pattern.pattern}`);
continue;
}
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
const line = lines[lineIndex];
let match;
while ((match = regex.exec(line)) !== null) {
const level = extractLevel(match, pattern);
const message = extractMessage(match, pattern, line);
const errorMessage = `Line ${lineIndex + 1}: ${message} (Pattern: ${pattern.description || "Unknown pattern"}, Raw log: ${truncateString(line.trim(), 120)})`;
if (level.toLowerCase() === "error") {
core.error(errorMessage);
hasErrors = true;
} else {
core.warning(errorMessage);
}
}
}
}
return hasErrors;
}
function extractLevel(match, pattern) {
if (pattern.level_group && pattern.level_group > 0 && match[pattern.level_group]) {
return match[pattern.level_group];
}
const fullMatch = match[0];
if (fullMatch.toLowerCase().includes("error")) {
return "error";
} else if (fullMatch.toLowerCase().includes("warn")) {
return "warning";
}
return "unknown";
}
function extractMessage(match, pattern, fullLine) {
if (pattern.message_group && pattern.message_group > 0 && match[pattern.message_group]) {
return match[pattern.message_group].trim();
}
return match[0] || fullLine.trim();
}
function truncateString(str, maxLength) {
if (!str) return "";
if (str.length <= maxLength) return str;
return str.substring(0, maxLength) + "...";
}
if (typeof module !== "undefined" && module.exports) {
module.exports = {
validateErrors,
extractLevel,
extractMessage,
getErrorPatternsFromEnv,
truncateString,
};
}
if (typeof module === "undefined" || require.main === module) {
main();
}
add_labels:
needs: agent
if: >
((always()) && (contains(needs.agent.outputs.output_types, 'add-labels'))) && ((github.event.issue.number) ||
(github.event.pull_request.number))
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
timeout-minutes: 10
outputs:
labels_added: ${{ steps.add_labels.outputs.labels_added }}
steps:
- name: Add Labels
id: add_labels
uses: actions/github-script@v8
env:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.agent.outputs.output }}
GITHUB_AW_LABELS_ALLOWED: ""
GITHUB_AW_LABELS_MAX_COUNT: 3
with:
script: |
function sanitizeLabelContent(content) {
if (!content || typeof content !== "string") {
return "";
}
let sanitized = content.trim();
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
sanitized = sanitized.replace(
/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
(_m, p1, p2) => `${p1}\`@${p2}\``
);
sanitized = sanitized.replace(/[<>&'"]/g, "");
return sanitized.trim();
}
async function main() {
const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT;
if (!outputContent) {
core.info("No GITHUB_AW_AGENT_OUTPUT environment variable found");
return;
}
if (outputContent.trim() === "") {
core.info("Agent output content is empty");
return;
}
core.debug(`Agent output content length: ${outputContent.length}`);
let validatedOutput;
try {
validatedOutput = JSON.parse(outputContent);
} catch (error) {
core.setFailed(`Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`);
return;
}
if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
core.warning("No valid items found in agent output");
return;
}
const labelsItem = validatedOutput.items.find(item => item.type === "add-labels");
if (!labelsItem) {
core.warning("No add-labels item found in agent output");
return;
}
core.debug(`Found add-labels item with ${labelsItem.labels.length} labels`);
if (process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true") {
let summaryContent = "## 🎭 Staged Mode: Add Labels Preview\n\n";
summaryContent += "The following labels would be added if staged mode was disabled:\n\n";
if (labelsItem.issue_number) {
summaryContent += `**Target Issue:** #${labelsItem.issue_number}\n\n`;
} else {
summaryContent += `**Target:** Current issue/PR\n\n`;
}
if (labelsItem.labels && labelsItem.labels.length > 0) {
summaryContent += `**Labels to add:** ${labelsItem.labels.join(", ")}\n\n`;
}
await core.summary.addRaw(summaryContent).write();
core.info("📝 Label addition preview written to step summary");
return;
}
const allowedLabelsEnv = process.env.GITHUB_AW_LABELS_ALLOWED?.trim();
const allowedLabels = allowedLabelsEnv
? allowedLabelsEnv
.split(",")
.map(label => label.trim())
.filter(label => label)
: undefined;
if (allowedLabels) {
core.debug(`Allowed labels: ${JSON.stringify(allowedLabels)}`);
} else {
core.debug("No label restrictions - any labels are allowed");
}
const maxCountEnv = process.env.GITHUB_AW_LABELS_MAX_COUNT;
const maxCount = maxCountEnv ? parseInt(maxCountEnv, 10) : 3;
if (isNaN(maxCount) || maxCount < 1) {
core.setFailed(`Invalid max value: ${maxCountEnv}. Must be a positive integer`);
return;
}
core.debug(`Max count: ${maxCount}`);
const labelsTarget = process.env.GITHUB_AW_LABELS_TARGET || "triggering";
core.info(`Labels target configuration: ${labelsTarget}`);
const isIssueContext = context.eventName === "issues" || context.eventName === "issue_comment";
const isPRContext =
context.eventName === "pull_request" ||
context.eventName === "pull_request_review" ||
context.eventName === "pull_request_review_comment";
if (labelsTarget === "triggering" && !isIssueContext && !isPRContext) {
core.info('Target is "triggering" but not running in issue or pull request context, skipping label addition');
return;
}
let issueNumber;
let contextType;
if (labelsTarget === "*") {
if (labelsItem.issue_number) {
issueNumber = typeof labelsItem.issue_number === "number" ? labelsItem.issue_number : parseInt(String(labelsItem.issue_number), 10);
if (isNaN(issueNumber) || issueNumber <= 0) {
core.setFailed(`Invalid issue number specified: ${labelsItem.issue_number}`);
return;
}
contextType = "issue";
} else {
core.setFailed('Target is "*" but no issue_number specified in labels item');
return;
}
} else if (labelsTarget && labelsTarget !== "triggering") {
issueNumber = parseInt(labelsTarget, 10);
if (isNaN(issueNumber) || issueNumber <= 0) {
core.setFailed(`Invalid issue number in target configuration: ${labelsTarget}`);
return;
}
contextType = "issue";
} else {
if (isIssueContext) {
if (context.payload.issue) {
issueNumber = context.payload.issue.number;
contextType = "issue";
} else {
core.setFailed("Issue context detected but no issue found in payload");
return;
}
} else if (isPRContext) {
if (context.payload.pull_request) {
issueNumber = context.payload.pull_request.number;
contextType = "pull request";
} else {
core.setFailed("Pull request context detected but no pull request found in payload");
return;
}
}
}
if (!issueNumber) {
core.setFailed("Could not determine issue or pull request number");
return;
}
const requestedLabels = labelsItem.labels || [];
core.debug(`Requested labels: ${JSON.stringify(requestedLabels)}`);
for (const label of requestedLabels) {
if (label && typeof label === "string" && label.startsWith("-")) {
core.setFailed(`Label removal is not permitted. Found line starting with '-': ${label}`);
return;
}
}
let validLabels;
if (allowedLabels) {
validLabels = requestedLabels.filter(label => allowedLabels.includes(label));
} else {
validLabels = requestedLabels;
}
let uniqueLabels = validLabels
.filter(label => label != null && label !== false && label !== 0)
.map(label => String(label).trim())
.filter(label => label)
.map(label => sanitizeLabelContent(label))
.filter(label => label)
.map(label => (label.length > 64 ? label.substring(0, 64) : label))
.filter((label, index, arr) => arr.indexOf(label) === index);
if (uniqueLabels.length > maxCount) {
core.debug(`too many labels, keep ${maxCount}`);
uniqueLabels = uniqueLabels.slice(0, maxCount);
}
if (uniqueLabels.length === 0) {
core.info("No labels to add");
core.setOutput("labels_added", "");
await core.summary
.addRaw(
`
## Label Addition
No labels were added (no valid labels found in agent output).
`
)
.write();
return;
}
core.info(`Adding ${uniqueLabels.length} labels to ${contextType} #${issueNumber}: ${JSON.stringify(uniqueLabels)}`);
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: uniqueLabels,
});
core.info(`Successfully added ${uniqueLabels.length} labels to ${contextType} #${issueNumber}`);
core.setOutput("labels_added", uniqueLabels.join("\n"));
const labelsListMarkdown = uniqueLabels.map(label => `- \`${label}\``).join("\n");
await core.summary
.addRaw(
`
## Label Addition
Successfully added ${uniqueLabels.length} label(s) to ${contextType} #${issueNumber}:
${labelsListMarkdown}
`
)
.write();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
core.error(`Failed to add labels: ${errorMessage}`);
core.setFailed(`Failed to add labels: ${errorMessage}`);
}
}
await main();
missing_tool:
needs: agent
if: (always()) && (contains(needs.agent.outputs.output_types, 'missing-tool'))
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 5
outputs:
tools_reported: ${{ steps.missing_tool.outputs.tools_reported }}
total_count: ${{ steps.missing_tool.outputs.total_count }}
steps:
- name: Record Missing Tool
id: missing_tool
uses: actions/github-script@v8
env:
GITHUB_AW_AGENT_OUTPUT: ${{ needs.agent.outputs.output }}
with:
script: |
async function main() {
const fs = require("fs");
const agentOutput = process.env.GITHUB_AW_AGENT_OUTPUT || "";
const maxReports = process.env.GITHUB_AW_MISSING_TOOL_MAX ? parseInt(process.env.GITHUB_AW_MISSING_TOOL_MAX) : null;
core.info("Processing missing-tool reports...");
core.info(`Agent output length: ${agentOutput.length}`);
if (maxReports) {
core.info(`Maximum reports allowed: ${maxReports}`);
}
const missingTools = [];
if (!agentOutput.trim()) {
core.info("No agent output to process");
core.setOutput("tools_reported", JSON.stringify(missingTools));
core.setOutput("total_count", missingTools.length.toString());
return;
}
let validatedOutput;
try {
validatedOutput = JSON.parse(agentOutput);
} catch (error) {
core.setFailed(`Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`);
return;
}
if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
core.info("No valid items found in agent output");
core.setOutput("tools_reported", JSON.stringify(missingTools));
core.setOutput("total_count", missingTools.length.toString());
return;
}
core.info(`Parsed agent output with ${validatedOutput.items.length} entries`);
for (const entry of validatedOutput.items) {
if (entry.type === "missing-tool") {
if (!entry.tool) {
core.warning(`missing-tool entry missing 'tool' field: ${JSON.stringify(entry)}`);
continue;
}
if (!entry.reason) {
core.warning(`missing-tool entry missing 'reason' field: ${JSON.stringify(entry)}`);
continue;
}
const missingTool = {
tool: entry.tool,
reason: entry.reason,
alternatives: entry.alternatives || null,
timestamp: new Date().toISOString(),
};
missingTools.push(missingTool);
core.info(`Recorded missing tool: ${missingTool.tool}`);
if (maxReports && missingTools.length >= maxReports) {
core.info(`Reached maximum number of missing tool reports (${maxReports})`);
break;
}
}
}
core.info(`Total missing tools reported: ${missingTools.length}`);
core.setOutput("tools_reported", JSON.stringify(missingTools));
core.setOutput("total_count", missingTools.length.toString());
if (missingTools.length > 0) {
core.info("Missing tools summary:");
core.summary
.addHeading("Missing Tools Report", 2)
.addRaw(`Found **${missingTools.length}** missing tool${missingTools.length > 1 ? "s" : ""} in this workflow execution.\n\n`);
missingTools.forEach((tool, index) => {
core.info(`${index + 1}. Tool: ${tool.tool}`);
core.info(` Reason: ${tool.reason}`);
if (tool.alternatives) {
core.info(` Alternatives: ${tool.alternatives}`);
}
core.info(` Reported at: ${tool.timestamp}`);
core.info("");
core.summary.addRaw(`### ${index + 1}. \`${tool.tool}\`\n\n`).addRaw(`**Reason:** ${tool.reason}\n\n`);
if (tool.alternatives) {
core.summary.addRaw(`**Alternatives:** ${tool.alternatives}\n\n`);
}
core.summary.addRaw(`**Reported at:** ${tool.timestamp}\n\n---\n\n`);
});
core.summary.write();
} else {
core.info("No missing tools reported in this workflow execution.");
core.summary.addHeading("Missing Tools Report", 2).addRaw("✅ No missing tools reported in this workflow execution.").write();
}
}
main().catch(error => {
core.error(`Error processing missing-tool reports: ${error}`);
core.setFailed(`Error processing missing-tool reports: ${error}`);
});