From 0883f0f9b92227155a05bd8df6cb97af84bbd89d Mon Sep 17 00:00:00 2001 From: Sofia Leon Date: Tue, 14 Jan 2025 19:45:03 -0800 Subject: [PATCH] chore: add tests to template automation and clean up templating logic --- .../.github/scripts/close-invalid-link.cjs | 27 +++-- .../scripts/close-invalid-link.test.cjs | 45 ++++++++ .../close-or-remove-response-label.test.cjs | 57 ++++++++++ .../.github/scripts/close-unresponsive.cjs | 106 +++++++++--------- .../node_library/.github/scripts/package.json | 17 +++ .../.github/scripts/remove-response-label.cjs | 28 ++--- .../node_library/.github/workflows/ci.yaml | 14 +++ 7 files changed, 216 insertions(+), 78 deletions(-) create mode 100644 synthtool/gcp/templates/node_library/.github/scripts/close-invalid-link.test.cjs create mode 100644 synthtool/gcp/templates/node_library/.github/scripts/close-or-remove-response-label.test.cjs create mode 100644 synthtool/gcp/templates/node_library/.github/scripts/package.json diff --git a/synthtool/gcp/templates/node_library/.github/scripts/close-invalid-link.cjs b/synthtool/gcp/templates/node_library/.github/scripts/close-invalid-link.cjs index d7a3688e7..97854277a 100644 --- a/synthtool/gcp/templates/node_library/.github/scripts/close-invalid-link.cjs +++ b/synthtool/gcp/templates/node_library/.github/scripts/close-invalid-link.cjs @@ -17,16 +17,16 @@ async function closeIssue(github, owner, repo, number) { owner: owner, repo: repo, issue_number: number, - body: 'Issue was opened with an invalid reproduction link. Please make sure the repository is a valid, publicly-accessible github repository, and make sure the url is complete (example: https://github.com/googleapis/google-cloud-node)' + body: "Issue was opened with an invalid reproduction link. Please make sure the repository is a valid, publicly-accessible github repository, and make sure the url is complete (example: https://github.com/googleapis/google-cloud-node)" }); await github.rest.issues.update({ owner: owner, repo: repo, issue_number: number, - state: 'closed' + state: "closed" }); } -module.exports = async ({github, context}) => { +module.exports = async ({ github, context }) => { const owner = context.repo.owner; const repo = context.repo.repo; const number = context.issue.number; @@ -37,18 +37,23 @@ module.exports = async ({github, context}) => { issue_number: number, }); - const isBugTemplate = issue.data.body.includes('Link to the code that reproduces this issue'); + const isBugTemplate = issue.data.body.includes("Link to the code that reproduces this issue"); if (isBugTemplate) { console.log(`Issue ${number} is a bug template`) try { - const link = issue.data.body.split('\n')[18].match(/(https?:\/\/(gist\.)?github.com\/.*)/)[0]; - console.log(`Issue ${number} contains this link: ${link}`) - const isValidLink = (await fetch(link)).ok; - console.log(`Issue ${number} has a ${isValidLink ? 'valid' : 'invalid'} link`) - if (!isValidLink) { - await closeIssue(github, owner, repo, number); - } + const text = issue.data.body; + const match = text.match(/Link to the code that reproduces this issue. A link to a \*\*public\*\* Github Repository with a minimal reproduction/); + if (match) { + const nextLineIndex = text.indexOf('http', match.index); + const link = text.substring(nextLineIndex, text.indexOf('\n', nextLineIndex)); + console.log(`Issue ${number} contains this link: ${link}`); + const isValidLink = (await fetch(link)).ok; + console.log(`Issue ${number} has a ${isValidLink ? "valid" : "invalid"} link`) + if (!isValidLink) { + await closeIssue(github, owner, repo, number); + } + } } catch (err) { await closeIssue(github, owner, repo, number); } diff --git a/synthtool/gcp/templates/node_library/.github/scripts/close-invalid-link.test.cjs b/synthtool/gcp/templates/node_library/.github/scripts/close-invalid-link.test.cjs new file mode 100644 index 000000000..9842c0471 --- /dev/null +++ b/synthtool/gcp/templates/node_library/.github/scripts/close-invalid-link.test.cjs @@ -0,0 +1,45 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const {describe, it} = require('mocha'); +const closeInvalidLink = require('./close-invalid-link.cjs'); +const fs = require('fs'); + +describe('Quickstart', () => { + it('does not do anything if it is not a bug', async () => { + const context = {repo: {owner: 'testOrg', repo: 'testRepo'}, issue: {number: 1}} + const octokit = {rest: {issues: {get: () => {return {data: {body: "I'm having a problem with this."}}}}}}; + await closeInvalidLink({github: octokit, context}); + }); + + it('does not do anything if it is a bug with an appropriate link', async () => { + const context = {repo: {owner: 'testOrg', repo: 'testRepo'}, issue: {number: 1}} + const octokit = {rest: {issues: {get: () => {return {data: {body: fs.readFileSync('./fixtures/validIssueBody.txt', 'utf-8')}}}}}}; + await closeInvalidLink({github: octokit, context}); + }); + + it('does not do anything if it is a bug with an appropriate link and the template changes', async () => { + const context = {repo: {owner: 'testOrg', repo: 'testRepo'}, issue: {number: 1}} + const octokit = {rest: {issues: {get: () => {return {data: {body: fs.readFileSync('./fixtures/validIssueBodyDifferentLinkLocation.txt', 'utf-8')}}}}}}; + await closeInvalidLink({github: octokit, context}); + }); + + it('closes the issue if the link is invalid', async () => { + const context = {repo: {owner: 'testOrg', repo: 'testRepo'}, issue: {number: 1}} + const octokit = {rest: {issues: {get: () => {return {data: {body: fs.readFileSync('./fixtures/invalidIssueBody.txt', 'utf-8')}}}, createComment: () => {return}, update: () => {return}}}}; + await closeInvalidLink({github: octokit, context}); + }); +}); diff --git a/synthtool/gcp/templates/node_library/.github/scripts/close-or-remove-response-label.test.cjs b/synthtool/gcp/templates/node_library/.github/scripts/close-or-remove-response-label.test.cjs new file mode 100644 index 000000000..f2bd369de --- /dev/null +++ b/synthtool/gcp/templates/node_library/.github/scripts/close-or-remove-response-label.test.cjs @@ -0,0 +1,57 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const {describe, it} = require('mocha'); +const removeResponseLabel = require('./remove-response-label.cjs'); +const closeUnresponsive = require('./close-unresponsive.cjs'); + +function getISODateDaysAgo(days) { + const today = new Date(); + const daysAgo = new Date(today.setDate(today.getDate() - days)); + return daysAgo.toISOString(); +} + +describe('Quickstart', () => { + it('closes the issue if the OP has not responded within the allotted time and there is a needs-more-info label', async () => { + const context = {owner: 'testOrg', repo: 'testRepo'} + const issuesInRepo = [{user: {login: 'OP'}, labels: [{name: 'needs more info'}]}] + const eventsInIssue = [{event: 'labeled', label: {name: 'needs more info'}, created_at: getISODateDaysAgo(16)}]; + const octokit = {rest: {issues: {listForRepo: () => {return {data: issuesInRepo}}, update: () => {return}, createComment: () => {return}}}, paginate: () => {return eventsInIssue}}; + await closeUnresponsive({github: octokit, context}); + }); + + it('does nothing if not enough time has passed and there is a needs-more-info label', async () => { + const context = {owner: 'testOrg', repo: 'testRepo'} + const issuesInRepo = [{user: {login: 'OP'}, labels: [{name: 'needs more info'}]}] + const eventsInIssue = [{event: 'labeled', label: {name: 'needs more info'}, created_at: getISODateDaysAgo(14)}]; + const octokit = {rest: {issues: {listForRepo: () => {return {data: issuesInRepo}}}}, paginate: () => {return eventsInIssue}}; + await closeUnresponsive({github: octokit, context}); + }); + + it('removes the label if OP responded', async () => { + const context = {actor: 'OP', repo: {owner: 'testOrg', repo: 'testRepo'}, issue: {number: 1}}; + const issueContext = {user: 'OP', login: 'OP', labels: [{name: 'needs more info'}]}; + const octokit = {rest: {issues: {get: () => {return {data: issueContext}}, removeLabel: () => {return}}}}; + await removeResponseLabel({github: octokit, context}); + }); + + it('does not remove the label if author responded', async () => { + const context = {actor: 'repo-maintainer', repo: {owner: 'testOrg', repo: 'testRepo'}, issue: {number: 1}}; + const issueContext = {user: 'OP', login: 'OP', labels: [{name: 'needs more info'}]}; + const octokit = {rest: {issues: {get: () => {return {data: issueContext}}}}}; + await removeResponseLabel({github: octokit, context}); + }); +}); diff --git a/synthtool/gcp/templates/node_library/.github/scripts/close-unresponsive.cjs b/synthtool/gcp/templates/node_library/.github/scripts/close-unresponsive.cjs index 142dc1265..0081b7a21 100644 --- a/synthtool/gcp/templates/node_library/.github/scripts/close-unresponsive.cjs +++ b/synthtool/gcp/templates/node_library/.github/scripts/close-unresponsive.cjs @@ -13,57 +13,57 @@ // limitations under the License. function labeledEvent(data) { - return data.event === 'labeled' && data.label.name === 'needs more info'; - } - - const numberOfDaysLimit = 15; - const close_message = `This has been closed since a request for information has \ - not been answered for ${numberOfDaysLimit} days. It can be reopened when the \ - requested information is provided.`; - - module.exports = async ({github, context}) => { - const owner = context.repo.owner; - const repo = context.repo.repo; - - const issues = await github.rest.issues.listForRepo({ - owner: owner, - repo: repo, - labels: 'needs more info', - }); - const numbers = issues.data.map((e) => e.number); - - for (const number of numbers) { - const events = await github.paginate( - github.rest.issues.listEventsForTimeline, - { - owner: owner, - repo: repo, - issue_number: number, - }, - (response) => response.data.filter(labeledEvent) - ); - - const latest_response_label = events[events.length - 1]; - - const created_at = new Date(latest_response_label.created_at); - const now = new Date(); - const diff = now - created_at; - const diffDays = diff / (1000 * 60 * 60 * 24); - - if (diffDays > numberOfDaysLimit) { - await github.rest.issues.update({ - owner: owner, - repo: repo, - issue_number: number, - state: 'closed', - }); - - await github.rest.issues.createComment({ - owner: owner, - repo: repo, - issue_number: number, - body: close_message, - }); - } + return data.event === "labeled" && data.label.name === "needs more info"; +} + +const numberOfDaysLimit = 15; +const close_message = `This has been closed since a request for information has \ +not been answered for ${numberOfDaysLimit} days. It can be reopened when the \ +requested information is provided.`; + +module.exports = async ({ github, context }) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + + const issues = await github.rest.issues.listForRepo({ + owner: owner, + repo: repo, + labels: "needs more info", + }); + const numbers = issues.data.map((e) => e.number); + + for (const number of numbers) { + const events = await github.paginate( + github.rest.issues.listEventsForTimeline, + { + owner: owner, + repo: repo, + issue_number: number, + }, + (response) => response.data.filter(labeledEvent) + ); + + const latest_response_label = events[events.length - 1]; + + const created_at = new Date(latest_response_label.created_at); + const now = new Date(); + const diff = now - created_at; + const diffDays = diff / (1000 * 60 * 60 * 24); + + if (diffDays > numberOfDaysLimit) { + await github.rest.issues.update({ + owner: owner, + repo: repo, + issue_number: number, + state: "closed", + }); + + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: number, + body: close_message, + }); } - }; + } +}; diff --git a/synthtool/gcp/templates/node_library/.github/scripts/package.json b/synthtool/gcp/templates/node_library/.github/scripts/package.json new file mode 100644 index 000000000..fe3a336a2 --- /dev/null +++ b/synthtool/gcp/templates/node_library/.github/scripts/package.json @@ -0,0 +1,17 @@ +{ + "name": "tests", + "private": true, + "description": "tests for script", + "scripts": { + "test": "mocha close-invalid-link.test.cjs && mocha close-or-remove-response-label.test.cjs" + }, + "author": "Google Inc.", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "devDependencies": { + "@octokit/rest": "^21.0.2", + "mocha": "^11.0.1" + } +} diff --git a/synthtool/gcp/templates/node_library/.github/scripts/remove-response-label.cjs b/synthtool/gcp/templates/node_library/.github/scripts/remove-response-label.cjs index 887cf349e..70db8408a 100644 --- a/synthtool/gcp/templates/node_library/.github/scripts/remove-response-label.cjs +++ b/synthtool/gcp/templates/node_library/.github/scripts/remove-response-label.cjs @@ -13,21 +13,21 @@ // limitations under the License. module.exports = async ({ github, context }) => { - const commenter = context.actor; - const issue = await github.rest.issues.get({ + const commenter = context.actor; + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const author = issue.data.user.login; + const labels = issue.data.labels.map((e) => e.name); + + if (author === commenter && labels.includes("needs more info")) { + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, + name: "needs more info", }); - const author = issue.data.user.login; - const labels = issue.data.labels.map((e) => e.name); - - if (author === commenter && labels.includes('needs more info')) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name: 'needs more info', - }); - } - }; + } +}; diff --git a/synthtool/gcp/templates/node_library/.github/workflows/ci.yaml b/synthtool/gcp/templates/node_library/.github/workflows/ci.yaml index e5dbe6108..18015524d 100644 --- a/synthtool/gcp/templates/node_library/.github/workflows/ci.yaml +++ b/synthtool/gcp/templates/node_library/.github/workflows/ci.yaml @@ -26,6 +26,18 @@ jobs: - run: npm test env: MOCHA_THROW_DEPRECATION: false + test-script: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - run: node --version + - run: npm install --engine-strict + - run: npm test + env: + MOCHA_THROW_DEPRECATION: false windows: runs-on: windows-latest steps: @@ -34,7 +46,9 @@ jobs: with: node-version: 18 - run: npm install --engine-strict + working-directory: .github/scripts - run: npm test + working-directory: .github/scripts env: MOCHA_THROW_DEPRECATION: false lint: