Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
with:
go-version: "1.25.10"

Expand All @@ -30,12 +32,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
with:
go-version: "1.25.10"

- name: Run e2e tests
run: go test -tags e2e ./internal/server/...

wrapper-tests-windows:
name: Cloud Sync Wrapper Tests (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
with:
go-version: "1.25.10"
- run: go test ./tools/ -run TestCloudSyncWrappers -v
38 changes: 37 additions & 1 deletion DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1476,7 +1476,43 @@ For a step-by-step recovery guide covering `chunk_id does not match payload cont

---

---
## Scheduled Explicit Cloud Sync Wrappers

The wrappers under `tools/` are an **alternative** to native autosync for hosts where you cannot keep `engram serve` running. They run `engram sync --cloud --project <project>` once per explicitly named project. **Choose ONE mode** -- native autosync (recommended) when a daemon is feasible, OR these wrappers for the no-daemon case. Do **not** run both at once. Cloud `--all` is intentionally unsupported; projects are never inferred from cwd or an env var.

### Bash: `tools/cloud-sync-projects.sh`

```sh
./tools/cloud-sync-projects.sh my-project my-other-project
./tools/cloud-sync-projects.sh --log /var/log/engram-cloud-sync.log my-project
```

Exit `0` if all syncs and log succeeded; `1` if any project or logging op failed; `2` on usage error. Default durable log `$ENGRAM_DATA_DIR/cloud-sync-projects.log` (`~/.engram` fallback); override `--log` > `ENGRAM_CLOUD_SYNC_LOG` > default. Status lines go to both timestamped console and log; command stdout+stderr preserved on console and appended to log. Nothing retried or silenced.

### PowerShell: `tools/cloud-sync-projects.ps1`

```powershell
pwsh ./tools/cloud-sync-projects.ps1 my-project my-other-project
pwsh ./tools/cloud-sync-projects.ps1 -LogPath C:\logs\engram-cloud-sync.log my-project
```

Requires PowerShell 7 (`pwsh`); 5.1 is not supported. Same behavior, exit codes, and log defaults as Bash; override `-LogPath` > `ENGRAM_CLOUD_SYNC_LOG` > default.

### Inspecting the last failure

`project FAILURE project=<name> exit=<n>` records the exact exit code from `engram sync --cloud --project <name>`:

```sh
grep 'project FAILURE' "${ENGRAM_DATA_DIR:-$HOME/.engram}/cloud-sync-projects.log" | tail -n 5
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

```powershell
# PowerShell 7 ($env:ENGRAM_DATA_DIR or $HOME/.engram fallback)
$d = if ($env:ENGRAM_DATA_DIR) { $env:ENGRAM_DATA_DIR } else { Join-Path $HOME '.engram' }
Select-String 'project FAILURE' (Join-Path $d 'cloud-sync-projects.log') | Select-Object -Last 5
```

Pass the failing project to [Engram Cloud Troubleshooting](docs/engram-cloud/troubleshooting.md) -- the wrappers record and propagate, not interpret or retry.

## Cloud Sync Audit Log

Expand Down
82 changes: 82 additions & 0 deletions tools/cloud-sync-projects.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
[CmdletBinding()]
param(
[string]$LogPath,
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
[string[]]$Projects
)

$ErrorActionPreference = 'Stop'
$defaultLogName = 'cloud-sync-projects.log'

if ($PSVersionTable.PSVersion.Major -lt 7) {
[Console]::Error.WriteLine('cloud-sync-projects.ps1: error: PowerShell 7 (pwsh) is required. 5.1 is not supported.')
exit 2
}

function Write-Usage {
@'
Usage: cloud-sync-projects.ps1 [-LogPath <path>] <project> [<project> ...]
Run `engram sync --cloud --project <project>` once per explicitly named project.
Exit 0 if all succeed, 1 if any project/log op fails, 2 on usage error.
-LogPath <path> Overrides default and ENGRAM_CLOUD_SYNC_LOG.
-Help Show this help.
Requires PowerShell 7 (pwsh); 5.1 is not supported.
'@ | Out-Host
}

$helpRequested = $false
$cleanProjects = @()
foreach ($a in $Projects) { if ($a -in @('-Help', '--help', '-h')) { $helpRequested = $true } else { $cleanProjects += $a } }
$Projects = $cleanProjects
if ($helpRequested) { Write-Usage; exit 0 }
if ($Projects.Count -eq 0) {
[Console]::Error.WriteLine('cloud-sync-projects.ps1: error: at least one project is required'); exit 2
}

$resolvedLog = $LogPath
if ([string]::IsNullOrEmpty($resolvedLog)) { $resolvedLog = $env:ENGRAM_CLOUD_SYNC_LOG }
if ([string]::IsNullOrEmpty($resolvedLog)) {
$dataDir = if ($env:ENGRAM_DATA_DIR) { $env:ENGRAM_DATA_DIR } else { (Join-Path $HOME '.engram') }
$resolvedLog = Join-Path $dataDir $defaultLogName
}
$resolvedLog = [System.IO.Path]::GetFullPath($resolvedLog)
if (-not (Test-Path -LiteralPath ([System.IO.Path]::GetDirectoryName($resolvedLog)) -PathType Container)) {
[Console]::Error.WriteLine("cloud-sync-projects.ps1: error: log directory does not exist: $resolvedLog"); exit 2
}

function Write-LogLine {
param([string]$Message)
$line = "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:sszzz')] $Message"
try { Add-Content -LiteralPath $resolvedLog -Value $line -Encoding UTF8 -ErrorAction Stop }
catch { [Console]::Error.WriteLine("cloud-sync-projects.ps1: error: failed to append to log: $resolvedLog"); return $false }
Write-Host $line
return $true
}

function Invoke-Project {
param([string]$Project)
if (-not (Write-LogLine "project START project=$Project")) { return -1 }
$exitCode = 0
$prevPref = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
& engram sync --cloud --project $Project 2>&1 | Tee-Object -FilePath $resolvedLog -Append -ErrorAction Stop | ForEach-Object { Write-Host $_ }
$exitCode = $LASTEXITCODE
if ($null -eq $exitCode) { $exitCode = 0 }
} catch {
[Console]::Error.WriteLine("cloud-sync-projects.ps1: error: invoke/tee failed for '$Project': $($_.Exception.Message)")
return -1
} finally {
$ErrorActionPreference = $prevPref
}
if ($exitCode -eq 0) { if (-not (Write-LogLine "project SUCCESS project=$Project exit=0")) { return -1 } }
else { if (-not (Write-LogLine "project FAILURE project=$Project exit=$exitCode")) { return -1 } }
return $exitCode
}

