mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
When not triggering via `workflow_dispatch`, it looks like the default values are simply empty. This PR creates an intermediate step to conditionally set defaults based on `github.event_name`. I'm also adding a commented-out step for installing `gh` that's required for local testing via `nektos/act`. It's not required in a 'real' runner.
286 lines
12 KiB
YAML
286 lines
12 KiB
YAML
name: AI Triage Automation
|
|
|
|
on:
|
|
issues:
|
|
types:
|
|
- labeled
|
|
workflow_dispatch:
|
|
inputs:
|
|
issue_url:
|
|
description: "GitHub Issue URL to process"
|
|
required: true
|
|
type: string
|
|
template_name:
|
|
description: "Coder template to use for workspace"
|
|
required: true
|
|
default: "traiage"
|
|
type: string
|
|
template_preset:
|
|
description: "Template preset to use"
|
|
required: true
|
|
default: "Default"
|
|
type: string
|
|
prefix:
|
|
description: "Prefix for workspace name"
|
|
required: false
|
|
default: "traiage"
|
|
type: string
|
|
cleanup:
|
|
description: "Cleanup workspace after triage."
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
|
|
jobs:
|
|
traiage:
|
|
name: Triage GitHub Issue with Claude Code
|
|
runs-on: ubuntu-latest
|
|
if: github.event.label.name == 'traiage' || github.event_name == 'workflow_dispatch'
|
|
timeout-minutes: 30
|
|
env:
|
|
CODER_URL: ${{ secrets.TRAIAGE_CODER_URL }}
|
|
CODER_SESSION_TOKEN: ${{ secrets.TRAIAGE_CODER_SESSION_TOKEN }}
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
actions: write
|
|
|
|
steps:
|
|
# This is only required for testing locally using nektos/act, so leaving commented out.
|
|
# An alternative is to use a larger or custom image.
|
|
# - name: Install Github CLI
|
|
# id: install-gh
|
|
# run: |
|
|
# (type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y)) \
|
|
# && sudo mkdir -p -m 755 /etc/apt/keyrings \
|
|
# && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
# && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
|
# && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
# && sudo mkdir -p -m 755 /etc/apt/sources.list.d \
|
|
# && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
|
# && sudo apt update \
|
|
# && sudo apt install gh -y
|
|
|
|
- name: Determine Inputs
|
|
id: determine-inputs
|
|
if: always()
|
|
env:
|
|
GITHUB_ACTOR: ${{ github.actor }}
|
|
GITHUB_EVENT_ISSUE_HTML_URL: ${{ github.event.issue.html_url }}
|
|
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
|
GITHUB_EVENT_USER_ID: ${{ github.event.sender.id }}
|
|
GITHUB_EVENT_USER_LOGIN: ${{ github.event.sender.login }}
|
|
INPUTS_CLEANUP: ${{ inputs.cleanup || false }}
|
|
INPUTS_ISSUE_URL: ${{ inputs.issue_url }}
|
|
INPUTS_TEMPLATE_NAME: ${{ inputs.template_name || 'traiage' }}
|
|
INPUTS_TEMPLATE_PRESET: ${{ inputs.template_preset || 'Default'}}
|
|
INPUTS_PREFIX: ${{ inputs.prefix || 'traiage' }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
echo "Using template name: ${INPUTS_TEMPLATE_NAME}"
|
|
echo "template_name=${INPUTS_TEMPLATE_NAME}" >> "${GITHUB_OUTPUT}"
|
|
|
|
echo "Using template preset: ${INPUTS_TEMPLATE_PRESET}"
|
|
echo "template_preset=${INPUTS_TEMPLATE_PRESET}" >> "${GITHUB_OUTPUT}"
|
|
|
|
echo "Using prefix: ${INPUTS_PREFIX}"
|
|
echo "prefix=${INPUTS_PREFIX}" >> "${GITHUB_OUTPUT}"
|
|
|
|
echo "Using cleanup: ${INPUTS_CLEANUP}"
|
|
echo "cleanup=${INPUTS_CLEANUP}" >> "${GITHUB_OUTPUT}"
|
|
|
|
# For workflow_dispatch, use the actor who triggered it
|
|
# For issues events, use the issue author.
|
|
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
|
|
if ! GITHUB_USER_ID=$(gh api "users/${GITHUB_ACTOR}" --jq '.id'); then
|
|
echo "::error::Failed to get GitHub user ID for actor ${GITHUB_ACTOR}"
|
|
exit 1
|
|
fi
|
|
echo "Using workflow_dispatch actor: ${GITHUB_ACTOR} (ID: ${GITHUB_USER_ID})"
|
|
echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}"
|
|
echo "github_username=${GITHUB_ACTOR}" >> "${GITHUB_OUTPUT}"
|
|
|
|
echo "Using issue URL: ${INPUTS_ISSUE_URL}"
|
|
echo "issue_url=${INPUTS_ISSUE_URL}" >> "${GITHUB_OUTPUT}"
|
|
|
|
exit 0
|
|
elif [[ "${GITHUB_EVENT_NAME}" == "issues" ]]; then
|
|
GITHUB_USER_ID=${GITHUB_EVENT_USER_ID}
|
|
echo "Using issue author: ${GITHUB_EVENT_USER_LOGIN} (ID: ${GITHUB_USER_ID})"
|
|
echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}"
|
|
echo "github_username=${GITHUB_EVENT_USER_LOGIN}" >> "${GITHUB_OUTPUT}"
|
|
|
|
echo "Using issue URL: ${GITHUB_EVENT_ISSUE_HTML_URL}"
|
|
echo "issue_url=${GITHUB_EVENT_ISSUE_HTML_URL}" >> "${GITHUB_OUTPUT}"
|
|
|
|
exit 0
|
|
else
|
|
echo "::error::Unsupported event type: ${GITHUB_EVENT_NAME}"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Verify organization membership
|
|
env:
|
|
GITHUB_ORG: ${{ github.repository_owner }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
GITHUB_USERNAME: ${{ steps.determine-inputs.outputs.github_username }}
|
|
GITHUB_USER_ID: ${{ steps.determine-inputs.outputs.github_user_id }}
|
|
run: |
|
|
# Check if the actor is a member of the organization
|
|
if ! gh api "orgs/${GITHUB_ORG}/members/${GITHUB_USERNAME}" --silent 2>/dev/null; then
|
|
echo "::error title=Access Denied::User ${GITHUB_USERNAME} is not a member of the ${GITHUB_ORG} organization"
|
|
echo "::error::You must be a member of the ${GITHUB_ORG} GitHub organization to run this workflow."
|
|
exit 1
|
|
fi
|
|
echo "::notice::User ${GITHUB_USERNAME} verified as member of ${GITHUB_ORG} organization"
|
|
|
|
- name: Extract context key from issue
|
|
id: extract-context
|
|
env:
|
|
ISSUE_URL: ${{ steps.determine-inputs.outputs.issue_url }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
issue_number="$(gh issue view "${ISSUE_URL}" --json number --jq '.number')"
|
|
context_key="gh-${issue_number}"
|
|
echo "context_key=${context_key}" >> "${GITHUB_OUTPUT}"
|
|
echo "CONTEXT_KEY=${context_key}" >> "${GITHUB_ENV}"
|
|
|
|
- name: Download and install Coder binary
|
|
shell: bash
|
|
env:
|
|
CODER_URL: ${{ secrets.TRAIAGE_CODER_URL }}
|
|
run: |
|
|
if [ "${{ runner.arch }}" == "ARM64" ]; then
|
|
ARCH="arm64"
|
|
else
|
|
ARCH="amd64"
|
|
fi
|
|
mkdir -p "${HOME}/.local/bin"
|
|
curl -fsSL --compressed "$CODER_URL/bin/coder-linux-${ARCH}" -o "${HOME}/.local/bin/coder"
|
|
chmod +x "${HOME}/.local/bin/coder"
|
|
export PATH="$HOME/.local/bin:$PATH"
|
|
coder version
|
|
coder whoami
|
|
echo "$HOME/.local/bin" >> "${GITHUB_PATH}"
|
|
|
|
- name: Get Coder username from GitHub actor
|
|
id: get-coder-username
|
|
env:
|
|
CODER_SESSION_TOKEN: ${{ secrets.TRAIAGE_CODER_SESSION_TOKEN }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
GITHUB_USER_ID: ${{ steps.determine-inputs.outputs.github_user_id }}
|
|
run: |
|
|
user_json=$(
|
|
coder users list --github-user-id="${GITHUB_USER_ID}" --output=json
|
|
)
|
|
coder_username=$(jq -r 'first | .username' <<< "$user_json")
|
|
[[ -z "${coder_username}" || "${coder_username}" == "null" ]] && echo "No Coder user with GitHub user ID ${GITHUB_USER_ID} found" && exit 1
|
|
echo "coder_username=${coder_username}" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
# TODO(Cian): this is a good use-case for 'recipes'
|
|
- name: Create Coder task
|
|
id: create-task
|
|
env:
|
|
CODER_USERNAME: ${{ steps.get-coder-username.outputs.coder_username }}
|
|
CONTEXT_KEY: ${{ steps.extract-context.outputs.context_key }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
ISSUE_URL: ${{ steps.determine-inputs.outputs.issue_url }}
|
|
PREFIX: ${{ steps.determine-inputs.outputs.prefix }}
|
|
RUN_ID: ${{ github.run_id }}
|
|
TEMPLATE_NAME: ${{ steps.determine-inputs.outputs.template_name }}
|
|
TEMPLATE_PARAMETERS: ${{ secrets.TRAIAGE_TEMPLATE_PARAMETERS }}
|
|
TEMPLATE_PRESET: ${{ steps.determine-inputs.outputs.template_preset }}
|
|
run: |
|
|
# Fetch issue description using `gh` CLI
|
|
issue_description=$(gh issue view "${ISSUE_URL}")
|
|
|
|
# Write a prompt to PROMPT_FILE
|
|
PROMPT=$(cat <<EOF
|
|
Analyze the below GitHub issue description, understand the root cause, and make appropriate changes to resolve the issue.
|
|
|
|
ISSUE URL: ${ISSUE_URL}
|
|
ISSUE DESCRIPTION BELOW:
|
|
|
|
${issue_description}
|
|
EOF
|
|
)
|
|
export PROMPT
|
|
|
|
export TASK_NAME="${PREFIX}-${CONTEXT_KEY}-${RUN_ID}"
|
|
echo "Creating task: $TASK_NAME"
|
|
./scripts/traiage.sh create
|
|
if [[ "${ISSUE_URL}" == "https://github.com/${GITHUB_REPOSITORY}"* ]]; then
|
|
gh issue comment "${ISSUE_URL}" --body "Task created: ${TASK_NAME}" --create-if-none --edit-last
|
|
else
|
|
echo "Skipping comment on other repo."
|
|
fi
|
|
echo "TASK_NAME=${CODER_USERNAME}/${TASK_NAME}" >> "${GITHUB_OUTPUT}"
|
|
echo "TASK_NAME=${CODER_USERNAME}/${TASK_NAME}" >> "${GITHUB_ENV}"
|
|
|
|
- name: Create and upload archive
|
|
id: create-archive
|
|
if: steps.determine-inputs.outputs.cleanup == 'true'
|
|
env:
|
|
BUCKET_PREFIX: "gs://coder-traiage-outputs/traiage"
|
|
CODER_USERNAME: ${{ steps.get-coder-username.outputs.coder_username }}
|
|
TASK_NAME: ${{ steps.create-task.outputs.TASK_NAME }}
|
|
run: |
|
|
echo "Waiting for task to complete..."
|
|
coder exp task status "${TASK_NAME}" --watch
|
|
echo "Creating archive for workspace: ${TASK_NAME}"
|
|
./scripts/traiage.sh archive
|
|
echo "archive_url=${BUCKET_PREFIX%%/}/$TASK_NAME.tar.gz" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: Generate a summary of the changes and post a comment on GitHub.
|
|
id: generate-summary
|
|
if: steps.determine-inputs.outputs.cleanup == 'true'
|
|
env:
|
|
ARCHIVE_URL: ${{ steps.create-archive.outputs.archive_url }}
|
|
BUCKET_PREFIX: "gs://coder-traiage-outputs/traiage"
|
|
CODER_USERNAME: ${{ steps.get-coder-username.outputs.coder_username }}
|
|
CONTEXT_KEY: ${{ steps.extract-context.outputs.context_key }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
ISSUE_URL: ${{ steps.determine-inputs.outputs.issue_url }}
|
|
TASK_NAME: ${{ steps.create-task.outputs.TASK_NAME }}
|
|
run: |
|
|
SUMMARY_FILE=$(mktemp)
|
|
trap 'rm -f "${SUMMARY_FILE}"' EXIT
|
|
AUTO_SUMMARY=$(./scripts/traiage.sh summary)
|
|
{
|
|
echo "## TrAIage Results"
|
|
echo "- **Issue URL:** ${ISSUE_URL}"
|
|
echo "- **Context Key:** ${CONTEXT_KEY}"
|
|
echo "- **Workspace:** ${TASK_NAME}"
|
|
echo "- **Archive URL:** ${ARCHIVE_URL}"
|
|
echo
|
|
echo "${AUTO_SUMMARY}"
|
|
echo
|
|
echo "To fetch the output to your own workspace:"
|
|
echo
|
|
echo '```bash'
|
|
echo "BUCKET_PREFIX=${BUCKET_PREFIX} TASK_NAME=${TASK_NAME} ./scripts/traiage.sh resume"
|
|
echo '```'
|
|
echo
|
|
} >> "${SUMMARY_FILE}"
|
|
|
|
if [[ "${ISSUE_URL}" == "https://github.com/${GITHUB_REPOSITORY}"* ]]; then
|
|
gh issue comment "${ISSUE_URL}" --body-file "${SUMMARY_FILE}" --create-if-none --edit-last
|
|
else
|
|
echo "Skipping comment on other repo."
|
|
fi
|
|
cat "${SUMMARY_FILE}" >> "${GITHUB_STEP_SUMMARY}"
|
|
|
|
- name: Cleanup task
|
|
if: steps.determine-inputs.outputs.cleanup == 'true' && steps.create-task.outputs.TASK_NAME != '' && steps.create-archive.outputs.archive_url != ''
|
|
run: |
|
|
echo "Cleaning up task: $TASK_NAME"
|
|
./scripts/traiage.sh delete || true
|