Skip to content

Commit f712456

Browse files
authored
Merge pull request #13 from coder/setup-mux-repo-pr
🤖 ci: add mux development scripts and PR workflow
1 parent 8187d48 commit f712456

File tree

6 files changed

+541
-0
lines changed

6 files changed

+541
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: "Setup Mux"
2+
description: "Setup Bun and install dependencies with caching"
3+
runs:
4+
using: "composite"
5+
steps:
6+
- name: Setup Bun
7+
uses: oven-sh/setup-bun@v2
8+
with:
9+
bun-version: latest
10+
11+
- name: Get Bun version
12+
id: bun-version
13+
shell: bash
14+
run: echo "version=$(bun --version)" >> $GITHUB_OUTPUT
15+
16+
- name: Cache node_modules
17+
id: cache-node-modules
18+
uses: actions/cache@v4
19+
with:
20+
path: node_modules
21+
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.bun-version.outputs.version }}-node-modules-${{ hashFiles('**/bun.lock') }}
22+
restore-keys: |
23+
${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.bun-version.outputs.version }}-node-modules-
24+
25+
- name: Cache bun install cache
26+
if: steps.cache-node-modules.outputs.cache-hit != 'true'
27+
id: cache-bun-install
28+
uses: actions/cache@v4
29+
with:
30+
path: ~/.bun/install/cache
31+
key: ${{ runner.os }}-bun-cache-${{ hashFiles('**/bun.lock') }}
32+
restore-keys: |
33+
${{ runner.os }}-bun-cache-
34+
35+
- name: Install dependencies
36+
if: steps.cache-node-modules.outputs.cache-hit != 'true'
37+
shell: bash
38+
run: bun install --frozen-lockfile

AGENTS.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,20 @@ Always run `bun typecheck` and `bun fmt` after changes to ensure that files are
3939
## Debugging
4040

4141
If the user provides a PR identifier, you should use the `gh` CLI to inspect the API so we can fix our implementation if it appears incorrect.
42+
43+
## PR + Release Workflow
44+
45+
- Reuse existing PRs; never close or recreate without instruction. Force-push updates.
46+
- After every push run:
47+
48+
```bash
49+
gh pr view <number> --json mergeable,mergeStateStatus | jq '.'
50+
./scripts/wait_pr_checks.sh <pr_number>
51+
```
52+
53+
- Generally run `wait_pr_checks` after submitting a PR to ensure CI passes.
54+
- Status decoding: `mergeable=MERGEABLE` clean; `CONFLICTING` needs resolution. `mergeStateStatus=CLEAN` ready, `BLOCKED` waiting for CI, `BEHIND` rebase, `DIRTY` conflicts.
55+
- If behind: `git fetch origin && git rebase origin/main && git push --force-with-lease`.
56+
- Never enable auto-merge or merge at all unless the user explicitly says "merge it".
57+
- PR descriptions: include only information a busy reviewer cannot infer; focus on implementation nuances or validation steps.
58+
- Title prefixes: `perf|refactor|fix|feat|ci|bench`, e.g., `🤖 fix: handle workspace rename edge cases`.

