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: 5 additions & 0 deletions .changeset/array-draggable-false-non-inline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apostrophe": minor
---

Added support for `draggable: false` on non-inline `array` schema fields. Previously this option was only respected when `inline: true`. When set on a standard (modal-based) array field, drag-and-drop reordering and keyboard reordering are now disabled in the array editor's slat list.
5 changes: 5 additions & 0 deletions .changeset/common-beans-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apostrophecms/seo": minor
---

Removes unimplemented hreflang output; use @apostrophecms/sitemap for hreflang support
5 changes: 5 additions & 0 deletions .changeset/curvy-bobcats-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apostrophe": minor
---

Introduced support for postgres://, sqlite://, and multipostgres:// database URIs in addition to mongodb://. The new db-connect API supports all of the database operations currently used in our own core, pro and multisite modules. For more information see the documentation.
5 changes: 5 additions & 0 deletions .changeset/env-secrets-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apostrophe": minor
---

The session secret and the uploadfs `disabledFileKey` can now be supplied via the `APOS_SESSION_SECRET` and `APOS_UPLOADFS_DISABLED_FILE_KEY` environment variables. As with other Apostrophe environment variables, these take precedence over the corresponding `app.js` configuration.
5 changes: 5 additions & 0 deletions .changeset/new-doors-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sanitize-html": patch
---

