Skip to content

fix: modified の書き換えが #998 で止まらなかった本当の原因を直す - #1009

Merged
hal-shu-sato merged 1 commit into
sourcefrom
fix/await-generated-writes
Aug 29, 2026
Merged

fix: modified の書き換えが #998 で止まらなかった本当の原因を直す#1009
hal-shu-sato merged 1 commit into
sourcefrom
fix/await-generated-writes

Conversation

@hal-shu-sato

Copy link
Copy Markdown
Member

先に結論

#998 では止まりませんでした。 マージ後のリリース 5 本を確認すると、modified 50 件は依然として毎回リリース時刻に書き換わっています。

08-23 13:20   50 件 / 全件が 2026-08-23T13:20 台
08-23 13:19   50 件 / 全件が 2026-08-23T13:19 台
08-23 13:01   50 件 / 全件が 2026-08-23T13:01 台

原因が 2 つあり、#998 はそのうち 1 つしか直していませんでした。 私の前回の検証が不十分だったためです(下の「検証の誤り」を参照)。

#998 自体は効いています

ワークフローのログで main の取得は成功しています。

##[group]Run git fetch --depth=1 origin main:main
 * [new branch]      main       -> main

つまり getListJsonFromMain()git ls-tree -r main は動くようになりました。その下にもう 1 つ原因がありました。

本当の原因: 生成の書き込みが待たれていない

util/release.ts の書き込みは 10 箇所すべてが void fs.outputJson(...) — Promise を投げっぱなしにしていて、誰も await しません。

function main(): void {
  core();      // void fs.outputJson(V3_CORE_JSON, ...) を発行するだけ
  convert();
  packages();
  scripts();
  list();      // ここで git hash-object v3/core.json を実行する
}

main() は同期的に呼ぶだけなので、list()v3/core.json をハッシュしようとする時点で、その書き込みがまだ完了していません。 probe を仕込んで直接確認しました。

[probe] list() 開始時点: v3/core.json exists = false

isDifferentFromMain() はこれを「main には在るのに作業ツリーに無い」と読み、削除された = 変更ありの枝に入ります。結果、全件が「変更あり」になります。

2 つの原因が同じ症状を出していた

getListJsonFromMain() isDifferentFromMain() 結果
#998 の前 main が無く null → 既定値で new Date() 両方無い → false 全件 now
#998 の後 前回の値を取得できる ✅ 書き込み未完了 → 「削除された」→ true 全件 now

経路が違うだけで結果が同じだったため、1 つ目を直しても表からは何も変わりませんでした。

修正

void fs.outputJson(fs.outputJsonSync( に置き換えます(10 箇所)。main() を同期のまま保てて、レースが消えます。生成するファイルは 100 個程度なので同期 I/O のコストは問題になりません。

公開データは壊れていません

Node はイベントループが空になるまでプロセスを終えないので、ファイル自体は最終的に必ず正しく書かれます。壊れていたのは 1 回の実行の中の順序だけです。

検証(まっさらな shallow clone、CI と同一条件)

--depth=1 --branch source でクローンし、main を取得し、v3/SPECIFICATION.md だけの状態から 1 回だけ実行しました。

修正前 修正後 公開中(main)
core.modified 2026-08-23T20:14:43.955Z 2026-08-23T13:20:49.752Z 2026-08-23T13:20:49.752Z
convert.modified 実行時刻 13:20:49.756Z 一致
packages[] 48 件 全件が実行時刻 全件が 13:20 一致

生成物そのものも変わっていないことを確認しました。

  • スキーマ検証: 100 / 100 valid
  • core.json / convert.json / scripts.json: 公開中とバイト一致
  • packages/*.json 96 件すべてが公開中とバイト一致

検証の誤り(前回)

#998 で「1 回目と 2 回目が同じ値になった」と報告しましたが、あの作業ツリーには前の実行で作った v3/ が既に残っていました。ファイルが存在する状態を測っていたので、実質 2 回目以降の挙動しか見ていませんでした。

CI の実行は毎回が 1 回目です。 今回はクローンからやり直して 1 回目を測っています。

この後

マージ後の最初のリリースでは、main 上の内容と生成物が一致するファイルの modified2026-08-23T13:20:49 台のまま固定されるはずです。それが確認できれば止まったと言えます。既に公開されてしまった値そのものは直りません(#998 に書いたとおりです)。

#998 made the main branch available so isDifferentFromMain() could compare
against it, and the fetch does work — the workflow log shows
"* [new branch] main -> main". Timestamps kept churning anyway, because
there is a second cause underneath.

The ten writes in this file are `void fs.outputJson(...)`: fire-and-forget
promises that nothing awaits. main() then calls core(), convert(),
packages(), scripts() and list() synchronously, so list() reaches
`git hash-object v3/core.json` while that write is still pending. A probe
confirms it directly — at the top of list(), fs.existsSync(V3_CORE_JSON)
is false on a clean checkout.

isDifferentFromMain() reads that as "exists on main, missing in the
workspace", takes its deleted-on-workspace branch, and reports every file
as changed. Before #998 the same runs ended up at "now" by a different
route: with no main branch, getListJsonFromMain() returned null and the
fallback stamped everything with new Date(). Two independent causes, one
symptom, which is why fixing the first one alone changed nothing visible.

Node drains the event loop before exit, so the files themselves are always
written correctly and the published data has never been wrong — only the
ordering within a run was. Making the writes synchronous removes the race
without changing what is produced: 100 of 100 files still validate, and
every generated file is byte-identical to the copy currently on main.

Verified on a fresh shallow clone made the way the runner makes it, which
is what the earlier check got wrong: it reused a workspace where a previous
run had already left v3/ in place, so it exercised the second run rather
than the first, and every CI run is a first run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hal-shu-sato
hal-shu-sato merged commit ae83372 into source Aug 29, 2026
3 checks passed
@hal-shu-sato
hal-shu-sato deleted the fix/await-generated-writes branch August 29, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant