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
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,14 @@ Never invent examples. When fixing a source-link 404:
### Deployment pipeline (no auto-deploy)
`ci.yml` only builds and pushes a Docker image to `ghcr.io/encryption4all/postguard-docs:edge` on every push to `main`. There is no deploy step. Production (docs.postguard.eu) runs an nginx container serving `docs/.vitepress/dist`; whatever host runs it must pull the new `edge` image and restart the container, or it serves the stale build. To detect a stale deployment, check the `last-modified` header on `index.html` via `curl -I https://docs.postguard.eu/` against a known commit date on `main`.

### `{{ }}` in prose breaks the build, even inside backticks
VitePress compiles every page as a Vue SFC, so `{{ ... }}` is a template interpolation wherever it appears — **including inside an inline code span**. Writing `` `${{ github.repository }}` `` while documenting a GitHub Actions expression fails `npm run docs:build` with `Cannot read properties of undefined (reading 'repository')` and a Vue server-renderer stack trace naming `.vitepress/.temp/<page>.md.js`, which points at the compiled temp file rather than the line you wrote. Name the context instead (`the workflow's github.repository context`), or wrap the span in `<span v-pre>`. A fenced code block is safe; an inline span is not. Always run `npm run docs:build` before pushing — this class of error does not show up in review.

### postguard-examples drift (known gotcha)
A past "consolidation" commit in postguard-examples flattened `pg-sveltekit/src/routes/download/` and `routes/send/` into a single top-level `+page.svelte`. Docs source-links pinned before that commit which point into those folders will 404. The pre-consolidation API form (`pg.decrypt({uuid, element, recipient})`) also differs from the post-consolidation one (`pg.open({uuid}).decrypt(...)`), so snippets cannot simply be repinned to a later hash without also updating the code shown.

### Canonical PKG / Cryptify hosts
Source of truth: `postguard-js/scripts/smoke.mjs`, `postguard-examples/pg-{node,dotnet,sveltekit}` configs.
Source of truth: `postguard-js/scripts/smoke.mjs`, `postguard-js/examples/pg-{node,dotnet,sveltekit}` configs (that second path was `postguard-examples/pg-*` before the repo was folded in and archived).

| Env | PKG | Cryptify |
|---|---|---|
Expand Down
4 changes: 3 additions & 1 deletion docs/repos/cryptify.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