Address a potential vulnerability when nonTextTags is configured in a nonstandard way. While it is never a good idea to remove known non-text tags from the standard list e.g. script, styles, etc., this change ensures that doing so does not result in nested tags being passed through without sanitization when they are not expressly allowed. (ApostropheCMS would never trigger this situation.) Thanks to [Dipanshu singh](https://github.com/Dipanshusinghh) for pointing out the issue and contributing the fix.
11 changes: 11 additions & 0 deletions .changeset/ripe-terms-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@apostrophecms/apostrophe-astro": minor
"apostrophe": minor
---

Fixed adding or removing an area field from a schema breaking existing documents on an external front such as Astro.

- `AposArea` now renders only schema-backed areas. A missing area no longer throws, and an area orphaned by removing its field from the schema (while its content remains in the document) renders nothing instead of breaking sibling areas in edit mode. Logged-in editors get a diagnostic message in place of an orphaned area; anonymous visitors see nothing.
- Editable documents sent to an external front now materialize empty area objects for schema area fields added after the document was created, so they can be edited in context.
- `apos.util.getManagerOf` accepts a `{ log }` option to suppress its error log when probing objects that may not have a manager.

5 changes: 5 additions & 0 deletions .changeset/shaky-regions-spend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apostrophecms/seo": minor
---

Removes the `seoSiteCanonicalUrl` field from global settings. The base URL is now derived automatically from `APOS_BASE_URL` or the `baseUrl` option. The value remains available at `req.data.global.seoSiteCanonicalUrl` for backwards compatibility.
5 changes: 5 additions & 0 deletions .changeset/smart-kids-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apostrophe": patch
---

Selecting an item in a relationship "browse" dialog no longer scrolls the title and Cancel/Select buttons out of view when the item is far down the list.
6 changes: 6 additions & 0 deletions .changeset/soft-hats-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@apostrophecms/cli": minor
---

**Breaking:** `apos create` is now an interactive guided installer (it delegates to `create-apostrophe`). The `<shortname>` positional argument and the `--starter` and `--mongodb-uri` options have been removed - project name, starter kit, and database are now chosen through prompts. For scripted installs, use `npm create apostrophe@latest -- --unattended` instead.

5 changes: 5 additions & 0 deletions .changeset/sparkly-experts-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apostrophe": patch
---

Sites with a custom filterByIndexPage method no longer experience failures in the sitemap module and potential creeping CPU performance penalties. A regression introduced with our static site support, but not specific to static sites.
5 changes: 5 additions & 0 deletions .changeset/twelve-paws-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apostrophecms/redirect": minor
---

Prevent infinite redirects to external URLs
5 changes: 5 additions & 0 deletions .changeset/violet-windows-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apostrophe": minor
---

JSX support for templates within ApostropheCMS. JSX is now co-equal with Nunjucks, with a gradual migration strategy. Anyone who is familiar with React will be very comfortable writing JSX templates, which also offer a superior debugging experience, and templates can be migrated gradually. JSX is a great option for those who don't wish to create parallel Astro and ApostropheCMS projects, but still prefer a modern syntax. For more information, see the new [JSX templates guide](https://apostrophecms.com/docs/guide/jsx-templates.html).
70 changes: 65 additions & 5 deletions .github/workflows/monorepo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ permissions:

env:
# Define supported runtime versions for matrix expansion once for the workflow.
NODE_VERSIONS_JSON: "[20,22,24]"
NODE_VERSIONS_JSON: "[22,24,26]"
MONGODB_VERSIONS_JSON: '["7","8"]'
REDIS_VERSION: "7"
POSTGRES_VERSION: "16"

on:
push:
Expand Down Expand Up @@ -124,7 +125,7 @@ jobs:
run: |
month='${{ steps.cache-month.outputs.value }}'
mongo_suffix=$(jq -r '.[]' <<< '${{ env.MONGODB_VERSIONS_JSON }}' | paste -sd'-' -)
echo "key=docker-images-${month}-mongo${mongo_suffix}-redis${{ env.REDIS_VERSION }}" >> "$GITHUB_OUTPUT"
echo "key=docker-images-${month}-mongo${mongo_suffix}-redis${{ env.REDIS_VERSION }}-pg${{ env.POSTGRES_VERSION }}" >> "$GITHUB_OUTPUT"

- name: Restore docker image cache
id: docker-cache
Expand All @@ -143,6 +144,8 @@ jobs:
done
docker pull "redis:${{ env.REDIS_VERSION }}"
docker save "redis:${{ env.REDIS_VERSION }}" -o ".github/docker-cache/redis-${{ env.REDIS_VERSION }}.tar"
docker pull "postgres:${{ env.POSTGRES_VERSION }}"
docker save "postgres:${{ env.POSTGRES_VERSION }}" -o ".github/docker-cache/postgres-${{ env.POSTGRES_VERSION }}.tar"

warm-sharp-cache:
name: Warm sharp/libvips cache (Node ${{ matrix.nodeVersion }})
Expand Down Expand Up @@ -216,7 +219,7 @@ jobs:
du -sh ~/.npm/_libvips || true

package-tests:
name: ${{ format('{0} ({1}, {2})', matrix.package, matrix.nodeVersion, matrix.needsMongo && matrix.mongodbVersion || 'n/a') }}
name: ${{ format('{0} ({1}, {2}{3})', matrix.group, matrix.nodeVersion, matrix.adapter || 'n/a', matrix.needsMongo && format(' mongo {0}', matrix.mongodbVersion) || '') }}
needs:
- setup
- shared-runtime
Expand Down Expand Up @@ -279,7 +282,7 @@ jobs:
- name: Restore docker image cache
id: docker-cache-restore
uses: actions/cache/restore@v4
if: matrix.needsMongo || matrix.needsRedis
if: matrix.needsMongo || matrix.needsRedis || matrix.needsPostgres
with:
path: .github/docker-cache
key: ${{ needs.shared-runtime.outputs.docker-cache-key }}
Expand All @@ -300,6 +303,14 @@ jobs:
if: steps.docker-cache-restore.outputs.cache-hit != 'true' && matrix.needsRedis
run: docker pull redis:${{ env.REDIS_VERSION }}

- name: Load cached Postgres image
if: steps.docker-cache-restore.outputs.cache-hit == 'true' && matrix.needsPostgres
run: docker load -i ".github/docker-cache/postgres-${{ env.POSTGRES_VERSION }}.tar"

- name: Pull Postgres image (cache miss)
if: steps.docker-cache-restore.outputs.cache-hit != 'true' && matrix.needsPostgres
run: docker pull postgres:${{ env.POSTGRES_VERSION }}

- name: Start MongoDB
if: matrix.needsMongo
run: |
Expand Down Expand Up @@ -358,15 +369,64 @@ jobs:
docker logs redis
exit 1

- name: Start PostgreSQL
if: matrix.needsPostgres
run: |
docker rm -f postgres >/dev/null 2>&1 || true
docker run -d \
--name postgres \
--publish 5432:5432 \
-e POSTGRES_HOST_AUTH_METHOD=trust \
--health-cmd "pg_isready -U postgres" \
--health-interval 5s \
--health-timeout 5s \
--health-retries 12 \
postgres:${{ env.POSTGRES_VERSION }}
echo "Waiting for PostgreSQL to report healthy..."
for attempt in $(seq 1 60); do
status=$(docker inspect --format='{{.State.Health.Status}}' postgres 2>/dev/null || echo "starting")
if [ "$status" = "healthy" ]; then
exit 0
fi
if [ "$status" = "unhealthy" ]; then
echo "PostgreSQL reported unhealthy" >&2
docker logs postgres
exit 1
fi
sleep 2
done
echo "PostgreSQL failed to become healthy in time" >&2
docker logs postgres
exit 1

- name: Install workspace dependencies
run: pnpm install --frozen-lockfile

- name: Run package tests
run: pnpm run --filter "${{ matrix.package }}" --if-present test
run: |
failed=0
for pkg in $(echo '${{ matrix.packages }}' | jq -r '.[]'); do
echo "::group::Testing $pkg"
if ! pnpm run --filter "$pkg" --if-present test; then
echo "::error::Tests failed for $pkg"
failed=1
fi
echo "::endgroup::"
done
exit $failed
env:
CI: true
# Yes we want import-export to test with automatic-translation
TEST_WITH_PRO: "1"
# Adapter selection: mongodb (default), postgres, or sqlite.
# ADAPTER is used by db-connect tests, APOS_TEST_DB_PROTOCOL by apostrophe tests.
ADAPTER: ${{ matrix.adapter }}
APOS_TEST_DB_PROTOCOL: ${{ matrix.adapter }}
PGUSER: postgres

- name: Stop PostgreSQL
if: always() && matrix.needsPostgres
run: docker rm -f postgres || true

- name: Stop Redis
if: always() && matrix.needsRedis
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/scripts/detect-impacted-packages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ async function main() {
package: name,
directory: packages.get(name).relativeDir,
requiresMongo: packages.get(name).requiresMongo !== false,
requiresRedis: packages.get(name).requiresRedis === true
requiresRedis: packages.get(name).requiresRedis === true,
mongodbOnly: packages.get(name).mongodbOnly === true
}))
};

