Skip to content

Commit e4c0d6b

Browse files
authored
Merge pull request #176 from daichan132/codex/cws-api-v2-20260124
feat(cws): Chrome提出をAPI v2に切り替え
2 parents 0c19672 + c6201c3 commit e4c0d6b

File tree

5 files changed

+248
-8
lines changed

5 files changed

+248
-8
lines changed

.github/workflows/cd.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,20 @@ jobs:
6666
.output/*-firefox.zip
6767
- name: Submit to stores
6868
run: |
69+
CHROME_ZIP=".output/youtube-live-chat-fullscreen-${{ needs.create_tag.outputs.version }}-chrome.zip"
70+
FIREFOX_ZIP=".output/youtube-live-chat-fullscreen-${{ needs.create_tag.outputs.version }}-firefox.zip"
71+
SOURCES_ZIP=".output/youtube-live-chat-fullscreen-${{ needs.create_tag.outputs.version }}-sources.zip"
72+
73+
yarn submit:chrome:v2 -- \
74+
--zip "$CHROME_ZIP" \
75+
--expected-version "${{ needs.create_tag.outputs.version }}"
76+
6977
yarn wxt submit \
70-
--chrome-zip .output/*-chrome.zip \
71-
--firefox-zip .output/*-firefox.zip --firefox-sources-zip .output/*-sources.zip
78+
--firefox-zip "$FIREFOX_ZIP" \
79+
--firefox-sources-zip "$SOURCES_ZIP"
7280
env:
7381
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
82+
CHROME_PUBLISHER_ID: ${{ secrets.CHROME_PUBLISHER_ID }}
7483
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
7584
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
7685
CHROME_REFRESH_TOKEN: ${{ secrets.REFRESH_TOKEN }}

entrypoints/content/hooks/ylcStyleChange/useYLCFontColorChange.spec.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ describe('useYLCFontColorChange', () => {
2323
})
2424

2525
expect(setProperty).toHaveBeenCalledWith('--extension-yt-live-font-color', 'rgba(10, 20, 30, 0.6)')
26-
expect(setProperty).toHaveBeenCalledWith(
27-
'--extension-yt-live-secondary-font-color',
28-
`rgba(10, 20, 30, ${secondaryAlpha})`,
29-
)
26+
expect(setProperty).toHaveBeenCalledWith('--extension-yt-live-secondary-font-color', `rgba(10, 20, 30, ${secondaryAlpha})`)
3027
expect(setProperty).toHaveBeenCalledTimes(2)
3128
})
3229
})

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "youtube-live-chat-fullscreen",
33
"private": true,
4-
"version": "2.2.3",
4+
"version": "2.2.4",
55
"type": "module",
66
"scripts": {
77
"dev": "wxt",
@@ -10,6 +10,7 @@
1010
"build:firefox": "wxt build -b firefox",
1111
"zip": "wxt zip",
1212
"zip:firefox": "wxt zip -b firefox",
13+
"submit:chrome:v2": "node scripts/cws-v2-submit.mjs",
1314
"compile": "tsc --noEmit",
1415
"postinstall": "wxt prepare",
1516
"format": "biome format --write ./entrypoints/** ./shared/**",

scripts/cws-v2-submit.mjs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#!/usr/bin/env node
2+
import { readFile } from 'node:fs/promises'
3+
4+
const args = process.argv.slice(2)
5+
6+
const getArgValue = (flag) => {
7+
const index = args.indexOf(flag)
8+
if (index === -1 || index === args.length - 1) return undefined
9+
return args[index + 1]
10+
}
11+
12+
const hasFlag = (flag) => args.includes(flag)
13+
14+
const mustEnv = (name) => {
15+
const value = process.env[name]
16+
if (!value) {
17+
console.error(`Missing required env var: ${name}`)
18+
process.exit(1)
19+
}
20+
return value
21+
}
22+
23+
const zipPath = getArgValue('--zip') ?? process.env.CWS_ZIP
24+
if (!zipPath) {
25+
console.error('Missing required --zip argument or CWS_ZIP env var')
26+
process.exit(1)
27+
}
28+
29+
const expectedVersion = getArgValue('--expected-version') ?? process.env.CWS_EXPECTED_VERSION
30+
const cancelPending = hasFlag('--cancel-pending') || process.env.CWS_CANCEL_PENDING === 'true'
31+
const skipReview = hasFlag('--skip-review') || process.env.CWS_SKIP_REVIEW === 'true'
32+
const publishType = process.env.CWS_PUBLISH_TYPE
33+
const deployPercentageRaw = process.env.CWS_DEPLOY_PERCENTAGE
34+
35+
const publisherId = mustEnv('CHROME_PUBLISHER_ID')
36+
const extensionId = mustEnv('CHROME_EXTENSION_ID')
37+
const clientId = mustEnv('CHROME_CLIENT_ID')
38+
const clientSecret = mustEnv('CHROME_CLIENT_SECRET')
39+
const refreshToken = mustEnv('CHROME_REFRESH_TOKEN')
40+
41+
const baseUrl = 'https://chromewebstore.googleapis.com'
42+
const itemName = `publishers/${publisherId}/items/${extensionId}`
43+
44+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
45+
46+
const requestJson = async (url, options = {}) => {
47+
const response = await fetch(url, options)
48+
const text = await response.text()
49+
let data
50+
try {
51+
data = text ? JSON.parse(text) : {}
52+
} catch {
53+
data = { raw: text }
54+
}
55+
56+
if (!response.ok) {
57+
const message = data?.error?.message || data?.message || text || response.statusText
58+
throw new Error(`HTTP ${response.status} ${response.statusText}: ${message}`)
59+
}
60+
61+
return data
62+
}
63+
64+
const getAccessToken = async () => {
65+
const body = new URLSearchParams({
66+
client_id: clientId,
67+
client_secret: clientSecret,
68+
refresh_token: refreshToken,
69+
grant_type: 'refresh_token',
70+
})
71+
72+
const data = await requestJson('https://oauth2.googleapis.com/token', {
73+
method: 'POST',
74+
headers: {
75+
'Content-Type': 'application/x-www-form-urlencoded',
76+
},
77+
body,
78+
})
79+
80+
if (!data.access_token) {
81+
throw new Error('No access_token in OAuth response')
82+
}
83+
84+
return data.access_token
85+
}
86+
87+
const fetchStatus = async (token) =>
88+
requestJson(`${baseUrl}/v2/${itemName}:fetchStatus`, {
89+
headers: {
90+
Authorization: `Bearer ${token}`,
91+
},
92+
})
93+
94+
const extractVersions = (revision) =>
95+
(revision?.distributionChannels ?? [])
96+
.map((channel) => channel?.crxVersion)
97+
.filter(Boolean)
98+
99+
const normalizeUploadState = (state) => {
100+
if (state === 'UPLOAD_IN_PROGRESS') return 'IN_PROGRESS'
101+
return state
102+
}
103+
104+
const formatRevision = (revision) => {
105+
if (!revision) return 'none'
106+
const versions = extractVersions(revision)
107+
const state = revision.state ?? 'UNKNOWN'
108+
if (!versions.length) return `${state} (no version)`
109+
return `${state} [${versions.join(', ')}]`
110+
}
111+
112+
const pollUpload = async (token) => {
113+
const attempts = Number(process.env.CWS_POLL_ATTEMPTS ?? 12)
114+
const intervalMs = Number(process.env.CWS_POLL_INTERVAL_MS ?? 5000)
115+
116+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
117+
await sleep(intervalMs)
118+
const status = await fetchStatus(token)
119+
const uploadState = normalizeUploadState(status.lastAsyncUploadState)
120+
console.log(`Upload status check ${attempt}/${attempts}: ${uploadState ?? 'UNKNOWN'}`)
121+
122+
if (uploadState === 'SUCCEEDED') return status
123+
if (uploadState === 'FAILED') {
124+
throw new Error('Upload failed (lastAsyncUploadState=FAILED)')
125+
}
126+
}
127+
128+
throw new Error('Upload still in progress after max attempts')
129+
}
130+
131+
const main = async () => {
132+
const token = await getAccessToken()
133+
134+
const statusBefore = await fetchStatus(token)
135+
const submittedState = statusBefore?.submittedItemRevisionStatus?.state
136+
const submittedVersions = extractVersions(statusBefore?.submittedItemRevisionStatus)
137+
const publishedVersions = extractVersions(statusBefore?.publishedItemRevisionStatus)
138+
139+
console.log(`Published revision (before): ${formatRevision(statusBefore?.publishedItemRevisionStatus)}`)
140+
console.log(`Submitted revision (before): ${formatRevision(statusBefore?.submittedItemRevisionStatus)}`)
141+
142+
const hasActiveSubmission = submittedState === 'PENDING_REVIEW' || submittedState === 'STAGED'
143+
const submittedMismatch =
144+
expectedVersion && submittedVersions.length > 0 && !submittedVersions.includes(expectedVersion)
145+
146+
if (hasActiveSubmission && (cancelPending || submittedMismatch)) {
147+
const reason = cancelPending
148+
? 'CWS_CANCEL_PENDING/--cancel-pending'
149+
: `submitted version mismatch (${submittedVersions.join(', ')})`
150+
console.log(`Cancelling existing submission: ${reason}`)
151+
await requestJson(`${baseUrl}/v2/${itemName}:cancelSubmission`, {
152+
method: 'POST',
153+
headers: {
154+
Authorization: `Bearer ${token}`,
155+
},
156+
})
157+
} else if (hasActiveSubmission) {
158+
console.log('Active submission exists; continuing without cancel')
159+
}
160+
161+
console.log(`Uploading package: ${zipPath}`)
162+
const zipData = await readFile(zipPath)
163+
const uploadData = await requestJson(`${baseUrl}/upload/v2/${itemName}:upload`, {
164+
method: 'POST',
165+
headers: {
166+
Authorization: `Bearer ${token}`,
167+
'Content-Type': 'application/zip',
168+
},
169+
body: zipData,
170+
})
171+
172+
const uploadState = normalizeUploadState(uploadData.uploadState)
173+
console.log(
174+
`Upload response: ${uploadState ?? 'UNKNOWN'}${uploadData.crxVersion ? ` (version ${uploadData.crxVersion})` : ''}`,
175+
)
176+
177+
if (expectedVersion && uploadData.crxVersion && uploadData.crxVersion !== expectedVersion) {
178+
throw new Error(`Uploaded version ${uploadData.crxVersion} does not match expected ${expectedVersion}`)
179+
}
180+
181+
if (uploadState === 'IN_PROGRESS') {
182+
await pollUpload(token)
183+
} else if (uploadState && uploadState !== 'SUCCEEDED') {
184+
throw new Error(`Upload failed with state: ${uploadState}`)
185+
}
186+
187+
const publishBody = {}
188+
if (skipReview) publishBody.skipReview = true
189+
if (publishType) publishBody.publishType = publishType
190+
if (deployPercentageRaw) {
191+
const deployPercentage = Number(deployPercentageRaw)
192+
if (Number.isNaN(deployPercentage)) {
193+
throw new Error(`Invalid CWS_DEPLOY_PERCENTAGE: ${deployPercentageRaw}`)
194+
}
195+
publishBody.deployInfos = [{ deployPercentage }]
196+
}
197+
198+
console.log('Publishing submission')
199+
await requestJson(`${baseUrl}/v2/${itemName}:publish`, {
200+
method: 'POST',
201+
headers: {
202+
Authorization: `Bearer ${token}`,
203+
...(Object.keys(publishBody).length ? { 'Content-Type': 'application/json' } : {}),
204+
},
205+
body: Object.keys(publishBody).length ? JSON.stringify(publishBody) : undefined,
206+
})
207+
208+
const statusAfter = await fetchStatus(token)
209+
const submittedAfterVersions = extractVersions(statusAfter?.submittedItemRevisionStatus)
210+
const submittedAfterState = statusAfter?.submittedItemRevisionStatus?.state
211+
212+
console.log(`Submitted revision (after): ${formatRevision(statusAfter?.submittedItemRevisionStatus)}`)
213+
214+
if (
215+
expectedVersion &&
216+
submittedAfterState &&
217+
submittedAfterVersions.length > 0 &&
218+
!submittedAfterVersions.includes(expectedVersion)
219+
) {
220+
throw new Error(
221+
`Submitted version ${submittedAfterVersions.join(', ')} does not match expected ${expectedVersion}`,
222+
)
223+
}
224+
225+
if (publishedVersions.length) {
226+
console.log(`Published revision (before): ${publishedVersions.join(', ')}`)
227+
}
228+
}
229+
230+
main().catch((error) => {
231+
console.error(error instanceof Error ? error.message : error)
232+
process.exit(1)
233+
})

shared/hooks/useMessage.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { act, render } from '@testing-library/react'
2-
import { describe, expect, it, vi } from 'vitest'
2+
import { describe, expect, it, type vi } from 'vitest'
33
import { useMessage } from './useMessage'
44

55
type RuntimeWithHelpers = {

0 commit comments

Comments
 (0)