DocEditorPlayground / scripts /sync-upstream.ps1
Eric Putnam
feat: deploy DocEditorPlayground as a static Hugging Face Space
8a8c5bb
Raw History Blame Contribute Delete
7.14 kB
<#
.SYNOPSIS
Mirror the upstream DocEditorPlayground checkout into ./app.
.DESCRIPTION
./app is a vendored copy of the upstream Next.js app, trimmed to just what
the Docker build needs. This script wipes and re-mirrors it, then records
the upstream commit in UPSTREAM.json so the Space can always be traced back
to a specific revision.
Anything you edit under ./app is destroyed by the next run. Fix upstream
instead, then re-sync.
.PARAMETER UpstreamPath
The local DocEditorPlayground checkout. Defaults to C:\dev\DocEditorPlayground.
.PARAMETER AllowDirty
Sync even when the upstream checkout has uncommitted changes. Without this,
a dirty upstream is an error, because the recorded commit would not describe
what actually got deployed.
.EXAMPLE
.\scripts\sync-upstream.ps1
#>
[CmdletBinding()]
param(
[string] $UpstreamPath = 'C:\dev\DocEditorPlayground',
[switch] $AllowDirty
)
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent $PSScriptRoot
$appDir = Join-Path $repoRoot 'app'
# --- Validate upstream -----------------------------------------------------
if (-not (Test-Path -LiteralPath $UpstreamPath -PathType Container)) {
throw "Upstream path not found: $UpstreamPath"
}
$upstreamPkgPath = Join-Path $UpstreamPath 'package.json'
if (-not (Test-Path -LiteralPath $upstreamPkgPath -PathType Leaf)) {
throw "No package.json at $UpstreamPath - is that really the DocEditorPlayground checkout?"
}
$upstreamPkg = Get-Content -LiteralPath $upstreamPkgPath -Raw | ConvertFrom-Json
if ($upstreamPkg.name -ne 'doceditorplayground') {
throw "Expected package name 'doceditorplayground' at $UpstreamPath but found '$($upstreamPkg.name)'."
}
# --- Read upstream git provenance -----------------------------------------
$commit = 'unknown'
$branch = 'unknown'
$isDirty = $false
$originUrl = 'unknown'
if (Test-Path -LiteralPath (Join-Path $UpstreamPath '.git')) {
Push-Location $UpstreamPath
try {
$commit = (& git rev-parse HEAD).Trim()
$branch = (& git rev-parse --abbrev-ref HEAD).Trim()
$status = & git status --porcelain
$isDirty = -not [string]::IsNullOrWhiteSpace(($status -join ''))
try { $originUrl = (& git remote get-url origin).Trim() } catch { $originUrl = 'none' }
}
finally { Pop-Location }
}
else {
Write-Warning "$UpstreamPath is not a git repo - provenance will be recorded as 'unknown'."
}
if ($isDirty -and -not $AllowDirty) {
throw @"
Upstream checkout has uncommitted changes.
Commit them in $UpstreamPath first so the deployed Space maps to a real revision,
or re-run with -AllowDirty to deploy the working tree as-is.
"@
}
# --- Mirror upstream -> ./app ---------------------------------------------
# Directories excluded as full paths are top-level only, so a same-named nested
# directory that the build needs is not silently dropped. Bare names (build
# output) are excluded at any depth on purpose.
$excludeDirs = @(
(Join-Path $UpstreamPath '.git'),
(Join-Path $UpstreamPath '.github'),
(Join-Path $UpstreamPath '.claude'),
(Join-Path $UpstreamPath '.dev'),
(Join-Path $UpstreamPath '.vscode'),
(Join-Path $UpstreamPath '.idea'),
(Join-Path $UpstreamPath 'tests'),
(Join-Path $UpstreamPath 'docs'),
(Join-Path $UpstreamPath 'scripts'),
'node_modules', '.next', 'out', 'dist',
'coverage', 'reports', 'test-results', 'playwright-report'
)
# Dev tooling and repo-hygiene files the container never reads. LICENSE,
# package-lock.json, and every config the build touches are deliberately kept.
$excludeFiles = @(
'CLAUDE.md', 'CHANGELOG.md', 'README.md',
'.claudeignore', '.backupignore',
'playwright.config.ts', 'vitest.config.ts',
'eslint.config.js', '.eslintrc.json', '.prettierrc',
'tsconfig.test.json',
'*.log', '*.tsbuildinfo', '.env', '.env.local'
)
Write-Host "Syncing $UpstreamPath -> $appDir" -ForegroundColor Cyan
New-Item -ItemType Directory -Force -Path $appDir | Out-Null
$robocopyArgs = @(
$UpstreamPath, $appDir,
'/MIR', # mirror: also deletes files removed upstream
'/NFL', '/NDL', # quiet: no per-file or per-directory listing
'/NJH', '/NP',
'/R:2', '/W:1'
) + @('/XD') + $excludeDirs + @('/XF') + $excludeFiles
& robocopy @robocopyArgs | Out-Null
# Robocopy exit codes 0-7 are success (8+ means copy failures).
if ($LASTEXITCODE -ge 8) {
throw "robocopy failed with exit code $LASTEXITCODE"
}
# --- Sanity-check what landed --------------------------------------------
$required = @(
'package.json', 'package-lock.json', '.npmrc', '.nvmrc',
'next.config.js', 'tsconfig.json',
'postcss.config.cjs', 'tailwind.config.js',
'pages', 'components', 'extensions', 'utils', 'types',
'styles', 'stubs', 'public'
)
$missing = $required | Where-Object { -not (Test-Path -LiteralPath (Join-Path $appDir $_)) }
if ($missing.Count -gt 0) {
throw "Sync incomplete - missing from app/: $($missing -join ', ')"
}
# --- Warn if the Dockerfile's Node pin drifted from .nvmrc ---------------
$nvmrc = (Get-Content -LiteralPath (Join-Path $appDir '.nvmrc') -Raw).Trim()
$dockerfilePath = Join-Path $repoRoot 'Dockerfile'
$dockerFrom = (Select-String -LiteralPath $dockerfilePath -Pattern '^FROM node:([^\s-]+)' ).Matches[0].Groups[1].Value
if ($dockerFrom -ne $nvmrc) {
Write-Warning "Node version drift: app/.nvmrc is $nvmrc but Dockerfile builds on node:$dockerFrom. Update the FROM line in Dockerfile."
}
# --- Record provenance ---------------------------------------------------
$upstream = [ordered]@{
upstreamRepo = $originUrl
upstreamPath = $UpstreamPath
commit = $commit
branch = $branch
dirtyWorkingTree = $isDirty
appVersion = $upstreamPkg.version
nodeVersion = $nvmrc
syncedAtUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
}
# Written without a BOM: Windows PowerShell 5.1's -Encoding utf8 emits one, and
# a BOM breaks strict JSON parsers that may read this file later.
$upstreamJson = ($upstream | ConvertTo-Json) + [Environment]::NewLine
[System.IO.File]::WriteAllText(
(Join-Path $repoRoot 'UPSTREAM.json'),
$upstreamJson,
(New-Object System.Text.UTF8Encoding $false)
)
# --- Summary -------------------------------------------------------------
$shortCommit = if ($commit.Length -ge 7) { $commit.Substring(0, 7) } else { $commit }
Write-Host ''
Write-Host "Synced doceditorplayground v$($upstreamPkg.version) @ $shortCommit ($branch)" -ForegroundColor Green
if ($isDirty) { Write-Warning 'Deployed from a dirty upstream working tree.' }
Write-Host ''
Push-Location $repoRoot
try {
$changes = & git status --porcelain
if ([string]::IsNullOrWhiteSpace(($changes -join ''))) {
Write-Host 'No changes - the Space already matches upstream.' -ForegroundColor Yellow
}
else {
Write-Host 'Pending changes:'
& git status --short
Write-Host ''
Write-Host 'Deploy with: .\scripts\deploy.ps1' -ForegroundColor Cyan
}
}
finally { Pop-Location }