Skip to content

Commit 0e70d87

Browse files
committed
自動リリーススクリプトを他プロジェクトから移植
1 parent 733573c commit 0e70d87

File tree

4 files changed

+284
-0
lines changed

4 files changed

+284
-0
lines changed

.github/workflows/webstore-beta.yaml

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Webstore BETA Publish
2+
3+
on:
4+
pull_request:
5+
types: [edited]
6+
7+
jobs:
8+
webstore_beta_publish:
9+
if: |
10+
github.event.changes.title && startsWith(github.event.pull_request.title, '[v')
11+
# github.event.pull_request.base.ref == 'master' &&
12+
# github.event.pull_request.head.ref == 'develop' &&
13+
runs-on: ubuntu-latest
14+
env:
15+
NODE_ENV: beta
16+
GOOGLEAPI_CLIENT_ID: ${{ secrets.GOOGLEAPI_CLIENT_ID }}
17+
GOOGLEAPI_CLIENT_SECRET: ${{ secrets.GOOGLEAPI_CLIENT_SECRET }}
18+
GOOGLEAPI_REFRESH_TOKEN: ${{ secrets.GOOGLEAPI_REFRESH_TOKEN }}
19+
CHROMEWEBSTORE_EXTENSION_ID: egkgleinehaapbpijnlpbllfeejjpceb
20+
steps:
21+
- uses: actions/checkout@v4
22+
- name: Use Node.js 20.x
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: 20.x
26+
- name: Setup pnpm
27+
uses: pnpm/action-setup@v4
28+
with:
29+
version: 9
30+
- run: pnpm install
31+
- run: make beta-release
32+
- name: Publish to Webstore (BETA)
33+
run: |
34+
npx -y tsx scripts/webstore-publish.ts ./release/艦これウィジェット-beta.zip

.github/workflows/webstore-prod.yaml

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: Webstore PROD Publish
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
branches:
7+
- main
8+
9+
jobs:
10+
webstore_prod_publish:
11+
runs-on: ubuntu-latest
12+
if: github.event_name == 'pull_request' && github.event.pull_request.merged && github.event.pull_request.base.ref == 'main' && github.event.pull_request.head.ref == 'develop'
13+
steps:
14+
- name: Checkout to ${{ github.event.pull_request.head.ref }}
15+
uses: actions/checkout@v4
16+
with:
17+
ref: develop
18+
fetch-depth: 0
19+
- name: Use Node.js 20.x
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: 20.x
23+
- name: Setup pnpm
24+
uses: pnpm/action-setup@v4
25+
with:
26+
version: 9
27+
- run: pnpm install
28+
env:
29+
NODE_ENV: development
30+
- name: リリース適正を判断
31+
run: |
32+
VERSION=`echo "${{ github.event.pull_request.title }}" | sed -E 's/^\[?v([0-9]+\.[0-9]+\.[0-9]+)\]?.*/\1/'`
33+
echo "[DEBUG]" "github.event.pull_request.title:" ${{ github.event.pull_request.title }}
34+
echo "[INFO]" "VERSION:" ${VERSION}
35+
if [ -z "${VERSION}" ]; then
36+
echo "[ERROR]" "PRタイトルにバージョンが含まれていません"
37+
exit 1
38+
fi
39+
RELEASENOTE_VERSION=`jq --raw-output ".releases[0].version" src/release-note.json`
40+
echo "[INFO]" "リリースノートのバージョンを確認: ${RELEASENOTE_VERSION}"
41+
# v-prefixがあるの注意
42+
if [ "$RELEASENOTE_VERSION" != "v${VERSION}" ]; then
43+
echo "[ERROR]" "リリースノートのバージョンがPRタイトルのバージョンと一致しません: ${RELEASENOTE_VERSION} != ${VERSION}"
44+
exit 1
45+
fi
46+
MANIFEST_VERSION=`jq --raw-output ".version" src/public/manifest.json`
47+
echo "[INFO]" "マニフェストのバージョンを確認: ${MANIFEST_VERSION}"
48+
if [ "$MANIFEST_VERSION" != "${VERSION}" ]; then
49+
echo "[ERROR]" "マニフェストのバージョンがPRタイトルのバージョンと一致しません: ${MANIFEST_VERSION} != ${VERSION}"
50+
exit 1
51+
fi
52+
PACKAGE_VERSION=`jq --raw-output ".version" package.json`
53+
echo "[INFO]" "パッケージのバージョンを確認: ${PACKAGE_VERSION}"
54+
if [ "$PACKAGE_VERSION" != "${VERSION}" ]; then
55+
echo "[ERROR]" "パッケージのバージョンがPRタイトルのバージョンと一致しません: ${PACKAGE_VERSION} != ${VERSION}"
56+
exit 1
57+
fi
58+
echo "[INFO]" "リリース適正を判断しました. このバージョンのソースコードでプロダクションリリースすべきです."
59+
- name: developに対してリリースバージョンのタグづけ
60+
env:
61+
GIT_CI_USER_NAME: Ayanel CI
62+
GIT_CI_USER_EMAIL: [email protected]
63+
run: |
64+
VERSION=`jq --raw-output ".releases[0].version" src/release-note.json`
65+
echo "[INFO]" "VERSION:" ${VERSION}
66+
git config --global user.name "${GIT_CI_USER_NAME}"
67+
git config --global user.email ${GIT_CI_USER_EMAIL}
68+
# COMMITS=`jq --raw-output '.releases[0].commits | map("- " + .hash + " " + .title) | .[]' src/release-note.json`
69+
# echo "[INFO]" "COMMITS:\n" ${COMMITS}
70+
if [ -n "${DRY_RUN}" ]; then
71+
echo "[DEBUG]" "DRY_RUN=${DRY_RUN} が指定されているため、タグのpushをスキップします"
72+
exit 0
73+
fi
74+
git tag ${VERSION}
75+
git push origin ${VERSION}
76+
- name: リリースビルド
77+
run: make release
78+
env:
79+
NODE_ENV: production
80+
- name: ウェブストアに公開申請提出
81+
env:
82+
GOOGLEAPI_CLIENT_ID: ${{ secrets.GOOGLEAPI_CLIENT_ID }}
83+
GOOGLEAPI_CLIENT_SECRET: ${{ secrets.GOOGLEAPI_CLIENT_SECRET }}
84+
GOOGLEAPI_REFRESH_TOKEN: ${{ secrets.GOOGLEAPI_REFRESH_TOKEN }}
85+
CHROMEWEBSTORE_EXTENSION_ID: iachoklpnnjfgmldgelflgifhdaebnol
86+
run: |
87+
if [ -n "${DRY_RUN}" ]; then
88+
echo "[DEBUG]" "DRY_RUN=${DRY_RUN} が指定されているため、ウェブストアへの公開をスキップします"
89+
exit 0
90+
fi
91+
npx -y tsx scripts/webstore-publish.ts ./release/艦これウィジェット.zip

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25+
.env
2526

