From 5e35298c65b1cc45938c464ca04fe7c5d2552d2d Mon Sep 17 00:00:00 2001
From: "release-please[bot]"
<55107282+release-please[bot]@users.noreply.github.com>
Date: Thu, 6 Aug 2026 14:45:22 -0700
Subject: [PATCH 1/9] chore: release main (#9093)
:robot: I have created a release *beep* *boop*
---
pack-n-play: 5.0.1
##
[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))
gcp-metadata: 9.0.1
##
[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))
googleapis-common: 9.0.1
##
[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))
bigquery: 9.0.1
##
[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))
bigtable: 7.1.0
##
[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))
databasecenter: 0.4.1
##
[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))
---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
---
.release-please-manifest.json | 12 +-
changelog.json | 109 +++++++++++++++++-
core/dev-packages/pack-n-play/CHANGELOG.md | 7 ++
core/dev-packages/pack-n-play/package.json | 2 +-
core/packages/gcp-metadata/CHANGELOG.md | 7 ++
core/packages/gcp-metadata/package.json | 2 +-
.../gcp-metadata/samples/package.json | 2 +-
.../nodejs-googleapis-common/CHANGELOG.md | 7 ++
.../nodejs-googleapis-common/package.json | 2 +-
.../samples/package.json | 2 +-
handwritten/bigquery/CHANGELOG.md | 7 ++
handwritten/bigquery/package.json | 2 +-
handwritten/bigtable/CHANGELOG.md | 7 ++
handwritten/bigtable/package.json | 2 +-
librarian.yaml | 2 +-
.../google-cloud-databasecenter/CHANGELOG.md | 7 ++
.../google-cloud-databasecenter/package.json | 2 +-
17 files changed, 165 insertions(+), 16 deletions(-)
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/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",
From 6b6a2396c2f1f072d0d3f3fc27f63913a3ce9a79 Mon Sep 17 00:00:00 2001
From: Santiago Quiroga <22756465+quirogas@users.noreply.github.com>
Date: Thu, 6 Aug 2026 14:51:56 -0700
Subject: [PATCH 2/9] ci(workflows): add node and pnpm v7 setup to generation
check (#9099)
Add Node.js 22 and pnpm v7 runtime setup steps to
`.github/workflows/generation_check.yaml` prior to tool installation and
generation verification.
On default GitHub hosted CI runners (`ubuntu-latest`), `pnpm` is not
pre-installed on the system `$PATH`, so executing `librarian install`
immediately fails due to `exec: "pnpm": executable file not found in
$PATH`.
---
.github/workflows/generation_check.yaml | 75 +++++++++++++++++++++++++
.github/workflows/librarian_tidy.yml | 47 ++++++++++++++++
2 files changed, 122 insertions(+)
create mode 100644 .github/workflows/generation_check.yaml
create mode 100644 .github/workflows/librarian_tidy.yml
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
From 812f79c44e6f7dae82f04258fb129f8b4799c34d Mon Sep 17 00:00:00 2001
From: Thiyagu K
Date: Fri, 3 Jul 2026 09:18:08 +0000
Subject: [PATCH 3/9] feat(storage): add x-goog-gcs-idempotency-token header
linked to gccl-invocation-id for request tracking
---
.../storage/src/nodejs-common/service.ts | 4 +-
handwritten/storage/src/nodejs-common/util.ts | 4 +-
handwritten/storage/src/resumable-upload.ts | 3 ++
handwritten/storage/test/headers.ts | 48 +++++++++++++++----
.../storage/test/nodejs-common/service.ts | 19 ++++++++
.../storage/test/nodejs-common/util.ts | 13 +++--
handwritten/storage/test/resumable-upload.ts | 36 ++++++++++++++
7 files changed, 111 insertions(+), 16 deletions(-)
diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts
index 9173a38f73d7..24610ca1dfed 100644
--- a/handwritten/storage/src/nodejs-common/service.ts
+++ b/handwritten/storage/src/nodejs-common/service.ts
@@ -269,12 +269,14 @@ export class Service {
if (this.providedUserAgent) {
userAgent = `${this.providedUserAgent} ${userAgent}`;
}
+ const idempotencyToken = crypto.randomUUID();
reqOpts.headers = {
...reqOpts.headers,
'User-Agent': userAgent,
'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${
pkg.version
- }-${getModuleFormat()} gccl-invocation-id/${crypto.randomUUID()}`,
+ }-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`,
+ 'x-goog-gcs-idempotency-token': idempotencyToken,
};
if (reqOpts[GCCL_GCS_CMD_KEY]) {
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..138415602d3e 100644
--- a/handwritten/storage/src/resumable-upload.ts
+++ b/handwritten/storage/src/resumable-upload.ts
@@ -829,6 +829,7 @@ export class Upload extends Writable {
headers: {
'User-Agent': getUserAgentString(),
'x-goog-api-client': googAPIClient,
+ 'x-goog-gcs-idempotency-token': this.currentInvocationId.uri,
...headers,
},
};
@@ -1008,6 +1009,7 @@ export class Upload extends Writable {
const headers: GaxiosOptions['headers'] = {
'User-Agent': getUserAgentString(),
'x-goog-api-client': googAPIClient,
+ 'x-goog-gcs-idempotency-token': this.currentInvocationId.chunk,
};
// If using multiple chunk upload, set appropriate header
@@ -1227,6 +1229,7 @@ export class Upload extends Writable {
'Content-Range': 'bytes */*',
'User-Agent': getUserAgentString(),
'x-goog-api-client': googAPIClient,
+ 'x-goog-gcs-idempotency-token': this.currentInvocationId.checkUploadStatus,
},
};
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..b830c65eb680 100644
--- a/handwritten/storage/test/nodejs-common/service.ts
+++ b/handwritten/storage/test/nodejs-common/service.ts
@@ -487,6 +487,25 @@ 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 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..0de3c85bae6b 100644
--- a/handwritten/storage/test/resumable-upload.ts
+++ b/handwritten/storage/test/resumable-upload.ts
@@ -877,12 +877,48 @@ 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 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({
From 10d82a24231a3e2779f7f0c8cccedc890eea3da4 Mon Sep 17 00:00:00 2001
From: Thiyagu K
Date: Fri, 3 Jul 2026 11:19:34 +0000
Subject: [PATCH 4/9] feat: allow user-provided x-goog-gcs-idempotency-token
and synchronize with gccl-invocation-id header
---
.../storage/src/nodejs-common/service.ts | 14 +++++--
handwritten/storage/src/resumable-upload.ts | 42 +++++++++++++++++--
.../storage/test/nodejs-common/service.ts | 30 +++++++++++++
handwritten/storage/test/resumable-upload.ts | 25 +++++++++++
4 files changed, 104 insertions(+), 7 deletions(-)
diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts
index 24610ca1dfed..b3087172bca6 100644
--- a/handwritten/storage/src/nodejs-common/service.ts
+++ b/handwritten/storage/src/nodejs-common/service.ts
@@ -269,15 +269,23 @@ export class Service {
if (this.providedUserAgent) {
userAgent = `${this.providedUserAgent} ${userAgent}`;
}
- const idempotencyToken = crypto.randomUUID();
+ const headers = reqOpts.headers || {};
+ const userTokenKey = Object.keys(headers).find(
+ key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
+ );
+ const idempotencyToken = userTokenKey
+ ? (headers[userTokenKey] as string)
+ : crypto.randomUUID();
reqOpts.headers = {
- ...reqOpts.headers,
+ ...headers,
'User-Agent': userAgent,
'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${
pkg.version
}-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`,
- 'x-goog-gcs-idempotency-token': idempotencyToken,
};
+ if (!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/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts
index 138415602d3e..6be497336a13 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,13 @@ export class Upload extends Writable {
delete metadata.contentType;
}
+ const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find(
+ key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
+ );
+ if (userTokenKey) {
+ this.currentInvocationId.uri = this.customRequestOptions.headers![userTokenKey] as string;
+ }
+
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
packageJson.version
}-${getModuleFormat()} gccl-invocation-id/${this.currentInvocationId.uri}`;
@@ -829,11 +840,14 @@ export class Upload extends Writable {
headers: {
'User-Agent': getUserAgentString(),
'x-goog-api-client': googAPIClient,
- 'x-goog-gcs-idempotency-token': this.currentInvocationId.uri,
...headers,
},
};
+ if (!userTokenKey) {
+ reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri;
+ }
+
if (metadata.contentLength) {
reqOpts.headers!['X-Upload-Content-Length'] =
metadata.contentLength.toString();
@@ -996,6 +1010,13 @@ export class Upload extends Writable {
},
});
+ const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find(
+ key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
+ );
+ if (userTokenKey) {
+ this.currentInvocationId.chunk = this.customRequestOptions.headers![userTokenKey] as string;
+ }
+
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
packageJson.version
}-${getModuleFormat()} gccl-invocation-id/${
@@ -1009,9 +1030,12 @@ export class Upload extends Writable {
const headers: GaxiosOptions['headers'] = {
'User-Agent': getUserAgentString(),
'x-goog-api-client': googAPIClient,
- 'x-goog-gcs-idempotency-token': this.currentInvocationId.chunk,
};
+ if (!userTokenKey) {
+ 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.
@@ -1211,6 +1235,13 @@ 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'
+ );
+ if (userTokenKey) {
+ this.currentInvocationId.checkUploadStatus = this.customRequestOptions.headers![userTokenKey] as string;
+ }
+
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
packageJson.version
}-${getModuleFormat()} gccl-invocation-id/${
@@ -1229,10 +1260,13 @@ export class Upload extends Writable {
'Content-Range': 'bytes */*',
'User-Agent': getUserAgentString(),
'x-goog-api-client': googAPIClient,
- 'x-goog-gcs-idempotency-token': this.currentInvocationId.checkUploadStatus,
},
};
+ if (!userTokenKey) {
+ opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus;
+ }
+
try {
const resp = await this.makeRequest(opts);
diff --git a/handwritten/storage/test/nodejs-common/service.ts b/handwritten/storage/test/nodejs-common/service.ts
index b830c65eb680..857ec514f10a 100644
--- a/handwritten/storage/test/nodejs-common/service.ts
+++ b/handwritten/storage/test/nodejs-common/service.ts
@@ -506,6 +506,36 @@ describe('Service', () => {
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 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/resumable-upload.ts b/handwritten/storage/test/resumable-upload.ts
index 0de3c85bae6b..6710f850d1a2 100644
--- a/handwritten/storage/test/resumable-upload.ts
+++ b/handwritten/storage/test/resumable-upload.ts
@@ -890,6 +890,31 @@ describe('resumable-upload', () => {
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 reuse the same x-goog-gcs-idempotency-token on retry of createURI', async () => {
let invocationCount = 0;
let token1 = '';
From 6ae895a4e8e36bfdac456db9d27d9a00ead9e86b Mon Sep 17 00:00:00 2001
From: Thiyagu K
Date: Fri, 3 Jul 2026 11:36:08 +0000
Subject: [PATCH 5/9] refactor: simplify sourcemap compilation scripts and add
optional chaining for idempotency token access
---
handwritten/storage/package.json | 9 +++++----
handwritten/storage/src/resumable-upload.ts | 12 ++++++------
2 files changed, 11 insertions(+), 10 deletions(-)
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/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts
index 6be497336a13..0f3c9088581e 100644
--- a/handwritten/storage/src/resumable-upload.ts
+++ b/handwritten/storage/src/resumable-upload.ts
@@ -810,11 +810,11 @@ export class Upload extends Writable {
delete metadata.contentType;
}
- const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find(
+ const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find(
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
if (userTokenKey) {
- this.currentInvocationId.uri = this.customRequestOptions.headers![userTokenKey] as string;
+ this.currentInvocationId.uri = this.customRequestOptions?.headers?.[userTokenKey] as string;
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1010,11 +1010,11 @@ export class Upload extends Writable {
},
});
- const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find(
+ const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find(
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
if (userTokenKey) {
- this.currentInvocationId.chunk = this.customRequestOptions.headers![userTokenKey] as string;
+ this.currentInvocationId.chunk = this.customRequestOptions?.headers?.[userTokenKey] as string;
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1235,11 +1235,11 @@ export class Upload extends Writable {
async checkUploadStatus(
config: CheckUploadStatusConfig = {},
): Promise> {
- const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find(
+ const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find(
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
if (userTokenKey) {
- this.currentInvocationId.checkUploadStatus = this.customRequestOptions.headers![userTokenKey] as string;
+ this.currentInvocationId.checkUploadStatus = this.customRequestOptions?.headers?.[userTokenKey] as string;
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
From 73a5e3d3f81ebcdd2fb2d3a17d310360a4b0789d Mon Sep 17 00:00:00 2001
From: Thiyagu K
Date: Fri, 3 Jul 2026 11:54:00 +0000
Subject: [PATCH 6/9] fix: ignore invalid empty or undefined idempotency tokens
and fallback to generating a UUID
---
.../storage/src/nodejs-common/service.ts | 8 +++---
handwritten/storage/src/resumable-upload.ts | 24 ++++++++++-------
.../storage/test/nodejs-common/service.ts | 27 +++++++++++++++++++
handwritten/storage/test/resumable-upload.ts | 23 ++++++++++++++++
4 files changed, 70 insertions(+), 12 deletions(-)
diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts
index b3087172bca6..7f60dc7a14d3 100644
--- a/handwritten/storage/src/nodejs-common/service.ts
+++ b/handwritten/storage/src/nodejs-common/service.ts
@@ -273,8 +273,10 @@ export class Service {
const userTokenKey = Object.keys(headers).find(
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
- const idempotencyToken = userTokenKey
- ? (headers[userTokenKey] as string)
+ const userTokenValue = userTokenKey ? headers[userTokenKey] : undefined;
+ const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
+ const idempotencyToken = hasValidUserToken
+ ? (userTokenValue as string)
: crypto.randomUUID();
reqOpts.headers = {
...headers,
@@ -283,7 +285,7 @@ export class Service {
pkg.version
}-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`,
};
- if (!userTokenKey) {
+ if (!hasValidUserToken) {
reqOpts.headers['x-goog-gcs-idempotency-token'] = idempotencyToken;
}
diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts
index 0f3c9088581e..caf12be1411e 100644
--- a/handwritten/storage/src/resumable-upload.ts
+++ b/handwritten/storage/src/resumable-upload.ts
@@ -813,8 +813,10 @@ export class Upload extends Writable {
const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find(
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
- if (userTokenKey) {
- this.currentInvocationId.uri = this.customRequestOptions?.headers?.[userTokenKey] as string;
+ const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined;
+ const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
+ if (hasValidUserToken) {
+ this.currentInvocationId.uri = userTokenValue as string;
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -844,7 +846,7 @@ export class Upload extends Writable {
},
};
- if (!userTokenKey) {
+ if (!hasValidUserToken) {
reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri;
}
@@ -1013,8 +1015,10 @@ export class Upload extends Writable {
const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find(
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
- if (userTokenKey) {
- this.currentInvocationId.chunk = this.customRequestOptions?.headers?.[userTokenKey] as string;
+ const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined;
+ const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
+ if (hasValidUserToken) {
+ this.currentInvocationId.chunk = userTokenValue as string;
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1032,7 +1036,7 @@ export class Upload extends Writable {
'x-goog-api-client': googAPIClient,
};
- if (!userTokenKey) {
+ if (!hasValidUserToken) {
headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk;
}
@@ -1238,8 +1242,10 @@ export class Upload extends Writable {
const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find(
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
- if (userTokenKey) {
- this.currentInvocationId.checkUploadStatus = this.customRequestOptions?.headers?.[userTokenKey] as string;
+ const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined;
+ const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
+ if (hasValidUserToken) {
+ this.currentInvocationId.checkUploadStatus = userTokenValue as string;
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1263,7 +1269,7 @@ export class Upload extends Writable {
},
};
- if (!userTokenKey) {
+ if (!hasValidUserToken) {
opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus;
}
diff --git a/handwritten/storage/test/nodejs-common/service.ts b/handwritten/storage/test/nodejs-common/service.ts
index 857ec514f10a..66ebd1cc8f53 100644
--- a/handwritten/storage/test/nodejs-common/service.ts
+++ b/handwritten/storage/test/nodejs-common/service.ts
@@ -536,6 +536,33 @@ describe('Service', () => {
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/resumable-upload.ts b/handwritten/storage/test/resumable-upload.ts
index 6710f850d1a2..a5c5c2705ab4 100644
--- a/handwritten/storage/test/resumable-upload.ts
+++ b/handwritten/storage/test/resumable-upload.ts
@@ -915,6 +915,29 @@ describe('resumable-upload', () => {
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 = '';
From eda1661446539ab6e4dbe4c61b64c1b0be86765d Mon Sep 17 00:00:00 2001
From: Thiyagu K
Date: Fri, 3 Jul 2026 13:19:11 +0000
Subject: [PATCH 7/9] fix: remove user-provided token header when injecting GCS
idempotency token to prevent conflicts
---
handwritten/storage/src/nodejs-common/service.ts | 3 +++
handwritten/storage/src/resumable-upload.ts | 9 +++++++++
2 files changed, 12 insertions(+)
diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts
index 7f60dc7a14d3..4273ed349744 100644
--- a/handwritten/storage/src/nodejs-common/service.ts
+++ b/handwritten/storage/src/nodejs-common/service.ts
@@ -286,6 +286,9 @@ export class Service {
}-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`,
};
if (!hasValidUserToken) {
+ if (userTokenKey) {
+ delete reqOpts.headers[userTokenKey];
+ }
reqOpts.headers['x-goog-gcs-idempotency-token'] = idempotencyToken;
}
diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts
index caf12be1411e..b1be2e116766 100644
--- a/handwritten/storage/src/resumable-upload.ts
+++ b/handwritten/storage/src/resumable-upload.ts
@@ -847,6 +847,9 @@ export class Upload extends Writable {
};
if (!hasValidUserToken) {
+ if (userTokenKey) {
+ delete reqOpts.headers![userTokenKey];
+ }
reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri;
}
@@ -1037,6 +1040,9 @@ export class Upload extends Writable {
};
if (!hasValidUserToken) {
+ if (userTokenKey) {
+ delete headers[userTokenKey];
+ }
headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk;
}
@@ -1270,6 +1276,9 @@ export class Upload extends Writable {
};
if (!hasValidUserToken) {
+ if (userTokenKey) {
+ delete opts.headers![userTokenKey];
+ }
opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus;
}
From b4b795278705353951e216552331224cbb0e16ec Mon Sep 17 00:00:00 2001
From: Thiyagu K
Date: Fri, 3 Jul 2026 13:55:22 +0000
Subject: [PATCH 8/9] fix: improve idempotency token validation and standardize
token removal logic during resumable uploads
---
.../storage/src/nodejs-common/service.ts | 2 +-
handwritten/storage/src/resumable-upload.ts | 21 ++++++++-----------
2 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts
index 4273ed349744..5a54ebf6ff86 100644
--- a/handwritten/storage/src/nodejs-common/service.ts
+++ b/handwritten/storage/src/nodejs-common/service.ts
@@ -274,7 +274,7 @@ export class Service {
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
const userTokenValue = userTokenKey ? headers[userTokenKey] : undefined;
- const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
+ const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== '';
const idempotencyToken = hasValidUserToken
? (userTokenValue as string)
: crypto.randomUUID();
diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts
index b1be2e116766..5abd829266cb 100644
--- a/handwritten/storage/src/resumable-upload.ts
+++ b/handwritten/storage/src/resumable-upload.ts
@@ -814,9 +814,11 @@ export class Upload extends Writable {
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined;
- const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
+ const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== '';
if (hasValidUserToken) {
this.currentInvocationId.uri = userTokenValue as string;
+ } else if (userTokenKey && this.customRequestOptions?.headers) {
+ delete this.customRequestOptions.headers[userTokenKey];
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -847,9 +849,6 @@ export class Upload extends Writable {
};
if (!hasValidUserToken) {
- if (userTokenKey) {
- delete reqOpts.headers![userTokenKey];
- }
reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri;
}
@@ -1019,9 +1018,11 @@ export class Upload extends Writable {
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined;
- const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
+ const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== '';
if (hasValidUserToken) {
this.currentInvocationId.chunk = userTokenValue as string;
+ } else if (userTokenKey && this.customRequestOptions?.headers) {
+ delete this.customRequestOptions.headers[userTokenKey];
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1040,9 +1041,6 @@ export class Upload extends Writable {
};
if (!hasValidUserToken) {
- if (userTokenKey) {
- delete headers[userTokenKey];
- }
headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk;
}
@@ -1249,9 +1247,11 @@ export class Upload extends Writable {
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined;
- const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
+ const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== '';
if (hasValidUserToken) {
this.currentInvocationId.checkUploadStatus = userTokenValue as string;
+ } else if (userTokenKey && this.customRequestOptions?.headers) {
+ delete this.customRequestOptions.headers[userTokenKey];
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1276,9 +1276,6 @@ export class Upload extends Writable {
};
if (!hasValidUserToken) {
- if (userTokenKey) {
- delete opts.headers![userTokenKey];
- }
opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus;
}
From eb8ba6a1b0b0a800d5e37cef586634b2b0d31adb Mon Sep 17 00:00:00 2001
From: Thiyagu K
Date: Thu, 9 Jul 2026 07:08:24 +0000
Subject: [PATCH 9/9] fix: perform immutable updates on customRequestOptions to
avoid mutating shared header objects
---
handwritten/storage/src/resumable-upload.ts | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts
index 5abd829266cb..d1adfee9d153 100644
--- a/handwritten/storage/src/resumable-upload.ts
+++ b/handwritten/storage/src/resumable-upload.ts
@@ -818,7 +818,11 @@ export class Upload extends Writable {
if (hasValidUserToken) {
this.currentInvocationId.uri = userTokenValue as string;
} else if (userTokenKey && this.customRequestOptions?.headers) {
- delete this.customRequestOptions.headers[userTokenKey];
+ this.customRequestOptions = {
+ ...this.customRequestOptions,
+ headers: { ...this.customRequestOptions.headers },
+ };
+ delete this.customRequestOptions.headers![userTokenKey];
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1022,7 +1026,11 @@ export class Upload extends Writable {
if (hasValidUserToken) {
this.currentInvocationId.chunk = userTokenValue as string;
} else if (userTokenKey && this.customRequestOptions?.headers) {
- delete this.customRequestOptions.headers[userTokenKey];
+ this.customRequestOptions = {
+ ...this.customRequestOptions,
+ headers: { ...this.customRequestOptions.headers },
+ };
+ delete this.customRequestOptions.headers![userTokenKey];
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1251,7 +1259,11 @@ export class Upload extends Writable {
if (hasValidUserToken) {
this.currentInvocationId.checkUploadStatus = userTokenValue as string;
} else if (userTokenKey && this.customRequestOptions?.headers) {
- delete this.customRequestOptions.headers[userTokenKey];
+ this.customRequestOptions = {
+ ...this.customRequestOptions,
+ headers: { ...this.customRequestOptions.headers },
+ };
+ delete this.customRequestOptions.headers![userTokenKey];
}
let googAPIClient = `${getRuntimeTrackingString()} gccl/${