Expand Down Expand Up @@ -130,14 +131,16 @@ async function loadPackages() {
const testConfig = manifest.apostropheTestConfig || {};
const requiresMongo = testConfig.requiresMongo !== false;
const requiresRedis = testConfig.requiresRedis === true;
const mongodbOnly = testConfig.mongodbOnly === true;

map.set(manifest.name, {
name: manifest.name,
relativeDir: path.posix.join('packages', entry.name),
dependencies,
hasTestScript,
requiresMongo,
requiresRedis
requiresRedis,
mongodbOnly
});
}));

Expand Down
126 changes: 118 additions & 8 deletions .github/workflows/scripts/expand-runtime-matrix.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
#!/usr/bin/env node
// Expands the impacted package matrix with runtime permutations supplied
// Expands the impacted package matrix with runtime permutations supplied
// via env vars.
//
// Packages are grouped into jobs to stay within GitHub's 256-entry matrix limit:
// - "apostrophe" runs solo (the main package, benefits from its own status).
// - All other database packages are grouped into an "ecosystem" job.
// - mongodbOnly packages are grouped into "ecosystem-mongodb".
// - Non-database packages are grouped into "standalone".
//
// For groups that need a database, three adapter variants are emitted:
// 1. mongodb – all Node versions × all MongoDB versions
// 2. postgres – latest LTS Node only, no MongoDB
// 3. sqlite – latest LTS Node only, no MongoDB
//
// The latest LTS Node version is the highest even-numbered entry in
// NODE_VERSIONS_JSON.

