Skip to content

Commit d192777

Browse files
committed
feat: repo scoping
1 parent 4f01511 commit d192777

File tree

9 files changed

+103
-13
lines changed

9 files changed

+103
-13
lines changed

lib/api/cache-entries.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const cacheEntriesRouter = base
4747
.optional()
4848
.describe('Optional fallback keys to try if the primary key does not match'),
4949
scopes: z.array(z.string()).describe('Scopes to search within, checked in order'),
50+
repoId: z.string().describe('Repository id to match the cache entry against'),
5051
version: z.string().describe('Cache version identifier'),
5152
}),
5253
)
@@ -67,6 +68,7 @@ export const cacheEntriesRouter = base
6768
keys: [input.primaryKey, ...(input.restoreKeys ?? [])],
6869
scopes: input.scopes,
6970
version: input.version,
71+
repoId: input.repoId,
7072
})
7173

7274
return cacheEntry ?? null
@@ -84,6 +86,7 @@ export const cacheEntriesRouter = base
8486
key: z.string().optional().describe('Filter by exact cache key'),
8587
version: z.string().optional().describe('Filter by exact cache version'),
8688
scope: z.string().optional().describe('Filter by exact cache scope'),
89+
repoId: z.string().optional().describe('Filter by exact repository id'),
8790
itemsPerPage: z
8891
.number()
8992
.int()
@@ -106,6 +109,7 @@ export const cacheEntriesRouter = base
106109
if (input.key) query.where('key', '=', input.key)
107110
if (input.version) query.where('version', '=', input.version)
108111
if (input.scope) query.where('scope', '=', input.scope)
112+
if (input.repoId) query.where('repoId', '=', input.repoId)
109113

