mirror of
https://github.com/rharkor/caching-for-turbo.git
synced 2025-06-08 01:37:01 +09:00
feat: new cleanup options
This commit is contained in:
parent
4e6c062f68
commit
c78899286f
20 changed files with 923 additions and 1992 deletions
|
@ -1,6 +1,6 @@
|
||||||
# Caching for Turborepo with GitHub Actions
|
# Caching for Turborepo with GitHub Actions
|
||||||
|
|
||||||
[](https://github.com/rharkor/caching-for-turbo/actions)
|
[](https://github.com/rharkor/caching-for-turbo/actions)
|
||||||
|
|
||||||
Supercharge your [Turborepo](https://turbo.build/repo/) builds with our
|
Supercharge your [Turborepo](https://turbo.build/repo/) builds with our
|
||||||
dedicated GitHub Actions caching service, designed to make your CI workflows
|
dedicated GitHub Actions caching service, designed to make your CI workflows
|
||||||
|
@ -55,7 +55,7 @@ the following step **before** you run `turbo build`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Cache for Turbo
|
- name: Cache for Turbo
|
||||||
uses: rharkor/caching-for-turbo@v1.7
|
uses: rharkor/caching-for-turbo@v1.8
|
||||||
```
|
```
|
||||||
|
|
||||||
This GitHub Action facilitates:
|
This GitHub Action facilitates:
|
||||||
|
|
19
action.yml
19
action.yml
|
@ -11,10 +11,29 @@ branding:
|
||||||
|
|
||||||
# Define your inputs here.
|
# Define your inputs here.
|
||||||
inputs:
|
inputs:
|
||||||
|
provider:
|
||||||
|
description: 'Provider to use for caching (github)'
|
||||||
|
required: true
|
||||||
|
default: 'github'
|
||||||
cache-prefix:
|
cache-prefix:
|
||||||
description: 'Prefix for the cache key'
|
description: 'Prefix for the cache key'
|
||||||
required: false
|
required: false
|
||||||
default: turbogha_
|
default: turbogha_
|
||||||
|
max-age:
|
||||||
|
description:
|
||||||
|
'Cleanup cache files older than this age (ex: 1mo, 1w, 1d). using
|
||||||
|
https://www.npmjs.com/package/parse-duration'
|
||||||
|
required: false
|
||||||
|
max-files:
|
||||||
|
description:
|
||||||
|
'Cleanup oldest cache files when number of files exceeds this limit (ex:
|
||||||
|
300)'
|
||||||
|
required: false
|
||||||
|
max-size:
|
||||||
|
description:
|
||||||
|
'Cleanup oldest cache files when total size exceeds this limit (ex: 100mb,
|
||||||
|
10gb)'
|
||||||
|
required: false
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: node20
|
using: node20
|
||||||
|
|
487
dist/post/index.js
generated
vendored
487
dist/post/index.js
generated
vendored
File diff suppressed because it is too large
Load diff
2
dist/post/index.js.map
generated
vendored
2
dist/post/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
3
dist/post/package.json
generated
vendored
Normal file
3
dist/post/package.json
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
1
dist/post/sourcemap-register.cjs
generated
vendored
Normal file
1
dist/post/sourcemap-register.cjs
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2187
dist/setup/index.js
generated
vendored
2187
dist/setup/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/setup/index.js.map
generated
vendored
2
dist/setup/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
27
dist/setup/licenses.txt
generated
vendored
27
dist/setup/licenses.txt
generated
vendored
|
@ -2033,6 +2033,33 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
parse-duration
|
||||||
|
MIT
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2013 Jake Rosoman <jkroso@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
'Software'), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
pino
|
pino
|
||||||
MIT
|
MIT
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
3
dist/setup/package.json
generated
vendored
Normal file
3
dist/setup/package.json
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
1
dist/setup/sourcemap-register.cjs
generated
vendored
Normal file
1
dist/setup/sourcemap-register.cjs
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -12,6 +12,7 @@
|
||||||
"@actions/cache": "^4.0.0",
|
"@actions/cache": "^4.0.0",
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
|
"parse-duration": "^2.1.4",
|
||||||
"stream-to-promise": "^3.0.0",
|
"stream-to-promise": "^3.0.0",
|
||||||
"wait-on": "^8.0.0"
|
"wait-on": "^8.0.0"
|
||||||
},
|
},
|
||||||
|
@ -7997,6 +7998,12 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-duration": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-b98m6MsCh+akxfyoz9w9dt0AlH2dfYLOBss5SdDsr9pkhKNvkWBXU/r8A4ahmIGByBOLV2+4YwfCuFxbDDaGyg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/parse-json": {
|
"node_modules/parse-json": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js"
|
".": "./dist/index.js"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
|
@ -38,6 +39,7 @@
|
||||||
"@actions/cache": "^4.0.0",
|
"@actions/cache": "^4.0.0",
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
|
"parse-duration": "^2.1.4",
|
||||||
"stream-to-promise": "^3.0.0",
|
"stream-to-promise": "^3.0.0",
|
||||||
"wait-on": "^8.0.0"
|
"wait-on": "^8.0.0"
|
||||||
},
|
},
|
||||||
|
|
19
src/lib/cache/index.ts
vendored
19
src/lib/cache/index.ts
vendored
|
@ -9,12 +9,9 @@ import {
|
||||||
} from 'node:fs'
|
} from 'node:fs'
|
||||||
import { getCacheClient } from './utils'
|
import { getCacheClient } from './utils'
|
||||||
import { getCacheKey, getFsCachePath, getTempCachePath } from '../constants'
|
import { getCacheKey, getFsCachePath, getTempCachePath } from '../constants'
|
||||||
|
import { RequestContext } from '../server'
|
||||||
type RequestContext = {
|
import * as core from '@actions/core'
|
||||||
log: {
|
import { TListFile } from '../server/cleanup'
|
||||||
info: (message: string) => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//* Cache API
|
//* Cache API
|
||||||
export async function saveCache(
|
export async function saveCache(
|
||||||
|
@ -68,3 +65,13 @@ export async function getCache(
|
||||||
const readableStream = createReadStream(fileRestorationPath)
|
const readableStream = createReadStream(fileRestorationPath)
|
||||||
return [size, readableStream, artifactTag]
|
return [size, readableStream, artifactTag]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteCache(): Promise<void> {
|
||||||
|
core.error(`Cannot delete github cache automatically.`)
|
||||||
|
throw new Error(`Cannot delete github cache automatically.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listCache(): Promise<TListFile[]> {
|
||||||
|
core.error(`Cannot list github cache automatically.`)
|
||||||
|
throw new Error(`Cannot list github cache automatically.`)
|
||||||
|
}
|
||||||
|
|
9
src/lib/cache/utils.ts
vendored
9
src/lib/cache/utils.ts
vendored
|
@ -6,7 +6,8 @@ import { createWriteStream } from 'node:fs'
|
||||||
import { unlink } from 'node:fs/promises'
|
import { unlink } from 'node:fs/promises'
|
||||||
import { getTempCachePath } from '../constants'
|
import { getTempCachePath } from '../constants'
|
||||||
import { restoreCache, saveCache } from '@actions/cache'
|
import { restoreCache, saveCache } from '@actions/cache'
|
||||||
|
import { deleteCache, listCache } from './index'
|
||||||
|
import { TProvider } from '../providers'
|
||||||
class HandledError extends Error {
|
class HandledError extends Error {
|
||||||
status: number
|
status: number
|
||||||
statusText: string
|
statusText: string
|
||||||
|
@ -31,7 +32,7 @@ function handleFetchError(message: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCacheClient() {
|
export function getGithubProvider(): TProvider {
|
||||||
if (!env.valid) {
|
if (!env.valid) {
|
||||||
throw new Error('Cache API env vars are not set')
|
throw new Error('Cache API env vars are not set')
|
||||||
}
|
}
|
||||||
|
@ -66,6 +67,8 @@ export function getCacheClient() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
save,
|
save,
|
||||||
restore
|
restore,
|
||||||
|
delete: deleteCache,
|
||||||
|
list: listCache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { join } from 'path'
|
||||||
import { env } from './env'
|
import { env } from './env'
|
||||||
|
|
||||||
export const serverPort = 41230
|
export const serverPort = 41230
|
||||||
export const cachePath = 'turbogha_v2'
|
export const cachePath = 'turbogha_'
|
||||||
export const cachePrefix = core.getInput('cache-prefix') || cachePath
|
export const cachePrefix = core.getInput('cache-prefix') || cachePath
|
||||||
export const getCacheKey = (hash: string, tag?: string): string =>
|
export const getCacheKey = (hash: string, tag?: string): string =>
|
||||||
`${cachePrefix}${hash}${tag ? `#${tag}` : ''}`
|
`${cachePrefix}${hash}${tag ? `#${tag}` : ''}`
|
||||||
|
|
20
src/lib/providers.ts
Normal file
20
src/lib/providers.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import { getGithubProvider } from './cache/utils'
|
||||||
|
import { Readable } from 'stream'
|
||||||
|
import { TListFile } from './server/cleanup'
|
||||||
|
|
||||||
|
export type TProvider = {
|
||||||
|
save: (key: string, stream: Readable) => Promise<void>
|
||||||
|
restore: (path: string, key: string) => Promise<string | undefined>
|
||||||
|
delete: () => Promise<void>
|
||||||
|
list: () => Promise<TListFile[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProvider = (): TProvider => {
|
||||||
|
const provider = core.getInput('provider')
|
||||||
|
if (provider === 'github') {
|
||||||
|
return getGithubProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Provider ${provider} not supported`)
|
||||||
|
}
|
99
src/lib/server/cleanup.ts
Normal file
99
src/lib/server/cleanup.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import { RequestContext } from '.'
|
||||||
|
import { deleteCache, listCache } from '../cache'
|
||||||
|
import parse from 'parse-duration'
|
||||||
|
|
||||||
|
export type TListFile = {
|
||||||
|
path: string
|
||||||
|
createdAt: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanup(ctx: RequestContext) {
|
||||||
|
const maxAge = core.getInput('max-age')
|
||||||
|
const maxFiles = core.getInput('max-files')
|
||||||
|
const maxSize = core.getInput('max-size')
|
||||||
|
|
||||||
|
if (!maxAge && !maxFiles && !maxSize) {
|
||||||
|
ctx.log.info('No cleanup options provided, skipping cleanup')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { maxAgeParsed, maxFilesParsed, maxSizeParsed } = {
|
||||||
|
maxAgeParsed: parse(maxAge),
|
||||||
|
maxFilesParsed: parseInt(maxFiles),
|
||||||
|
maxSizeParsed: parseInt(maxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxAge && !maxAgeParsed) {
|
||||||
|
core.error('Invalid max-age provided')
|
||||||
|
throw new Error('Invalid max-age provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxFiles && !maxFilesParsed) {
|
||||||
|
core.error('Invalid max-files provided')
|
||||||
|
throw new Error('Invalid max-files provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxSize && !maxSizeParsed) {
|
||||||
|
core.error('Invalid max-size provided')
|
||||||
|
throw new Error('Invalid max-size provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await listCache()
|
||||||
|
|
||||||
|
const fileToDelete: TListFile[] = []
|
||||||
|
if (maxAgeParsed) {
|
||||||
|
const now = new Date()
|
||||||
|
const age = new Date(now.getTime() - maxAgeParsed)
|
||||||
|
fileToDelete.push(...files.filter(file => new Date(file.createdAt) < age))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxFilesParsed && files.length > maxFilesParsed) {
|
||||||
|
const sortedByDate = [...files].sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
)
|
||||||
|
const excessFiles = sortedByDate.slice(0, files.length - maxFilesParsed)
|
||||||
|
excessFiles.forEach(file => {
|
||||||
|
if (!fileToDelete.some(f => f.path === file.path)) {
|
||||||
|
fileToDelete.push(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxSizeParsed) {
|
||||||
|
let totalSize = files.reduce((sum, file) => sum + file.size, 0)
|
||||||
|
|
||||||
|
if (totalSize > maxSizeParsed) {
|
||||||
|
const sortedByDate = [...files].sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const file of sortedByDate) {
|
||||||
|
if (totalSize <= maxSizeParsed) break
|
||||||
|
|
||||||
|
if (!fileToDelete.some(f => f.path === file.path)) {
|
||||||
|
fileToDelete.push(file)
|
||||||
|
totalSize -= file.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileToDelete.length > 0) {
|
||||||
|
ctx.log.info(`Cleaning up ${fileToDelete.length} files`)
|
||||||
|
|
||||||
|
for (const file of fileToDelete) {
|
||||||
|
try {
|
||||||
|
await deleteCache()
|
||||||
|
ctx.log.info(`Deleted ${file.path}`)
|
||||||
|
} catch (error) {
|
||||||
|
core.error(`Failed to delete ${file.path}: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.log.info('No files to clean up')
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,13 @@
|
||||||
import Fastify from 'fastify'
|
import Fastify from 'fastify'
|
||||||
import { serverPort } from '../constants'
|
import { serverPort } from '../constants'
|
||||||
import { getCache, saveCache } from '../cache'
|
import { getCache, saveCache } from '../cache'
|
||||||
|
import { cleanup } from './cleanup'
|
||||||
|
|
||||||
|
export type RequestContext = {
|
||||||
|
log: {
|
||||||
|
info: (message: string) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function server(): Promise<void> {
|
export async function server(): Promise<void> {
|
||||||
//* Create the server
|
//* Create the server
|
||||||
|
@ -14,12 +21,16 @@ export async function server(): Promise<void> {
|
||||||
})
|
})
|
||||||
|
|
||||||
//? Shut down the server
|
//? Shut down the server
|
||||||
const shutdown = () => {
|
const shutdown = async (ctx: RequestContext) => {
|
||||||
|
//* Handle cleanup
|
||||||
|
await cleanup(ctx)
|
||||||
|
|
||||||
|
// Exit the server after responding (100ms)
|
||||||
setTimeout(() => process.exit(0), 100)
|
setTimeout(() => process.exit(0), 100)
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
fastify.delete('/shutdown', async () => {
|
fastify.delete('/shutdown', async request => {
|
||||||
return shutdown()
|
return shutdown(request)
|
||||||
})
|
})
|
||||||
|
|
||||||
//? Handle streaming requets body
|
//? Handle streaming requets body
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "ESNext",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "bundler",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue