tools/assets-automation/asset-sync/assets.ps1 (514 lines of code) (raw):
Set-StrictMode -Version 3
$REPO_ROOT = Resolve-Path (Join-Path $PSScriptRoot ".." "..")
$ASSETS_STORE = (Join-Path $REPO_ROOT ".assets")
$EXPECTED_MEMBERS = @("AssetsRepo", "AssetsRepoPrefixPath", "AssetsRepoBranch", "SHA")
. (Join-Path $REPO_ROOT "eng" "common" "scripts" "common.ps1")
<#
.SYNOPSIS
Checks the contents of a directory, then returns an array of booleans @(<assetsJsonPresent>, <isRootFolder>).
.DESCRIPTION
Evaluates a directory by checking its contents. First value of the tuple is whether or not a "assets.json" file
is present A "root" directory is one where a assets.json is present OR where we are as far up the file tree as
we can possibly ascend.
.PARAMETER TargetPath
A targeted directory. This MUST be a directory, not a file path.
#>
Function EvaluateDirectory {
param (
[Parameter(Mandatory = $true)]
[string] $TargetPath
)
if (!(Test-Path $TargetPath)){
return @(
$false, $false
)
}
# can't handle files here
if (Test-Path $TargetPath -PathType Leaf) {
throw "Evaluated a file `"$TargetPath`" as a directory. Exiting."
}
return @(
(Test-Path -Path (Join-Path $TargetPath "assets.json")),
(Test-Path -Path (Join-Path $TargetPath ".git"))
)
}
<#
.SYNOPSIS
From a start directory, stops when it finds the root of a git repository (or root of disk).
.PARAMETER StartPath
The starting directory.
#>
Function AscendToRepoRoot {
param(
[Parameter(Mandatory = $false)]
[string] $StartPath
)
$pathForManipulation = $StartPath
if(Test-Path $StartPath -PathType Leaf){
$pathForManipulation = Split-Path $pathForManipulation
}
$foundConfig, $reachedRoot = EvaluateDirectory -TargetPath $pathForManipulation
while (-not $reachedRoot){
$pathForManipulation, $remainder = Split-Path $pathForManipulation
$foundConfig, $reachedRoot = EvaluateDirectory -TargetPath $pathForManipulation
}
return $pathForManipulation
}
<#
.SYNOPSIS
Traverses up from a provided target directory to find a assets JSON, parses it, and returns the location and JSON content.
.DESCRIPTION
Traverses upwards until it hits either a `.git` folder or a `assets.json` file. Throws an exception if it can't find a assets.json before it hits root.
.PARAMETER TargetPath
Optional argument specifying the directory to start traversing up from. If not provided, current working directory will be used.
#>
Function ResolveAssetsJson {
param (
[Parameter(Mandatory = $false)]
[string] $TargetPath
)
$pathForManipulation = $TargetPath
$foundConfig = $false
$reachedRoot = $false
if(-not $TargetPath){
$pathForManipulation = Get-Location
}
else {
$pathForManipulation = Resolve-Path -Path $TargetPath
}
$foundConfig, $reachedRoot = EvaluateDirectory -TargetPath $pathForManipulation
while (-not $foundConfig -and -not $reachedRoot){
$pathForManipulation, $remainder = Split-Path $pathForManipulation
$foundConfig, $reachedRoot = EvaluateDirectory -TargetPath $pathForManipulation
}
if ($foundConfig){
$discoveredPath = Join-Path $pathForManipulation "assets.json"
}
else {
throw "Unable to locate assets.json"
}
# path to assets Json
$config = (Get-Content -Raw -Path $discoveredPath | ConvertFrom-Json)
Add-Member -InputObject $config -MemberType "NoteProperty" -Name "AssetsJsonLocation" -Value "$discoveredPath"
$relPath = AscendToRepoRoot -StartPath $discoveredPath
if($relPath){
try {
Push-Location $relPath
$relPath = Resolve-Path -Relative -Path $discoveredPath
# relpaths are returned with "./<blah>"
# given that, we need to get rid of it. This has possibility for bugs down the line.
$relPath = $relPath -replace "^(\.\/)|(\.\\)"
}
finally {
Pop-Location
}
# relative path to assets Json from within path
Add-Member -InputObject $config -MemberType "NoteProperty" -Name "AssetsJsonRelativeLocation" -Value $relPath
}
$props = Get-Member -InputObject $config -MemberType NoteProperty | ForEach-Object { $_.Name }
$missingMembers = Compare-Object -ReferenceObject $EXPECTED_MEMBERS -DifferenceObject $props `
| Where-Object { $_.SideIndicator -ne "=>"} | Foreach-Object { $_.InputObject }
if($missingMembers){
if($missingMembers.Length -gt 0){
$allMissingMembers = $missingMembers -Join ", "
throw "Missing required members for assets json detected: `"$($allMissingMembers)`""
}
}
return $config
}
<#
.SYNOPSIS
Returns the location of the "assets" store. This should return a string of the form "<path to language repo root>/.assets."
#>
Function ResolveAssetStoreLocation {
if (-not (Test-Path $ASSETS_STORE)){
New-Item -Type Directory -Force -Path $ASSETS_STORE | Out-Null
}
$ASSETS_STORE = Resolve-Path $ASSETS_STORE
return $ASSETS_STORE
}
<#
Takes an input string, returns the MD5 hash for the entire thing.
.PARAMETER Input
Any string.
#>
Function GetMD5Hash {
param(
[Parameter(Mandatory=$true)]
[string] $Input
)
$stringAsStream = [System.IO.MemoryStream]::new()
$writer = [System.IO.StreamWriter]::new($stringAsStream)
$writer.write($Input)
$writer.Flush()
$stringAsStream.Position = 0
return Get-FileHash -InputStream $stringAsStream -Algorithm MD5
}
<#
.SYNOPSIS
Given a configuration, where will the assets repo exist? This function will both return that answer, as well as ensure
that directory exists.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
#>
Function ResolveAssetRepoLocation {
param(
[Parameter(Mandatory=$true)]
[PSCustomObject] $Config
)
$assetsLocation = ResolveAssetStoreLocation
$repoName = $Config.AssetsRepo.Replace("/", ".")
# this is where we will need to handle the multi-copying of the repository.
# to begin with, we will use the relative path of the assets json in combination with the
# Repo/RepoId to create a unique hash. In the future, we will need to take the targeted commit into account,
# and resolve conflicts
if ($Config.AssetsRepoId) {
$repoName = $Config.AssetsRepoId
}
$repoNameHashed = GetMD5Hash -Input ((Join-Path $repoName $Config.AssetsJsonRelativeLocation).ToString())
$repoPath = (Join-Path $assetsLocation $repoNameHashed.Hash)
if (-not (Test-Path $repoPath)){
New-Item -Type Directory -Force -Path $repoPath | Out-Null
}
return $repoPath
}
<#
.SYNOPSIS
Gets the default branch from the git repo targeted in a assets.json.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
#>
Function GetDefaultBranch {
param(
[Parameter(Mandatory=$true)]
[PSCustomObject] $Config
)
$repoJsonResult = Invoke-RestMethod -Method "GET" -Uri "https://api.github.com/repos/$($Config.AssetsRepo)"
return ($repoJsonResult | ConvertFrom-Json).default_branch
}
<#
.SYNOPSIS
This function returns a boolean that indicates whether or not the assets repo has been initialized.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
#>
Function IsAssetsRepoInitialized {
param(
[PSCustomObject] $Config
)
$result = $false
$assetRepoLocation = ResolveAssetRepoLocation -Config $Config
try {
$gitLocation = Join-Path $assetRepoLocation ".git"
$result = Test-Path $gitLocation
}
catch {
Write-Error $_
$result = $false
}
return $result
}
<#
.SYNOPSIS
Given a configuration, determine which paths must be added to the sparse checkout of the assets repo.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
#>
Function ResolveCheckoutPaths {
param(
[Parameter(Mandatory=$true)]
[PSCustomObject] $Config
)
$assetsJsonFolder = Split-Path $Config.AssetsJsonRelativeLocation
if(!$assetsJsonFolder -or $assetsJsonFolder -in @(".", "./", ".\"))
{
return $Config.AssetsRepoPrefixPath.Replace("`\", "/")
}
else {
$result = (Join-Path $Config.AssetsRepoPrefixPath $assetsJsonFolder).Replace("`\", "/")
return $result
}
}
<#
.SYNOPSIS
Resolve which branch on the assets repo should be checked out, given an input Configuration.
.DESCRIPTION
Determines the presence of a branch on the git repo. If the relevant auto/<service> branch does not exist, we should use main.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
#>
Function ResolveTargetBranch {
param(
[Parameter(Mandatory=$true)]
[PSCustomObject] $Config
)
$assetRepo = ResolveAssetRepoLocation -Config $Config
$branch = "main"
try {
Push-Location $assetRepo
$latestCommit = git rev-parse "origin/$($Config.AssetsRepoBranch)"
if($LASTEXITCODE -eq 0) {
$branch = $Config.AssetsRepoBranch
}
}
finally {
Pop-Location
}
return $branch
}
<#
.SYNOPSIS
Uses a target config to reset an already initialized assets repository to another service/commit SHA.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json content from ResolveAssetsJson.
.PARAMETER SparseCheckoutPath
The target path within the repo that should be sparsely checked out. Multiple path values are supported, but one must place spaces between them.
#>
Function CheckoutRepoAtConfig {
param(
[Parameter(Mandatory=$true)]
[PSCustomObject] $Config,
[string] $SparseCheckoutPath
)
Write-Host "git sparse-checkout set $($SparseCheckoutPath)"
git sparse-checkout set $SparseCheckoutPath
Write-Host "git checkout $($Config.SHA)"
git checkout $($Config.SHA)
}
<#
.SYNOPSIS
Initializes a recordings repo based on an assets.json file.
.DESCRIPTION
This Function will NOT re-initialize a repo if it discovers the repo already ready to go.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json content from ResolveAssetsJson.
.PARAMETER ForceReinitialize
Should this assets repo be renewed regardless of current status?
#>
Function InitializeAssetsRepo {
param(
[Parameter(Mandatory=$true)]
[PSCustomObject] $Config,
[Parameter(Mandatory=$false)]
[boolean] $ForceReinitialize = $false
)
$assetRepo = ResolveAssetRepoLocation -Config $Config
$initialized = IsAssetsRepoInitialized -Config $Config
$workCompleted = $false
if ($ForceReinitialize)
{
Remove-Item -Force -R "$assetRepo/*"
$initialized = $false
}
if (!$initialized){
try {
Push-Location $assetRepo
Write-Host "git clone --no-checkout --filter=tree:0 https://github.com/$($Config.AssetsRepo) ."
git clone --no-checkout --filter=tree:0 https://github.com/$($Config.AssetsRepo) .
$targetPath = ResolveCheckoutPaths -Config $Config
Write-Host "git sparse-checkout init"
git sparse-checkout init
CheckoutRepoAtConfig -Config $Config -SparseCheckoutPath $targetPath
if($LASTEXITCODE -gt 0){
throw "Unable to clone to directory $assetRepo"
}
$workCompleted = $true
}
finally {
Pop-Location
}
}
return $workCompleted
}
<#
.SYNOPSIS
Used to interrupt script flow and get user input.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
.PARAMETER UserPrompt
This is the message that should be shown to the user before waiting for input.
#>
Function GetUserInput {
param(
[Parameter(Mandatory = $true)]
[PSCustomObject] $Config,
[Parameter(Mandatory = $false)]
[string] $UserPrompt = "Please type some input, then press ENTER to accept."
)
return Read-Host $UserPrompt
}
<#
.SYNOPSIS
Are there any files changed?
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
#>
Function DetectPendingChanges {
param(
[Parameter(Mandatory = $true)]
[PSCustomObject] $Config
)
$assetRepo = ResolveAssetRepoLocation -Config $Config
$filesChanged = @()
try {
Push-Location $assetRepo
Write-Host "git diff-index --name-only HEAD"
$filesChanged = git diff-index --name-only HEAD
}
finally {
Pop-Location
}
return $filesChanged
}
<#
.SYNOPSIS
This function will forcibly reset the repo to a targeted SHA. This is a **destructive** update.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
#>
Function ResetAssetsRepo {
param (
[Parameter(Mandatory = $true)]
[PSCustomObject] $Config,
[Parameter(Mandatory = $false)]
[bool] $IgnorePendingChanges = $false
)
try {
$assetRepo = ResolveAssetRepoLocation -Config $Config
$allowReset = $true
Push-Location $assetRepo
if(!$IgnorePendingChanges){
# detect pending changes
$pendingChanges = DetectPendingChanges -Config $Config
if($pendingChanges){
Write-Host "Visible Pending Changes:"
Write-Host $pendingChanges
$userInput = GetUserInput "This operation will need to undo pending changes prior to checking out a different SHA. To abandon pending changes, enter 'y'. An empty 'enter' or 'n' will result in no action."
if($userInput.Trim().ToLower() -ne 'y'){
$allowReset = $false
}
}
}
if($allowReset){
Write-Host "git checkout *"
git checkout *
Write-Host "git clean -xdf"
git clean -xdf
# need to figure out the sparse checkouts if we want to optimize this as much as possible
# for prototyping checking out the whole repo is fine
if($Config.SHA){
CheckoutRepoAtConfig -Config $Config -SparseCheckoutPath (ResolveCheckoutPaths -Config $Config)
}
}
}
finally {
Pop-Location
}
}
<#
.SYNOPSIS
This function's purpose is solely to update a assets.json (both config and on file) with a new recording SHA.
.DESCRIPTION
Retrieves the location of the target recording.json by looking at a property of the Config object. Updates the file at rest, returns the completed object.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
.PARAMETER NewSHA
A string representing the new SHA.
#>
Function UpdateAssetsJson {
param(
[Parameter(Mandatory=$true)]
[PSCustomObject] $Config,
[Parameter(Mandatory=$true)]
[string] $NewSHA
)
$jsonAtRest = Get-Content -Raw -Path $Config.AssetsJsonLocation | ConvertFrom-Json
# update the sha in our current live config
$Config.SHA = $NewSHA
# ensure it is propogated to disk
$jsonAtRest.SHA = $NewSHA
$jsonAtRest | ConvertTo-Json | Set-Content -Path $Config.AssetsJsonLocation
return $Config
}
<#
.SYNOPSIS
This function assumes a set of changes and attempts to use the provided config to automatically push a commit to the configured branch and repo combination.
.PARAMETER Config
A PSCustomObject that contains an auto-parsed assets.json object from ResolveAssetsJson.
#>
Function PushAssetsRepoUpdate {
param(
[Parameter(Mandatory=$true)]
[PSCustomObject] $Config
)
$newSha = $Config.SHA
$gitUser = git config --global user.name
$autoCommitMessage = "Automatic asset update from $gitUser."
$assetRepo = ResolveAssetRepoLocation -Config $Config
try {
Push-Location $assetRepo
$statusResult = git status --porcelain
if(!$statusResult){
Write-Host "No pending changes."
exit 0
}
$alreadyLatestSHA = $true
Write-Host "git rev-parse origin/$($Config.AssetsRepoBranch)"
$retrievedLatestSHA = git rev-parse origin/$($Config.AssetsRepoBranch)
Write-Host "Latest SHA is $retrievedLatestSHA."
# if the above command fails with code 128, the target auto commit branch does not exist, and we need to create it
if($LASTEXITCODE -eq 128){
Write-Host "Need to checkout new branch based on current changes."
$alreadyLatestSHA = $true
}
elseif ($LASTEXITCODE -ne 0) {
Write-Error "A non-code-128 error is not expected here. Check git command output above."
exit 1
}
# if the branch already exists, we need to check to see if we're actually on the latest commit
else {
if($retrievedLatestSHA -ne $Config.SHA){
$alreadyLatestSHA = $false
}
}
Write-Host "Based off latest commit: $alreadyLatestSHA"
# if we are based off the latest commit (or it's a nonexistent branch), all we gotta do is checkout a new branch of the correct name and push it.
if($alreadyLatestSHA) {
Write-Host "git checkout -b $($Config.AssetsRepoBranch)"
git checkout -b $($Config.AssetsRepoBranch)
Write-Host "git add -A ."
git add -A .
Write-Host "git commit -m `"$autoCommitMessage`""
git commit -m "$autoCommitMessage"
Write-Host "git push origin $($Config.AssetsRepoBranch)"
git push origin $($Config.AssetsRepoBranch)
}
else {
# TODO is there a noticable downside to stash versus saving our own patchfile like in git-branch-push?
Write-Host "git stash"
git stash
# TODO we want to only fetch the latest commit, instead of the entire branch
Write-Host "git fetch origin $($Config.AssetsRepoBranch)"
git fetch origin $($Config.AssetsRepoBranch)
Write-Host "git checkout $($Config.AssetsRepoBranch)"
git checkout $($Config.AssetsRepoBranch)
Write-Host "git stash pop"
git stash pop
Write-Host "git add -A ."
git add -A .
Write-Host "git commit -m `"$autoCommitMessage`""
git commit -m "$autoCommitMessage"
Write-Host "git push origin $($Config.AssetsRepoBranch)"
git push origin $($Config.AssetsRepoBranch)
}
$newSha = git rev-parse HEAD
UpdateAssetsJson -Config $Config -NewSHA $newSha
}
catch {
Write-Error $_
}
finally {
Pop-Location
}
return $newSha
}