[GitHub](https://github.com/encryption4all/postguard/tree/main/cryptify) · Rust · File Sharing Service

The source lives at `cryptify/` in the [postguard](https://github.com/encryption4all/postguard) Cargo workspace, compiled against the in-tree `pg-core`. The [cryptify](https://github.com/encryption4all/cryptify) repository still builds and publishes the `ghcr.io/encryption4all/cryptify` image; moving that is a separate ops change.
The source lives at `cryptify/` in the [postguard](https://github.com/encryption4all/postguard) Cargo workspace, compiled against the in-tree `pg-core`. That workspace's `delivery.yml` is now the **sole** publisher of the `ghcr.io/encryption4all/cryptify` image, unconditionally since [postguard#293](https://github.com/encryption4all/postguard/pull/293); the standalone `encryption4all/cryptify` repository had its build and push jobs retired and is archived. The GHCR package name did not change, so nothing downstream had to be repointed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postguard#293 is linked as /pull/293, but 293 is an issue, not a PR: "task: cut cryptify's Docker publish pipeline over to postguard, then retire the old repo's build" (closed). I checked via the API — has("pull_request") is false.

GitHub redirects /pull/N to /issues/N, so nothing 404s and check:links will not catch it either, since it only walks the 45 pinned source links. The same number is cited correctly as /issues/312 two lines down.

Suggested change
The source lives at `cryptify/` in the [postguard](https://github.com/encryption4all/postguard) Cargo workspace, compiled against the in-tree `pg-core`. That workspace's `delivery.yml` is now the **sole** publisher of the `ghcr.io/encryption4all/cryptify` image, unconditionally since [postguard#293](https://github.com/encryption4all/postguard/pull/293); the standalone `encryption4all/cryptify` repository had its build and push jobs retired and is archived. The GHCR package name did not change, so nothing downstream had to be repointed.
The source lives at `cryptify/` in the [postguard](https://github.com/encryption4all/postguard) Cargo workspace, compiled against the in-tree `pg-core`. That workspace's `delivery.yml` is now the **sole** publisher of the `ghcr.io/encryption4all/cryptify` image, unconditionally since [postguard#293](https://github.com/encryption4all/postguard/issues/293); the standalone `encryption4all/cryptify` repository had its build and push jobs retired and is archived. The GHCR package name did not change, so nothing downstream had to be repointed.


One gap to know about when pinning a version. `cryptify-v0.1.28` was released from the workspace before that publish job existed, so **no `0.1.28` image was ever built**, and it cannot be produced by re-running anything. The highest semver tag in the registry is `0.1.27`, pushed by the now-archived repo. `edge` tracks `main` and is current. See [postguard#312](https://github.com/encryption4all/postguard/issues/312).

Cryptify is the file encryption and sharing service that PostGuard uses for delivering encrypted files. It allows encrypting any file with an identity attribute. Only people who can prove they have that attribute can decrypt and view the contents.

Expand Down
12 changes: 7 additions & 5 deletions docs/repos/postguard-outlook-addon.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,14 @@ Production hosts and PKG / Cryptify URLs are baked in at Docker build time via t

## Releasing

Releases run on every push to `master` via `.github/workflows/release.yml`:
Releases come out of the [postguard-js](https://github.com/encryption4all/postguard-js) workspace, from `apps/outlook-addon`, via `.github/workflows/outlook-addon.yml`. The standalone `postguard-outlook-addon` repository is archived: it releases nothing and its `release.yml` cannot run.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 157, two lines above this hunk, still reads:

Production hosts and PKG / Cryptify URLs are baked in at Docker build time via the ADDIN_PUBLIC_URL, PKG_URL, CRYPTIFY_URL, and POSTGUARD_WEBSITE_URL build args (see .github/workflows/release.yml).

That is the archived repo's workflow which this very line declares cannot run. Those build args now come from the PROD_* / EDGE_* env block in outlook-addon.yml and the corresponding ARGs in apps/outlook-addon/Dockerfile.

Leaving it is the exact stale-pointer failure mode the PR exists to remove, and it is two lines from the fix. (Outside the diff, so no suggestion block.)


1. `googleapis/release-please-action` watches conventional commits and opens a release PR. Merging that PR cuts a tagged release.
2. On non-release pushes, CI builds and pushes `ghcr.io/encryption4all/postguard-outlook-addon:edge` (and a `sha-<commit>` tag) using staging hosts.
3. On release pushes, CI builds the same image with production hosts and tags it with the released version.
1. [changesets](https://github.com/changesets/changesets) sets the version. It bumps `package.json` only, so `pnpm --filter postguard-outlook-addin sync-version` propagates it into `manifest.xml`'s `<Version>`.
2. On PRs and pushes to `main`, CI builds and pushes `ghcr.io/encryption4all/postguard-outlook-addon:edge` (plus a `sha-<commit>` tag) using staging hosts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Step 2 is wrong on three counts, and two of them are stale text carried over from the old release-please description.

I read outlook-addon.yml in postguard-js:

  • PRs do not push. The PR path is the docker job, if: github.event_name == 'pull_request', and it sets push: false. Pushing only happens in the image job, if: github.event_name == 'push'.
  • The PR build uses production hosts, not staging. Its build args are PROD_ADDIN_HOST / PROD_PKG_URL / PROD_CRYPTIFY_URL / PROD_WEBSITE_URL.
  • There is no sha-<commit> tag. grep for sha- across the workflow returns nothing. The only tags emitted are ${IMAGE}:edge on main and ${IMAGE}:${version},${IMAGE}:latest on a tag.

Worth fixing because the website page in this same PR gets the equivalent sentence right: "Publishing happens on main only; PRs build without pushing."

Suggested change
2. On PRs and pushes to `main`, CI builds and pushes `ghcr.io/encryption4all/postguard-outlook-addon:edge` (plus a `sha-<commit>` tag) using staging hosts.
2. On pushes to `main`, CI builds and pushes `ghcr.io/encryption4all/postguard-outlook-addon:edge` using staging hosts. On PRs the image is built but not pushed, and with production hosts, since the build refuses staging fallbacks in production mode.

3. Pushing an **app-scoped** tag — `outlook-addin-v1.0.0`, never `v*`, because the tag namespace is shared with `@e4a/pg-js`'s changesets releases — builds the same image with production hosts, tags it with the released version, and creates a GitHub release carrying `manifest.xml` as an asset.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things on this line.

The tag path also pushes :latest. The workflow emits echo "tags=${IMAGE}:${version},${IMAGE}:latest". Worth naming on a page that otherwise carefully explains which GHCR tags exist and that postguard-ops pins this package, since a moving :latest is the kind of thing an ops pin can quietly be resting on.

Em dash and bold budget. The repo's own CLAUDE.md (lines 92-93) sets "target zero, hard max one per 1,000 words" for each, and adds "Do not bold for emphasis in running prose." Counting em dashes at this commit against base:

Page Em dashes Words
postguard-website.md 0 → 4 1,272
postguard-outlook-addon.md 8 → 11 1,871
postguard-tb-addon.md 1 → 3 2,045

All three land over the hard max. For reference docs/repos/postguard.md runs 2,282 words with zero, so the rule is being kept elsewhere. The bolded **sole**, **app-scoped** and **not** are emphasis in running prose, which the rule names specifically. Most of the dashes read fine as commas, parentheses or a full stop. (The new CLAUDE.md paragraph in this PR has the same issue, incidentally.)

A rewrite of this line that fixes both:

Suggested change
3. Pushing an **app-scoped** tag `outlook-addin-v1.0.0`, never `v*`, because the tag namespace is shared with `@e4a/pg-js`'s changesets releasesbuilds the same image with production hosts, tags it with the released version, and creates a GitHub release carrying `manifest.xml` as an asset.
3. Pushing an app-scoped tag (`outlook-addin-v1.0.0`, never `v*`, because the tag namespace is shared with `@e4a/pg-js`'s changesets releases) builds the same image with production hosts, tags it with the released version and `:latest`, and creates a GitHub release carrying `manifest.xml` as an asset.


The image name is hardcoded rather than derived from the workflow's `github.repository` context, which resolves to `postguard-js` while [postguard-ops](https://github.com/privacybydesign/postguard-ops) pins `ghcr.io/encryption4all/postguard-outlook-addon`. The GHCR package name did not change on the move, so nothing downstream had to be repointed.

The image is an NGINX container serving the built add-in over HTTPS. To deploy, pull the new tag and restart the container on the host serving `addin.postguard.eu` (or `addin.staging.postguard.eu` for `:edge`). There is no automatic deploy step in CI today.

The Office add-in store submission and admin-center deployment still happen out of band against the published manifest.
The Office add-in store submission and admin-center deployment still happen out of band against the published manifest. Point them at the monorepo's `outlook-addin-v*` releases. The archived repository's `releases/latest/download/manifest.xml` still resolves and still serves v0.5.0's manifest, and it always will — an archived repo keeps serving its release assets, so anything left pointing there is frozen with no error to notice.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"still serves v0.5.0's manifest" is misleading in the one place it matters most, and I checked rather than assumed.

I fetched both assets. The archived repo's releases/latest/download/manifest.xml (HTTP 200) declares:

<Id>149e61a5-f295-4bcd-be3a-1a6114166f26</Id>
<Version>1.0.0.0</Version>

The current outlook-addin-v1.0.0 asset declares the same <Id> and the same <Version>. A full diff of the two files is three lines: the archived one carries extra staging.postguard.eu, addin.staging.postguard.eu and localhost:3000 <AppDomain> entries. Nothing else differs.

So an admin who follows this page's advice and checks the version to see whether they are on the frozen manifest reads 1.0.0.0 either way and concludes they are current. The paragraph promises a distinguishing signal that does not exist — on the page whose whole thesis is that frozen artifacts give no signal. Say that the version is identical.

Suggested change
The Office add-in store submission and admin-center deployment still happen out of band against the published manifest. Point them at the monorepo's `outlook-addin-v*` releases. The archived repository's `releases/latest/download/manifest.xml` still resolves and still serves v0.5.0's manifest, and it always will — an archived repo keeps serving its release assets, so anything left pointing there is frozen with no error to notice.
The Office add-in store submission and admin-center deployment still happen out of band against the published manifest. Point them at the monorepo's `outlook-addin-v*` releases. The archived repository's `releases/latest/download/manifest.xml` still resolves and always will, and it cannot be told apart by version: it declares the same `<Version>` of `1.0.0.0`, the same `<Id>`, and the same `addin.postguard.eu` `<SourceLocation>` as the current `outlook-addin-v1.0.0` asset, differing only by three leftover staging `<AppDomain>` entries. Anything left pointing there is frozen with nothing at all to notice it by.

32 changes: 18 additions & 14 deletions docs/repos/postguard-tb-addon.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,25 +394,29 @@ To load the extension in Thunderbird: open **Add-ons Manager** > **gear icon** >

## Releasing

The version must be updated in three files before releasing:
Releases come out of the [postguard-js](https://github.com/encryption4all/postguard-js) workspace, from `apps/tb-addon`. The standalone `postguard-tb-addon` repository is archived and releases nothing.

1. `package.json` (`"version"`)
2. `manifest.json` (`"version"`)
3. `updates.json` (add a new entry with the new version)
Versions are set by [changesets](https://github.com/changesets/changesets), which bumps `package.json` only. Thunderbird reads `manifest.json` and the auto-update channel reads `updates.json`, so `pnpm --filter postguard-tb-addon sync-version` propagates the version into both, and `pnpm check-version` fails the PR when they drift. The release job refuses a tag that does not match.

Then commit, push, and tag:
Release tags are **app-scoped**: `tb-addon-v0.9.4`, never `v*`. The tag namespace is shared with `@e4a/pg-js`'s changesets releases (and still holds the pre-monorepo `v2.3.3`-style pg-js tags), so a bare `v*` trigger would fire on another package's release.

### The auto-update channel

Installed add-ons poll the `update_url` baked into the build they are running. For 0.9.4 onward that is the raw file on the monorepo's default branch:

```bash
git add package.json manifest.json updates.json
git commit -m "Bump version to X.Y.Z"
git push origin main
git tag vX.Y.Z && git push origin vX.Y.Z
```
https://raw.githubusercontent.com/encryption4all/postguard-js/main/apps/tb-addon/updates.json
```

It is deliberately **not** a release asset. `releases/latest` in a repo shared with `@e4a/pg-js` is whichever package released most recently, and a pg-js release carries no `updates.json` at all.

Pushing a `v*` tag triggers the CI pipeline which builds the `.xpi` file and creates a GitHub release.
Everything up to 0.9.3 baked in `postguard-tb-addon/releases/latest/download/updates.json` instead — the archived repo. Those installs are carried across by a single release there, tagged `channel-migration-0.9.4`, whose `updates.json` advertises 0.9.4 with an `update_link` into postguard-js. Taking that update moves the add-on onto the new channel permanently. The tag deliberately does not start with `v` so the old repository's build workflow did not fire for it, and the release ships no `.xpi` — it exists only to move the channel. This is why archiving a repo that hosts a self-hosted add-on update channel has to be preceded by one last publish from it; archive first and every installed add-on stops updating with no error.

## CI/CD

| Workflow | Trigger | What it does |
|---|---|---|
| `build.yml` | Tag push (`v*`) | Validates version consistency, builds, packages `.xpi`, creates GitHub release |
Workflow: `.github/workflows/tb-addon.yml` in postguard-js.

| Trigger | What it does |
|---|---|
| PR / push to `main` | `pnpm check-version`, typecheck, tests, build the extension. Deliberately not path-filtered, so an SDK change in `packages/pg-js` is tested against the add-on in the same PR |
| Tag push (`tb-addon-v*`) | Packages the `.xpi` and creates the GitHub release carrying it and `updates.json` |
20 changes: 14 additions & 6 deletions docs/repos/postguard-website.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ The PostGuard web frontend for encrypting and sending files. Users pick files, c

The website uses `@e4a/pg-js` with two Vite plugins for WASM support (`vite-plugin-wasm` and `vite-plugin-top-level-await`).

The website contains two submodules: Cryptify (the file sharing backend, embedded in an iframe) and the Thunderbird addon (the `.xpi` file can be downloaded from the website). To update the submodules:
`apps/website` carries two git submodules, used by the local dev stack rather than by the built site: `cryptify` and `postguard`. Initialise them with:

```bash
git submodule update --init --recursive
```

Note that the `cryptify` submodule still points at `encryption4all/cryptify`, which is archived — the service is a member of the [postguard](/repos/postguard) workspace now, so that pin can never advance. Tracked in [encryption4all/postguard-js#225](https://github.com/encryption4all/postguard-js/issues/225).

The Thunderbird `.xpi` is **not** a submodule. `scripts/sync-addons.mjs` mirrors the add-on release artifacts into `static/downloads/` — the `.xpi` from the monorepo's `tb-addon-v*` releases and the Outlook `manifest.xml` from its `outlook-addin-v*` releases — refreshing on a 6h interval inside the container.

For a step-by-step example of building a web application with PostGuard, see the [pg-sveltekit](/repos/pg-sveltekit) example, which follows the same patterns as this website.

## Recipient URL forms
Expand Down Expand Up @@ -155,11 +159,15 @@ The runtime tier exists because these values change per environment without rebu

## Releasing

This repository uses [Release-please](https://github.com/googleapis/release-please) for automated versioning. Merging a release PR triggers a multi-architecture Docker image build pushed to GHCR.
Releases come out of the [postguard-js](https://github.com/encryption4all/postguard-js) workspace via `.github/workflows/website.yml`. Versioning is [changesets](https://github.com/changesets/changesets), not release-please, and the standalone `postguard-website` repository is archived and releases nothing.

The image is `ghcr.io/encryption4all/postguard-website` — the same GHCR package name the standalone repo published, hardcoded in the workflow rather than derived from the `github.repository` context (which resolves to `postguard-js`) precisely so [postguard-ops](https://github.com/privacybydesign/postguard-ops)' pin keeps working. Publishing happens on `main` only; PRs build without pushing.

## CI/CD

| Workflow | Trigger | What it does |
|---|---|---|
| `ci.yml` | Push/PR | Svelte type checks, release-please, multi-arch Docker build |
| `pr-title.yml` | PR | Validates PR title format |
Workflow: `.github/workflows/website.yml` in postguard-js. Deliberately not path-filtered to `apps/website/**`, so an SDK change in `packages/pg-js` is tested against the site in the same PR.

| Trigger | What it does |
|---|---|
| PR / push to `main` | Svelte type checks, lint, CSS custom-property lint, unit tests, and an nginx config syntax test over both `docker/nginx.dev.conf` and `docker/default.conf.template` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR-check row omits the test-e2e job, which runs pnpm test:e2e (Playwright chromium) on the same pull_request and push triggers as the jobs listed, and uploads playwright-report/ plus test-results/ on failure.

Since this table is the page's answer to "what gates a website PR", the slowest gate is the one most worth naming.

Suggested change
| PR / push to `main` | Svelte type checks, lint, CSS custom-property lint, unit tests, and an nginx config syntax test over both `docker/nginx.dev.conf` and `docker/default.conf.template` |
| PR / push to `main` | Svelte type checks, lint, CSS custom-property lint, unit tests, Playwright e2e tests, and an nginx config syntax test over both `docker/nginx.dev.conf` and `docker/default.conf.template` |

| Push to `main` | Multi-arch Docker build pushed to GHCR (`:edge`, plus the changesets version on a release commit) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"plus the changesets version on a release commit" inverts what the workflow does, and website.yml calls this out explicitly in a comment on the finalize job:

The version tag is unconditional on main rather than gated on a release step: changesets bumps apps/website/package.json in its Version Packages PR, so merging that PR is what moves this tag. Re-tagging an unchanged version is a no-op.

The metadata step is type=raw,value=${{ steps.version.outputs.value }} with no enable= condition, and finalize is gated only on github.ref == 'refs/heads/main'. So every push to main re-applies the version tag.

For contrast, delivery.yml in postguard does gate its version tags: type=raw,value=...,enable=${{ needs.release-plz-release.outputs.cryptify_version != '' }}.

Suggested change
| Push to `main` | Multi-arch Docker build pushed to GHCR (`:edge`, plus the changesets version on a release commit) |
| Push to `main` | Multi-arch Docker build pushed to GHCR (`:edge`, plus the `apps/website/package.json` version, applied on every push to `main` rather than only on a release commit) |

Loading