Skip to content

Commit

Permalink
MultiCI: Abstract gh action exec calls to wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
Nava2 committed Jan 9, 2025
1 parent 1743076 commit 02206f4
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 22 deletions.
28 changes: 27 additions & 1 deletion sources/src/actions/github-actions-env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as core from '@actions/core'
import {state} from '../env/state'
import * as ghExec from '@actions/exec'

import {exec, GradleEnvExecOptions} from '../env/execution'
import {log, LogLevel} from '../env/logging'
import {state} from '../env/state'

function setupState(): void {
state.setImpl({
Expand Down Expand Up @@ -37,7 +40,30 @@ function setupLogger(): void {
})
}

function setupExec(): void {
exec.setImpl({
group: core.group,

run: async (command: string, args?: string[], options?: GradleEnvExecOptions): Promise<void> => {
await ghExec.exec(command, args, options)
},

exec: async (commandLine: string, args?: string[], options?: GradleEnvExecOptions): Promise<number> => {
return await ghExec.exec(commandLine, args, options)
},

getExecOutput: async (
command: string,
args?: string[],
options?: GradleEnvExecOptions
): Promise<{stdout: string; stderr: string}> => {
return await ghExec.getExecOutput(command, args, options)
}
})
}

export const configureGithubEnv = (): void => {
setupState()
setupLogger()
setupExec()
}
6 changes: 2 additions & 4 deletions sources/src/caching/cache-cleaner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'

import fs from 'fs'
import path from 'path'
import * as provisioner from '../execution/provision'
import {exec} from '../env/execution'
import {log} from '../env/logging'
import {state} from '../env/state'

Expand Down Expand Up @@ -60,7 +58,7 @@ export class CacheCleaner {
// TODO: This is ineffective: we should be using the newest version of Gradle that ran a build, or a newer version if it's available on PATH.
const executable = await provisioner.provisionGradleAtLeast('8.12')

await core.group('Executing Gradle to clean up caches', async () => {
await exec.group('Executing Gradle to clean up caches', async () => {
log.info(`Cleaning up caches last used before ${cleanTimestamp}`)
await this.executeCleanupBuild(executable, cleanupProjectDir)
})
Expand Down
2 changes: 1 addition & 1 deletion sources/src/caching/cache-utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as cache from '@actions/cache'
import * as exec from '@actions/exec'

import * as crypto from 'crypto'
import * as path from 'path'
import * as fs from 'fs'

import {exec} from '../env/execution'
import {log} from '../env/logging'
import {CacheEntryListener} from './cache-reporting'

Expand Down
8 changes: 4 additions & 4 deletions sources/src/caching/caches.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as core from '@actions/core'
import {
CacheListener,
EXISTING_GRADLE_HOME,
Expand All @@ -10,6 +9,7 @@ import {CacheCleaner} from './cache-cleaner'
import {DaemonController} from '../daemon-controller'
import {CacheConfig} from '../configuration'
import {BuildResults} from '../build-results'
import {exec} from '../env/execution'
import {log} from '../env/logging'
import {state} from '../env/state'

Expand Down Expand Up @@ -65,7 +65,7 @@ export async function restore(
return
}

await core.group('Restore Gradle state from cache', async () => {
await exec.group('Restore Gradle state from cache', async () => {
await gradleStateCache.restore(cacheListener)
})
}
Expand Down Expand Up @@ -94,7 +94,7 @@ export async function save(
return
}

await core.group('Stopping Gradle daemons', async () => {
await exec.group('Stopping Gradle daemons', async () => {
await daemonController.stopAllDaemons()
})

Expand All @@ -111,7 +111,7 @@ export async function save(
}
}

await core.group('Caching Gradle state', async () => {
await exec.group('Caching Gradle state', async () => {
return new GradleUserHomeCache(userHome, gradleUserHome, cacheConfig).save(cacheListener)
})
}
Expand Down
2 changes: 1 addition & 1 deletion sources/src/caching/gradle-user-home-cache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as exec from '@actions/exec'
import * as glob from '@actions/glob'

import path from 'path'
Expand All @@ -9,6 +8,7 @@ import {saveCache, restoreCache, tryDelete} from './cache-utils'
import {CacheConfig, ACTION_METADATA_DIR} from '../configuration'
import {GradleHomeEntryExtractor, ConfigurationCacheEntryExtractor} from './gradle-home-extry-extractor'
import {getPredefinedToolchains, mergeToolchainContent, readResourceFileAsString} from './gradle-user-home-utils'
import {exec} from '../env/execution'
import {log} from '../env/logging'
import {state} from '../env/state'

Expand Down
2 changes: 1 addition & 1 deletion sources/src/daemon-controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as path from 'path'
import {BuildResults} from './build-results'
import {exec} from './env/execution'
import {log} from './env/logging'

export class DaemonController {
Expand Down
89 changes: 89 additions & 0 deletions sources/src/env/execution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Provides a wrapper for execution of system commands in a job.
*/
export interface GradleEnvExecutionImpl {
/**
* Wrap an asynchronous function call in a "group" of execution in a CI pipeline.
* @param name Name of the group.
* @param fn
*/
group<R>(name: string, fn: () => Promise<R>): Promise<R>

/**
* Executes
* @param command
* @param args
*/
run(command: string, args?: string[], options?: GradleEnvExecOptions): Promise<void>

/**
* Executes command and returns the output.
* @param command
* @param args
* @param options
*/
getExecOutput(
command: string,
args?: string[],
options?: GradleEnvExecOptions
): Promise<{stdout: string; stderr: string}>

/**
* Exec a command.
* Output will be streamed to the live console.
* Returns promise with return code
*
* @param commandLine command to execute (can include additional args). Must be correctly escaped.
* @param args optional arguments for tool. Escaping is handled by the lib.
* @param options optional exec options. See ExecOptions
* @returns Promise<number> exit code
*/
exec(command: string, args?: string[], options?: GradleEnvExecOptions): Promise<number>
}

/**
* Interface for exec options
*/
export interface GradleEnvExecOptions {
/** optional working directory. defaults to current */
cwd?: string

silent?: boolean

ignoreReturnCode?: boolean

/** optional envvar dictionary. defaults to current process's env */
env?: {
[key: string]: string
}
}

class GradleExec implements GradleEnvExecutionImpl {
private impl!: GradleEnvExecutionImpl

setImpl(impl: GradleEnvExecutionImpl): void {
this.impl = impl
}

async group<R>(name: string, fn: () => Promise<R>): Promise<R> {
return await this.impl.group(name, fn)
}

async run(command: string, args?: string[], options?: GradleEnvExecOptions): Promise<void> {
await this.impl.run(command, args, options)
}

async exec(command: string, args?: string[], options?: GradleEnvExecOptions): Promise<number> {
return await this.impl.exec(command, args, options)
}

async getExecOutput(
command: string,
args?: string[],
options?: GradleEnvExecOptions
): Promise<{stdout: string; stderr: string}> {
return await this.impl.getExecOutput(command, args, options)
}
}

export const exec = new GradleExec()
4 changes: 2 additions & 2 deletions sources/src/execution/gradle.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as exec from '@actions/exec'

import which from 'which'
import * as semver from 'semver'

import * as provisioner from './provision'
import * as gradlew from './gradlew'
import {exec} from '../env/execution'
import {state} from '../env/state'

export async function provisionAndMaybeExecute(
Expand Down
5 changes: 3 additions & 2 deletions sources/src/execution/provision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {findGradleVersionOnPath, versionIsAtLeast} from './gradle'
import * as gradlew from './gradlew'
import {handleCacheFailure} from '../caching/cache-utils'
import {CacheConfig} from '../configuration'
import {exec} from '../env/execution'
import {log} from '../env/logging'

const gradleVersionsBaseUrl = 'https://services.gradle.org/versions'
Expand Down Expand Up @@ -106,7 +107,7 @@ async function findGradleVersionDeclaration(version: string): Promise<GradleVers
}

async function installGradleVersion(versionInfo: GradleVersionInfo): Promise<string> {
return core.group(`Provision Gradle ${versionInfo.version}`, async () => {
return exec.group(`Provision Gradle ${versionInfo.version}`, async () => {
const gradleOnPath = await findGradleVersionOnPath()
if (gradleOnPath?.version === versionInfo.version) {
log.info(`Gradle version ${versionInfo.version} is already available on PATH. Not installing.`)
Expand All @@ -118,7 +119,7 @@ async function installGradleVersion(versionInfo: GradleVersionInfo): Promise<str
}

async function installGradleVersionAtLeast(versionInfo: GradleVersionInfo): Promise<string> {
return core.group(`Provision Gradle >= ${versionInfo.version}`, async () => {
return exec.group(`Provision Gradle >= ${versionInfo.version}`, async () => {
const gradleOnPath = await findGradleVersionOnPath()
if (gradleOnPath && versionIsAtLeast(gradleOnPath.version, versionInfo.version)) {
log.info(
Expand Down
2 changes: 1 addition & 1 deletion sources/src/setup-gradle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as path from 'path'
import * as os from 'os'
Expand All @@ -17,6 +16,7 @@ import {
getWorkspaceDirectory
} from './configuration'
import * as wrapperValidator from './wrapper-validation/wrapper-validator'
import {exec} from './env/execution'
import {log} from './env/logging'
import {state} from './env/state'

Expand Down
5 changes: 2 additions & 3 deletions sources/src/wrapper-validation/wrapper-validator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as core from '@actions/core'

import {WrapperValidationConfig} from '../configuration'
import {ChecksumCache} from './cache'
import {findInvalidWrapperJars} from './validate'
import {JobFailure} from '../errors'
import {exec} from '../env/execution'
import {log} from '../env/logging'

export async function validateWrappers(
Expand All @@ -26,7 +25,7 @@ export async function validateWrappers(
previouslyValidatedChecksums
)
if (result.isValid()) {
await core.group('All Gradle Wrapper jars are valid', async () => {
await exec.group('All Gradle Wrapper jars are valid', async () => {
log.debug(`Loaded previously validated checksums from cache: ${previouslyValidatedChecksums.join(', ')}`)
log.info(result.toDisplayString())
})
Expand Down
3 changes: 1 addition & 2 deletions sources/test/jest/cache-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as exec from '@actions/exec'
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import fs from 'fs'
import path from 'path'
import {CacheCleaner} from '../../src/caching/cache-cleaner'
import { exec } from '../../src/env/execution'

import { configureTestEnv } from './test-env'
configureTestEnv()
Expand Down
46 changes: 46 additions & 0 deletions sources/test/jest/test-env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { state, GradleEnvStateImplementation } from '../../src/env/state'
import { exec, GradleEnvExecOptions } from '../../src/env/execution'
import { log } from '../../src/env/logging'

import util from 'util'
import { exec as cp_exec } from 'child_process'

const pexec = util.promisify(cp_exec);

class TestStateImplementation implements GradleEnvStateImplementation {
readonly state: Record<string, string> = {}
Expand Down Expand Up @@ -49,6 +56,45 @@ function setupState(): void {
state.setImpl(testState)
}

function setupExec(): void {
exec.setImpl(
{
group: <R>(name: string, fn: () => Promise<R>): Promise<R> => {
try {
log.debug(`::group-enter:: ${name}`)
return fn()
} finally {
log.debug(`::group-exit:: ${name}`)
}
},

run: async (command: string, args?: string[], options?: GradleEnvExecOptions): Promise<void> => {
await pexec([command, ...(args ?? [])].join(' '), options)
},

exec: async (command: string, args?: string[], options?: GradleEnvExecOptions): Promise<number> => {
try {
await pexec([command, ...(args ?? [])].join(' '), options)
return 0
} catch (error) {
log.warn(`Failed to run ${command}: ${error}`)
return 1
}
},

getExecOutput: async (
command: string,
args?: string[],
options?: GradleEnvExecOptions
): Promise<{stdout: string; stderr: string}> => {
const result = await pexec([command, ...(args ?? [])].join(' '), options)
return {stdout: result.stdout.toString(), stderr: result.stderr.toString()}
}
}
)
}

export const configureTestEnv = (): void => {
setupState()
setupExec()
}

0 comments on commit 02206f4

Please sign in to comment.