diff --git a/.github/actions/agent-status-comment/action.yml b/.github/actions/agent-status-comment/action.yml new file mode 100644 index 0000000000..70bea09867 --- /dev/null +++ b/.github/actions/agent-status-comment/action.yml @@ -0,0 +1,126 @@ +name: Agent Status Comment +description: >- + Post or update a status comment on an issue or PR for a fullsend agent + workflow run. Start comments include a run-specific HTML marker. On + completion, the start comment is edited in place if it is still the + last comment; otherwise a new completion comment is posted. Cancelled + runs delete their start comment to avoid stale "in progress" messages. + +inputs: + description: + description: >- + Human-readable action phrase (e.g. "Reviewing this PR"). Used as-is for + start comments; completion comments prepend "Finished". Must be a + hardcoded literal — do not pass user-controlled content. + required: true + number: + description: Issue or PR number to comment on + required: true + repo: + description: Repository full name (owner/repo) to comment on + required: true + sha: + description: Commit SHA the workflow is running against + required: true + token: + description: GitHub token with issues:write permission + required: true + status: + description: >- + Workflow job status (success/failure/cancelled). When set, updates or + posts a completion comment; when empty, posts a start comment. + required: false + default: '' + +runs: + using: composite + steps: + - name: Post status comment + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + DESCRIPTION: ${{ inputs.description }} + NUMBER: ${{ inputs.number }} + REPO: ${{ inputs.repo }} + SHA: ${{ inputs.sha }} + STATUS: ${{ inputs.status }} + run: | + set -euo pipefail + + if [[ -z "${NUMBER}" || "${NUMBER}" == "null" ]]; then + echo "::notice::No issue/PR number — skipping status comment" + exit 0 + fi + + MARKER="" + RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + SHORT_SHA="${SHA:0:7}" + TIMESTAMP=$(date -u +'%-I:%M %p UTC') + + find_start_comment() { + gh api "repos/${REPO}/issues/${NUMBER}/comments" \ + --paginate \ + --jq "[.[] | select(.body | contains(\"${MARKER}\")) | .id] | first" \ + 2>/dev/null || true + } + + if [[ -z "${STATUS}" ]]; then + # Post start comment + printf -v BODY '%s\n%s\n%s' \ + "${MARKER}" \ + "🤖 ${DESCRIPTION} · Started ${TIMESTAMP}" \ + "Commit: \`${SHORT_SHA}\` · [View workflow run →](${RUN_URL})" + gh issue comment "${NUMBER}" --repo "${REPO}" --body "${BODY}" + + elif [[ "${STATUS}" == "cancelled" ]]; then + # Cancelled (superseded) run — delete the start comment. + COMMENT_ID=$(find_start_comment) + if [[ -n "${COMMENT_ID}" && "${COMMENT_ID}" != "null" ]]; then + gh api -X DELETE "repos/${REPO}/issues/comments/${COMMENT_ID}" 2>/dev/null || true + fi + + else + # Completion — edit in place if start comment is still last, + # otherwise post a new comment. + case "${STATUS}" in + success) STATUS_LABEL="✅ Success" ;; + failure) STATUS_LABEL="❌ Failure" ;; + *) STATUS_LABEL="❓ ${STATUS}" ;; + esac + + COMMENT_ID=$(find_start_comment) + + # Extract start time from the existing comment body + START_TIME="" + if [[ -n "${COMMENT_ID}" && "${COMMENT_ID}" != "null" ]]; then + START_TIME=$(gh api "repos/${REPO}/issues/comments/${COMMENT_ID}" \ + --jq '.body' 2>/dev/null \ + | grep -oP 'Started \K[0-9]+:[0-9]+ [AP]M UTC' || true) + fi + + if [[ -n "${START_TIME}" ]]; then + TIME_PART=" · Started ${START_TIME} · Completed ${TIMESTAMP}" + else + TIME_PART=" · Completed ${TIMESTAMP}" + fi + + printf -v BODY '%s\n%s\n%s' \ + "${MARKER}" \ + "🤖 Finished ${DESCRIPTION} · ${STATUS_LABEL}${TIME_PART}" \ + "Commit: \`${SHORT_SHA}\` · [View workflow run →](${RUN_URL})" + + if [[ -n "${COMMENT_ID}" && "${COMMENT_ID}" != "null" ]]; then + # Check if start comment is the last comment on the issue/PR + LAST_COMMENT_ID=$(gh api "repos/${REPO}/issues/${NUMBER}/comments?per_page=1&direction=desc" \ + --jq '.[0].id' 2>/dev/null || true) + if [[ "${COMMENT_ID}" == "${LAST_COMMENT_ID}" ]]; then + gh api -X PATCH "repos/${REPO}/issues/comments/${COMMENT_ID}" \ + -f body="${BODY}" 2>/dev/null || true + else + gh issue comment "${NUMBER}" --repo "${REPO}" --body "${BODY}" + fi + else + # Start comment not found — post fresh + gh issue comment "${NUMBER}" --repo "${REPO}" --body "${BODY}" + fi + fi diff --git a/.github/workflows/reusable-code.yml b/.github/workflows/reusable-code.yml index 722b937691..ef66c32de7 100644 --- a/.github/workflows/reusable-code.yml +++ b/.github/workflows/reusable-code.yml @@ -118,6 +118,10 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Record target commit SHA + id: target-sha + run: echo "sha=$(git -C target-repo rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Validate inputs env: ISSUE_NUMBER: ${{ fromJSON(inputs.event_payload).issue.number }} @@ -142,6 +146,17 @@ jobs: CODE_ISSUE_NUMBER: ${{ fromJSON(inputs.event_payload).issue.number }} run: bash .github/scripts/setup-agent-env.sh + - name: Post start comment + if: steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Implementing changes and creating a PR + number: ${{ fromJSON(inputs.event_payload).issue.number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.target-sha.outputs.sha }} + token: ${{ steps.app-token.outputs.token }} + - name: Run code agent uses: fullsend-ai/fullsend@v0 env: @@ -154,3 +169,15 @@ jobs: with: agent: code version: ${{ inputs.fullsend_version }} + + - name: Post completion comment + if: always() && steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Implementing changes and creating a PR + number: ${{ fromJSON(inputs.event_payload).issue.number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.target-sha.outputs.sha }} + token: ${{ steps.app-token.outputs.token }} + status: ${{ job.status }} diff --git a/.github/workflows/reusable-fix.yml b/.github/workflows/reusable-fix.yml index 018087927b..b4a68bb8be 100644 --- a/.github/workflows/reusable-fix.yml +++ b/.github/workflows/reusable-fix.yml @@ -340,6 +340,17 @@ jobs: FIX_REPO_FULL_NAME: ${{ inputs.source_repo }} run: bash .github/scripts/setup-agent-env.sh + - name: Post start comment + if: steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Addressing review feedback on this PR + number: ${{ steps.context.outputs.pr_number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.pre-agent.outputs.head }} + token: ${{ steps.app-token.outputs.token }} + - name: Run fix agent uses: fullsend-ai/fullsend@v0 env: @@ -356,3 +367,15 @@ jobs: with: agent: fix version: ${{ inputs.fullsend_version }} + + - name: Post completion comment + if: always() && steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Addressing review feedback on this PR + number: ${{ steps.context.outputs.pr_number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.pre-agent.outputs.head }} + token: ${{ steps.app-token.outputs.token }} + status: ${{ job.status }} diff --git a/.github/workflows/reusable-retro.yml b/.github/workflows/reusable-retro.yml index 747fac7c04..9c15ff6256 100644 --- a/.github/workflows/reusable-retro.yml +++ b/.github/workflows/reusable-retro.yml @@ -116,6 +116,10 @@ jobs: fetch-depth: 1 persist-credentials: false + - name: Record target commit SHA + id: target-sha + run: echo "sha=$(git -C target-repo rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Setup GCP and prepare credentials uses: fullsend-ai/fullsend/.github/actions/setup-gcp@v0 with: @@ -131,6 +135,17 @@ jobs: RETRO_CLOUD_ML_REGION: ${{ inputs.gcp_region }} run: bash .github/scripts/setup-agent-env.sh + - name: Post start comment + if: steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Running a retrospective + number: ${{ fromJSON(inputs.event_payload).pull_request.number || fromJSON(inputs.event_payload).issue.number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.target-sha.outputs.sha }} + token: ${{ steps.app-token.outputs.token }} + - name: Run retro agent uses: fullsend-ai/fullsend@v0 env: @@ -142,3 +157,15 @@ jobs: with: agent: retro version: ${{ inputs.fullsend_version }} + + - name: Post completion comment + if: always() && steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Running a retrospective + number: ${{ fromJSON(inputs.event_payload).pull_request.number || fromJSON(inputs.event_payload).issue.number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.target-sha.outputs.sha }} + token: ${{ steps.app-token.outputs.token }} + status: ${{ job.status }} diff --git a/.github/workflows/reusable-review.yml b/.github/workflows/reusable-review.yml index e0e4337348..e583a976c4 100644 --- a/.github/workflows/reusable-review.yml +++ b/.github/workflows/reusable-review.yml @@ -117,6 +117,10 @@ jobs: fetch-depth: 1 persist-credentials: false + - name: Record target commit SHA + id: target-sha + run: echo "sha=$(git -C target-repo rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Pre-fetch prior review context id: prior-review env: @@ -144,6 +148,17 @@ jobs: REVIEW_CLOUD_ML_REGION: ${{ inputs.gcp_region }} run: bash .github/scripts/setup-agent-env.sh + - name: Post start comment + if: steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Reviewing this PR + number: ${{ fromJSON(inputs.event_payload).pull_request.number || fromJSON(inputs.event_payload).issue.number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.target-sha.outputs.sha }} + token: ${{ steps.app-token.outputs.token }} + - name: Run review agent uses: fullsend-ai/fullsend@v0 env: @@ -158,3 +173,15 @@ jobs: with: agent: review version: ${{ inputs.fullsend_version }} + + - name: Post completion comment + if: always() && steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Reviewing this PR + number: ${{ fromJSON(inputs.event_payload).pull_request.number || fromJSON(inputs.event_payload).issue.number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.target-sha.outputs.sha }} + token: ${{ steps.app-token.outputs.token }} + status: ${{ job.status }} diff --git a/.github/workflows/reusable-triage.yml b/.github/workflows/reusable-triage.yml index fbf0a0f3cd..f93d0c1c25 100644 --- a/.github/workflows/reusable-triage.yml +++ b/.github/workflows/reusable-triage.yml @@ -116,6 +116,10 @@ jobs: fetch-depth: 1 persist-credentials: false + - name: Record target commit SHA + id: target-sha + run: echo "sha=$(git -C target-repo rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Setup GCP and prepare credentials uses: fullsend-ai/fullsend/.github/actions/setup-gcp@v0 with: @@ -131,6 +135,17 @@ jobs: TRIAGE_CLOUD_ML_REGION: ${{ inputs.gcp_region }} run: bash .github/scripts/setup-agent-env.sh + - name: Post start comment + if: steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Triaging this issue + number: ${{ fromJSON(inputs.event_payload).issue.number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.target-sha.outputs.sha }} + token: ${{ steps.app-token.outputs.token }} + - name: Run triage agent uses: fullsend-ai/fullsend@v0 env: @@ -138,3 +153,15 @@ jobs: with: agent: triage version: ${{ inputs.fullsend_version }} + + - name: Post completion comment + if: always() && steps.app-token.outputs.token != '' + continue-on-error: true + uses: fullsend-ai/fullsend/.github/actions/agent-status-comment@v0 + with: + description: Triaging this issue + number: ${{ fromJSON(inputs.event_payload).issue.number }} + repo: ${{ inputs.source_repo }} + sha: ${{ steps.target-sha.outputs.sha }} + token: ${{ steps.app-token.outputs.token }} + status: ${{ job.status }} diff --git a/internal/scaffold/fullsend-repo/.github/actions/agent-status-comment/action.yml b/internal/scaffold/fullsend-repo/.github/actions/agent-status-comment/action.yml new file mode 100644 index 0000000000..70bea09867 --- /dev/null +++ b/internal/scaffold/fullsend-repo/.github/actions/agent-status-comment/action.yml @@ -0,0 +1,126 @@ +name: Agent Status Comment +description: >- + Post or update a status comment on an issue or PR for a fullsend agent + workflow run. Start comments include a run-specific HTML marker. On + completion, the start comment is edited in place if it is still the + last comment; otherwise a new completion comment is posted. Cancelled + runs delete their start comment to avoid stale "in progress" messages. + +inputs: + description: + description: >- + Human-readable action phrase (e.g. "Reviewing this PR"). Used as-is for + start comments; completion comments prepend "Finished". Must be a + hardcoded literal — do not pass user-controlled content. + required: true + number: + description: Issue or PR number to comment on + required: true + repo: + description: Repository full name (owner/repo) to comment on + required: true + sha: + description: Commit SHA the workflow is running against + required: true + token: + description: GitHub token with issues:write permission + required: true + status: + description: >- + Workflow job status (success/failure/cancelled). When set, updates or + posts a completion comment; when empty, posts a start comment. + required: false + default: '' + +runs: + using: composite + steps: + - name: Post status comment + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + DESCRIPTION: ${{ inputs.description }} + NUMBER: ${{ inputs.number }} + REPO: ${{ inputs.repo }} + SHA: ${{ inputs.sha }} + STATUS: ${{ inputs.status }} + run: | + set -euo pipefail + + if [[ -z "${NUMBER}" || "${NUMBER}" == "null" ]]; then + echo "::notice::No issue/PR number — skipping status comment" + exit 0 + fi + + MARKER="" + RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + SHORT_SHA="${SHA:0:7}" + TIMESTAMP=$(date -u +'%-I:%M %p UTC') + + find_start_comment() { + gh api "repos/${REPO}/issues/${NUMBER}/comments" \ + --paginate \ + --jq "[.[] | select(.body | contains(\"${MARKER}\")) | .id] | first" \ + 2>/dev/null || true + } + + if [[ -z "${STATUS}" ]]; then + # Post start comment + printf -v BODY '%s\n%s\n%s' \ + "${MARKER}" \ + "🤖 ${DESCRIPTION} · Started ${TIMESTAMP}" \ + "Commit: \`${SHORT_SHA}\` · [View workflow run →](${RUN_URL})" + gh issue comment "${NUMBER}" --repo "${REPO}" --body "${BODY}" + + elif [[ "${STATUS}" == "cancelled" ]]; then + # Cancelled (superseded) run — delete the start comment. + COMMENT_ID=$(find_start_comment) + if [[ -n "${COMMENT_ID}" && "${COMMENT_ID}" != "null" ]]; then + gh api -X DELETE "repos/${REPO}/issues/comments/${COMMENT_ID}" 2>/dev/null || true + fi + + else + # Completion — edit in place if start comment is still last, + # otherwise post a new comment. + case "${STATUS}" in + success) STATUS_LABEL="✅ Success" ;; + failure) STATUS_LABEL="❌ Failure" ;; + *) STATUS_LABEL="❓ ${STATUS}" ;; + esac + + COMMENT_ID=$(find_start_comment) + + # Extract start time from the existing comment body + START_TIME="" + if [[ -n "${COMMENT_ID}" && "${COMMENT_ID}" != "null" ]]; then + START_TIME=$(gh api "repos/${REPO}/issues/comments/${COMMENT_ID}" \ + --jq '.body' 2>/dev/null \ + | grep -oP 'Started \K[0-9]+:[0-9]+ [AP]M UTC' || true) + fi + + if [[ -n "${START_TIME}" ]]; then + TIME_PART=" · Started ${START_TIME} · Completed ${TIMESTAMP}" + else + TIME_PART=" · Completed ${TIMESTAMP}" + fi + + printf -v BODY '%s\n%s\n%s' \ + "${MARKER}" \ + "🤖 Finished ${DESCRIPTION} · ${STATUS_LABEL}${TIME_PART}" \ + "Commit: \`${SHORT_SHA}\` · [View workflow run →](${RUN_URL})" + + if [[ -n "${COMMENT_ID}" && "${COMMENT_ID}" != "null" ]]; then + # Check if start comment is the last comment on the issue/PR + LAST_COMMENT_ID=$(gh api "repos/${REPO}/issues/${NUMBER}/comments?per_page=1&direction=desc" \ + --jq '.[0].id' 2>/dev/null || true) + if [[ "${COMMENT_ID}" == "${LAST_COMMENT_ID}" ]]; then + gh api -X PATCH "repos/${REPO}/issues/comments/${COMMENT_ID}" \ + -f body="${BODY}" 2>/dev/null || true + else + gh issue comment "${NUMBER}" --repo "${REPO}" --body "${BODY}" + fi + else + # Start comment not found — post fresh + gh issue comment "${NUMBER}" --repo "${REPO}" --body "${BODY}" + fi + fi