@rharkor/caching-for-turbo 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1 -1
- package/package.json +2 -2
- package/.devcontainer/devcontainer.json +0 -41
- package/.gitattributes +0 -3
- package/.github/workflows/check-dist.yml +0 -43
- package/.github/workflows/ci.yml +0 -152
- package/.github/workflows/codeql-analysis.yml +0 -46
- package/.github/workflows/release.yml +0 -47
- package/.node-version +0 -1
- package/.prettierignore +0 -3
- package/.prettierrc.json +0 -16
- package/.releaserc.json +0 -132
- package/CHANGELOG.md +0 -9
- package/check-full-turbo.sh +0 -12
- package/eslint.config.mjs +0 -27
- package/renovate.json +0 -17
- package/script/release +0 -59
- package/src/cli.ts +0 -94
- package/src/dev/cleanup.ts +0 -14
- package/src/dev-run.ts +0 -16
- package/src/index.ts +0 -6
- package/src/lib/constants.ts +0 -29
- package/src/lib/core.ts +0 -62
- package/src/lib/env/index.ts +0 -22
- package/src/lib/ping.ts +0 -7
- package/src/lib/providers/cache/index.ts +0 -87
- package/src/lib/providers/cache/utils.ts +0 -70
- package/src/lib/providers/s3/index.ts +0 -237
- package/src/lib/providers.ts +0 -42
- package/src/lib/server/cleanup.ts +0 -111
- package/src/lib/server/index.ts +0 -85
- package/src/lib/server/utils.ts +0 -91
- package/src/main.ts +0 -21
- package/src/post.ts +0 -26
- package/tsconfig.json +0 -19
- package/turbo.json +0 -9
package/src/cli.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { config } from 'dotenv'
|
|
4
|
-
import { server } from './lib/server'
|
|
5
|
-
import { killServer, launchServer } from './lib/server/utils'
|
|
6
|
-
import { logger } from '@rharkor/logger'
|
|
7
|
-
import { ping } from './lib/ping'
|
|
8
|
-
import { serverLogFile } from './lib/constants'
|
|
9
|
-
import { writeFile } from 'fs/promises'
|
|
10
|
-
import { version } from '../package.json'
|
|
11
|
-
|
|
12
|
-
// Load environment variables from .env file if it exists
|
|
13
|
-
config()
|
|
14
|
-
|
|
15
|
-
const main = async (): Promise<void> => {
|
|
16
|
-
await logger.init()
|
|
17
|
-
|
|
18
|
-
const args = process.argv.slice(2)
|
|
19
|
-
|
|
20
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
21
|
-
console.log(`
|
|
22
|
-
Turborepo GitHub Actions Cache Server
|
|
23
|
-
|
|
24
|
-
Usage:
|
|
25
|
-
turbogha [options]
|
|
26
|
-
|
|
27
|
-
Options:
|
|
28
|
-
--server Run the server in foreground mode
|
|
29
|
-
--help, -h Show this help message
|
|
30
|
-
--version, -v Show version
|
|
31
|
-
--ping Ping the server
|
|
32
|
-
--kill Kill the server
|
|
33
|
-
|
|
34
|
-
Environment Variables:
|
|
35
|
-
The following environment variables are supported for S3 configuration:
|
|
36
|
-
- S3_ACCESS_KEY_ID: AWS S3 access key ID
|
|
37
|
-
- S3_SECRET_ACCESS_KEY: AWS S3 secret access key
|
|
38
|
-
- S3_BUCKET: AWS S3 bucket name
|
|
39
|
-
- S3_REGION: AWS S3 region
|
|
40
|
-
- S3_ENDPOINT: S3 endpoint (default: https://s3.amazonaws.com)
|
|
41
|
-
- S3_PREFIX: Prefix for S3 objects (default: turbogha/)
|
|
42
|
-
- PROVIDER: Cache provider (github or s3)
|
|
43
|
-
|
|
44
|
-
Examples:
|
|
45
|
-
# Run server in background and export environment variables
|
|
46
|
-
turbogha
|
|
47
|
-
|
|
48
|
-
# Run server in foreground mode
|
|
49
|
-
turbogha --server
|
|
50
|
-
|
|
51
|
-
# Ping the server
|
|
52
|
-
turbogha --ping
|
|
53
|
-
|
|
54
|
-
# With S3 configuration
|
|
55
|
-
S3_ACCESS_KEY_ID=your-key S3_SECRET_ACCESS_KEY=your-secret S3_BUCKET=your-bucket S3_REGION=us-east-1 turbogha
|
|
56
|
-
`)
|
|
57
|
-
process.exit(0)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (args.includes('--version') || args.includes('-v')) {
|
|
61
|
-
console.log(version)
|
|
62
|
-
process.exit(0)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
if (args.includes('--ping')) {
|
|
67
|
-
await ping()
|
|
68
|
-
} else if (args.includes('--kill')) {
|
|
69
|
-
await killServer()
|
|
70
|
-
} else if (args.includes('--server')) {
|
|
71
|
-
// Empty log file
|
|
72
|
-
await writeFile(serverLogFile, '', { flag: 'w' })
|
|
73
|
-
// Run server in foreground mode
|
|
74
|
-
console.log('Starting Turborepo cache server in foreground mode...')
|
|
75
|
-
await server()
|
|
76
|
-
} else {
|
|
77
|
-
// Run server in background mode and export environment variables
|
|
78
|
-
console.log('Starting Turborepo cache server...')
|
|
79
|
-
// Empty log file
|
|
80
|
-
await writeFile(serverLogFile, '', { flag: 'w' })
|
|
81
|
-
await launchServer()
|
|
82
|
-
console.log(
|
|
83
|
-
'\nServer is running! You can now use Turbo with remote caching.'
|
|
84
|
-
)
|
|
85
|
-
console.log('\nTo stop the server, run:')
|
|
86
|
-
console.log('curl -X DELETE http://localhost:41230/shutdown')
|
|
87
|
-
}
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.error('Error:', error)
|
|
90
|
-
process.exit(1)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
main()
|
package/src/dev/cleanup.ts
DELETED
package/src/dev-run.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// Run the server in foreground and kill it after the test
|
|
2
|
-
|
|
3
|
-
import { config } from 'dotenv'
|
|
4
|
-
config()
|
|
5
|
-
|
|
6
|
-
import { server } from './lib/server'
|
|
7
|
-
import { launchServer } from './lib/server/utils'
|
|
8
|
-
|
|
9
|
-
const main = async (): Promise<void> => {
|
|
10
|
-
//* Run server
|
|
11
|
-
server()
|
|
12
|
-
//* Run launch server
|
|
13
|
-
await launchServer(true)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
main()
|
package/src/index.ts
DELETED
package/src/lib/constants.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { join } from 'path'
|
|
2
|
-
import { env } from './env'
|
|
3
|
-
import { core } from './core'
|
|
4
|
-
|
|
5
|
-
// Helper function to get input value, prioritizing environment variables for local development
|
|
6
|
-
const getInput = (name: string, envName?: string): string | undefined => {
|
|
7
|
-
// In GitHub Actions context, try core.getInput first
|
|
8
|
-
if (process.env.CI === 'true') {
|
|
9
|
-
const coreInput = core.getInput(name)
|
|
10
|
-
if (coreInput) return coreInput
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Fall back to environment variable
|
|
14
|
-
const envVar = envName || name.toUpperCase().replace(/-/g, '_')
|
|
15
|
-
return process.env[envVar]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const serverPort = 41230
|
|
19
|
-
export const cachePath = 'turbogha_'
|
|
20
|
-
export const cachePrefix = getInput('cache-prefix', 'CACHE_PREFIX') || cachePath
|
|
21
|
-
export const getCacheKey = (hash: string, tag?: string): string =>
|
|
22
|
-
`${cachePrefix}${hash}${tag ? `#${tag}` : ''}`
|
|
23
|
-
export const serverLogFile = env.RUNNER_TEMP
|
|
24
|
-
? join(env.RUNNER_TEMP, 'turbogha.log')
|
|
25
|
-
: '/tmp/turbogha.log'
|
|
26
|
-
export const getFsCachePath = (hash: string): string =>
|
|
27
|
-
join(env.RUNNER_TEMP || '/tmp', `${hash}.tg.bin`)
|
|
28
|
-
export const getTempCachePath = (key: string): string =>
|
|
29
|
-
join(env.RUNNER_TEMP || '/tmp', `cache-${key}.tg.bin`)
|
package/src/lib/core.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import * as coreLib from '@actions/core'
|
|
2
|
-
import { logger as loggerLib } from '@rharkor/logger'
|
|
3
|
-
|
|
4
|
-
const isCI = process.env.CI === 'true'
|
|
5
|
-
|
|
6
|
-
export const core = {
|
|
7
|
-
isCI,
|
|
8
|
-
setFailed: (message: string) => {
|
|
9
|
-
if (isCI) {
|
|
10
|
-
coreLib.setFailed(message)
|
|
11
|
-
} else {
|
|
12
|
-
loggerLib.error(message)
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
getInput: (name: string) => {
|
|
16
|
-
if (isCI) {
|
|
17
|
-
return coreLib.getInput(name)
|
|
18
|
-
}
|
|
19
|
-
return undefined
|
|
20
|
-
},
|
|
21
|
-
exportVariable: (name: string, value: string) => {
|
|
22
|
-
if (isCI) {
|
|
23
|
-
coreLib.exportVariable(name, value)
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
//* Logger
|
|
27
|
-
info: (message: string) => {
|
|
28
|
-
if (isCI) {
|
|
29
|
-
coreLib.info(message)
|
|
30
|
-
} else {
|
|
31
|
-
loggerLib.info(message)
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
error: (message: string) => {
|
|
35
|
-
if (isCI) {
|
|
36
|
-
coreLib.error(message)
|
|
37
|
-
} else {
|
|
38
|
-
loggerLib.error(message)
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
debug: (message: string) => {
|
|
42
|
-
if (isCI) {
|
|
43
|
-
coreLib.debug(message)
|
|
44
|
-
} else {
|
|
45
|
-
loggerLib.debug(message)
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
log: (message: string) => {
|
|
49
|
-
if (isCI) {
|
|
50
|
-
coreLib.info(message)
|
|
51
|
-
} else {
|
|
52
|
-
loggerLib.log(message)
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
success: (message: string) => {
|
|
56
|
-
if (isCI) {
|
|
57
|
-
coreLib.info(message)
|
|
58
|
-
} else {
|
|
59
|
-
loggerLib.success(message)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
package/src/lib/env/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
const envObject = {
|
|
2
|
-
ACTIONS_RUNTIME_TOKEN: process.env.ACTIONS_RUNTIME_TOKEN,
|
|
3
|
-
ACTIONS_CACHE_URL: process.env.ACTIONS_CACHE_URL,
|
|
4
|
-
RUNNER_TEMP: process.env.RUNNER_TEMP
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
type TInvalidEnv = {
|
|
8
|
-
valid: false
|
|
9
|
-
} & typeof envObject
|
|
10
|
-
|
|
11
|
-
type TValidEnv = {
|
|
12
|
-
valid: true
|
|
13
|
-
} & {
|
|
14
|
-
[K in keyof typeof envObject]: NonNullable<(typeof envObject)[K]>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type TEnv = TInvalidEnv | TValidEnv
|
|
18
|
-
|
|
19
|
-
export const env = {
|
|
20
|
-
valid: Object.values(envObject).every(value => value !== undefined),
|
|
21
|
-
...envObject
|
|
22
|
-
} as TEnv
|
package/src/lib/ping.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { env } from '../../env'
|
|
3
|
-
import { pipeline } from 'node:stream/promises'
|
|
4
|
-
import {
|
|
5
|
-
createReadStream,
|
|
6
|
-
createWriteStream,
|
|
7
|
-
existsSync,
|
|
8
|
-
statSync
|
|
9
|
-
} from 'node:fs'
|
|
10
|
-
import { getCacheKey, getFsCachePath, getTempCachePath } from '../../constants'
|
|
11
|
-
import { RequestContext } from '../../server'
|
|
12
|
-
import { TListFile } from '../../server/cleanup'
|
|
13
|
-
import { getCacheClient } from './utils'
|
|
14
|
-
import { TProvider } from '../../providers'
|
|
15
|
-
import { core } from 'src/lib/core'
|
|
16
|
-
|
|
17
|
-
//* Cache API
|
|
18
|
-
export async function saveCache(
|
|
19
|
-
ctx: RequestContext,
|
|
20
|
-
hash: string,
|
|
21
|
-
tag: string,
|
|
22
|
-
stream: Readable
|
|
23
|
-
): Promise<void> {
|
|
24
|
-
if (!env.valid) {
|
|
25
|
-
ctx.log.info(
|
|
26
|
-
`Using filesystem cache because cache API env vars are not set`
|
|
27
|
-
)
|
|
28
|
-
await pipeline(stream, createWriteStream(getFsCachePath(hash)))
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
const client = getCacheClient()
|
|
32
|
-
const key = getCacheKey(hash, tag)
|
|
33
|
-
await client.save(key, stream)
|
|
34
|
-
ctx.log.info(`Saved cache ${key} for ${hash}`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function getCache(
|
|
38
|
-
ctx: RequestContext,
|
|
39
|
-
hash: string
|
|
40
|
-
): Promise<
|
|
41
|
-
[number | undefined, Readable | ReadableStream, string | undefined] | null
|
|
42
|
-
> {
|
|
43
|
-
//* Get cache from filesystem if cache API env vars are not set
|
|
44
|
-
if (!env.valid) {
|
|
45
|
-
const path = getFsCachePath(hash)
|
|
46
|
-
if (!existsSync(path)) return null
|
|
47
|
-
const size = statSync(path).size
|
|
48
|
-
return [size, createReadStream(path), undefined]
|
|
49
|
-
}
|
|
50
|
-
//* Get cache from cache API
|
|
51
|
-
const client = getCacheClient()
|
|
52
|
-
const cacheKey = getCacheKey(hash)
|
|
53
|
-
const fileRestorationPath = getTempCachePath(cacheKey)
|
|
54
|
-
const foundKey = await client.restore(fileRestorationPath, cacheKey)
|
|
55
|
-
ctx.log.info(`Cache lookup for ${cacheKey}`)
|
|
56
|
-
if (!foundKey) {
|
|
57
|
-
ctx.log.info(`Cache lookup did not return data`)
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
const [foundCacheKey, artifactTag] = String(foundKey).split('#')
|
|
61
|
-
if (foundCacheKey !== cacheKey) {
|
|
62
|
-
ctx.log.info(`Cache key mismatch: ${foundCacheKey} !== ${cacheKey}`)
|
|
63
|
-
return null
|
|
64
|
-
}
|
|
65
|
-
const size = statSync(fileRestorationPath).size
|
|
66
|
-
const readableStream = createReadStream(fileRestorationPath)
|
|
67
|
-
return [size, readableStream, artifactTag]
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function deleteCache(): Promise<void> {
|
|
71
|
-
core.error(`Cannot delete github cache automatically.`)
|
|
72
|
-
throw new Error(`Cannot delete github cache automatically.`)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function listCache(): Promise<TListFile[]> {
|
|
76
|
-
core.error(`Cannot list github cache automatically.`)
|
|
77
|
-
throw new Error(`Cannot list github cache automatically.`)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const getGithubProvider = (): TProvider => {
|
|
81
|
-
return {
|
|
82
|
-
save: saveCache,
|
|
83
|
-
get: getCache,
|
|
84
|
-
delete: deleteCache,
|
|
85
|
-
list: listCache
|
|
86
|
-
}
|
|
87
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { env } from '../../env'
|
|
3
|
-
import streamToPromise from 'stream-to-promise'
|
|
4
|
-
import { createWriteStream } from 'node:fs'
|
|
5
|
-
import { unlink } from 'node:fs/promises'
|
|
6
|
-
import { getTempCachePath } from '../../constants'
|
|
7
|
-
import { restoreCache, saveCache } from '@actions/cache'
|
|
8
|
-
import { core } from 'src/lib/core'
|
|
9
|
-
class HandledError extends Error {
|
|
10
|
-
status: number
|
|
11
|
-
statusText: string
|
|
12
|
-
data: unknown
|
|
13
|
-
constructor(status: number, statusText: string, data: unknown) {
|
|
14
|
-
super(`${status}: ${statusText}`)
|
|
15
|
-
this.status = status
|
|
16
|
-
this.statusText = statusText
|
|
17
|
-
this.data = data
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function handleFetchError(message: string) {
|
|
22
|
-
return (error: unknown) => {
|
|
23
|
-
if (error instanceof HandledError) {
|
|
24
|
-
core.error(`${message}: ${error.status} ${error.statusText}`)
|
|
25
|
-
core.error(JSON.stringify(error.data))
|
|
26
|
-
throw error
|
|
27
|
-
}
|
|
28
|
-
core.error(`${message}: ${error}`)
|
|
29
|
-
throw error
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function getCacheClient() {
|
|
34
|
-
if (!env.valid) {
|
|
35
|
-
throw new Error('Cache API env vars are not set')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const save = async (key: string, stream: Readable): Promise<void> => {
|
|
39
|
-
try {
|
|
40
|
-
//* Create a temporary file to store the cache
|
|
41
|
-
const tempFile = getTempCachePath(key)
|
|
42
|
-
const writeStream = createWriteStream(tempFile)
|
|
43
|
-
await streamToPromise(stream.pipe(writeStream))
|
|
44
|
-
core.info(`Saved cache to ${tempFile}`)
|
|
45
|
-
|
|
46
|
-
core.info(`Saving cache for key: ${key}, path: ${tempFile}`)
|
|
47
|
-
await saveCache([tempFile], key)
|
|
48
|
-
core.info(`Saved cache ${key}`)
|
|
49
|
-
|
|
50
|
-
//* Remove the temporary file
|
|
51
|
-
await unlink(tempFile)
|
|
52
|
-
} catch (error) {
|
|
53
|
-
handleFetchError('Unable to upload cache')(error)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const restore = async (
|
|
58
|
-
path: string,
|
|
59
|
-
key: string
|
|
60
|
-
): Promise<string | undefined> => {
|
|
61
|
-
core.info(`Querying cache for key: ${key}, path: ${path}`)
|
|
62
|
-
|
|
63
|
-
return restoreCache([path], key, [])
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
save,
|
|
68
|
-
restore
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { TProvider } from 'src/lib/providers'
|
|
2
|
-
|
|
3
|
-
import { Readable } from 'stream'
|
|
4
|
-
import { RequestContext } from '../../server'
|
|
5
|
-
import { TListFile } from '../../server/cleanup'
|
|
6
|
-
import {
|
|
7
|
-
S3Client,
|
|
8
|
-
GetObjectCommand,
|
|
9
|
-
DeleteObjectCommand,
|
|
10
|
-
ListObjectsV2Command
|
|
11
|
-
} from '@aws-sdk/client-s3'
|
|
12
|
-
import { Upload } from '@aws-sdk/lib-storage'
|
|
13
|
-
import { getCacheKey } from 'src/lib/constants'
|
|
14
|
-
import { core } from 'src/lib/core'
|
|
15
|
-
|
|
16
|
-
// Helper function to get input value, prioritizing environment variables for local development
|
|
17
|
-
const getInput = (name: string, envName?: string): string | undefined => {
|
|
18
|
-
// In GitHub Actions context, try core.getInput first
|
|
19
|
-
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
20
|
-
const coreInput = core.getInput(name)
|
|
21
|
-
if (coreInput) return coreInput
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Fall back to environment variable
|
|
25
|
-
const envVar = envName || name.toUpperCase().replace(/-/g, '_')
|
|
26
|
-
return process.env[envVar]
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const getS3Provider = (): TProvider => {
|
|
30
|
-
const s3AccessKeyId = getInput('s3-access-key-id', 'S3_ACCESS_KEY_ID')
|
|
31
|
-
const s3SecretAccessKey = getInput(
|
|
32
|
-
's3-secret-access-key',
|
|
33
|
-
'S3_SECRET_ACCESS_KEY'
|
|
34
|
-
)
|
|
35
|
-
const s3Bucket = getInput('s3-bucket', 'S3_BUCKET')
|
|
36
|
-
const s3Region = getInput('s3-region', 'S3_REGION')
|
|
37
|
-
const s3Endpoint =
|
|
38
|
-
getInput('s3-endpoint', 'S3_ENDPOINT') || 'https://s3.amazonaws.com'
|
|
39
|
-
const s3Prefix = getInput('s3-prefix', 'S3_PREFIX') || 'turbogha/'
|
|
40
|
-
|
|
41
|
-
if (!s3AccessKeyId || !s3SecretAccessKey || !s3Bucket || !s3Region) {
|
|
42
|
-
throw new Error(
|
|
43
|
-
'S3 provider requires s3-access-key-id, s3-secret-access-key, s3-bucket, and s3-region. Set these as environment variables or GitHub Actions inputs.'
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const s3Client = new S3Client({
|
|
48
|
-
region: s3Region,
|
|
49
|
-
endpoint: s3Endpoint,
|
|
50
|
-
credentials: {
|
|
51
|
-
accessKeyId: s3AccessKeyId,
|
|
52
|
-
secretAccessKey: s3SecretAccessKey
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
const getS3Key = (hash: string, tag?: string) => {
|
|
57
|
-
const key = getCacheKey(hash, tag)
|
|
58
|
-
if (s3Prefix) {
|
|
59
|
-
return `${s3Prefix}${key}`
|
|
60
|
-
}
|
|
61
|
-
return key
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const save = async (
|
|
65
|
-
ctx: RequestContext,
|
|
66
|
-
hash: string,
|
|
67
|
-
tag: string,
|
|
68
|
-
stream: Readable
|
|
69
|
-
): Promise<void> => {
|
|
70
|
-
const objectKey = getS3Key(hash, tag)
|
|
71
|
-
console.log({ objectKey, s3Prefix })
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
// Use the S3 Upload utility which handles multipart uploads for large files
|
|
75
|
-
const upload = new Upload({
|
|
76
|
-
client: s3Client,
|
|
77
|
-
params: {
|
|
78
|
-
Bucket: s3Bucket,
|
|
79
|
-
Key: objectKey,
|
|
80
|
-
Body: stream,
|
|
81
|
-
ContentType: 'application/octet-stream'
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
await upload.done()
|
|
86
|
-
ctx.log.info(`Saved artifact to S3: ${objectKey}`)
|
|
87
|
-
} catch (error) {
|
|
88
|
-
ctx.log.info(`Error saving artifact to S3: ${error}`)
|
|
89
|
-
throw error
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const get = async (
|
|
94
|
-
ctx: RequestContext,
|
|
95
|
-
hash: string
|
|
96
|
-
): Promise<
|
|
97
|
-
[number | undefined, Readable | ReadableStream, string | undefined] | null
|
|
98
|
-
> => {
|
|
99
|
-
// First try to get with just the hash
|
|
100
|
-
const objectKey = getS3Key(hash)
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
// Try to find the object
|
|
104
|
-
const listCommand = new ListObjectsV2Command({
|
|
105
|
-
Bucket: s3Bucket,
|
|
106
|
-
Prefix: objectKey,
|
|
107
|
-
MaxKeys: 10
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
const listResponse = await s3Client.send(listCommand)
|
|
111
|
-
|
|
112
|
-
if (!listResponse.Contents || listResponse.Contents.length === 0) {
|
|
113
|
-
ctx.log.info(`No cached artifact found for ${hash}`)
|
|
114
|
-
return null
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Find the most recent object that matches the hash prefix
|
|
118
|
-
const matchingObjects = listResponse.Contents.filter(
|
|
119
|
-
obj => obj.Key && obj.Key.startsWith(objectKey)
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
if (matchingObjects.length === 0) {
|
|
123
|
-
return null
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Sort by last modified date, newest first
|
|
127
|
-
matchingObjects.sort((a, b) => {
|
|
128
|
-
const dateA = a.LastModified?.getTime() || 0
|
|
129
|
-
const dateB = b.LastModified?.getTime() || 0
|
|
130
|
-
return dateB - dateA
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
const latestObject = matchingObjects[0]
|
|
134
|
-
const key = latestObject.Key as string
|
|
135
|
-
|
|
136
|
-
// Get the object
|
|
137
|
-
const getCommand = new GetObjectCommand({
|
|
138
|
-
Bucket: s3Bucket,
|
|
139
|
-
Key: key
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
const response = await s3Client.send(getCommand)
|
|
143
|
-
|
|
144
|
-
if (!response.Body) {
|
|
145
|
-
ctx.log.info(`Failed to get artifact body from S3`)
|
|
146
|
-
return null
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const size = response.ContentLength
|
|
150
|
-
const stream = response.Body as Readable
|
|
151
|
-
|
|
152
|
-
// Extract the tag if it exists
|
|
153
|
-
let artifactTag: string | undefined
|
|
154
|
-
if (key.includes('#')) {
|
|
155
|
-
const parts = key.split('#')
|
|
156
|
-
artifactTag = parts[parts.length - 1]
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
ctx.log.info(`Retrieved artifact from S3: ${key}`)
|
|
160
|
-
return [size, stream, artifactTag]
|
|
161
|
-
} catch (error) {
|
|
162
|
-
ctx.log.info(`Error getting artifact from S3: ${error}`)
|
|
163
|
-
return null
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const deleteObj = async (key: string): Promise<void> => {
|
|
168
|
-
try {
|
|
169
|
-
const deleteCommand = new DeleteObjectCommand({
|
|
170
|
-
Bucket: s3Bucket,
|
|
171
|
-
Key: key
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
await s3Client.send(deleteCommand)
|
|
175
|
-
} catch (error) {
|
|
176
|
-
core.error(`Error deleting artifact from S3: ${error}`)
|
|
177
|
-
throw error
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const list = async (): Promise<TListFile[]> => {
|
|
182
|
-
try {
|
|
183
|
-
const files: TListFile[] = []
|
|
184
|
-
let continuationToken: string | undefined
|
|
185
|
-
|
|
186
|
-
do {
|
|
187
|
-
// Create a new command for each request with the current continuation token
|
|
188
|
-
const listCommand = new ListObjectsV2Command({
|
|
189
|
-
Bucket: s3Bucket,
|
|
190
|
-
Prefix: s3Prefix,
|
|
191
|
-
MaxKeys: 1000,
|
|
192
|
-
ContinuationToken: continuationToken
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
core.debug(
|
|
196
|
-
`Listing S3 objects with prefix ${s3Prefix}${continuationToken ? ' and continuation token' : ''}`
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
const response = await s3Client.send(listCommand)
|
|
200
|
-
|
|
201
|
-
if (response.Contents && response.Contents.length > 0) {
|
|
202
|
-
core.debug(`Found ${response.Contents.length} objects`)
|
|
203
|
-
|
|
204
|
-
const objects = response.Contents.filter(obj => obj.Key).map(
|
|
205
|
-
(obj): TListFile => {
|
|
206
|
-
return {
|
|
207
|
-
path: obj.Key as string,
|
|
208
|
-
createdAt: (obj.LastModified || new Date()).toISOString(),
|
|
209
|
-
size: obj.Size || 0
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
files.push(...objects)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
continuationToken = response.NextContinuationToken
|
|
218
|
-
if (continuationToken) {
|
|
219
|
-
core.debug(`NextContinuationToken: ${continuationToken}`)
|
|
220
|
-
}
|
|
221
|
-
} while (continuationToken)
|
|
222
|
-
|
|
223
|
-
core.debug(`Total files listed: ${files.length}`)
|
|
224
|
-
return files
|
|
225
|
-
} catch (error) {
|
|
226
|
-
core.error(`Error listing artifacts from S3: ${error}`)
|
|
227
|
-
throw error
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
save,
|
|
233
|
-
get,
|
|
234
|
-
delete: deleteObj,
|
|
235
|
-
list
|
|
236
|
-
}
|
|
237
|
-
}
|
package/src/lib/providers.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'stream'
|
|
2
|
-
import { TListFile } from './server/cleanup'
|
|
3
|
-
import { RequestContext } from './server'
|
|
4
|
-
import { getGithubProvider } from './providers/cache'
|
|
5
|
-
import { getS3Provider } from './providers/s3'
|
|
6
|
-
import { core } from './core'
|
|
7
|
-
|
|
8
|
-
export type TProvider = {
|
|
9
|
-
save: (
|
|
10
|
-
ctx: RequestContext,
|
|
11
|
-
hash: string,
|
|
12
|
-
tag: string,
|
|
13
|
-
stream: Readable
|
|
14
|
-
) => Promise<void>
|
|
15
|
-
get: (
|
|
16
|
-
ctx: RequestContext,
|
|
17
|
-
hash: string
|
|
18
|
-
) => Promise<
|
|
19
|
-
[number | undefined, Readable | ReadableStream, string | undefined] | null
|
|
20
|
-
>
|
|
21
|
-
delete: (key: string) => Promise<void>
|
|
22
|
-
list: () => Promise<TListFile[]>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const getProvider = (): TProvider => {
|
|
26
|
-
const provider = core.getInput('provider') || process.env.PROVIDER
|
|
27
|
-
|
|
28
|
-
if (!provider) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
'Provider is required. Set PROVIDER environment variable or provider input.'
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (provider === 'github') {
|
|
35
|
-
return getGithubProvider()
|
|
36
|
-
}
|
|
37
|
-
if (provider === 's3') {
|
|
38
|
-
return getS3Provider()
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
throw new Error(`Provider ${provider} not supported`)
|
|
42
|
-
}
|