import { readFile } from 'fs/promises';

const args = process.argv.slice(2);
Expand Down Expand Up @@ -36,25 +51,120 @@ function parseJsonArray(name, raw) {
const nodeVersions = parseJsonArray('NODE_VERSIONS_JSON', process.env.NODE_VERSIONS_JSON);
const mongodbVersions = parseJsonArray('MONGODB_VERSIONS_JSON', process.env.MONGODB_VERSIONS_JSON);

// Latest LTS = highest even-numbered Node version
const latestLts = [...nodeVersions]
.filter((v) => Number(v) % 2 === 0)
.sort((a, b) => Number(b) - Number(a))[0];

const impact = JSON.parse(await readFile(impactPath, 'utf8'));
const packages = impact?.matrix?.include || [];
const include = [];

// The main apostrophe package always gets its own jobs for clear CI status.
const SOLO_PACKAGES = new Set(['apostrophe']);

// Sort packages into groups
const solo = [];
const ecosystem = [];
const ecosystemMongodbOnly = [];
const standalone = [];

for (const pkg of packages) {
const needsMongo = pkg.requiresMongo !== false;
const needsRedis = pkg.requiresRedis === true;
const mongoTargets = needsMongo ? mongodbVersions : [''];
const needsDb = pkg.requiresMongo !== false;
if (SOLO_PACKAGES.has(pkg.package)) {
solo.push(pkg);
} else if (needsDb && pkg.mongodbOnly) {
ecosystemMongodbOnly.push(pkg);
} else if (needsDb) {
ecosystem.push(pkg);
} else {
standalone.push(pkg);
}
}

const include = [];

// Emit runtime combinations for a group of packages.
function emitGroup(group, pkgs) {
if (!pkgs.length) {
return;
}
const packageNames = JSON.stringify(pkgs.map((p) => p.package));
const needsRedis = pkgs.some((p) => p.requiresRedis === true);
const mongodbOnly = pkgs.every((p) => p.mongodbOnly);

// mongodb: all Node versions × all MongoDB versions
for (const nodeVersion of nodeVersions) {
for (const mongodbVersion of mongoTargets) {
for (const mongodbVersion of mongodbVersions) {
include.push({
...pkg,
group,
packages: packageNames,
nodeVersion,
mongodbVersion,
needsMongo,
adapter: 'mongodb',
needsMongo: true,
needsPostgres: false,
needsRedis
});
}
}
// postgres and sqlite: latest LTS only, skip for mongodb-only groups
if (!mongodbOnly) {
include.push({
group,
packages: packageNames,
nodeVersion: latestLts,
mongodbVersion: '',
adapter: 'postgres',
needsMongo: false,
needsPostgres: true,
needsRedis
});
include.push({
group,
packages: packageNames,
nodeVersion: latestLts,
mongodbVersion: '',
adapter: 'sqlite',
needsMongo: false,
needsPostgres: false,
needsRedis
});
}
}

// Emit non-database group (no adapter variants, just Node versions)
function emitStandalone(group, pkgs) {
if (!pkgs.length) {
return;
}
const packageNames = JSON.stringify(pkgs.map((p) => p.package));
for (const nodeVersion of nodeVersions) {
include.push({
group,
packages: packageNames,
nodeVersion,
mongodbVersion: '',
adapter: '',
needsMongo: false,
needsPostgres: false,
needsRedis: false
});
}
}

// Solo packages each get their own group
for (const pkg of solo) {
emitGroup(pkg.package, [pkg]);
}

if (ecosystem.length) {
emitGroup('ecosystem', ecosystem);
}
if (ecosystemMongodbOnly.length) {
emitGroup('ecosystem-mongodb', ecosystemMongodbOnly);
}
if (standalone.length) {
emitStandalone('standalone', standalone);
}

process.stdout.write(JSON.stringify({ include }));
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ public/apos-frontend
.DS_Store
coverage/
.nyc_output
claude-tools/logs/
.claude
specs/
Loading
Loading