Skip to content

Added config-driven store selection for redirects#27894

Merged
vershwal merged 8 commits into
mainfrom
config-driven-store-selection
May 21, 2026
Merged

Added config-driven store selection for redirects#27894
vershwal merged 8 commits into
mainfrom
config-driven-store-selection

Conversation

@vershwal
Copy link
Copy Markdown
Member

@vershwal vershwal commented May 14, 2026

ref https://linear.app/ghost/issue/HKG-1701

Made the redirects store selectable via Ghost's existing adapter-manager.

Selection now flowed through the same path as storage, sso, cache, and scheduling.

  • adapter-manager now knew about a redirects type, validated against a runtime RedirectsStoreBase.
  • Config used adapters.redirects.active = "FileStore" | "GCSStore", with per-class config under adapters.redirects.<ClassName>. It defaulted to FileStore.
  • RedirectsService got its store via adapterManager.getAdapter('redirects'); it no longer knew which implementation backed it.
  • Stores lived alongside other in-tree adapters at core/server/adapters/redirects/.

Notes

  • GCSStore constructor was reshaped (vs Added GCSStore for redirects with timestamped backups #27891): It took config primitives (bucket, region, endpoint, accessKeyId, …) plus an optional s3Client for test injection — same hybrid as S3Storage.ts. This reversed Added GCSStore for redirects with timestamped backups #27891's decision to require an injected s3Client, because adapter-manager passed a plain config object to constructors and couldn't build dependencies. The store had to own client construction.
  • Partial-credential validation was restored for the same reason — accessKeyId and secretAccessKey had to be supplied together once the store built the S3Client.
  • RedirectsStoreBase was a marker classFileStore and GCSStore shared zero implementation, so the base only declared requiredFns. This trade-off was accepted for adapter-manager pattern alignment.
  • basePath was injected at runtime in adapter-manager/config.js because it was computed from Ghost's content directory. This mirrored the scheduling.schedulerUrl precedent.

Tests

  • Unit tests passed: FileStore 17/17, GCSStore 3/3, redirects-service 18/18, adapter-manager 7/7.
  • Integration test was relocated to test/integration/adapters/redirects/ using the existing precedent from test/integration/adapters/redis/. It ran against MinIO via pnpm dev:storage locally, or the GHA MinIO container from HKG-1699 in CI.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR refactors the custom redirects storage system from a hardcoded FileStore dependency into an adapter-managed architecture. A new RedirectsStoreBase class defines the requiredFns contract (getAll and replaceAll), which FileStore and a new S3RedirectsStore both extend. The custom-redirects service now resolves its store via adapterManager.getAdapter('redirects') rather than instantiating FileStore directly. The S3RedirectsStore adds bucket-based storage with tenant prefix scoping and server-side backup semantics. Configuration and adapter registration plumb FileStore's base path, and test suites validate both store implementations, including MinIO infrastructure helpers and a restructured broken-adapter test.

Possibly related PRs

  • TryGhost/Ghost#27697: Complements this PR by refactoring custom-redirects/index.js to obtain the redirects store via adapterManager.getAdapter('redirects'), whereas the related PR had integrated RedirectsService with FileStore directly.
  • TryGhost/Ghost#27858: Both PRs update MinIO test infrastructure for redirects storage by modifying ghost/core/test/utils/minio.ts to centralize S3 client configuration, enabling the S3 store integration tests in this PR.
  • TryGhost/Ghost#27891: The related PR originally added GCSStore with timestamped backup semantics, which this PR removes in favor of the new S3RedirectsStore adapter with equivalent behavior.

Suggested reviewers

  • allouis
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Added config-driven store selection for redirects' clearly and concisely summarizes the main change: enabling configurable selection of redirect storage backends.
Description check ✅ Passed The description is directly related to the changeset, explaining the adapter-manager integration, configuration structure, store implementations, and testing approach.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch config-driven-store-selection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vershwal
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/core/core/server/services/custom-redirects/make-store.ts`:
- Line 30: The current assignment for active uses the logical OR fallback which
treats empty string as falsey and coerces '' to 'file'; change the fallback to a
nullish default so only undefined/null fall back. Locate the active declaration
that reads config.get('storage:redirects:active') and replace the || 'file'
behavior with a nullish coalescing default (so empty string remains an explicit
value and will trigger the unknown-value handling elsewhere).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5e050373-08cb-487b-a864-989c453a74d7

📥 Commits

Reviewing files that changed from the base of the PR and between e22826d and a50c5c3.

📒 Files selected for processing (4)
  • ghost/core/core/server/services/custom-redirects/index.js
  • ghost/core/core/server/services/custom-redirects/make-store.ts
  • ghost/core/test/integration/services/custom-redirects/make-store.test.ts
  • ghost/core/test/unit/server/services/custom-redirects/make-store.test.ts

Comment thread ghost/core/core/server/services/custom-redirects/make-store.ts Outdated
Comment thread ghost/core/core/server/services/custom-redirects/make-store.ts Outdated
@vershwal vershwal requested a review from allouis May 14, 2026 07:25
@vershwal vershwal force-pushed the GCSStoreForRedirects branch from 50ae95c to 57bed7a Compare May 14, 2026 12:58
Base automatically changed from GCSStoreForRedirects to main May 14, 2026 13:31
@vershwal vershwal force-pushed the config-driven-store-selection branch 2 times, most recently from 2179a3b to abb180f Compare May 18, 2026 10:58
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/core/core/server/adapters/redirects/GCSStore.ts`:
- Around line 58-63: The validation currently only checks accessKeyId vs
secretAccessKey and allows a standalone sessionToken; update the credential
validation in GCSStore to also consider options.sessionToken so that if
sessionToken is provided without both accessKeyId and secretAccessKey it throws
errors.IncorrectUsageError (using tpl(messages.partialCredentials)); modify both
places where hasAccessKey/hasSecretKey are computed (the blocks around the
current checks) to compute hasSessionToken = Boolean(options.sessionToken) and
require that if hasSessionToken is true then hasAccessKey && hasSecretKey must
also be true (otherwise throw the same partialCredentials error).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 96113f5e-e29a-4edb-b555-c65514c4c631

📥 Commits

Reviewing files that changed from the base of the PR and between a50c5c3 and 2179a3b.

📒 Files selected for processing (11)
  • ghost/core/core/server/adapters/redirects/FileStore.ts
  • ghost/core/core/server/adapters/redirects/GCSStore.ts
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.d.ts
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.js
  • ghost/core/core/server/services/adapter-manager/config.js
  • ghost/core/core/server/services/adapter-manager/index.js
  • ghost/core/core/server/services/custom-redirects/index.js
  • ghost/core/core/shared/config/defaults.json
  • ghost/core/test/integration/adapters/redirects/gcs-store.test.ts
  • ghost/core/test/unit/server/adapters/redirects/file-store.test.ts
  • ghost/core/test/unit/server/adapters/redirects/gcs-store.test.ts
✅ Files skipped from review due to trivial changes (3)
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.d.ts
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.js
  • ghost/core/core/server/services/adapter-manager/config.js

Comment thread ghost/core/core/server/adapters/redirects/S3RedirectsStore.ts
@github-actions
Copy link
Copy Markdown
Contributor

E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 26029295641 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@vershwal vershwal force-pushed the config-driven-store-selection branch from 319f2e0 to 02fba9a Compare May 19, 2026 04:46
@vershwal
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@vershwal vershwal force-pushed the config-driven-store-selection branch from 02fba9a to 537da55 Compare May 19, 2026 04:55
@vershwal
Copy link
Copy Markdown
Member Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

✅ Actions performed

Reviews resumed.

Comment thread ghost/core/core/server/services/adapter-manager/config.js Outdated
Comment thread ghost/core/core/server/adapters/redirects/GCSStore.ts Outdated
@vershwal vershwal force-pushed the config-driven-store-selection branch from 140a341 to 939967d Compare May 19, 2026 11:15
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
ghost/core/test/integration/adapters/redirects/gcs-store.test.ts (1)

26-28: 💤 Low value

Consider escaping the prefix parameter in regex construction.

The static analysis tool flagged this as a potential ReDoS vulnerability because the prefix parameter is directly interpolated into the regex pattern. While the risk is very low here (test code with controlled string literal inputs like 'tenant-abc'), you could eliminate the warning by escaping the prefix using a library like escape-string-regexp or manually escaping regex special characters.

🔒 Optional fix to escape the prefix
+// Helper to escape regex special chars
+const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
 const backupKeyPattern = (prefix = '') => new RegExp(
-    `^${prefix ? `${prefix}/` : ''}redirects-\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2}\\.json$`
+    `^${prefix ? `${escapeRegex(prefix)}/` : ''}redirects-\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2}\\.json$`
 );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/test/integration/adapters/redirects/gcs-store.test.ts` around
lines 26 - 28, The regex in backupKeyPattern directly interpolates the prefix
parameter which can introduce a ReDoS/regex-injection warning; update the
function backupKeyPattern to escape prefix before building the RegExp (e.g.,
import and call escapeStringRegexp(prefix) from the escape-string-regexp package
or replace with a small utility that escapes regex metacharacters) and then
interpolate the escapedPrefix into the pattern so the generated RegExp is safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/core/core/server/adapters/redirects/GCSStore.ts`:
- Around line 110-127: The CopyObjectCommand in replaceAll uses CopySource:
`${this.bucket}/${key}` which must be URL-encoded; update replaceAll so
CopySource is built as `${this.bucket}/${encodeURIComponent(key)}` (keep
getBackupRedirectsFilePath, buildKey, PutObjectCommand and other logic
unchanged) to ensure the key portion is properly encoded when calling
CopyObjectCommand.

---

Nitpick comments:
In `@ghost/core/test/integration/adapters/redirects/gcs-store.test.ts`:
- Around line 26-28: The regex in backupKeyPattern directly interpolates the
prefix parameter which can introduce a ReDoS/regex-injection warning; update the
function backupKeyPattern to escape prefix before building the RegExp (e.g.,
import and call escapeStringRegexp(prefix) from the escape-string-regexp package
or replace with a small utility that escapes regex metacharacters) and then
interpolate the escapedPrefix into the pattern so the generated RegExp is safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b3fcb8fa-af4a-431a-859a-b1dfeccd7d76

📥 Commits

Reviewing files that changed from the base of the PR and between f40b1d0 and 939967d.

📒 Files selected for processing (15)
  • ghost/core/core/server/adapters/redirects/FileStore.ts
  • ghost/core/core/server/adapters/redirects/GCSStore.ts
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.d.ts
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.js
  • ghost/core/core/server/services/adapter-manager/config.js
  • ghost/core/core/server/services/adapter-manager/index.js
  • ghost/core/core/server/services/custom-redirects/index.js
  • ghost/core/core/shared/config/defaults.json
  • ghost/core/test/integration/adapters/redirects/gcs-store.test.ts
  • ghost/core/test/integration/services/custom-redirects/gcs-store.test.ts
  • ghost/core/test/unit/server/adapters/redirects/file-store.test.ts
  • ghost/core/test/unit/server/adapters/redirects/gcs-store.test.ts
  • ghost/core/test/unit/server/adapters/storage/index.test.js
  • ghost/core/test/unit/server/services/custom-redirects/gcs-store.test.ts
  • ghost/core/test/utils/minio.ts
💤 Files with no reviewable changes (2)
  • ghost/core/test/integration/services/custom-redirects/gcs-store.test.ts
  • ghost/core/test/unit/server/services/custom-redirects/gcs-store.test.ts
✅ Files skipped from review due to trivial changes (1)
  • ghost/core/test/unit/server/adapters/redirects/file-store.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.js
  • ghost/core/core/shared/config/defaults.json
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.d.ts
  • ghost/core/core/server/adapters/redirects/FileStore.ts
  • ghost/core/core/server/services/custom-redirects/index.js
  • ghost/core/test/unit/server/adapters/redirects/gcs-store.test.ts
  • ghost/core/core/server/services/adapter-manager/index.js

Comment thread ghost/core/core/server/adapters/redirects/S3RedirectsStore.ts
@vershwal vershwal force-pushed the config-driven-store-selection branch from 939967d to f9ef392 Compare May 19, 2026 11:45
@vershwal vershwal requested a review from allouis May 19, 2026 12:01
Comment thread ghost/core/core/server/adapters/redirects/S3RedirectsStore.ts
Comment thread ghost/core/core/server/adapters/redirects/GCSStore.ts
@vershwal vershwal added the preview Deploy a PR preview environment label May 20, 2026
@Ghost-Slimer Ghost-Slimer temporarily deployed to pr-preview-27894 May 20, 2026 12:23 Destroyed
@vershwal vershwal removed the preview Deploy a PR preview environment label May 20, 2026
@Ghost-Slimer Ghost-Slimer temporarily deployed to staging-pr-27894 May 20, 2026 12:40 Destroyed
@vershwal vershwal force-pushed the config-driven-store-selection branch from f9ef392 to 7b7656d Compare May 21, 2026 04:38
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/core/core/server/adapters/redirects/FileStore.ts`:
- Around line 49-50: The FileStore.getAll logging currently emits the full
redirects payload (JSON.stringify(parsed)) at info level which risks leaking
sensitive URL/query data and large noisy logs; update FileStore.getAll (and the
analogous log in the nearby method at lines 55-56) to stop logging the entire
parsed/redirects object — instead log a safe, minimal summary such as the number
of redirects or switch to a lower verbosity (debug/trace) and/or sanitize URLs
before logging so that sensitive query/path data is not recorded.

In `@ghost/core/core/server/adapters/redirects/S3RedirectsStore.ts`:
- Around line 115-116: The info log in S3RedirectsStore.getAll currently
serializes and logs the full parsed redirect objects (logging.info(`[redirects]
S3RedirectsStore.getAll: contents=${JSON.stringify(parsed)}`)), which can expose
sensitive URLs and bloat logs; change this to avoid logging full objects —
instead log a safe summary such as the number of redirects and/or a
non-sensitive identifier (e.g., logging.info(`[redirects]
S3RedirectsStore.getAll: count=${parsed.length}`) or list only sanitized
fields), and apply the same change to the similar log at the other location
(lines 122-123) so no full redirect payload is written to info logs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 216eab63-aa75-4d45-921b-d9c04f5e81e7

📥 Commits

Reviewing files that changed from the base of the PR and between f9ef392 and 844c2cb.

📒 Files selected for processing (17)
  • ghost/core/core/server/adapters/redirects/FileStore.ts
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.d.ts
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.js
  • ghost/core/core/server/adapters/redirects/S3RedirectsStore.ts
  • ghost/core/core/server/services/adapter-manager/config.js
  • ghost/core/core/server/services/adapter-manager/index.js
  • ghost/core/core/server/services/custom-redirects/gcs-store.ts
  • ghost/core/core/server/services/custom-redirects/index.js
  • ghost/core/core/server/services/custom-redirects/redirects-service.ts
  • ghost/core/core/shared/config/defaults.json
  • ghost/core/test/integration/adapters/redirects/s3-redirects-store.test.ts
  • ghost/core/test/integration/services/custom-redirects/gcs-store.test.ts
  • ghost/core/test/unit/server/adapters/redirects/file-store.test.ts
  • ghost/core/test/unit/server/adapters/redirects/s3-redirects-store.test.ts
  • ghost/core/test/unit/server/adapters/storage/index.test.js
  • ghost/core/test/unit/server/services/custom-redirects/gcs-store.test.ts
  • ghost/core/test/utils/minio.ts
💤 Files with no reviewable changes (3)
  • ghost/core/test/unit/server/services/custom-redirects/gcs-store.test.ts
  • ghost/core/core/server/services/custom-redirects/gcs-store.ts
  • ghost/core/test/integration/services/custom-redirects/gcs-store.test.ts
✅ Files skipped from review due to trivial changes (4)
  • ghost/core/core/server/adapters/redirects/RedirectsStoreBase.d.ts
  • ghost/core/core/server/services/custom-redirects/redirects-service.ts
  • ghost/core/core/shared/config/defaults.json
  • ghost/core/test/unit/server/adapters/redirects/file-store.test.ts

Comment thread ghost/core/core/server/adapters/redirects/FileStore.ts Outdated
Comment thread ghost/core/core/server/adapters/redirects/S3RedirectsStore.ts Outdated
@Ghost-Slimer Ghost-Slimer temporarily deployed to staging-pr-27894 May 21, 2026 05:16 Destroyed
@Ghost-Slimer Ghost-Slimer temporarily deployed to staging-pr-27894 May 21, 2026 06:41 Destroyed
@Ghost-Slimer Ghost-Slimer temporarily deployed to staging-pr-27894 May 21, 2026 06:58 Destroyed
@vershwal vershwal force-pushed the config-driven-store-selection branch from 9e54791 to 62e3540 Compare May 21, 2026 09:09
vershwal added 8 commits May 21, 2026 14:39
ref https://linear.app/ghost/issue/HKG-1701

- Adapter-manager requires a runtime base class for `instanceof` validation and a `requiredFns` array for method-shape checks before it will hand back an adapter instance.
- Marker-only — `FileStore` and `GCSStore` share zero implementation, so the base class is the cost of pattern alignment rather than a place for shared logic.
ref https://linear.app/ghost/issue/HKG-1701

- Adapter-manager resolves via `require(<paths>/<type>/<class>)`, so FileStore has to live at `adapters/redirects/` for the registration to find it.
- Now `extends RedirectsStoreBase` with `super()` to pass the `instanceof` gate. The .d.ts shim alongside the JS base class lets the TS adapter default-import it under `esModuleInterop` (TS won't resolve a `.js` module without `allowJs`).
- Test relocated to `test/unit/server/adapters/redirects/` to match the layout `S3Storage` already follows. Contract helper stays put — moving it would drag the in-memory-store test along with no real benefit.
ref https://linear.app/ghost/issue/HKG-1701

- Constructor now takes config primitives plus an optional `s3Client` for test injection — same hybrid as `S3Storage.ts`. Adapter-manager passes a plain config object to the constructor and can't build dependencies, so the store has to own client construction.
- Restored partial-credential validation we dropped when the client became required — both `accessKeyId` and `secretAccessKey` must be supplied together so a misconfigured operator gets a clear error instead of silent anonymous S3 calls.
- Switched to `export default` so the file clears the filename lint rule via the same exemption `S3Storage.ts` uses.
- Tests relocated to `test/{unit,integration}/server/adapters/redirects/`. Unit drops the `missingClient` case and restores partial-cred cases; integration is unchanged behaviourally.
ref https://linear.app/ghost/issue/HKG-1701

- Registered `redirects` as an adapter type backed by `RedirectsStoreBase`. Operators now pick `FileStore` (default) or `GCSStore` via `adapters.redirects.active`, with per-class config under `adapters.redirects.<ClassName>`.
- Injected `basePath` into the FileStore config at runtime in `adapter-manager/config.js` — mirrors the `scheduling.schedulerUrl` precedent, since the path comes from Ghost's content directory and can't be a static value. Guarded on `paths:contentPath` so unit tests that replace the `paths` config (e.g. storage's bad-adapter test) don't trip on an unrelated TypeError.
- Switched FileStore to `export default` so adapter-manager's `resolveAdapterExport` can find it via the same `.default` branch `S3Storage` and `GCSStore` already rely on. Named-only ESM exports fall through every branch and crash with `Adapter is not a constructor`.
- `custom-redirects/index.js` now resolves the store via `adapterManager.getAdapter('redirects')`; direct `new FileStore(...)` is gone and the service is store-agnostic.
- Resulting key layout per site:
    s3://<bucket>/<tenantPrefix>/redirects.json
    s3://<bucket>/<tenantPrefix>/redirects-YYYY-MM-DD-HH-mm-ss.json
  Deliberately flat under the tenant prefix (no extra `data/`
  subdir) — redirects has no CDN-facing URL prefix to mirror, and
  the local `<contentPath>/data/redirects.json` location is a
  filesystem-organisation choice that has no operational meaning
  on a shared bucket
- `tenantPrefix` is optional and defaults to empty, so existing
  self-hosted GCS deployments and Ghost's own tests keep writing
  to `redirects.json` at the bucket root unchanged
- Integration tests cover scoped reads/writes, scoped backup
  keys, multi-tenant isolation against a shared bucket, and
  slash normalisation
ref https://linear.app/ghost/issue/HKG-1701

- Removed `s3Client` and `getBackupKey` from `GCSStoreOptions`. Both were test-only escape hatches that gave the constructor two input shapes; it now takes config primitives only, matching the production config shape.
- Simplified the FileStore basePath block in `adapter-manager/config.js` to a single `||=` per allouis's suggestion. The previous four-condition guard existed only to dodge `storage/index.test.js`'s "broken storage" case — fixed that test instead by setting `paths:storage` via the colon-key form so the rest of `paths` stays intact.
- Rewrote the integration test against the same pattern as `adapter-cache-redis.test.js`: direct adapter construction, shared bucket per suite, no config manipulation. Backup-key tests match the per-second timestamp the store emits in production, with real waits between writes where distinct timestamps are needed.
- Extracted `getMinioConfig()` in `test/utils/minio.ts` so the admin S3Client (fixture setup) and the GCSStore under test share the same primitives.
ref https://linear.app/ghost/issue/HKG-1701

- The adapter is a generic S3-compatible client (AWS via IAM roles,
  GCS-HMAC, R2, MinIO, …), not GCS-specific. The old name implied
  otherwise and was a confusion vector during PR review.
- `S3RedirectsStore` keeps the purpose-in-the-name pattern used by
  `S3Storage` (storage) while disambiguating from it. Sibling
  `FileStore` keeps its name; only the GCS-flavoured class moved.
- Config key under `adapters.redirects.active` becomes
  `"S3RedirectsStore"` — Zuul will need a matching update (follow-up).
- No behaviour change; pure rename + symbol/messages updates.
@vershwal vershwal force-pushed the config-driven-store-selection branch from 62e3540 to 10a2e4f Compare May 21, 2026 09:09
@vershwal vershwal merged commit 5f27a65 into main May 21, 2026
42 checks passed
@vershwal vershwal deleted the config-driven-store-selection branch May 21, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants