Skip to content

Commit 6236c0b

Browse files
committed
chore: update cloudflare workflow
1 parent 86e7229 commit 6236c0b

6 files changed

Lines changed: 103 additions & 15 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Deploy Download Worker
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'workers/**'
9+
- '.github/workflows/deploy-download-worker.yml'
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
15+
concurrency:
16+
group: download-worker
17+
cancel-in-progress: false
18+
19+
jobs:
20+
deploy:
21+
runs-on: ubuntu-latest
22+
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Deploy Cloudflare Worker
28+
uses: cloudflare/wrangler-action@v3
29+
with:
30+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
31+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
32+
workingDirectory: workers
33+
command: deploy
34+
secrets: |
35+
GITHUB_TOKEN
36+
env:
37+
GITHUB_TOKEN: ${{ secrets.MEMOH_GITHUB_TOKEN }}

.github/workflows/deploy-landing.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ on:
55
branches:
66
- main
77
paths:
8-
- './'
8+
- 'index.html'
9+
- 'package.json'
10+
- 'pnpm-lock.yaml'
11+
- 'public/**'
12+
- 'scripts/**'
13+
- 'src/**'
914
- '.github/workflows/deploy-landing.yml'
1015
workflow_dispatch:
1116

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,19 @@ Deploy the Worker after configuring the `memoh.ai/downloads/desktop/*` route:
8484
npx wrangler deploy --config workers/wrangler.toml
8585
```
8686

87-
For higher GitHub API limits, set a Worker secret:
87+
GitHub Actions also deploys the Worker from `.github/workflows/deploy-download-worker.yml` when files under `workers/` change. Add these repository secrets before relying on the workflow:
88+
89+
```text
90+
CLOUDFLARE_API_TOKEN
91+
CLOUDFLARE_ACCOUNT_ID
92+
MEMOH_GITHUB_TOKEN
93+
```
94+
95+
`MEMOH_GITHUB_TOKEN` is written to the Worker as the runtime `GITHUB_TOKEN` secret, so the Worker can call the GitHub Releases API without hitting unauthenticated rate limits. A fine-grained read-only token for the release repository is enough.
96+
97+
If a download URL returns a GitHub Pages 404 page, the Worker route is not active for that path yet. Re-run the Worker deployment and, if Cloudflare cached the old 404, purge `/downloads/desktop/*`.
98+
99+
For manual deployments, set the Worker secret directly:
88100

89101
```bash
90102
npx wrangler secret put GITHUB_TOKEN --config workers/wrangler.toml

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
8-
"build": "vue-tsc -b && vite build",
8+
"build": "vue-tsc -b && vite build && node scripts/create-spa-fallback.mjs",
99
"preview": "vite preview"
1010
},
1111
"dependencies": {

scripts/create-spa-fallback.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { copyFile, mkdir } from 'node:fs/promises'
2+
import { resolve } from 'node:path'
3+
4+
const distDir = resolve('dist')
5+
const indexPath = resolve(distDir, 'index.html')
6+
7+
await copyFile(indexPath, resolve(distDir, '404.html'))
8+
9+
for (const route of ['desktop']) {
10+
const routeDir = resolve(distDir, route)
11+
await mkdir(routeDir, { recursive: true })
12+
await copyFile(indexPath, resolve(routeDir, 'index.html'))
13+
}
14+
15+
console.log('Created SPA fallback files')

workers/desktop-download-proxy.js

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ const downloadFileName = (tag, slug) => {
3434
return `Memoh-${sanitizeFileNamePart(tag)}-${sanitizeFileNamePart(slug)}`
3535
}
3636

37+
const githubAssetFileName = (tag, slug) => {
38+
const version = tag.replace(/^v/i, '')
39+
return `Memoh-Desktop-${version}-${slug}`
40+
}
41+
42+
const githubAssetUrl = (repo, tag, slug) => {
43+
return `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(githubAssetFileName(tag, slug))}`
44+
}
45+
3746
const jsonResponse = (body, init = {}) => {
3847
return new Response(JSON.stringify(body, null, 2), {
3948
...init,
@@ -67,7 +76,9 @@ const githubFetch = async (url, env, accept) => {
6776
})
6877

6978
if (!response.ok) {
70-
throw new Error(`GitHub request failed: ${response.status} ${url}`)
79+
const detail = await response.text().catch(() => '')
80+
const message = detail ? ` ${detail.slice(0, 240)}` : ''
81+
throw new Error(`GitHub request failed: ${response.status} ${url}${message}`)
7182
}
7283

7384
return response
@@ -83,21 +94,30 @@ const pickLatestRelease = (releases) => {
8394
.sort((a, b) => releaseTimestamp(b) - releaseTimestamp(a))[0]
8495
}
8596

86-
const getLatestRelease = async (env) => {
97+
const getLatestRelease = async (env, ctx) => {
8798
const repo = env.MEMOH_RELEASE_REPO || RELEASE_REPO
99+
const cache = caches.default
100+
const cacheKey = new Request(`https://memoh.internal/downloads/latest-release/${encodeURIComponent(repo)}`)
101+
const cached = await cache.match(cacheKey)
102+
if (cached) return cached.json()
103+
88104
const response = await githubFetch(`https://api.github.com/repos/${repo}/releases?per_page=30`, env)
89105
const latest = pickLatestRelease(await response.json())
90106

91107
if (!latest) {
92108
throw new Error(`No published releases found for ${repo}`)
93109
}
94110

111+
ctx.waitUntil(cache.put(cacheKey, jsonResponse(latest, {
112+
headers: cacheHeaders(LATEST_TTL_SECONDS),
113+
})))
114+
95115
return latest
96116
}
97117

98-
const getRelease = async (tag, env) => {
118+
const getRelease = async (tag, env, ctx) => {
99119
if (tag === 'latest') {
100-
return getLatestRelease(env)
120+
return getLatestRelease(env, ctx)
101121
}
102122

103123
const repo = env.MEMOH_RELEASE_REPO || RELEASE_REPO
@@ -172,11 +192,10 @@ const proxyAsset = async (request, env, ctx, tag, slug) => {
172192
if (cached) return withCacheStatus(cached, 'HIT')
173193
}
174194

175-
const release = await getRelease(tag, env)
176-
const selected = findAsset(release, slug)
177-
if (!selected) return notFound()
195+
if (!ASSETS[slug]) return notFound()
178196

179-
const assetResponse = await fetch(selected.asset.browser_download_url, {
197+
const repo = env.MEMOH_RELEASE_REPO || RELEASE_REPO
198+
const assetResponse = await fetch(githubAssetUrl(repo, tag, slug), {
180199
method: request.method === 'HEAD' ? 'HEAD' : 'GET',
181200
headers: {
182201
'user-agent': 'memoh-landing-download-proxy',
@@ -195,8 +214,8 @@ const proxyAsset = async (request, env, ctx, tag, slug) => {
195214
headers: assetResponse.headers,
196215
})
197216
response.headers.set('cache-control', `public, max-age=${ASSET_TTL_SECONDS}, immutable`)
198-
response.headers.set('content-disposition', `attachment; filename="${downloadFileName(release.tag_name, slug)}"`)
199-
response.headers.set('x-memoh-release-tag', release.tag_name)
217+
response.headers.set('content-disposition', `attachment; filename="${downloadFileName(tag, slug)}"`)
218+
response.headers.set('x-memoh-release-tag', tag)
200219
response.headers.set('x-memoh-cache', 'MISS')
201220
response.headers.delete('set-cookie')
202221

@@ -238,7 +257,7 @@ export default {
238257
const cached = await cache.match(cacheKey)
239258
if (cached) return withCacheStatus(cached, 'HIT')
240259

241-
const release = await getRelease(parsed.tag, env)
260+
const release = await getRelease(parsed.tag, env, ctx)
242261
const response = jsonResponse(request.method === 'HEAD' ? undefined : buildManifest(url, release, env.MEMOH_RELEASE_REPO || RELEASE_REPO), {
243262
headers: cacheHeaders(parsed.tag === 'latest' ? LATEST_TTL_SECONDS : ASSET_TTL_SECONDS, {
244263
'x-memoh-cache': 'MISS',
@@ -253,7 +272,7 @@ export default {
253272
if (!ASSETS[parsed.file]) return notFound()
254273

255274
if (parsed.tag === 'latest') {
256-
const release = await getRelease('latest', env)
275+
const release = await getRelease('latest', env, ctx)
257276
const redirectUrl = new URL(request.url)
258277
redirectUrl.pathname = `${DOWNLOAD_PREFIX}/${encodeURIComponent(release.tag_name)}/${parsed.file}`
259278

0 commit comments

Comments
 (0)