diff --git a/.github/workflows/generation_check.yaml b/.github/workflows/generation_check.yaml new file mode 100644 index 000000000000..4e8c12451f2f --- /dev/null +++ b/.github/workflows/generation_check.yaml @@ -0,0 +1,75 @@ +--- +name: generation-check + +on: + push: + branches: + - main + pull_request: + # Allows manual triggering for debugging purpose. + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read # Read repository contents + +jobs: + regeneration: + name: Regenerate and verify clean git tree + runs-on: ubuntu-latest + permissions: + contents: read # Read repository contents + issues: write # Required for creating issue on main branch failure + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + librarian: + - 'librarian.yaml' + # Version of librarian is pulled from librarian.yaml, + # action is pinned to commit SHA to satisfy blanket security policy. + - uses: googleapis/librarian@35997441eafc2b02716804f9baba1e3f04f8a44f # v0.31.1 + if: steps.changes.outputs.librarian == 'true' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + - name: Use Node.js 22 + if: steps.changes.outputs.librarian == 'true' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 22 + - name: Setup pnpm + if: steps.changes.outputs.librarian == 'true' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: ^7.0.0 + - name: Install tools + if: steps.changes.outputs.librarian == 'true' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + run: librarian install + - name: Regenerate + if: steps.changes.outputs.librarian == 'true' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + run: | + librarian generate --all + if [ -n "$(git status --porcelain)" ]; then + git status + echo "==================== GIT DIFF ====================" + git diff + echo "==================================================" + echo "Regeneration failed. Please run 'librarian generate --all' to update the generated files." + exit 1 + fi + - name: Create issue if previous step fails + if: ${{ failure() && github.ref == 'refs/heads/main' }} + uses: googleapis/librarian/.github/actions/create-issue-on-failure@35997441eafc2b02716804f9baba1e3f04f8a44f # v0.31.1 + with: + title: "Librarian generate diff check failed on main branch" + body: | + The librarian generate diff check failed on main branch. + + To keep the `main` branch healthy, please consider **reverting the triggering change** first. Once the revert is merged, you can investigate the failure and submit a new PR with the fix. + + Please check the logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/librarian_tidy.yml b/.github/workflows/librarian_tidy.yml new file mode 100644 index 000000000000..e830d1d3f187 --- /dev/null +++ b/.github/workflows/librarian_tidy.yml @@ -0,0 +1,47 @@ +name: librarian tidy + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + tidy-check: + name: Verify librarian.yaml is tidy + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + librarian: + - 'librarian.yaml' + # Version of librarian is pulled from librarian.yaml, + # action is pinned to commit SHA to satisfy blanket security policy. + - uses: googleapis/librarian@35997441eafc2b02716804f9baba1e3f04f8a44f # v0.31.1 + + - name: Run librarian tidy + if: steps.changes.outputs.librarian == 'true' + run: librarian tidy + + - name: Check for diff + if: steps.changes.outputs.librarian == 'true' + run: | + if ! git diff --exit-code; then + V=$(librarian config get version 2>/dev/null || echo "latest") + echo "librarian.yaml is not tidy. Please run:" + echo "" + echo " librarian tidy" + echo "" + echo "to tidy librarian.yaml and commit the changes." + exit 1 + fi diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1986fffc0f38..48aef931009f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,15 +2,15 @@ "core/common": "8.0.0", "core/dev-packages/jsdoc-fresh": "6.0.0", "core/dev-packages/jsdoc-region-tag": "5.0.0", - "core/dev-packages/pack-n-play": "5.0.0", + "core/dev-packages/pack-n-play": "5.0.1", "core/generator/gapic-generator-typescript": "5.0.0", "core/packages/gapic-node-processing": "0.2.0", "core/packages/gax": "6.0.0", "core/packages/gaxios": "7.3.0", - "core/packages/gcp-metadata": "9.0.0", + "core/packages/gcp-metadata": "9.0.1", "core/packages/google-auth-library-nodejs": "11.0.0", "core/packages/logging-utils": "2.0.0", - "core/packages/nodejs-googleapis-common": "9.0.0", + "core/packages/nodejs-googleapis-common": "9.0.1", "core/packages/nodejs-proto-files": "6.0.0", "core/packages/proto3-json-serializer-nodejs": "4.0.0", "core/packages/retry-request": "9.0.0", @@ -21,9 +21,9 @@ "core/precise-date": "6.0.0", "core/projectify": "6.0.0", "core/promisify": "6.0.0", - "handwritten/bigquery": "9.0.0", + "handwritten/bigquery": "9.0.1", "handwritten/bigquery-storage": "6.0.0", - "handwritten/bigtable": "7.0.0", + "handwritten/bigtable": "7.1.0", "handwritten/cloud-profiler": "8.0.0", "handwritten/datastore": "11.0.0", "handwritten/error-reporting": "4.0.0", @@ -104,7 +104,7 @@ "packages/google-cloud-connectors": "0.6.0", "packages/google-cloud-contactcenterinsights": "5.0.0", "packages/google-cloud-contentwarehouse": "3.0.0", - "packages/google-cloud-databasecenter": "0.4.0", + "packages/google-cloud-databasecenter": "0.4.1", "packages/google-cloud-datacatalog": "6.0.0", "packages/google-cloud-datacatalog-lineage": "3.0.0", "packages/google-cloud-datacatalog-lineage-configmanagement": "0.2.0", diff --git a/changelog.json b/changelog.json index f4dc6b25538c..a9e0bf8919e6 100644 --- a/changelog.json +++ b/changelog.json @@ -1,6 +1,113 @@ { "repository": "googleapis/google-cloud-node", "entries": [ + { + "changes": [ + { + "type": "fix", + "sha": "23aee114a947a29f31977aab8de9c1c7ecd62188", + "message": "Upgrade types sinon for databasecenter", + "issues": [ + "9098" + ] + } + ], + "version": "0.4.1", + "language": "JAVASCRIPT", + "artifactName": "@google-cloud/databasecenter", + "id": "c0b55b44-2b73-457b-9e25-8d86ece8b512", + "createTime": "2026-08-05T21:09:31.314Z" + }, + { + "changes": [ + { + "type": "feat", + "sha": "f63f914e69106f581448837f5ee212ab71f85775", + "message": "Split gapic and proto usage from the handwritten library into bigtable-api", + "issues": [ + "8934" + ], + "scope": "bigtable" + } + ], + "version": "7.1.0", + "language": "JAVASCRIPT", + "artifactName": "@google-cloud/bigtable", + "id": "dc784ab8-295a-4e02-8261-f7935abb2788", + "createTime": "2026-08-05T21:09:31.294Z" + }, + { + "changes": [ + { + "type": "fix", + "sha": "7f8e23c9652e34869c2fafe54f63cc63ac9acaa8", + "message": "Remove owlbot configs", + "issues": [ + "8936" + ], + "scope": "bigquery" + } + ], + "version": "9.0.1", + "language": "JAVASCRIPT", + "artifactName": "@google-cloud/bigquery", + "id": "de1dae99-d138-451f-a0db-e716e0003958", + "createTime": "2026-08-05T21:09:31.273Z" + }, + { + "changes": [ + { + "type": "fix", + "sha": "340364f8fab67adaf4e684fb3241de499394bf6b", + "message": "Discovery url handling, error code review helper, and pack-n-play test timeouts", + "issues": [ + "9061" + ], + "scope": "core" + } + ], + "version": "9.0.1", + "language": "JAVASCRIPT", + "artifactName": "googleapis-common", + "id": "2d37b8dd-1417-4f37-ae57-18b166ea395a", + "createTime": "2026-08-05T21:09:31.248Z" + }, + { + "changes": [ + { + "type": "fix", + "sha": "340364f8fab67adaf4e684fb3241de499394bf6b", + "message": "Discovery url handling, error code review helper, and pack-n-play test timeouts", + "issues": [ + "9061" + ], + "scope": "core" + } + ], + "version": "9.0.1", + "language": "JAVASCRIPT", + "artifactName": "gcp-metadata", + "id": "9eda0e2d-73dd-410e-a223-df0621e697d6", + "createTime": "2026-08-05T21:09:31.228Z" + }, + { + "changes": [ + { + "type": "fix", + "sha": "340364f8fab67adaf4e684fb3241de499394bf6b", + "message": "Discovery url handling, error code review helper, and pack-n-play test timeouts", + "issues": [ + "9061" + ], + "scope": "core" + } + ], + "version": "5.0.1", + "language": "JAVASCRIPT", + "artifactName": "pack-n-play", + "id": "80b6e996-31bf-482f-8d5c-02220564552f", + "createTime": "2026-08-05T21:09:31.200Z" + }, { "changes": [ { @@ -88682,5 +88789,5 @@ "createTime": "2023-01-28T04:18:24.718Z" } ], - "updateTime": "2026-08-04T23:49:33.509Z" + "updateTime": "2026-08-05T21:09:31.314Z" } \ No newline at end of file diff --git a/core/dev-packages/pack-n-play/CHANGELOG.md b/core/dev-packages/pack-n-play/CHANGELOG.md index cbfa24ef4098..ff9c3f04cd3c 100644 --- a/core/dev-packages/pack-n-play/CHANGELOG.md +++ b/core/dev-packages/pack-n-play/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog +## [5.0.1](https://github.com/googleapis/google-cloud-node/compare/pack-n-play-v5.0.0...pack-n-play-v5.0.1) (2026-08-05) + + +### Bug Fixes + +* **core:** Discovery url handling, error code review helper, and pack-n-play test timeouts ([#9061](https://github.com/googleapis/google-cloud-node/issues/9061)) ([340364f](https://github.com/googleapis/google-cloud-node/commit/340364f8fab67adaf4e684fb3241de499394bf6b)) + ## [5.0.0](https://github.com/googleapis/google-cloud-node/compare/pack-n-play-v4.2.3...pack-n-play-v5.0.0) (2026-07-29) diff --git a/core/dev-packages/pack-n-play/package.json b/core/dev-packages/pack-n-play/package.json index ad6dc1a6938b..8c52b49721cd 100644 --- a/core/dev-packages/pack-n-play/package.json +++ b/core/dev-packages/pack-n-play/package.json @@ -1,7 +1,7 @@ { "name": "pack-n-play", "description": "Ensure your library installs and compiles", - "version": "5.0.0", + "version": "5.0.1", "author": "Google LLC", "license": "Apache-2.0", "repository": { diff --git a/core/packages/gcp-metadata/CHANGELOG.md b/core/packages/gcp-metadata/CHANGELOG.md index ff0063d2f2a9..4ceb65011936 100644 --- a/core/packages/gcp-metadata/CHANGELOG.md +++ b/core/packages/gcp-metadata/CHANGELOG.md @@ -5,6 +5,13 @@ [1]: https://www.npmjs.com/package/gcp-metadata?activeTab=versions +## [9.0.1](https://github.com/googleapis/google-cloud-node/compare/gcp-metadata-v9.0.0...gcp-metadata-v9.0.1) (2026-08-05) + + +### Bug Fixes + +* **core:** Discovery url handling, error code review helper, and pack-n-play test timeouts ([#9061](https://github.com/googleapis/google-cloud-node/issues/9061)) ([340364f](https://github.com/googleapis/google-cloud-node/commit/340364f8fab67adaf4e684fb3241de499394bf6b)) + ## [9.0.0](https://github.com/googleapis/google-cloud-node/compare/gcp-metadata-v8.1.4...gcp-metadata-v9.0.0) (2026-07-29) diff --git a/core/packages/gcp-metadata/package.json b/core/packages/gcp-metadata/package.json index 8b26fc8f3ae1..78eb4d77cc95 100644 --- a/core/packages/gcp-metadata/package.json +++ b/core/packages/gcp-metadata/package.json @@ -1,6 +1,6 @@ { "name": "gcp-metadata", - "version": "9.0.0", + "version": "9.0.1", "description": "Get the metadata from a Google Cloud Platform environment", "repository": { "type": "git", diff --git a/core/packages/gcp-metadata/samples/package.json b/core/packages/gcp-metadata/samples/package.json index 5790a1b71927..a7a0992b436f 100644 --- a/core/packages/gcp-metadata/samples/package.json +++ b/core/packages/gcp-metadata/samples/package.json @@ -14,7 +14,7 @@ "test": "mocha" }, "dependencies": { - "gcp-metadata": "^9.0.0" + "gcp-metadata": "^9.0.1" }, "devDependencies": { "chai": "^4.2.0", diff --git a/core/packages/nodejs-googleapis-common/CHANGELOG.md b/core/packages/nodejs-googleapis-common/CHANGELOG.md index 511e86585498..cf43f81713c0 100644 --- a/core/packages/nodejs-googleapis-common/CHANGELOG.md +++ b/core/packages/nodejs-googleapis-common/CHANGELOG.md @@ -5,6 +5,13 @@ [1]: https://www.npmjs.com/package/nodejs-googleapis-common?activeTab=versions +## [9.0.1](https://github.com/googleapis/google-cloud-node/compare/googleapis-common-v9.0.0...googleapis-common-v9.0.1) (2026-08-05) + + +### Bug Fixes + +* **core:** Discovery url handling, error code review helper, and pack-n-play test timeouts ([#9061](https://github.com/googleapis/google-cloud-node/issues/9061)) ([340364f](https://github.com/googleapis/google-cloud-node/commit/340364f8fab67adaf4e684fb3241de499394bf6b)) + ## [9.0.0](https://github.com/googleapis/google-cloud-node/compare/googleapis-common-v8.0.3...googleapis-common-v9.0.0) (2026-07-29) diff --git a/core/packages/nodejs-googleapis-common/package.json b/core/packages/nodejs-googleapis-common/package.json index 082b61bf7942..2ad882e902af 100644 --- a/core/packages/nodejs-googleapis-common/package.json +++ b/core/packages/nodejs-googleapis-common/package.json @@ -1,6 +1,6 @@ { "name": "googleapis-common", - "version": "9.0.0", + "version": "9.0.1", "description": "A common tooling library used by the googleapis npm module. You probably don't want to use this directly.", "repository": { "type": "git", diff --git a/core/packages/nodejs-googleapis-common/samples/package.json b/core/packages/nodejs-googleapis-common/samples/package.json index 192850261c50..07446f01ac9f 100644 --- a/core/packages/nodejs-googleapis-common/samples/package.json +++ b/core/packages/nodejs-googleapis-common/samples/package.json @@ -15,7 +15,7 @@ "test": "echo \"There are no sample tests 👻\"" }, "dependencies": { - "googleapis-common": "^9.0.0" + "googleapis-common": "^9.0.1" }, "devDependencies": { "mocha": "^8.0.0" diff --git a/handwritten/bigquery/CHANGELOG.md b/handwritten/bigquery/CHANGELOG.md index af977317129a..12b796fd7bec 100644 --- a/handwritten/bigquery/CHANGELOG.md +++ b/handwritten/bigquery/CHANGELOG.md @@ -5,6 +5,13 @@ [1]: https://www.npmjs.com/package/@google-cloud/bigquery?activeTab=versions +## [9.0.1](https://github.com/googleapis/google-cloud-node/compare/bigquery-v9.0.0...bigquery-v9.0.1) (2026-08-05) + + +### Bug Fixes + +* **bigquery:** Remove owlbot configs ([#8936](https://github.com/googleapis/google-cloud-node/issues/8936)) ([7f8e23c](https://github.com/googleapis/google-cloud-node/commit/7f8e23c9652e34869c2fafe54f63cc63ac9acaa8)) + ## [9.0.0](https://github.com/googleapis/google-cloud-node/compare/bigquery-v8.3.1...bigquery-v9.0.0) (2026-08-04) diff --git a/handwritten/bigquery/package.json b/handwritten/bigquery/package.json index b0d97aa060c1..8c08afd9d8d2 100644 --- a/handwritten/bigquery/package.json +++ b/handwritten/bigquery/package.json @@ -1,7 +1,7 @@ { "name": "@google-cloud/bigquery", "description": "Google BigQuery Client Library for Node.js", - "version": "9.0.0", + "version": "9.0.1", "license": "Apache-2.0", "author": "Google LLC", "engines": { diff --git a/handwritten/bigtable/CHANGELOG.md b/handwritten/bigtable/CHANGELOG.md index 2acbd3b45145..003c12d67c79 100644 --- a/handwritten/bigtable/CHANGELOG.md +++ b/handwritten/bigtable/CHANGELOG.md @@ -5,6 +5,13 @@ [1]: https://www.npmjs.com/package/@google-cloud/bigtable?activeTab=versions +## [7.1.0](https://github.com/googleapis/google-cloud-node/compare/bigtable-v7.0.0...bigtable-v7.1.0) (2026-08-05) + + +### Features + +* **bigtable:** Split gapic and proto usage from the handwritten library into bigtable-api ([#8934](https://github.com/googleapis/google-cloud-node/issues/8934)) ([f63f914](https://github.com/googleapis/google-cloud-node/commit/f63f914e69106f581448837f5ee212ab71f85775)) + ## [7.0.0](https://github.com/googleapis/google-cloud-node/compare/bigtable-v6.5.1...bigtable-v7.0.0) (2026-08-04) diff --git a/handwritten/bigtable/package.json b/handwritten/bigtable/package.json index 85d6d08be38e..7c5baeebd273 100644 --- a/handwritten/bigtable/package.json +++ b/handwritten/bigtable/package.json @@ -1,6 +1,6 @@ { "name": "@google-cloud/bigtable", - "version": "7.0.0", + "version": "7.1.0", "description": "Cloud Bigtable Client Library for Node.js", "keywords": [ "bigtable", diff --git a/handwritten/storage/package.json b/handwritten/storage/package.json index ed2c26993026..7f71363f4105 100644 --- a/handwritten/storage/package.json +++ b/handwritten/storage/package.json @@ -54,6 +54,7 @@ "compile:cjs": "tsc -p ./tsconfig.cjs.json", "compile:esm": "tsc -p .", "compile": "npm run compile:cjs && npm run compile:esm", + "compile:sourcemaps": "npm run compile:cjs -- --sourceMap && npm run compile:esm -- --sourceMap", "conformance-test": "mocha --parallel build/cjs/conformance-test/ --require build/cjs/conformance-test/globalHooks.js", "docs": "jsdoc -c .jsdoc.json", "fix": "gts fix", @@ -61,13 +62,13 @@ "postcompile": "cp ./src/package-json-helper.cjs ./build/cjs/src && cp ./src/package-json-helper.cjs ./build/esm/src", "postcompile:cjs": "babel --plugins gapic-tools/build/src/replaceImportMetaUrl,gapic-tools/build/src/toggleESMFlagVariable build/cjs/src/util.js -o build/cjs/src/util.js && cp internal-tooling/helpers/package.cjs.json build/cjs/package.json", "precompile": "rm -rf build/", - "preconformance-test": "npm run compile:cjs -- --sourceMap", - "predocs": "npm run compile:cjs -- --sourceMap", + "preconformance-test": "npm run compile:sourcemaps", + "predocs": "npm run compile:sourcemaps", "prelint": "cd samples; npm link ../; npm install", "prepare": "npm run compile", "presystem-test:esm": "npm run compile:esm", - "presystem-test": "npm run compile -- --sourceMap", - "pretest": "npm run compile -- --sourceMap", + "presystem-test": "npm run compile:sourcemaps", + "pretest": "npm run compile:sourcemaps", "samples-test": "npm link && cd samples/ && npm link ../ && npm test && cd ../", "system-test:esm": "mkdir -p $HOME/.config && mocha build/esm/system-test --timeout 600000 --exit", "system-test": "mkdir -p $HOME/.config && mocha build/cjs/system-test --timeout 600000 --exit", diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts index 9173a38f73d7..5a54ebf6ff86 100644 --- a/handwritten/storage/src/nodejs-common/service.ts +++ b/handwritten/storage/src/nodejs-common/service.ts @@ -269,13 +269,28 @@ export class Service { if (this.providedUserAgent) { userAgent = `${this.providedUserAgent} ${userAgent}`; } + const headers = reqOpts.headers || {}; + const userTokenKey = Object.keys(headers).find( + key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' + ); + const userTokenValue = userTokenKey ? headers[userTokenKey] : undefined; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== ''; + const idempotencyToken = hasValidUserToken + ? (userTokenValue as string) + : crypto.randomUUID(); reqOpts.headers = { - ...reqOpts.headers, + ...headers, 'User-Agent': userAgent, 'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${ pkg.version - }-${getModuleFormat()} gccl-invocation-id/${crypto.randomUUID()}`, + }-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`, }; + if (!hasValidUserToken) { + if (userTokenKey) { + delete reqOpts.headers[userTokenKey]; + } + reqOpts.headers['x-goog-gcs-idempotency-token'] = idempotencyToken; + } if (reqOpts[GCCL_GCS_CMD_KEY]) { reqOpts.headers['x-goog-api-client'] += diff --git a/handwritten/storage/src/nodejs-common/util.ts b/handwritten/storage/src/nodejs-common/util.ts index e6c4db98b095..f0c4c7f419b2 100644 --- a/handwritten/storage/src/nodejs-common/util.ts +++ b/handwritten/storage/src/nodejs-common/util.ts @@ -1042,11 +1042,13 @@ export class Util { } _getDefaultHeaders(gcclGcsCmd?: string) { + const idempotencyToken = crypto.randomUUID(); const headers = { 'User-Agent': getUserAgentString(), 'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${ packageJson.version - }-${getModuleFormat()} gccl-invocation-id/${crypto.randomUUID()}`, + }-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`, + 'x-goog-gcs-idempotency-token': idempotencyToken, }; if (gcclGcsCmd) { diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts index df63ec637b1d..d1adfee9d153 100644 --- a/handwritten/storage/src/resumable-upload.ts +++ b/handwritten/storage/src/resumable-upload.ts @@ -338,7 +338,11 @@ export class Upload extends Writable { timeOfFirstRequest: number; isPartialUpload: boolean; - private currentInvocationId = { + private currentInvocationId: { + checkUploadStatus: string; + chunk: string; + uri: string; + } = { checkUploadStatus: crypto.randomUUID(), chunk: crypto.randomUUID(), uri: crypto.randomUUID(), @@ -806,6 +810,21 @@ export class Upload extends Writable { delete metadata.contentType; } + const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find( + key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' + ); + const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== ''; + if (hasValidUserToken) { + this.currentInvocationId.uri = userTokenValue as string; + } else if (userTokenKey && this.customRequestOptions?.headers) { + this.customRequestOptions = { + ...this.customRequestOptions, + headers: { ...this.customRequestOptions.headers }, + }; + delete this.customRequestOptions.headers![userTokenKey]; + } + let googAPIClient = `${getRuntimeTrackingString()} gccl/${ packageJson.version }-${getModuleFormat()} gccl-invocation-id/${this.currentInvocationId.uri}`; @@ -833,6 +852,10 @@ export class Upload extends Writable { }, }; + if (!hasValidUserToken) { + reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri; + } + if (metadata.contentLength) { reqOpts.headers!['X-Upload-Content-Length'] = metadata.contentLength.toString(); @@ -995,6 +1018,21 @@ export class Upload extends Writable { }, }); + const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find( + key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' + ); + const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== ''; + if (hasValidUserToken) { + this.currentInvocationId.chunk = userTokenValue as string; + } else if (userTokenKey && this.customRequestOptions?.headers) { + this.customRequestOptions = { + ...this.customRequestOptions, + headers: { ...this.customRequestOptions.headers }, + }; + delete this.customRequestOptions.headers![userTokenKey]; + } + let googAPIClient = `${getRuntimeTrackingString()} gccl/${ packageJson.version }-${getModuleFormat()} gccl-invocation-id/${ @@ -1010,6 +1048,10 @@ export class Upload extends Writable { 'x-goog-api-client': googAPIClient, }; + if (!hasValidUserToken) { + headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk; + } + // If using multiple chunk upload, set appropriate header if (multiChunkMode) { // We need to know how much data is available upstream to set the `Content-Range` header. @@ -1209,6 +1251,21 @@ export class Upload extends Writable { async checkUploadStatus( config: CheckUploadStatusConfig = {}, ): Promise> { + const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find( + key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' + ); + const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== ''; + if (hasValidUserToken) { + this.currentInvocationId.checkUploadStatus = userTokenValue as string; + } else if (userTokenKey && this.customRequestOptions?.headers) { + this.customRequestOptions = { + ...this.customRequestOptions, + headers: { ...this.customRequestOptions.headers }, + }; + delete this.customRequestOptions.headers![userTokenKey]; + } + let googAPIClient = `${getRuntimeTrackingString()} gccl/${ packageJson.version }-${getModuleFormat()} gccl-invocation-id/${ @@ -1230,6 +1287,10 @@ export class Upload extends Writable { }, }; + if (!hasValidUserToken) { + opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus; + } + try { const resp = await this.makeRequest(opts); diff --git a/handwritten/storage/test/headers.ts b/handwritten/storage/test/headers.ts index 9ccc685814bb..a19e56556597 100644 --- a/handwritten/storage/test/headers.ts +++ b/handwritten/storage/test/headers.ts @@ -65,11 +65,15 @@ describe('headers', () => { } catch (err) { if (err !== error) throw err; } - assert.ok( - /^gl-node\/(?[^W]+) gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.test( - requests[0].headers['x-goog-api-client'] - ) - ); + const apiClientHeader = requests[0].headers['x-goog-api-client']; + const match = + /^gl-node\/(?[^W]+) gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.exec( + apiClientHeader + ); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + const idempotencyToken = requests[0].headers['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); }); it('populates x-goog-api-client header (deno)', async () => { @@ -87,10 +91,34 @@ describe('headers', () => { } catch (err) { if (err !== error) throw err; } - assert.ok( - /^gl-deno\/0.00.0 gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.test( - requests[1].headers['x-goog-api-client'] - ) - ); + const apiClientHeader = requests[1].headers['x-goog-api-client']; + const match = + /^gl-deno\/0.00.0 gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.exec( + apiClientHeader + ); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + const idempotencyToken = requests[1].headers['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); + }); + + it('generates unique tokens for different requests', async () => { + const storage = new Storage(); + const bucket = storage.bucket('foo-bucket'); + try { + await bucket.create(); + } catch (err) { + if (err !== error) throw err; + } + try { + await bucket.create(); + } catch (err) { + if (err !== error) throw err; + } + const token1 = requests[requests.length - 2].headers['x-goog-gcs-idempotency-token']; + const token2 = requests[requests.length - 1].headers['x-goog-gcs-idempotency-token']; + assert.ok(token1); + assert.ok(token2); + assert.notStrictEqual(token1, token2); }); }); diff --git a/handwritten/storage/test/nodejs-common/service.ts b/handwritten/storage/test/nodejs-common/service.ts index 502c4e5419f9..66ebd1cc8f53 100644 --- a/handwritten/storage/test/nodejs-common/service.ts +++ b/handwritten/storage/test/nodejs-common/service.ts @@ -487,6 +487,82 @@ describe('Service', () => { service.request_(reqOpts, assert.ifError); }); + it('should add the x-goog-gcs-idempotency-token header matching the gccl-invocation-id', done => { + service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { + const pkg = service.packageJson; + const r = new RegExp( + `^gl-node/${process.versions.node} gccl/${ + pkg.version + }-${getModuleFormat()} gccl-invocation-id/(?[^W]+)$` + ); + const match = r.exec(reqOpts.headers!['x-goog-api-client']); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + const idempotencyToken = reqOpts.headers!['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); + done(); + }; + + service.request_(reqOpts, assert.ifError); + }); + + it('should respect user-provided x-goog-gcs-idempotency-token case-insensitively and align it with gccl-invocation-id', done => { + const customToken = 'my-custom-token-123'; + const customReqOpts = { + ...reqOpts, + headers: { + 'X-Goog-Gcs-Idempotency-Token': customToken, + }, + }; + + service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { + const pkg = service.packageJson; + const r = new RegExp( + `^gl-node/${process.versions.node} gccl/${ + pkg.version + }-${getModuleFormat()} gccl-invocation-id/(?[^W]+)$` + ); + const match = r.exec(reqOpts.headers!['x-goog-api-client']); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + assert.strictEqual(invocationId, customToken); + + // Verify there is no duplicate x-goog-gcs-idempotency-token header + assert.strictEqual(reqOpts.headers!['x-goog-gcs-idempotency-token'], undefined); + assert.strictEqual(reqOpts.headers!['X-Goog-Gcs-Idempotency-Token'], customToken); + done(); + }; + + service.request_(customReqOpts, assert.ifError); + }); + + it('should ignore invalid user-provided idempotency tokens and fallback to generating a UUID', done => { + const customReqOpts = { + ...reqOpts, + headers: { + 'X-Goog-Gcs-Idempotency-Token': undefined as unknown as string, + }, + }; + + service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { + const pkg = service.packageJson; + const r = new RegExp( + `^gl-node/${process.versions.node} gccl/${pkg.version + }-${getModuleFormat()} gccl-invocation-id/(?[^W]+)$` + ); + const match = r.exec(reqOpts.headers!['x-goog-api-client']); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + + // Verify a fallback token was generated and matches the invocation ID + const idempotencyToken = reqOpts.headers!['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); + done(); + }; + + service.request_(customReqOpts, assert.ifError); + }); + it('should add the `gccl-gcs-cmd` to the api-client header when provided', done => { const expected = 'example.expected/value'; service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { diff --git a/handwritten/storage/test/nodejs-common/util.ts b/handwritten/storage/test/nodejs-common/util.ts index 88e6905ffb52..cf3a9ef47d25 100644 --- a/handwritten/storage/test/nodejs-common/util.ts +++ b/handwritten/storage/test/nodejs-common/util.ts @@ -77,10 +77,15 @@ function fakeRequest() { } fakeRequest.defaults = (defaults: r.CoreOptions) => { - assert.ok( - /^gl-node\/(?[^W]+) gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.test( - defaults.headers!['x-goog-api-client'] - ) + const match = + /^gl-node\/(?[^W]+) gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.exec( + defaults.headers!['x-goog-api-client'] as string + ); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + assert.strictEqual( + defaults.headers!['x-goog-gcs-idempotency-token'], + invocationId ); return fakeRequest; }; diff --git a/handwritten/storage/test/resumable-upload.ts b/handwritten/storage/test/resumable-upload.ts index 6e6985878b00..a5c5c2705ab4 100644 --- a/handwritten/storage/test/resumable-upload.ts +++ b/handwritten/storage/test/resumable-upload.ts @@ -877,12 +877,96 @@ describe('resumable-upload', () => { delete metadataNoHeaders.contentLength; delete metadataNoHeaders.contentType; assert.deepStrictEqual(reqOpts.data, metadataNoHeaders); + assert(reqOpts.headers); + const apiClientHeader = reqOpts.headers['x-goog-api-client']; + const match = X_GOOG_API_HEADER_REGEX.exec(apiClientHeader as string); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + const idempotencyToken = reqOpts.headers['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); done(); return {headers: {location: '/foo'}}; }; up.createURI(); }); + it('should respect user-provided x-goog-gcs-idempotency-token case-insensitively and align it with gccl-invocation-id in createURI', async () => { + const customToken = 'my-custom-resumable-token'; + up.customRequestOptions = { + headers: { + 'X-Goog-Gcs-Idempotency-Token': customToken, + }, + }; + + up.authClient.request = async (combinedReqOpts: GaxiosOptions) => { + assert(combinedReqOpts.headers); + const apiClientHeader = combinedReqOpts.headers['x-goog-api-client']; + const match = X_GOOG_API_HEADER_REGEX.exec(apiClientHeader as string); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + assert.strictEqual(invocationId, customToken); + + // Verify there is no duplicate x-goog-gcs-idempotency-token header + assert.strictEqual(combinedReqOpts.headers['x-goog-gcs-idempotency-token'], undefined); + assert.strictEqual(combinedReqOpts.headers['X-Goog-Gcs-Idempotency-Token'], customToken); + return {headers: {location: '/foo'}}; + }; + + await up.createURI(); + }); + + it('should ignore invalid user-provided idempotency tokens and fallback to generating a UUID in createURI', async () => { + up.customRequestOptions = { + headers: { + 'X-Goog-Gcs-Idempotency-Token': '', // invalid empty string + }, + }; + + up.authClient.request = async (combinedReqOpts: GaxiosOptions) => { + assert(combinedReqOpts.headers); + const apiClientHeader = combinedReqOpts.headers['x-goog-api-client']; + const match = X_GOOG_API_HEADER_REGEX.exec(apiClientHeader as string); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + + // Verify a fallback token was generated and matches the invocation ID + const idempotencyToken = combinedReqOpts.headers['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); + return {headers: {location: '/foo'}}; + }; + + await up.createURI(); + }); + + it('should reuse the same x-goog-gcs-idempotency-token on retry of createURI', async () => { + let invocationCount = 0; + let token1 = ''; + let token2 = ''; + + up.makeRequest = async (reqOpts: GaxiosOptions) => { + invocationCount++; + assert(reqOpts.headers); + if (invocationCount === 1) { + token1 = reqOpts.headers['x-goog-gcs-idempotency-token'] as string; + const error = new GaxiosError( + 'Retriable error', + {} as GaxiosOptions, + { status: 500 } as GaxiosResponse + ); + throw error; + } else if (invocationCount === 2) { + token2 = reqOpts.headers['x-goog-gcs-idempotency-token'] as string; + return { headers: { location: '/foo' } }; + } + return { headers: { location: '/foo' } }; + }; + + await up.createURI(); + assert.strictEqual(invocationCount, 2); + assert.ok(token1); + assert.strictEqual(token1, token2); + }); + it('should pass through the KMS key name', done => { const kmsKeyName = 'kms-key-name'; const up = upload({ diff --git a/librarian.yaml b/librarian.yaml index d4d29c13e820..f55632ab1c08 100644 --- a/librarian.yaml +++ b/librarian.yaml @@ -743,7 +743,7 @@ libraries: nodejs: default_version: v1 - name: google-cloud-databasecenter - version: 0.4.0 + version: 0.4.1 apis: - path: google/cloud/databasecenter/v1beta copyright_year: "2026" diff --git a/packages/google-cloud-databasecenter/CHANGELOG.md b/packages/google-cloud-databasecenter/CHANGELOG.md index 1a55e239d67a..366f8547a3bc 100644 --- a/packages/google-cloud-databasecenter/CHANGELOG.md +++ b/packages/google-cloud-databasecenter/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.4.1](https://github.com/googleapis/google-cloud-node/compare/databasecenter-v0.4.0...databasecenter-v0.4.1) (2026-08-05) + + +### Bug Fixes + +* Upgrade types sinon for databasecenter ([#9098](https://github.com/googleapis/google-cloud-node/issues/9098)) ([23aee11](https://github.com/googleapis/google-cloud-node/commit/23aee114a947a29f31977aab8de9c1c7ecd62188)) + ## [0.4.0](https://github.com/googleapis/google-cloud-node/compare/databasecenter-v0.3.0...databasecenter-v0.4.0) (2026-08-04) diff --git a/packages/google-cloud-databasecenter/package.json b/packages/google-cloud-databasecenter/package.json index 61bfb2f90cde..fd525383d2c5 100644 --- a/packages/google-cloud-databasecenter/package.json +++ b/packages/google-cloud-databasecenter/package.json @@ -1,6 +1,6 @@ { "name": "@google-cloud/databasecenter", - "version": "0.4.0", + "version": "0.4.1", "description": "Database Center API client for Node.js", "repository": { "type": "git",