110114
const [cacheEntries, countResult] = await Promise.all([
111115
query
@@ -151,6 +155,7 @@ export const cacheEntriesRouter = base
151155
key: z.string().optional().describe('Filter by exact cache key'),
152156
version: z.string().optional().describe('Filter by exact cache version'),
153157
scope: z.string().optional().describe('Filter by exact cache scope'),
158+
repoId: z.string().optional().describe('Filter by exact repository id'),
154159
}),
155160
)
156161
.handler(async ({ input, context }) => {

lib/db.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const cacheEntrySchema = z.object({
1717
key: z.string(),
1818
version: z.string(),
1919
scope: z.string(),
20+
repoId: z.string(),
2021
updatedAt: z.number(),
2122
locationId: z.string(),
2223
})
@@ -38,6 +39,7 @@ export const uploadSchema = z.object({
3839
key: z.string(),
3940
version: z.string(),
4041
scope: z.string(),
42+
repoId: z.string(),
4143
createdAt: z.number(),
4244
lastPartUploadedAt: z.number().nullable(),
4345
startedPartUploadCount: z.number(),
@@ -55,7 +57,8 @@ export interface Database {
5557
const dbLogger = logger.withTag('db')
5658

5759
export const getDatabase = createSingletonPromise(async () => {
58-
if(process.env.NODE_CAGED === 'true' && env.DB_DRIVER === 'sqlite') throw new Error('SQLite is not supported with `caged` image variant.')
60+
if (process.env.NODE_CAGED === 'true' && env.DB_DRIVER === 'sqlite')
61+
throw new Error('SQLite is not supported with `caged` image variant.')
5962

6063
const dialect = await match(env)
6164
.with({ DB_DRIVER: 'postgres' }, async (env) => {

lib/migrations.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,46 @@ export function migrations(driver: Env['DB_DRIVER']) {
114114
await db.schema.alterTable('uploads').dropColumn('scope').execute()
115115
},
116116
},
117+
$3_repoId: {
118+
async up(db) {
119+
const repoIdColumnType = driver === 'mysql' ? 'varchar(255)' : 'text'
120+
121+
// clear all existing entries
122+
const adapter = await Storage.getAdapterFromEnv()
123+
124+
await Promise.all([
125+
db.deleteFrom('cache_entries').execute(),
126+
db.deleteFrom('storage_locations').execute(),
127+
db.deleteFrom('uploads').execute(),
128+
adapter.clear(),
129+
])
130+
131+
await db.schema
132+
.alterTable('cache_entries')
133+
.addColumn('repoId', repoIdColumnType, (col) => col.notNull())
134+
.execute()
135+
await db.schema
136+
.createIndex('idx_cache_entries_repoId')
137+
.on('cache_entries')
138+
.columns(['repoId'])
139+
.execute()
140+
141+
await db.schema
142+
.alterTable('uploads')
143+
.addColumn('repoId', repoIdColumnType, (col) => col.notNull())
144+
.execute()
145+
await db.schema
146+
.createIndex('idx_uploads_repoId')
147+
.on('uploads')
148+
.columns(['repoId'])
149+
.execute()
150+
},
151+
async down(db) {
152+
await db.schema.dropIndex('idx_cache_entries_repoId').execute()
153+
await db.schema.alterTable('cache_entries').dropColumn('repoId').execute()
154+
await db.schema.dropIndex('idx_uploads_repoId').execute()
155+
await db.schema.alterTable('uploads').dropColumn('repoId').execute()
156+
},
157+
},
117158
} satisfies Record<string, Migration>
118159
}

lib/scope.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function parseJsonScopes(json: string) {
4040
}
4141
}
4242

43-
export async function getCacheScopes(event: H3Event) {
43+
export async function getCacheScope(event: H3Event) {
4444
const token = getBearerToken(event)
4545
if (!token)
4646
throw createError({ statusCode: 401, message: 'Authorization header missing or malformed' })
@@ -61,5 +61,12 @@ export async function getCacheScopes(event: H3Event) {
6161
if (!hasAtLeast(scopes, 1))
6262
throw createError({ statusCode: 401, message: 'Token does not contain any cache scopes' })
6363

64-
return scopes
64+
const repoId = decoded.repository_id
65+
if (!repoId || typeof repoId !== 'string')
66+
throw createError({ statusCode: 401, message: 'Token does not contain repository id' })
67+
68+
return {
69+
scopes,
70+
repoId,
71+
}
6572
}

lib/storage.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,23 @@ export class Storage {
9191
.execute()
9292
}
9393

94-
async completeUpload(key: string, version: string, scope: string) {
94+
async completeUpload({
95+
key,
96+
version,
97+
scope,
98+
repoId,
99+
}: {
100+
key: string
101+
version: string
102+
scope: string
103+
repoId: string
104+
}) {
95105
const upload = await this.db
96106
.selectFrom('uploads')
97107
.where('key', '=', key)
98108
.where('version', '=', version)
99109
.where('scope', '=', scope)
110+
.where('repoId', '=', repoId)
100111
.selectAll()
101112
.executeTakeFirst()
102113
if (!upload) return
@@ -141,6 +152,7 @@ export class Storage {
141152
.where('key', '=', key)
142153
.where('version', '=', version)
143154
.where('scope', '=', scope)
155+
.where('repoId', '=', repoId)
144156
.innerJoin('storage_locations', 'storage_locations.id', 'cache_entries.locationId')
145157
.select(['cache_entries.id', 'cache_entries.locationId', 'storage_locations.folderName'])
146158
.executeTakeFirst()
@@ -169,6 +181,7 @@ export class Storage {
169181
updatedAt: Date.now(),
170182
locationId,
171183
scope,
184+
repoId,
172185
})
173186
.execute()
174187

@@ -305,11 +318,23 @@ export class Storage {
305318
}
306319
}
307320

308-
async createUpload(key: string, version: string, scope: string) {
321+
async createUpload({
322+
key,
323+
version,
324+
scope,
325+
repoId,
326+
}: {
327+
key: string
328+
version: string
329+
scope: string
330+
repoId: string
331+
}) {
309332
const existingUpload = await this.db
310333
.selectFrom('uploads')
311334
.where('key', '=', key)
312335
.where('version', '=', version)
336+
.where('scope', '=', scope)
337+
.where('repoId', '=', repoId)
313338
.select('id')
314339
.executeTakeFirst()
315340
if (existingUpload) return
@@ -324,6 +349,7 @@ export class Storage {
324349
key,
325350
version,
326351
scope,
352+
repoId,
327353
lastPartUploadedAt: null,
328354
finishedPartUploadCount: 0,
329355
startedPartUploadCount: 0,
@@ -337,17 +363,20 @@ export class Storage {
337363
keys: [primaryKey, ...restoreKeys],
338364
version,
339365
scopes,
366+
repoId,
340367
}: {
341368
keys: [string, ...string[]]
342369
version: string
343370
scopes: string[]
371+
repoId: string
344372
}) {
345373
for (const scope of scopes) {
346374
const exactPrimaryMatch = await this.db
347375
.selectFrom('cache_entries')
348376
.where('key', '=', primaryKey)
349377
.where('version', '=', version)
350378
.where('scope', '=', scope)
379+
.where('repoId', '=', repoId)
351380
.selectAll()
352381
.executeTakeFirst()
353382
if (exactPrimaryMatch)
@@ -361,6 +390,7 @@ export class Storage {
361390
.where('key', 'like', `${primaryKey}%`)
362391
.where('version', '=', version)
363392
.where('scope', '=', scope)
393+
.where('repoId', '=', repoId)
364394
.orderBy('cache_entries.updatedAt', 'desc')
365395
.selectAll()
366396
.executeTakeFirst()
@@ -379,6 +409,7 @@ export class Storage {
379409
.where('key', '=', key)
380410
.where('version', '=', version)
381411
.where('scope', '=', scope)
412+
.where('repoId', '=', repoId)
382413
.orderBy('updatedAt', 'desc')
383414
.selectAll()
384415
.executeTakeFirst()
@@ -393,6 +424,7 @@ export class Storage {
393424
.where('key', 'like', `${key}%`)
394425
.where('version', '=', version)
395426
.where('scope', '=', scope)
427+
.where('repoId', '=', repoId)
396428
.orderBy('updatedAt', 'desc')
397429
.selectAll()
398430
.executeTakeFirst()

routes/twirp/github.actions.results.api.v1.CacheService/CreateCacheEntry.post.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from 'zod'
22
import { env } from '~/lib/env'
3-
import { getCacheScopes } from '~/lib/scope'
3+
import { getCacheScope } from '~/lib/scope'
44
import { getStorage } from '~/lib/storage'
55

66
const bodySchema = z.object({
@@ -9,7 +9,7 @@ const bodySchema = z.object({
99
})
1010

1111
export default defineEventHandler(async (event) => {
12-
const scopes = await getCacheScopes(event)
12+
const { scopes, repoId } = await getCacheScope(event)
1313

1414
const body = (await readBody(event)) as unknown
1515
const parsedBody = bodySchema.safeParse(body)
@@ -26,7 +26,7 @@ export default defineEventHandler(async (event) => {
2626
if (!writeScope)
2727
throw createError({ statusCode: 403, message: 'No scope with write permission found' })
2828

29-
const upload = await storage.createUpload(key, version, writeScope.Scope)
29+
const upload = await storage.createUpload({ key, version, scope: writeScope.Scope, repoId })
3030
if (!upload)
3131
return {
3232
ok: false,

routes/twirp/github.actions.results.api.v1.CacheService/FinalizeCacheEntryUpload.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from 'zod'
2-
import { getCacheScopes } from '~/lib/scope'
2+
import { getCacheScope } from '~/lib/scope'
33
import { getStorage } from '~/lib/storage'
44

55
const bodySchema = z.object({
@@ -8,7 +8,7 @@ const bodySchema = z.object({
88
})
99

1010
export default defineEventHandler(async (event) => {
11-
const scopes = await getCacheScopes(event)
11+
const { scopes, repoId } = await getCacheScope(event)
1212

1313
const parsedBody = bodySchema.safeParse(await readBody(event))
1414
if (!parsedBody.success)
@@ -24,7 +24,7 @@ export default defineEventHandler(async (event) => {
2424
if (!writeScope)
2525
throw createError({ statusCode: 403, message: 'No scope with write permission found' })
2626

27-
const upload = await storage.completeUpload(key, version, writeScope.Scope)
27+
const upload = await storage.completeUpload({ key, version, scope: writeScope.Scope, repoId })
2828
if (!upload)
2929
throw createError({
3030
statusCode: 404,

routes/twirp/github.actions.results.api.v1.CacheService/GetCacheEntryDownloadURL.post.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { map, pipe, prop, sortBy } from 'remeda'
22
import { z } from 'zod'
3-
import { getCacheScopes } from '~/lib/scope'
3+
import { getCacheScope } from '~/lib/scope'
44
import { getStorage } from '~/lib/storage'
55

66
const bodySchema = z.object({
@@ -10,7 +10,7 @@ const bodySchema = z.object({
1010
})
1111

1212
export default defineEventHandler(async (event) => {
13-
const scopes = await getCacheScopes(event)
13+
const { scopes, repoId } = await getCacheScope(event)
1414

1515
const parsedBody = bodySchema.safeParse(await readBody(event))
1616
if (!parsedBody.success)
@@ -26,6 +26,7 @@ export default defineEventHandler(async (event) => {
2626
keys: [key, ...(restore_keys ?? [])],
2727
version,
2828
scopes: pipe(scopes, sortBy([prop('Permission'), 'desc']), map(prop('Scope'))),
29+
repoId,
2930
})
3031
if (!match)
3132
return {

tests/e2e.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe(`save and restore cache with @actions/cache package`, () => {
1616
process.env.ACTIONS_CACHE_SERVICE_V2 = 'true'
1717
process.env.ACTIONS_RUNTIME_TOKEN = await new SignJWT({
1818
ac: JSON.stringify([{ Scope: 'refs/heads/main', Permission: 3 }]),
19+
repository_id: '123',
1920
})
2021
.setProtectedHeader({ alg: 'HS256' })
2122
.sign(crypto.createSecretKey('mock-secret-key', 'ascii'))

0 commit comments

Comments
 (0)