@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/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()
@@ -1,14 +0,0 @@
1
- import { config } from 'dotenv'
2
- config()
3
-
4
- import { cleanup } from 'src/lib/server/cleanup'
5
-
6
- const main = async () => {
7
- await cleanup({
8
- log: {
9
- info: console.log
10
- }
11
- })
12
- }
13
-
14
- main()
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
@@ -1,6 +0,0 @@
1
- /**
2
- * The entrypoint for the action.
3
- */
4
- import { run } from './main'
5
-
6
- run()
@@ -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
- }
@@ -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,7 +0,0 @@
1
- import { core } from './core'
2
- import { waitForServer } from './server/utils'
3
-
4
- export const ping = async () => {
5
- await waitForServer()
6
- core.success('Server is up and running')
7
- }
@@ -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
- }
@@ -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
- }