diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 39a62ca6b..0bb507a61 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:0d39e59663287ae929c1d4ccf8ebf7cef9946826c9b86eda7e85d8d752dbb584 -# created: 2024-11-21T22:39:44.342569463Z + digest: sha256:bdf89cdfb5b791d382184a7a769862b15c38e94e7d82b268c58d40d8952720f2 +# created: 2025-10-03T19:51:38.870830821Z diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 986a53552..2a5e6cc77 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,30 @@ -Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: -- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/nodejs-firestore/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +> Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: + +## Description + +> Please provide a detailed description for the change. +> As much as possible, please try to keep changes separate by purpose. For example, try not to make a one-line bug fix in a feature request, or add an irrelevant README change to a bug fix. + +## Impact + +> What's the impact of this change? + +## Testing + +> Have you added unit and integration tests if necessary? +> Were any tests changed? Are any breaking changes necessary? + +## Additional Information + +> Any additional details that we should be aware of? + +## Checklist + +- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/nodejs-firestore/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass -- [ ] Code coverage does not decrease (if any source code was changed) -- [ ] Appropriate docs were updated (if necessary) +- [ ] Code coverage does not decrease +- [ ] Appropriate docs were updated +- [ ] Appropriate comments were added, particularly in complex areas or places that require background +- [ ] No new warnings or issues will be generated from this change -Fixes # 🦕 +Fixes #issue_number_goes_here 🦕 diff --git a/.github/scripts/close-invalid-link.cjs b/.github/scripts/close-invalid-link.cjs index d7a3688e7..fdb514881 100644 --- a/.github/scripts/close-invalid-link.cjs +++ b/.github/scripts/close-invalid-link.cjs @@ -12,21 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. +const fs = require('fs'); +const yaml = require('js-yaml'); +const path = require('path'); +const TEMPLATE_FILE_PATH = path.resolve(__dirname, '../ISSUE_TEMPLATE/bug_report.yml') + async function closeIssue(github, owner, repo, number) { await github.rest.issues.createComment({ 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,20 +42,32 @@ module.exports = async ({github, context}) => { issue_number: number, }); - const isBugTemplate = issue.data.body.includes('Link to the code that reproduces this issue'); + const yamlData = fs.readFileSync(TEMPLATE_FILE_PATH, 'utf8'); + const obj = yaml.load(yamlData); + const linkMatchingText = (obj.body.find(x => {return x.type === 'input' && x.validations.required === true && x.attributes.label.includes('link')})).attributes.label; + const isBugTemplate = issue.data.body.includes(linkMatchingText); 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.indexOf(linkMatchingText); + if (match !== -1) { + const nextLineIndex = text.indexOf('http', match); + if (nextLineIndex == -1) { + await closeIssue(github, owner, repo, number); + return; + } + 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); } } -}; +}; \ No newline at end of file diff --git a/.github/scripts/close-unresponsive.cjs b/.github/scripts/close-unresponsive.cjs index 142dc1265..6f81b508f 100644 --- a/.github/scripts/close-unresponsive.cjs +++ b/.github/scripts/close-unresponsive.cjs @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +/// 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. @@ -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, + }); } - }; + } +}; \ No newline at end of file diff --git a/.github/scripts/fixtures/invalidIssueBody.txt b/.github/scripts/fixtures/invalidIssueBody.txt new file mode 100644 index 000000000..504bd6690 --- /dev/null +++ b/.github/scripts/fixtures/invalidIssueBody.txt @@ -0,0 +1,50 @@ +### Please make sure you have searched for information in the following guides. + +- [X] Search the issues already opened: https://github.com/GoogleCloudPlatform/google-cloud-node/issues +- [X] Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js +- [X] Check our Troubleshooting guide: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/troubleshooting +- [X] Check our FAQ: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/faq +- [X] Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md +- [X] Check out our authentication guide: https://github.com/googleapis/google-auth-library-nodejs +- [X] Check out handwritten samples for many of our APIs: https://github.com/GoogleCloudPlatform/nodejs-docs-samples + +### A screenshot that you have tested with "Try this API". + + +N/A + +### Link to the code that reproduces this issue. A link to a **public** Github Repository or gist with a minimal reproduction. + +not-a-link + +### A step-by-step description of how to reproduce the issue, based on the linked reproduction. + + +Change MY_PROJECT to your project name, add credentials if needed and run. + +### A clear and concise description of what the bug is, and what you expected to happen. + +The application crashes with the following exception (which there is no way to catch). It should just emit error, and allow graceful handling. +TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object + at _write (node:internal/streams/writable:474:13) + at Writable.write (node:internal/streams/writable:502:10) + at Duplexify._write (/project/node_modules/duplexify/index.js:212:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at Pumpify. (/project/node_modules/@google-cloud/speech/build/src/helpers.js:79:27) + at Object.onceWrapper (node:events:633:26) + at Pumpify.emit (node:events:518:28) + at obj. [as _write] (/project/node_modules/stubs/index.js:28:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at PassThrough.ondata (node:internal/streams/readable:1007:22) + at PassThrough.emit (node:events:518:28) + at addChunk (node:internal/streams/readable:559:12) { + code: 'ERR_INVALID_ARG_TYPE' + + +### A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + +No library should crash an application this way. \ No newline at end of file diff --git a/.github/scripts/fixtures/validIssueBody.txt b/.github/scripts/fixtures/validIssueBody.txt new file mode 100644 index 000000000..6e0ace338 --- /dev/null +++ b/.github/scripts/fixtures/validIssueBody.txt @@ -0,0 +1,50 @@ +### Please make sure you have searched for information in the following guides. + +- [X] Search the issues already opened: https://github.com/GoogleCloudPlatform/google-cloud-node/issues +- [X] Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js +- [X] Check our Troubleshooting guide: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/troubleshooting +- [X] Check our FAQ: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/faq +- [X] Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md +- [X] Check out our authentication guide: https://github.com/googleapis/google-auth-library-nodejs +- [X] Check out handwritten samples for many of our APIs: https://github.com/GoogleCloudPlatform/nodejs-docs-samples + +### A screenshot that you have tested with "Try this API". + + +N/A + +### Link to the code that reproduces this issue. A link to a **public** Github Repository or gist with a minimal reproduction. + +https://gist.github.com/orgads/13cbf44c91923da27d8772b5f10489c9 + +### A step-by-step description of how to reproduce the issue, based on the linked reproduction. + + +Change MY_PROJECT to your project name, add credentials if needed and run. + +### A clear and concise description of what the bug is, and what you expected to happen. + +The application crashes with the following exception (which there is no way to catch). It should just emit error, and allow graceful handling. +TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object + at _write (node:internal/streams/writable:474:13) + at Writable.write (node:internal/streams/writable:502:10) + at Duplexify._write (/project/node_modules/duplexify/index.js:212:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at Pumpify. (/project/node_modules/@google-cloud/speech/build/src/helpers.js:79:27) + at Object.onceWrapper (node:events:633:26) + at Pumpify.emit (node:events:518:28) + at obj. [as _write] (/project/node_modules/stubs/index.js:28:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at PassThrough.ondata (node:internal/streams/readable:1007:22) + at PassThrough.emit (node:events:518:28) + at addChunk (node:internal/streams/readable:559:12) { + code: 'ERR_INVALID_ARG_TYPE' + + +### A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + +No library should crash an application this way. \ No newline at end of file diff --git a/.github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt b/.github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt new file mode 100644 index 000000000..984a420e3 --- /dev/null +++ b/.github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt @@ -0,0 +1,50 @@ +### Please make sure you have searched for information in the following guides. + +- [X] Search the issues already opened: https://github.com/GoogleCloudPlatform/google-cloud-node/issues +- [X] Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js +- [X] Check our Troubleshooting guide: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/troubleshooting +- [X] Check our FAQ: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/faq +- [X] Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md +- [X] Check out our authentication guide: https://github.com/googleapis/google-auth-library-nodejs +- [X] Check out handwritten samples for many of our APIs: https://github.com/GoogleCloudPlatform/nodejs-docs-samples + +### A screenshot that you have tested with "Try this API". + + +N/A + +### A step-by-step description of how to reproduce the issue, based on the linked reproduction. + + +Change MY_PROJECT to your project name, add credentials if needed and run. + +### A clear and concise description of what the bug is, and what you expected to happen. + +The application crashes with the following exception (which there is no way to catch). It should just emit error, and allow graceful handling. +TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object + at _write (node:internal/streams/writable:474:13) + at Writable.write (node:internal/streams/writable:502:10) + at Duplexify._write (/project/node_modules/duplexify/index.js:212:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at Pumpify. (/project/node_modules/@google-cloud/speech/build/src/helpers.js:79:27) + at Object.onceWrapper (node:events:633:26) + at Pumpify.emit (node:events:518:28) + at obj. [as _write] (/project/node_modules/stubs/index.js:28:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at PassThrough.ondata (node:internal/streams/readable:1007:22) + at PassThrough.emit (node:events:518:28) + at addChunk (node:internal/streams/readable:559:12) { + code: 'ERR_INVALID_ARG_TYPE' + +### Link to the code that reproduces this issue. A link to a **public** Github Repository with a minimal reproduction. + + +https://gist.github.com/orgads/13cbf44c91923da27d8772b5f10489c9 + +### A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + +No library should crash an application this way. \ No newline at end of file diff --git a/.github/scripts/remove-response-label.cjs b/.github/scripts/remove-response-label.cjs index 887cf349e..4a784ddf7 100644 --- a/.github/scripts/remove-response-label.cjs +++ b/.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', - }); - } - }; + } +}; \ No newline at end of file diff --git a/.github/scripts/tests/close-invalid-link.test.cjs b/.github/scripts/tests/close-invalid-link.test.cjs new file mode 100644 index 000000000..f63ee89c8 --- /dev/null +++ b/.github/scripts/tests/close-invalid-link.test.cjs @@ -0,0 +1,86 @@ +// 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'); +const sinon = require('sinon'); + +describe('close issues with invalid links', () => { + let octokitStub; + let issuesStub; + + beforeEach(() => { + issuesStub = { + get: sinon.stub(), + createComment: sinon.stub(), + update: sinon.stub(), + }; + octokitStub = { + rest: { + issues: issuesStub, + }, + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('does not do anything if it is not a bug', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: "I'm having a problem with this." } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.createComment); + sinon.assert.notCalled(issuesStub.update); + }); + + 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 } }; + issuesStub.get.resolves({ data: { body: fs.readFileSync('./fixtures/validIssueBody.txt', 'utf-8') } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.createComment); + sinon.assert.notCalled(issuesStub.update); + }); + + 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 } }; + issuesStub.get.resolves({ data: { body: fs.readFileSync('./fixtures/validIssueBodyDifferentLinkLocation.txt', 'utf-8') } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.createComment); + sinon.assert.notCalled(issuesStub.update); + }); + + it('closes the issue if the link is invalid', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: fs.readFileSync('./fixtures/invalidIssueBody.txt', 'utf-8') } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.calledOnce(issuesStub.createComment); + sinon.assert.calledOnce(issuesStub.update); + }); +}); \ No newline at end of file diff --git a/.github/scripts/tests/close-or-remove-response-label.test.cjs b/.github/scripts/tests/close-or-remove-response-label.test.cjs new file mode 100644 index 000000000..fb092c536 --- /dev/null +++ b/.github/scripts/tests/close-or-remove-response-label.test.cjs @@ -0,0 +1,109 @@ +// 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, beforeEach, afterEach } = require('mocha'); +const removeResponseLabel = require('../remove-response-label.cjs'); +const closeUnresponsive = require('../close-unresponsive.cjs'); +const sinon = require('sinon'); + +function getISODateDaysAgo(days) { + const today = new Date(); + const daysAgo = new Date(today.setDate(today.getDate() - days)); + return daysAgo.toISOString(); +} + +describe('close issues or remove needs more info labels', () => { + let octokitStub; + let issuesStub; + let paginateStub; + + beforeEach(() => { + issuesStub = { + listForRepo: sinon.stub(), + update: sinon.stub(), + createComment: sinon.stub(), + get: sinon.stub(), + removeLabel: sinon.stub(), + }; + paginateStub = sinon.stub(); + octokitStub = { + rest: { + issues: issuesStub, + }, + paginate: paginateStub, + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + 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) }]; + + issuesStub.listForRepo.resolves({ data: issuesInRepo }); + paginateStub.resolves(eventsInIssue); + + await closeUnresponsive({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.listForRepo); + sinon.assert.calledOnce(paginateStub); + sinon.assert.calledOnce(issuesStub.update); + sinon.assert.calledOnce(issuesStub.createComment); + }); + + 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) }]; + + issuesStub.listForRepo.resolves({ data: issuesInRepo }); + paginateStub.resolves(eventsInIssue); + + await closeUnresponsive({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.listForRepo); + sinon.assert.calledOnce(paginateStub); + sinon.assert.notCalled(issuesStub.update); + sinon.assert.notCalled(issuesStub.createComment); + }); + + it('removes the label if OP responded', async () => { + const context = { actor: 'OP', repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + const issueContext = { user: {login: 'OP'}, labels: [{ name: 'needs more info' }] }; + + issuesStub.get.resolves({ data: issueContext }); + + await removeResponseLabel({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.calledOnce(issuesStub.removeLabel); + }); + + 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: {login: 'OP'}, labels: [{ name: 'needs more info' }] }; + + issuesStub.get.resolves({ data: issueContext }); + + await removeResponseLabel({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.removeLabel); + }); +}); \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e20759835..c5c64ab05 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,9 +9,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [14, 16, 18] + node: [14, 16, 18, 20] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -26,10 +26,24 @@ jobs: - run: npm test env: MOCHA_THROW_DEPRECATION: false + test-script: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - run: node --version + - run: npm install --engine-strict + working-directory: .github/scripts + - run: npm test + working-directory: .github/scripts + env: + MOCHA_THROW_DEPRECATION: false windows: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -40,7 +54,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -49,7 +63,7 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 442a46bcc..531054022 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -10,7 +10,12 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - run: npm install + working-directory: ./.github/scripts - uses: actions/github-script@v7 with: script: | diff --git a/.github/workflows/response.yaml b/.github/workflows/response.yaml index 6ed37326f..e81a3603a 100644 --- a/.github/workflows/response.yaml +++ b/.github/workflows/response.yaml @@ -13,7 +13,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/github-script@v7 with: script: | @@ -27,7 +27,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/github-script@v7 with: script: | diff --git a/README.md b/README.md index e4a568405..0742ec880 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # [Cloud Firestore: Node.js Client](https://github.com/googleapis/nodejs-firestore) [![release level](https://img.shields.io/badge/release%20level-stable-brightgreen.svg?style=flat)](https://cloud.google.com/terms/launch-stages) -[![npm version](https://img.shields.io/npm/v/@google-cloud/firestore.svg)](https://www.npmjs.org/package/@google-cloud/firestore) +[![npm version](https://img.shields.io/npm/v/@google-cloud/firestore.svg)](https://www.npmjs.com/package/@google-cloud/firestore) diff --git a/renovate.json b/renovate.json index c5c702cf4..f39fd3232 100644 --- a/renovate.json +++ b/renovate.json @@ -15,6 +15,10 @@ { "extends": "packages:linters", "groupName": "linters" + }, + { + "matchManagers": ["github-actions"], + "enabled": false } ], "ignoreDeps": ["typescript"]