$overall = 0
if (-not (Write-LogLine "wrapper START projects=$($Projects.Count) log=$resolvedLog")) { $overall = 1 }
foreach ($proj in $Projects) { if ((Invoke-Project -Project $proj) -ne 0) { $overall = 1 } }
if ($overall -eq 0) { if (-not (Write-LogLine 'wrapper END result=success')) { $overall = 1 } }
else { if (-not (Write-LogLine "wrapper END result=failure overall=$overall")) { $overall = 1 } }
exit $overall
75 changes: 75 additions & 0 deletions tools/cloud-sync-projects.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -uo pipefail

PROG_NAME="cloud-sync-projects.sh"
DEFAULT_LOG_NAME="cloud-sync-projects.log"

usage() {
cat <<'USAGE'
Usage: cloud-sync-projects.sh [--log <path>] <project> [<project> ...]
Run `engram sync --cloud --project <project>` once per explicitly named project.
Exit 0 if all succeed, 1 if any project/log op fails, 2 on usage error.
--log <path> Overrides default and ENGRAM_CLOUD_SYNC_LOG.
-h, --help Show this help.
Env: ENGRAM_DATA_DIR (defaults to ~/.engram); ENGRAM_CLOUD_SYNC_LOG (log override).
USAGE
}

die_usage() { printf '%s: error: %s\n' "$PROG_NAME" "$*" >&2; exit 2; }
log_path=""
projects=()
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--log) [ $# -ge 2 ] || die_usage "--log requires a path argument"; log_path="$2"; shift 2 ;;
--log=*) log_path="${1#--log=}"; [ -n "$log_path" ] || die_usage "--log requires a non-empty path"; shift ;;
--) shift; while [ $# -gt 0 ]; do projects+=("$1"); shift; done ;;
-*) die_usage "unknown option: $1" ;;
*) projects+=("$1"); shift ;;
esac
done

[ "${#projects[@]}" -gt 0 ] || die_usage "at least one project is required"

[ -z "$log_path" ] && log_path="${ENGRAM_CLOUD_SYNC_LOG:-}"
if [ -z "$log_path" ]; then
log_path="${ENGRAM_DATA_DIR:-$HOME/.engram}/$DEFAULT_LOG_NAME"
fi
case "$log_path" in /*) ;; *) log_path="$PWD/$log_path" ;; esac # absolute

log_dir="$(dirname "$log_path")"
[ -d "$log_dir" ] || { printf '%s: error: log directory does not exist: %s\n' "$PROG_NAME" "$log_dir" >&2; exit 2; }

logline() {
local ts; ts="$(date '+%Y-%m-%dT%H:%M:%S%z')" || return 1
printf '[%s] %s\n' "$ts" "$*" >>"$log_path" || return 1
printf '[%s] %s\n' "$ts" "$*"
}

run_project() {
local proj="$1" rc tee_rc
local -a statuses
logline "project START project=$proj" || return 1
engram sync --cloud --project "$proj" 2>&1 | tee -a "$log_path"
statuses=("${PIPESTATUS[@]}") # snapshot before any other command mutates it
rc=${statuses[0]:-1}; tee_rc=${statuses[1]:-1}
if [ "$rc" -eq 0 ]; then
logline "project SUCCESS project=$proj exit=0" || return 1
else
logline "project FAILURE project=$proj exit=$rc" || return 1
fi
[ "$tee_rc" -ne 0 ] && [ "$rc" -eq 0 ] && return 1 # tee/log failed
return "$rc"
}

overall=0
logline "wrapper START projects=${#projects[@]} log=$log_path" || overall=1
for proj in "${projects[@]}"; do
run_project "$proj" || overall=1
done
if [ "$overall" -eq 0 ]; then
logline "wrapper END result=success" || overall=1
else
logline "wrapper END result=failure overall=$overall" || overall=1
fi
exit "$overall"
Loading