Skip to content

Commit

Permalink
feat: mount support (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
samypr100 authored May 12, 2024
1 parent ff49dab commit 5f95600
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 120 deletions.
62 changes: 62 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
drive-format: NTFS
drive-type: Fixed
drive-path: "my_awesome_drive.vhdx"
mount-if-exists: false
workspace-copy: true

test-large-fixed:
Expand All @@ -84,3 +85,64 @@ jobs:
with:
drive-size: 10GB
drive-type: Dynamic

test-path-with-spaces:
runs-on: windows-2022
steps:
- name: Check out source code
uses: actions/checkout@v4
- name: Setup Dev Drive
uses: ./
with:
drive-size: 50MB
drive-format: NTFS
drive-path: "path/to/my/dev drive with spaces.vhdx"
- name: Write File to Dev Drive
working-directory: ${{ env.DEV_DRIVE }}
run: New-Item test.txt

test-cache-storage:
runs-on: windows-2022
steps:
- name: Check out source code
uses: actions/checkout@v4
- name: Setup Dev Drive
uses: ./
with:
drive-size: 50MB
drive-format: NTFS
drive-path: "my_cached_drive.vhdx"
mount-if-exists: true
- name: Write File to Dev Drive
working-directory: ${{ env.DEV_DRIVE }}
run: |
New-Item test.txt
Dismount-VHD -Path ${{ env.DEV_DRIVE_PATH }}
- name: Cache Dev Drive
uses: actions/cache/save@v4
with:
path: ${{ env.DEV_DRIVE_PATH }}
key: test-cache-${{ github.run_id }}
outputs:
dev-drive-path: ${{ env.DEV_DRIVE_PATH }}

test-cache-retrieval:
runs-on: windows-2022
needs: [test-cache-storage]
steps:
- name: Check out source code
uses: actions/checkout@v4
- name: Retrieve Cached Dev Drive
uses: actions/cache/restore@v4
with:
path: ${{ needs.test-cache-storage.outputs.dev-drive-path }}
key: test-cache-${{ github.run_id }}
fail-on-cache-miss: true
- name: Setup Dev Drive
uses: ./
with:
drive-path: "my_cached_drive.vhdx"
mount-if-exists: true
- name: Check File in Dev Drive
working-directory: ${{ env.DEV_DRIVE }}
run: Test-Path test.txt -PathType Leaf
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ You can optionally pass parameters to the action as follows:
drive-format: ReFS
drive-type: Fixed
drive-path: "dev_drive.vhdx"
mount-if-exists: false
workspace-copy: true
```
Expand Down Expand Up @@ -66,6 +67,12 @@ payload to cache when the job ends.

`Fixed` gives you a notable performance boost, but there's a small creation overhead.

#### `mount-if-exists`

Mounts the Dev Drive if it already exists at `drive-path` location. When it does not exist,
it will fall back to creating one at that location instead. This is useful when your workflow
caches the Dev Drive for further use in other jobs via `actions/cache`.

#### `workspace-copy`

This copies `${{ github.workspace }}` to your Dev Drive. Usually when you use `actions/checkout`
Expand All @@ -92,6 +99,16 @@ When `workspace-copy` is set to true, this contains the workspace location as re
by the dev drive location. For example if your GitHub workspace is `C:\a\<project-name>\<project-name>`
your dev drive workspace will be `E:\<project-name>` by default assuming the drive letter is `E`.

#### `DEV_DRIVE_PATH`

The canonical location of the VHDX file.

When `drive-path` is set to a relative path like `my_drive.vhdx`
the location in this variable will likely be `C:\my_drive.vhdx`.

On the other hand, when `drive-path` is set to an absolute path,
that's likely what this variable will contain after normalization.

### Examples

```yaml
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ inputs:
drive-type:
description: "Determines whether all space is allocated initially or over time. Example: Fixed or Dynamic."
default: "Dynamic"
mount-if-exists:
description: "Supports mounting an existing VHDX located in `drive-path`, avoiding creation overhead."
default: "false"
workspace-copy:
description: "Copy your GITHUB_WORKSPACE checkout to your Dev Drive. Examples: true, false."
default: "false"
Expand Down
2 changes: 1 addition & 1 deletion dist/main.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dist/post.js

Large diffs are not rendered by default.

16 changes: 3 additions & 13 deletions src/cleanup-impl.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as core from '@actions/core'
import { exec, ExecOptions } from '@actions/exec'
import { POWERSHELL_BIN, StateVariables, WIN_PLATFORM } from './constants'
import { quote } from 'shell-quote'
import { StateVariables, WIN_PLATFORM } from './constants'
import { dismount } from './vhd-commands'

export async function cleanup(): Promise<void> {
if (process.platform !== WIN_PLATFORM) {
Expand All @@ -13,15 +12,6 @@ export async function cleanup(): Promise<void> {
const drivePath = core.getState(StateVariables.DevDrivePath)
core.debug(`Retrieved State ${StateVariables.DevDrivePath}=${drivePath}`)

const pathArg = quote([drivePath])
const pwshCommand = `Dismount-VHD -Path ${pathArg}`
const pwshCommandArgs = ['-NoProfile', '-Command', `. {${pwshCommand}}`]

const options: ExecOptions = {}
options.failOnStdErr = false
options.ignoreReturnCode = true

const ret = await exec(POWERSHELL_BIN, pwshCommandArgs, options)

const ret = await dismount(drivePath)
core.info(`Removal completed with exit code ${ret}...`)
}
4 changes: 3 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ export enum ExternalInputs {
DriveFormat = 'drive-format',
DrivePath = 'drive-path',
DriveType = 'drive-type',
MountIfExists = 'mount-if-exists',
WorkspaceCopy = 'workspace-copy',
}

export enum EnvVariables {
DevDrive = 'DEV_DRIVE',
DevDriveWorkspace = 'DEV_DRIVE_WORKSPACE',
DevDrivePath = 'DEV_DRIVE_PATH',
}

export enum StateVariables {
DevDrivePath = 'DEV_DRIVE_PATH',
DevDrivePath = EnvVariables.DevDrivePath,
}

export enum GithubVariables {
Expand Down
10 changes: 9 additions & 1 deletion src/setup-dev-drive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ async function main() {
const driveFormat = core.getInput(ExternalInputs.DriveFormat)
const drivePath = core.getInput(ExternalInputs.DrivePath)
const driveType = core.getInput(ExternalInputs.DriveType)
const mountIfExists = core.getBooleanInput(ExternalInputs.MountIfExists)
const copyWorkspace = core.getBooleanInput(ExternalInputs.WorkspaceCopy)
await setup(driveSize, driveFormat, drivePath, driveType, copyWorkspace)
await setup(
driveSize,
driveFormat,
drivePath,
driveType,
mountIfExists,
copyWorkspace,
)
}

main().catch(err => core.setFailed(err.message))
132 changes: 31 additions & 101 deletions src/setup-impl.ts
Original file line number Diff line number Diff line change
@@ -1,112 +1,24 @@
import * as core from '@actions/core'
import { toPlatformPath } from '@actions/core'
import { exec, ExecOptions } from '@actions/exec'
import { quote } from 'shell-quote'
import os from 'node:os'
import fs from 'fs-extra'
import path from 'node:path'
import { compare } from 'compare-versions'
import {
EnvVariables,
ExternalInputs,
GithubVariables,
NATIVE_DEV_DRIVE_WIN_VERSION,
POWERSHELL_BIN,
StateVariables,
VHDDriveTypes,
VHDX_EXTENSION,
WIN_PLATFORM,
} from './constants'
import { create, mount } from './vhd-commands'

function genSetupCommandArgs(
driveSize: string,
driveFormat: string,
drivePath: string,
driveType: string,
): string[] {
const sizeArg = quote([driveSize])
const formatArg = quote([driveFormat])
const pathArg = quote([drivePath])

const osVersion = os.release()
const supportsDevDrive = compare(os.release(), NATIVE_DEV_DRIVE_WIN_VERSION, '>=')
core.debug(`Windows Version ${osVersion}. Native Dev Drive? ${supportsDevDrive}`)

let formatCmd = `Format-Volume -FileSystem ${formatArg} -Confirm:$false -Force ;`
if (supportsDevDrive && formatArg === 'ReFS') {
formatCmd = `Format-Volume -DevDrive -Confirm:$false -Force ;`
}

const pwshCommand = [
`$DevDrive = New-VHD -Path ${pathArg} -SizeBytes ${sizeArg} -${driveType} |`,
'Mount-VHD -Passthru |',
'Initialize-Disk -Passthru |',
'New-Partition -AssignDriveLetter -UseMaximumSize |',
formatCmd,
`Write-Output ($DevDrive.DriveLetter + ':')`,
].join(' ')

core.debug(`Generated the following command:\n${pwshCommand}`)

return ['-NoProfile', '-Command', `. {${pwshCommand}}`]
}

async function execSetupCommand(
driveSize: string,
driveFormat: string,
drivePath: string,
driveType: string,
): Promise<string> {
const options: ExecOptions = {}
let outStr = ''
let errStr = ''
options.listeners = {
stdout: (data: Buffer) => {
outStr += data.toString()
},
stderr: (data: Buffer) => {
errStr += data.toString()
},
}

const pwshCommandArgs = genSetupCommandArgs(
driveSize,
driveFormat,
drivePath,
driveType,
)

await exec(POWERSHELL_BIN, pwshCommandArgs, options)

core.debug(`Command stdout:\n${outStr}`)
core.debug(`Command stderr:\n${errStr}`)

// Trim because Buffers tend to have newlines.
const cleanedErr = errStr.trim()
if (cleanedErr.length > 0) {
core.error('Failed to created Dev Drive...')
throw new Error(cleanedErr)
}

// Trim because Buffers tend to have newlines.
const driveLetter = outStr.trim()
if (driveLetter.length !== 2) {
core.error(`Failed to recover Dev Drive...`)
throw new Error('Exit due to unrecoverable state.')
}

// Do a check on the Dev Drive
// If it fails, this will raise an error and the generic handler will take care of it.
await fs.access(driveLetter, fs.constants.F_OK | fs.constants.W_OK)

return driveLetter
}

async function doCreateDevDrive(
async function doDevDriveCommand(
driveSize: string,
driveFormat: string,
drivePath: string,
driveType: string,
mountIfExists: boolean,
): Promise<string> {
// Normalize User Path Input
let normalizedDrivePath = toPlatformPath(drivePath)
Expand All @@ -123,22 +35,38 @@ async function doCreateDevDrive(
throw new Error(`Make sure ${ExternalInputs.DriveType} is either ${allowedMsg}`)
}

core.info('Creating Dev Drive...')
const driveLetter = await execSetupCommand(
driveSize,
driveFormat,
normalizedDrivePath,
driveType,
)
if (mountIfExists) {
try {
// Do a check on the Dev Drive
await fs.access(normalizedDrivePath, fs.constants.F_OK | fs.constants.R_OK)
} catch (e) {
core.debug((e as NodeJS.ErrnoException).message)
// Fallback to creation...
mountIfExists = false
core.info('Dev Drive did not exist, will create instead...')
}
}

let driveLetter
if (!mountIfExists) {
core.info('Creating Dev Drive...')
driveLetter = await create(driveSize, driveFormat, normalizedDrivePath, driveType)
core.info('Successfully created Dev Drive...')
} else {
core.info('Mounting Dev Drive...')
driveLetter = await mount(normalizedDrivePath)
core.info('Successfully mounted Dev Drive...')
}

core.debug(`Exporting EnvVar ${EnvVariables.DevDrive}=${driveLetter}`)
core.exportVariable(EnvVariables.DevDrive, driveLetter)

core.debug(`Exporting EnvVar ${EnvVariables.DevDrivePath}=${normalizedDrivePath}`)
core.exportVariable(EnvVariables.DevDrivePath, normalizedDrivePath)

core.debug(`Saving State ${StateVariables.DevDrivePath}=${normalizedDrivePath}`)
core.saveState(StateVariables.DevDrivePath, normalizedDrivePath)

core.info('Successfully created Dev Drive...')

return driveLetter
}

Expand Down Expand Up @@ -167,17 +95,19 @@ export async function setup(
driveFormat: string,
drivePath: string,
driveType: string,
mountIfExists: boolean,
copyWorkspace: boolean,
): Promise<void> {
if (process.platform !== WIN_PLATFORM) {
throw new Error('This action can only run on windows.')
}

const driveLetter = await doCreateDevDrive(
const driveLetter = await doDevDriveCommand(
driveSize,
driveFormat,
drivePath,
driveType,
mountIfExists,
)

if (copyWorkspace) {
Expand Down
Loading

0 comments on commit 5f95600

Please sign in to comment.