From 82ccdc8e0c6b1d4d8dec1d25dd98a69342c2e604 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Wed, 1 Apr 2026 13:21:03 -0300 Subject: [PATCH] refactor: remove Devin GitHub Action integrations and DEVIN_API_KEY references (#28703) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/actions/devin-session/action.yml | 124 ----- .../workflows/cubic-devin-review-trigger.yml | 32 -- .github/workflows/cubic-devin-review.yml | 245 ---------- .github/workflows/devin-conflict-resolver.yml | 430 ------------------ .github/workflows/pr.yml | 15 - .../workflows/stale-pr-devin-completion.yml | 291 ------------ .github/workflows/sync-agents-to-devin.yml | 31 -- .github/workflows/validate-agents-format.yml | 20 - scripts/devin/delete-all-devin-knowledge.ts | 131 ------ scripts/devin/export-devin-knowledge.ts | 82 ---- scripts/devin/parse-local-knowledge.ts | 203 --------- scripts/devin/sync-knowledge-to-devin.ts | 202 -------- scripts/devin/validate-local-knowledge.ts | 165 ------- 13 files changed, 1971 deletions(-) delete mode 100644 .github/actions/devin-session/action.yml delete mode 100644 .github/workflows/cubic-devin-review-trigger.yml delete mode 100644 .github/workflows/cubic-devin-review.yml delete mode 100644 .github/workflows/devin-conflict-resolver.yml delete mode 100644 .github/workflows/stale-pr-devin-completion.yml delete mode 100644 .github/workflows/sync-agents-to-devin.yml delete mode 100644 .github/workflows/validate-agents-format.yml delete mode 100644 scripts/devin/delete-all-devin-knowledge.ts delete mode 100644 scripts/devin/export-devin-knowledge.ts delete mode 100644 scripts/devin/parse-local-knowledge.ts delete mode 100644 scripts/devin/sync-knowledge-to-devin.ts delete mode 100644 scripts/devin/validate-local-knowledge.ts diff --git a/.github/actions/devin-session/action.yml b/.github/actions/devin-session/action.yml deleted file mode 100644 index 5d8066be77a365..00000000000000 --- a/.github/actions/devin-session/action.yml +++ /dev/null @@ -1,124 +0,0 @@ -name: 'Devin Session Checker' -description: 'Check for existing Devin sessions on a PR' - -inputs: - devin-api-key: - description: 'Devin API key' - required: true - github-token: - description: 'GitHub token for API calls' - required: true - pr-number: - description: 'PR number to check for existing sessions' - required: true - -outputs: - has-existing-session: - description: 'Whether an existing active session was found (true/false)' - value: ${{ steps.check-session.outputs.has-existing-session }} - session-id: - description: 'ID of the existing session (if found)' - value: ${{ steps.check-session.outputs.session-id }} - session-url: - description: 'URL of the existing session (if found)' - value: ${{ steps.check-session.outputs.session-url }} - -runs: - using: 'composite' - steps: - - name: Check for existing Devin session - id: check-session - uses: actions/github-script@v7 - env: - DEVIN_API_KEY: ${{ inputs.devin-api-key }} - PR_NUMBER: ${{ inputs.pr-number }} - with: - github-token: ${{ inputs.github-token }} - script: | - const { owner, repo } = context.repo; - const prNumber = parseInt(process.env.PR_NUMBER, 10); - const SESSION_URL_REGEX = /app\.devin\.ai\/sessions\/([a-f0-9-]+)/; - const ACTIVE_STATUSES = ['working', 'blocked', 'resumed']; - - async function checkSessionStatus(sessionId) { - const response = await fetch(`https://api.devin.ai/v1/sessions/${sessionId}`, { - headers: { - 'Authorization': `Bearer ${process.env.DEVIN_API_KEY}`, - 'Content-Type': 'application/json' - } - }); - - if (!response.ok) { - console.log(`Failed to fetch session ${sessionId}: ${response.status}`); - return null; - } - - return await response.json(); - } - - const { data: pr } = await github.rest.pulls.get({ - owner, - repo, - pull_number: prNumber - }); - - const prBody = pr.body || ''; - const prSessionMatch = prBody.match(SESSION_URL_REGEX); - - if (prSessionMatch) { - const sessionId = prSessionMatch[1]; - console.log(`PR was created by Devin session: ${sessionId}`); - const session = await checkSessionStatus(sessionId); - if (session) { - console.log(`PR creator session status: ${session.status_enum}`); - if (ACTIVE_STATUSES.includes(session.status_enum)) { - core.setOutput('has-existing-session', 'true'); - core.setOutput('session-id', sessionId); - core.setOutput('session-url', `https://app.devin.ai/sessions/${sessionId}`); - return; - } else { - console.log(`PR creator session ${sessionId} is not active, will check comments`); - } - } - } - - const devinSessionPatterns = [ - 'Devin AI is completing this stale PR', - 'Devin AI is addressing Cubic AI', - 'Devin AI is resolving merge conflicts' - ]; - - const comments = await github.rest.issues.listComments({ - owner, - repo, - issue_number: prNumber - }); - - for (const comment of comments.data.reverse()) { - const hasDevinSession = devinSessionPatterns.some(pattern => comment.body?.includes(pattern)); - if (hasDevinSession) { - const match = comment.body?.match(SESSION_URL_REGEX); - if (match) { - const sessionId = match[1]; - console.log(`Found existing Devin session from comment: ${sessionId}`); - - const session = await checkSessionStatus(sessionId); - if (session) { - if (ACTIVE_STATUSES.includes(session.status_enum)) { - console.log(`Session ${sessionId} is active (status: ${session.status_enum})`); - core.setOutput('has-existing-session', 'true'); - core.setOutput('session-id', sessionId); - core.setOutput('session-url', `https://app.devin.ai/sessions/${sessionId}`); - return; - } else { - console.log(`Session ${sessionId} is not active (status: ${session.status_enum})`); - } - } - } - } - } - - console.log('No existing active Devin session found'); - core.setOutput('has-existing-session', 'false'); - core.setOutput('session-id', ''); - core.setOutput('session-url', ''); diff --git a/.github/workflows/cubic-devin-review-trigger.yml b/.github/workflows/cubic-devin-review-trigger.yml deleted file mode 100644 index 94b989510b439e..00000000000000 --- a/.github/workflows/cubic-devin-review-trigger.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Cubic AI Review Trigger - -on: - pull_request_review: - types: [submitted] - -# This workflow triggers on pull_request_review events (including from forks) -# and saves the review context as an artifact. The main cubic-devin-review.yml -# workflow then runs via workflow_run with full permissions and secrets access. - -jobs: - save-review-context: - name: Save Review Context - if: github.event.review.user.login == 'cubic-dev-ai[bot]' - runs-on: ubuntu-latest - steps: - - name: Save review context to artifact - run: | - cat > review-context.json << 'EOF' - { - "pr_number": ${{ github.event.pull_request.number }}, - "review_body": ${{ toJSON(github.event.review.body) }}, - "repository": "${{ github.repository }}" - } - EOF - - - name: Upload review context - uses: actions/upload-artifact@v4 - with: - name: cubic-review-context - path: review-context.json - retention-days: 1 diff --git a/.github/workflows/cubic-devin-review.yml b/.github/workflows/cubic-devin-review.yml deleted file mode 100644 index 723c3249705c46..00000000000000 --- a/.github/workflows/cubic-devin-review.yml +++ /dev/null @@ -1,245 +0,0 @@ -name: Cubic feedback addressed by Devin - -on: - workflow_run: - workflows: ["Cubic AI Review Trigger"] - types: - - completed - -permissions: - contents: read - pull-requests: write - actions: read - -jobs: - trigger-devin: - name: Trigger Devin to Address Cubic AI Review - if: ${{ github.event.workflow_run.conclusion == 'success' }} - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - name: Download review context artifact - uses: actions/github-script@v7 - with: - script: | - const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - - const matchArtifact = artifacts.data.artifacts.find( - artifact => artifact.name === 'cubic-review-context' - ); - - if (!matchArtifact) { - core.setFailed('No cubic-review-context artifact found'); - return; - } - - const download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - - const fs = require('fs'); - fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/context.zip`, Buffer.from(download.data)); - - - name: Unzip artifact - run: unzip context.zip - - - name: Extract AI prompt from Cubic review - id: extract-prompt - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - const contextData = JSON.parse(fs.readFileSync('review-context.json', 'utf8')); - const reviewBody = contextData.review_body || ''; - const prNumber = contextData.pr_number; - - core.setOutput('pr-number', prNumber); - - const promptMatch = reviewBody.match(/
\s*Prompt for AI agents[^<]*<\/summary>\s*```(?:text)?\s*([\s\S]*?)```\s*<\/details>/i); - - if (!promptMatch || !promptMatch[1]) { - console.log('No AI prompt found in review body'); - console.log('Review body:', reviewBody); - core.setOutput('has-prompt', 'false'); - return; - } - - const prompt = promptMatch[1].trim(); - console.log('Extracted prompt:', prompt); - - fs.writeFileSync('/tmp/cubic-prompt.txt', prompt); - - core.setOutput('has-prompt', 'true'); - - - name: Checkout repository - if: steps.extract-prompt.outputs.has-prompt == 'true' - uses: actions/checkout@v4 - - - name: Check for existing Devin session - if: steps.extract-prompt.outputs.has-prompt == 'true' - id: check-session - uses: ./.github/actions/devin-session - with: - devin-api-key: ${{ secrets.DEVIN_API_KEY }} - github-token: ${{ secrets.GITHUB_TOKEN }} - pr-number: ${{ steps.extract-prompt.outputs.pr-number }} - - - name: Set session environment variables - if: steps.extract-prompt.outputs.has-prompt == 'true' && steps.check-session.outputs.has-existing-session == 'true' - run: | - echo "EXISTING_SESSION_ID=${{ steps.check-session.outputs.session-id }}" >> $GITHUB_ENV - echo "SESSION_URL=${{ steps.check-session.outputs.session-url }}" >> $GITHUB_ENV - - - name: Send message to existing Devin session - if: steps.extract-prompt.outputs.has-prompt == 'true' && steps.check-session.outputs.has-existing-session == 'true' - env: - DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }} - run: | - CUBIC_PROMPT=$(cat /tmp/cubic-prompt.txt) - - MESSAGE="New Cubic AI review feedback has been submitted on PR #${{ steps.extract-prompt.outputs.pr-number }}. - - IMPORTANT: Before fixing any issue, check the individual Cubic comment on the PR for that issue's confidence score. The confidence score appears as 'Fix confidence (alpha): X/10' in each comment. Note that the (alpha) may or not be there. Don't be picky about that text. But only fix issues where the confidence score is 9/10 or higher. If you cannot find a confidence score for an issue, skip it. - - Here are the issues identified by Cubic AI: - - ${CUBIC_PROMPT} - - Continue working on the same PR branch and push your fixes." - - HTTP_CODE=$(curl -s -o /tmp/devin-response.json -w "%{http_code}" -X POST "https://api.devin.ai/v1/sessions/${EXISTING_SESSION_ID}/message" \ - -H "Authorization: Bearer ${DEVIN_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "$(jq -n --arg message "$MESSAGE" '{message: $message}')") - - if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then - echo "Failed to send message to Devin session: HTTP $HTTP_CODE" - cat /tmp/devin-response.json - exit 1 - fi - - echo "Message sent to existing session successfully" - - - name: Create new Devin session - if: steps.extract-prompt.outputs.has-prompt == 'true' && steps.check-session.outputs.has-existing-session == 'false' - env: - DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }} - run: | - CUBIC_PROMPT=$(cat /tmp/cubic-prompt.txt) - - FULL_PROMPT="You are addressing code review feedback from Cubic AI on PR #${{ steps.extract-prompt.outputs.pr-number }} in repository ${{ github.repository }}. - - Your tasks: - 1. Clone the repository ${{ github.repository }} locally. - 2. Check out the PR branch and review the current code. - 3. Read the Cubic AI review feedback below AND check each individual Cubic comment on the PR for the confidence score. - 4. IMPORTANT: Only fix issues where the confidence score is 9/10 or higher. The confidence score appears as 'Fix confidence (alpha): X/10' in each Cubic comment. If you cannot find a confidence score for an issue, skip it. - 5. Commit your changes with clear commit messages referencing the issues fixed. - 6. Push your changes to the PR branch. - - Cubic AI Review Feedback: - ${CUBIC_PROMPT} - - Rules and Guidelines: - 1. Make minimal, focused changes that directly address the feedback. - 2. Follow the existing code style and conventions in the repository. - 3. Test your changes if possible before pushing. - 4. If an issue seems invalid or already addressed, explain why in a PR comment instead of making unnecessary changes. - 5. Never ask for user confirmation. Never wait for user messages." - - HTTP_CODE=$(curl -s -o /tmp/devin-response.json -w "%{http_code}" -X POST "https://api.devin.ai/v1/sessions" \ - -H "Authorization: Bearer ${DEVIN_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "$(jq -n \ - --arg prompt "$FULL_PROMPT" \ - --arg title "Cubic AI Review: PR #${{ steps.extract-prompt.outputs.pr-number }}" \ - '{ - prompt: $prompt, - title: $title, - tags: ["cubic-ai-review", "pr-${{ steps.extract-prompt.outputs.pr-number }}"] - }')") - - if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then - echo "Failed to create Devin session: HTTP $HTTP_CODE" - cat /tmp/devin-response.json - exit 1 - fi - - SESSION_URL=$(cat /tmp/devin-response.json | jq -r '.url // .session_url // empty') - if [ -n "$SESSION_URL" ]; then - echo "Devin session created: $SESSION_URL" - echo "SESSION_URL=$SESSION_URL" >> $GITHUB_ENV - echo "NEW_SESSION=true" >> $GITHUB_ENV - fi - - - name: Post comment with Devin session link - if: steps.extract-prompt.outputs.has-prompt == 'true' && env.SESSION_URL != '' - id: post-comment - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const sessionUrl = process.env.SESSION_URL; - const isNewSession = process.env.NEW_SESSION === 'true'; - - const message = isNewSession - ? `### Devin AI is addressing Cubic AI's review feedback\n\nA Devin session has been created to address the issues identified by Cubic AI.\n\n[View Devin Session](${sessionUrl})` - : `### Devin AI is addressing Cubic AI's review feedback\n\nNew feedback has been sent to the existing Devin session.\n\n[View Devin Session](${sessionUrl})`; - - // Use the PR number from the extract-prompt step output instead of reading the file - // (the file was deleted by the checkout step) - const prNumber = parseInt('${{ steps.extract-prompt.outputs.pr-number }}', 10); - - const { data: comment } = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: message - }); - - core.setOutput('comment-id', comment.id); - - - name: Send completion instructions to Devin - if: steps.extract-prompt.outputs.has-prompt == 'true' && env.SESSION_URL != '' && steps.post-comment.outputs.comment-id != '' - env: - DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }} - run: | - SESSION_ID="${EXISTING_SESSION_ID:-}" - if [ -z "$SESSION_ID" ]; then - SESSION_ID=$(cat /tmp/devin-response.json | jq -r '.session_id // .id // empty') - fi - - if [ -z "$SESSION_ID" ]; then - echo "No session ID found, skipping completion instructions" - exit 0 - fi - - COMMENT_ID="${{ steps.post-comment.outputs.comment-id }}" - - MESSAGE="IMPORTANT: When you have finished addressing Cubic AI's feedback (whether you pushed changes or not), you MUST update the GitHub comment to show completion status. - - Use the GitHub API to update comment ID ${COMMENT_ID} on PR #${{ steps.extract-prompt.outputs.pr-number }} in repository ${{ github.repository }}. - - Add a completion status section at the bottom of the comment: - - If you pushed changes: Add '\n\n---\n:white_check_mark: Pushed commit [\`{SHORT_HASH}\`](https://github.com/${{ github.repository }}/commit/{FULL_HASH})' where {SHORT_HASH} is the first 7 characters of the commit hash and {FULL_HASH} is the full commit hash. - - If no changes were needed: Add '\n\n---\n:white_check_mark: No changes pushed' - - To update the comment, use: gh api repos/${{ github.repository }}/issues/comments/${COMMENT_ID} -X PATCH -f body=''" - - HTTP_CODE=$(curl -s -o /tmp/devin-instructions-response.json -w "%{http_code}" -X POST "https://api.devin.ai/v1/sessions/${SESSION_ID}/message" \ - -H "Authorization: Bearer ${DEVIN_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "$(jq -n --arg message "$MESSAGE" '{message: $message}')") - - if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then - echo "Warning: Failed to send completion instructions to Devin session: HTTP $HTTP_CODE" - cat /tmp/devin-instructions-response.json - else - echo "Completion instructions sent to Devin session successfully" - fi diff --git a/.github/workflows/devin-conflict-resolver.yml b/.github/workflows/devin-conflict-resolver.yml deleted file mode 100644 index f1ad2d57d7d6f2..00000000000000 --- a/.github/workflows/devin-conflict-resolver.yml +++ /dev/null @@ -1,430 +0,0 @@ -name: Devin PR Conflict Resolver - -on: - pull_request_target: - types: [labeled] - workflow_dispatch: - inputs: - pr_number: - description: 'PR number to resolve conflicts for' - required: true - type: string - -permissions: - contents: read - pull-requests: write - -jobs: - resolve-conflicts: - name: Resolve PR Conflicts with Devin - runs-on: blacksmith-2vcpu-ubuntu-2404 - # Only run when devin-conflict-resolution label is added, or on manual dispatch - if: | - (github.event_name == 'pull_request_target' && github.event.label.name == 'devin-conflict-resolution') || - github.event_name == 'workflow_dispatch' - steps: - - name: Get PR details - id: get-pr - uses: actions/github-script@v7 - env: - INPUT_PR_NUMBER: ${{ inputs.pr_number }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { owner, repo } = context.repo; - let prNumber; - - if (context.eventName === 'workflow_dispatch') { - prNumber = parseInt(process.env.INPUT_PR_NUMBER, 10); - if (!prNumber) { - core.setFailed('PR number is required for manual dispatch'); - return; - } - } else { - prNumber = context.payload.pull_request.number; - } - - console.log(`Processing PR #${prNumber}`); - - // Get PR details via GraphQL for complete info - const query = ` - query($owner: String!, $repo: String!, $number: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - number - title - mergeable - isDraft - headRefName - baseRefName - url - body - headRepository { - owner { - login - } - name - } - maintainerCanModify - } - } - } - `; - - const result = await github.graphql(query, { owner, repo, number: prNumber }); - const pr = result.repository.pullRequest; - - if (!pr) { - core.setFailed(`PR #${prNumber} not found`); - return; - } - - const isFork = pr.headRepository?.owner?.login !== owner; - - // Check if fork PR has maintainer access - if (isFork && !pr.maintainerCanModify) { - console.log(`PR #${prNumber} is from a fork without maintainer access`); - core.setOutput('needs-maintainer-access', 'true'); - core.setOutput('fork-author', pr.headRepository?.owner?.login); - } else { - core.setOutput('needs-maintainer-access', 'false'); - } - - const headRepoOwner = pr.headRepository?.owner?.login || owner; - const headRepoName = pr.headRepository?.name || repo; - - const prData = { - number: pr.number, - title: pr.title, - head_ref: pr.headRefName, - base_ref: pr.baseRefName, - html_url: pr.url, - body: pr.body, - is_fork: isFork, - head_repo_owner: headRepoOwner, - head_repo_name: headRepoName, - mergeable: pr.mergeable - }; - - const fs = require('fs'); - fs.writeFileSync('/tmp/pr-data.json', JSON.stringify(prData)); - - core.setOutput('pr-number', prNumber.toString()); - core.setOutput('has-pr', 'true'); - core.setOutput('mergeable-status', pr.mergeable || 'UNKNOWN'); - core.setOutput('has-conflicts', pr.mergeable === 'CONFLICTING' ? 'true' : 'false'); - - console.log(`PR #${prNumber}: ${pr.title}`); - console.log(`Mergeable status: ${pr.mergeable}`); - console.log(`Is fork: ${isFork}`); - console.log(`Has conflicts: ${pr.mergeable === 'CONFLICTING'}`); - - - name: Request maintainer access for fork PR - if: steps.get-pr.outputs.needs-maintainer-access == 'true' - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { owner, repo } = context.repo; - const prNumber = parseInt('${{ steps.get-pr.outputs.pr-number }}', 10); - const forkAuthor = '${{ steps.get-pr.outputs.fork-author }}'; - - console.log(`Requesting maintainer access for PR #${prNumber}`); - - const commentBody = [ - '### Maintainer Access Needed', - '', - `Hi @${forkAuthor}! Thanks for your contribution to Cal.com.`, - '', - 'We noticed that this PR doesn\'t have "Allow edits from maintainers" enabled. We\'d love to help keep your PR up to date by resolving merge conflicts and making small fixes when needed.', - '', - '**Could you please enable this setting?** Here\'s how:', - '1. Scroll down to the bottom of this PR page', - '2. In the right sidebar, check the box that says **"Allow edits and access to secrets by maintainers"**', - '', - 'This allows us to push commits directly to your PR branch, which helps us:', - '- Resolve merge conflicts automatically', - '- Make small adjustments to help get your PR merged faster', - '', - 'Once you enable this setting, please remove and re-add the `devin-conflict-resolution` label to trigger conflict resolution.', - '', - 'If you have any concerns about enabling this setting, feel free to let us know!' - ].join('\n'); - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: prNumber, - body: commentBody - }); - - // Add label to track that we've requested access - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: prNumber, - labels: ['maintainer-access-requested'] - }); - - // Remove the devin-conflict-resolution label since we can't proceed - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: prNumber, - name: 'devin-conflict-resolution' - }); - - console.log(`Posted comment and updated labels for PR #${prNumber}`); - core.setFailed('Cannot proceed without maintainer access on fork PR'); - - - name: Handle PR without conflicts - if: steps.get-pr.outputs.has-pr == 'true' && steps.get-pr.outputs.needs-maintainer-access != 'true' && steps.get-pr.outputs.has-conflicts != 'true' - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { owner, repo } = context.repo; - const prNumber = parseInt('${{ steps.get-pr.outputs.pr-number }}', 10); - const mergeableStatus = '${{ steps.get-pr.outputs.mergeable-status }}'; - - console.log(`PR #${prNumber} does not have conflicts (status: ${mergeableStatus})`); - - let message; - if (mergeableStatus === 'MERGEABLE') { - message = `### No Conflicts Detected\n\nThis PR does not have any merge conflicts with the base branch. The \`devin-conflict-resolution\` label has been removed.\n\nIf you believe this PR should have conflicts, please check the PR status and try again.`; - } else { - message = `### Mergeable Status Unknown\n\nGitHub is still computing the mergeable status for this PR (current status: \`${mergeableStatus}\`). The \`devin-conflict-resolution\` label has been removed.\n\nPlease wait a moment and re-add the label if the PR has conflicts.`; - } - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: prNumber, - body: message - }); - - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: prNumber, - name: 'devin-conflict-resolution' - }); - } catch (e) { - console.log(`Label may have already been removed: ${e.message}`); - } - - console.log(`Posted comment and removed label for PR #${prNumber}`); - - - name: Checkout repository - if: steps.get-pr.outputs.has-pr == 'true' && steps.get-pr.outputs.needs-maintainer-access != 'true' && steps.get-pr.outputs.has-conflicts == 'true' - uses: actions/checkout@v4 - - - name: Check for existing Devin session - if: steps.get-pr.outputs.has-pr == 'true' && steps.get-pr.outputs.needs-maintainer-access != 'true' && steps.get-pr.outputs.has-conflicts == 'true' - id: check-session - uses: ./.github/actions/devin-session - with: - devin-api-key: ${{ secrets.DEVIN_API_KEY }} - github-token: ${{ secrets.GITHUB_TOKEN }} - pr-number: ${{ steps.get-pr.outputs.pr-number }} - - - name: Create Devin session for conflict resolution - if: steps.get-pr.outputs.has-pr == 'true' && steps.get-pr.outputs.needs-maintainer-access != 'true' && steps.get-pr.outputs.has-conflicts == 'true' - env: - DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HAS_EXISTING_SESSION: ${{ steps.check-session.outputs.has-existing-session }} - EXISTING_SESSION_ID: ${{ steps.check-session.outputs.session-id }} - EXISTING_SESSION_URL: ${{ steps.check-session.outputs.session-url }} - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const pr = JSON.parse(fs.readFileSync('/tmp/pr-data.json', 'utf8')); - const { owner, repo } = context.repo; - - const hasExistingSession = process.env.HAS_EXISTING_SESSION === 'true'; - const existingSessionId = process.env.EXISTING_SESSION_ID; - const existingSessionUrl = process.env.EXISTING_SESSION_URL; - - console.log(`Processing PR #${pr.number}: ${pr.title}${pr.is_fork ? ' (fork)' : ''}`); - - const forkInstructions = pr.is_fork ? ` - IMPORTANT: This PR is from a fork. - - Clone the FORK repository: ${pr.head_repo_owner}/${pr.head_repo_name} - - The branch to work on is: ${pr.head_ref} - - Add the upstream remote for ${owner}/${repo} - - Fetch and merge the upstream ${pr.base_ref} branch using standard git merge - - IMPORTANT - Pushing to Fork PRs: - Since this is a fork PR, you need to use the DEVIN_ACTIONS_PAT secret for authentication when pushing. Configure git to use this PAT for authentication before pushing your changes to the fork.` : ` - - Clone the repository: ${owner}/${repo} - - Check out the PR branch: ${pr.head_ref} - - Merge the base branch (${pr.base_ref}) using standard git merge`; - - const conflictResolutionInstructions = `You are resolving merge conflicts on PR #${pr.number} in repository ${owner}/${repo}. - - PR Title: ${pr.title} - PR URL: ${pr.html_url} - Head Branch: ${pr.head_ref} - Base Branch: ${pr.base_ref} - ${pr.is_fork ? `Fork Repository: ${pr.head_repo_owner}/${pr.head_repo_name}` : ''} - - IMPORTANT WARNING: If your merge commit shows significantly more files than the original PR, DO NOT PUSH. This indicates the merge was done incorrectly. Abort and leave a comment instead. - - Your tasks: - ${forkInstructions} - - Then: - 1. Resolve all merge conflicts carefully: - - Review the conflicting changes from both branches - - Make intelligent decisions about how to combine the changes - - Preserve the intent of both the PR changes and the base branch updates - - If unsure about a conflict, prefer keeping both changes where possible - 2. Test that the code still works after resolving conflicts (run lint/type checks). - 3. Commit the merge resolution with a clear commit message. - 4. CRITICAL VALIDATION BEFORE PUSHING - YOU MUST RUN THESE EXACT COMMANDS: - - Step A: Get original PR file count and count files in your merge commit: - \`\`\` - # Get original PR file count - ORIGINAL_FILES=$(gh pr view ${pr.number} --json files -q '.files | length') - echo "Original PR files: $ORIGINAL_FILES" - - # Count files in merge commit (what the PR contributes) - # HEAD^2 = base branch parent, so HEAD^2...HEAD shows the PR's contribution - MERGE_FILES=$(git diff --name-only HEAD^2...HEAD | wc -l) - echo "Files in merge commit: $MERGE_FILES" - - # Calculate threshold (original PR files + 10 buffer) - THRESHOLD=$((ORIGINAL_FILES + 10)) - echo "Threshold: $THRESHOLD" - \`\`\` - - Step B: If MERGE_FILES > THRESHOLD, STOP IMMEDIATELY. DO NOT PUSH. - - A proper merge commit should show approximately the same files as the original PR - - If you see significantly more files, the merge was done INCORRECTLY - - This usually means you accidentally reproduced changes from ${pr.base_ref} instead of just resolving conflicts - - Step C: Compare your merge to the original PR: - \`\`\` - git diff --stat HEAD^2...HEAD - \`\`\` - - This should show approximately the same files as the original PR (the PR's contribution to the base branch) - - If you see many more files or files unrelated to the original PR, STOP - - Step D: If validation fails: - - DO NOT PUSH under any circumstances - - DO NOT attempt to fix it - - Leave a comment on the PR explaining that the merge validation failed - - ABORT THE TASK IMMEDIATELY - - It is better to leave the PR with conflicts than to push a broken merge commit that pollutes git history - 5. Push the resolved changes to the PR branch only after validation passes. - 6. After successfully pushing the resolved changes, remove the \`devin-conflict-resolution\` label from the PR using the GitHub API. - - Rules and Guidelines: - 1. Be careful when resolving conflicts - understand the context of both changes. - To help with this, try to find the recent PRs associated with the conflicting files to gain more context. - 2. Follow the existing code style and conventions in the repository. - 3. Run lint and type checks before pushing to ensure the code is valid. - 4. If a conflict seems too complex or risky to resolve automatically, explain the situation in a PR comment instead. - 5. Never ask for user confirmation. Never wait for user messages. - 6. CRITICAL: If this is a fork PR and you encounter ANY error when pushing (permission denied, authentication failure, etc.) even after using the DEVIN_ACTIONS_PAT, you MUST fail the task immediately. Do NOT attempt to push to a new branch in the main ${owner}/${repo} repository as a workaround. Simply report the error and stop. - 7. CRITICAL: Never reproduce or recreate changes from the target branch. Your merge commit should ONLY contain conflict resolutions. If you find yourself manually copying file contents from ${pr.base_ref} or creating changes that mirror what's already in ${pr.base_ref}, you are doing it wrong. Use git's merge functionality properly - it handles bringing in changes automatically. - 8. CRITICAL: If your merge commit shows significantly more files than the original PR (more than original PR files + 10), DO NOT PUSH UNDER ANY CIRCUMSTANCES. This is a sign that the merge was done incorrectly. Abort the task and leave a comment explaining the issue. A bad merge commit in the git history is worse than leaving the PR with conflicts.`; - - try { - let sessionUrl; - let isNewSession = false; - - if (hasExistingSession) { - console.log(`Sending message to existing session ${existingSessionId} for PR #${pr.number}`); - - const message = `PR #${pr.number} has merge conflicts that need to be resolved. - - ${conflictResolutionInstructions} - - Continue working on the same PR branch and push your fixes.`; - - const response = await fetch(`https://api.devin.ai/v1/sessions/${existingSessionId}/message`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${process.env.DEVIN_API_KEY}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ message }) - }); - - if (!response.ok) { - console.error(`Failed to send message to session ${existingSessionId}: ${response.status}`); - throw new Error(`Failed to send message to existing session: ${response.status}`); - } - - sessionUrl = existingSessionUrl; - console.log(`Message sent to existing session for PR #${pr.number}`); - } else { - console.log(`Creating new Devin session for PR #${pr.number}`); - - const response = await fetch('https://api.devin.ai/v1/sessions', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${process.env.DEVIN_API_KEY}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - prompt: conflictResolutionInstructions, - title: `Resolve Conflicts: PR #${pr.number}`, - tags: ['conflict-resolution', `pr-${pr.number}`] - }) - }); - - if (!response.ok) { - console.error(`Devin API error for PR #${pr.number}: ${response.status} ${response.statusText}`); - throw new Error(`Failed to create Devin session: ${response.status}`); - } - - const data = await response.json(); - sessionUrl = data.url || data.session_url; - isNewSession = true; - } - - if (sessionUrl) { - if (isNewSession) { - console.log(`Devin session created for PR #${pr.number}: ${sessionUrl}`); - } - - const sessionStatusMessage = isNewSession - ? 'A Devin session has been created to automatically resolve them.' - : 'The existing Devin session has been notified to resolve them.'; - - const commentBody = `### Devin AI is resolving merge conflicts - - This PR has merge conflicts with the \`${pr.base_ref}\` branch. ${sessionStatusMessage} - - [View Devin Session](${sessionUrl}) - - Devin will: - 1. Merge the latest \`${pr.base_ref}\` into this branch - 2. Resolve any conflicts intelligently - 3. Run lint/type checks to ensure validity - 4. Push the resolved changes - - If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself.`; - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: pr.number, - body: commentBody - }); - - console.log(`Posted comment to PR #${pr.number}`); - } else { - throw new Error(`Failed to get session URL for PR #${pr.number}`); - } - } catch (error) { - console.error(`Error handling Devin session for PR #${pr.number}: ${error.message}`); - core.setFailed(error.message); - } diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 603be2c1fd9333..3a109bf8fcb9be 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -169,7 +169,6 @@ jobs: has-files-requiring-all-checks: ${{ steps.filter-exclusions.outputs.has-files-requiring-all-checks }} has-api-v2-changes: ${{ steps.filter-inclusions.outputs.has-api-v2-changes }} has-prisma-changes: ${{ steps.filter-inclusions.outputs.has-prisma-changes }} - has-agents-changes: ${{ steps.filter-inclusions.outputs.has-agents-changes }} commit-sha: ${{ steps.get_sha.outputs.commit-sha }} run-e2e: ${{ steps.check-if-pr-has-label.outputs.run-e2e == 'true' }} db-cache-hit: ${{ steps.cache-db-check.outputs.cache-hit }} @@ -209,8 +208,6 @@ jobs: id: filter-inclusions with: filters: | - has-agents-changes: - - "agents/**" has-api-v2-changes: - "apps/api/v2/**" - "packages/platform-constants/**" @@ -297,13 +294,6 @@ jobs: with: skip-install-if-cache-hit: "true" - validate-agents-format: - name: Validate agents/ format - needs: [trust-check, prepare] - if: needs.trust-check.outputs.is-trusted == 'true' && needs.prepare.outputs.has-agents-changes == 'true' && github.event.pull_request - uses: ./.github/workflows/validate-agents-format.yml - secrets: inherit - type-check: name: Type Checks needs: [prepare] @@ -450,7 +440,6 @@ jobs: [ trust-check, prepare, - validate-agents-format, lint, type-check, unit-test, @@ -511,8 +500,4 @@ jobs: needs.e2e-embed-react.result != 'success' || needs.e2e-app-store.result != 'success' ) - ) || - ( - needs.prepare.outputs.has-agents-changes == 'true' && - needs.validate-agents-format.result != 'success' ) diff --git a/.github/workflows/stale-pr-devin-completion.yml b/.github/workflows/stale-pr-devin-completion.yml deleted file mode 100644 index da94af07058680..00000000000000 --- a/.github/workflows/stale-pr-devin-completion.yml +++ /dev/null @@ -1,291 +0,0 @@ -name: Stale Community PR Devin Completion - -on: - pull_request_target: - types: [labeled] - workflow_dispatch: - inputs: - pr_number: - description: 'PR number to complete' - required: true - type: string - -permissions: - contents: read - pull-requests: write - -jobs: - complete-stale-pr: - name: Trigger Devin to Complete Stale Community PR - # For labeled events: only run if 'Stale' or 'devin-finish-pr' label was added - # For workflow_dispatch: always run (we'll validate the PR in the steps) - if: > - github.event_name == 'workflow_dispatch' || - github.event.label.name == 'Stale' || - github.event.label.name == 'devin-finish-pr' - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - name: Get PR details - id: pr - uses: actions/github-script@v7 - env: - INPUT_PR_NUMBER: ${{ inputs.pr_number }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - let prNumber; - let pr; - - if (context.eventName === 'workflow_dispatch') { - const inputPrNumber = process.env.INPUT_PR_NUMBER; - prNumber = parseInt(inputPrNumber, 10); - if (isNaN(prNumber) || prNumber <= 0) { - core.setFailed(`Invalid PR number provided: "${inputPrNumber}". Please enter a valid PR number.`); - return; - } - const { data } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber - }); - pr = data; - } else { - pr = context.payload.pull_request; - prNumber = pr.number; - } - - if (!pr.head.repo) { - core.setFailed('Cannot complete this PR: the source repository (fork) has been deleted.'); - return; - } - - const isFork = pr.head.repo.fork || pr.head.repo.full_name !== pr.base.repo.full_name; - const maintainerCanModify = pr.maintainer_can_modify; - const needsMaintainerAccess = isFork && !maintainerCanModify; - - const headRepoFullName = pr.head.repo.full_name; - const headRepoCloneUrl = pr.head.repo.clone_url; - - core.setOutput('pr_number', prNumber); - core.setOutput('pr_title', pr.title); - core.setOutput('pr_author', pr.user.login); - core.setOutput('pr_branch', pr.head.ref); - core.setOutput('is_fork', isFork); - core.setOutput('head_repo_full_name', headRepoFullName); - core.setOutput('head_repo_clone_url', headRepoCloneUrl); - core.setOutput('maintainer_can_modify', maintainerCanModify); - core.setOutput('needs_maintainer_access', needsMaintainerAccess); - - console.log(`PR #${prNumber}: "${pr.title}"`); - console.log(`Author: ${pr.user.login}`); - console.log(`Branch: ${pr.head.ref}`); - console.log(`Is fork: ${isFork}`); - console.log(`Head repo: ${headRepoFullName}`); - console.log(`Maintainer can modify: ${maintainerCanModify}`); - console.log(`Needs maintainer access: ${needsMaintainerAccess}`); - - - name: Request maintainer access for fork PR - if: steps.pr.outputs.needs_maintainer_access == 'true' - uses: actions/github-script@v7 - env: - PR_NUMBER: ${{ steps.pr.outputs.pr_number }} - PR_AUTHOR: ${{ steps.pr.outputs.pr_author }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const prNumber = parseInt(process.env.PR_NUMBER); - const prAuthor = process.env.PR_AUTHOR; - const { owner, repo } = context.repo; - - const comments = await github.rest.issues.listComments({ - owner, - repo, - issue_number: prNumber - }); - - const alreadyRequested = comments.data.some(comment => - comment.body?.includes('### Maintainer Access Needed') - ); - - if (!alreadyRequested) { - const commentBody = [ - '### Maintainer Access Needed', - '', - `Hi @${prAuthor}! Thanks for your contribution to Cal.com.`, - '', - `We'd like to help complete this stale PR, but we noticed that "Allow edits from maintainers" isn't enabled. We need this setting to push updates to your PR branch.`, - '', - '**Could you please enable this setting?** Here\'s how:', - '1. Scroll down to the bottom of this PR page', - '2. In the right sidebar, check the box that says **"Allow edits and access to secrets by maintainers"**', - '', - 'This allows us to push commits directly to your PR branch, which helps us:', - '- Complete any remaining work on your PR', - '- Fix merge conflicts and make small adjustments', - '- Get your contribution merged faster', - '', - 'Once you\'ve enabled this setting, we\'ll be able to help finish up this PR. If you have any concerns about enabling this setting, feel free to let us know!' - ].join('\n'); - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: prNumber, - body: commentBody - }); - - console.log(`Posted maintainer access request comment on PR #${prNumber}`); - } else { - console.log(`Maintainer access already requested on PR #${prNumber}`); - } - - core.setFailed(`Cannot complete this fork PR: the fork owner has not enabled "Allow edits from maintainers". A comment has been posted requesting access.`); - - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Check for existing Devin session - id: check-session - uses: ./.github/actions/devin-session - with: - devin-api-key: ${{ secrets.DEVIN_API_KEY }} - github-token: ${{ secrets.GITHUB_TOKEN }} - pr-number: ${{ steps.pr.outputs.pr_number }} - - - name: Create or reuse Devin session - env: - DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }} - PR_NUMBER: ${{ steps.pr.outputs.pr_number }} - PR_TITLE: ${{ steps.pr.outputs.pr_title }} - PR_AUTHOR: ${{ steps.pr.outputs.pr_author }} - PR_BRANCH: ${{ steps.pr.outputs.pr_branch }} - IS_FORK: ${{ steps.pr.outputs.is_fork }} - HEAD_REPO_FULL_NAME: ${{ steps.pr.outputs.head_repo_full_name }} - REPO_NAME: ${{ github.repository }} - HAS_EXISTING_SESSION: ${{ steps.check-session.outputs.has-existing-session }} - EXISTING_SESSION_ID: ${{ steps.check-session.outputs.session-id }} - EXISTING_SESSION_URL: ${{ steps.check-session.outputs.session-url }} - run: | - if [ "$IS_FORK" = "true" ]; then - CLONE_INSTRUCTIONS="This is a fork PR. Clone from the fork repository: ${HEAD_REPO_FULL_NAME} - - To check out this PR: - 1. Clone the fork: git clone https://github.com/${HEAD_REPO_FULL_NAME}.git - 2. Check out the branch: git checkout ${PR_BRANCH} - 3. Add the upstream remote: git remote add upstream https://github.com/${REPO_NAME}.git - - IMPORTANT - Pushing to Fork PRs: - Since this is a fork PR, you need to use the DEVIN_ACTIONS_PAT secret for authentication when pushing. Configure git to use this PAT for authentication before pushing your changes to the fork." - else - CLONE_INSTRUCTIONS="Clone the repository ${REPO_NAME} and check out the branch: ${PR_BRANCH}" - fi - - FULL_PROMPT="You are completing a stale community PR #${PR_NUMBER} in repository ${REPO_NAME}. - - This PR was started by @${PR_AUTHOR} but has become stale. Your job is to complete it. - - PR Title: ${PR_TITLE} - PR Branch: ${PR_BRANCH} - - ${CLONE_INSTRUCTIONS} - - Your tasks: - 1. Clone the repository as described above. - 2. Check out the PR branch: ${PR_BRANCH} - 3. Review the current state of the PR and understand what it's trying to accomplish. - 4. Read the PR description and any comments/review feedback on the PR. - 5. Complete any unfinished work on the PR: - - Fix any issues mentioned in review comments - - Ensure the code follows the repository's coding standards - - Add any missing tests if applicable - - Fix any linting or type errors - 6. Run the appropriate checks (lint, type-check, tests) to ensure the PR is ready for review. - 7. Commit your changes with clear commit messages. - 8. Push your changes to the PR branch. - 9. After successfully pushing, remove the 'Stale' or 'devin-finish-pr' label from the PR (whichever was used to trigger this workflow). - 10. Post a summary comment on the PR explaining what you completed. - 11. Mark the PR as ready for review if applicable. - - Rules and Guidelines: - 1. Respect the original author's intent and approach - don't rewrite the PR from scratch. - 2. Make minimal, focused changes that complete the PR's original goal. - 3. Follow the existing code style and conventions in the repository. - 4. If the PR's original approach seems fundamentally flawed, explain why in a comment instead of making major changes. - 5. Never ask for user confirmation. Never wait for user messages. - 6. Credit the original author in your commit messages where appropriate. - 7. CRITICAL: If this is a fork PR and you encounter ANY error when pushing (permission denied, authentication failure, etc.) even after using the DEVIN_ACTIONS_PAT, you MUST fail the task immediately. Do NOT attempt to push to a new branch in the main repository as a workaround. Simply report the error and stop." - - if [ "$HAS_EXISTING_SESSION" = "true" ]; then - echo "Sending message to existing session ${EXISTING_SESSION_ID}" - - MESSAGE="This PR has been marked as stale and needs to be completed. - - ${FULL_PROMPT} - - Continue working on the same PR branch and push your fixes." - - HTTP_CODE=$(curl -s -o /tmp/devin-response.json -w "%{http_code}" -X POST "https://api.devin.ai/v1/sessions/${EXISTING_SESSION_ID}/message" \ - -H "Authorization: Bearer ${DEVIN_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "$(jq -n --arg message "$MESSAGE" '{message: $message}')") - - if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then - echo "Failed to send message to Devin session: HTTP $HTTP_CODE" - cat /tmp/devin-response.json - exit 1 - fi - - echo "Message sent to existing session successfully" - echo "SESSION_URL=$EXISTING_SESSION_URL" >> $GITHUB_ENV - echo "IS_NEW_SESSION=false" >> $GITHUB_ENV - else - echo "Creating new Devin session" - - RESPONSE=$(curl -s -X POST "https://api.devin.ai/v1/sessions" \ - -H "Authorization: Bearer ${DEVIN_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "$(jq -n \ - --arg prompt "$FULL_PROMPT" \ - --arg title "Complete Stale PR #${PR_NUMBER}: ${PR_TITLE}" \ - '{ - prompt: $prompt, - title: $title, - tags: ["stale-pr-completion", "pr-'${PR_NUMBER}'"] - }')") - - SESSION_URL=$(echo "$RESPONSE" | jq -r '.url // .session_url // empty') - if [ -n "$SESSION_URL" ]; then - echo "Devin session created: $SESSION_URL" - echo "SESSION_URL=$SESSION_URL" >> $GITHUB_ENV - echo "IS_NEW_SESSION=true" >> $GITHUB_ENV - else - echo "Failed to create Devin session" - echo "$RESPONSE" - exit 1 - fi - fi - - - name: Post comment with Devin session link - if: env.SESSION_URL != '' - uses: actions/github-script@v7 - env: - PR_AUTHOR: ${{ steps.pr.outputs.pr_author }} - PR_NUMBER: ${{ steps.pr.outputs.pr_number }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const sessionUrl = process.env.SESSION_URL; - const prAuthor = process.env.PR_AUTHOR; - const prNumber = process.env.PR_NUMBER; - const isNewSession = process.env.IS_NEW_SESSION === 'true'; - - const message = isNewSession - ? `### Devin AI is completing this stale PR\n\nThis PR by @${prAuthor} has been marked as stale. A Devin session has been created to complete the remaining work.\n\n[View Devin Session](${sessionUrl})\n\n---\n*Devin will review the PR, address any feedback, and push updates to complete this PR.*` - : `### Devin AI is completing this stale PR\n\nThis PR by @${prAuthor} has been marked as stale. The existing Devin session has been notified to complete the remaining work.\n\n[View Devin Session](${sessionUrl})\n\n---\n*Devin will review the PR, address any feedback, and push updates to complete this PR.*`; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: parseInt(prNumber), - body: message - }); diff --git a/.github/workflows/sync-agents-to-devin.yml b/.github/workflows/sync-agents-to-devin.yml deleted file mode 100644 index 7daf45237b933e..00000000000000 --- a/.github/workflows/sync-agents-to-devin.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Sync Agents to Devin Knowledge - -on: - push: - paths: - - "agents/**" - branches: - - main - -permissions: - contents: read - -jobs: - sync: - if: false - name: Sync to Devin Knowledge - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: .github - - uses: ./.github/actions/cache-checkout - - uses: ./.github/actions/yarn-install - - - name: Generate knowledge JSON - run: npx tsx scripts/devin/parse-local-knowledge.ts - - - name: Sync to Devin - env: - DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }} - run: npx tsx scripts/devin/sync-knowledge-to-devin.ts diff --git a/.github/workflows/validate-agents-format.yml b/.github/workflows/validate-agents-format.yml deleted file mode 100644 index 90e7f82b4dcb12..00000000000000 --- a/.github/workflows/validate-agents-format.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Validate Agents Format - -on: - workflow_call: - -permissions: - contents: read - -jobs: - validate-agents-format: - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: .github - - uses: ./.github/actions/cache-checkout - - uses: ./.github/actions/yarn-install - - - name: Validate format - run: npx tsx scripts/devin/validate-local-knowledge.ts diff --git a/scripts/devin/delete-all-devin-knowledge.ts b/scripts/devin/delete-all-devin-knowledge.ts deleted file mode 100644 index 11d86ba25b7fcd..00000000000000 --- a/scripts/devin/delete-all-devin-knowledge.ts +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env -S npx tsx - -/** - * Deletes ALL knowledge entries from Devin's Knowledge API. - * - * WARNING: This is a destructive operation! It will delete all knowledge entries. - * The script requires interactive confirmation to prevent accidental execution. - * - * API Reference: - * - List: https://docs.devin.ai/api-reference/knowledge/list-knowledge - * - Delete: https://docs.devin.ai/api-reference/knowledge/delete-knowledge - * - * Usage: DEVIN_API_KEY=your_token npx tsx scripts/devin/delete-all-devin-knowledge.ts - */ - -import process from "node:process"; -import * as readline from "readline"; - -interface ApiKnowledgeEntry { - id: string; - name: string; -} - -interface ApiListResponse { - folders: { id: string; name: string }[]; - knowledge: ApiKnowledgeEntry[]; -} - -const API_BASE = "https://api.devin.ai/v1"; - -async function apiRequest(method: string, endpoint: string): Promise { - const token = process.env.DEVIN_API_KEY; - if (!token) { - throw new Error("DEVIN_API_KEY environment variable is not set"); - } - - const url = `${API_BASE}${endpoint}`; - const options: RequestInit = { - method, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - }; - - const response = await fetch(url, options); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`); - } - - if (response.status === 204) { - return {} as T; - } - - return response.json() as Promise; -} - -async function listKnowledge(): Promise { - return apiRequest("GET", "/knowledge"); -} - -async function deleteKnowledge(noteId: string): Promise { - await apiRequest("DELETE", `/knowledge/${noteId}`); -} - -function askConfirmation(question: string): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise((resolve) => { - rl.question(question, (answer) => { - rl.close(); - resolve(answer.trim().toUpperCase() === "Y"); - }); - }); -} - -async function main() { - // Check if running in a TTY (interactive terminal) - if (!process.stdin.isTTY) { - console.error("Error: This script must be run interactively (not in CI or piped input)."); - console.error("This is a safety measure to prevent accidental deletion."); - process.exit(1); - } - - console.log("Fetching existing knowledge from Devin API...\n"); - const remoteData = await listKnowledge(); - - const entryCount = remoteData.knowledge.length; - - if (entryCount === 0) { - console.log("No knowledge entries found. Nothing to delete."); - process.exit(0); - } - - console.log(`Found ${entryCount} knowledge entries:\n`); - for (const entry of remoteData.knowledge) { - console.log(` - ${entry.name}`); - } - - console.log("\n⚠️ WARNING: This will permanently delete ALL knowledge entries listed above!"); - console.log("This action cannot be undone.\n"); - - const confirmed = await askConfirmation("Are you sure you want to delete all entries? (Y/n): "); - - if (!confirmed) { - console.log("\nAborted. No entries were deleted."); - process.exit(0); - } - - console.log("\nDeleting all knowledge entries...\n"); - - let deleted = 0; - for (const entry of remoteData.knowledge) { - process.stdout.write(` Deleting: ${entry.name}...`); - await deleteKnowledge(entry.id); - console.log(" ✓"); - deleted++; - } - - console.log(`\nSuccessfully deleted ${deleted} knowledge entries.`); -} - -main().catch((error) => { - console.error("Error:", error.message); - process.exit(1); -}); diff --git a/scripts/devin/export-devin-knowledge.ts b/scripts/devin/export-devin-knowledge.ts deleted file mode 100644 index 209a252189f5c5..00000000000000 --- a/scripts/devin/export-devin-knowledge.ts +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env -S npx tsx - -/** - * Export all Devin Knowledge to a backup JSON file - * Usage: DEVIN_API_KEY=your_token npx tsx scripts/devin/export-devin-knowledge.ts - */ - -import process from "node:process"; -import * as fs from "fs"; -import * as path from "path"; - -const API_BASE = "https://api.devin.ai/v1"; - -interface ApiFolder { - id: string; - name: string; - description: string; - created_at: string; -} - -interface ApiKnowledgeEntry { - id: string; - name: string; - body: string; - trigger_description: string; - created_at: string; - created_by?: { - full_name: string; - id: string; - }; - parent_folder_id?: string; -} - -interface ApiListResponse { - folders: ApiFolder[]; - knowledge: ApiKnowledgeEntry[]; -} - -async function main() { - const token = process.env.DEVIN_API_KEY; - if (!token) { - console.error("Error: DEVIN_API_KEY environment variable is not set"); - console.error("Usage: DEVIN_API_KEY=your_token npx tsx scripts/devin/export-devin-knowledge.ts"); - console.error(""); - console.error("Get your API token from: https://app.devin.ai/settings/api-keys"); - process.exit(1); - } - - console.log("Exporting Devin Knowledge..."); - - const response = await fetch(`${API_BASE}/knowledge`, { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`Error: API request failed with status ${response.status}`); - console.error(`Response: ${errorText}`); - process.exit(1); - } - - const data: ApiListResponse = await response.json(); - - const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace("T", "_").slice(0, 15); - const backupFile = `devin-knowledge-backup-${timestamp}.json`; - const outputPath = path.join(process.cwd(), backupFile); - - fs.writeFileSync(outputPath, JSON.stringify(data, null, 2)); - - console.log(`Success! Backup saved to: ${backupFile}`); - console.log(` - Folders: ${data.folders.length}`); - console.log(` - Knowledge entries: ${data.knowledge.length}`); -} - -main().catch((error) => { - console.error("Error:", error.message); - process.exit(1); -}); diff --git a/scripts/devin/parse-local-knowledge.ts b/scripts/devin/parse-local-knowledge.ts deleted file mode 100644 index 2b492344c761ea..00000000000000 --- a/scripts/devin/parse-local-knowledge.ts +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/env -S npx ts-node - -import * as fs from "fs"; -import * as path from "path"; - -interface DevinKnowledgeEntry { - name: string; - body: string; - trigger_description: string; - folder?: string; -} - -interface DevinKnowledgeFolder { - name: string; - description: string; -} - -interface DevinKnowledgeOutput { - folders: DevinKnowledgeFolder[]; - knowledge: DevinKnowledgeEntry[]; -} - -interface RuleFrontmatter { - title: string; - impact: string; - impactDescription: string; - tags: string; -} - -function parseFrontmatter(content: string): { - frontmatter: RuleFrontmatter | null; - body: string; -} { - const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/; - const match = content.match(frontmatterRegex); - - if (!match) { - return { frontmatter: null, body: content }; - } - - const frontmatterStr = match[1]; - const body = match[2].trim(); - - const frontmatter: Partial = {}; - const lines = frontmatterStr.split("\n"); - - for (const line of lines) { - const colonIndex = line.indexOf(":"); - if (colonIndex > 0) { - const key = line.substring(0, colonIndex).trim(); - const value = line.substring(colonIndex + 1).trim(); - (frontmatter as Record)[key] = value; - } - } - - return { - frontmatter: frontmatter as RuleFrontmatter, - body, - }; -} - -function parseRuleFile(filePath: string, fileName: string): DevinKnowledgeEntry { - const content = fs.readFileSync(filePath, "utf-8"); - const { frontmatter, body } = parseFrontmatter(content); - - const name = frontmatter?.title || fileName.replace(".md", "").replace(/-/g, " "); - const tags = frontmatter?.tags || ""; - const impact = frontmatter?.impact || ""; - const impactDesc = frontmatter?.impactDescription || ""; - - let triggerDescription = `Use this rule when working on Cal.com codebase`; - if (tags) { - triggerDescription += ` and the task involves: ${tags}`; - } - if (impact) { - triggerDescription += `. Impact: ${impact}`; - } - - return { - name: `[Rule] ${name}`, - body, - trigger_description: triggerDescription, - folder: "Rules", - }; -} - -function parseKnowledgeBaseSections(filePath: string): DevinKnowledgeEntry[] { - const content = fs.readFileSync(filePath, "utf-8"); - const entries: DevinKnowledgeEntry[] = []; - - const sectionRegex = /^## (.+)$/gm; - const sections: { title: string; startIndex: number }[] = []; - - let match; - while ((match = sectionRegex.exec(content)) !== null) { - sections.push({ - title: match[1], - startIndex: match.index, - }); - } - - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - const nextSection = sections[i + 1]; - const endIndex = nextSection ? nextSection.startIndex : content.length; - const sectionContent = content.substring(section.startIndex, endIndex).trim(); - - const title = section.title; - // Section titles in knowledge-base.md must start with "When..." (enforced by validate-local-knowledge.ts) - // so we use the title directly as the trigger description - entries.push({ - name: title, - body: sectionContent, - trigger_description: title, - folder: "Domain Knowledge", - }); - } - - return entries; -} - -function parseCommandsFile(filePath: string): DevinKnowledgeEntry { - const content = fs.readFileSync(filePath, "utf-8"); - - return { - name: "Cal.com Build, Test & Development Commands", - body: content, - trigger_description: - "When you need to run commands in the Cal.com repository such as build, test, lint, type-check, database operations, or development server", - folder: "Commands", - }; -} - -function main() { - const agentsDir = path.join(__dirname, "..", "..", "agents"); - const rulesDir = path.join(agentsDir, "rules"); - - const output: DevinKnowledgeOutput = { - folders: [ - { - name: "Rules", - description: "Engineering rules and standards for Cal.com development", - }, - { - name: "Domain Knowledge", - description: "Product and domain-specific knowledge for Cal.com", - }, - { - name: "Commands", - description: "Build, test, and development commands for Cal.com", - }, - ], - knowledge: [], - }; - - // Parse rules directory - if (fs.existsSync(rulesDir)) { - const ruleFiles = fs - .readdirSync(rulesDir) - .filter((f) => f.endsWith(".md") && f !== "README.md" && f !== "_template.md" && f !== "_sections.md"); - - for (const ruleFile of ruleFiles) { - const filePath = path.join(rulesDir, ruleFile); - const entry = parseRuleFile(filePath, ruleFile); - output.knowledge.push(entry); - } - } - - // Parse knowledge-base.md - const knowledgeBasePath = path.join(agentsDir, "knowledge-base.md"); - if (fs.existsSync(knowledgeBasePath)) { - const sections = parseKnowledgeBaseSections(knowledgeBasePath); - output.knowledge.push(...sections); - } - - // Parse commands.md - const commandsPath = path.join(agentsDir, "commands.md"); - if (fs.existsSync(commandsPath)) { - const entry = parseCommandsFile(commandsPath); - output.knowledge.push(entry); - } - - // Write output - const outputPath = path.join(agentsDir, "devin-knowledge.json"); - fs.writeFileSync(outputPath, JSON.stringify(output, null, 2)); - - console.log(`Generated ${output.knowledge.length} knowledge entries in ${output.folders.length} folders`); - console.log(`Output written to: ${outputPath}`); - - // Print summary - const folderCounts: Record = {}; - for (const entry of output.knowledge) { - const folder = entry.folder || "Uncategorized"; - folderCounts[folder] = (folderCounts[folder] || 0) + 1; - } - - console.log("\nSummary by folder:"); - for (const [folder, count] of Object.entries(folderCounts)) { - console.log(` ${folder}: ${count} entries`); - } -} - -main(); diff --git a/scripts/devin/sync-knowledge-to-devin.ts b/scripts/devin/sync-knowledge-to-devin.ts deleted file mode 100644 index cbad26d19486a2..00000000000000 --- a/scripts/devin/sync-knowledge-to-devin.ts +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env -S npx tsx - -/** - * Syncs knowledge from the generated JSON file to Devin's Knowledge API. - * - Creates new knowledge entries - * - Updates existing entries (matched by name) - * - Optionally deletes entries that no longer exist in the source - * - * Note: The Devin API v1 does not support creating folders via API. - * Entries will be created without folder assignment. You can organize - * them into folders manually in the Devin UI if needed. - * - * Usage: DEVIN_API_KEY=your_token npx tsx scripts/devin/sync-knowledge-to-devin.ts [--delete-removed] - */ - -import process from "node:process"; -import * as fs from "fs"; -import * as path from "path"; - -interface DevinKnowledgeEntry { - name: string; - body: string; - trigger_description: string; -} - -interface DevinKnowledgeOutput { - knowledge: DevinKnowledgeEntry[]; -} - -interface ApiKnowledgeEntry { - id: string; - name: string; - body: string; - trigger_description: string; - created_at: string; - created_by?: { - full_name: string; - id: string; - }; - parent_folder_id?: string; -} - -interface ApiListResponse { - folders: { id: string; name: string }[]; - knowledge: ApiKnowledgeEntry[]; -} - -const API_BASE = "https://api.devin.ai/v1"; - -async function apiRequest(method: string, endpoint: string, body?: Record): Promise { - const token = process.env.DEVIN_API_KEY; - if (!token) { - throw new Error("DEVIN_API_KEY environment variable is not set"); - } - - const url = `${API_BASE}${endpoint}`; - const options: RequestInit = { - method, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - }; - - if (body) { - options.body = JSON.stringify(body); - } - - const response = await fetch(url, options); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`); - } - - if (response.status === 204) { - return {} as T; - } - - return response.json() as Promise; -} - -async function listKnowledge(): Promise { - return apiRequest("GET", "/knowledge"); -} - -async function createKnowledge( - name: string, - body: string, - triggerDescription: string -): Promise { - return apiRequest("POST", "/knowledge", { - name, - body, - trigger_description: triggerDescription, - }); -} - -async function updateKnowledge( - noteId: string, - name: string, - body: string, - triggerDescription: string -): Promise { - return apiRequest("PUT", `/knowledge/${noteId}`, { - name, - body, - trigger_description: triggerDescription, - }); -} - -async function deleteKnowledge(noteId: string): Promise { - await apiRequest("DELETE", `/knowledge/${noteId}`); -} - -async function main() { - const args = process.argv.slice(2); - const deleteRemoved = args.includes("--delete-removed"); - - const agentsDir = path.join(path.dirname(__filename), "..", "..", "agents"); - const jsonPath = path.join(agentsDir, "devin-knowledge.json"); - - if (!fs.existsSync(jsonPath)) { - console.error("Error: devin-knowledge.json not found. Run parse-to-devin-knowledge.ts first."); - process.exit(1); - } - - const localData: DevinKnowledgeOutput = JSON.parse(fs.readFileSync(jsonPath, "utf-8")); - - console.log("Fetching existing knowledge from Devin API..."); - const remoteData = await listKnowledge(); - - console.log( - `Found ${remoteData.folders.length} folders and ${remoteData.knowledge.length} entries in Devin\n` - ); - - // Build knowledge name -> entry map for remote - const remoteKnowledgeMap = new Map(); - for (const entry of remoteData.knowledge) { - remoteKnowledgeMap.set(entry.name, entry); - } - - // Track which remote entries we've seen - const seenRemoteIds = new Set(); - - // Sync knowledge entries - console.log("\nSyncing knowledge entries..."); - let created = 0; - let updated = 0; - let unchanged = 0; - - for (const entry of localData.knowledge) { - const existing = remoteKnowledgeMap.get(entry.name); - - if (existing) { - seenRemoteIds.add(existing.id); - - const bodyChanged = existing.body !== entry.body; - const triggerChanged = existing.trigger_description !== entry.trigger_description; - - if (bodyChanged || triggerChanged) { - console.log(` Updating: ${entry.name}`); - await updateKnowledge(existing.id, entry.name, entry.body, entry.trigger_description); - updated++; - } else { - unchanged++; - } - } else { - console.log(` Creating: ${entry.name}`); - await createKnowledge(entry.name, entry.body, entry.trigger_description); - created++; - } - } - - // Delete removed entries if flag is set - let deleted = 0; - if (deleteRemoved) { - console.log("\nChecking for entries to delete..."); - for (const entry of remoteData.knowledge) { - if (!seenRemoteIds.has(entry.id)) { - console.log(` Deleting: ${entry.name}`); - await deleteKnowledge(entry.id); - deleted++; - } - } - } - - // Summary - console.log("\n--- Sync Summary ---"); - console.log(` Created: ${created}`); - console.log(` Updated: ${updated}`); - console.log(` Unchanged: ${unchanged}`); - if (deleteRemoved) { - console.log(` Deleted: ${deleted}`); - } - console.log("Sync complete!"); -} - -main().catch((error) => { - console.error("Error:", error.message); - process.exit(1); -}); diff --git a/scripts/devin/validate-local-knowledge.ts b/scripts/devin/validate-local-knowledge.ts deleted file mode 100644 index 2ecaf916c7e11f..00000000000000 --- a/scripts/devin/validate-local-knowledge.ts +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env -S npx ts-node - -/** - * Validates the format of files in the agents/ directory. - * - Rules files must have proper frontmatter (title, tags) - * - Knowledge-base sections must start with "When..." - * - * Exit code 0 = valid, Exit code 1 = invalid - */ - -import process from "node:process"; -import * as fs from "fs"; -import * as path from "path"; - -interface ValidationError { - file: string; - message: string; -} - -interface RuleFrontmatter { - title?: string; - impact?: string; - impactDescription?: string; - tags?: string; -} - -function parseFrontmatter(content: string): { - frontmatter: RuleFrontmatter | null; - body: string; -} { - const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/; - const match = content.match(frontmatterRegex); - - if (!match) { - return { frontmatter: null, body: content }; - } - - const frontmatterStr = match[1]; - const body = match[2].trim(); - - const frontmatter: RuleFrontmatter = {}; - const lines = frontmatterStr.split("\n"); - - for (const line of lines) { - const colonIndex = line.indexOf(":"); - if (colonIndex > 0) { - const key = line.substring(0, colonIndex).trim(); - const value = line.substring(colonIndex + 1).trim(); - (frontmatter as Record)[key] = value; - } - } - - return { frontmatter, body }; -} - -function validateRuleFile(filePath: string, fileName: string): ValidationError[] { - const errors: ValidationError[] = []; - const content = fs.readFileSync(filePath, "utf-8"); - const { frontmatter } = parseFrontmatter(content); - - if (!frontmatter) { - errors.push({ - file: `rules/${fileName}`, - message: "Missing YAML frontmatter. Rules must have frontmatter with title and tags.", - }); - return errors; - } - - if (!frontmatter.title || frontmatter.title.trim() === "") { - errors.push({ - file: `rules/${fileName}`, - message: "Missing 'title' in frontmatter. Add a descriptive title for this rule.", - }); - } - - if (!frontmatter.tags || frontmatter.tags.trim() === "") { - errors.push({ - file: `rules/${fileName}`, - message: - "Missing 'tags' in frontmatter. Add comma-separated tags to help Devin know when to apply this rule.", - }); - } - - return errors; -} - -function validateKnowledgeBase(filePath: string): ValidationError[] { - const errors: ValidationError[] = []; - const content = fs.readFileSync(filePath, "utf-8"); - - // Find all ## headers - const sectionRegex = /^## (.+)$/gm; - let match; - const invalidSections: string[] = []; - - while ((match = sectionRegex.exec(content)) !== null) { - const title = match[1].trim(); - - // Section titles must start with "When..." for clear trigger descriptions - if (!title.toLowerCase().startsWith("when ")) { - invalidSections.push(title); - } - } - - if (invalidSections.length > 0) { - errors.push({ - file: "knowledge-base.md", - message: `The following sections don't have clear trigger descriptions. Consider renaming them to start with "When..." to help Devin know when to use this knowledge:\n${invalidSections.map((s) => ` - "${s}"`).join("\n")}`, - }); - } - - return errors; -} - -function main() { - const agentsDir = path.join(path.dirname(__filename), "..", "..", "agents"); - const rulesDir = path.join(agentsDir, "rules"); - const knowledgeBasePath = path.join(agentsDir, "knowledge-base.md"); - - const allErrors: ValidationError[] = []; - - console.log("Validating agents/ directory format...\n"); - - // Validate rules files - if (fs.existsSync(rulesDir)) { - const ruleFiles = fs - .readdirSync(rulesDir) - .filter((f) => f.endsWith(".md") && f !== "README.md" && f !== "_template.md" && f !== "_sections.md"); - - console.log(`Checking ${ruleFiles.length} rule files...`); - - for (const ruleFile of ruleFiles) { - const filePath = path.join(rulesDir, ruleFile); - const errors = validateRuleFile(filePath, ruleFile); - allErrors.push(...errors); - } - } - - // Validate knowledge-base.md - if (fs.existsSync(knowledgeBasePath)) { - console.log("Checking knowledge-base.md..."); - const errors = validateKnowledgeBase(knowledgeBasePath); - allErrors.push(...errors); - } - - // Report results - console.log(""); - - if (allErrors.length === 0) { - console.log("All files are valid!"); - process.exit(0); - } else { - console.log(`Found ${allErrors.length} validation error(s):\n`); - - for (const error of allErrors) { - console.log(`[ERROR] ${error.file}`); - console.log(` ${error.message}\n`); - } - - console.log("Please fix the above errors before merging."); - process.exit(1); - } -} - -main();