googleapis

Stars and mentions

Last 30 days, Oct 5 – Nov 3

GitHub stars285
mentions23
06121824Oct 5Oct 19Nov 3
UTC days · repositories googleapis owns8 repositories

Package downloads

Cumulative recorded downloads · since 2025-10-28

npm—
PyPI1.5B
0383M766.1M1.1B1.5BOct 5Oct 19Nov 3
Oct 30npm: 0/10 packages observed · PyPI: 1/3 packages observed

Across 13 known packages in repositories googleapis owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete. PyPI excludes known mirrors.

Top repositories

googleapis’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside googleapis’s account where googleapis opened or reviewed pull requests in the past 365 days.

Who noticed googleapis

Everyone who starred or forked googleapis’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

23posts
Nov 40 posts
  1. Upgrade a Major Dependency With a Coding Agent: Changelog First, Then the Failing Tests - Book: AI That Acts - The series: AI in TypeScript — 5 books, from your first LLM call to agents in production — all five here - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- openai-node 7.0.0 came out on July 27, 2026, with one item under breaking changes: the SDK now requires Node.js 22. Three months later, 7.44.0, a minor release, carries the line "Breaking change in a minor release." It says callers that directly access item.content or item.role on Live Decision items "may no longer compile." 7.43.0, a few days earlier, has the same label for a change to DecisionInputMessage. Type "upgrade openai to v7" into a coding agent. It bumps the version and starts reading type errors. It has never seen those release notes. Every fix it makes is a guess about what changed, and in a diff a guess that compiles looks just like a fix that follows the release note. The order I'd use: find out what the new version demands, read what changed in every release you are skipping, record what actually broke in your code. Then the agent gets all three as its brief. Steps 1 to 3 are plain gh, npm, git and tsc in a terminal. From step 4 on I use Hermes IDE, which I build, so weigh that part accordingly. The running example is a TypeScript app in ~/code/app on openai@6.49.0, the release that the 7.0.0 notes compare against, going to 7.47.0, the latest release as of October 31. Swap in your own package and versions. Step 1: Ask npm what the new version demands Four read-only commands in your main checkout. Nothing changes on disk. cd ~/code/app npm outdated openai npm view openai@7.47.0 engines peerDependencies node --version npm explain zod npm outdated shows Current, Wanted and Latest. Wanted is the highest version your package.json range allows, so with ^6.49.0 it stays on 6.x while Latest shows 7.x. npm prints that row in yellow, its colour for a newer version outside your range. npm view reads the target's manifest from the registry before you install anything. The 7.47.0 package.json sets "node": ">=22.0.0" and lists optional peers, among them zod at ^3.25 || ^4.0 and ws at ^8.21.0. Both fields bite later than you'd think. npm's package.json docs say that unless you set engine-strict, the engines field "will only produce warnings." On Node 20 the install goes through and you find out in CI or at runtime. Peers bite only if you already have them. openai marks zod and ws optional, and the same docs say "Npm will not automatically install optional peer dependencies." But if your tree already holds a zod or ws outside those ranges, a conflicting requirement "may cause an error if the tree cannot be resolved correctly." npm explain zod prints the chain of dependencies behind each copy of zod you have, so you know whether your code or some other package owns it. If node --version prints anything below 22, that is its own change. Make it before the SDK bump, or at least name it in the brief. Step 2: Pull every release note in the range First, keep all the working files out of git. Every file in this tutorial starts with .upgrade-: echo '.upgrade-*' >> .gitignore The major release's own notes are one call to gh release view: gh release view v7.0.0 -R openai/openai-node \ --json body -q .body Those are the notes most people read, and for this upgrade they say the least. To get every release between your version and the target, list them all with gh api and filter them with jq. Save this filter as .upgrade-between.jq: .[][] | select(.draft | not) | select(.tag_name | test("^v[0-9]+\\.[0-9]+\\.[0-9]+$")) | (.tag_name | ltrimstr("v") | split(".") | map(tonumber)) as $v | select($v > $from and $v <= $to) | "## \(.tag_name) (\(.published_at[0:10]))\n\n\(.body // "")\n" Then fetch, filter and search: gh api --paginate --slurp \ 'repos/openai/openai-node/releases?per_page=100' \ > .upgrade-releases.json jq -r --argjson from '[6,49,0]' --argjson to '[7,47,0]' \ -f .upgrade-between.jq .upgrade-releases.json \ > .upgrade-changelog.md grep -n -i 'breaking' .upgrade-changelog.md --paginate --slurp returns one array of pages, which is why the filter starts with .[][]. The test line skips prerelease tags such as -beta.1, whose parts aren't plain numbers. Each tag becomes an array like [7,44,0], and jq orders arrays lexically, element by element, so 7.10.0 lands above 7.9.0 where a string comparison would put it below. The output is newest first. That grep is your short list. For this range it catches at least the three entries from the top of this post: the Node 22 requirement and the two minor releases labelled breaking. The rest of .upgrade-changelog.md is what the agent reads when an error doesn't match any of them. !A long unrolled scroll of release notes under a magnifying glass, with one line glowing burnt orange among dozens of grey ones The breaking change you need is rarely in the major release's notes alone. Step 3: Bump in a throwaway worktree and save what broke You want the real failure list before the agent starts, and you don't want your main checkout sitting on a half-upgraded lockfile meanwhile. A detached git worktree is a second folder on the same commit, with no branch to clean up afterwards. git worktree add --detach ../app-probe cd ../app-probe npm ci npm install openai@7.47.0 npx tsc --noEmit --pretty false \ > ../app/.upgrade-tsc.txt 2>&1 npm test > ../app/.upgrade-tests.txt 2>&1 cd ../app git worktree remove --force ../app-probe npm ci comes first because a new worktree holds tracked files only, so there is no node_modules yet. --noEmit type-checks without writing any output. --pretty false matters when output goes to a file: pretty is on by default and adds colour and context, and the plain form is easier for you and the agent to read line by line. Both outputs are written back into the main folder, so they outlive the probe. --force is there only because the probe holds an uncommitted bump on purpose. Git's docs say remove refuses an unclean worktree without it. Keep that refusal on any worktree that holds work you care about. grep -c 'error TS' .upgrade-tsc.txt counts the type errors. If both files come back clean, the upgrade may be one line plus the Node change. Step 4: Write the brief and start the task Save this as .upgrade-brief.md, edited for your versions: Upgrade openai from 6.49.0 to 7.47.0. Read before editing: - .upgrade-changelog.md: every release note in range - .upgrade-tsc.txt, .upgrade-tests.txt: what broke 1. Run `npm install openai@7.47.0`, then `npx tsc --noEmit` and `npm test`. If the failures differ from the files, stop and say so. 2. Fix one failure per turn. Start each reply with the error line you fixed. 3. Name the release note each fix follows. If no note explains a failure, stop and ask. 4. Don't edit or delete tests. Don't add @ts-ignore, @ts-expect-error or `as any`. 5. 7.x needs Node.js 22. Update "engines" and the Node version in CI to match, in its own turn. 6. Done when tsc and npm test both pass. Rules 1 and 2 keep you and the agent on the same failure list, one entry per reply, which is what the review in step 5 needs. Rule 3 forces every change to point at a note, and rule 4 shuts the cheapest way to turn a suite green. Next, the recipe Hermes runs for every new task's worktree, from the recipe docs: # .hermes/worktree.toml copy = [".upgrade-*"] done_when = ["npx tsc --noEmit", "npm test"] copy brings files from your project folder into the same path in the worktree, but only files git ignores. If a pattern matches a tracked file, Hermes refuses the whole recipe and setup doesn't run. That is why the .gitignore line came first in step 2. done_when holds the default checks for the task's Done-When, here the same two commands as rule 6. The first time Hermes sees the recipe, and whenever it changes, it asks you to choose Run setup or Skip. Choose Run setup: per the docs, Skip "starts the session without setup." Commit the recipe and the .gitignore line together. Then press ⌘N (Ctrl+Shift+N on Windows and Linux), pick Claude Code or Codex, and type Read .upgrade-brief.md and follow it. The agent starts in a real terminal on a new hermes/ branch, in its own worktree, with that line as its first prompt. Your main checkout stays on 6.49.0 and keeps working while it runs. Hermes never answers a permission prompt for you, so when the agent asks to run npm install, you answer in its terminal. !The Hermes New task sheet with a task typed in and chips for Claude Code, the project, a new hermes/ worktree, Accept edits, model and effort, above a preview of the claude command The ⌘N New task sheet in Hermes 2.3.4: chips for Claude Code, the project, a new hermes/ worktree, Accept edits, model and effort, and the "Hermes will run" preview of the claude command. Step 5: Review one failure per turn, then land Because of rule 2, every agent reply should map to one line in .upgrade-tsc.txt or .upgrade-tests.txt. The "Agent replies" strip under the session shows one chip per reply with the lines it changed. Click a chip to see that reply's changes, or to restore your files to that point. ⌘G (Ctrl+Shift+G on Windows and Linux) opens the Review Desk, which shows changes by turn or by file, with risk flags. Read it by turn with the failure list open beside it. For each turn, check three things: the diff touches the code the error line points at, it follows the release note the agent named, and it leaves the tests alone. Switch to the by-file view for that last one. A line comment goes back to the agent that wrote the line. Say a turn silences the 7.44.0 error with a cast: comment on it and ask for the narrowing the release notes suggest, checking 'content' in event.item before reading message fields. !The Hermes Review Desk in By turn view, listing two turns with file and line counts, the first turn's diff of a TypeScript file, the changed-files list and a drafted commit message The Review Desk (⌘G) in By turn view in Hermes 2.3.4: two turns listed with their file and line counts, turn 1's diff, the changed-files list, and a commit message drafted from the turns. Without Hermes, the same review from inside the task's worktree is two commands. The agent's edits usually sit uncommitted until you land, so compare the working tree, not commits: git status --short git diff main -- '*.test.ts' The first lists every file the agent touched. The second should print nothing; any output there means a test was edited. When the checks pass, Land commits, opens a pull request or merges locally, into the branch the task started from. For a dependency upgrade, take the pull request: CI runs the Node versions and environments your laptop doesn't. Watch it with gh pr checks --watch. If CI disagrees, don't merge. Paste the failing job's output into a new .upgrade-ci.txt and tell the agent to read it. If you landed locally instead and main goes red, Land can be undone. What this doesn't replace Bots already open the bump PR, and both of the common ones put release notes in it. A Dependabot PR carries Release notes, Changelog and Commits sections. Renovate's docs say it filters the releases to the versions the PR updates and embeds them in the PR body. If you already use one, that is step 2 done for you. What they don't give the agent is your failure list and the rules. You don't need Hermes for the brief. Claude Code's overview lists updating dependencies among the jobs it handles, and the Codex CLI runs commands in your terminal too. Hand either one .upgrade-brief.md in a plain worktree and the method holds. Hermes adds the task's own branch and worktree, the per-turn review and Land with undo, all on your machine. It isn't affiliated with OpenAI, Anthropic or any agent vendor. I haven't run this upgrade on a real app myself. How many errors you get, and which notes they trace to, depends on which parts of the SDK your code touches. That is why you capture them before the agent starts. --- If this was useful I build Hermes IDE, free and source-available (BSL 1.1) on macOS, Windows and Linux: download it or star it on GitHub if the task-per-worktree flow saves you a broken main. If the app you're upgrading calls tools through an LLM SDK, AI That Acts is the book about that layer: tool calling, functions and a first working agent, in TypeScript. That is the code a release like 7.44.0 reaches into. ![AI That Acts — AI in TypeScript](amazon.com) The whole AI in TypeScript series, from your first LLM call to agents in production, is here.

  2. Gemini 4 Argon, 18 Days After GA: 50 on the Independent Index, 110M Tokens to Get There - Book: LLM Observability Pocket Guide - Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- Google says Gemini 4 Argon "leads the frontier on a majority of benchmarks". On the independent Artificial Analysis leaderboard, Argon (high) now scores 50. At the same high setting, Claude Opus 5.5 scores 52 and GPT-6 Astra scores 55. Argon went generally available on the paid Gemini API on October 13, as gemini-4-argon. That is 18 days of real use. On October 28, Google announced Gemini 4 Carbon, a stronger model almost nobody can call yet. What the independent numbers say Start with the leaderboard. Artificial Analysis ranks Argon (high) #13 of 58 on its Intelligence Index v5.0. It lists only the high setting for Argon, which is why the comparison above uses other models at high too. At max effort, Opus 5.5 scores 58 and Astra 57 on the same leaderboard, but Argon has no max entry to set against them. The score has moved since R&D World's October 1 check. R&D World reported 53 at the highest reasoning setting then. Artificial Analysis has since moved to Intelligence Index v5.0, which Google's Carbon post calls "recently updated," so the two numbers may not be directly comparable. I don't know whether that change explains the whole gap. Token use is the bigger story. Artificial Analysis says Argon generated 110M tokens to run the index, against a median of 70M, and calls that "somewhat verbose." At launch, R&D World put it at about 62,000 output tokens per index task versus about 27,000 for GPT-6 Astra. That verbosity is what makes the price promo matter. In the same R&D World piece, Argon cost $1.99 per index task at the introductory price and $3.98 at the standard price. Astra cost $3.26. On October 31 the leaderboard lists $7.68 per task for Argon (high), next to $3.46 for Opus 5.5 (high) and $3.24 for Astra (high). Those current numbers run on the v5.0 index, where Argon used 110M tokens, and Artificial Analysis prices Argon at the $4 / $20 per million tokens standard rate rather than today's promo. Both $3.98 and $7.68 use that list price, so the jump likely reflects the new index version; on its own it doesn't show the model got worse. !Editorial illustration of a glossy launch scorecard on a pedestal, while a long ribbon of tokens spills off a desk beside it and coils on the floor Launch charts measure the score. Your bill measures the tokens it took to get there. Google's own table already showed the coding gaps. Argon scores 55.0% on FrontierSWE v2 against Astra's 65.5%, and 57.4% on Terminal-Bench 4.0 against Opus 5.5's 66.4%, per R&D World's reading of Google's numbers. The same article reports that on Arena's agent leaderboard Argon ranked 8th overall when checked on October 1, from 3,417 sessions, with a rank range of 3rd to 16th. What developers report These are anecdotes from small groups, not studies. Read them as signals. Every's "vibe check" panel, with Dan Shipper and his team, was the most detailed early test, as summarised by BigGo. Argon was a strong structural editor. Shipper said it shaved days off an 8,000-word piece, and it generated more ideas than rival models in the panel's 3D and game tests. Where it fell down: it was weaker than Claude at checking and revising its own code. One racing game it built had a wall that made the track unplayable. Its writing tested about two grade levels harder to read than rivals'. Shipper called the Antigravity harness "a terrible harness." On Google's own developer forum, a thread titled "How bad is Gemini 4 Argon?" opened on October 16. The first post called its coding "underwhelming." A later reply described a large codebase where Argon finished a task, then kept rereading old specs and tests and started doubting work that was correct. That poster said it "prioritizes rules over correctness." Another reply granted that it is strong at knowledge work. A separate October 20 thread listed three problems: audio uploads not recognised in the Gemini web app, repeated approval prompts during pull request workflows in Antigravity, and quota that runs out fast during normal development. A reply the same day asked for reproduction steps, the Antigravity version and the OS. Not every test went against it. A Puter writing test of 140 runs, published October 19, found Argon answered opinion questions more directly than Gemini 3.8 Flash and hedged less, though it missed length limits more often. The forum reports and the Every panel point at the same weak spot: it doesn't check its own code well. Carbon: one outside number, the rest is Google's Gemini 4 Carbon is not something you can test. Google's announcement says it goes first to "a subset of cyber defenders," then to trusted partners through the Fairwind Program, and to developers and businesses after more work on guardrails, with no date. It was not in the Gemini API changelog as of October 31. One number does come from outside Google. Artificial Analysis lists Carbon (high) at 59, #1 on its index, and Google quotes that score in its post. The same page shows Carbon generated 200M tokens to run the index, against a 70M median, at $7.76 per task. So the verbosity pattern carries over. !Editorial illustration of a sealed glass display case holding a glowing orange gem, with a velvet rope in front and a small queue of silhouetted developers waiting outside Carbon on October 31: announced, benchmarked, and behind the rope for most developers. Everything else in the post is Google's own testing: SEC-bench Pro 88.2%, ExploitGym 54.8%, CWE-bench 71.0%, about half as many unsupported claims as Argon, 26% less overreach, and 0.0% attack success on Gray Swan's indirect prompt injection benchmark against Argon's 0.7%. Those may hold up. Nobody outside the access programs can check them yet. Treat them the way you would have treated Argon's launch table on day one. Measure it on your workload before December 1 Argon costs $2 / $10 per million input / output tokens through November 30, then $4 / $20 from December 1. It is not on the free tier. Output pricing includes thinking tokens, which is where a verbose model costs you. So measure tokens per task on your own prompts, and price them at both rates. With the Google Gen AI SDK for Python: from google import genai client = genai.Client() # reads GEMINI_API_KEY resp = client.models.generate_content( model="gemini-4-argon", contents="Review this diff for bugs: ...", ) u = resp.usage_metadata inp = u.prompt_token_count or 0 out = (u.candidates_token_count or 0) + ( u.thoughts_token_count or 0 ) now = inp * 2 / 1e6 + out * 10 / 1e6 dec = inp * 4 / 1e6 + out * 20 / 1e6 print(f"in={inp} out={out}") print(f"${now:.4f} now, ${dec:.4f} from Dec 1") Run your ten most common tasks through it, and the same tasks through the model you use today. Compare cost per finished task, not price per token. If you'd rather compare agents than raw API calls: I build Hermes IDE, so weigh this with that in mind. Hermes runs Antigravity CLI as a built-in agent next to Claude Code and Codex. Its model list comes from agy models, so Argon shows up if your Google account has it (Antigravity offers it on Google AI Pro, Ultra and Enterprise plans). Each task can get its own branch and worktree, so you can give Antigravity and Claude Code the same ticket on one repo. For the cost side, use the snippet above. Hermes is not affiliated with Google. !The Hermes New task sheet with its agent picker open, listing Claude Code and Codex as installed, then Antigravity CLI, Gemini CLI and other agents, plus Custom agent The ⌘N agent picker in Hermes 2.3.4, with Antigravity CLI among the supported agents. Have you moved real work to Argon since October 13? Did your token counts match the leaderboard's, or your own expectations? Tell me in the comments. --- If this was useful If you want to try Argon next to the agents you already use, Hermes IDE is free on macOS, Windows and Linux, and the source is on GitHub. Benchmark scores don't tell you what a model costs on your traffic. The LLM Observability Pocket Guide covers how to trace token use, cost and quality per request, and how to pick the tools that do it, so a model switch is a measured decision. ![LLM Observability Pocket Guide: Picking the Right Tracing & Evals Tools for Your Team](amazon.com)

  3. Code generator開発記(Part 2) ! この記事は2023年7月3日に執筆されたものです。 こんにちは。Inflabのフロントエンドエンジニア、Lucasです。 前回の記事では、code generatorの必要性と、自前で実装するに至るまでの悩みを共有しました。今回は、code generatorをはじめ、schema validator、type guardをどのように実装したのかを簡単にご紹介したいと思います。 最初にお伝えしておくと、これはとてもシンプルなアイデアで、実装に複雑な技術は必要ありません。その前提で読み進めていただければと思います。 1. 目標と必要な機能 まず、前回の記事で立てた目標について改めて考えてみます。私はcode generatorを実装する前に、次のような目標を立てました。 ファイルまたはネットワークリクエストでOpenAPI specification JSONを直接パースし、望む形の結果をファイルとして生成できること。 フロントエンドエンジニアが、APIのschemaの変化にできるだけ手間なく対応できること。 フロントエンドエンジニアが、生成物として出力したいAPIを選択できること。 APIで宣言したresponseをJSON validatorで検証できるtype guardが自動生成されること。 バックエンドエンジニアが、フロントエンドの手間を気にせずAPIを作れること。 バックエンドエンジニアが宣言したSwaggerと実際のAPI responseの形が一致しているかを、integration testで検証できること。 特に最後の目標は、Elixirのdoc testの概念を借りたものです。昨年取り組んだ個人プロジェクトでテストを書いた際の感触が良く、GraphQLがサポートしているようなschemaの整合性検証も、近い形なら実現できるのではないかと期待していました。特に私たちのバックエンドチームはテストを書くことを非常に重視しているので、OpenAPI specificationと実際のAPIの結果をテストする手軽な方法を提供できるだろうと考えました。 以下、OpenAPI specification JSONはOASと略します。 2. 使用したライブラリ まず、実装に使ったライブラリを紹介します。おそらくほとんどの方がご存じだと思います。 1. 実装 inversify 機能が増えるほど、コードベースは必然的に大きく複雑になるだろうと考えました。また、複数のオプションをサポートする予定だったので、実行時に動く実装も変わりうると見ていました。 そのため機能間の依存関係が複雑になりそうだと考え、悩んだ末に依存性注入(DI)のためのIoCコンテナを探すことにしました。私はinversifyを選びましたが、もう少し軽量に使いたいのであれば、typediという選択肢もあります。 json-schema-to-typescript 事実上、code generatorの中核となるライブラリです。Code generatorのほぼすべての実装は、このライブラリから望む結果を得るためのオブジェクトを作る作業だと言っても過言ではありません。例えば、次のようなJSON schemaがある場合、 { "title": "Example Schema", "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" }, "age": { "description": "Age in years", "type": "integer", "minimum": 0 }, "hairColor": { "enum": ["black", "brown", "blue"], "type": "string" } }, "additionalProperties": false, "required": ["firstName", "lastName"] } 次のように型を生成します。 export interface ExampleSchema { firstName: string; lastName: string; /** - Age in years */ age?: number; hairColor?: 'black' | 'brown' | 'blue'; } lodash object-traversal js-yaml オプションをYAML形式で受け取るつもりでインストールしました。今思えば、普通にJavaScriptで受け取っても問題なかったと思います。 jsonschema Code generatorが受け付けられるオプションを検証するために選びました。オプション関連のコードがif ~ elseだらけになるのを避けたかったのです。 2. ビルド、デプロイ TypeScript tsup esbuildベースのビルドツールです。速くて設定が簡単なので選びました。 Release Please ライブラリではありませんが、このGitHub Actionのおかげで手軽にデプロイできたので紹介します。 コミットメッセージの中にconventional commit形式で書かれたものがあれば、pull requestの作成からデプロイまで自動で行ってくれます。特にchange log、release、git tagなども自動で管理してくれるので、開発者はコードだけに集中できます。 ちなみに、Inflabのフロントエンドチームでは、monorepoで構成されたパッケージはchangesetsで、単独のリポジトリで運用しているパッケージはRelease Pleaseで管理しています。 commitlint 上で説明したRelease Pleaseの動作の仕組み上、開発者のミスを防ぐために導入しました。コードの変更がコミットされる前にコミットメッセージを検査し、conventional commitの形式に従っていなければコミットできないようにします。 コミットメッセージの一貫性を保ち、デプロイのためにコードを変更したのに、コミットメッセージの誤りでパッケージがデプロイされないという問題を防ぐのが狙いです。 3. テスト Jest 3. 実装の詳細 Code generatorは、大きく3つの段階に分けて実行されます。1つ目は前処理の段階です。正しいオプションが渡されたかを検証し、OASのJSONファイルを読み込みます。続いて、ユーザーが望む前処理ロジック(Pre process)を実行して、読み込んだOAS JSONを加工します。 2つ目の段階では、Taskと呼ばれる作業単位が並列に実行されます。どのタスクを実行するかは、そのタスクのオプションが正しく入力されているかどうかによって決まります。 ReactQueryTaskでは、前処理済みのOAS JSONから生成物に使うオブジェクトを作り、適切な名前と対応付けます。Query、url params、response、request bodyなど、必要と思われる要素をすべて抽出し、型とhooksに変換します。 TypeGuardTaskでは、ReactQueryTaskで作られたインターフェースを検証するtype guard関数を作ります。 SchemaValidatorTaskは、OAS JSONで宣言されたすべてのschemaを検証できるstandaloneのAjvインスタンスを生成します。Ajvについては、後ほど改めて詳しく紹介します。 Taskの並列実行が終わったら、最後の段階として、指定された場所へファイルを書き出します。フローチャートで表すと、次のようになります。 1. オプションの入力を受け取る ユーザーから希望するオプションをYAML形式で受け取るように構成しました。そのオプションは次のとおりです。 loader: fetch | file [required] schema: string [required] output-dir: string [required] react-query: [optional] output: string [required] fetcher: string [required] version: 3 | 4 [optional] default: 3 validator: [optional] output: string [required] typeguard: [optional] output: string [required] preprocess: [optional] filterDeprecated: boolean [optional] default: false extractEnum: boolean [optional] default: false apiPaths: - path: string [required] exact: boolean [optional] tags: - string [required] 2. 前処理 1. オプションの検証 YAMLファイルの有無の確認、node argumentsの入力チェック、kebab-caseのオプションのcamelCaseへの変換など、I/Oと副作用はConfigProviderが担当するようにしました。 一方、react-query、preprocessなど各オプションのパース、検証、値の設定は、ConfigLoaderを実装したオブジェクトが担います。各オブジェクトがパースしたオプションは、ConfigProviderに渡されます。こうしたのは、オプションごとの変更の影響を互いに切り離すためです。 例えば、react queryを担当するクラスは次のように実装されています。 import Ajv from 'ajv'; import type ConfigLoader from './ConfigLoader'; import type { JSONSchemaType } from 'ajv'; import { isNil } from 'lodash'; import { InvalidConfigException } from '../utils/exception'; type ReactQueryConfigOptions = { output: string; fetcher: string; version?: number; removeApiEndpointPrefix?: boolean; }; type YamlReactQueryConfig = { reactQuery?: ReactQueryConfigOptions; }; /** - 1. react-queryオプションのjson validationロジック */ const schema: JSONSchemaType<YamlReactQueryConfig> = { type: 'object', properties: { reactQuery: { type: 'object', nullable: true, properties: { output: { type: 'string' }, fetcher: { type: 'string' }, version: { type: 'number', nullable: true, default: 4, minimum: 3, maximum: 4 }, removeApiEndpointPrefix: { type: 'boolean', nullable: true }, }, required: ['output', 'fetcher'], }, }, additionalProperties: true, }; const validate = new Ajv({ useDefaults: true }).compile(schema); export type ReactQueryConfig = ReactQueryConfigOptions | false; export default class ReactQueryConfigLoader implements ConfigLoader<ReactQueryConfig> { /** - 2. react-queryのオプションの検証 */ load(optionCandidate: unknown) { if (!validate(optionCandidate)) { throw new InvalidConfigException('ReactQueryConfigLoader: not a react query config'); } /** - 3. react-queryのオプションを返す - 戻り値の型はReactQueryConfigOptions | false */ if (isNil(optionCandidate.reactQuery)) { return false; } return optionCandidate.reactQuery; } } ReactQueryConfigLoaderでパースしたオプションは、ConfigProviderのパブリックメソッドとして提供されます。 export class ConfigProviderImpl implements ConfigProvider { private loader: Loader; private options: Options; constructor() { /** - 1. yamlファイルの読み込み */ const yamlCandidate = this.loadConfig(); const reactQueryConfigLoader = new ReactQueryConfigLoader(); /** - 2. react-queryのオプションのパースを依頼 */ const reactQueryConfig = reactQueryConfigLoader.load(yamlCandidate); this.options = { reactQuery: reactQueryConfig, }; } private loadConfig() { const flags = args.parse(process.argv); const yamlPath = resolve(process.cwd(), flags.config); if (typeof yamlPath !== 'string') { throw new InvalidPathException('ConfigProvider: failed to open yaml config file'); } const yamlConfig = camelizeKeys(yaml.load(readFileSync(resolve(__dirname, yamlPath), 'utf-8'))); return yamlConfig; } getOptions(): Options { return this.options; } /** - 3. オプションが必要なオブジェクトにオプションを提供 */ taskOption(): TaskOption { return pick(this.options, ['reactQuery']); } } この構造のおかげで、ConfigLoaderを実装するだけでConfigProviderから使えるようになります。つまり、オプションはいつでも柔軟に追加・変更できます。 2. OpenAPI specificationファイルを受け取る 当初の目標の中に、「ファイルまたはネットワークリクエストでOAS JSONを直接パースし、望む形の結果をファイルとして生成できること。」がありました。これを実現するために、Loaderを実装したFileLoaderとFetchLoaderを作り、inversifyのfactory関数を使って、オプションに応じて使うべきローダーを選択するようにしました。 // inversify.config.ts const container = new Container(); /** - 1. IoCコンテナにオブジェクトを登録 */ container.bind<Loader>(TYPES.Loader).to(FetchLoader).whenTargetNamed('fetch'); container.bind<Loader>(TYPES.Loader).to(FileLoader).whenTargetNamed('file'); /** - 2. ファクトリー関数の設定 */ const loaderFactory = container .bind<interfaces.Factory<Loader>>(TYPES.LoaderFactory) .toFactory<Loader, ['file' | 'fetch']>((context) => { return (named) => { /** - 3. どのオブジェクトをどのように読み込むかを明示 */ return context.container.getNamed<Loader>(TYPES.Loader, named); }; }); // loader.ts const selectedLoader = container.getNamed(TYPES.LoaderFactory, options.loader); このように実装すると、既存のコードを修正せずに別のLoaderを追加できるというメリットがあります。例えば、databaseからOAS JSONを読み込む必要がある場合は、次のように対応できます。 class DatabaseLoader implements Loader { // ... } container.bind<Loader>(TYPES.Loader).to(DatabaseLoader).whenTargetNamed('db'); const databaseLoader = container.getNamed(TYPES.LoaderFactory, 'db'); 3. Preprocess pipe Preprocess pipeは、読み込んだOAS JSONをさまざまな条件で加工する工程です。DeprecatedなAPIを除外したり、特定のAPIだけを選択したり、enumの抽出を選択したりといった処理を担当します。preprocessのオプションはいろいろ選べ、こちらもオプションに対応する前処理のロジックが実行時に決まります。PreprocessPipeクラスが、先ほど紹介したfactory関数でPreprocessを実装したクラスを選んで配列に入れ、OAS JSONを順番に渡しながらロジックを実行します。サンプルコードは次のとおりです。 import { flow } from 'lodash'; class PreprocessPipe { private readonly preprocessOptions: PreprocessConfig; private readonly initialPreprocess: Preprocess[]; constructor( @inject(TYPES.PreprocessFactory) private readonly preprocessFactory: (name: string) => Preprocess, ) {} exec(swaggerSpecJSON: SwaggerSpecJSON | undefined): SwaggerSpecJSON | undefined { /** - 1. オプションで選択されたpreprocessの実装だけを選んで配列に入れる */ const preprocess = this.scheduledPreprocess(); const preprocessorFunctions = preprocess.map((preprocess) => preprocess.exec.bind(preprocess)); /** - 2. preprocessの実装を順番に実行 */ const result = flow(preprocessorFunctions)(swaggerSpecJSON); return result; } private scheduledPreprocess() { if (!this.preprocessOptions) { return []; } /** - enabled = trueのオプションのオブジェクトだけを読み込む */ const selectedPreprocess = Object.entries(this.preprocessOptions) .filter(([, enabled]) => enabled) .map(([name]) => this.preprocessFactory(name)); return this.initialPreprocess.concat(selectedPreprocess); } } こちらも、オプションを追加・削除しても既存のロジックに影響しないように実装しています。オプションの読み込みと検証、OAS JSONファイルの読み込み、preprocessの実行という前処理の段階は、使う場所によって実行のされ方が変わりうるため、できるだけ柔軟に設計しました。 3. ReactQueryTask 上で説明したとおり、json-schema-to-typescriptというライブラリで望む生成物を得るためにオブジェクトを作る工程です。ある意味で最も重要な部分なので、責務を細かく分けることに気を配りました。基本的な流れは次のとおりです。 Generator役のオブジェクトは、Parser役のオブジェクトに必要な部分のパースを依頼します。Parserが結果を返すと、それぞれの役割に応じて生成物を文字列として組み立てます。 Object Generatingを担当するオブジェクトは、パースされたデータを使って適切な形のオブジェクトを作ります。インターフェースとして生成される名前を指定して、json-schema-to-typescriptに渡します。例えば次のとおりです。 import { compile } from 'json-schema-to-typescript'; const queryString = queryStringParser.generate(oasJson); /** - { - "title": "V1UserListQueryString", - "type": "object", - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - } - } - } */ const result = await compile(queryString); /** - export interface V1UserListQueryString { - firstName?: string; - lastName?: string; - } */ react-queryで使うhooksを作るオブジェクトも、同じくパースされたデータから適切な形のhooksの文字列を作ります。Hooksの場合は、template literalを使えば生成結果を簡単に宣言できます。 // template.ts export default function useMutationTemplate(name: string, method: string) { const type = ${name}Response; const optionType = UseMutationOptions<${name}Response, unknown, ${name}Variables, TContext>; return `export function use${name}Mutation<TContext>({ url, options, requestOptions, }: { url: string; options?: ${optionType}; requestOptions?: RequestOptions; }) { const result = useMutation<${type}, unknown, ${name}Variables, TContext>( (variables) => fetcher.${method}(url, variables, requestOptions), options, ) return { ...result, data: result.data?.data }; }`; } // ReactMutationGenerator.ts const result = useMutationTemplate('V1UserList', 'POST'); このようにgenerate layerで作られた結果は、それぞれstring[]の形でReactQueryTaskに戻されます。ここまでの説明は長くてピンとこないかもしれませんが、次のダイアグラムを見ていただければ、思ったよりシンプルな構造であることがおわかりいただけると思います。 4. TypeGuardTask 1. Type guardとは何か TypeScriptにおけるtype guardとは、複数の型を取りうる変数について、型の推論範囲を絞り込む(narrowing)ための関数のことです。例えば次のとおりです。 // type guard const isString = (value: unknown): value is string => typeof value === 'string'; const numOrStringDivide = (value: string | number) => { if (isString(value)) { return parseInt(value, 10) / 2; // value -> string } return value / 2; // value -> number }; フロントエンドでtype guardが必要になるのは、上の例のようにprimitive typeを区別したい場合もありますが、API responseの違いを見分けて処理を分岐させなければならない場面が出てくるからです。特に、通常の関数だけでなくcomponentの中でも便利です。例えば、API responseの形に応じてレンダリングするcomponentを切り替えたい場合です。こうした場面で、Inflabのフロントエンドチームはtype guardを積極的に使っています。 const isStudent = (value: Student | Teacher): value is Student => { return isPlainObject(value) && 'grade' in value && typeof value.grade === 'string'; }; export function Profile() { // Student | Teacher const { data } = useProfileQuery(); return ( <div>{isStudent(data) ? <StudentProfile data={data} /> : <TeacherProfile data={data} />}</div> ); } 2. Type guardの落とし穴 もちろん、GraphQLを使えば__typenameというプロパティを使って簡単に区別できます。しかし、そうでない場合は一般的に、API responseの形を必要な分だけパースする方法で型を区別することになります。上の例で言えば、ある値がオブジェクトで、'grade'というstring型のプロパティを持っていれば、'Student'型とみなすことになります。 これはTypeScriptの構造的型システム(structural type system)に起因するもので、よくまとまったドキュメントが多いので、ここではこれ以上説明しません。 本題に戻ると、Inflabのフロントエンドチームではtype guardを積極的に使っていますが、メリットと同じくらいデメリットもはっきりしていました。それは、type guard自体が誤って宣言されている場合、ロジック上の誤りが表に出てこないということです。代表的なのが、API responseが変わったのにtype guardを変更し忘れたケースです。 上の'Student'型で言えば、API responseが変更されて'grade'というプロパティの型が変わったり、プロパティ自体がなくなったりするケースが考えられます。こうなると、コードレビューをしても見つけにくいだけでなく、コンパイル時には何のエラーも発生しないため、ランタイムで問題が発覚することが多いです。 3. Type guardを自動で作る こうした理由から、type guardもOAS JSONから自動で作られたらどうだろう? というアイデアが浮かびました。同僚と議論した結果、jsonschemaを使ってOASのschemaを検証できる形にし、ファイルとして出力する機能を実装することに決めました。実装全体の流れは次のとおりです。 まず、OAS JSONをJSON schemaの形に変換し、JSON schemaがサポートしていないプロパティを取り除きます。 import { draft7 } from 'json-schema-migrate'; import { unset } from 'lodash'; function convertSwaggerSpecToJSONSchema(swaggerSpec: SwaggerSpecJSON) { const componentSwaggerSpecs = swaggerSpec.components.schemas; /** - 1. OAS -> json schemaの形に変換 */ draft7( toJsonSchema(swaggerSpec.components.schemas, { strictMode: true, }), ); /** - 2. json schemaでサポートされていないプロパティを削除 */ removeUnSupportProperty(componentSwaggerSpecs); return componentSwaggerSpecs; } function removeUnSupportProperty(componentSwaggerSpecs: Record<string, JSONSchema4>) { const notSupportedType = ['default', 'deprecated', 'nullable', 'tsEnumNames', 'example']; traverse(componentSwaggerSpecs, ({ parent, key }) => { key ??= ''; if (notSupportedType.includes(key)) { unset(parent, key); } }); } こうして作ったJSON schemaを、template関数を使って文字列にすれば完成です。 const defaultTypeGuardTemplate = (jsonSchemas: Record<string, JSONSchema4>) => { return [ importTemplate(Object.keys(jsonSchemas)).trim(), 'const validator = new Validator();', Object.entries(jsonSchemas) .map(([key, schema]) => typeGuardTemplate(key, schema)) .join('') .trim(), ].join('\n\n'); }; const importTemplate = (keys: string[]) => { return ` import { Validator } from 'jsonschema'; import type { ${keys.join(', ')} } from './schema'; `; }; const typeGuardTemplate = (type: string, jsonSchema: JSONSchema4) => { const stringifyJsonScheme = JSON.stringify(jsonSchema); return ` export const ${is${type}} = (value: unknown): value is ${type} => { return validator.validate(value, ${stringifyJsonScheme}).valid; } `; }; const schema = convertSwaggerSpecToJSONSchema(oasJson); const result = defaultTypeGuardTemplate(schema); まだ完璧に動作するわけではありませんが、問題なく使えるようドッグフーディングを続けています。 5. SchemaValidatorTask 上でもお伝えしたとおり、Elixirのdoc testの考え方を取り入れ、バックエンドエンジニアが自分の宣言したOASと実際のresponseが一致しているかを、integration testの段階で検証できる機能を提供しようとしました。ライブラリにはAjvを選びました。バックエンド側で別途ライブラリをインストールしなくても、standaloneモードで動作するからです。 SchemaValidatorTaskの仕組みは、前の2つのタスクに比べてとてもシンプルです。OASをJSON schemaに変換し、standaloneモードを適用した生成物を返せば完了です。 import toJsonSchema from '@openapi-contrib/openapi-schema-to-json-schema'; import Ajv from 'ajv'; import standaloneCode from 'ajv/dist/standalone'; const schema = toJsonSchema(oasJson); const ajv = new Ajv({ schemas, code: { source: true, optimize: true, }, }); const result = standaloneCode(ajv); 6. 生成物の出力 各タスクでは、受け取ったstring配列から一定の基準で重複を取り除きます。ここまで問題がなければ、オプションで指定されたパスに結果を出力します。 4. 生成物の形 次のようなOAS JSONを受け取ったと仮定すると、各タスクの実行結果は以下のとおりです。 OAS.json{ "openapi": "3.0.0", "paths": { "/api/v1/category": { "get": { "operationId": "CategoryB2cController_findAll", "summary": "すべてのカテゴリー一覧", "description": "すべてのカテゴリー一覧を取得する。", "parameters": [], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "properties": { "statusCode": { "type": "string" }, "message": { "type": "string" }, "data": { "$ref": "#/components/schemas/CategoryAllResponse" } } } } } } }, "tags": ["カテゴリーenum API"] } } }, "components": { "schemas": { "CodeName": { "type": "object", "properties": { "code": { "type": "string" }, "name": { "type": "string" } }, "required": ["code", "name"] }, "JobGroupCodeName": { "type": "object", "properties": { "code": { "type": "string" }, "name": { "type": "string" }, "jobs": { "type": "array", "items": { "$ref": "#/components/schemas/CodeName" } } }, "required": ["code", "name", "jobs"] }, "CategoryAllResponse": { "type": "object", "properties": { "jobGroup": { "type": "array", "items": { "$ref": "#/components/schemas/JobGroupCodeName" } }, "jobSeekerStatus": { "type": "array", "items": { "$ref": "#/components/schemas/CodeName" } } }, "required": ["jobSeekerStatus"] } } } } ReactQueryTaskimport { useQuery, useMutation } from '@tanstack/react-query'; import type { UseQueryOptions, UseMutationOptions, QueryKey } from '@tanstack/react-query'; import fetcher from './Fetcher'; import type { RequestOptions } from './Fetcher'; export type Maybe<T> = T | null; export type OneOf<T> = T extends unknown[] ? T[number] : T extends readonly unknown[] ? T[number] : T; export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]-?: Exclude<T[K], undefined>; }; export type Values<T extends Record<string, unknown>> = T extends Record<string, infer U> ? U : T extends { [key: string]: infer U } ? U : never; export const enum ResponseStatus { OK = 'OK', SERVER_ERROR = 'SERVER_ERROR', DUPLICATE = 'DUPLICATE', NOT_FOUND = 'NOT_FOUND', BAD_REQUEST = 'BAD_REQUEST', BAD_PARAMETER = 'BAD_PARAMETER', CONFLICT = 'CONFLICT', UNAUTHORIZED = 'UNAUTHORIZED', FORBIDDEN = 'FORBIDDEN', PAYLOAD_TOO_LARGE = 'PAYLOAD_TOO_LARGE', TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS', } export type Success<T = unknown> = { statusCode: ResponseStatus.OK; message: ''; data: T; }; export type Error = { statusCode: Omit<keyof typeof ResponseStatus, 'OK'>; message: string; data: Maybe<''>; }; /** - @typeParam T - Response data type */ export type ApiResponse<T = unknown> = Success<T>; export const API_ENDPOINT = { V1_CATEGORY: '/api/v1/category', } as const; export const fetchV1Category = async (url: string, requestOptions?: RequestOptions) => fetcher.get<V1CategoryResponse>(url, requestOptions); export function useV1CategoryQuery({ queryKey, url, options, requestOptions, }: { queryKey: QueryKey; url: string; options?: Omit< UseQueryOptions<V1CategoryResponse, unknown>, 'queryKey' | 'queryFn' | 'initialData' > & { initialData?: () => undefined; }; requestOptions?: RequestOptions; }) { const result = useQuery<V1CategoryResponse, unknown>( queryKey, () => fetchV1Category(url, requestOptions), options, ); return { ...result, data: result.data?.data }; } /** - created by ResponseSchemaGenerator */ export interface V1Category { data: Maybe<CategoryAllResponse>; } export interface CategoryAllResponse { jobGroup: Maybe<JobGroupCodeName[]>; jobSeekerStatus: CodeName[]; } export interface JobGroupCodeName { code: string; name: string; jobs: CodeName[]; } export interface CodeName { code: string; name: string; } export type V1CategoryResponse = ApiResponse<V1Category['data']>; TypeGuardTaskimport { Validator } from 'jsonschema'; import type { CodeName, JobGroupCodeName, CategoryAllResponse } from './schema'; const validator = new Validator(); export const isCodeName = (value: unknown): value is CodeName => { return validator.validate(value, { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' } }, required: ['code', 'name'], }).valid; }; export const isJobGroupCodeName = (value: unknown): value is JobGroupCodeName => { return validator.validate(value, { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' }, jobs: { type: 'array', items: { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' } }, required: ['code', 'name'], }, }, }, required: ['code', 'name', 'jobs'], }).valid; }; export const isCategoryAllResponse = (value: unknown): value is CategoryAllResponse => { return validator.validate(value, { type: 'object', properties: { jobGroup: { type: 'array', items: { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' }, jobs: { type: 'array', items: { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' } }, required: ['code', 'name'], }, }, }, required: ['code', 'name', 'jobs'], }, }, jobSeekerStatus: { type: 'array', items: { type: 'object', properties: { code: { type: 'string' }, name: { type: 'string' } }, required: ['code', 'name'], }, }, }, required: ['jobSeekerStatus'], }).valid; }; SchemaValidatorTaskの結果は1つの複雑なファイルになるため、ここでは紹介を割愛します。 こうして作られた生成物は、フロントエンドのコードで次のように使えます。 import { useV1CategoryQuery, API_ENDPOINT } from './schema'; import { isJobGroupCodeName } from './guard'; const retrieveJobGroupCodeNameJobs = (value: unknown) => { if (isJobGroupCodeName(value)) { return value.jobs; } return []; }; export function A() { const { data } = useV1CategoryQuery({ url: API_ENDPOINT.V1_CATEGORY, queryKey: [API_ENDPOINT.V1_CATEGORY], }); return <div>{data.jobSeekerStatus.map((status) => status.name)}</div>; } バックエンドのテストコードでは、schema validatorを次のように使えます。 import validator from './validator'; describe('categoryAllResponse', () => { it('categoryAllResponseのOASと実際の結果は一致しなければならない。', async () => { const response = await controller.categoryAllResponse(); const result = validator.categoryAllResponse(result); expect(result).toBe(true); }); }); 5. まとめ 最初にお伝えしたとおり、一見すると工程は複雑に見えますが、まったくそんなことはありません。むしろ実装中に悩んだのは、責務をどう適切に分担するかと、SonarQubeのduplicate line code smellを避けるためにコードの重複をどう抑えるかでした。そのため、コード全体を通して次のような選択をしました。 各オブジェクトの責務を細かく分離するよう努めました。 各オブジェクトの横断的関心事はServiceというlayerに分け、必要なオブジェクトが必要な分だけ参照するように実装しました。 実行時に動くロジックを切り替える必要がある場合は、Strategyパターンを積極的に使いました。 同じロジックで小さな部分だけを変える必要がある場合は、Template Methodパターンを使いましたが、継承が1段階を超えないように実装しました。 実装した構造を簡単に表すと、次のようになります。 実装の紹介は以上です。 次回の記事では、code generatorを導入してから働き方がどのように変わったのかをご紹介します。

  4. 🔥 @golang just became the most-used language on @googlecloud. Java is close behind (on raw SDK API requests). Fastest growing? Python, Rust, C++, JS, and C# adding users and SDK requests rapidly. Install the Go SDK (googleapis/google-cloud-go), along with Genkit Go, and ADK Go!

    26 likes · 2 reposts · 1,132 views
  5. YouTube · · YouTube description · The Mr. Ozz · googleapis/google-api-python-client

    Build a RAG SaaS with pipeshub-ai, gridex & MagicUI | Open-Source Stack In this episode we build a private company knowledge assistant by integrating pipeshub-ai + gridex + magicui + google-api-python-client + umami. documents become a permission-aware search and answer workspace Repositories used in this architecture: - pipeshub-ai: pipeshub-ai/pipeshub-ai - gridex: gridex/gridex - magicui: magicuidesign/magicui - google-api-python-client: googleapis/google-api-python-client - umami: umami-software/umami This episode explains one integrated product architecture, including interfaces, data flow, deployment, security, and failure handling. Subscribe for daily open-source product builds: youtube.com #GitHub #OpenSource #SoftwareArchitecture #Programming

    32 views
  6. GCS Write Errors Ran for 12.5 Hours in us-central1. Spool Uploads to Disk, Drain Later - Book: System Design Pocket Guide: Fundamentals - Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- On October 8, 2026, Google Cloud Storage in us-central1 spent twelve and a half hours returning errors and timeouts on writes. Google's incident report puts the impact from 04:00 to 16:30 US/Pacific, which is 11:00 to 23:30 UTC. Every status update posted during the day carried the same line under Workaround: "None at this time." Then it came back. The final report adds a second window on October 9, 04:05 to 09:46 Pacific, in the us-central1-b zone, for a total of 18 hours and 11 minutes. If your service writes user uploads or exports straight to a bucket in that region, that was up to 18 hours in which any write could come back as a 503 or hang until your timeout fired, and the writes that did succeed were slow. Google's load balancer is not yours to fix; what your process does with the bytes when the bucket says no is. The answer fits in plain Node. Failed writes land in a bounded spool on local disk; a drainer pushes them back with jittered retries once the bucket recovers; a gauge shows how deep the backlog is. It all runs against a fake bucket you can break on command, so you can watch the spool fill and empty. What the report says, and why your client's retries run out The report is specific. A "traffic sorting optimization configuration update" to the Cloud Storage load-balancing layer bypassed capacity-aware routing and pushed write traffic onto low-capacity backend nodes, which ran out of resources. A separate metadata database issue added errors on top. Engineers tried to add emergency capacity, but per the report "this was blocked by systemic regional capacity limits." The October 9 recurrence came from an incomplete rollback of an upload planner feature. What clients saw: "Customers primarily observed HTTP 503 UNAVAILABLE errors on WriteObject operations and client-side timeouts." Reads were hit less. Other regions were fine. The official client's answer to a 503 is on the Cloud Storage retry strategy page, which lists 408, 429 and 5xx responses, plus socket timeouts, as retryable, and recommends exponential backoff with jitter. The Node.js defaults on that page: 3 retries, a 1-second initial wait, a multiplier of 2, a 64-second maximum wait, and a 600-second overall deadline. Uploads are only retried automatically when they are idempotent, meaning you passed an ifGenerationMatch precondition. So the client gives up after three retries, or ten minutes at the very most. Against a twelve-hour outage, in-request retries are a rounding error. Past that point, the bytes have to live somewhere other than the request, and they have to survive a process restart, because over twelve hours you will deploy, scale, or crash at least once. !A cloud with a cracked arrow pointing into it, and beside it a small sturdy barrel on the ground slowly filling with paper envelopes When the bucket stops accepting writes, the bytes need a place to wait that outlives the request. A bounded spool on disk The spool is a directory. One file per pending object. Each file holds a one-line JSON header with the object name, then the raw bytes. Three rules keep it honest: - Write to a temp file, then rename. A crash mid-write leaves a .tmp file that init() deletes, never a half-written entry the drainer would choke on. - Cap both file count and bytes. A spool without a cap turns a storage outage into a full-disk outage on your own host. - Reserve capacity before the first await. Twenty concurrent failures must not all pass the cap check and then all write. // spool.mjs import { mkdir, readdir, readFile, rename, rm, stat, writeFile, } from 'node:fs/promises'; import { join } from 'node:path'; import { randomUUID } from 'node:crypto'; export class SpoolFullError extends Error {} export class Spool { constructor(dir, { maxBytes, maxFiles }) { this.dir = dir; this.maxBytes = maxBytes; this.maxFiles = maxFiles; this.bytes = 0; this.files = 0; } async init() { await mkdir(this.dir, { recursive: true }); for (const f of await readdir(this.dir)) { const path = join(this.dir, f); if (f.endsWith('.tmp')) { await rm(path, { force: true }); } else if (f.endsWith('.spool')) { this.bytes += (await stat(path)).size; this.files += 1; } } } async put(name, data) { const head = Buffer.from(JSON.stringify({ name }) + '\n'); const body = Buffer.concat([head, data]); if (this.files + 1 > this.maxFiles || this.bytes + body.length > this.maxBytes) { throw new SpoolFullError(`spool full: ${this.files}`); } // reserve first, so concurrent puts respect the cap this.files += 1; this.bytes += body.length; const id = `${Date.now()}-${randomUUID()}`; const final = join(this.dir, `${id}.spool`); const tmp = `${final}.tmp`; try { await writeFile(tmp, body, { flush: true }); await rename(tmp, final); } catch (err) { this.files -= 1; this.bytes -= body.length; await rm(tmp, { force: true }); throw err; } } async list() { const names = await readdir(this.dir); return names.filter((f) => f.endsWith('.spool')).sort(); } async oldestAgeMs() { const [first] = await this.list(); return first ? Date.now() - Number(first.split('-')[0]) : 0; } async oldest() { const [first] = await this.list(); if (!first) return null; const path = join(this.dir, first); const buf = await readFile(path); const nl = buf.indexOf(10); // the header ends at the first \n const { name } = JSON.parse(buf.subarray(0, nl).toString()); const data = buf.subarray(nl + 1); return { path, name, data, size: buf.length }; } async remove(entry) { await rm(entry.path); this.files -= 1; this.bytes -= entry.size; } } flush: true makes writeFile call sync() on the handle before closing it, so the bytes are on disk before the rename publishes the entry. The option landed in Node v21.0.0 and v20.10.0 per the Node fs docs. File names start with Date.now(), so a lexical sort gives you oldest-first, and the age of the oldest entry falls out of the name without opening the file. JSON.stringify escapes newlines inside the object name, so the first raw \n in the file always ends the header. The upload path is short. Try the bucket with a timeout. If the failure is one a retry could fix, spool it. If the spool is full, put throws SpoolFullError and your caller decides what to do, which is the backpressure: return a 503 to your own client, pause a consumer, stop reading from a queue. The one thing you must not do is drop the write silently. // uploader.mjs const WRITE_TIMEOUT_MS = 2_000; const NET = ['ECONNRESET', 'ETIMEDOUT', 'EPIPE']; export function isRetryable(err) { if (err?.name === 'TimeoutError') return true; if (NET.includes(err?.code)) return true; const status = Number(err?.code); return status === 408 || status === 429 || status >= 500; } export async function upload(bucket, spool, name, data) { try { await bucket.write(name, data, { signal: AbortSignal.timeout(WRITE_TIMEOUT_MS), }); return 'stored'; } catch (err) { if (!isRetryable(err)) throw err; await spool.put(name, data); // may throw SpoolFullError return 'spooled'; } } The retryable set mirrors Google's list: 408, 429, 5xx, and timeouts. A 403 or 404 is a bug in your code or config, and spooling it would hide that bug for hours. AbortSignal.timeout() gives you a signal that aborts after the given milliseconds with a TimeoutError as its reason, which is what isRetryable checks for. Drain with full jitter The drainer is one loop. Take the oldest entry, try to write it, delete it on success. On a retryable failure, sleep a random amount between zero and an exponentially growing ceiling. That is the "full jitter" variant from Marc Brooker's Exponential Backoff and Jitter post on the AWS Architecture Blog. When a region comes back, every client that spooled is about to retry at once. Randomized sleeps spread that herd out instead of hitting the recovering backend in synchronized waves. // drainer.mjs import { isRetryable } from './uploader.mjs'; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); export async function drain(bucket, spool, opts = {}) { const { baseMs = 250, capMs = 30_000, idleMs = 1_000, timeoutMs = 5_000, signal, } = opts; let failures = 0; while (!signal?.aborted) { const entry = await spool.oldest(); if (!entry) { await sleep(idleMs); continue; } try { await bucket.write(entry.name, entry.data, { signal: AbortSignal.timeout(timeoutMs), }); await spool.remove(entry); failures = 0; } catch (err) { if (!isRetryable(err)) throw err; failures += 1; const ceiling = Math.min(capMs, baseMs * 2 ** failures); await sleep(Math.random() * ceiling); // full jitter } } } One drainer per process, writing one entry at a time, is deliberately slow. A recovering storage backend is the worst place to aim a burst. If your backlog is large enough that serial draining takes too long, run a small fixed number of these loops over disjoint entries, not a Promise.all over the whole directory. !A funnel pouring paper envelopes back up toward a cloud, the envelopes spaced at uneven random intervals rather than in a solid stream Full jitter: each retry waits a random slice of a growing window, so recovering clients don't arrive in lockstep. The real bucket adapter has one more task beyond calling the client: make the write safe to repeat. A write that timed out on your side may still have landed on Google's side. With ifGenerationMatch: 0, the request preconditions docs say the write only succeeds if no live object with that name exists, and fails with 412 Precondition Failed otherwise. For an object name that is unique per upload, a 412 on a retry means "your earlier attempt worked," so the adapter treats it as success. The preconditionOpts shape is the one Google's Node upload sample uses. // gcs-bucket.mjs import { Storage } from '@google-cloud/storage'; function abortable(promise, signal) { return new Promise((resolve, reject) => { if (signal.aborted) return reject(signal.reason); signal.addEventListener( 'abort', () => reject(signal.reason), { once: true }, ); promise.then(resolve, reject); }); } export class GcsBucket { constructor(bucketName) { this.bucket = new Storage().bucket(bucketName); } async write(name, data, { signal }) { const save = this.bucket.file(name).save(data, { resumable: false, preconditionOpts: { ifGenerationMatch: 0 }, }); try { await abortable(save, signal); } catch (err) { if (err?.code === 412) return; // an earlier try landed throw err; } } } abortable stops your code from waiting. It does not cancel the HTTP request underneath, which is why the adapter sends the precondition: the abandoned request can still succeed later, and the next attempt has to cope with that. The status-on-err.code check matches how the client's ApiError carries the HTTP status. Confirm it against the client version you pin, because a wrong check here turns every successful retry into a 412 that stops the drainer, with the entry still on disk. Watch it fill and drain The fake bucket fails on command. While failing is true, half its writes throw a 503 and the other half hang until the caller's timeout fires, which matches the two symptoms in Google's report. // fake-bucket.mjs export class FakeBucket { objects = new Map(); failing = false; async write(name, data, { signal }) { if (this.failing) { if (Math.random() < 0.5) { const err = new Error('503 UNAVAILABLE'); err.code = 503; throw err; } await new Promise((_, reject) => { if (signal.aborted) return reject(signal.reason); signal.addEventListener( 'abort', () => reject(signal.reason), { once: true }, ); }); } this.objects.set(name, Buffer.from(data)); } } The metrics endpoint exposes the three numbers you will alert on, in the Prometheus text exposition format. No client library needed. // metrics.mjs import { createServer } from 'node:http'; export function serveMetrics(spool, port = 9464) { return createServer(async (req, res) => { if (req.url !== '/metrics') return res.writeHead(404).end(); const age = (await spool.oldestAgeMs()) / 1000; res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4', }); res.end([ '# TYPE upload_spool_files gauge', `upload_spool_files ${spool.files}`, '# TYPE upload_spool_bytes gauge', `upload_spool_bytes ${spool.bytes}`, '# TYPE upload_spool_oldest_age_seconds gauge', `upload_spool_oldest_age_seconds ${age}`, '', ].join('\n')); }).listen(port); } The demo produces 50 writes a second, breaks the bucket from second 2 to second 8, and stops producing at second 10. The spool is capped at 200 files, which a six-second outage at that rate overflows on purpose, so you also see backpressure kick in. // demo.mjs (Node 20.10+; run: node demo.mjs) import { FakeBucket } from './fake-bucket.mjs'; import { Spool, SpoolFullError } from './spool.mjs'; import { upload } from './uploader.mjs'; import { drain } from './drainer.mjs'; import { serveMetrics } from './metrics.mjs'; const bucket = new FakeBucket(); const spool = new Spool('./spool-data', { maxBytes: 5 * 1024 * 1024, maxFiles: 200, }); await spool.init(); const stop = new AbortController(); const drainer = drain(bucket, spool, { signal: stop.signal, capMs: 2_000, }); const metrics = serveMetrics(spool); let n = 0; const counts = { stored: 0, spooled: 0, rejected: 0 }; const producer = setInterval(async () => { const name = `events/${n++}.json`; const data = Buffer.from(JSON.stringify({ n, at: Date.now() })); try { counts[await upload(bucket, spool, name, data)] += 1; } catch (err) { if (!(err instanceof SpoolFullError)) throw err; counts.rejected += 1; } }, 20); const report = setInterval(() => { console.log( `depth=${spool.files} in_bucket=${bucket.objects.size}`, JSON.stringify(counts), ); }, 500); setTimeout(() => { bucket.failing = true; }, 2_000); setTimeout(() => { bucket.failing = false; }, 8_000); setTimeout(() => clearInterval(producer), 10_000); setTimeout(async () => { stop.abort(); clearInterval(report); metrics.close(); await drainer; console.log('final depth', spool.files); }, 20_000); What to look for in the log lines: depth sits at 0 for two seconds, climbs once the bucket breaks, hits the 200 cap, and rejected starts counting. After second 8 the drainer's next attempt succeeds, its failure counter resets, and depth falls back to 0. On a clean run, the number to check at the end is that in_bucket equals stored + spooled: everything the spool accepted made it to the bucket, and everything it refused was reported to the caller, not lost. While it runs, curl localhost:9464/metrics shows the gauges move. Kill the process mid-outage and start it again, and init() picks the backlog up from disk. Alert on the age, not only the depth. A deep spool that is draining is fine. A shallow spool whose oldest entry is two hours old means the drainer is stuck. - alert: UploadSpoolStuck expr: upload_spool_oldest_age_seconds > 900 for: 5m - alert: UploadSpoolNearlyFull expr: upload_spool_bytes > 0.8 * 5242880 for: 2m Where this stops helping The spool needs a disk that survives the process. On Cloud Run, the container contract says the writable filesystem "is an in-memory file system, so writing to it uses the instance's memory," and that data written to it "doesn't persist when the instance stops." A spool there eats your memory limit and dies with the instance. Same story for a Kubernetes emptyDir when the pod is deleted. You want a persistent volume, or a VM disk. Ordering is not preserved. While the spool drains, fresh writes go straight to the bucket and overtake older spooled ones. If a consumer depends on write order, route every write through the spool whenever it is non-empty. list() reads the whole directory on every drain step. That is fine for thousands of entries and wasteful for millions. At that scale, keep an in-memory index built at init() or move to an append-only log with an offset file. A poison entry blocks the line. If a spooled object keeps failing with a non-retryable error, the drainer throws and stops, and the age alert above is how you find out. Decide up front whether such entries get moved to a dead-letter directory or need a human. And the spool lives in the same failure domain as your host. If the zone that holds your disk is the one having a bad day, the backlog waits with it. A second bucket in another region is the other half of this design, and it costs you reconciliation logic on read. The spool buys you hours of not losing data. It doesn't replace that decision. --- If this was useful The spool is a small instance of an old idea: when a dependency is down, absorb the load in a bounded buffer, push back on callers when the buffer fills, and recover gently with jittered retries. The System Design Pocket Guide: Fundamentals covers those building blocks, from queues and backpressure to retries, timeouts and availability trade-offs, so you can reach for the right one before the next status page says "None at this time." ![System Design Pocket Guide: Fundamentals — Core Building Blocks for Scalable Systems](amazon.com)

  7. This is a real gem: googleapis/release-please Thanks @seuros!!

    2 likes · 87 views
  8. this is from 4 months old Github commit before 3.6 flash "gemini-4-flash-preview": "gemma4" here is the link: github.com/googleapis/python-genai/commit/ca97c58… t.co

    39 likes · 1 repost · 2,217 views
  9. Google's Abirami Sukumaran Wants Java Agents to Stop Talking Directly to Databases An AI agent that can answer questions about a database is easy to demo. The difficult moment comes when a user says, “Book the 8 AM one,” and the system must remember which trip was discussed, enforce the user's identity, and create a real transaction without letting the model invent or alter privileged fields. That is the problem behind a recent Java release from Google Cloud. On October 6, 2026, Staff Developer Advocate Abirami Sukumaran and co-authors Stenal Jolly and Anubhav Dhawan announced version 1.0 of the MCP Toolbox Java SDK. Its notable idea is not that an LLM can generate SQL. It is that the model should see a narrow set of named tools while a separate service owns the database queries, authentication, and sensitive parameters. Google's announcement describes this pattern through a fictional transit concierge. This is a breakdown of that public release and its design, not a report of hands-on testing. The code and examples below are adapted from the published documentation, and vendor claims about production readiness should be validated against your own requirements. The useful question is not “Can the model write SQL?” Giving a model direct database access can look like the shortest route from a chat box to useful answers. But it also makes a language model part of the query-construction and permission boundary. A natural-language request may be ambiguous, credentials may be broader than intended, and a seemingly harmless tool can have effects that are hard to reverse. Google's example separates the pieces. The developer defines tools in a Toolbox configuration, including parameterized SQL. The Java application connects to the Toolbox server through MCP. The agent can discover and invoke the published tools, but the server-side configuration controls what those tools actually do. In the transit example, one tool searches schedules, another looks up pet policies, and a third books a ticket. The announcement's sample uses relational schedule and booking data alongside policy text and vector search. This isn't a guarantee that an agent is safe. A badly designed tool can still expose too much or perform a dangerous action. The architectural move is more modest and more useful: make the model choose among developer-defined capabilities instead of granting it a general-purpose database credential and hoping its prompt remains careful. What Sukumaran's announcement emphasizes The release note is written by a developer advocate, but the strongest part is its focus on boundaries that tend to get overlooked in prototype code. Bound parameters stay outside the model's control. The SDK supports binding values on the server side and pruning those bound parameters from tool definitions exposed to the LLM. Google gives tenant_id as an example: the application can bind a tenant context without inviting the model to choose a different tenant. This helps reduce one class of confused-deputy and cross-tenant mistakes. It does not replace authorization checks in the data service, and the bound value still needs to come from a trusted identity path. Authentication is separated from tool logic. Version 1.0 adds CredentialsProvider and AuthMethods abstractions. The announcement says credentials are resolved asynchronously for each request, with Google Application Default Credentials support included. That gives an application a defined place to obtain identity rather than hard-coding a token into a prompt or tool argument. The actual IAM policy, service identity, token audience, and user-to-data authorization remain the application's responsibility. The client is asynchronous-first. The official Java SDK repository says the client is designed around Java 17 or later, HttpClient, and CompletableFuture. Tool calls can be composed asynchronously, or deliberately blocked on with .join() when a synchronous boundary is appropriate. The distinction matters in server code: a convenience .join() inside a request path can occupy a thread while waiting, so the calling framework and execution model should guide how results are awaited. Transport and operations get practical attention. The release adds an HTTP transport abstraction, session tracking, protocol version selection, custom headers, default tool-parameter values, and a runtime warning when credentials are about to cross plaintext HTTP. None of those features is a complete operational policy. They are places where an integration library can make safer configurations easier to express and certain mistakes more visible. The team describes 1.0 as a stable, backwards-compatible foundation. That is the project's release statement, not an independent security certification or proof that every feature is equally mature across environments. A model-facing tool should be smaller than a database API A useful design exercise is to describe the model's job without mentioning the database. “Find up to five available trips between two cities” is a bounded capability. “Run arbitrary SQL against the production database” is not. For example, the Toolbox announcement shows a query-schedules tool with origin, destination, and a limit default. Its SQL uses bound placeholders rather than interpolating model-provided text into a query. The same design principle applies outside SQL: make each tool's purpose explicit, constrain its inputs, and return only the fields needed for the next decision. An application could start by publishing a read-only schedule search. After testing its access controls and result handling, it might add a booking operation that requires an authenticated user and validates the selected trip again at execution time. The booking tool should not trust a model's statement that a user has permission, nor should a hidden prompt be treated as authorization. A more complete flow has distinct checks: 1. Authenticate the person or service making the request. 2. Expose only the tools appropriate for that identity and workflow. 3. Validate tool arguments against application rules, not just a JSON schema. 4. Bind trusted context such as tenant or user identity outside the model's editable arguments. 5. Re-check availability and authorization when committing a transaction. 6. Return a clear result and record an audit event that avoids logging secrets. MCP standardizes how clients and servers exchange tool information. It does not decide which user is allowed to book, whether a query is safe, how a transaction is rolled back, or what belongs in an audit record. Those remain system design decisions. Where the example is strong, and where teams should be cautious The strongest part of the fictional transit flow is its separation between a conversational interface and the actual data operations. The agent handles the dialogue; dedicated tools define the operations; the Toolbox server mediates access. The example also makes a real application concern visible: follow-up messages depend on conversation state, while transactions depend on current business state. Those are not the same thing. Remembering “the 8 AM trip” should not mean trusting stale availability from an earlier turn. There are also gaps between a launch example and a production review. The blog's scenario is explicitly fictional. It does not establish measured latency, failure rates, resistance to prompt injection, or the security of a deployment. Nor does showing a sample schema prove that a general-purpose database agent is safe. Treat the post as a design proposal and a quickstart, then inspect the code and test your own environment. The repository itself includes an important qualification: its README says feature support is still in progress and is not at parity with the Python, JavaScript/TypeScript, and Go SDKs. It lists Java 17+ support and calls GraalVM Native Image experimental. Those notes are in the current project documentation. That is a useful counterweight to the polished “production workloads” framing in the launch post. Before adopting it, verify the exact features your application needs, the current release, and compatibility with your runtime and framework. The Google Cloud example uses Spring Boot, LangChain4j, AlloyDB, and Cloud Run. Those are choices for that demonstration, not prerequisites of the SDK. The repository describes the Java client as framework-agnostic and mentions Spring Boot, Quarkus, Jakarta EE, and custom Java applications as possible consumers. That flexibility is valuable, but you still need to decide where the MCP server runs, how it authenticates, and whether a managed database product is appropriate for your workload. A small evaluation checklist Before connecting an agent to real data, try this review against a disposable environment: - Tool scope: Can each tool be described as one narrow operation? Remove generic query or arbitrary-command tools unless there is a compelling, separately governed need. - Identity: Is caller identity derived from a trusted authentication flow, rather than from model-generated text? - Bound context: Are tenant, account, and user identifiers injected or checked by trusted code? Can the model see or change them? - Read versus write: Are search and mutation separate capabilities? Does a write revalidate current state and authorization at commit time? - Transport: Are credentials sent only over HTTPS? Does the service identity have only the permissions required for the operation? - Failure behavior: What happens if the model times out, the Toolbox is unavailable, a database call is retried, or the client loses its session? - Observability: Can you trace a tool invocation to an authenticated request without recording raw credentials or unnecessary personal data? - SDK fit: Does the Java SDK currently support the protocol and runtime features you actually depend on? Check the README and changelog rather than assuming parity from the version number. The checklist is less glamorous than an autonomous concierge demo. It is also where an enterprise integration becomes a system that can be reviewed and operated. The takeaway Sukumaran's MCP Toolbox Java SDK 1.0 announcement is interesting because it puts a concrete Java interface around a larger architectural argument: an agent should receive capabilities, not a blank check. The SDK offers building blocks for tool discovery, invocation, authentication, bound parameters, and asynchronous Java clients. Those blocks can make a carefully scoped integration easier to implement. They cannot supply your authorization model or prove the resulting application safe. The durable lesson is to keep policy and transaction rules in ordinary, testable services, let the model operate through deliberately small tools, and treat each write as an application decision rather than a conversational suggestion. For more practical writing on Java, backend systems, and AI infrastructure, subscribe for future guides and comparisons. Have you connected an AI agent to a database yet? What boundary did you find hardest to get right? Sources - Google Cloud: MCP Toolbox Java SDK v1.0 announcement, October 6, 2026 - Google Cloud's MCP Toolbox Java SDK repository - MCP Toolbox documentation

    1 reaction
  10. Conventional Commits に切り替えて、本当に変わったこと 最初期の履歴には、nuxt.js Learning、bug fixed、almost finishedというコミットが残っています。当時はそれで困らないと思っていました。数か月後、自分で変更理由を調べるまでは。 それからConventional Commitsを個人のルールとして使い始めました。2年分の履歴を集計すると、type付きコミットの比率は初期の約2%から、後期プロジェクト全体で約65%まで増えています。ただし、自動リリースやCHANGELOG生成まで整備したわけではありません。この記事では、改善した部分と、今も雑なまま残っている部分の両方を書きます。 半年後の自分が履歴を読めなかった Conventional Commitsを使って得た一番直接的なメリットは、CHANGELOGの自動生成ではなく、半年後の自分でもgit logを読めるようになったことでした。 働き始めた頃の私は、commit message を思いついたまま書いていました。2 年分のデータを後から集計してみると、いくつかの比率がなかなか目を引きます。 以下の「後期」は複数プロジェクトを合算した値です。単一プロジェクトでは24~90%程度のばらつきがあり、どのリポジトリも一様に改善したわけではありません。 観点 初期(最初のいくつかのプロジェクト) 後期(後期プロジェクト合計) semantic prefix 付き(feat/fix/refactor) 約 2% 約 65% body 付き(少し説明を足す) 約 5% 約 60%(プロジェクト間で 24〜90%) 複数行 message 約 2% 約 60% 平均 message 長 約 7 語 約 9〜10 語 数字だけを見るときれいですが、実際には定着まで時間がかかり、今も規約外のコミットが残っています。 背景:当時の commit message はどれだけ雑だったか 入社後に最初に引き継いだ業務プロジェクトは、ある製造業のお客さま向けの倉庫管理 UI でした。当時の私は Nuxt を触り始めたばかりで、commit は「ひと区切りついたら一筆書く」という状態でした。 当時のメッセージをいくつか拾ってみます(固有名詞は匿名化していますが、雰囲気はこのままです)。 nuxt.js Learning try to show pics in the xxxPage bug fixed almost finished bug repair and change some code format except the double floor task creation func, almost finish bug fixed ってどの bug を直したのか? almost finished は何が 90% 完了したのか? nuxt.js Learning の行に至っては、フレームワークを触り始めたばかりの頃の「日記」です。当時はプロジェクトが動いて、ちゃんと納品できていれば、message がこんな調子でも誰も何も言いませんでした。 問題はそのあとです。数か月後に「この状態切り替えのロジックはどの commit で変えたんだっけ」と調べに戻ると、git log --oneline を bug fixed / almost finished と一行ずつたどっていくしかなく、まったく当てになりませんでした。最後は git log -p で diff を一区切りずつ読む羽目になり、コードを読み直すより遅いくらいでした。 その頃の私はこれを「問題」だとは思っておらず、履歴を調べるのが面倒くさいな、と諦めていただけでした。 転機:自分で立ち上げたあのプロジェクトから 転機は 2025 年の半ばでした。私が初めてゼロから自分でプロジェクトを立ち上げたときです。それまではずっと他人のコードを引き継いで上に足していく形でしたが、このときはリポジトリ全体を自分で組みました。 不思議なもので、最初の commit から自分で作り始めたからこそ「今回はちゃんと書きたい」という気持ちが湧きました。Conventional Commits の規約を真面目に読んだのもこの頃です。中身は拍子抜けするほどシンプルでした。 <type>(<scope>): <summary> <body 任意> type はよく使うものが数種類だけ。feat(機能追加)/ fix(バグ修正)/ refactor(リファクタ)/ chore(雑務)/ docs(ドキュメント)。私はツールは一切入れず、そのプロジェクトから commit のたびにまず「これはどの type か」を考えてから summary を書くようにしました。「そのプロジェクトから」と書いて「初日からできた」と書かないのには理由があります。そのプロジェクトでいうと、init から、毎回ちゃんと feat:/fix: 接頭辞を付けられる状態になるまで、習慣として定着するのに 2 か月近くかかりました。規約はスイッチではなく、じわじわ育っていくものです。 ここは正直に言っておきます。私は commitlint / husky のような機械的にチェックするツールは入れていないし、チームで強制されていたわけでもありません。 最初から最後まで、自分で自分に課した一本の規律です。なので以下のデータは「一人で続けた結果」であって、「CI で弾かれてコミットできない」結果ではありません。この 2 つの重みの違いは、自分でも分かっています。 before / after:変化は測れる 初期プロジェクトと後期プロジェクトの commit を並べて比べると、差ははっきり出ます。 初期は上のbug fixedの世界でした。後期はこうなりました(同じく固有名詞は匿名化しています)。 feat: enhance area selection, fix status bugs, and add charging task support - Allow locked areas to be clicked and show selected highlight - Fix toggle status bug: use level-based lookup instead of index-based - Add helper to disable areas containing equipment racks fix: Correct reachability logic for area accessibility check refactor: extract <component> logic into composables どこが違うのか。 その commit が何をするものか一目で分かる —— feat か fix か、接頭辞をさっと見るだけで足ります body に「何を / なぜ」変えたかが書いてある —— たとえば「index ベースの参照を level ベースに置き換えた」と書いてあれば、半年後の自分でも当時のハマりどころを思い出せます git log --oneline が読めるリストになる —— almost finished の羅列ではなく データで見ると(ここでの後期の数字はいずれも後期プロジェクト合計で、単一リポジトリではありません)、semantic prefix は約 2% から約 65% へ(おおよそ 30 倍)、body 付きは 5% から約 60% へ(プロジェクトによって 24% から 90% までと均一ではありません)、複数行 message は 2% から 60% へ、平均長は約 7 語から約 9〜10 語へ。はっきり指せるマイルストーンの一つが、2025 年 9 月の feat: Add automated status monitoring です。これはそのプロジェクトの init から 2 か月以上経ってからのことで、規約がじわじわ育つものだということを改めて示しています。 本当に変わったこと 数字は表面です。日常レベルで本当に変わったのは、この 3 つだと思っています。 第一に、git log が使えるものになりました。 今は「この機能はどの commit で入ったか」を調べるとき、git log --oneline --grep=feat を一発、あるいは接頭辞をさっと眺めるだけで特定できます。以前の git log -p で diff を一区切りずつ読む苦行は、ほぼなくなりました。 第二に、変更を見直す単位が揃いました。 1コミットに1つのtypeを付けると、未来の自分も「これは機能追加か、不具合修正か」を意識してdiffを読めます。bodyに理由を残したコミットは、コードだけを読み直すより判断が速くなりました。 第三に、自分の作業ログになりました。 これは思わぬ収穫でした。body 付きの commit を書き続けると、git 履歴そのものが「この 2 年で自分が何をやって、どんな落とし穴を踏んだか」の記録になります。職務経歴書を書くときや成長を振り返るとき、この履歴は記憶よりずっと当てになります。この記事のデータも、実はこの履歴から拾い出したものです。 正直な部分:できなかったことのほうが、たぶん語る価値がある ここで記事が終わったら、ただの「規約を入れたら最高」という宣伝文になってしまいます。でも実際はそんなに円満ではありません。私ができなかったいくつかの点こそ、この記事で残したい本題です。 100% 規約化には至っていません。 後期のリポジトリですら、HEAD にはまだこんな message が残っています。 constrained the retrieval of park(only type 0 and 6) 接頭辞なし、全部小文字、思いつくまま。傾向としては、機能追加やバグ修正のような「ちゃんとした」変更はきちんと feat: / fix: を付けるけれど、ちょっとした一時的な調整になると手を抜く、という感じです。あとで気づいたことが一つあります。100% 徹底する必要は実はないし、難しいということです。重要で、あとから振り返って調べる変更を規約に沿って書ければ十分で、一行残らず完璧にと突き詰めるのは、別の種類の消耗です。 1 つの commit に複数の type が混ざっていることがよくあります。 上の feat: ... fix status bugs, and add charging task support は、厳密には Conventional Commits の「1 commit に 1 つのこと」という理想に反しています。feat であると同時に fix でもあるからです。現実には作業中の変更がいくつもの関心事をまたぐことが多く、そこまできれいに分けるには commit の粒度にもっと気を配る必要があります。これはまだ練習中です。 最大の弱点:release / CHANGELOG の自動化はまったく手を付けていません。 Conventional Commits の最大の売りは、それをもとに CHANGELOG を自動生成し、SemVer のバージョンを自動で上げられることです。でも数えてみたら —— 9 プロジェクトを通じて git tag 0 個、release 0 個でした。つまり私は規約を入れたのに、その恩恵をまだ一口も食べていないのです。接頭辞をどれだけ整えても、tag と自動化がなければ「log が読める」という一層の価値しか発揮できず、残りの大半のポテンシャルは無駄になっています。 それは最後まで個人の規律であって、チームの規約ではありませんでした。 CI 上で commitlint が弾いてくれるわけではなく、すべて自覚頼みです。良いところは導入コストがゼロなこと、悪いところは —— 気を抜くと崩れること(上の接頭辞なしの行がその証拠です)。 次に直したいこと Conventional Commitsを個人のルールとして使い始めたことで、git logは少なくとも「後から検索できる記録」になりました。一方、これは運用の入口にすぎません。今も次の課題が残っています。 接頭辞は付けるようになりましたが、自動化はまだつないでいません。残っている課題は二つです。 commitlint + huskyを入れる。 commit-msgフックで規約に合わないメッセージを止め、人の注意力だけに頼らないようにする。 tag + 自動CHANGELOGをつなぐ。 release-pleaseなどを使い、コミット履歴をリリース記録に反映する。 次に同じ規模のプロジェクトを始めるなら、初日からcommitlintを入れます。私の場合、ルールを覚えるまでに約2か月かかりました。人の注意力だけに任せるより、最初から機械に止めてもらうほうが早かったはずです。それでも、bug fixedしか残っていなかった頃と比べれば、接頭辞を付ける習慣だけでも履歴の使い勝手は大きく変わりました。 参考リンク Conventional Commits(公式仕様) commitlint release-please standard-version

Trending awards, every completed day and week
Gold
01st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
0Owned by googleapis now

No trending awards yet

None of the repositories googleapis owns has finished in the top three of a completed trending day or week, by stars, forks or PR authors. See who is trending now.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.