2627
# Release
2728
release

scripts/webstore-publish.ts

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/** webstore-publish.ts
2+
* ビルド済みzipファイルを受けてChrome拡張をChromeウェブストアに公開するスクリプト
3+
* @see https://developer.chrome.com/docs/webstore/using-api
4+
* @see https://developer.chrome.com/docs/webstore/api
5+
*
6+
* @env {string} GOOGLEAPI_CLIENT_ID Google Cloud Consoleで作成したOAuth 2.0 クライアント ID
7+
* @env {string} GOOGLEAPI_CLIENT_SECRET Google Cloud Consoleで作成したOAuth 2.0 クライアント シークレット
8+
* @env {string} GOOGLEAPI_REFRESH_TOKEN Google OAuth 2.0 Playgroundで取得したリフレッシュトークン
9+
* @env {string} CHROMEWEBSTORE_EXTENSION_ID Chromeウェブストアで取得した拡張機能ID
10+
* @env {string} NODE_ENV デフォルトは"production"、"development"の場合はtrusted_testers=true
11+
**/
12+
13+
import fs from "fs";
14+
15+
const __main__ = async () => {
16+
if (process.argv.length < 3) {
17+
console.error("zip file path is required.");
18+
console.error("Usage: node webstore-publish.ts <zip_file_path>");
19+
process.exit(1);
20+
}
21+
const [/* nodepath */, /* _scriptpath */, zip_file_path] = process.argv;
22+
if (!fs.existsSync(zip_file_path)) {
23+
console.error(`zip file not found: ${zip_file_path}`);
24+
process.exit(1);
25+
}
26+
try {
27+
await __webstore_publish__(zip_file_path);
28+
} catch (e) {
29+
console.error("[ERROR]", e);
30+
process.exit(1);
31+
}
32+
};
33+
34+
const __webstore_publish__ = async (
35+
zip_file_path: string,
36+
client_id: string = process.env.GOOGLEAPI_CLIENT_ID!,
37+
client_secret: string = process.env.GOOGLEAPI_CLIENT_SECRET!,
38+
refresh_token: string = process.env.GOOGLEAPI_REFRESH_TOKEN!,
39+
extension_id: string = process.env.CHROMEWEBSTORE_EXTENSION_ID!,
40+
trusted_testers: boolean = false, // process.env.NODE_ENV !== "production", // Betaもリンク知ってるひとに公開なので false でいい
41+
) => {
42+
console.log("[INFO]", "START PUBLISHING...");
43+
44+
// (1) リフレッシュトークンを使ってアクセストークンを取得
45+
const refreshResponse = await refreshAccessToken(client_id, client_secret, refresh_token);
46+
const authbody = (await refreshResponse.json()) as OAuthResponse;
47+
const { access_token, token_type, scope, expires_in } = authbody;
48+
if (!refreshResponse.ok) throw new Error(`http response of REFRESH is NOT OK: ${refreshResponse.statusText}\n${JSON.stringify(authbody)}`,);
49+
console.log("[INFO]", "ACCESS TOKEN REFRESHED:", token_type, scope, expires_in);
50+
if (!access_token) throw new Error(`couldn't retrieve access_token from this refresh_token`);
51+
52+
// (2) アクセストークンを使ってzipファイルをアップロード
53+
const uploadResponse = await uploadPackageFile(access_token, zip_file_path, extension_id);
54+
const uploadbody = await uploadResponse.json();
55+
console.log("[INFO]", "UPLOAD PACKAGE FILE:", uploadResponse.ok, uploadResponse.status);
56+
if (!uploadResponse.ok) throw new Error(`http response of UPLOAD is NOT OK: ${uploadResponse.statusText}\n${JSON.stringify(uploadbody)}`);
57+
console.log("[INFO]", "UPLOAD SUCCESSFULLY DONE:", uploadbody);
58+
59+
// (3) アップロードしたzipファイルを公開申請
60+
const publishResponse = await publishUploadedPackageFile(access_token, extension_id, trusted_testers);
61+
const publishbody = await publishResponse.json();
62+
console.log("[INFO]", "PUBLISH NEW PACKGE:", publishResponse.ok, publishResponse.status);
63+
if (!publishResponse.ok) throw new Error(`http response of PUBLISH is NOT OK: ${publishResponse.statusText}\n${JSON.stringify(publishbody)}`);
64+
console.log("[INFO]", "PUBLISH SUCCESSFULLY DONE:", publishbody);
65+
};
66+
67+
/**
68+
* OAuth 2.0 レスポンス
69+
* @see https://developer.chrome.com/docs/webstore/using-api?hl=ja#test-oauth
70+
**/
71+
interface OAuthResponse {
72+
access_token: string;
73+
expires_in: number;
74+
refresh_token: string;
75+
token_type: string;
76+
scope: string;
77+
}
78+
79+
/**
80+
* refreshAccessToken
81+
* デフォルトでは、得られた access_token は40分でExpireするため、
82+
* publishのAPIを叩く前に、必ずここで access_token を新たに取得
83+
* する必要がある.
84+
* ということで、こいつは refresh_token なんかを使って access_token
85+
* を得るメソッドです。
86+
*
87+
* @param {string} client_id
88+
* @param {string} client_secret
89+
* @param {string} refresh_token
90+
*
91+
* @returns {Promise<OAuthResponse>}
92+
*/
93+
async function refreshAccessToken(
94+
client_id: string,
95+
client_secret: string,
96+
refresh_token: string
97+
): Promise<Response> {
98+
return fetch("https://www.googleapis.com/oauth2/v4/token", {
99+
method: "POST",
100+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
101+
body: new URLSearchParams({
102+
client_id: client_id,
103+
client_secret: client_secret,
104+
refresh_token: refresh_token,
105+
grant_type: "refresh_token",
106+
}),
107+
});
108+
}
109+
110+
/**
111+
* uploadPackageFile
112+
* ちゃんとリフレッシュされてるアクセストークンを使って、
113+
* 指定されたファイルを指定されたアプリにアップロードする.
114+
* まだpublishされてないので注意.
115+
*
116+
* @param {string} access_token
117+
* @param {string} zip_file_path
118+
* @param {string} extension_id
119+
*
120+
* @returns {Promise<Response>}
121+
*/
122+
async function uploadPackageFile(
123+
access_token: string,
124+
zip_file_path: string,
125+
extension_id: string
126+
): Promise<Response> {
127+
const buf = fs.readFileSync(zip_file_path);
128+
return fetch(`https://www.googleapis.com/upload/chromewebstore/v1.1/items/${extension_id}`, {
129+
method: "PUT",
130+
headers: { "Authorization": `Bearer ${access_token}`, "x-goog-api-version": "2" },
131+
body: Buffer.from(buf),
132+
});
133+
}
134+
135+
/**
136+
* publishUploadedPackageFile
137+
* パブリッシュする.
138+
*
139+
* @param {string} access_token
140+
* @param {string} extension_id
141+
* @param {boolean} trustedTesters
142+
*
143+
* @return {Promise<Response>}
144+
*/
145+
async function publishUploadedPackageFile(
146+
access_token: string,
147+
extension_id: string,
148+
trustedTesters: boolean
149+
) {
150+
const query = new URLSearchParams({ publishTarget: trustedTesters ? "trustedTesters" : "default" });
151+
return fetch(`https://www.googleapis.com/chromewebstore/v1.1/items/${extension_id}/publish?${query.toString()}`, {
152+
method: "POST",
153+
headers: { "Authorization": `Bearer ${access_token}`, "x-goog-api-version": "2", "Content-Length": "0" },
154+
});
155+
}
156+
157+
// Entrypoint
158+
__main__();

0 commit comments

Comments
 (0)