foam_sync.ps1 (LIFEBALANCE) 2025-06-19T01:06:29Z
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# .gitignore
|
||||||
|
.logs/
|
||||||
231
foam_sync.ps1
231
foam_sync.ps1
@ -35,69 +35,86 @@
|
|||||||
$frequencyMinutes = 2 # How often the Scheduled Task should attempt to run this script
|
$frequencyMinutes = 2 # How often the Scheduled Task should attempt to run this script
|
||||||
$executionTimeLimitBufferSeconds = 30 # Buffer: task stops if it runs longer than (frequency - buffer)
|
$executionTimeLimitBufferSeconds = 30 # Buffer: task stops if it runs longer than (frequency - buffer)
|
||||||
|
|
||||||
# --- Script Setup ---
|
|
||||||
$scriptPath = $MyInvocation.MyCommand.Path
|
|
||||||
$scriptDir = Split-Path -Path $scriptPath -Parent
|
|
||||||
$scriptName = (Get-Item $scriptPath).Name
|
|
||||||
Write-Host "Script: $scriptName at $scriptPath"
|
|
||||||
Write-Host "Repository directory: $scriptDir"
|
|
||||||
Write-Host "Sync frequency: Every $frequencyMinutes minutes"
|
|
||||||
|
|
||||||
# --- Scheduled Task Setup ---
|
# --- Scheduled Task Setup ---
|
||||||
$taskName = "FoamGitSync"
|
$taskName = "FoamGitSync"
|
||||||
$taskDescription = "Periodically synchronizes the Git repository at $scriptDir using $scriptName."
|
|
||||||
|
|
||||||
# Run as the user who executes this script.
|
# --- Log File Setup ---
|
||||||
$taskPrincipal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive
|
# Log inside the repository, in a .logs subfolder. Ensure this is in .gitignore
|
||||||
# Define a very long duration (e.g., 40 years)
|
$scriptDirForLog = $PSScriptRoot # Use PSScriptRoot for robustness in determining script's dir
|
||||||
$practicallyIndefiniteDuration = New-TimeSpan -Days (365 * 40 + 10) # Approx 40 years, accounting for leap years
|
$logDir = Join-Path -Path $scriptDirForLog -ChildPath ".logs"
|
||||||
# Trigger configuration
|
if (-not (Test-Path -Path $logDir)) {
|
||||||
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $frequencyMinutes) -RepetitionDuration $practicallyIndefiniteDuration
|
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$logFilePath = Join-Path -Path $logDir -ChildPath "foam_sync.log"
|
||||||
|
|
||||||
# Action configuration:
|
Start-Transcript -Path $logFilePath -Append -IncludeInvocationHeader -Force
|
||||||
# We use an indirection technique to ensure the window is truly hidden.
|
|
||||||
# The scheduled task launches an initial PowerShell.
|
|
||||||
# This initial PowerShell then uses Start-Process to launch the *actual* script in a new, hidden PowerShell process.
|
|
||||||
|
|
||||||
# 1. Innermost PowerShell's -File argument: Path to the script, with internal double quotes escaped as ""
|
try {
|
||||||
# e.g., \"C:\path\to your script.ps1\"
|
# Wrap main script logic in try for Stop-Transcript in finally
|
||||||
$scriptPathForInnermostFileParam = $scriptPath.Replace('"', '""')
|
|
||||||
$innermostFileArg = "\`"$scriptPathForInnermostFileParam\`""
|
|
||||||
|
|
||||||
# 2. Argument string for the innermost PowerShell instance (the one executing the actual script)
|
# --- Script Setup ---
|
||||||
# e.g., -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"
|
$scriptPath = $MyInvocation.MyCommand.Path
|
||||||
$innermostPSArgs = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File $innermostFileArg"
|
$scriptDir = Split-Path -Path $scriptPath -Parent # This is the Git repository root
|
||||||
|
$scriptName = (Get-Item $scriptPath).Name
|
||||||
|
|
||||||
# 3. The innermostPSArgs string needs its single quotes escaped (as '') because it will be wrapped in single quotes for Start-Process's -ArgumentList
|
Write-Host "Script: $scriptName at $scriptPath"
|
||||||
$innermostPSArgsEscapedForStartProcess = $innermostPSArgs.Replace("'", "''")
|
Write-Host "Repository directory: $scriptDir"
|
||||||
|
Write-Host "Sync frequency: Every $frequencyMinutes minutes"
|
||||||
|
Write-Host "Log file: $logFilePath"
|
||||||
|
|
||||||
# 4. Command string that the first PowerShell instance (launched by Task Scheduler) will execute using Start-Process.
|
$taskDescription = "Periodically synchronizes the Git repository at $scriptDir using $scriptName."
|
||||||
# e.g., Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"' -WindowStyle Hidden
|
|
||||||
$commandToRunViaStartProcess = "Start-Process -FilePath powershell.exe -ArgumentList '$innermostPSArgsEscapedForStartProcess' -WindowStyle Hidden"
|
|
||||||
|
|
||||||
# 5. Argument string FOR THE TASK SCHEDULER to pass to the first powershell.exe.
|
# Run as the user who executes this script.
|
||||||
# The $commandToRunViaStartProcess is the value for -Command, and needs to be quoted for registration.
|
$taskPrincipal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive
|
||||||
# e.g., -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"' -WindowStyle Hidden"
|
# Define a very long duration (e.g., 40 years)
|
||||||
$actionArgumentForRegistration = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \`"$commandToRunViaStartProcess\`""
|
$practicallyIndefiniteDuration = New-TimeSpan -Days (365 * 40 + 10) # Approx 40 years, accounting for leap years
|
||||||
|
# Trigger configuration
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $frequencyMinutes) -RepetitionDuration $practicallyIndefiniteDuration
|
||||||
|
|
||||||
# 6. Expected argument string WHEN RETRIEVED by Get-ScheduledTask.
|
# Action configuration:
|
||||||
# Task Scheduler/PowerShell often strips the outermost quotes from the -Command value when retrieved.
|
# We use an indirection technique to ensure the window is truly hidden.
|
||||||
$expectedRetrievedActionArgument = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command $commandToRunViaStartProcess"
|
# The scheduled task launches an initial PowerShell.
|
||||||
|
# This initial PowerShell then uses Start-Process to launch the *actual* script in a new, hidden PowerShell process.
|
||||||
|
|
||||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArgumentForRegistration
|
# 1. Innermost PowerShell's -File argument: Path to the script, with internal double quotes escaped as ""
|
||||||
|
# e.g., \"C:\path\to your script.ps1\"
|
||||||
|
$scriptPathForInnermostFileParam = $scriptPath.Replace('"', '""')
|
||||||
|
$innermostFileArg = "\`"$scriptPathForInnermostFileParam\`""
|
||||||
|
|
||||||
# Task settings
|
# 2. Argument string for the innermost PowerShell instance (the one executing the actual script)
|
||||||
# Calculate a realistic execution time limit based on the frequency
|
# e.g., -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"
|
||||||
$executionTimeLimitTotalSeconds = ($frequencyMinutes * 60) - $executionTimeLimitBufferSeconds
|
$innermostPSArgs = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File $innermostFileArg"
|
||||||
if ($executionTimeLimitTotalSeconds -lt 30) {
|
|
||||||
|
# 3. The innermostPSArgs string needs its single quotes escaped (as '') because it will be wrapped in single quotes for Start-Process's -ArgumentList
|
||||||
|
$innermostPSArgsEscapedForStartProcess = $innermostPSArgs.Replace("'", "''")
|
||||||
|
|
||||||
|
# 4. Command string that the first PowerShell instance (launched by Task Scheduler) will execute using Start-Process.
|
||||||
|
# e.g., Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"' -WindowStyle Hidden
|
||||||
|
$commandToRunViaStartProcess = "Start-Process -FilePath powershell.exe -ArgumentList '$innermostPSArgsEscapedForStartProcess' -WindowStyle Hidden"
|
||||||
|
|
||||||
|
# 5. Argument string FOR THE TASK SCHEDULER to pass to the first powershell.exe.
|
||||||
|
# The $commandToRunViaStartProcess is the value for -Command, and needs to be quoted for registration.
|
||||||
|
# e.g., -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"C:\path\to your script.ps1\"' -WindowStyle Hidden"
|
||||||
|
$actionArgumentForRegistration = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \`"$commandToRunViaStartProcess\`""
|
||||||
|
|
||||||
|
# 6. Expected argument string WHEN RETRIEVED by Get-ScheduledTask.
|
||||||
|
# Task Scheduler/PowerShell often strips the outermost quotes from the -Command value when retrieved.
|
||||||
|
$expectedRetrievedActionArgument = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command $commandToRunViaStartProcess"
|
||||||
|
|
||||||
|
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArgumentForRegistration
|
||||||
|
|
||||||
|
# Task settings
|
||||||
|
# Calculate a realistic execution time limit based on the frequency
|
||||||
|
$executionTimeLimitTotalSeconds = ($frequencyMinutes * 60) - $executionTimeLimitBufferSeconds
|
||||||
|
if ($executionTimeLimitTotalSeconds -lt 30) {
|
||||||
# Ensure a minimum sensible execution time (e.g., 30 seconds)
|
# Ensure a minimum sensible execution time (e.g., 30 seconds)
|
||||||
$executionTimeLimitTotalSeconds = 30
|
$executionTimeLimitTotalSeconds = 30
|
||||||
}
|
}
|
||||||
$taskExecutionTimeLimit = New-TimeSpan -Seconds $executionTimeLimitTotalSeconds
|
$taskExecutionTimeLimit = New-TimeSpan -Seconds $executionTimeLimitTotalSeconds
|
||||||
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit $taskExecutionTimeLimit
|
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit $taskExecutionTimeLimit
|
||||||
|
|
||||||
# Check and configure the scheduled task
|
# Check and configure the scheduled task
|
||||||
try {
|
try {
|
||||||
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
if ($existingTask) {
|
if ($existingTask) {
|
||||||
@ -159,33 +176,33 @@ try {
|
|||||||
Write-Warning "You may need to run this script as Administrator."
|
Write-Warning "You may need to run this script as Administrator."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
# This outer catch is for unexpected errors, e.g., if Get-ScheduledTask had -ErrorAction Stop
|
# This outer catch is for unexpected errors, e.g., if Get-ScheduledTask had -ErrorAction Stop
|
||||||
Write-Warning "An unexpected error occurred during scheduled task setup for '$taskName'. Error: $($_.Exception.Message)"
|
Write-Warning "An unexpected error occurred during scheduled task setup for '$taskName'. Error: $($_.Exception.Message)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Git Operations ---
|
# --- Git Operations ---
|
||||||
Write-Host "Navigating to repository: $scriptDir"
|
Write-Host "Navigating to repository: $scriptDir"
|
||||||
try {
|
try {
|
||||||
Set-Location -Path $scriptDir -ErrorAction Stop
|
Set-Location -Path $scriptDir -ErrorAction Stop
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Error "Unable to find repository at $scriptDir. Exiting script."
|
Write-Error "Unable to find repository at $scriptDir. Exiting script."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Staging all changes with 'git add .'"
|
Write-Host "Staging all changes with 'git add .'"
|
||||||
git add .
|
git add .
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Warning "'git add .' command failed with exit code $LASTEXITCODE."
|
Write-Warning "'git add .' command failed with exit code $LASTEXITCODE."
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Checking for staged changes with 'git diff --staged --quiet'..."
|
Write-Host "Checking for staged changes with 'git diff --staged --quiet'..."
|
||||||
git diff --staged --quiet
|
git diff --staged --quiet
|
||||||
$changesStaged = ($LASTEXITCODE -ne 0)
|
$changesStaged = ($LASTEXITCODE -ne 0)
|
||||||
|
|
||||||
if ($changesStaged) {
|
if ($changesStaged) {
|
||||||
Write-Host "Staged changes detected. Creating commit..."
|
Write-Host "Staged changes detected. Creating commit..."
|
||||||
$commitMessage = "$scriptName ($($env:COMPUTERNAME)) $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
|
$commitMessage = "$scriptName ($($env:COMPUTERNAME)) $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
|
||||||
Write-Host "Commit message: $commitMessage"
|
Write-Host "Commit message: $commitMessage"
|
||||||
@ -196,70 +213,70 @@ if ($changesStaged) {
|
|||||||
else {
|
else {
|
||||||
Write-Host "Commit created successfully."
|
Write-Host "Commit created successfully."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Host "No relevant changes detected to commit."
|
Write-Host "No relevant changes detected to commit."
|
||||||
Write-Host "Resetting staging area with 'git reset HEAD --quiet'."
|
Write-Host "Resetting staging area with 'git reset HEAD --quiet'."
|
||||||
git reset HEAD --quiet
|
git reset HEAD --quiet
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Updating remote 'origin' with 'git remote update origin --prune'..."
|
Write-Host "Updating remote 'origin' with 'git remote update origin --prune'..."
|
||||||
git remote update origin --prune
|
git remote update origin --prune
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Error "Unable to update remote 'origin'. Exiting script."
|
Write-Error "Unable to update remote 'origin'. Exiting script."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
$localCommit = (git rev-parse HEAD 2>$null).Trim()
|
$localCommit = (git rev-parse HEAD 2>$null).Trim()
|
||||||
if ($LASTEXITCODE -ne 0 -or -not $localCommit) { Write-Error "Failed to get local HEAD commit. Exiting script."; exit 1 }
|
if ($LASTEXITCODE -ne 0 -or -not $localCommit) { Write-Error "Failed to get local HEAD commit. Exiting script."; exit 1 }
|
||||||
|
|
||||||
$remoteBranch = "origin/main"
|
$remoteBranch = "origin/main"
|
||||||
$remoteCommit = (git rev-parse $remoteBranch 2>$null).Trim()
|
$remoteCommit = (git rev-parse $remoteBranch 2>$null).Trim()
|
||||||
if ($LASTEXITCODE -ne 0 -or -not $remoteCommit) { Write-Error "Failed to get remote '$remoteBranch' commit. Exiting script."; exit 1 }
|
if ($LASTEXITCODE -ne 0 -or -not $remoteCommit) { Write-Error "Failed to get remote '$remoteBranch' commit. Exiting script."; exit 1 }
|
||||||
|
|
||||||
Write-Host "Local HEAD commit: $localCommit"
|
Write-Host "Local HEAD commit: $localCommit"
|
||||||
Write-Host "Remote '$remoteBranch' commit: $remoteCommit"
|
Write-Host "Remote '$remoteBranch' commit: $remoteCommit"
|
||||||
|
|
||||||
if ($localCommit -eq $remoteCommit) {
|
if ($localCommit -eq $remoteCommit) {
|
||||||
Write-Host "Local and remote are already in sync."
|
Write-Host "Local and remote are already in sync."
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
$sleepyTime = Get-Random -Minimum 1 -Maximum 15
|
$sleepyTime = Get-Random -Minimum 1 -Maximum 15
|
||||||
Write-Host "Local and remote differ. Sleeping for $sleepyTime seconds..."
|
Write-Host "Local and remote differ. Sleeping for $sleepyTime seconds..."
|
||||||
Start-Sleep -Seconds $sleepyTime
|
Start-Sleep -Seconds $sleepyTime
|
||||||
|
|
||||||
$localCommitAfterSleep = (git rev-parse HEAD 2>$null).Trim()
|
$localCommitAfterSleep = (git rev-parse HEAD 2>$null).Trim()
|
||||||
if ($LASTEXITCODE -ne 0 -or -not $localCommitAfterSleep) { Write-Error "Failed to re-get local HEAD commit. Exiting script."; exit 1 }
|
if ($LASTEXITCODE -ne 0 -or -not $localCommitAfterSleep) { Write-Error "Failed to re-get local HEAD commit. Exiting script."; exit 1 }
|
||||||
$remoteCommitAfterSleep = (git rev-parse $remoteBranch 2>$null).Trim()
|
$remoteCommitAfterSleep = (git rev-parse $remoteBranch 2>$null).Trim()
|
||||||
if ($LASTEXITCODE -ne 0 -or -not $remoteCommitAfterSleep) { Write-Error "Failed to re-get remote '$remoteBranch' commit. Exiting script."; exit 1 }
|
if ($LASTEXITCODE -ne 0 -or -not $remoteCommitAfterSleep) { Write-Error "Failed to re-get remote '$remoteBranch' commit. Exiting script."; exit 1 }
|
||||||
|
|
||||||
if ($localCommitAfterSleep -eq $remoteCommitAfterSleep) {
|
if ($localCommitAfterSleep -eq $remoteCommitAfterSleep) {
|
||||||
Write-Host "Local and remote became synchronized during sleep."
|
Write-Host "Local and remote became synchronized during sleep."
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
$localCommit = $localCommitAfterSleep
|
$localCommit = $localCommitAfterSleep
|
||||||
$remoteCommit = $remoteCommitAfterSleep
|
$remoteCommit = $remoteCommitAfterSleep
|
||||||
|
|
||||||
Write-Host "Proceeding with sync logic..."
|
Write-Host "Proceeding with sync logic..."
|
||||||
|
|
||||||
git merge-base --is-ancestor $localCommit $remoteCommit
|
git merge-base --is-ancestor $localCommit $remoteCommit
|
||||||
$localIsAncestorOfRemote = ($LASTEXITCODE -eq 0)
|
$localIsAncestorOfRemote = ($LASTEXITCODE -eq 0)
|
||||||
|
|
||||||
git merge-base --is-ancestor $remoteCommit $localCommit
|
git merge-base --is-ancestor $remoteCommit $localCommit
|
||||||
$remoteIsAncestorOfLocal = ($LASTEXITCODE -eq 0)
|
$remoteIsAncestorOfLocal = ($LASTEXITCODE -eq 0)
|
||||||
|
|
||||||
if ($localIsAncestorOfRemote) {
|
if ($localIsAncestorOfRemote) {
|
||||||
Write-Host "Remote '$remoteBranch' is ahead. Pulling with rebase..."
|
Write-Host "Remote '$remoteBranch' is ahead. Pulling with rebase..."
|
||||||
git pull --rebase origin main
|
git pull --rebase origin main
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Error "'git pull --rebase' failed. Manual intervention may be required."; exit 1 }
|
if ($LASTEXITCODE -ne 0) { Write-Error "'git pull --rebase' failed. Manual intervention may be required."; exit 1 }
|
||||||
}
|
}
|
||||||
elseif ($remoteIsAncestorOfLocal) {
|
elseif ($remoteIsAncestorOfLocal) {
|
||||||
Write-Host "Local HEAD is ahead. Pushing..."
|
Write-Host "Local HEAD is ahead. Pushing..."
|
||||||
git push origin main
|
git push origin main
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Error "'git push' failed. Manual intervention may be required."; exit 1 }
|
if ($LASTEXITCODE -ne 0) { Write-Error "'git push' failed. Manual intervention may be required."; exit 1 }
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Host "Local HEAD and remote '$remoteBranch' have diverged."
|
Write-Host "Local HEAD and remote '$remoteBranch' have diverged."
|
||||||
$remoteTimestampStr = (git log --pretty=format:"%at" -n 1 $remoteBranch 2>$null).Trim()
|
$remoteTimestampStr = (git log --pretty=format:"%at" -n 1 $remoteBranch 2>$null).Trim()
|
||||||
if ($LASTEXITCODE -ne 0 -or -not $remoteTimestampStr) { Write-Error "Failed to get remote commit timestamp for '$remoteBranch'. Exiting script."; exit 1 }
|
if ($LASTEXITCODE -ne 0 -or -not $remoteTimestampStr) { Write-Error "Failed to get remote commit timestamp for '$remoteBranch'. Exiting script."; exit 1 }
|
||||||
@ -287,7 +304,11 @@ else {
|
|||||||
Write-Host "Pushing changes after rebase..."
|
Write-Host "Pushing changes after rebase..."
|
||||||
git push origin main
|
git push origin main
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Error "'git push' after rebase failed. Manual intervention may be required."; exit 1 }
|
if ($LASTEXITCODE -ne 0) { Write-Error "'git push' after rebase failed. Manual intervention may be required."; exit 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Synchronization process completed successfully."
|
Write-Host "Synchronization process completed successfully."
|
||||||
exit 0
|
exit 0
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Stop-Transcript
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user