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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ keeps reading that version for at least 24 months after a successor lands.

## [Unreleased]

### Installer: fix and harden the curl|sh installer

The `scripts/install.sh` one-liner now works against real releases: it
builds the versioned goreleaser archive name, resolves `latest` via the
GitHub release redirect, and parses `--version`/`--bindir`/`--no-verify`
flags correctly (previously `latest` and the unversioned archive name
both 404'd, and `--version` was mis-read). The script is strict POSIX
`sh` so the canonical `curl | sh` works under dash/busybox without a
bash re-exec. Downloads are verified by SHA-256 against `checksums.txt`,
and by cosign signature when cosign is installed. Added a Cloudflare
Worker (`deploy/cloudflare/`) to serve the script at get.pghardstorage.org.

### Docs: brand the documentation site

The documentation site now matches the pghardstorage.org brand: the
Expand Down
62 changes: 62 additions & 0 deletions deploy/cloudflare/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Cloudflare Worker — `get.pghardstorage.org`

Serves the one-line installer so this works:

```sh
curl -sSL https://get.pghardstorage.org | sh
```

The Worker (`get-installer-worker.js`) fetches `scripts/install.sh` from
the repo's `main` branch and returns it as `text/plain`, cached at the
edge. Editing `scripts/install.sh` on `main` updates the served
installer within the cache TTL — no Worker redeploy needed.

`wrangler.toml` here carries the name (`pghardstorage-get`), entrypoint,
and the `get.pghardstorage.org` custom-domain route, so a single deploy
wires up serving end-to-end.

## Setup — GitHub integration (recommended)

Cloudflare's Git integration ("Workers Builds") redeploys the Worker on
every push to `main`. One-time wiring in the Cloudflare dashboard:

1. **Workers & Pages → Create → Workers tab → Connect to Git**
(a.k.a. "Import a repository").
2. Authorise GitHub and pick `cybertec-postgresql/pg_hardstorage`.
3. **Build settings:**
- **Root directory:** `deploy/cloudflare` ← where this `wrangler.toml` lives
- **Build command:** *(leave empty — plain JS, nothing to build)*
- **Deploy command:** `npx wrangler deploy` *(default)*
- **Production branch:** `main`
4. **Save and Deploy.** The first deploy reads `wrangler.toml`, publishes
the Worker, and binds `get.pghardstorage.org` automatically
(`custom_domain = true` creates the DNS record + TLS cert).

Prerequisite: the `pghardstorage.org` zone is on this Cloudflare account.

## Setup — manual (fallback)

If you'd rather not connect Git, deploy from this directory with an
authenticated `wrangler` (`npm i -g wrangler && wrangler login`):

```sh
cd deploy/cloudflare
wrangler deploy # reads wrangler.toml: name, entrypoint, route
```

## Verify (either path)

```sh
curl -sSL https://get.pghardstorage.org | head -20 # should print the script
curl -sSL https://get.pghardstorage.org | sh # should install
```

## Notes

- **Pin to a release instead of `main`:** edit `INSTALL_SCRIPT_URL` in
the Worker to `.../<tag>/scripts/install.sh` if you want the served
installer frozen to a tagged release.
- **Cache TTL:** `CACHE_TTL_SECONDS` (default 300s) controls how fast an
`install.sh` change propagates.
- This Worker only ever emits the installer script; it rejects non-GET
methods and sends `X-Content-Type-Options: nosniff`.
71 changes: 71 additions & 0 deletions deploy/cloudflare/get-installer-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Cloudflare Worker for https://get.pghardstorage.org
*
* Serves the canonical install script as text/plain so that
*
* curl -sSL https://get.pghardstorage.org | sh
*
* pipes the real installer into the shell. The script body is fetched
* from the repo's main branch (raw.githubusercontent.com) and cached at
* the edge, so updating scripts/install.sh on main updates the served
* installer without redeploying this Worker.
*
* Why a Worker rather than a plain redirect:
* - We can pin the Content-Type to text/plain (some clients choke on
* GitHub raw's charset quirks; `curl | sh` doesn't care, but a
* human opening the URL in a browser gets readable text).
* - We control caching + can swap the upstream (e.g. pin to a tag)
* in one place.
* - No dependence on a 30x redirect surviving every client's flags.
*
* Deploy: see deploy/cloudflare/README.md.
*/

// Source of truth for the installer. Pin to a tag instead of `main`
// (e.g. .../v1.0.0/scripts/install.sh) if you want the served installer
// frozen to a release rather than tracking main.
const INSTALL_SCRIPT_URL =
"https://raw.githubusercontent.com/cybertec-postgresql/pg_hardstorage/main/scripts/install.sh";

// Edge cache lifetime for the fetched script (seconds). Short enough
// that a fix to install.sh propagates quickly, long enough to absorb
// install spikes without hammering the origin.
const CACHE_TTL_SECONDS = 300;

export default {
async fetch(request) {
// Only GET/HEAD make sense for a script endpoint.
if (request.method !== "GET" && request.method !== "HEAD") {
return new Response("Method Not Allowed\n", {
status: 405,
headers: { Allow: "GET, HEAD", "Content-Type": "text/plain" },
});
}

const upstream = await fetch(INSTALL_SCRIPT_URL, {
cf: { cacheTtl: CACHE_TTL_SECONDS, cacheEverything: true },
});

if (!upstream.ok) {
return new Response(
"Installer temporarily unavailable. " +
"See https://github.com/cybertec-postgresql/pg_hardstorage/releases\n",
{ status: 502, headers: { "Content-Type": "text/plain" } }
);
}

const body = await upstream.text();

return new Response(body, {
status: 200,
headers: {
// text/plain so `curl | sh` and a browser both behave.
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": `public, max-age=${CACHE_TTL_SECONDS}`,
// Defensive headers — this endpoint only ever emits a script.
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
},
});
},
};
23 changes: 23 additions & 0 deletions deploy/cloudflare/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Wrangler config for the get.pghardstorage.org installer Worker.
#
# Consumed both by `wrangler deploy` and by Cloudflare's Git
# integration (Workers Builds): point the build at this directory
# (deploy/cloudflare) as the root and Cloudflare reads this file.
#
# The `routes` block binds the Worker to the custom hostname on the
# pghardstorage.org zone, so a deploy also wires up serving at
# https://get.pghardstorage.org (custom_domain = Cloudflare manages the
# DNS record + TLS cert automatically).

name = "pghardstorage-get"
main = "get-installer-worker.js"

# Pin the Workers runtime semantics to a known date. Bump deliberately
# after testing if you need newer runtime behaviour.
compatibility_date = "2025-06-22"

# Serve the Worker at the custom hostname. Requires the
# pghardstorage.org zone to be on this Cloudflare account (it is).
[[routes]]
pattern = "get.pghardstorage.org"
custom_domain = true
10 changes: 8 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ comment block declares otherwise.

## Key files / subdirs

- `install.sh` — the curlable installer published at the official download
URL; resolves the latest release and drops the binary into `$PREFIX/bin`
- `install.sh` — the curlable installer served at https://get.pghardstorage.org;
resolves the latest release (or `--version <tag>`), downloads the matching
goreleaser tarball, verifies its SHA-256 against `checksums.txt` (and the
cosign signature when cosign is installed), then drops the binary into
`$PREFIX/bin`. The serving endpoint is the Cloudflare Worker under
`../deploy/cloudflare/`.
- `demo-quickstart.sh` — five-minute end-to-end demo (local repo, backup,
restore) used by `../docs/tutorials/getting-started.md`
- `devcluster.sh` — local Patroni + pg_hardstorage dev cluster for maintainers
Expand All @@ -26,6 +30,8 @@ comment block declares otherwise.

- `../docs/tutorials/getting-started.md` — narrative wrapping
`demo-quickstart.sh`
- `../deploy/cloudflare/` — the Worker that serves `install.sh` at
`get.pghardstorage.org`
- `../Makefile` — build / test entry points (not here)
- `../docs/how-to/packaging/` — release engineering procedures

Expand Down
Loading
Loading