Windows - TECH PLAY - TECH PLAY

TECH PLAY

Windows

イベント

マガジン

該当するコンテンツが見つかりませんでした

技術ブログ

開発2部の内原です。 自分の観測範囲では、最近Claude CodeやCodex CLI、Gemini CLIといったAIツールのCLIを使うケースが増え、以前に比べてターミナルソフトを使う機会が多くなったように感じます。 ただ、日常的に使っているとしても、このソフトウェアが何をしているかを正しく理解しているとは言えない状態でした。ターミナル、シェル、ttyの区別が曖昧だったり、Ctrl-Cでプログラムが停止するのはなぜか、文字の色付けは誰が担当しているのか、といったことを人に説明できるだけの知識は持てていないという自覚がありました。 そこでこの記事では、ターミナルソフトの挙動について改めて学んでみます。 なお、ターミナルソフトは本来MacやWindows上で動作するGUIアプリケーションとして実装されるものですが、今回の目的はあくまで内部の挙動の把握であるため、描画やフォントといったGUI部分は扱わず、その裏側の機構のみを対象とします。POSIX環境を前提に、C言語で動作を確かめる方針です。 用語の整理 まず、ターミナルを使っているときに関わってくる要素を整理します。 ターミナルエミュレータ 普段ターミナルソフトと呼んでいるGUIアプリケーション キー入力の受付と画面の描画を担当 pty(擬似端末、pseudo tty) カーネルが提供する、双方向なバイト列のやり取りに用いられる仕組み masterとslaveの2つの終端を持ち、master側にターミナルエミュレータ、slave側にシェルがそれぞれ接続される カーネル内の端末処理層(line discipline) ptyのmasterとslaveの間に位置する加工層 行編集、エコー、シグナル生成などを担当 シェルや各プログラム ptyのslaveを標準入出力として読み書きする データの流れを図にすると以下のようになります。 ターミナルのデータの流れ 両端のターミナルエミュレータとシェルはユーザー空間のプロセスで、その間のptyとline disciplineはカーネル内に存在します。 ttyという名前はteletypewriterに由来します。かつてはコンピュータにシリアル回線で物理的な端末(VT100など)を接続しており、カーネルはその回線の先にいる端末とのやりとりをttyとして扱っていました。物理端末の役割をソフトウェアで再現したものがターミナルエミュレータで、シリアル回線の代わりを務めるのがptyです。 自分のシェルがどの端末につながっているかは tty コマンドで確認できます。 $ tty /dev/ttys003 ps でTTY列を表示すると、プロセスと端末の対応が見えます。 $ ps -o pid,tty,comm PID TTY COMM 1029 ttys000 -zsh 14861 ttys000 tmux 1041 ttys001 -zsh 43561 ttys008 vim ターミナルのウィンドウやタブ、tmuxのペインごとに、別々のptyが割り当てられていることがわかります。 ptyを開いてシェルを実行 ターミナルエミュレータの中核は、ptyを確保してシェルを起動し、そのあとはバイト列を中継し続けることです。 以下の minipty.c は、 script(1) コマンドとほぼ同じ動作をする、画面描画を持たない簡易的なターミナルの実装です。GUI部分こそ存在しませんが、ターミナルソフトとやっていること自体は同じと言えます。 #define _GNU_SOURCE #include <fcntl.h> #include <poll.h> #include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #include <termios.h> #include <unistd.h> static struct termios orig; static void restore ( void ) { tcsetattr (STDIN_FILENO, TCSANOW, &orig); } int main ( void ) { // 1. ptyのmasterを確保し、対応するslaveのパスを取得 int master = posix_openpt (O_RDWR | O_NOCTTY); grantpt (master); unlockpt (master); char *slave_name = ptsname (master); pid_t pid = fork (); if (pid == 0 ) { // 2. 子:新しいセッションを生成し、slaveを制御端末にしてfd 0,1,2に接続 setsid (); int slave = open (slave_name, O_RDWR); ioctl (slave, TIOCSCTTY, 0 ); dup2 (slave, 0 ); dup2 (slave, 1 ); dup2 (slave, 2 ); close (slave); close (master); execl ( "/bin/sh" , "sh" , NULL ); _exit ( 1 ); } // 3. 親:自分の端末をrawモードに変更し、キー入力を加工なしで素通しする if ( isatty (STDIN_FILENO)) { tcgetattr (STDIN_FILENO, &orig); atexit (restore); // 終了時に元の設定へ戻す struct termios raw = orig; cfmakeraw (&raw); tcsetattr (STDIN_FILENO, TCSANOW, &raw); } // 4. 親:「キーボード → master」「master → 画面」の中継をくり返すのみ struct pollfd fds[] = {{STDIN_FILENO, POLLIN, 0 }, {master, POLLIN, 0 }}; char buf[ 4096 ]; for (;;) { poll (fds, 2 , - 1 ); if (fds[ 0 ].revents & POLLIN) { ssize_t n = read (STDIN_FILENO, buf, sizeof buf); if (n <= 0 ) fds[ 0 ].fd = - 1 ; // 入力が尽きたら以降は監視しない else write (master, buf, n); } if (fds[ 1 ].revents & (POLLIN | POLLHUP)) { ssize_t n = read (master, buf, sizeof buf); if (n <= 0 ) break ; // シェルが終了した write (STDOUT_FILENO, buf, n); } } return 0 ; } 説明のため、エラー処理は省略しています。処理は大きく4つに分かれます。 ptyの確保 posix_openpt(3) でmasterを開く grantpt(3) と unlockpt(3) で、対応するslaveを使用可能にする ptsname(3) でslaveのデバイスパス( /dev/ttys005 など)を取得 子プロセスの準備 setsid(2) で新しいセッションを作成し、元の端末から切り離す slaveを開いて制御端末にする dup2(2) でslaveをfd 0, 1, 2に複製してから、 execl(3) でシェルを起動 親プロセスの端末設定 自分自身の端末をrawモードに設定 中継 poll(2) で2つのfdを監視し、キーボードから来たバイト列はmasterへ、masterから来たバイト列は画面へ書き出す dup2(2) による複製は、 以前の記事 で扱ったリダイレクトと同じ仕組みです。 準備が終わった時点で、fdは以下のような状態になっています。 minipty実行時のfdの接続 実行して、子のシェルで tty を打ってみます。 $ tty /dev/ttys003 $ cc minipty.c -o minipty && ./minipty sh-3.2$ tty /dev/ttys005 sh-3.2$ exit exit $ 子のシェルは、元の端末( /dev/ttys003 )とは別のpty( /dev/ttys005 )につながっています。 親プロセスの処理は、2つのfdの間でバイト列を転送しているだけです。プロンプトの表示も、打った文字が画面に出るのも、シェルや次の章で扱うline disciplineの仕事であるため、このプログラム自体はデータの中身を一切解釈していません。 GUIのターミナルソフトは、この中継部分の画面側を自前の描画に置き換えたものだと考えることができます。 line disciplineとtermios ptyのmasterとslaveの間には、line disciplineという加工層が存在しています。普段は意識しませんが、ターミナルの基本的な挙動の多くはここで実現されています。 line disciplineの処理 現在の設定は stty -a で確認できます。以下はmacOSでの出力の抜粋です。 $ stty -a lflags: icanon isig iexten echo echoe -echok echoke -echonl echoctl iflags: -istrip icrnl -inlcr -igncr ixon -ixoff ixany imaxbel -iutf8 oflags: opost onlcr -oxtabs -onocr -onlret cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>; eol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V; ... 主な項目の意味は以下のとおりです。 icanon canonicalモード 入力を行単位で蓄積し、Enterが押された時点でプログラムに渡す Backspaceによる削除もこの段階で処理される echo 受け取った文字をmaster側へ送り返して画面に表示する isig intr = ^C などの特殊文字を受け取ったら、シグナルを生成する Ctrl-Cを押したとき、ターミナルソフトは0x03の1バイトを送るだけ line disciplineがそれをSIGINTに変換し、フォアグラウンドのプロセスに送ることでプログラムが停止する onlcr 出力の改行 \n を \r\n に変換する rawモード vimやless、AIツールのCLIのようなTUIアプリケーションは、line disciplineの加工をほとんど無効にしたrawモードで動作しています。1キーごとに反応し、カーソル移動や補完、エコーの表示方法を自前で制御する必要があるためです。 line disciplineの設定はtermios構造体で表され、 tcgetattr(3) と tcsetattr(3) で読み書きします。 cfmakeraw(3) を使うと、icanon、echo、isigなどの加工をまとめて無効にできます。 前章のプログラムで親プロセスが自分の端末をrawモードにしていたのも同じ理由です。このプログラムではline disciplineが2つ直列に並んでいます。 親プロセスと本物の端末の間のline discipline masterとslaveの間のline discipline(子のシェル側) 親側をrawモードにしないと、Ctrl-Cは親側のline disciplineでSIGINTに変換され、子のシェルではなく親自身が終了してしまいますし、エコーも二重になります。親側をrawにして素通しすることで、加工は子のシェル側のline disciplineだけが担当するようになります。 エスケープシーケンス line disciplineを通過した出力は、最終的にターミナルソフトに単なるバイト列として届きます。 文字の色やカーソルの移動は、バイト列の中に埋め込まれた制御用の文字列、エスケープシーケンスによって実現されています。 以下のコマンドでは赤文字で red という文字が出力されます。 $ printf '\e[31mred\e[0m\n' ESC( \e )、 [ 、数字、終端文字、で命令を表します。 上の例では ESC [31m で文字色を赤にし、 ESC [0m で装飾を元に戻しています。 以下はよく使われるエスケープシーケンスです。 シーケンス 意味 ESC [31m 文字色を赤に設定 ESC [0m 装飾をリセット ESC [2J 画面全体を消去 ESC [5;10H カーソルを5行10列目に移動 ESC [A カーソルを1行上に移動 ターミナルソフト側の処理 ターミナルソフトは、受け取ったバイト列を状態機械によってパースし、画面バッファを更新しています。 ターミナルソフトの出力処理 パーサ 通常の文字を受け取ったら、カーソル位置のセルに書き込んでカーソルを進める ESCを受け取ったら、エスケープシーケンスの読み取り状態に遷移 [ に続く数字をパラメータとして蓄積し、終端文字( m 、 H 、 J など)を受け取った時点で命令を実行 画面バッファ 行数 × 列数のセルの配列 各セルは文字と属性(文字色、背景色、太字など)を保持 描画 画面バッファの内容をフォントで描画する(今回は対象外) TERMとterminfo エスケープシーケンスは端末の種類によって異なる場合があります。そこでvimやless、AIツールのCLIのようにシーケンスを出力する側のプログラムは、環境変数 TERM で出力先の端末の種類を知ります。vimやlessは、さらにterminfoというデータベースから対応するシーケンスを引いています。 TERM 自体は、ターミナルソフトがシェルを起動する際に自身の端末種類を伝えるために設定しておくものです。 $ echo $TERM xterm-256color $ infocmp xterm-256color ... clear=\E[H\E[2J, cup=\E[%i%p1%d;%p2%dH, ... clear は画面消去、 cup はカーソル移動、などを表しています。 なおncursesなどの端末用ライブラリを利用すればこの定義を自動的に参照してくれるため、アプリケーション側でシーケンスを直接記述することを避けられます。 isattyとパイプ ここまでは、プログラムの標準入出力が端末につながっている場合の話でした。しかし標準出力の行き先はパイプやファイルの場合もあり、多くのプログラムは出力先が端末かどうかで挙動を変えています。 判定には isatty(3) を使います。 #include <stdio.h> #include <unistd.h> int main ( void ) { if ( isatty (STDOUT_FILENO)) { printf ( " \033 [32mstdout is a terminal \033 [0m \n " ); } else { printf ( "stdout is NOT a terminal \n " ); } printf ( "line 1 \n " ); sleep ( 2 ); // 端末なら1行ずつ、パイプならまとめて出力されることを確認するための待機 printf ( "line 2 \n " ); return 0 ; } 同じプログラムを3通りの方法で実行してみます。 $ ./isatty_demo stdout is a terminal line 1 line 2 $ ./isatty_demo | cat stdout is NOT a terminal line 1 line 2 $ ./minipty sh-3.2$ ./isatty_demo | cat stdout is NOT a terminal ... sh-3.2$ ./isatty_demo stdout is a terminal ... 端末に直接出力すると緑色で表示され、 | cat を付与すると色付けはなくなります。前述のminiptyの中で実行した場合も、子のシェルの標準出力はpty slaveなので端末として判定されます。 ls --color=auto や git も同じように判定しています。 ls --color=auto 端末に出力するときだけ色を付ける git log など 端末に出力するときはページャ(less等)を起動し、パイプ先では起動しない AIエージェントがコマンドを実行するとき この挙動は、AIエージェントにコマンドを実行させるときにも関わってきます。 試しにClaude Codeに tty と、シェル自身の標準入出力を確認するコマンドを実行させてみました。 $ tty not a tty $ lsof -p $$ -a -d 0-2 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME zsh 549 uchihara 0r CHR 3,2 0t0 336 /dev/null zsh 549 uchihara 1w REG 1,14 18 8392275 /private/tmp/claude-502/.../tasks/bryiflukh.output zsh 549 uchihara 2w REG 1,14 18 8392275 /private/tmp/claude-502/.../tasks/bryiflukh.output 自分の環境では、標準入力は /dev/null 、標準出力と標準エラー出力はファイルに接続されており、ptyは使われていませんでした。このため、端末上で人間が実行した場合とは以下のような違いが生じます。 色付けが行われない isatty(3) が偽になるため、 ls や git はカラー無効とする 出力が一括で行われる 行単位ではなくブロック単位でバッファリングされる 時間のかかるスクリプトの途中経過が見えないことがある 対話的なコマンドは利用不可能 標準入力が /dev/null に接続されているので、入力を求めるコマンドはすぐにEOFを受け取って終了する パスワード入力のように /dev/tty を直接開くコマンドは、制御端末がないため失敗する 標準入力がパイプのまま開いている実装では、入力を待ち続けて固まることがある まとめ ターミナルソフトが担当しているのは、以下の処理でした。 pty masterの読み書き キー入力をバイト列に変換してmasterに書き込む masterから届いたバイト列のエスケープシーケンスを解釈して描画する それ以外の、行編集やエコー、Ctrl-Cによるプロセスの停止はカーネルのline disciplineが、コマンドの解釈はシェルが担当しています。日常的に使っているターミナルソフトも、中身は数十行のCで再現できる程度の単純な機構の組み合わせであることがわかりました。
はじめに # リリースの改竄を防ぎソフトウェアサプライチェーンのセキュリティを確保する Immutable releases を試すため、個人開発の Electron アプリのリポジトリに適用したのが1年前。 https://developer.mamezou-tech.com/blogs/2025/11/16/github-immutable-releases/ それから随分経ちましたが、eMudhra のインシデントで古い Electron アプリを放置しておくのはよくないということでリリース作業を行いました。 筆者の Electron アプリのリポジトリは以下です。 https://github.com/kondoumh/sbe --> eMudhra のセキュリティインシデントについて 認証局「eMudhra」の「emSign G3」階層で、中間CAの鍵8件(TLS用6件、コード署名用2件)が復元できるほど脆弱に生成されていた問題。 窓の杜の記事より https://forest.watch.impress.co.jp/docs/news/2152996.html Jack さんのブログにあるように、Electron は Chromium の証明書ストアを同梱しており、Chromium や Node を更新してビルドし直さない限りストアが更新されません。つまり、古いバージョンの Electron で作られたアプリを配布し続けていると、信頼すべきでなくなったルート証明書をいつまでも信頼し続けてしまいます。 https://blog.jxck.io/entries/2026-10-25/emudhra.html Electron アプリはビルド時点のルート証明書を内包するため、古い Electron バージョンでビルドされたままのアプリは、失効すべき証明書を信頼し続けてしまうリスクがあるということです。 リリース用ワークフローにエラーが発生 # さて、アプリの細かい修正や Electron のバージョンアップを実施して、リリースの段階になりました。このリポジトリでは新しいバージョンのタグを作成し push するとリリース用のワークフローが起動するように設定しています。冒頭の記事で書いているように、macOS / Windows / Linux のバイナリーを strategy.matrix を使ってそれぞれのプラットフォームの Actions Runner で生成し、GitHub のリリースを作成する softprops/action-gh-release を使ってリリースしています。 今回、アプリのバージョンを v3.9.0 に bump してこのワークフローを1年ぶりに起動したところ以下のようなエラーで失敗しました。 Found release v3.9.0 (with id=432353934) ⚠️ Unexpected error fetching GitHub release for tag refs/tags/v3.9.0: HttpError: Validation Failed: {"resource":"Release","code":"custom","field":"target_commitish","message":"target_commitish cannot be changed when release is immutable"} - https://docs.github.com/rest/releases/releases#update-a-release Error: Validation Failed: {"resource":"Release","code":"custom","field":"target_commitish","message":"target_commitish cannot be changed when release is immutable"} - https://docs.github.com/rest/releases/releases#update-a-release Immutable releases にまつわるエラーのようです。 調べると、v3.9.0 のタグに2つのリリースオブジェクトが作成されているようでした。 id=432353876(draft, immutable=false): アセットに必要なファイル(sbe-Setup-3.9.0.exe, sbe-3.9.0-universal.dmg, sbe-3.9.0.AppImage)は全部揃っている。そしてまだ Imutable にはなっていない id=432353934(published, prerelease, immutable=true): アセット0件のまま Immutable として確定されてしまい、同じタグ名を握ってしまっている 初期のワークフローの実装 # エラーが発生した時点のワークフローファイルは以下のようになっていました。strategy.matrix で各プラットフォームの Runner のジョブでビルドとリリースを一気に行う形です。 Release.yml(エラー発生時点) name: Release on: push: branches: - "!*" tags: - "v*" jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [windows-latest, macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v4 - name: Setup nodejs uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm install - name: Package (Windows / Linux) if: matrix.os != 'macos-latest' run: npm run dist env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Package (macOS) if: matrix.os == 'macos-latest' run: npx electron-builder --mac --universal env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish uses: softprops/action-gh-release@v2 with: files: | dist/**/*.exe dist/**/*.deb dist/**/*.AppImage dist/**/*.dmg prerelease: true ## Pre-release の状態で作成する action-gh-release のパラメータに prerelease: true を指定しているため、それぞれの Runner でビルド成果物を Pre-release に追加するワークフローとなっています。 当時は、この設定でちゃんとドラフトのリリースが作成されていたようです。記事にも prerelease を true にすることで、ドラフトリリースを作成してくれます。 とあり、ドラフトで添付ファイルと差分を確認してから Publish をクリックするという、人間が最後に明示的に公開する手順を踏んでいました。 失敗した Pre-release は捨てるしかない # 今回、v3.9.0 の重複して作られたリリースのうち、空のまま immutable 化してしまった方を削除すれば、もう一方の(アセットが揃っている)ドラフトを publish できると考え、空のリリースを削除しました。ところが、ドラフトページから publish ボタンを押すと、今度は以下のエラーになりました。 There was an error creating your Release: tag name was used by an immutable release. Pre-release とはいえ一度 immutable なリリースに使われたタグ名は、そのリリースを削除してもリポジトリ側で「使用済み」として記録され続けるようです(改竄防止のための仕様としては筋が通っています)。v3.9.0 というタグ自体がもう使えなくなってしまったので、バージョンを上げて v3.9.1 として仕切り直すことにしました。 レースコンディションの回避 # 対策として最初に考えたのは、strategy.matrix で独立した3つのプロセスで publish が実行されることによるレースの解消です。並列の matrix ジョブが実行され、各ジョブが個別に softprops/action-gh-release を呼んで同じ v3.9.0 タグのリリースを作成・更新しようとします。そこへほぼ同時にリクエストが飛んだ結果、同じタグに対してリリースオブジェクトが複数作られるような動きになったのではないか。そこで、ビルドとリリースでジョブを分けて、ビルドだけをマトリクスで実行するように修正しました。また、バイナリを作成している electron-builder のオプションに --publish never をつけることで暗黙的な公開を抑止しました。 name: Release on: push: branches: - "!*" tags: - "v*" jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [windows-latest, macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v4 - name: Setup nodejs uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm install - name: Package (Windows / Linux) if: matrix.os != 'macos-latest' run: npm run dist -- --publish never - name: Package (macOS) if: matrix.os == 'macos-latest' run: npx electron-builder --mac --universal --publish never # electron-builder の暗黙リリースを抑止 - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: dist-${{ matrix.os }} path: | dist/**/*.exe dist/**/*.exe.blockmap dist/**/*.deb dist/**/*.AppImage dist/**/*.dmg dist/**/*.dmg.blockmap dist/**/*.zip dist/**/*.zip.blockmap dist/latest*.yml if-no-files-found: ignore publish: needs: build runs-on: ubuntu-latest steps: - name: Download artifacts uses: actions/download-artifact@v4 with: path: dist pattern: dist-* merge-multiple: true - name: Publish uses: softprops/action-gh-release@v2 with: files: | dist/*.exe dist/*.deb dist/*.AppImage dist/*.dmg dist/*.zip prerelease: true fail-fast: false にしているので、全ての Runner のジョブが終わってから後続の publish ジョブが実行されます。 Pre-release 自体が immutable になっていた # ジョブを分離したワークフローで v3.9.1 のタグを push し直したところ、3つの Runner のビルドジョブはすべて成功しました。ところが、最後の publish ジョブの Publish ステップそのものが、今度は以下のエラーで失敗してしまいました。 Cannot upload asset sbe-3.9.1-universal-mac.zip to an immutable release. ジョブは1つに統合したはずなのに、4つのアセット(exe, dmg, zip, AppImage)のうち3つまではアップロードできて、最後の1つだけ弾かれる、という中途半端な失敗でした。原因は softprops/action-gh-release 自身が出力したエラーメッセージに書かれていました。 GitHub only allows asset uploads before a release is published, but draft prereleases publish with the release.published event instead of release.prereleased. If you need prereleases with assets on an immutable-release repository, keep the release as a draft with draft: true, then publish it later from that draft and subscribe downstream workflows to release.published. つまり、Immutable releases が有効なリポジトリでは prerelease: true を指定しただけの場合、リリースの作成と同時に release.published イベントが発火して即座に公開・immutable 化されてしまうとのことでした。4つのアセットをほぼ同時にアップロードし始めたものの、その途中でリリースがロックされてしまい、間に合わなかったアセットだけ弾かれた、というわけです。 そこで、以下のように softprops/action-gh-release のオプションで draft: true を指定し、Pre-release ではなくドラフトでリリースを作成するように変更しました。 Release.yml - name: Publish uses: softprops/action-gh-release@v2 with: files: | dist/*.exe dist/*.deb dist/*.AppImage dist/*.dmg dist/*.zip prerelease: true draft: true # ドラフトモードでリリースを作成 v3.9.1 のタグも、空のまま immutable 化したリリースに使われてしまっていたため、こちらも再利用できません。結局、v3.9.2 としてもう一度タグを push し直し、ようやく4つのアセットが揃ったドラフトリリースが作成されました。ドラフトのまま変更可能な状態なので、不要な .blockmap ファイルなどを削除し、内容を確認してから publish をクリックして、無事リリースが完了しました。 何が変わったのか # softprops/action-gh-release の現在の最新版である@v3のドキュメントにはこうあります。 GitHub only allows asset uploads before a release is published, but draft prereleases publish with the release.published event instead of release.prereleased. If you need prereleases with assets on an immutable-release repository, keep the release as a draft with draft: true, then publish it later from that draft. つまり現時点で prerelease: true では即座に release.published イベントで公開・immutable 化されてしまうということです。 1年前は prerelease: true でも人間が「Publish」を押すまでは公開されない・ロックされない挙動だった(事実上 draft と同じように振る舞っていた) 今は prerelease: true だけだと即座に公開・ロックされ、 draft: true を明示しないと同じ挙動にならない。 つまり、ジョブが並列で同じリリースに書き込みに行く構造自体は1年前から変わっていない。当時それでも壊れなかったのは、「Pre-release は人間が手動 Publish するまでロックされない」という仕様のおかげで、並列書き込みが衝突する前に猶予があったから。GitHub がこの間に「Pre-release も即ロック」という形に仕様を厳格化した(おそらく、誰も手動で publish しなければ永久にミュータブルなままで immutable release の保証が骨抜きになる抜け道を塞ぐため)。そして、今回1年ぶりに実行したことでその変更の影響を受けてしまったということのようです。 --> Information 上記は推論であり、この仕様変更を明記した GitHub 公式の Changelog ブログのエントリは見つけられませんでした。確度の高い状況証拠(筆者の過去のブログの記述・softprops/action-gh-release ドキュメントの現在の注意書き)からの推測である点はご承知おきください。 さいごに # 以上、Immutable releases の仕様変更によりリリース用ビルドパイプラインがいつの間にか壊れていた話でした。 今回は v3.8.0 から v3.9.2 へのアップデートになりましたが、v3.9.0 と v3.9.1 は immutable なリリースにタグ名を奪われたことで使い捨てになっています。実際のアプリコードの変更は CWE-918 (SSRF) の修正など軽微な修正のみ。残りはすべてこのワークフロー周りの修正によるバージョン消費でした。 Matrix ビルドはジョブを並列化する有効な手段ですが、Immutable Releases のようにリソースを固定化してしまう機能と併用する際は注意が必要ということがわかりました。Immutable Release 自体はよりセキュアになっていると思いますので、安心して使っていただいてよいと思います。
はじめに こんにちは、バックエンドエンジニアのかがの( @ykagano )です。 2026年10月に開催された「パフォーマンスチューニング夜話 feat.おたのし味テック会」にゲスト登壇しました。 本記事では、当日の登壇内容やパネルトークの様子についてお届けします! イベント概要 「おたのし味テック会」は、株式会社viviONさまと虎の穴ラボ株式会社さまを中心に開催されている技術イベントシリーズです。 今回は「パフォーマンスチューニング」をテーマに、「もっと速くしたい」を肴に語り合うエンジニア交流会として、viviONさまのオフィスで開催されました。 vivion.connpass.com BASEからの登壇 ECカートの負荷テストとパフォーマンスチューニングの実践(かがの) 前回viviONさまのイベントでお話しした負荷改善の取り組みを、今回は負荷テストの環境づくりから掘り下げてお話ししました。 目標とするキャパシティを決め、Locust と WireMock で継続的に回せる負荷テスト環境を作り、検証環境を本番と同等に近づけていった流れを紹介しています。 本番と同等の負荷をかけることで、DBの設定や外部APIのRateLimitなど、検証環境との差異がいくつも見つかりました。 パフォーマンスチューニングでは、決済時ではなく、その手前の情報取得系APIのRateLimitで引っかかっていた部分の通信を削減することで決済流量を増やすことができました。 負荷対策は「ここまでの負荷なら問題ない」と関係者で共通認識を持てるところまでやり切ることが大事だと考えています。 speakerdeck.com 前回参加させていただいた、viviONさまのイベントの登壇レポートはこちらです。 devblog.thebase.in 負荷テスト環境の詳細は、こちらの連載記事でも紹介しています。 devblog.thebase.in 現地で見たセッションの感想 ここでは主催のviviONさま・虎の穴ラボさまから登壇されたお二人のセッションを、現地でXに投稿した感想とあわせて紹介します。 10年動いているサービスの実録パフォーマンスチューニング / 奥谷さん @okutann88 10年動き続けているシステムの歴史を追いながら、その時々でパフォーマンスを改善し続けてきたお話でした。 中でも、在庫のない商品は行ロックを取らないようにする工夫は、私自身も在庫管理に関わっているのでとても共感できました! 高負荷時は機能を間引くモードがあるというのも、そうした発想がなかったのでとても参考になりました! 在庫管理大事 🙂‍↕️ #otami pic.twitter.com/cgbeDvsCRF — ykagano (@ykagano) 2026年10月22日 クラウドゲーミングのパフォーマンス最適化に挑む / 粟田さん @castanea クラウドゲーミングの仕組みをよく分かっていなかったので、WineというOSSを活用してWindows向けのゲームをLinux上で動作させているところなど、知らない世界を垣間見ることができました。 映像の遅延が起きていた箇所はゲーム画面、動画エンコーダー、ネットワーク、動画デコーダーのどこでしょうか、という問いかけがあったのですが、遅延していたのはユーザーの端末だったというのは意外でした! またパフォーマンスチューニングの事例では、実際に使えるのは2コア分のCPUしかないのに8スレッドがロックを取り合っていた、というお話も面白かったです! 遅延の原因はユーザーの端末だったんだ! #otami pic.twitter.com/rodUCjCssW — ykagano (@ykagano) 2026年10月22日 パネルトーク セッションの後は、登壇者3名でのパネルトークでした。 「パフォーマンスチューニングでのAIの使い方」がテーマでした。 私からは、AIを主に設計時の壁打ちに活用していることや、設計の概要と要点をHTMLにまとめてチームで共有していることをお話ししました。 また、3人に共通していたのは、ログの調査や分析にAIを活用していることでした。AIによる分析結果をパフォーマンスチューニングに活かしているという点も共通しており、興味深かったです。 パネルトークに参加するのは今回がはじめてでしたが、奥谷さん・粟田さんのお二人がどんどん話を深掘りしてくださったおかげで、色んな視点からお話しできてとても楽しかったです! おわりに パフォーマンスチューニングという一つのテーマでも、10年動いているサービス、クラウドゲーミング、ECとそれぞれ違う現場の話が聞けたのではないかと思います。とても楽しい夜でした。 懇親会では、同じテーブルになった皆さまとも登壇内容を話題に意見交換ができてよかったです。 主催いただいた株式会社viviONさま、虎の穴ラボ株式会社さま、そしてご参加いただいた皆さまありがとうございました! BASEではエンジニアを募集しております。よろしければ、採用情報もぜひご覧いただけますと幸いです。 binc.jp

動画

該当するコンテンツが見つかりませんでした

書籍