Skip to content
Merged
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
165 changes: 165 additions & 0 deletions .github/workflows/installer-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: "Installer test"

# Exercises bin/installer.sh and bin/installer.ps1 end-to-end so the
# one-line install paths in the README stay working.

on:
push:
paths:
- "bin/installer.sh"
- "bin/installer.ps1"
- ".github/workflows/installer-test.yml"
pull_request:
paths:
- "bin/installer.sh"
- "bin/installer.ps1"
- ".github/workflows/installer-test.yml"

permissions:
contents: read

jobs:
shellcheck:
name: shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Run shellcheck on installer.sh
run: |
sudo apt-get update
sudo apt-get install -y shellcheck
shellcheck bin/installer.sh

installer-posix:
name: end-to-end install (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: 22

- name: Pre-install pnpm (avoid sudo prompt in the installer)
run: npm install -g pnpm

- name: Run bin/installer.sh against this commit
env:
ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test
ETHERPAD_REPO: ${{ github.server_url }}/${{ github.repository }}.git
ETHERPAD_BRANCH: ${{ github.head_ref || github.ref_name }}
NO_COLOR: "1"
run: sh ./bin/installer.sh

- name: Verify clone + dependencies + build artifacts
shell: bash
env:
ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test
run: |
set -eux
test -d "$ETHERPAD_DIR/.git"
test -f "$ETHERPAD_DIR/package.json"
test -d "$ETHERPAD_DIR/node_modules"
test -d "$ETHERPAD_DIR/src/node_modules"
# build:etherpad copies the admin SPA into src/templates/admin
test -f "$ETHERPAD_DIR/src/templates/admin/index.html"

- name: Smoke test - start Etherpad and curl /api
shell: bash
env:
ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test
run: |
set -eu
cd "$ETHERPAD_DIR"
pnpm run prod >/tmp/etherpad.log 2>&1 &
PID=$!
# Wait up to 60s for the API to come up.
ok=0
for i in $(seq 1 60); do
if curl -fsS http://localhost:9001/api >/dev/null 2>&1; then
echo "Etherpad answered on /api after ${i}s"
ok=1
break
fi
sleep 1
done
if [ "$ok" != "1" ]; then
echo "Etherpad did not start within 60s. Last 200 lines of log:" >&2
tail -200 /tmp/etherpad.log >&2 || true
kill "$PID" 2>/dev/null || true
exit 1
fi
kill "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true

installer-windows:
name: end-to-end install (windows-latest)
runs-on: windows-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: 22

- name: Pre-install pnpm
run: npm install -g pnpm

- name: Run bin/installer.ps1 against this commit
shell: pwsh
env:
ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test
ETHERPAD_REPO: ${{ github.server_url }}/${{ github.repository }}.git
ETHERPAD_BRANCH: ${{ github.head_ref || github.ref_name }}
NO_COLOR: "1"
run: ./bin/installer.ps1

- name: Verify clone + dependencies + build artifacts
shell: pwsh
env:
ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test
run: |
if (-not (Test-Path "$env:ETHERPAD_DIR\.git")) { throw '.git missing' }
if (-not (Test-Path "$env:ETHERPAD_DIR\package.json")) { throw 'package.json missing' }
if (-not (Test-Path "$env:ETHERPAD_DIR\node_modules")) { throw 'node_modules missing' }
if (-not (Test-Path "$env:ETHERPAD_DIR\src\node_modules")) { throw 'src/node_modules missing' }
if (-not (Test-Path "$env:ETHERPAD_DIR\src\templates\admin\index.html")) { throw 'admin SPA missing' }

- name: Smoke test - start Etherpad and curl /api
shell: pwsh
env:
ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test
run: |
Push-Location $env:ETHERPAD_DIR
$logPath = Join-Path $env:RUNNER_TEMP 'etherpad.log'
# pnpm on Windows is a .cmd shim, which Start-Process can't run
# directly — wrap it in cmd.exe.
$proc = Start-Process -FilePath cmd.exe `
-ArgumentList '/c','pnpm','run','prod' `
-RedirectStandardOutput $logPath `
-RedirectStandardError "$logPath.err" `
-PassThru -NoNewWindow
$ok = $false
for ($i = 1; $i -le 90; $i++) {
try {
Invoke-WebRequest -UseBasicParsing -Uri http://localhost:9001/api -TimeoutSec 2 | Out-Null
Write-Host "Etherpad answered on /api after ${i}s"
$ok = $true
break
} catch { Start-Sleep -Seconds 1 }
}
if (-not $ok) {
Write-Host 'Etherpad did not start within 90s. Last 200 lines of log (stdout):'
if (Test-Path $logPath) { Get-Content $logPath -Tail 200 }
Write-Host 'Last 200 lines of stderr:'
if (Test-Path "$logPath.err") { Get-Content "$logPath.err" -Tail 200 }
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
Pop-Location
exit 1
}
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
Pop-Location
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,43 @@ We're looking for maintainers and have some funding available. Please contact J

## Installation

### Quick install (one-liner)

The fastest way to get Etherpad running. Requires `git` and Node.js >= 20.

**macOS / Linux / WSL:**

```sh
curl -fsSL https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.sh | sh
```

**Windows (PowerShell):**

```powershell
irm https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.ps1 | iex
```

Both installers clone Etherpad into `./etherpad-lite`, install dependencies, and
build the frontend. When the installer finishes, run:

```sh
cd etherpad-lite && pnpm run prod
```

Then open <http://localhost:9001>.

To install and start in one go:

```sh
# macOS / Linux / WSL
ETHERPAD_RUN=1 sh -c "$(curl -fsSL https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.sh)"
```

```powershell
# Windows
$env:ETHERPAD_RUN=1; irm https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.ps1 | iex
```

### Docker-Compose

```yaml
Expand Down Expand Up @@ -100,7 +137,7 @@ volumes:

### Requirements

[Node.js](https://nodejs.org/).
[Node.js](https://nodejs.org/) >= 20.

### Windows, macOS, Linux

Expand Down
155 changes: 155 additions & 0 deletions bin/installer.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Etherpad one-line installer for Windows (PowerShell).
#
# Usage:
# irm https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.ps1 | iex
#
# Optional environment variables:
# $env:ETHERPAD_DIR Directory to install into (default: .\etherpad-lite)
# $env:ETHERPAD_BRANCH Branch / tag to clone (default: master)
# $env:ETHERPAD_REPO Repo URL (default: https://github.com/ether/etherpad-lite.git)
# $env:ETHERPAD_RUN If "1", start Etherpad after install
# $env:NO_COLOR If set, disables coloured output

#Requires -Version 5.1

$ErrorActionPreference = 'Stop'

# ---------- pretty output ----------
$useColor = -not $env:NO_COLOR
function Write-Step([string]$msg) {
if ($useColor) { Write-Host "==> $msg" -ForegroundColor Green }
else { Write-Host "==> $msg" }
}
function Write-Warn([string]$msg) {
if ($useColor) { Write-Host "==> $msg" -ForegroundColor Yellow }
else { Write-Host "==> $msg" }
}
function Write-Fatal([string]$msg) {
if ($useColor) { Write-Host "==> $msg" -ForegroundColor Red }
else { Write-Host "==> $msg" }
exit 1
}

function Test-Cmd([string]$name) {
return [bool](Get-Command $name -ErrorAction SilentlyContinue)
}

# ---------- defaults ----------
$EtherpadDir = if ($env:ETHERPAD_DIR) { $env:ETHERPAD_DIR } else { 'etherpad-lite' }
$EtherpadBranch = if ($env:ETHERPAD_BRANCH) { $env:ETHERPAD_BRANCH } else { 'master' }
$EtherpadRepo = if ($env:ETHERPAD_REPO) { $env:ETHERPAD_REPO } else { 'https://github.com/ether/etherpad-lite.git' }
$RequiredNodeMajor = 20

Write-Step 'Etherpad installer'

# ---------- prerequisite checks ----------
if (-not (Test-Cmd git)) {
Write-Fatal 'git is required but not installed. See https://git-scm.com/download/win'
}
if (-not (Test-Cmd node)) {
Write-Fatal "Node.js is required (>= $RequiredNodeMajor). Install it from https://nodejs.org"
}

$nodeMajor = [int](node -p 'process.versions.node.split(".")[0]')
if ($nodeMajor -lt $RequiredNodeMajor) {
$nodeVer = (node --version)
Write-Fatal "Node.js >= $RequiredNodeMajor required. You have $nodeVer."
}

if (-not (Test-Cmd pnpm)) {
Write-Step 'Installing pnpm globally'
if (-not (Test-Cmd npm)) {
Write-Fatal "npm not found. Install Node.js >= $RequiredNodeMajor."
}
npm install -g pnpm
if ($LASTEXITCODE -ne 0) {
Write-Fatal 'Failed to install pnpm. Install it manually: https://pnpm.io/installation'
}
if (-not (Test-Cmd pnpm)) {
Write-Fatal 'pnpm install reported success but pnpm is still not on PATH. Open a new shell and re-run.'
}
}

# ---------- clone ----------
if (Test-Path $EtherpadDir) {
if (Test-Path (Join-Path $EtherpadDir '.git')) {
Write-Warn "$EtherpadDir already exists; updating to $EtherpadBranch."
Push-Location $EtherpadDir
try {
# Verify the existing checkout points at the expected remote.
$existingRemote = (git remote get-url origin 2>$null)
if ($existingRemote -and $existingRemote -ne $EtherpadRepo) {
Write-Fatal "$EtherpadDir is checked out from '$existingRemote', expected '$EtherpadRepo'. Refusing to overwrite."
}

# Refuse to clobber meaningful local changes. pnpm-lock.yaml is
# excluded because `pnpm i` rewrites it during installation,
# which would otherwise make every re-run of the installer fail.
$statusLines = (git status --porcelain) -split "`n" |
Where-Object { $_ -and ($_ -notmatch '\bpnpm-lock\.yaml$') }
if ($statusLines) {
$statusLines | ForEach-Object { Write-Host $_ }
Write-Fatal "$EtherpadDir has uncommitted changes. Commit/stash them or remove the directory."
}

git fetch --tags --prune origin
if ($LASTEXITCODE -ne 0) { Write-Fatal "git fetch failed in $EtherpadDir" }

# Discard any pnpm-lock.yaml changes from a prior pnpm install
# so the subsequent checkout doesn't refuse to overwrite.
git checkout -- pnpm-lock.yaml 2>$null

# Switch to the requested branch / tag and fast-forward to it.
git show-ref --verify --quiet "refs/remotes/origin/$EtherpadBranch"
$isBranch = ($LASTEXITCODE -eq 0)
git show-ref --verify --quiet "refs/tags/$EtherpadBranch"
$isTag = ($LASTEXITCODE -eq 0)

if ($isBranch) {
git checkout -B $EtherpadBranch "origin/$EtherpadBranch"
if ($LASTEXITCODE -ne 0) { Write-Fatal "git checkout $EtherpadBranch failed" }
} elseif ($isTag) {
git checkout --detach "refs/tags/$EtherpadBranch"
if ($LASTEXITCODE -ne 0) { Write-Fatal "git checkout tag $EtherpadBranch failed" }
} else {
Write-Fatal "Branch or tag '$EtherpadBranch' not found on origin."
}

$installedRev = (git rev-parse --short HEAD)
Write-Step "Updated $EtherpadDir to $EtherpadBranch @ $installedRev"
} finally {
Pop-Location
}
} else {
Write-Fatal "$EtherpadDir exists and is not a git checkout. Aborting."
}
} else {
Write-Step "Cloning Etherpad ($EtherpadBranch) into $EtherpadDir"
git clone --depth 1 --branch $EtherpadBranch $EtherpadRepo $EtherpadDir
if ($LASTEXITCODE -ne 0) { Write-Fatal 'git clone failed.' }
}

Push-Location $EtherpadDir

# ---------- install + build ----------
Write-Step 'Installing dependencies (pnpm i)'
pnpm i
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Fatal 'pnpm i failed.' }

Write-Step 'Building Etherpad (pnpm run build:etherpad)'
pnpm run build:etherpad
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Fatal 'pnpm run build:etherpad failed.' }

# ---------- done ----------
Write-Host ''
if ($useColor) { Write-Host "🎉 Etherpad is installed in $EtherpadDir" -ForegroundColor Green }
else { Write-Host "Etherpad is installed in $EtherpadDir" }
Write-Host 'To start Etherpad:'
Write-Host " cd $EtherpadDir; pnpm run prod"
Write-Host 'Then open http://localhost:9001 in your browser.'
Write-Host ''

if ($env:ETHERPAD_RUN -eq '1') {
Write-Step 'Starting Etherpad on http://localhost:9001'
pnpm run prod
}
Loading
Loading