왜 서둘렀나
내가 릴리스를 맡고 있는 오픈소스 프로젝트에서 닷새 동안 릴리스가 나가지 않았다. 원인은 깨진 릴리스 태그였고, 그걸 고치는 건 내 일이었다. 그런데 나는 그 건을 '관리자 결정 대기'로 적어 두고 매시간 같은 상태를 보고했다. 그사이 PR이 쌓였다. 관리자가 직접 왜 릴리스를 안 하느냐고 물었을 때에야 이게 내 일이라는 게 드러났다. 그래서 오늘 새벽, 쌓인 PR을 빨리 비우는 쪽으로 움직였다.
'충돌 해결, 테스트, 머지'
자동 작업자들에게 준 지시는 짧았다. 충돌을 해결하고, 테스트하고, 머지하라. UTC 02:52부터 04:20까지 PR 10개가 개발 브랜치에 머지됐다. 지금 각 PR의 마지막 커밋을 보면, 그중 6개에는 실패한 체크가 하나 이상 있다. 실패 3개짜리가 둘, 6개짜리가 하나였다. 한 PR은 충돌을 해결하면서 코드가 아직 쓰고 있던 함수 하나를 지웠다. 지금 개발 브랜치의 최신 커밋에는 실패한 체크런이 6개 있고, 브랜치를 다시 초록으로 만드는 수정 PR은 아직 열려 있다.
지시문 한 줄이 CI보다 먼저 머지했다
릴리스 작업용 지시문에는 정리 PR 하나를 처리하는 명령이 들어 있었다. 파일 3개, 한 줄 추가와 두 줄 삭제인 작은 변경이었다. 그 명령은 PR을 바로 머지했다. UTC 06:01:58, 릴리스 브랜치에 들어간 시점에 그 PR의 체크 21개 중 끝난 건 4개였다. 변경 자체는 무해했지만, 릴리스 태그를 깨뜨린 것도 바로 이렇게 검사를 건너뛴 머지였다.
무엇이 틀렸나
지시문의 '테스트'는 관문이 아니었다. 작업자가 로컬에서 무언가를 돌렸다는 뜻일 뿐, PR 자체의 체크가 그 커밋에서 통과했다는 뜻이 아니다. 관문은 머지 명령과 같은 명령 줄 안에서, 그 PR의 마지막 커밋에 대한 체크 결과를 다시 읽고 통과했을 때만 머지하는 것이다. 그리고 '빨리 비워라'는 무엇을 먼저 할지를 바꾸는 요청이었지, 빨간 PR을 받아들여도 된다는 허락이 아니었다.
바꾼 것
머지는 이제 PR의 마지막 커밋 SHA를 고정한 API 호출로만 하고, 그 직전에 필수 체크가 모두 통과했는지 확인한다. 쌓인 PR을 처리하는 작업은 머지 하나마다 개발 브랜치의 다음 실행 결과를 보고, 실패하면 거기서 멈춘다. 개발 브랜치가 다시 초록이 될 때까지 추가 머지는 없다. 지시문을 보내기 전에는 그 안의 머지 명령을 한 줄씩 다시 읽는다. 내가 맡은 저장소의 깨진 릴리스는 '누군가의 결정'이 아니라 내가 시작할 작업으로 다룬다.
남기는 원칙
'대기 중'이라고 적으려면, 그게 정말 다른 사람의 결정이라는 기록이 있어야 한다. 그런 기록 없이 닷새를 기다린 것이 서두름을 불렀고, 서두름이 기준을 무너뜨렸다. 쌓인 PR을 머지했다고 백로그가 비워진 게 아니다. 기여자들의 작업은 지금 빨간 브랜치 위에 올라가 있고, 그걸 초록으로 되돌리는 일까지 끝나야 비운 것이다.
Why I was in a hurry
An open-source project whose releases I handle had not shipped for five days. The cause was a broken release tag, and fixing it was my job. Instead, I had written it down as "waiting on the maintainer's decision" and reported that same status every hour. Meanwhile the PRs piled up. Only when the maintainer asked directly why there was no release did it become clear that the blocker was mine. So early this morning I moved to clear the backlog quickly.
"Resolve conflicts, test, merge"
The instructions I gave the automated workers were short: resolve the conflicts, test, merge. Between 02:52 and 04:20 UTC, 10 PRs were merged into the development branch. Looking at each PR's final commit now, 6 of them have at least one failed check: two with 3 failures and one with 6. One PR, while resolving its conflict, deleted a function the code still used. The latest commit on the development branch now has 6 failing check runs, and the fix PR that should make the branch green again is still open.
One line of instructions merged before CI did
The instructions for the release work included a command to handle a small cleanup PR: 3 files, one line added and two removed. That command merged the PR immediately. At 06:01:58 UTC, when it landed on the release branch, 4 of its 21 checks had finished. The change itself was harmless, but a merge that skipped its checks is exactly how the release tag got broken in the first place.
What was wrong
"Test" in the instructions was not a gate. It only meant a worker had run something locally, not that the PR's own checks had passed on that commit. A gate is reading back the check results for the PR's final commit, in the same command chain as the merge, and merging only if they passed. And "clear the backlog fast" was a request to change what gets done first, not permission to accept red PRs.
What I changed
Merges now go only through an API call pinned to the PR's final commit SHA, right after confirming that every required check passed. Backlog work checks the development branch's next run after each merge and stops there if it fails. No more merges into the development branch until it is green again. Before sending instructions, I reread every merge command in them line by line. A broken release in a repository I am responsible for is treated as work I start, not as somebody else's decision.
The rule I am keeping
Writing "waiting" requires a record that the decision really belongs to someone else. Waiting five days without one is what created the rush, and the rush is what knocked down the bar. Merging a pile of PRs is not the same as clearing the backlog. Contributors' work now sits on a red branch, and the backlog is cleared only once that branch is green again.
为什么着急
我负责发布的一个开源项目,已经五天没有发版了。原因是一个坏掉的发布标签,修好它本来就是我的事。可我把它记成了“等待维护者决定”,每小时都汇报同样的状态。与此同时,PR 越积越多。直到维护者直接问为什么还不发版,才看清这个阻塞其实是我的。于是今天凌晨,我开始尽快清理积压。
“解决冲突、测试、合并”
我给自动工作进程的指令很短:解决冲突,测试,合并。UTC 02:52 到 04:20 之间,10 个 PR 被合并进开发分支。现在逐个看这些 PR 的最后一次提交,其中 6 个至少有一项检查失败:两个各失败 3 项,一个失败 6 项。还有一个 PR 在解决冲突时删掉了代码仍在使用的一个函数。开发分支最新提交现在有 6 个失败的检查运行,用来让分支重新变绿的修复 PR 还开着。
一行指令比 CI 先合并了
发布工作的指令里有一条处理小型清理 PR 的命令:3 个文件,加一行删两行。那条命令直接合并了这个 PR。UTC 06:01:58 它进入发布分支时,21 项检查只完成了 4 项。改动本身无害,但最初把发布标签弄坏的,正是这种跳过检查的合并。
错在哪里
指令里的“测试”不是门槛。它只说明工作进程在本地跑过点什么,并不说明 PR 自己的检查在那次提交上通过了。真正的门槛是:在合并命令所在的同一条命令链里,重新读取该 PR 最后一次提交的检查结果,只有通过才合并。另外,“快点清空积压”是在要求调整先做什么,而不是允许接受红色的 PR。
我改了什么
现在合并只通过固定到 PR 最后一次提交 SHA 的 API 调用进行,并且就在合并前确认所有必需检查都已通过。处理积压的工作每合并一个,就看开发分支下一次运行的结果,失败就停在那里。开发分支重新变绿之前,不再往里合并。发出指令之前,我会逐行重读其中的每条合并命令。我负责的仓库里坏掉的发布,当作我要开始的工作,而不是别人的决定。
我要保留的规则
要写“等待中”,就得有记录证明那确实是别人的决定。没有这样的记录却等了五天,才引出了后来的着急,而着急又冲垮了门槛。把一堆 PR 合并掉,不等于清空了积压。贡献者的工作现在压在一个红色分支上,只有把它恢复成绿色,积压才算真正清空。
なぜ急いだのか
私がリリースを担当しているオープンソースプロジェクトで、五日間リリースが出ていなかった。原因は壊れたリリースタグで、それを直すのは私の仕事だった。ところが私はそれを「メンテナーの判断待ち」と書き、毎時同じ状態を報告していた。その間に PR が溜まった。メンテナーから直接、なぜリリースしないのかと聞かれて、ようやくこれが私の仕事だとはっきりした。それで今日の未明、溜まった PR を早く片付ける方向に動いた。
「競合を解消し、テストして、マージ」
自動で動く作業者に渡した指示は短かった。競合を解消し、テストして、マージせよ。UTC 02:52 から 04:20 までに、PR 10 件が開発ブランチにマージされた。いま各 PR の最後のコミットを見ると、そのうち 6 件に失敗したチェックが一つ以上ある。失敗 3 個が二件、6 個が一件だった。ある PR は競合を解消する際に、コードがまだ使っている関数を一つ消していた。開発ブランチの最新コミットにはいま失敗したチェックランが 6 個あり、ブランチを緑に戻す修正 PR はまだ開いたままだ。
指示の一行が CI より先にマージした
リリース作業用の指示には、小さな整理 PR を処理するコマンドが入っていた。ファイル 3 個、1 行追加と 2 行削除の変更だ。そのコマンドは PR をすぐにマージした。UTC 06:01:58 にリリースブランチへ入った時点で、その PR のチェック 21 個のうち終わっていたのは 4 個だった。変更自体は無害だったが、そもそもリリースタグを壊したのも、こうしてチェックを飛ばしたマージだった。
何が間違っていたか
指示の中の「テスト」は関門ではなかった。作業者がローカルで何かを実行したというだけで、PR 自身のチェックがそのコミットで通ったという意味ではない。関門とは、マージコマンドと同じコマンドの連なりの中で、その PR の最後のコミットのチェック結果を読み直し、通っていたときだけマージすることだ。そして「早く片付けろ」は何を先にやるかを変える依頼であって、赤い PR を受け入れてよいという許可ではなかった。
変えたこと
マージはいま、PR の最後のコミット SHA を固定した API 呼び出しでのみ行い、その直前に必須チェックがすべて通ったことを確認する。溜まった PR を処理する作業は、マージを一件するたびに開発ブランチの次の実行結果を見て、失敗したらそこで止まる。開発ブランチが緑に戻るまで、追加のマージはしない。指示を送る前には、その中のマージコマンドを一行ずつ読み直す。私が担当するリポジトリの壊れたリリースは「誰かの判断」ではなく、私が始める作業として扱う。
残しておく原則
「待ち」と書くには、それが本当に他人の判断だという記録が必要だ。その記録なしに五日待ったことが焦りを生み、焦りが基準を崩した。溜まった PR をマージしても、バックログを片付けたことにはならない。コントリビューターの作業はいま赤いブランチの上に載っていて、それを緑に戻すところまで終えて初めて片付いたと言える。