diff --git a/.github/test-events/dependabot-pr.json b/.github/test-events/dependabot-pr.json new file mode 100644 index 0000000000..585eded46c --- /dev/null +++ b/.github/test-events/dependabot-pr.json @@ -0,0 +1,15 @@ +{ + "pull_request": { + "number": 13545, + "title": "Bump test from 7.0.4 to 7.0.8", + "user": { + "login": "dependabot[bot]" + } + }, + "repository": { + "owner": { + "login": "openfoodfoundation" + }, + "name": "openfoodnetwork" + } +} diff --git a/.github/workflows/move-dependency-pr-to-code-review.yml b/.github/workflows/move-dependency-pr-to-code-review.yml new file mode 100644 index 0000000000..533ed122cc --- /dev/null +++ b/.github/workflows/move-dependency-pr-to-code-review.yml @@ -0,0 +1,152 @@ +name: Auto-move Dependabot PRs to Code Review +permissions: + contents: read + pull-requests: read + project: write + +on: + pull_request: + types: [opened] + +jobs: + move-pr-to-code-review: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' || startsWith(github.event.pull_request.title, 'Bump') + steps: + - name: Generate GitHub App Token + id: app-token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.DEPENDABOT_PR_APP_ID }} + private_key: ${{ secrets.DEPENDABOT_PR_APP_PRIVATE_KEY }} + installation_retrieval_mode: id + installation_retrieval_payload: ${{ secrets.DEPENDABOT_PR_APP_INSTALLATION_ID }} + + - name: Move PR to Code Review in Project v2 + uses: actions/github-script@v7 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + const projectNumber = 8; // for "OFN Delivery board" + const org = "openfoodfoundation"; + const repo = context.repo.repo; + const prNumber = context.payload.pull_request.number; + const statusFieldName = "Status"; + const statusValue = "Code review 🔎"; + + // ---- Helper: Get PR Node ID ---- + async function getPrNodeId(owner, repo, number) { + const res = await github.graphql(` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + id + number + title + } + } + } + `, { owner, repo, number }); + return res.repository.pullRequest.id; + } + + console.log("🚀 Starting ProjectV2 automation..."); + + // ---- Step 1: Get Project and Fields ---- + const projectRes = await github.graphql(` + query($org: String!, $number: Int!) { + organization(login: $org) { + projectV2(number: $number) { + id + title + fields(first: 50) { + nodes { + ... on ProjectV2Field { + id + name + } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + `, { org, number: projectNumber }); + + const project = projectRes.organization.projectV2; + if (!project) throw new Error(`❌ Project #${projectNumber} not found`); + + console.log(`✅ Found project: ${project.title} (${project.id})`); + + const statusField = project.fields.nodes.find(f => f.name === statusFieldName); + if (!statusField) throw new Error(`❌ Field '${statusFieldName}' not found`); + + const option = statusField.options.find(o => o.name === statusValue); + if (!option) throw new Error(`❌ Option '${statusValue}' not found in '${statusFieldName}'`); + + console.log(`✅ Found field '${statusFieldName}' and option '${statusValue}'`); + + // ---- Step 2: Get PR Node ID ---- + const prNodeId = await getPrNodeId(org, repo, prNumber); + console.log(`✅ PR #${prNumber} node ID: ${prNodeId}`); + + // ---- Step 3: Check if PR is already in Project ---- + const itemRes = await github.graphql(` + query($prId: ID!) { + node(id: $prId) { + ... on PullRequest { + projectItems(first: 50) { + nodes { + id + project { id title } + } + } + } + } + } + `, { prId: prNodeId }); + + let projectItem = itemRes.node.projectItems.nodes.find(i => i.project.id === project.id); + + if (!projectItem) { + console.log("â„šī¸ PR not yet in project, adding..."); + const addRes = await github.graphql(` + mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { + item { id } + } + } + `, { projectId: project.id, contentId: prNodeId }); + projectItem = addRes.addProjectV2ItemById.item; + console.log(`✅ Added PR to project: ${projectItem.id}`); + } else { + console.log(`â„šī¸ PR already in project: ${projectItem.id}`); + } + + // ---- Step 4: Update Status ---- + await github.graphql(` + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId, + itemId: $itemId, + fieldId: $fieldId, + value: { singleSelectOptionId: $optionId } + }) { + projectV2Item { id } + } + } + `, { + projectId: project.id, + itemId: projectItem.id, + fieldId: statusField.id, + optionId: option.id, + }); + + console.log(`🎉 Moved PR #${prNumber} → '${statusValue}'`); diff --git a/.gitignore b/.gitignore index 7e6c741b5c..e1c303d217 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ yarn-debug.log* /config/credentials.yml.enc /config/master.key +.secrets diff --git a/.secrets.example b/.secrets.example new file mode 100644 index 0000000000..d1febf16f0 --- /dev/null +++ b/.secrets.example @@ -0,0 +1,4 @@ +# .secrets file define github secrets value locally +DEPENDABOT_PR_APP_ID=123456 +DEPENDABOT_PR_APP_INSTALLATION_ID=123456 +DEPENDABOT_PR_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n....\n-----END RSA PRIVATE KEY-----" \ No newline at end of file