Tumgik
0-9 · 1 year
Text
社内github actions現状確認
いろいろ動くようになってきていたのでまとめた。
初期設定
社内action側の https://github.com/${org}/${repo}/settings/actions にある、 Access を Not accessible 以外にする。
できること
過去、 using: "composite" 内から uses: を使うことができなかったが、使えるようになっていた。
呼び出された側のactionでローカルファイルを参照した場合、呼び出し側のローカルファイルが参照される。
参照側
- uses: ${org}/${repo}@${tag or branch}
or
- uses: ${org}/${repo}/${dir_name}@${tag or branch}
${dir_name} が指定された場合、参照先 ${repo} 内の /${dir_name}/action.yml が呼び出される。
ただし、 ${dir_name} が /.github/workflows/${workflow} を参照している場合、workflowの参照とみなされる。 (もしかしたらファイルを参照しているとworkflow、ディレクトリを参照しているとactionとみなしてるかも?)
複数actionsの運用
action毎にrepositoryを作成する
Pros
一般的なgithub actionのプラクティスを適応できる
Cons
管理するrepositoryが増える
一つのrepository内でbranch毎に別のactionを作成し、参照側はbranch指定で呼び出す
Pros
(3と比較して)一般的なgithub actionのプラクティスを適応できる
リポジトリが増えない
Cons
参照側でtagを指定できないため、バージョン管理が煩雑 (ただし、そもそもgithub actionsは @v1 のような指定をしても v1 というタグの指定にしかならないため、 branch_v1 のようなbranch名での運用でも大差ないかもしれない)
開発時に「どのブランチが外部から参照されているか?」が不明なため、間違って参照されているブランチが削除される懸念がある (main branchに外部提供しているactionを列挙し、各branchのCIからmain branchのactionを呼び出して、検証することはできそう)
一つのrepository内でディレクトリを分けてactionを作成し作成し、参照側はディレクトリ指定で呼び出す
Pros
リポジトリが増えない
ディレクトリ毎にactionを管理できるので一覧性が高い
Cons
tag, branchを指定したバージョン管理は困難 (/action_v1 、/action_v2 のようなディレクトリ名を使ったバージョン管理になりそう)
一般的なgithub actionsのプラクティスの適応は困難(monorepoと思えばいけるかも?)
テスト
.github/workflows/on_push.yml に以下のような内容を書く
name: test on: push jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ./ with: example: aaa # 必要に応じてaction実行後の結果が正しいかの検証を行う
6 notes · View notes
0-9 · 1 year
Text
APIのレスポンスで404が返ってきたら強いエラーを出す
サーバ側では404エラーはよくあることなのでそんなに重要なエラーとはみなされない。 (ユーザが適当なリクエストを投げるのはよくあることなので)
クライアント側でAPIレスポンスのエラーはよくあることなのでそんなに重要なエラーとはみなされない。 (ユーザ側の事情でリクエストが切断されることはよくあるので)
ただ、クライアント側でAPIレスポンスが404を返す場合にはよっぽどのことが起こっている可能性が高いので強いエラーを返す。 (クライアント側のAPIリクエストパスが間違っているか、サーバ側で間違ってまだ使っているAPIパスを消してしまったか)
1 note · View note
0-9 · 1 year
Text
webpack 5でmax_line_lenを指定する方法
0-9, webpack 4でmax_line_lenを指定する方法 のWebpack 5版
こっちはそこまで遅いということはなかった
const TerserPlugin = require('terser-webpack-plugin'); module.exports = { mode: "production", plugins: [ new TerserPlugin({ terserOptions: { format: { max_line_len: 160, } } }), ], };
1 note · View note
0-9 · 2 years
Text
ブラウザ経由マイナンバーカード公的個人認証現状確認会
この記事はCivicTech & GovTech Advent Calendar 2021の4日目(代打)の投稿です。
ブラウザ経由でマイナンバーカードを使った公的個人認証の現状に関してまとめてみました。
PC(WebUSB)
マイナンバーカードはUSBのカードリーダーで読み込めるため、WebUSB経由であればブラウザから公的個人認証が可能です。
実際にソニーからPaSoRi用WebUSBのSDKが公開されていて、Web上に事例も公開されています。
ただし、WebUSBは基本的に接続先のUSB機器と直接通信する必要があるため、異なる機器への通信にはその機器向けのコードを記述する必要があります。 (Windowsはドライバの変更のみ必要)
Mobile(WebNFC)
マイナンバーカードとはNFCで通信できるため、WebNFCをサポートしているAndroid Chromeでマイナンバーカードが読めるか試してみましたがダメでした。
仕様上の課題に関してはSupport ISODepでのやり取りされています。 また、公的個人認証のためにはSecure Element対応も必要ですが、GlobalPlatform/WebApis-for-SEを見ると進展はないように見えます。
まとめ
PCは特定のカードリーダーであればブラウザからマイナンバーカードを使った公的個人認証が可能です。 (ただし、不特定多数のカードリーダーへの対応は現実的ではないかもしれません)
Mobileは現状不可能で、今後のハードルもかなり高そうな印象でした。
1 note · View note
0-9 · 3 years
Text
Sentry SDKの設定にテストを書く
社内でSentry SDKを使っているプロダクトが複数あり、ここに設定していくのが大変だったので設定を共有化した。 (特にフロントエンドは共通で弾きたいエラーが多いので)
その際「設定が正しく書かれているか?」をテストしたくなり色々やってみた。
Sentry SDK設定の共通化方法
こんな感じのpackageを社内のgithub packagesで公開している。
import * as Integrations from "@sentry/integrations"; import * as Sentry from "@sentry/browser"; import { Event, EventHint } from "@sentry/types/dist/event"; const setDenyUrls = ( denyUrls: Sentry.BrowserOptions["denyUrls"] ): Sentry.BrowserOptions["denyUrls"] => { return [ // localhostでのエラーはsentryへ送らない(port numberは可変なため指定しない) /^http:\/\/localhost:\d+\//, // ページを保存した状態はsentryへ送らない /^file:/, // ngrokは送らない /^https?:\/\/\w+\.ngrok\.io\//, //拡張系のエラーは無視する /^(chrome|safari)-extension:\/\//, ...BaseDenyUrls ]; }; const setIntegrations = ( integrations: Sentry.BrowserOptions["integrations"] ): Sentry.BrowserOptions["integrations"] => { return ( integrations || [ new Integrations.CaptureConsole(), new Integrations.Dedupe(), new Integrations.ExtraErrorData(), ] ); }; const setIgnoreErrors = ( ignoreErrors: Sentry.BrowserOptions["ignoreErrors"] ): Sentry.BrowserOptions["ignoreErrors"] => { return [ ...(ignoreErrors || []), "onSvFinishLoading", "Failed to fetch", "Network request failed", "timeout of 0ms exceeded", "サーバに接続できませんでした", "ネットワーク接続が切れました", "キャンセルしました", "Đã mất kết nối mạng.", "AbortError: Aborted", "Network Error", "Request aborted", "A network error occurred", "window.webkit.messageHandlers", ]; }; type MatcherType<l extends string r> = { [key in L]: () => R }; const extraFilter = <r>( extra: Event["extra"], matcher: MatcherType ): R => { if ("string" !== typeof extra?.["xxx"]) { return matcher.unmatch(); } if (extra["xxx"].match(/xxx/)) { return matcher.match(); } return matcher.unmatch(); }; const contextsFilter = <r>( contexts: Event["contexts"], matcher: MatcherType ): R => { if ("object" !== typeof contexts?.["ServerParseError"]) { return matcher.unmatch(); } if ("string" !== typeof contexts["ServerParseError"]["bodyText"]) { return matcher.unmatch(); } const bodyText = contexts["ServerParseError"]["bodyText"]; if (bodyText.match(/xxx/i)) { return matcher.match(); } return matcher.unmatch(); }; export function makeInit (options: Sentry.BrowserOptions): Sentry.BrowserOptions { if ("enabled" in options && options.enabled === false) { return options; } options.denyUrls = setDenyUrls(options.denyUrls); options.integrations = setIntegrations(options.integrations); options.ignoreErrors = setIgnoreErrors(options.ignoreErrors); const beforeSend = options.beforeSend; options.beforeSend = (event, hint?: EventHint) => { return extraFilter(event.extra, { match: () => null, unmatch: () => contextsFilter(event.contexts, { match: () => null, unmatch: () => beforeSend?.(event, hint) || event, }), }); }; return options; }
Sentry SDK設定に対するテスト
こんな感じでテストを書いている。 (DevDependenciesに@sentry/nodeを含めている)
以下の方法はsentry@v7で動かなくなった。
Sentry Testkit for JavaScript | Sentry Documentationが案内されていたので、- Sentry-Testkit - Testing Sentry Reports Became Easyを使用する。
import * as Sentry from "@sentry/node"; import { Transport } from "@sentry/types/dist/transport"; import { Status } from "@sentry/types/dist/status"; import SentrySetting from "../src"; import { NodeOptions } from "@sentry/node/dist/types"; import { captureEvent } from "@sentry/minimal"; class TestTransport implements Transport { public static sendEventMock = jest.fn(); sendEvent(event) { TestTransport.sendEventMock(event); return Promise.resolve({ status: Status.Skipped, }); } close() { return Promise.resolve(true); } } const ExpectMockCall = async (result: "filtered" | "throwing") => { // fakeTimerもsetTimeout(r)もだめだったので50ms待つ // (実際は10msでも行けたけど念の為5倍) await new Promise<void>((r) => setTimeout(r, 50)); const count = TestTransport.sendEventMock.mock.calls.length; expect(count ? "throwing" : "filtered").toStrictEqual(result); }; describe("Sentry SDK settings", () => { afterEach(() => { TestTransport.sendEventMock = jest.fn(); }); const sentryOptions: NodeOptions = { dsn: "正しいDNSをSentry設定画面から取得して書く。ただし、Transportで送信をブロックしているためSentryへは送信されない", transport: TestTransport, }; it("mock call", async () => { Sentry.init(sentryOptions); Sentry.captureException(new Error("test")); await ExpectMockCall("throwing"); }); it.each([ ["filtered", "onSvFinishLoading"], ["filtered", "Request aborted"], // ignoreErrorsは部分一致なので、追加の文字があってもフィルタされる ["filtered", "Request aborted hoge"], ["throwing", "hoge"], ] as const)("ignoreErrors: %s %s", async (result, error) => { Sentry.init(SentrySetting.makeInit(sentryOptions)); Sentry.captureException(new Error(error)); await ExpectMockCall(result); }); it.each([ // このファイルのファイル名を書く ["filtered", "sentry.test.ts"], ["throwing", "hoge"], ] as const)("denyUrls: %s %s", async (result, url) => { Sentry.init( SentrySetting.makeInit({ ...sentryOptions, denyUrls: [url], }) ); Sentry.captureException(new Error("error")); await ExpectMockCall(result); }); it.each([ ["filtered", "xxx"], ["throwing", "{}"], ] as const)("extraFilter: %s %s", async (result, xxx) => { Sentry.init(SentrySetting.makeInit(sentryOptions)); Sentry.captureEvent({ extra: { xxx, }, }); await ExpectMockCall(result); }); it.each([ ["filtered", "xxx"], ["throwing", "{}"], ] as const)("contextsFilter: %s %s", async (result, bodyText) => { Sentry.init(SentrySetting.makeInit(sentryOptions)); Sentry.captureEvent({ contexts: { ServerParseError: { bodyText, }, }, }); await ExpectMockCall(result); }); });
これで「実際にエラーが起こった場合に正しくFilterされるか?」をテストできる。
ちなみに、上記で設定している extra はSentryの設定画面で言うところの ADDITIONAL DATA にあたる。
テスト書いてて気づいたけど、 ignoreErrors は文字列指定では部分一致なので注意。 (完全一致にする場合、正規表現で ^xxx$ と指定する必要がある)
0 notes
0-9 · 3 years
Text
storybookをgithub pagesでhostingする方法
プロダクト支援チームでkintoneのStorybookをホスティングした話 - Cybozu Inside Out | サイボウズエンジニアのブログ で紹介されていた「GitHub Pagesでhosting」をやっているので方法を紹介します。
実際に開発してくれたのは shinya-maruyama ですが、サンプルコードを公開したのでここで紹介したいと思います。
サンプルリポジトリはこちらです
動作
以下のような動作を行います。
PR毎にStorybookをbuildし、PR毎に個別のURLを発行する
PR毎の個別URLをPR commentに投稿する
PRが閉じられたらPR毎の個別URLを破棄する
特定のbranch(ex. main, develop)へのmerge時に、特定のURLのstorybookを更新する
利点
他の様々な方法でも「PR毎に個別URLを発行する」、「特定のbranch(ex. main, develop)のstorybookは常に特定のURLで保持される」のはよく行われます。
しかし、github pagesを使うと認証をgithubに任せることができるため、外部への公開状態を制御したい場合に非常に便利です。
欠点
PRを生成するたびにgitのcommit logが追加されるので、「開発と無関係なcommit logが増えるのは嫌」という場合には他の方法のほうが良いかもしれません。
その他
github actionsとしてまとめようかと思いましたが、依存関係的に難しそうだったので一旦サンプルコードとして公開しました。
0 notes
0-9 · 3 years
Text
FastlaneでError with uncommitted changes in working directory
もし該当ファイルが更新されてはまずい場合はちゃんと直す。
該当ファイルが更新されても問題ない場合は .gitignore 追記すれば無視してくれる。
0 notes
0-9 · 3 years
Text
package.jsonが複数ある場合(npm or yarn) workspaceへの移行方法
最初からworkspaceで開発する場合、何も考えずに設定すればいい。
ただ、開発が進んでからworkspaceへ移行する場合、何も考えずに移行すると依存関係で問題が発生する可能性がある。
簡単な移行方法
一番上のpackage.jsonでworkspaceの設定を行い、各lock file(package-lock.jsonやyarn.lock)を削除すればいい。 (もし、package.jsonでresolutionsを指定している場合、まとめて一番上のpackage.jsonに指定する)
ただし、この場合、各lock fileで指定されているversionは無視されるため、意図せず依存関係が更新される問題が有る。
依存関係が更新されても良い場合、この方法で更新して良い。
安全な移行方法
もし、依存関係が更新されると困る場合、以下の方法で移行する。
package.jsonの依存関係から "^" を削除する できる限りパージョンを固定する
lock fileを作り直す yarnの場合 rm -f yarn.lock && yarn 、 npmの場合 rm -f package-lock.json && npm i する
lock fileの差分を確認し、問題ないか確認 ここで動作確認も行なう
すべてのlock fileを削除し、workspaceへ移行する 3で動作確認ができれいれば安全に移行できる
ある程度のバージョン更新は行われるが、ある程度安全に移行できるはず。
2 notes · View notes
0-9 · 3 years
Text
renovatebot運用時にpackage.jsonの依存関係versionになにを指定するか
renovatebot運用時にpackage.jsonの依存関係versionになにを指定するかの話
package.jsonの依存関係
package.jsonの依存関係は通常以下のような指定になっている。
"dependencies": { "axios": "^0.21.1" },
この形式はバージョン番号をいい感じで管理してくれるが、依存関係がタイミングによって変わり、安定した管理ができない。
この問題を解決するため、現在ではyarn.lockやpackage-lock.jsonをあわせてcommitすることが行われている。
renovatebot運用時になにを指定するか
renovatebot(or dependabot)を運用する場合、バージョンはbotが管理して随時更新される。
"^0.21.1"のような形式で指定すると基本的にはpackage-lock.jsonやyarn.lockのみが更新されるが、"0.21.1"のような形式で指定するとpackage-lock.jsonやyarn.lockと一緒にpackage.jsonのバージョン番号も更新してくれる。
そこで以下のような選択肢がある。
"^0.21.1"形式
npm(or yarn)で追加したときとおなじ形式
Pros
renovatebot(or dependabot)の差分が少ない
Cons
package.jsonからlock file(package-lock.json、yarn.lock)の再現性が低い
"0.21.1"形式
npm(or yarn)で追加したときから^を消した型式
Pros
package.jsonからlock file(package-lock.json、yarn.lock)の再現性が高い
Cons
renovatebot(or dependabot)の差分が多い
"latest"
version番号に常に最新を意味する "latest" (or "*")と指定する(その場合でもpackage-lock.jsonやyarn.lockで指定されたバージョンが使用される)
Pros
renovatebot(or dependabot)の差分が少ない
package.jsonのバージョン指定をすべて統一できる
Cons
package.jsonからlock file(package-lock.json、yarn.lock)の再現性が低い
どれを選択するか
基本的に「lock file(package-lock.json、yarn.lock)の再現性」をどこまで重視するかという判断になる。
個人的には上記2を採用するのが良いのではと考えている。
lock file(package-lock.json、yarn.lock)の再現性
基本的にgit等のバージョン管理システムを使用している場合「ファイルが失われる」リスクはかなり低いため、lock file(package-lock.json、yarn.lock)の紛失は懸念していない。
ただし、workspaceへの移行する場合など、lock file(package-lock.json、yarn.lock)の依存関係が失われる場合はあり、その場合lock file(package-lock.json、yarn.lock)の再現性は重要になる。
その他
package.jsonでバージョンを指定していてもpackage-lock.json、yarn.lockは必要か?
package.jsonでバージョンを直接指定している場合(上記2のパターン)、すべての依存関係のバージョンが固定されるためpackage-lock.json、yarn.lockが必要無いのではという疑問があるかもしれない。
この場合、直接の依存先は常に同じバージョンになるが、依存先のpackage.jsonでは "^0.0.0" 型式で指定されているため「依存先の依存先」のバージョンが固定できない。
(もし、依存先のバージョン指定も全て "0.0.0" 型式で指定されている場合、package-lock.json、yarn.lockはいらなくなる)
1 note · View note
0-9 · 3 years
Text
CIのcacheに使うkeyをどう管理するか
CIのcacheに使うkeyをどう管理するか
- uses: actions/cache@v2 with: path: node_modules key: ${{ 'ここをどうするか' }}
node_modulesをcacheする場合、基本的には以下のような情報をkeyとして使用する。
${{ runner.os }} node_modulesはOS依存の内容が含まれるため、OSが変わると使用できない
node_version(node12とかnode14とか) node_modulesはnodeのバージョン依存の内容が含まれるため、nodeのバージョンが変わると使用できない
${{ hashFiles('yarn.lock') }} lockファイルのhash
しかし、これだけだと以下のような理由でcacheをクリアしたくなったときに困る。
nodeのバージョンは変わらないが、yarnのバージョンは変えたい keyにyarnのバージョンを含めてもいいかもしれない
cacheの状態が不安定で内容が壊れている
気分
そのため、keyには以下のような方法でcacheバージョン的なものが付与されることが多い。
直接記述
直接keyの中にcache1と書いて数字を増やす
Pros
設定ファイルを見れば現在のバージョン状態がわかる
更新方法がわかりやすい
特定のブランチのみacheをクリアすることが容易
Cons
更新が手間(文字列置換で行うとそこまででもないけど)
全体での一律管理ができない
環境変数
CIサービスの環境変数経由で埋め込む
Pros
更新は簡単
全体での一律管理がでできる
Cons
CIサービス(github actions)によっては環境変数の設定に権限が必要
リポジトリ内のファイルのみで現在のバージョン状態がわからない
更新方法がCIサービスごとに違う
特定のブランチのみacheをクリアできない
cacheバージョンファイル
cacheのバージョンになるファイルをコミットして、そのファイルのhashを使う
Pros
更新は簡単
全体での一律管理がでできる
設定ファイルを見れば現在のバージョン状態がわかる
更新方法がわかりやすい
特定のブランチのみcacheをクリアすることが容易
更新方法がCIサービスに依存しない
更新時に特殊な権限を必要としない
Cons
hashを使用するため最終的に展開されるkeyが長くなる
設定ファイルに記述するkeyも長くなる
管理するファイルが増える
cacheバージョンファイルに関しては以下のような内容を使用している。 (hashを使用するため、内容は自由に記述できる)
複数箇所から参照するキャッシュのバージョン生成用ファイル キャッシュの整合性の問題が出た場合に以下の数字を更新する 1
keyとして使用する場合には以下のように記述する。
${{ hashFiles('.github/workflows/cache-version.txt') }}
最終的にkey全体は以下のようになる。 (これより前のstepでnode_versionを定義している)
${{ runner.os }}-v${{ hashFiles('.github/workflows/cache-version.txt') }}-nodemodules-node${{ steps.node_version.outputs.version }}-${{ hashFiles('yarn.lock') }}
1 note · View note
0-9 · 3 years
Text
Safari cannot open the page
Safari cannot open the page. The error was: "The operation couldn't be completed. (WebKitBlobResource error 1)".
Before
const objURL = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = objURL; a.target = "_blank"; a.click(); setTimeout(() => { window.URL.revokeObjectURL(objURL); }, 2500);
After
const objURL = window.URL.createObjectURL(blob); const a = document.createElement("a"); window.open(objURL); setTimeout(() => { window.URL.revokeObjectURL(objURL); }, 2500);
0 notes
0-9 · 3 years
Text
dependency-cruiserがtypescriptを認識しない問題の解決
dependency-cruiserが .tsx? を認識しなかったので調査した。
dependency-cruiserは基本的に .js 以外を自動的に認識するが、その挙動は実行時に「 require('typescript') が成功するか」といった方法で行われている。 (今回は基本的に .tsx? しか使ってなかったので調べていないが、 .jsx 等も同じ方法で検証されていたはず)
そのため、以下のようなディレクトリ構成で、 sub2 で depcruise を実行してもtypescriptの認識が行われない。 ( package-name/node_modules/dependency-cruiser 以下で require('typescript') しても package-name/sub2/node_modules/typescript が読み込めないため)
package-name ├── node_modules │   └── dependency-cruiser ├── package.json ├── sub1 │   ├── node_modules │   └── package.json └── sub2    ├── node_modules │   └── typescript └── package.json
上記のような構成の場合、以下のような方法で解決する。
yarn workspace 等で package-name/node_modules にまとめる (この場合でもバージョン違い等で dependency-cruiser と typescript が違う node_modules 以下に保存されないように注意)
それぞれ実行される階層の package.json に dependency-cruiser を追加する
1はなんらかの理由で突然 dependency-cruiser が typescript を認識しなくなるので注意が必要( renovatebot 等で全体のバージョンを固定していれば問題ない可能性も高い)
2は上記の挙動を知らないとぱっと見無駄な指定がされているように見えるので注意
この挙動に関しては知らないと混乱する可能性が高いが、他に解決策もなさそうなのでバグ報告はしていない
0 notes
0-9 · 3 years
Text
github actionsのcacheでcircle ciのepoch相当のことをする
jsのbuildでは node_modules/.cache 以下にwebpack等のbuild cacheが保存されているため、結果は毎回保存しておきたい。
ただし、同じkeyをしているするとcacheの保存がskipされるため更新されない。
circle ciでは {{ epoch }} という指定を行うことでこれを回避している。
github actionsでは特にこれ用の指定はないようなので $(date '+%Y%m%d-%H%M%S') の結果を保存して使用する。
- name: Get current date id: date run: echo "::set-output name=date::$(date '+%Y%m%d-%H%M%S')" - name: Cache build cache uses: actions/cache@v2 with: path: 'node_modules/.cache' key: ${{ runner.os }}-v1-build-cache-node14-${{ steps.date.outputs.date }} restore-keys: | ${{ runner.os }}-v1-build-cache-node14-
key は毎回違うものになるためcacheのrestore時には key は無視されるが、 restore-keys を指定しているため前回保存されたものが使用される。
(厳密には同じ秒に実行されたものはskipされるが、あくまでもbuild cacheなので厳密には考えない)
build cacheは場合によって生成結果に影響を与えるため、release build時には使用しないこと。
(上記は on push 時の使用を想定。安全を考えるならkeyの一部に ${{ hashFiles('yarn.lock') }} 相当の値を割り当てる方がいいかもしれない)
1 note · View note
0-9 · 3 years
Text
CircleCIでreact-scriptsのbuild時間を短縮する
react-scriptsはejectしないとwebpackの設定を変更できず、tsconfig.jsonの noEmit を強要するため incremental を有効化できない。
ただし、内部的には incremental が有効化されており node_modules/.cache 以下にcacheが出力されている。
つまり、CI環境でも node_modules/.cache を保存することでbuildを高速化することができる。
具体的には以下のように node_modules/.cache 以下をcacheする。 ({{ epoch }} を指定することで、常に最後にsaveされたcacheを使う)
- restore_cache: name: Restoring node Build Cache key: node-build-v8- - run: yarn build - save_cache: name: Saving node Build Cache key: node-build-v8-{{ epoch }} paths: - node_modules/.cache
build cacheを使うのはpush時のみにして、PR merge時等はrestore_cacheなしで行う方がいいかもしれない。 (その場合でもsave_cachehは行っていい)
1 note · View note
0-9 · 4 years
Text
IE11で動かないnpmパッケージのrenovate.json
とりあえず手元で問題になったやつだけ
renovate-config/default.json at master · kyo-ago/renovate-config
.github/renovate.json5 に以下の記述を行う。
{ "extends": [ "github>kyo-ago/renovate-config" ] }
0 notes
0-9 · 4 years
Text
プロダクト負債というもの
root Design Meetup #1 「UX負債への取り組み」 - connpass に参加していてUX負債とは技術的負債に非常に近いものを感じた。
そこから考えてみると、特にUXや技術に関わりなく共通的に負債を概念を考えられるのではないかと思ったので書く。
現状プロダクトを運営していく上で積み重なっていく問題を横断的に指す言葉として、こ���では「プロダクト負債」とする。
当初「サービス負債」としていたが、いくつか検索したところ、「product debt(プロダクト負債)」は既に使われている用語のようなので書き直した。
Googleで検索した範囲では「プロダクト負債」だと数件のヒットがあった。
「product-debt」だとそれなりの件数がヒットするが、いくつかみた範囲ではここで紹介している内容より狭い範囲を想定しているように見えた。 (が、この記事は翻訳ではないため、既存の概念とは一致していない可能性が高い)
前提
自分は開発者なので、基本的に技術的負債から発想している。
プロダクト負債とは
継続的に運営されているプロダクトの中で、それを選択時点と状況が変わったのに、状況に対して修正されていないもの。
なぜ各負債の共通概念が必要なのか?
これまでプロダクト負債の中では主に「技術的負債」という形で技術的な部分が多く語られてきた。
しかし、プロダクトを運営していく過程では技術部分以外にも負債は溜まっていくし、それぞれの負債は相互に連携している場合もある。
各分野が単独で負債の解消を目指した場合、個別最適に陥り効率的な改善につながらない場合もある
また、別の分野からのアプローチで対応が不要になる場合もある。
そして、負債の概念にはあえて負債を積むことによって運営を行う上での時間を購入できる場合もある。
そのため、各負債共通の概念を定義することで、プロダクト全体を見ながら適切に負債に対処したり、場合によっては負債を積むという選択も可能になる。
各分野の負債
技術的負債
一般的によく語られているため省略する。
UX負債
かなり想像して書くが、以下のようなものがあるだろう。
色、サイズの不統一性
ダイアログの多様
戻り先が遷移元と違う
必要画面サイズの拡大(横スクロール)
多言語化時の文字の崩れ
学習の終わったA/Bテストの削除漏れ
ちなみに、既に提唱されているため「UX負債」としているが、自分は以下の記事の中では「デザイン的な負債」という呼び方をしていた。
0-9, 「大規模なUI改修」を行うとどうなるか
ビジネス的負債
主に制作フローから発していない負債を想定してビジネス的負債と呼ぶ。
ビジネス的負債に関しては現状語られているところを知らないため、完全に想像である。
もしかしたら既に別の用語で語られている可能性もある。
特定のクライアントのみの特殊契約
複雑な課金テーブル
個別の請求フロー
古い決済手法への依存
制作フローから発していない負債ではあるが、制作フローに対して影響を及ぼす可能性が高い。
共通する概念
「当初想定されていなかった」、「ここまで(大きく|ひどく|長く)なると思っていなかった」というワードが出る場合、負債である可能性が高い。
基本的には当初は想定しておらず、状況が変わった後も適切な対応が取られていなかったものは負債と表現していいだろう。
どう対応するか
各分野ごとに個別の対応方法はあるが、プロダクト負債に対応する方法としては以下のような方法を提案する。
1. 各分野ごとのメンバーで「負債」の概念に関して認識を合わせる
分野によっては別の用語が用いられている場合もある。
必ずしも「負債」という用語を選択する必要はないが、どういった用語を用いるのかは統一した方がいい。
ただし、以下のような概念は共有した方がいい。
負債は必ずしも悪ではない
負債は全くない状態を目指すものではない
負債は運営をしていく以上避けられないものである
現在の負債状況の確認と継続的な変化の監視は必要である
場合によっては負債の追加を行う方がいい場合もある
負債は多くなればなるほど減らすことが難しくなる
負債が一定以上になると追加、変更は行えても削除ができなくなる(想定外の問題が発生する)状態になる
7の状態を超えると追加は行えても変更ができなくなる(想定外の問題が発生する)状態になる
8の状態を超えるとプロダクト運営が行えなくなる
2. 現状どういった負債があるかを確認する
各分野ごとにどういった負債があるかを確認する。
ここでは、純粋にその分野独自の負債も含める。
この時、可能な限り「どういった経緯で追加、実施されたのか」、「なぜ対応されなかったのか」など、背景も含めて共有する
3. プロダクト運営に対する影響度合いで重み付けを行う
確認した負債ごとにプロダクト運営に対してどの程度影響するかの重み付けを行う。
この項目は非常に難しいことが予想され、実際どういった手法を用いれば良いか自分でもわからない。
ただ、各分野ごとではなく、プロダクト全体としてみることで重み付けが変わる場合もあると考える。
例えば、以下のような状況がありえるかもしれない。
技術的な分野から見ると管理画面のフロントエンド部分で非常に大きな負債が溜まっていると認識しているが、プロダクト全体から見ると今後もほとんど修正が予定されておらず返済価値が低い
ビジネス的な分野から見ると課金設計に非常に大きな負債が溜まっていると認識しているが、プロダクト全体から見ると課金テーブル部分の柔軟性が高いために十分吸収可能なため返済価値が低い
UX的な分野から見るとデザインの古いページに非常に大きな負債が溜まっていると認識しているが、プロダクト全体から見るとそもそもページを破棄しても良いため返済価値が低い
4. 重み付けの高いものから対応方法を検討する
重み付けを行ったらそれぞれの項目に対して対応方法を検討する。
完全にその分野に閉じた負債であればその分野のみで対応することも可能であるが、基本的にはどこかしら連携していることが多い。
例えば、以下のような対応があるかもしれない。
負債返却のためのメンバーを採用する
検索部分の負荷が高いが負債化が激しい場合、価格表から検索機能を分けて特別価格に、検索機能を使えるユーザを制限する
一部のユーザのみ大幅なリソースを使っている場合、運営側からヒアリング、説得、警告を行うことで対応を行う
別言語に対して対応する場合、一部の機能だけを抽出したUIを作成する
個別契約が頻発する場合、個別契約の行われやすい部分を抽出し、設定可能な機能として実装する
5. 振り返りと繰り返し
プロダクト負債はプロダクトを運営していく上で避けられないものである。
そのため、ここで挙げた対応方法を継続的に繰り返し、結果を振り返ることで適切に監視、管理していくことが重要である。
注意しなければならない点として、プロダクトの運営は本来ユーザに対して価値を届けることであって、負債を0にすることではない。
プロダクト負債によってプロダクトの運営に支障が出る状況は避けるべきだが、必要であればプロダクトの拡大のために負債を積むことも必要である。
これらはスクラム等のフローに組み込むことが可能かもしれない。
避けた方がいいこと
基本的に負債の一括返済はお勧めしない。
1. 大規模改修
主にUX負債と技術的負債の一括返済のために大規模改修が計画されることがある。
これに関しては以前書いたが、負債が高くなっている状態での改修はリ��クが高い。
0-9, 「大規模なUI改修」を行うとどうなるか
また、一括改修は組織に負債の返却に関しての知見がたまらないため、負債のコントロールが難しくなる。
もし、一度一括返済ができてしまうと、組織的に負債の返却方法として一括返済しか選択できず、毎回大規模改修となってしまう危険性がある。
そして、基本的に大規模改修は失敗するまで大規模化していき、最終的に大規模な失敗となって現れる。
大規模な失敗は社員の退職リスクやプロダクト改善の停滞にも繋がるため避けるべきである。
2. 個人改修
それ以外にも「特定のメンバーが英雄的な行動で一括返済を行う」場合もある。
この場合にも組織的に負債をコントロールする技術がたまらないためお勧めできない。
プロダクトを運営する上でプロダクト負債が貯まることは避けられないが、その負債を運営組織としてではなく個人が解消してしまった場合、その個人が退職した場合などに負債の返済が行えなくなる懸念がある。
また、負債化の原因(もしくはその負債を積むことで最もメリットを得られる人)と、一括返済を行うメンバーが同じ人間の場合には比較的問題は小さいが、多くの場合これは別の人間の場合が多いため、原因と結果のバランスが難しくなる。
どんな負債でも自分以外の人間が知らないうちに返済していると、自分がどの程度の負債を返済可能なのかの見積もりを誤ったり、そもそも負債を積んでいる認識を持てなかったりする。
そのため、基本的に負債は運営しながらの個別返却をワークフローに組み込むべきで、一括返済はお勧めしない。
個人でできること
ここまで上げた内容は組織として負債に対応する方法ではあるが、組織的に負債に対応できない状況もありうる。
ここでは組織的に負債に対応できない場合に、個人としてできることを書く。
1. 組織的に負債に対応できるように働きかける
プロダクトの運営に対して最も妥当と思われる対応。
そもそも技術者以外に負債的な概念を持っていない場合もあり、プロダクト負債的な概念が組織に共有されていない場合もある。
まずはそういった概念の共有から行うことで、組織的に負債に対して向き合っていけるようになっていく場合もある。
2. 負債を返却する
個人、もしくは各専門領域内で負債を返却する。
これまで主に技術的負債と言われるものに対して行われてきていた方法。
「週に1日、機能追加以外を行う日を設ける」、「スプリントに一定の余裕を持たせる」などの方法で行われてきた。
プロダクト全体で対応するより適切な対応が取れない可能性が高いが、プロダクト組織全体での対応より難易度が低い。
3. 負債を無視する
組織的な働きかけをできない、しても効果がなかった場合、個人としてできる対応として負債を無視することがある。
自分の職務領域に負債がある場合、成果を発揮することが難しくなる場合もあるが、それは自分のスキル的な限界として受け入れる。
自分以外のメンバーに対して負債的な発言は行わず、負債を含めた見積もりを行ってタスクをこなしていく。
人によっては不誠実と感じる可能性もあるが、そもそもプロダクト負債は各分野が関連して負債化している場合も多く、単独の分野での解決は難しいか効果的でない場合も多い。
また、そもそも一つの分野からみた負債は、全体から見るとプロダクト運営に対して大きな影響を及ぼさない場合もある。
そのため、現状組織的な課題になっていない場合、一旦負債を無視して作業を進めることはそこまで不誠実ではない場合も多い。
ただし、「(開発速度|クオリティ)を上げる方法」を問われた場合、負債の存在と返済方法について提案するのは良いかもしれない。
4. 負債化を加速させる
専門領域外の負債は一定規模まで大きくならないと認識できないため、自分の専門領域の負債化を加速させることでプロダクト全体で課題化する。
負債化を加速させる場面では時間的なコストが低くなることが多いため、専門領域外から見るとプロダクト運営に積極的に見える可能性がある。
プロダクト運営に支障が出るレベルまで負債化した時点で、組織的に対応せざるを得なくなるか、破綻するかのいずれかになる。
組織的に対応できるようになれば、組織としての対応能力として大きな力になる可能性が高い。
もちろん破綻する場合もあるが、破綻までにはある程度時間があるし、それまでにプロダクトがうまく回れば採用等で対応できる余地が生まれる場合もある。
そもそもプロダクトがうまく回らなければ負債を返却しても意味がないため、状況によっては負債化の加速も正しい対応になる場合もある。
まとめ
ここまで技術的負債をベースにしてプロダクト負債というものの検討を行った。
負債はプロダクト運営を行っている以上避けられないが、必ずしも避けるべきものではなく、場合によっては積極的に取り入れていくべき状況もありえる。
ただし、負債には一定のリスクがあり、状況把握と返済計画のない負債は破綻につながる場合もある。
技術的負債の概念には20年以上の蓄積があるが、プロダクト負債の概念は比較的若い概念のためこれからの蓄積が必要である。
参考
プロダクトが進捗していないと感じた時の戦い方 - hikoharu's PM blog
1 note · View note
0-9 · 4 years
Text
Webフロントエンドのリリース戦略
Webフロントエンドのリリースをどうするのか
前置き
Web開発に分業化が進み、Webフロントエンドの開発は独自に行われることが多くなった。
しかし、Webフロントエンドのリソースの配信はサーバサイドが絡むことも多く、Webフロントエンドに取って最適な方法が取られているとはいえない場合も多い。
ここでは、主にiOS, Androidアプリなどのリリース方法を参考にWebフロントエンドのリソースの配信(リリース)をどうするかを書く。
Webフロントエンド特有の事情
iOS, Androidアプリの場合、リリースに関しては事実上プラットフォームから指定されているため選択肢が少ない。
しかし、Webフロントエンドの場合、事実上URLで指定できる場所にさえおければリリース処理が終わることが多く、自由度が高い。
前提
ここで言う「Webフロントエンド」とは、「Webアプリケーション」と言われるようなそれなりにコード量の多いものを想定している。
Webフロントエンドのリリース戦略とは
以下のようなリリース戦略を提案する。
developへmerge時にフロントエンドチームへリリース
平日17時にdevelopを開発チームリリース
平日10時にdevelopをmainへmerge
平日14時から1時間ごとにmainを段階リリース
平日17時にmainを全体へリリースする
上記の「平日」は祝日を考慮し、休日の前日は除外する方が良い。 (Google Calendar APIから取得するとメンテナンスコストが低くて良い)
もし課金サービスであるなら、段階リリースは非課金ユーザに対して行うことが望ましい。 (Sentry等でエラー監視を行う場合、段階リリース対象のユーザのエラーに注意すること)
時間はあくまでも参考なので、主な業務時間に合わせて調整する。 (上記は主にtoBの場合を想定している)
もし、各段階で障害が発覚した場合、develop -> mainの順で修正し、リリースを行い直す。
コンセプト
重要なのは以下の点。
リリースは自動であること ただし、障害発生時の対応のため、手動でリリーする方法も確保すること
段階的にリリースされること
上記の例の場合、以下の順番でリリースされる
主な開発者(問題があった場合自分で修正できる者)
開発者全体(エラーがあっても対処できる者)
段階リリース対象者
全ユーザ
リリース段階に社内の非開発者を対象とすることは推奨しない (ユーザサポート等でユーザと環境が違った場合に支障が出やすいため)
まとめ
主にWebフロントエンドを対象としたリリース戦略を書いた。
上記はリリース戦略はWebフロントエンドに限らないが、Webフロントエンドの場合リリースにあたりDB等の状態を考慮する必要が低いためリリース戦略の自由度が高い。
過去、実際に上記のリリースフローで運用したが、考慮事項が少なくドキュメントも少なかった。
3 notes · View notes