diff --git a/.github/scripts/night-owl/prepare-jira-context.sh b/.github/scripts/night-owl/prepare-jira-context.sh new file mode 100644 index 000000000..f20e65af2 --- /dev/null +++ b/.github/scripts/night-owl/prepare-jira-context.sh @@ -0,0 +1,456 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${GH_TOKEN:?GH_TOKEN is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +: "${NIGHT_OWL_CANDIDATE_JQL:?NIGHT_OWL_CANDIDATE_JQL is required}" +: "${NIGHT_OWL_JIRA_LABEL:?NIGHT_OWL_JIRA_LABEL is required}" +: "${NIGHT_OWL_JIRA_PROJECT:?NIGHT_OWL_JIRA_PROJECT is required}" +: "${NIGHT_OWL_JIRA_SITE:?NIGHT_OWL_JIRA_SITE is required}" + +emit_output() { + printf '%s=%s\n' "$1" "$2" >>"$GITHUB_OUTPUT" +} + +emit_multiline_output() { + local name="$1" + local value="$2" + local delimiter="EOF_$(openssl rand -hex 12)" + + { + printf '%s<<%s\n' "$name" "$delimiter" + printf '%s\n' "$value" + printf '%s\n' "$delimiter" + } >>"$GITHUB_OUTPUT" +} + +normalize_collection() { + jq -c ' + if type == "array" then . + elif .issues? then .issues + elif .values? then .values + elif .results? then .results + elif .items? then .items + elif .comments? then .comments + else [] + end + ' +} + +normalize_single() { + jq -c ' + if type == "array" then .[0] + elif .issues? then .issues[0] + elif .values? then .values[0] + elif .results? then .results[0] + elif .items? then .items[0] + else . + end + ' +} + +adf_to_text() { + jq -r ' + def textify: + if . == null then "" + elif type == "string" then . + elif type == "array" then + map(textify) + | map(select(length > 0)) + | join("\n") + elif type == "object" then + ([.text? // empty, (.content? | textify)] | map(select(length > 0)) | join("\n")) + else + tostring + end; + textify | gsub("\r"; "") + ' +} + +compact_text() { + printf '%s' "$1" | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//' +} + +truncate_text() { + local text + local limit + + text="$(compact_text "$1")" + limit="${2:-400}" + + if [ "${#text}" -le "$limit" ]; then + printf '%s' "$text" + return + fi + + printf '%s…' "${text:0:$((limit - 1))}" +} + +issue_key() { + jq -r '.key // .issueKey // empty' +} + +issue_summary() { + jq -r '.fields.summary // .summary // empty' +} + +issue_status() { + jq -r '.fields.status.name // .status.name // .status // empty' +} + +issue_type() { + jq -r '.fields.issuetype.name // .issuetype.name // .issueType.name // empty' +} + +issue_project() { + jq -r '.fields.project.key // .project.key // empty' +} + +issue_parent_key() { + jq -r '.fields.parent.key // .parent.key // empty' +} + +issue_labels_csv() { + jq -r '[.fields.labels[]?, .labels[]?] | unique | join(", ")' +} + +issue_has_label() { + local label="$1" + jq -e --arg label "$label" '[.fields.labels[]?, .labels[]?] | index($label) != null' >/dev/null +} + +issue_description_text() { + local issue_json="$1" + local description_text + + description_text="$( + jq '.fields.description // .description // null' <<<"$issue_json" | adf_to_text + )" + + if [ -n "$(compact_text "$description_text")" ]; then + printf '%s\n' "$description_text" + else + printf '_None provided._\n' + fi +} + +has_open_pr_for_key() { + local key="$1" + local pattern="(^|[^A-Z0-9])${key}([^A-Z0-9]|$)" + + jq -e --arg pattern "$pattern" ' + any( + .[]?; + ([.title // "", .body // "", .headRefName // ""] | join("\n")) | test($pattern; "i") + ) + ' <<<"$OPEN_PULL_REQUESTS_JSON" >/dev/null +} + +format_comments_markdown() { + local comments_json="$1" + local result='' + local comment='' + + while IFS= read -r comment; do + local author="" + local created="" + local body="" + local body_text="" + local excerpt="" + + author="$(jq -r '.author.displayName // .author.name // "unknown author"' <<<"$comment")" + created="$(jq -r '.updated // .created // "unknown date"' <<<"$comment")" + body="$(jq '.body // .comment // .text // null' <<<"$comment")" + body_text="$(adf_to_text <<<"$body")" + excerpt="$(truncate_text "$body_text" 400)" + + if [ -z "$excerpt" ]; then + excerpt='(no comment body)' + fi + + result+="- ${author} (${created}): ${excerpt}"$'\n' + done < <(normalize_collection <<<"$comments_json" | jq -c '.[]?') + + if [ -z "$result" ]; then + printf '_No comments loaded._\n' + else + printf '%s' "$result" + fi +} + +format_links_markdown() { + local links_json="$1" + local result='' + local link='' + + while IFS= read -r link; do + local relationship="" + local linked_key="" + local linked_status="" + local linked_summary="" + local line="" + + relationship="$(jq -r '.type.name // .linkType.name // .name // "linked"' <<<"$link")" + linked_key="$(jq -r '.outwardIssue.key // .inwardIssue.key // .workItem.key // .linkedWorkItem.key // .issue.key // .key // empty' <<<"$link")" + linked_status="$(jq -r '.outwardIssue.fields.status.name // .outwardIssue.status.name // .outwardIssue.status // .inwardIssue.fields.status.name // .inwardIssue.status.name // .inwardIssue.status // .workItem.fields.status.name // .workItem.status.name // .workItem.status // empty' <<<"$link")" + linked_summary="$(jq -r '.outwardIssue.fields.summary // .outwardIssue.summary // .inwardIssue.fields.summary // .inwardIssue.summary // .workItem.fields.summary // .workItem.summary // .linkedWorkItem.fields.summary // .linkedWorkItem.summary // .issue.fields.summary // .issue.summary // empty' <<<"$link")" + + line="- ${relationship}" + if [ -n "$linked_key" ]; then + line+=" ${linked_key}" + fi + if [ -n "$linked_status" ]; then + line+=" (${linked_status})" + fi + if [ -n "$linked_summary" ]; then + line+=": ${linked_summary}" + fi + + result+="${line}"$'\n' + done < <(normalize_collection <<<"$links_json" | jq -c '.[]?') + + if [ -z "$result" ]; then + printf '_No direct issue links loaded._\n' + else + printf '%s' "$result" + fi +} + +set_starving_outputs() { + local outcome="$1" + + emit_output prep_status starving + emit_output issue_key "" + emit_output issue_url "" + emit_output issue_summary "" + emit_output parent_key "" + emit_output parent_url "" + emit_output parent_summary "" + emit_multiline_output context_markdown "" + emit_multiline_output slack_message "$outcome" +} + +search_workitems() { + if acli jira workitem search --jql "$NIGHT_OWL_CANDIDATE_JQL" --paginate --json 2>/dev/null; then + return + fi + + acli jira workitem search --jql "$NIGHT_OWL_CANDIDATE_JQL" --limit 50 --json +} + +view_workitem() { + local key="$1" + local fields="${2:-*all}" + + if acli jira workitem view "$key" --fields "$fields" --json 2>/dev/null; then + return + fi + + acli jira workitem view --key "$key" --fields "$fields" --json +} + +list_comments() { + local key="$1" + + if acli jira workitem comment list --key "$key" --order -updated --limit 10 --json 2>/dev/null; then + return + fi + + acli jira workitem comment list "$key" --order -updated --limit 10 --json +} + +list_links() { + local key="$1" + + if acli jira workitem link list --key "$key" --json 2>/dev/null; then + return + fi + + acli jira workitem link list "$key" --json +} + +OPEN_PULL_REQUESTS_JSON="$( + gh pr list \ + --repo "$GITHUB_REPOSITORY" \ + --state open \ + --limit 100 \ + --json number,title,body,headRefName,url +)" + +SEARCH_RESULTS_JSON="$(search_workitems)" + +selected_issue_json='' +selected_issue_key='' +skipped_in_flight=0 +candidate_count=0 + +while IFS= read -r candidate; do + current_key="$(issue_key <<<"$candidate")" + current_status="$(issue_status <<<"$candidate")" + + if [ -z "$current_key" ]; then + continue + fi + + candidate_count=$((candidate_count + 1)) + + case "$current_status" in + Open|TODO|"To Do") ;; + *) + continue + ;; + esac + + if has_open_pr_for_key "$current_key"; then + skipped_in_flight=$((skipped_in_flight + 1)) + continue + fi + + full_issue_json_raw="$(view_workitem "$current_key" "*all")" + full_issue_json="$(normalize_single <<<"$full_issue_json_raw")" + + if [ "$(issue_project <<<"$full_issue_json")" != "$NIGHT_OWL_JIRA_PROJECT" ]; then + continue + fi + + if ! issue_has_label "$NIGHT_OWL_JIRA_LABEL" <<<"$full_issue_json"; then + continue + fi + + case "$(issue_status <<<"$full_issue_json")" in + Open|TODO|"To Do") ;; + *) + continue + ;; + esac + + selected_issue_key="$current_key" + selected_issue_json="$full_issue_json" + break +done < <(normalize_collection <<<"$SEARCH_RESULTS_JSON" | jq -c '.[]?') + +if [ -z "$selected_issue_key" ]; then + if [ "$candidate_count" -eq 0 ]; then + set_starving_outputs "Night Owl: :sleeping: starving +Outcome: No Jira ticket matched the Night Owl query for project ${NIGHT_OWL_JIRA_PROJECT}, label ${NIGHT_OWL_JIRA_LABEL}, and statuses Open/TODO/To Do. +Action needed: Add a qualifying Jira ticket for the agent." + else + set_starving_outputs "Night Owl: :sleeping: starving +Outcome: No Jira ticket remained after skipping tickets already covered by an open pull request and revalidating the Night Owl filters. +Context: ${skipped_in_flight} matching ticket(s) were already in flight. +Action needed: Add another qualifying Jira ticket or finish the open PRs already covering the current queue." + fi + exit 0 +fi + +selected_issue_summary="$(issue_summary <<<"$selected_issue_json")" +selected_issue_status="$(issue_status <<<"$selected_issue_json")" +selected_issue_type="$(issue_type <<<"$selected_issue_json")" +selected_issue_labels="$(issue_labels_csv <<<"$selected_issue_json")" +selected_issue_url="https://${NIGHT_OWL_JIRA_SITE}/browse/${selected_issue_key}" + +parent_key="$(issue_parent_key <<<"$selected_issue_json")" +parent_json='' +parent_summary='' +parent_url='' + +if [ -n "$parent_key" ]; then + parent_json_raw="$(view_workitem "$parent_key" "*all")" + parent_json="$(normalize_single <<<"$parent_json_raw")" + parent_summary="$(issue_summary <<<"$parent_json")" + parent_url="https://${NIGHT_OWL_JIRA_SITE}/browse/${parent_key}" +fi + +comments_json='[]' +comments_note='' +if ! comments_json="$(list_comments "$selected_issue_key")"; then + comments_json='[]' + comments_note='_Comments could not be loaded by the prep job._' +fi + +links_json='[]' +links_note='' +if ! links_json="$(list_links "$selected_issue_key")"; then + links_json='[]' + links_note='_Direct issue links could not be loaded by the prep job._' +fi + +selected_issue_description="$(issue_description_text "$selected_issue_json")" +parent_description='_No parent ticket._' +if [ -n "$parent_json" ]; then + parent_description="$(issue_description_text "$parent_json")" +fi + +comments_section="$(format_comments_markdown "$comments_json")" +links_section="$(format_links_markdown "$links_json")" + +context_markdown="$( + cat < - (needs.pre_activation.outputs.activated == 'true') && (github.event_name != 'workflow_run' || github.event.workflow_run.repository.id == github.repository_id && - (!(github.event.workflow_run.repository.fork))) + needs: night_owl_prepare runs-on: ubuntu-slim permissions: actions: read @@ -116,8 +108,6 @@ jobs: with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} - trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} env: GH_AW_SETUP_WORKFLOW_NAME: "CI Failure Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/ci-failure-triage-agent.lock.yml@${{ github.ref }} @@ -137,7 +127,7 @@ jobs: GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","acli.atlassian.com","api.atlassian.com","sonarsource.atlassian.net"]' GH_AW_INFO_FIREWALL_ENABLED: "true" GH_AW_INFO_AWF_VERSION: "v0.25.55" GH_AW_INFO_AWMG_VERSION: "" @@ -211,27 +201,35 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN: ${{ needs.night_owl_prepare.outputs.context_markdown }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY: ${{ needs.night_owl_prepare.outputs.issue_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY: ${{ needs.night_owl_prepare.outputs.issue_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL: ${{ needs.night_owl_prepare.outputs.issue_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY: ${{ needs.night_owl_prepare.outputs.parent_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY: ${{ needs.night_owl_prepare.outputs.parent_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL: ${{ needs.night_owl_prepare.outputs.parent_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS: ${{ needs.night_owl_prepare.outputs.prep_status }} # poutine:ignore untrusted_checkout_exec run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_71205a221871e33c_EOF' + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' - GH_AW_PROMPT_71205a221871e33c_EOF + GH_AW_PROMPT_d3dcfaa93bd10850_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_71205a221871e33c_EOF' + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' Tools: create_pull_request, missing_tool, missing_data, slack_notify - GH_AW_PROMPT_71205a221871e33c_EOF + GH_AW_PROMPT_d3dcfaa93bd10850_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_71205a221871e33c_EOF' + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' - GH_AW_PROMPT_71205a221871e33c_EOF + GH_AW_PROMPT_d3dcfaa93bd10850_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_71205a221871e33c_EOF' + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -263,18 +261,26 @@ jobs: - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). - GH_AW_PROMPT_71205a221871e33c_EOF + GH_AW_PROMPT_d3dcfaa93bd10850_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_71205a221871e33c_EOF' + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' {{#runtime-import .github/workflows/ci-failure-triage-agent.md}} - GH_AW_PROMPT_71205a221871e33c_EOF + GH_AW_PROMPT_d3dcfaa93bd10850_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN: ${{ needs.night_owl_prepare.outputs.context_markdown }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY: ${{ needs.night_owl_prepare.outputs.issue_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY: ${{ needs.night_owl_prepare.outputs.issue_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL: ${{ needs.night_owl_prepare.outputs.issue_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY: ${{ needs.night_owl_prepare.outputs.parent_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY: ${{ needs.night_owl_prepare.outputs.parent_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL: ${{ needs.night_owl_prepare.outputs.parent_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS: ${{ needs.night_owl_prepare.outputs.prep_status }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -294,7 +300,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN: ${{ needs.night_owl_prepare.outputs.context_markdown }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY: ${{ needs.night_owl_prepare.outputs.issue_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY: ${{ needs.night_owl_prepare.outputs.issue_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL: ${{ needs.night_owl_prepare.outputs.issue_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY: ${{ needs.night_owl_prepare.outputs.parent_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY: ${{ needs.night_owl_prepare.outputs.parent_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL: ${{ needs.night_owl_prepare.outputs.parent_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS: ${{ needs.night_owl_prepare.outputs.prep_status }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -315,7 +328,14 @@ jobs: GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS } }); - name: Validate prompt placeholders @@ -347,10 +367,11 @@ jobs: retention-days: 1 agent: - needs: activation + needs: + - activation + - night_owl_prepare runs-on: ubuntu-latest permissions: - actions: read contents: read issues: read pull-requests: read @@ -482,25 +503,25 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6c0d4912a9f47461_EOF' - {"create_pull_request":{"base_branch":"${{ env.HEAD_BRANCH }}","draft":true,"labels":["ci-fix","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"allowed","title_prefix":"[ci-fix] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"report_incomplete":{},"slack-notify":{"description":"Send a CI failure triage message to Slack","inputs":{"message":{"default":null,"description":"The triage message to send","required":true,"type":"string"}}}} - GH_AW_SAFE_OUTPUTS_CONFIG_6c0d4912a9f47461_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_45b7b35a3323608a_EOF' + {"create_pull_request":{"base_branch":"${{ env.NIGHT_OWL_BASE_BRANCH }}","draft":true,"labels":["night-owl","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"fallback-to-issue","title_prefix":"[night-owl] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"report_incomplete":{},"slack-notify":{"description":"Send a night-owl message to Slack","inputs":{"message":{"default":null,"description":"The night-owl message to send","required":true,"type":"string"}}}} + GH_AW_SAFE_OUTPUTS_CONFIG_45b7b35a3323608a_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[ci-fix] \". Labels [\"ci-fix\" \"automated\"] will be automatically added. PRs will be created as drafts." + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[night-owl] \". Labels [\"night-owl\" \"automated\"] will be automatically added. PRs will be created as drafts." }, "repo_params": {}, "dynamic_tools": [ { - "description": "Send a CI failure triage message to Slack", + "description": "Send a night-owl message to Slack", "inputSchema": { "additionalProperties": false, "properties": { "message": { - "description": "The triage message to send", + "description": "The night-owl message to send", "type": "string" } }, @@ -704,7 +725,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_f5c54b8c57ac837c_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_72f50d2a7606cca8_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -714,7 +735,7 @@ jobs: "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests,actions" + "GITHUB_TOOLSETS": "context,repos,pull_requests" }, "guard-policies": { "allow-only": { @@ -745,7 +766,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_f5c54b8c57ac837c_EOF + GH_AW_MCP_CONFIG_72f50d2a7606cca8_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -779,7 +800,7 @@ jobs: export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["acli.atlassian.com","api.atlassian.com","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","sonarsource.atlassian.net","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then @@ -875,7 +896,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "acli.atlassian.com,api.atlassian.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sonarsource.atlassian.net,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: @@ -977,6 +998,7 @@ jobs: - activation - agent - detection + - night_owl_prepare - safe_outputs - slack_notify if: > @@ -1313,39 +1335,99 @@ jobs: } } - pre_activation: - runs-on: ubuntu-slim + night_owl_prepare: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + pull-requests: read + outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - matched_command: '' - setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} - setup-span-id: ${{ steps.setup.outputs.span-id }} - setup-trace-id: ${{ steps.setup.outputs.trace-id }} + context_markdown: ${{ steps.prepare.outputs.context_markdown }} + issue_key: ${{ steps.prepare.outputs.issue_key }} + issue_summary: ${{ steps.prepare.outputs.issue_summary }} + issue_url: ${{ steps.prepare.outputs.issue_url }} + parent_key: ${{ steps.prepare.outputs.parent_key }} + parent_summary: ${{ steps.prepare.outputs.parent_summary }} + parent_url: ${{ steps.prepare.outputs.parent_url }} + prep_status: ${{ steps.prepare.outputs.prep_status }} + slack_message: ${{ steps.prepare.outputs.slack_message }} steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Get Jira credentials from Vault + id: secrets + uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # c154b4a417b51cb98dd71137f49bf20e77c56820 with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} + secrets: | + development/kv/data/jira user | JIRA_USER; + development/kv/data/jira token | JIRA_TOKEN; + development/kv/data/slack token | SLACK_BOT_TOKEN; + - name: Install Atlassian CLI + run: | + case "${RUNNER_ARCH}" in + X64) platform=amd64 ;; + ARM64) platform=arm64 ;; + *) + echo "::error::Unsupported runner architecture: ${RUNNER_ARCH}" + exit 1 + ;; + esac + + curl -fsSLo "${RUNNER_TEMP}/acli" "https://acli.atlassian.com/linux/latest/acli_linux_${platform}/acli" + chmod +x "${RUNNER_TEMP}/acli" + install_dir="${RUNNER_TEMP}/night-owl-bin" + mkdir -p "${install_dir}" + mv "${RUNNER_TEMP}/acli" "${install_dir}/acli" + echo "${install_dir}" >> "${GITHUB_PATH}" + shell: bash + - name: Authenticate Atlassian CLI + run: | + echo "::add-mask::${JIRA_TOKEN}" + printf '%s\n' "${JIRA_TOKEN}" | acli jira auth login --email "${JIRA_USER}" --site "${NIGHT_OWL_JIRA_SITE}" --token env: - GH_AW_SETUP_WORKFLOW_NAME: "CI Failure Triage Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/ci-failure-triage-agent.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.52" - GH_AW_INFO_AWF_VERSION: "v0.25.55" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + JIRA_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} + JIRA_USER: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} + shell: bash + - name: Prepare Jira context for Night Owl + id: prepare + run: bash .github/scripts/night-owl/prepare-jira-context.sh env: - GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + GH_TOKEN: ${{ github.token }} + continue-on-error: true + shell: bash + - name: Post starvation message to Slack + if: ${{ steps.prepare.outcome == 'success' && steps.prepare.outputs.prep_status == 'starving' }} + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e # 70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); - await main(); + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: ${{ steps.prepare.outputs.slack_message }} + - name: Post preparation failure to Slack + if: ${{ steps.prepare.outcome == 'failure' }} + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e # 70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} + with: + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: | + Night Owl: :warning: preparation failure + Outcome: Jira preparation failed before the coding agent could start. + Action needed: Inspect the `night_owl_prepare` job logs for this workflow run and verify ACLI installation, ACLI authentication, and Jira access for `${{ env.NIGHT_OWL_JIRA_SITE }}`. + - name: Fail job when preparation fails + if: ${{ steps.prepare.outcome == 'failure' }} + run: exit 1 + shell: bash safe_outputs: needs: @@ -1449,7 +1531,7 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ env.HEAD_BRANCH }} + ref: ${{ env.NIGHT_OWL_BASE_BRANCH }} token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false fetch-depth: 0 @@ -1482,11 +1564,11 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_DOMAINS: "acli.atlassian.com,api.atlassian.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sonarsource.atlassian.net,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"slack_notify\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ env.HEAD_BRANCH }}\",\"draft\":true,\"labels\":[\"ci-fix\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"allowed\",\"title_prefix\":\"[ci-fix] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ env.NIGHT_OWL_BASE_BRANCH }}\",\"draft\":true,\"labels\":[\"night-owl\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"title_prefix\":\"[night-owl] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1550,6 +1632,6 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ runner.temp }}/gh-aw/safe-jobs/agent_output.json SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} with: - channel-id: squad-integration-on-call + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} slack-message: ${{ steps.extract.outputs.message }} diff --git a/.github/workflows/ci-failure-triage-agent.md b/.github/workflows/ci-failure-triage-agent.md index 9697825f3..1ded85a78 100644 --- a/.github/workflows/ci-failure-triage-agent.md +++ b/.github/workflows/ci-failure-triage-agent.md @@ -1,56 +1,141 @@ --- on: + schedule: + - cron: '0 0 * * *' workflow_dispatch: - inputs: - workflow_conclusion: - description: 'Conclusion of the failed workflow run' - default: 'failure' - head_branch: - description: 'Head branch of the failed workflow run' - workflow_run: - workflows: ["*"] - types: [completed] - branches: - - 'task/dam/enable-ci-failure-triager' - - 'master' - -concurrency: ci-triage-${{ github.run_id }} + +concurrency: night-owl permissions: contents: read - actions: read issues: read pull-requests: read checkout: - fetch-depth: 0 +network: + allowed: + - defaults + - acli.atlassian.com + - api.atlassian.com + - sonarsource.atlassian.net + tools: github: - toolsets: [context, repos, issues, pull_requests, actions] + toolsets: [context, repos, pull_requests] env: - WORKFLOW_CONCLUSION: ${{ inputs.workflow_conclusion || github.event.workflow_run.conclusion }} - HEAD_BRANCH: ${{ inputs.head_branch || github.event.workflow_run.head_branch }} + NIGHT_OWL_BASE_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || 'master' }} + NIGHT_OWL_CANDIDATE_JQL: 'project = CLI AND labels = "for-agent" AND statusCategory = "To Do" ORDER BY created ASC' + NIGHT_OWL_JIRA_LABEL: for-agent + NIGHT_OWL_JIRA_PROJECT: CLI + NIGHT_OWL_JIRA_SITE: sonarsource.atlassian.net + NIGHT_OWL_SLACK_CHANNEL_ID: C0B7GN16473 + +jobs: + night_owl_prepare: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: read + outputs: + prep_status: ${{ steps.prepare.outputs.prep_status }} + issue_key: ${{ steps.prepare.outputs.issue_key }} + issue_url: ${{ steps.prepare.outputs.issue_url }} + issue_summary: ${{ steps.prepare.outputs.issue_summary }} + parent_key: ${{ steps.prepare.outputs.parent_key }} + parent_url: ${{ steps.prepare.outputs.parent_url }} + parent_summary: ${{ steps.prepare.outputs.parent_summary }} + context_markdown: ${{ steps.prepare.outputs.context_markdown }} + slack_message: ${{ steps.prepare.outputs.slack_message }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Get Jira credentials from Vault + id: secrets + uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 + with: + secrets: | + development/kv/data/jira user | JIRA_USER; + development/kv/data/jira token | JIRA_TOKEN; + development/kv/data/slack token | SLACK_BOT_TOKEN; + - name: Install Atlassian CLI + shell: bash + run: | + case "${RUNNER_ARCH}" in + X64) platform=amd64 ;; + ARM64) platform=arm64 ;; + *) + echo "::error::Unsupported runner architecture: ${RUNNER_ARCH}" + exit 1 + ;; + esac + + curl -fsSLo "${RUNNER_TEMP}/acli" "https://acli.atlassian.com/linux/latest/acli_linux_${platform}/acli" + chmod +x "${RUNNER_TEMP}/acli" + install_dir="${RUNNER_TEMP}/night-owl-bin" + mkdir -p "${install_dir}" + mv "${RUNNER_TEMP}/acli" "${install_dir}/acli" + echo "${install_dir}" >> "${GITHUB_PATH}" + - name: Authenticate Atlassian CLI + shell: bash + run: | + echo "::add-mask::${JIRA_TOKEN}" + printf '%s\n' "${JIRA_TOKEN}" | acli jira auth login --email "${JIRA_USER}" --site "${NIGHT_OWL_JIRA_SITE}" --token + env: + JIRA_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} + JIRA_USER: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} + - name: Prepare Jira context for Night Owl + id: prepare + continue-on-error: true + shell: bash + run: bash .github/scripts/night-owl/prepare-jira-context.sh + env: + GH_TOKEN: ${{ github.token }} + - name: Post starvation message to Slack + if: ${{ steps.prepare.outcome == 'success' && steps.prepare.outputs.prep_status == 'starving' }} + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} + with: + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: ${{ steps.prepare.outputs.slack_message }} + - name: Post preparation failure to Slack + if: ${{ steps.prepare.outcome == 'failure' }} + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} + with: + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: | + Night Owl: :warning: preparation failure + Outcome: Jira preparation failed before the coding agent could start. + Action needed: Inspect the `night_owl_prepare` job logs for this workflow run and verify ACLI installation, ACLI authentication, and Jira access for `${{ env.NIGHT_OWL_JIRA_SITE }}`. + - name: Fail job when preparation fails + if: ${{ steps.prepare.outcome == 'failure' }} + shell: bash + run: exit 1 safe-outputs: noop: false create-pull-request: draft: true - title-prefix: "[ci-fix] " - labels: [ci-fix, automated] - protected-files: allowed - base-branch: ${{ env.HEAD_BRANCH }} + title-prefix: "[night-owl] " + labels: [night-owl, automated] + protected-files: fallback-to-issue + base-branch: ${{ env.NIGHT_OWL_BASE_BRANCH }} jobs: slack-notify: needs: safe_outputs - description: "Send a CI failure triage message to Slack" + description: "Send a night-owl message to Slack" runs-on: ubuntu-latest permissions: id-token: write inputs: message: - description: "The triage message to send" + description: "The night-owl message to send" required: true type: string steps: @@ -80,153 +165,88 @@ safe-outputs: env: SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} with: - channel-id: squad-integration-on-call + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} slack-message: ${{ steps.extract.outputs.message }} --- # CI Failure Triage Agent -## Instructions - -1. Check the `$WORKFLOW_CONCLUSION` environment variable. If it is not `failure`, stop immediately and do nothing. -2. Follow the triage skill instructions below. - ---- - -# CI Failure Triage Skill - -You are a CI failure triage agent. When a workflow run fails on `master` -(or a `task/dam/enable-ci-failure-triager` development branch), you diagnose the failure, -post a proposed remediation to Slack, and — when you have high confidence -in a mechanical fix — open a PR with the fix automatically. - ---- - -## 1. Extract context - -Use the GitHub MCP to read the workflow run that triggered this agent. Extract: - -- **Run ID** -- **Workflow name** and a direct URL to the run -- **Branch** (head branch) -- **Commit SHA** (head SHA) -- **Commit author** and **short commit message** (first line) +This workflow is temporarily repurposed to run the Night Owl implementation flow so it can be triggered from the existing workflow slot on `master`. ---- - -## 2. Loop guard - -Use the GitHub MCP to list previous runs of the `ci-failure-triage-agent` -workflow that share the same commit SHA. - -- If any previous run **completed successfully**, exit silently — a Slack - message has already been posted for this commit. -- If no successful prior run exists, continue. - ---- - -## 3. Fetch failure logs +## Instructions -Use the GitHub MCP to get the failed jobs for the run and their log output. +1. Read `AGENTS.md` and `CLAUDE.md` before changing code so you follow the repository-specific rules. +2. Jira preparation has already been done for you by the workflow using Atlassian CLI. Do not call Jira, Atlassian MCP, or `acli` yourself. +3. Use GitHub MCP plus the local checkout for repository context, implementation work, and PR checks. +4. Emit exactly one `slack_notify` safe output for every terminal outcome after Jira prep succeeds: + - `blocked`: the selected ticket or its prepared parent/linked context is missing key product or implementation decisions; + - `draft-pr-opened`: the implementation is done and a draft PR was created. +5. If the prepared Jira status below is not `ready`, stop immediately without emitting any safe outputs. The workflow already handled starvation or infrastructure notification. -Parse the output to identify: -- Which job(s) failed -- Which step(s) failed -- The exact error messages and stack traces +## Prepared Jira Inputs ---- +- Prep status: `${{ needs.night_owl_prepare.outputs.prep_status }}` +- Ticket: `${{ needs.night_owl_prepare.outputs.issue_key }}` +- Ticket URL: `${{ needs.night_owl_prepare.outputs.issue_url }}` +- Ticket summary: `${{ needs.night_owl_prepare.outputs.issue_summary }}` +- Parent: `${{ needs.night_owl_prepare.outputs.parent_key }}` +- Parent URL: `${{ needs.night_owl_prepare.outputs.parent_url }}` +- Parent summary: `${{ needs.night_owl_prepare.outputs.parent_summary }}` -## 4. Diagnose the failure +${{ needs.night_owl_prepare.outputs.context_markdown }} -Categorise the root cause: +Treat the prepared Jira content above as the source of truth. Do not invent missing Jira details and do not assume you can fetch more Jira data later. -| Category | Signals | -|----------|---------| -| **Flaky / transient** | "connection refused", "timeout", "rate limit", "502", intermittent network errors, `SIGKILL` with no code context | -| **Build error** | Compilation errors, missing imports, type errors | -| **Test failure** | Assertion errors, test assertion mismatches | -| **Lint / format** | Style violations, formatter diffs | -| **Code quality** | SonarCloud issues on changed files | -| **Dependency** | Lock file out of sync, missing package, version conflict | -| **Infrastructure** | Runner OOM, disk full, missing secret/env var | +## 1. Decide whether the ticket is actionable -Use the GitHub MCP to read the relevant source files referenced in the error -output to confirm the root cause before formulating a remediation. +Stop and emit a `slack_notify` message without creating a PR if any required behavior is missing or ambiguous, including: ---- +- acceptance criteria, +- scope boundaries, +- rollout expectations, +- API or CLI contract decisions, +- conflicting instructions between the ticket and its parent context. -## 5. Formulate a proposed remediation +The blocked Slack message must include: -Write a concrete next step — what a human (or a future write-enabled agent) -should do. Tailor it to the category: +- the Jira key and summary, +- the parent issue key and summary when you used one, +- a short explanation of why you stopped, +- the concrete missing decisions or questions that a human needs to answer. -| Category | Proposed remediation | -|----------|----------------------| -| Flaky / transient | Re-run the failed jobs | -| Build / test / lint | "Investigate `` — the error suggests ``" | -| Code quality | "Submit `sonar remediate --project --issues ,`" | -| Dependency | "Regenerate the lockfile via ``" | -| Infrastructure | "Check runner capacity / verify secret `` is set" | -| Uncertain | "Manual investigation needed — error at `` does not match a known pattern" | +## 2. Implement the ticket ---- +If the issue is actionable: -## 5b. Auto-fix (conditional) - -After diagnosing and formulating a remediation, assess your confidence in the fix: - -- **High confidence**: the fix is mechanical or deterministic — formatting, missing import, - lockfile regeneration, simple typo, straightforward test assertion update. -- **Low confidence**: the fix requires design decisions, affects multiple systems, the root - cause is uncertain, or the change is non-trivial. - -**If confidence is high:** - -1. Use the GitHub MCP to search for open PRs with `[ci-fix]` in the title that touch the - same file(s). If one already exists, skip PR creation. -2. Apply the fix. You **must** create a branch before committing — the framework - bundles your changes by branch ref, and without a `refs/heads/*` entry the - bundle will fail to apply. - ```bash - # 1. Create a branch (any ci-fix/ name works) - git checkout -b ci-fix/ - # 2. Make the code changes to the file(s) - # 3. Stage ALL changed files explicitly - git add ... - # 4. Verify there are staged changes - git status - # 5. Commit - git commit -m ": " - ``` -3. Open a PR via the `create-pull-request` safe output. In the PR description include: - - The diagnosis category and root cause summary - - A link to the failed workflow run - - The exact error that was fixed -4. Note the PR URL for inclusion in the Slack message. - -**If confidence is low:** skip PR creation entirely and proceed to Slack notification. +1. Read the relevant repository files and follow `AGENTS.md` / `CLAUDE.md`. +2. Create a branch before committing, using the Jira key in the branch name, for example `night-owl/CLI-123-short-slug`. +3. Implement the change. +4. Run the smallest set of meaningful checks that match the files you changed. If you edit TypeScript, follow the repository formatting requirements before you finish. +5. If you cannot finish a coherent implementation or cannot validate it well enough to justify a draft PR, stop and emit a blocked Slack message explaining what remains. ---- +## 3. Open a draft PR -## 6. Notify Slack +When the implementation is complete: -Emit a `slack_notify` safe output with: +1. Use the `create-pull-request` safe output. The PR must remain a draft. +2. Use a PR title that starts with the Jira key, for example `CLI-123 Implement ...`. +3. In the PR description include: + - the Jira ticket link and summary, + - the parent ticket link if you used parent context, + - a short implementation summary, + - the checks you ran, + - any open questions or follow-ups that remain. -- `message`: a structured message containing: - - **Workflow**: `{workflow-name}` — link to the run - - **Branch**: `{head-branch}` - - **Commit**: `{short-sha}` by `{author}` — `{commit-subject}` - - **Diagnosis**: `{category}` — 1–2 sentences explaining the root cause - - **Proposed remediation**: the concrete next step from §5 - - **Fix PR**: if a PR was created in §5b, include the PR URL +## 4. Notify Slack ---- +Emit a `slack_notify` message for every terminal outcome you handle in the agent. -## Rules +The message should be structured and concise: -- Post **at most one** Slack message per failed commit (loop guard in §2). -- Only create a PR when confidence in the fix is high and no existing `[ci-fix]` PR addresses the same issue. -- Only modify files when creating a high-confidence fix PR. Never open issues. -- If the failure category is uncertain, still post to Slack — include the raw error - excerpt and flag the diagnosis as "uncertain". +- `Night Owl`: blocked or draft PR opened +- `Ticket`: Jira key, link, and summary when applicable +- `Parent`: parent Jira key and summary when applicable +- `Outcome`: one or two sentences +- `PR`: include the PR URL when one was created +- `Checks`: include the checks you ran when a PR was created diff --git a/.github/workflows/night-owl.lock.yml b/.github/workflows/night-owl.lock.yml new file mode 100644 index 000000000..cb4265c6a --- /dev/null +++ b/.github/workflows/night-owl.lock.yml @@ -0,0 +1,1637 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"ee4270148b1deb7e28facefb2bc647e07b0b67bd767541d20eddf3b0faf5fcf2","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"SonarSource/vault-action-wrapper","sha":"c154b4a417b51cb98dd71137f49bf20e77c56820","version":"c154b4a417b51cb98dd71137f49bf20e77c56820"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"46d564922b082d0db93244972e8005ea6904ee5f","version":"v0.76.1"},{"repo":"slackapi/slack-github-action","sha":"70cd7be8e40a46e8b0eced40b0de447bdb42f68e","version":"70cd7be8e40a46e8b0eced40b0de447bdb42f68e"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.76.1). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# +# Frontmatter env variables: +# - NIGHT_OWL_BASE_BRANCH: (main workflow) +# - NIGHT_OWL_CANDIDATE_JQL: (main workflow) +# - NIGHT_OWL_JIRA_LABEL: (main workflow) +# - NIGHT_OWL_JIRA_PROJECT: (main workflow) +# - NIGHT_OWL_JIRA_SITE: (main workflow) +# - NIGHT_OWL_SLACK_CHANNEL_ID: (main workflow) +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # c154b4a417b51cb98dd71137f49bf20e77c56820 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 +# - slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e # 70cd7be8e40a46e8b0eced40b0de447bdb42f68e +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.55 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.55 +# - ghcr.io/github/gh-aw-mcpg:v0.3.19 +# - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + +name: "Night Owl" +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + +permissions: {} + +concurrency: night-owl + +run-name: "Night Owl" + +env: + NIGHT_OWL_BASE_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || 'master' }} + NIGHT_OWL_CANDIDATE_JQL: project = CLI AND labels = "for-agent" AND statusCategory = "To Do" ORDER BY created ASC + NIGHT_OWL_JIRA_LABEL: for-agent + NIGHT_OWL_JIRA_PROJECT: CLI + NIGHT_OWL_JIRA_SITE: sonarsource.atlassian.net + NIGHT_OWL_SLACK_CHANNEL_ID: C0B7GN16473 + +jobs: + activation: + needs: night_owl_prepare + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Night Owl" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/night-owl.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AGENT_VERSION: "1.0.52" + GH_AW_INFO_CLI_VERSION: "v0.76.1" + GH_AW_INFO_WORKFLOW_NAME: "Night Owl" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","acli.atlassian.com","api.atlassian.com","sonarsource.atlassian.net"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "night-owl.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.76.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN: ${{ needs.night_owl_prepare.outputs.context_markdown }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY: ${{ needs.night_owl_prepare.outputs.issue_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY: ${{ needs.night_owl_prepare.outputs.issue_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL: ${{ needs.night_owl_prepare.outputs.issue_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY: ${{ needs.night_owl_prepare.outputs.parent_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY: ${{ needs.night_owl_prepare.outputs.parent_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL: ${{ needs.night_owl_prepare.outputs.parent_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS: ${{ needs.night_owl_prepare.outputs.prep_status }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' + + GH_AW_PROMPT_d3dcfaa93bd10850_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' + + Tools: create_pull_request, missing_tool, missing_data, slack_notify + GH_AW_PROMPT_d3dcfaa93bd10850_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' + + GH_AW_PROMPT_d3dcfaa93bd10850_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + - **checkouts**: The following repositories have been checked out and are available in the workspace: + - `$GITHUB_WORKSPACE` → `__GH_AW_GITHUB_REPOSITORY__` (cwd) [full history, all branches available as remote-tracking refs] + - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). + + + GH_AW_PROMPT_d3dcfaa93bd10850_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_d3dcfaa93bd10850_EOF' + + {{#runtime-import .github/workflows/night-owl.md}} + GH_AW_PROMPT_d3dcfaa93bd10850_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN: ${{ needs.night_owl_prepare.outputs.context_markdown }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY: ${{ needs.night_owl_prepare.outputs.issue_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY: ${{ needs.night_owl_prepare.outputs.issue_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL: ${{ needs.night_owl_prepare.outputs.issue_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY: ${{ needs.night_owl_prepare.outputs.parent_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY: ${{ needs.night_owl_prepare.outputs.parent_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL: ${{ needs.night_owl_prepare.outputs.parent_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS: ${{ needs.night_owl_prepare.outputs.prep_status }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN: ${{ needs.night_owl_prepare.outputs.context_markdown }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY: ${{ needs.night_owl_prepare.outputs.issue_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY: ${{ needs.night_owl_prepare.outputs.issue_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL: ${{ needs.night_owl_prepare.outputs.issue_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY: ${{ needs.night_owl_prepare.outputs.parent_key }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY: ${{ needs.night_owl_prepare.outputs.parent_summary }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL: ${{ needs.night_owl_prepare.outputs.parent_url }} + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS: ${{ needs.night_owl_prepare.outputs.prep_status }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_CONTEXT_MARKDOWN, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_KEY, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_SUMMARY, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_ISSUE_URL, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_KEY, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_SUMMARY, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PARENT_URL, + GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS: process.env.GH_AW_NEEDS_NIGHT_OWL_PREPARE_OUTPUTS_PREP_STATUS + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: + - activation + - night_owl_prepare + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: nightowl + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Night Owl" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/night-owl.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 ghcr.io/github/gh-aw-mcpg:v0.3.19 ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_45b7b35a3323608a_EOF' + {"create_pull_request":{"base_branch":"${{ env.NIGHT_OWL_BASE_BRANCH }}","draft":true,"labels":["night-owl","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"fallback-to-issue","title_prefix":"[night-owl] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"report_incomplete":{},"slack-notify":{"description":"Send a night-owl message to Slack","inputs":{"message":{"default":null,"description":"The night-owl message to send","required":true,"type":"string"}}}} + GH_AW_SAFE_OUTPUTS_CONFIG_45b7b35a3323608a_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[night-owl] \". Labels [\"night-owl\" \"automated\"] will be automatically added. PRs will be created as drafts." + }, + "repo_params": {}, + "dynamic_tools": [ + { + "description": "Send a night-owl message to Slack", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "The night-owl message to send", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "slack_notify" + } + ] + } + GH_AW_VALIDATION_JSON: | + { + "create_pull_request": { + "defaultMax": 1, + "fields": { + "base": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.19' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_72f50d2a7606cca8_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_72f50d2a7606cca8_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["acli.atlassian.com","api.atlassian.com","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","sonarsource.atlassian.net","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "acli.atlassian.com,api.atlassian.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sonarsource.atlassian.net,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - night_owl_prepare + - safe_outputs + - slack_notify + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-night-owl" + cancel-in-progress: false + queue: max + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Night Owl" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/night-owl.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Night Owl" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/night-owl.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Night Owl" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/night-owl.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Night Owl" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/night-owl.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Night Owl" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/night-owl.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "night-owl" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "20" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Night Owl" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/night-owl.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Night Owl" + WORKFLOW_DESCRIPTION: "No description provided" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + night_owl_prepare: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + pull-requests: read + + outputs: + context_markdown: ${{ steps.prepare.outputs.context_markdown }} + issue_key: ${{ steps.prepare.outputs.issue_key }} + issue_summary: ${{ steps.prepare.outputs.issue_summary }} + issue_url: ${{ steps.prepare.outputs.issue_url }} + parent_key: ${{ steps.prepare.outputs.parent_key }} + parent_summary: ${{ steps.prepare.outputs.parent_summary }} + parent_url: ${{ steps.prepare.outputs.parent_url }} + prep_status: ${{ steps.prepare.outputs.prep_status }} + slack_message: ${{ steps.prepare.outputs.slack_message }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Get Jira credentials from Vault + id: secrets + uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # c154b4a417b51cb98dd71137f49bf20e77c56820 + with: + secrets: | + development/kv/data/jira user | JIRA_USER; + development/kv/data/jira token | JIRA_TOKEN; + development/kv/data/slack token | SLACK_BOT_TOKEN; + - name: Install Atlassian CLI + run: | + case "${RUNNER_ARCH}" in + X64) platform=amd64 ;; + ARM64) platform=arm64 ;; + *) + echo "::error::Unsupported runner architecture: ${RUNNER_ARCH}" + exit 1 + ;; + esac + + curl -fsSLo "${RUNNER_TEMP}/acli" "https://acli.atlassian.com/linux/latest/acli_linux_${platform}/acli" + chmod +x "${RUNNER_TEMP}/acli" + install_dir="${RUNNER_TEMP}/night-owl-bin" + mkdir -p "${install_dir}" + mv "${RUNNER_TEMP}/acli" "${install_dir}/acli" + echo "${install_dir}" >> "${GITHUB_PATH}" + shell: bash + - name: Authenticate Atlassian CLI + run: | + echo "::add-mask::${JIRA_TOKEN}" + printf '%s\n' "${JIRA_TOKEN}" | acli jira auth login --email "${JIRA_USER}" --site "${NIGHT_OWL_JIRA_SITE}" --token + env: + JIRA_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} + JIRA_USER: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} + shell: bash + - name: Prepare Jira context for Night Owl + id: prepare + run: bash .github/scripts/night-owl/prepare-jira-context.sh + env: + GH_TOKEN: ${{ github.token }} + continue-on-error: true + shell: bash + - name: Post starvation message to Slack + if: ${{ steps.prepare.outcome == 'success' && steps.prepare.outputs.prep_status == 'starving' }} + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e # 70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} + with: + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: ${{ steps.prepare.outputs.slack_message }} + - name: Post preparation failure to Slack + if: ${{ steps.prepare.outcome == 'failure' }} + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e # 70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} + with: + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: | + Night Owl: :warning: preparation failure + Outcome: Jira preparation failed before the coding agent could start. + Action needed: Inspect the `night_owl_prepare` job logs for this workflow run and verify ACLI installation, ACLI authentication, and Jira access for `${{ env.NIGHT_OWL_JIRA_SITE }}`. + - name: Fail job when preparation fails + if: ${{ steps.prepare.outcome == 'failure' }} + run: exit 1 + shell: bash + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/night-owl" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.52" + GH_AW_WORKFLOW_ID: "night-owl" + GH_AW_WORKFLOW_NAME: "Night Owl" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/night-owl.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Night Owl" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/night-owl.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Extract base branch from agent output + id: extract-base-branch + if: steps.download-agent-output.outcome == 'success' + shell: bash + run: | + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + BASE_BRANCH=$("$GH_AW_NODE" -e " + try { + const data = JSON.parse(require('fs').readFileSync('/tmp/gh-aw/agent_output.json', 'utf8')); + const item = (data.items || []).find(i => + (i.type === 'create_pull_request' || i.type === 'push_to_pull_request_branch') && + i.base_branch + ); + if (item) process.stdout.write(item.base_branch); + } catch(e) {} + " 2>/dev/null || true) + # Validate: only allow safe git branch name characters + if [[ "$BASE_BRANCH" =~ ^[a-zA-Z0-9/_.-]+$ ]] && [ ${#BASE_BRANCH} -le 255 ]; then + printf 'base-branch=%s\n' "$BASE_BRANCH" >> "$GITHUB_OUTPUT" + echo "Extracted base branch from safe output: $BASE_BRANCH" + fi + fi + - name: Checkout repository (trusted default branch for comment events) + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 0 + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.NIGHT_OWL_BASE_BRANCH }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 0 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "acli.atlassian.com,api.atlassian.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sonarsource.atlassian.net,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUT_JOBS: "{\"slack_notify\":\"\"}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ env.NIGHT_OWL_BASE_BRANCH }}\",\"draft\":true,\"labels\":[\"night-owl\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"title_prefix\":\"[night-owl] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"report_incomplete\":{}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + + slack_notify: + needs: + - agent + - detection + - safe_outputs + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'slack_notify') + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: ${{ runner.temp }}/gh-aw/safe-jobs/ + - name: Extract message from agent output + id: extract + run: | + if [ -f "$GH_AW_AGENT_OUTPUT" ]; then + MESSAGE=$(cat "$GH_AW_AGENT_OUTPUT" | jq -r '.items[] | select(.type == "slack_notify") | .message') + DELIMITER=$(openssl rand -hex 16) + echo "message<<$DELIMITER" >> "$GITHUB_OUTPUT" + echo "$MESSAGE" >> "$GITHUB_OUTPUT" + echo "$DELIMITER" >> "$GITHUB_OUTPUT" + else + echo "::error::No agent output found at $GH_AW_AGENT_OUTPUT" + exit 1 + fi + env: + GH_AW_AGENT_OUTPUT: ${{ runner.temp }}/gh-aw/safe-jobs/agent_output.json + - name: Get Slack token from Vault + id: secrets + uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # c154b4a417b51cb98dd71137f49bf20e77c56820 + env: + GH_AW_AGENT_OUTPUT: ${{ runner.temp }}/gh-aw/safe-jobs/agent_output.json + with: + secrets: | + development/kv/data/slack token | SLACK_BOT_TOKEN; + - name: Post to Slack + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e # 70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + GH_AW_AGENT_OUTPUT: ${{ runner.temp }}/gh-aw/safe-jobs/agent_output.json + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} + with: + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: ${{ steps.extract.outputs.message }} + diff --git a/.github/workflows/night-owl.md b/.github/workflows/night-owl.md new file mode 100644 index 000000000..7cb76ee09 --- /dev/null +++ b/.github/workflows/night-owl.md @@ -0,0 +1,252 @@ +--- +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +concurrency: night-owl + +permissions: + contents: read + issues: read + pull-requests: read + +checkout: + - fetch-depth: 0 + +network: + allowed: + - defaults + - acli.atlassian.com + - api.atlassian.com + - sonarsource.atlassian.net + +tools: + github: + toolsets: [context, repos, pull_requests] + +env: + NIGHT_OWL_BASE_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || 'master' }} + NIGHT_OWL_CANDIDATE_JQL: 'project = CLI AND labels = "for-agent" AND statusCategory = "To Do" ORDER BY created ASC' + NIGHT_OWL_JIRA_LABEL: for-agent + NIGHT_OWL_JIRA_PROJECT: CLI + NIGHT_OWL_JIRA_SITE: sonarsource.atlassian.net + NIGHT_OWL_SLACK_CHANNEL_ID: C0B7GN16473 + +jobs: + night_owl_prepare: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: read + outputs: + prep_status: ${{ steps.prepare.outputs.prep_status }} + issue_key: ${{ steps.prepare.outputs.issue_key }} + issue_url: ${{ steps.prepare.outputs.issue_url }} + issue_summary: ${{ steps.prepare.outputs.issue_summary }} + parent_key: ${{ steps.prepare.outputs.parent_key }} + parent_url: ${{ steps.prepare.outputs.parent_url }} + parent_summary: ${{ steps.prepare.outputs.parent_summary }} + context_markdown: ${{ steps.prepare.outputs.context_markdown }} + slack_message: ${{ steps.prepare.outputs.slack_message }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Get Jira credentials from Vault + id: secrets + uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 + with: + secrets: | + development/kv/data/jira user | JIRA_USER; + development/kv/data/jira token | JIRA_TOKEN; + development/kv/data/slack token | SLACK_BOT_TOKEN; + - name: Install Atlassian CLI + shell: bash + run: | + case "${RUNNER_ARCH}" in + X64) platform=amd64 ;; + ARM64) platform=arm64 ;; + *) + echo "::error::Unsupported runner architecture: ${RUNNER_ARCH}" + exit 1 + ;; + esac + + curl -fsSLo "${RUNNER_TEMP}/acli" "https://acli.atlassian.com/linux/latest/acli_linux_${platform}/acli" + chmod +x "${RUNNER_TEMP}/acli" + install_dir="${RUNNER_TEMP}/night-owl-bin" + mkdir -p "${install_dir}" + mv "${RUNNER_TEMP}/acli" "${install_dir}/acli" + echo "${install_dir}" >> "${GITHUB_PATH}" + - name: Authenticate Atlassian CLI + shell: bash + run: | + echo "::add-mask::${JIRA_TOKEN}" + printf '%s\n' "${JIRA_TOKEN}" | acli jira auth login --email "${JIRA_USER}" --site "${NIGHT_OWL_JIRA_SITE}" --token + env: + JIRA_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} + JIRA_USER: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} + - name: Prepare Jira context for Night Owl + id: prepare + continue-on-error: true + shell: bash + run: bash .github/scripts/night-owl/prepare-jira-context.sh + env: + GH_TOKEN: ${{ github.token }} + - name: Post starvation message to Slack + if: ${{ steps.prepare.outcome == 'success' && steps.prepare.outputs.prep_status == 'starving' }} + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} + with: + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: ${{ steps.prepare.outputs.slack_message }} + - name: Post preparation failure to Slack + if: ${{ steps.prepare.outcome == 'failure' }} + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} + with: + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: | + Night Owl: :warning: preparation failure + Outcome: Jira preparation failed before the coding agent could start. + Action needed: Inspect the `night_owl_prepare` job logs for this workflow run and verify ACLI installation, ACLI authentication, and Jira access for `${{ env.NIGHT_OWL_JIRA_SITE }}`. + - name: Fail job when preparation fails + if: ${{ steps.prepare.outcome == 'failure' }} + shell: bash + run: exit 1 + +safe-outputs: + noop: false + create-pull-request: + draft: true + title-prefix: "[night-owl] " + labels: [night-owl, automated] + protected-files: fallback-to-issue + base-branch: ${{ env.NIGHT_OWL_BASE_BRANCH }} + jobs: + slack-notify: + needs: safe_outputs + description: "Send a night-owl message to Slack" + runs-on: ubuntu-latest + permissions: + id-token: write + inputs: + message: + description: "The night-owl message to send" + required: true + type: string + steps: + - name: Extract message from agent output + id: extract + run: | + if [ -f "$GH_AW_AGENT_OUTPUT" ]; then + MESSAGE=$(cat "$GH_AW_AGENT_OUTPUT" | jq -r '.items[] | select(.type == "slack_notify") | .message') + DELIMITER=$(openssl rand -hex 16) + echo "message<<$DELIMITER" >> "$GITHUB_OUTPUT" + echo "$MESSAGE" >> "$GITHUB_OUTPUT" + echo "$DELIMITER" >> "$GITHUB_OUTPUT" + else + echo "::error::No agent output found at $GH_AW_AGENT_OUTPUT" + exit 1 + fi + env: + GH_AW_AGENT_OUTPUT: ${{ runner.temp }}/gh-aw/safe-jobs/agent_output.json + - name: Get Slack token from Vault + id: secrets + uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 + with: + secrets: | + development/kv/data/slack token | SLACK_BOT_TOKEN; + - name: Post to Slack + uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e + env: + SLACK_BOT_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} + with: + channel-id: ${{ env.NIGHT_OWL_SLACK_CHANNEL_ID }} + slack-message: ${{ steps.extract.outputs.message }} + +--- + +# Night Owl + +You are the nightly implementation agent for this repository. + +## Instructions + +1. Read `AGENTS.md` and `CLAUDE.md` before changing code so you follow the repository-specific rules. +2. Jira preparation has already been done for you by the workflow using Atlassian CLI. Do not call Jira, Atlassian MCP, or `acli` yourself. +3. Use GitHub MCP plus the local checkout for repository context, implementation work, and PR checks. +4. Emit exactly one `slack_notify` safe output for every terminal outcome after Jira prep succeeds: + - `blocked`: the selected ticket or its prepared parent/linked context is missing key product or implementation decisions; + - `draft-pr-opened`: the implementation is done and a draft PR was created. +5. If the prepared Jira status below is not `ready`, stop immediately without emitting any safe outputs. The workflow already handled starvation or infrastructure notification. + +## Prepared Jira Inputs + +- Prep status: `${{ needs.night_owl_prepare.outputs.prep_status }}` +- Ticket: `${{ needs.night_owl_prepare.outputs.issue_key }}` +- Ticket URL: `${{ needs.night_owl_prepare.outputs.issue_url }}` +- Ticket summary: `${{ needs.night_owl_prepare.outputs.issue_summary }}` +- Parent: `${{ needs.night_owl_prepare.outputs.parent_key }}` +- Parent URL: `${{ needs.night_owl_prepare.outputs.parent_url }}` +- Parent summary: `${{ needs.night_owl_prepare.outputs.parent_summary }}` + +${{ needs.night_owl_prepare.outputs.context_markdown }} + +Treat the prepared Jira content above as the source of truth. Do not invent missing Jira details and do not assume you can fetch more Jira data later. + +## 1. Decide whether the ticket is actionable + +Stop and emit a `slack_notify` message without creating a PR if any required behavior is missing or ambiguous, including: + +- acceptance criteria, +- scope boundaries, +- rollout expectations, +- API or CLI contract decisions, +- conflicting instructions between the ticket and its parent context. + +The blocked Slack message must include: + +- the Jira key and summary, +- the parent issue key and summary when you used one, +- a short explanation of why you stopped, +- the concrete missing decisions or questions that a human needs to answer. + +## 2. Implement the ticket + +If the issue is actionable: + +1. Read the relevant repository files and follow `AGENTS.md` / `CLAUDE.md`. +2. Create a branch before committing, using the Jira key in the branch name, for example `night-owl/CLI-123-short-slug`. +3. Implement the change. +4. Run the smallest set of meaningful checks that match the files you changed. If you edit TypeScript, follow the repository formatting requirements before you finish. +5. If you cannot finish a coherent implementation or cannot validate it well enough to justify a draft PR, stop and emit a blocked Slack message explaining what remains. + +## 3. Open a draft PR + +When the implementation is complete: + +1. Use the `create-pull-request` safe output. The PR must remain a draft. +2. Use a PR title that starts with the Jira key, for example `CLI-123 Implement ...`. +3. In the PR description include: + - the Jira ticket link and summary, + - the parent ticket link if you used parent context, + - a short implementation summary, + - the checks you ran, + - any open questions or follow-ups that remain. + +## 4. Notify Slack + +Emit a `slack_notify` message for every terminal outcome you handle in the agent. + +The message should be structured and concise: + +- `Night Owl`: blocked or draft PR opened +- `Ticket`: Jira key, link, and summary when applicable +- `Parent`: parent Jira key and summary when applicable +- `Outcome`: one or two sentences +- `PR`: include the PR URL when one was created +- `Checks`: include the checks you ran when a PR was created diff --git a/CLAUDE.md b/CLAUDE.md index c74eb2245..6bf6f2439 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,6 +108,12 @@ Do **not** use `bun test --coverage` directly — Bun's native LCOV reporter emi When adding, removing, or changing commands, scripts, or project structure, update `CLAUDE.md`, and `AGENTS.md` to reflect the change before finishing. +## GitHub Agentic Workflows + +- Agentic workflow source files live in `.github/workflows/*.md`. Generated `.lock.yml` files are compiler output; update them with `gh aw compile ` instead of editing them directly. +- `night-owl.md` runs nightly at `00:00 UTC`, installs Atlassian CLI in a prep job, authenticates with Vault-provided Jira bot credentials, selects the first `CLI` Jira ticket labeled `for-agent` in status `Open`, `TODO`, or `To Do`, materializes the ticket plus parent/comment/link context, posts Slack when the queue is starving or prep fails, then lets the agent work only from that prepared Jira context and open draft PRs. Scheduled runs target `master`; manual `workflow_dispatch` runs target the branch they were launched from so Night Owl testing on feature branches does not drag workflow changes into the generated PR patch. +- `ci-failure-triage-agent.md` is temporarily repurposed to mirror `night-owl.md` so the Night Owl flow can be triggered from the existing workflow slot during rollout; like `night-owl.md`, manual dispatches target the selected branch while the nightly schedule targets `master`. + ## Docs site (`docs/`) The docs site is generated from the CLI source — do not edit `commands.json`, `llms.txt`, or `sitemap.xml` by hand. This is done by automation post-release.