scripts/check_codex_comments.sh

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [ $# -eq 0 ]; then
5+
echo "Usage: $0 <pr_number>"
6+
exit 1
7+
fi
8+
9+
PR_NUMBER=$1
10+
BOT_LOGIN_REST="chatgpt-codex-connector[bot]"
11+
BOT_LOGIN_GRAPHQL="chatgpt-codex-connector"
12+
13+
echo "Checking for unresolved Codex comments in PR #${PR_NUMBER}..."
14+
15+
# Use GraphQL to get all comments (including minimized status)
16+
GRAPHQL_QUERY='query($owner: String!, $repo: String!, $pr: Int!) {
17+
repository(owner: $owner, name: $repo) {
18+
pullRequest(number: $pr) {
19+
comments(first: 100) {
20+
nodes {
21+
id
22+
author { login }
23+
body
24+
createdAt
25+
isMinimized
26+
}
27+
}
28+
reviewThreads(first: 100) {
29+
nodes {
30+
id
31+
isResolved
32+
comments(first: 1) {
33+
nodes {
34+
id
35+
author { login }
36+
body
37+
createdAt
38+
path
39+
line
40+
}
41+
}
42+
}
43+
}
44+
}
45+
}
46+
}'
47+
48+
REPO_INFO=$(gh repo view --json owner,name --jq '{owner: .owner.login, name: .name}')
49+
OWNER=$(echo "$REPO_INFO" | jq -r '.owner')
50+
REPO=$(echo "$REPO_INFO" | jq -r '.name')
51+
52+
RESULT=$(gh api graphql \
53+
-f query="$GRAPHQL_QUERY" \
54+
-F owner="$OWNER" \
55+
-F repo="$REPO" \
56+
-F pr="$PR_NUMBER")
57+
58+
# Filter regular comments from bot that aren't minimized, excluding:
59+
# - "Didn't find any major issues" (no issues found)
60+
# - "usage limits have been reached" (rate limit error, not a real review)
61+
REGULAR_COMMENTS=$(echo "$RESULT" | jq "[.data.repository.pullRequest.comments.nodes[] | select(.author.login == \"${BOT_LOGIN_GRAPHQL}\" and .isMinimized == false and (.body | test(\"Didn't find any major issues|usage limits have been reached\") | not))]")
62+
REGULAR_COUNT=$(echo "$REGULAR_COMMENTS" | jq 'length')
63+
64+
# Filter unresolved review threads from bot
65+
UNRESOLVED_THREADS=$(echo "$RESULT" | jq "[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and .comments.nodes[0].author.login == \"${BOT_LOGIN_GRAPHQL}\")]")
66+
UNRESOLVED_COUNT=$(echo "$UNRESOLVED_THREADS" | jq 'length')
67+
68+
TOTAL_UNRESOLVED=$((REGULAR_COUNT + UNRESOLVED_COUNT))
69+
70+
echo "Found ${REGULAR_COUNT} unminimized regular comment(s) from bot"
71+
echo "Found ${UNRESOLVED_COUNT} unresolved review thread(s) from bot"
72+
73+
if [ $TOTAL_UNRESOLVED -gt 0 ]; then
74+
echo ""
75+
echo "❌ Found ${TOTAL_UNRESOLVED} unresolved comment(s) from Codex in PR #${PR_NUMBER}"
76+
echo ""
77+
echo "Codex comments:"
78+
79+
if [ $REGULAR_COUNT -gt 0 ]; then
80+
echo "$REGULAR_COMMENTS" | jq -r '.[] | " - [\(.created_at)] \(.body[0:100] | gsub("\\n"; " "))..."'
81+
fi
82+
83+
if [ $UNRESOLVED_COUNT -gt 0 ]; then
84+
THREAD_SUMMARY=$(echo "$UNRESOLVED_THREADS" | jq '[.[] | {
85+
createdAt: .comments.nodes[0].createdAt,
86+
thread: .id,
87+
comment: .comments.nodes[0].id,
88+
path: (.comments.nodes[0].path // "comment"),
89+
line: (.comments.nodes[0].line // ""),
90+
snippet: (.comments.nodes[0].body[0:100] | gsub("\n"; " "))
91+
}]')
92+
93+
echo "$THREAD_SUMMARY" | jq -r '.[] | " - [\(.createdAt)] thread=\(.thread) comment=\(.comment) \(.path):\(.line) - \(.snippet)..."'
94+
echo ""
95+
echo "Resolve review threads with: ./scripts/resolve_pr_comment.sh <thread_id>"
96+
fi
97+
98+
echo ""
99+
echo "Please address or resolve all Codex comments before merging."
100+
exit 1
101+
else
102+
echo "✅ No unresolved Codex comments found"
103+
exit 0
104+
fi

scripts/check_pr_reviews.sh

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env bash
2+
# Check for unresolved PR review comments
3+
# Usage: ./scripts/check_pr_reviews.sh <pr_number>
4+
# Exits 0 if all resolved, 1 if unresolved comments exist
5+
6+
set -e
7+
8+
if [ -z "$1" ]; then
9+
echo "Usage: $0 <pr_number>"
10+
exit 1
11+
fi
12+
13+
PR_NUMBER="$1"
14+
15+
# Query for unresolved review threads
16+
UNRESOLVED=$(gh api graphql -f query="
17+
{
18+
repository(owner: \"coder\", name: \"mux\") {
19+
pullRequest(number: $PR_NUMBER) {
20+
reviewThreads(first: 100) {
21+
nodes {
22+
id
23+
isResolved
24+
comments(first: 1) {
25+
nodes {
26+
author { login }
27+
body
28+
diffHunk
29+
commit { oid }
30+
}
31+
}
32+
}
33+
}
34+
}
35+
}
36+
}" --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {thread_id: .id, user: .comments.nodes[0].author.login, body: .comments.nodes[0].body, diff_hunk: .comments.nodes[0].diffHunk, commit_id: .comments.nodes[0].commit.oid}')
37+
38+
if [ -n "$UNRESOLVED" ]; then
39+
echo "❌ Unresolved review comments found:"
40+
echo "$UNRESOLVED" | jq -r '" \(.user): \(.body)"'
41+
echo ""
42+
echo "To resolve a comment thread, use:"
43+
echo "$UNRESOLVED" | jq -r '" ./scripts/resolve_pr_comment.sh \(.thread_id)"'
44+
echo ""
45+
echo "View PR: https://github.com/coder/mux/pull/$PR_NUMBER"
46+
exit 1
47+
fi
48+
49+
echo "✅ All review comments resolved"
50+
exit 0

scripts/extract_pr_logs.sh

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/env bash
2+
# Extract logs from failed GitHub Actions runs for a PR
3+
# Usage: ./scripts/extract_pr_logs.sh <pr_number_or_run_id> [job_name_pattern] [--wait]
4+
#
5+
# Examples:
6+
# ./scripts/extract_pr_logs.sh 329 # Latest failed run for PR #329
7+
# ./scripts/extract_pr_logs.sh 329 Integration # Only Integration Test jobs
8+
# ./scripts/extract_pr_logs.sh 329 --wait # Wait for logs to be available
9+
# ./scripts/extract_pr_logs.sh 18640062283 # Specific run ID
10+
11+
set -euo pipefail
12+
13+
INPUT="${1:-}"
14+
JOB_PATTERN="${2:-}"
15+
WAIT_FOR_LOGS=false
16+
17+
# Parse flags
18+
if [[ "$JOB_PATTERN" == "--wait" ]]; then
19+
WAIT_FOR_LOGS=true
20+
JOB_PATTERN=""
21+
elif [[ "${3:-}" == "--wait" ]]; then
22+
WAIT_FOR_LOGS=true
23+
fi
24+
25+
if [[ -z "$INPUT" ]]; then
26+
echo "❌ Usage: $0 <pr_number_or_run_id> [job_name_pattern]" >&2
27+
echo "" >&2
28+
echo "Examples:" >&2
29+
echo " $0 329 # Latest failed run for PR #329 (RECOMMENDED)" >&2
30+
echo " $0 329 Integration # Only Integration Test jobs from PR #329" >&2
31+
echo " $0 18640062283 # Specific run ID" >&2
32+
exit 1
33+
fi
34+
35+
# Detect if input is PR number or run ID (run IDs are much longer)
36+
if [[ "$INPUT" =~ ^[0-9]{1,5}$ ]]; then
37+
PR_NUMBER="$INPUT"
38+
echo "🔍 Finding latest failed run for PR #$PR_NUMBER..." >&2
39+
40+
# Get the latest failed run for this PR
41+
RUN_ID=$(gh pr checks "$PR_NUMBER" --json name,link,state --jq '.[] | select(.state == "FAILURE") | .link' | head -1 | sed -E 's|.*/runs/([0-9]+).*|\1|' || echo "")
42+
43+
if [[ -z "$RUN_ID" ]]; then
44+
echo "❌ No failed runs found for PR #$PR_NUMBER" >&2
45+
echo "" >&2
46+
echo "Current check status:" >&2
47+
gh pr checks "$PR_NUMBER" 2>&1 || true
48+
exit 1
49+
fi
50+
51+
echo "📋 Found failed run: $RUN_ID" >&2
52+
else
53+
RUN_ID="$INPUT"
54+
echo "📋 Fetching logs for run $RUN_ID..." >&2
55+
fi
56+
57+
# Get all jobs for this run
58+
JOBS=$(gh run view "$RUN_ID" --json jobs -q '.jobs[]' 2>/dev/null)
59+
60+
if [[ -z "$JOBS" ]]; then
61+
echo "❌ No jobs found for run $RUN_ID" >&2
62+
echo "" >&2
63+
echo "Check if run ID is correct:" >&2
64+
echo " gh run list --limit 10" >&2
65+
exit 1
66+
fi
67+
68+
# Filter to failed jobs only (unless specific pattern requested)
69+
if [[ -z "$JOB_PATTERN" ]]; then
70+
FAILED_JOBS=$(echo "$JOBS" | jq -r 'select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "CANCELLED")')
71+
if [[ -n "$FAILED_JOBS" ]]; then
72+
echo "🎯 Showing only failed jobs (use job_pattern to see others)" >&2
73+
JOBS="$FAILED_JOBS"
74+
fi
75+
fi
76+
77+
# Parse jobs and filter by pattern if provided
78+
if [[ -n "$JOB_PATTERN" ]]; then
79+
MATCHING_JOBS=$(echo "$JOBS" | jq -r "select(.name | test(\"$JOB_PATTERN\"; \"i\")) | .databaseId")
80+
if [[ -z "$MATCHING_JOBS" ]]; then
81+
echo "❌ No jobs matching pattern '$JOB_PATTERN'" >&2
82+
echo "" >&2
83+
echo "Available jobs:" >&2
84+
echo "$JOBS" | jq -r '.name' >&2
85+
exit 1
86+
fi
87+
JOB_IDS="$MATCHING_JOBS"
88+
else
89+
JOB_IDS=$(echo "$JOBS" | jq -r '.databaseId')
90+
fi
91+
92+
# Map job names to local commands for reproduction
93+
suggest_local_command() {
94+
local job_name="$1"
95+
case "$job_name" in
96+
*"Static Checks"* | *"lint"* | *"typecheck"* | *"fmt"*)
97+
echo "💡 Reproduce locally: make static-check"
98+
;;
99+
*"Integration Tests"*)
100+
echo "💡 Reproduce locally: make test-integration"
101+
;;
102+
*"Test"*)
103+
echo "💡 Reproduce locally: make test"
104+
;;
105+
*"Build"*)
106+
echo "💡 Reproduce locally: make build"
107+
;;
108+
*"End-to-End"*)
109+
echo "💡 Reproduce locally: make test-e2e"
110+
;;
111+
esac
112+
}
113+
114+
# Extract and display logs for each job
115+
for JOB_ID in $JOB_IDS; do
116+
JOB_INFO=$(echo "$JOBS" | jq -r "select(.databaseId == $JOB_ID)")
117+
JOB_NAME=$(echo "$JOB_INFO" | jq -r '.name')
118+
JOB_STATUS=$(echo "$JOB_INFO" | jq -r '.conclusion // .status')
119+
120+
echo "" >&2
121+
echo "════════════════════════════════════════════════════════════" >&2
122+
echo "Job: $JOB_NAME (ID: $JOB_ID) - $JOB_STATUS" >&2
123+
echo "════════════════════════════════════════════════════════════" >&2
124+
125+
# Suggest local reproduction command
126+
suggest_local_command "$JOB_NAME" >&2
127+
echo "" >&2
128+
129+
# Fetch logs with retry logic if --wait flag is set
130+
MAX_RETRIES=3
131+
RETRY_COUNT=0
132+
133+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
134+
# Use gh api to fetch logs (works for individual completed jobs even if run is in progress)
135+
if gh api "/repos/coder/mux/actions/jobs/$JOB_ID/logs" 2>/dev/null; then
136+
break
137+
else
138+
RETRY_COUNT=$((RETRY_COUNT + 1))
139+
if [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$WAIT_FOR_LOGS" = true ]; then
140+
echo "⏳ Logs not ready yet, waiting 5 seconds... (attempt $RETRY_COUNT/$MAX_RETRIES)" >&2
141+
sleep 5
142+
else
143+
echo "⚠️ Could not fetch logs for job $JOB_ID" >&2
144+
if [ "$WAIT_FOR_LOGS" = false ]; then
145+
echo " Tip: Use --wait flag to retry if logs are still processing" >&2
146+
else
147+
echo " (logs may have expired or are still processing)" >&2
148+
fi
149+
break
150+
fi
151+
fi
152+
done
153+
done

0 commit comments

Comments
 (0)