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
52 changes: 41 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ jobs:
- 'ghost/**'
- '!ghost/admin/**'
- '!ghost/core/core/server/data/tinybird/**'
# Unit tests + vitest config are exercised only by job_unit-tests;
# they never affect the acceptance / legacy / ghost-cli jobs.
- '!ghost/core/test/unit/**'
- '!ghost/core/vitest.config.ts'
- '!ghost/core/test/utils/vitest-setup.ts'
tinybird:
- '.github/workflows/ci.yml'
- 'compose.dev.analytics.yaml'
Expand All @@ -121,6 +126,18 @@ jobs:
- '!**/*.md'
- '!.devcontainer/**'
- '!.vscode/**'
# Drives the run_e2e output: matches any changed file that could
# affect a running Ghost instance. Test files, test config and
# docs are excluded — a change confined to them cannot alter
# product behaviour, so the build + E2E lane is skipped.
# ghost/core test paths are listed today; app test paths can be
# added here as their conventions are confirmed.
e2e:
- '!**/*.md'
- '!.devcontainer/**'
- '!.vscode/**'
- '!ghost/core/test/**'
- '!ghost/core/vitest.config.ts'

- name: Define Node test matrix
id: node_matrix
Expand Down Expand Up @@ -175,6 +192,10 @@ jobs:
changed_tinybird: ${{ steps.changed.outputs.tinybird }}
changed_tinybird_datafiles: ${{ steps.changed.outputs.tinybird-datafiles }}
changed_any_code: ${{ steps.changed.outputs.any-code }}
# Single gate for the build + browser-E2E lane. True for tags, or when a
# changed file could affect a running Ghost instance (see the `e2e` path
# filter above). A test-only / docs-only change keeps this false.
run_e2e: ${{ env.IS_TAG == 'true' || steps.changed.outputs.e2e == 'true' }}
is_main: ${{ env.IS_MAIN }}
is_tag: ${{ env.IS_TAG }}
is_development: ${{ env.IS_DEVELOPMENT }}
Expand Down Expand Up @@ -258,7 +279,7 @@ jobs:
NX_BASE: ${{ needs.job_setup.outputs.nx_base }}
NX_HEAD: ${{ env.HEAD_COMMIT }}

- uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
Expand Down Expand Up @@ -322,7 +343,7 @@ jobs:
name: admin-coverage
path: ghost/*/coverage/cobertura-coverage.xml

- uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
Expand Down Expand Up @@ -395,7 +416,7 @@ jobs:
name: unit-coverage
path: ghost/*/coverage/cobertura-coverage.xml

- uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
Expand Down Expand Up @@ -523,7 +544,7 @@ jobs:
ghost/*/coverage-e2e/cobertura-coverage.xml
ghost/*/coverage-integration/cobertura-coverage.xml

- uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
Expand Down Expand Up @@ -609,7 +630,7 @@ jobs:
exit 1
fi

- uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
Expand Down Expand Up @@ -662,7 +683,7 @@ jobs:
path: apps/${{ steps.app_name.outputs.name }}/playwright-report
retention-days: 30

- uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
Expand All @@ -679,7 +700,7 @@ jobs:
working-directory: ghost/core/core/server/data/tinybird
services:
tinybird:
image: tinybirdco/tinybird-local:latest@sha256:39209d37844d613b57d499052341fc457da62cc1770fe0c5efaee94a88e5399a
image: tinybirdco/tinybird-local:latest@sha256:9d9c77fe276920acb56f0405ef36393a4831c3c193b221313157203901cab586
ports:
- 7181:7181
steps:
Expand Down Expand Up @@ -761,7 +782,7 @@ jobs:
run: |
[ -f ~/.ghost/logs/*.log ] && cat ~/.ghost/logs/*.log

- uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
Expand All @@ -771,6 +792,8 @@ jobs:
job_build_artifacts:
name: Build & Publish Artifacts
needs: [job_setup]
# Root of the build + browser-E2E lane (see run_e2e in job_setup).
if: needs.job_setup.outputs.run_e2e == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -1031,6 +1054,8 @@ jobs:
job_build_e2e_public_apps:
name: Build E2E Public App Assets
needs: [job_setup]
# Root of the build + browser-E2E lane (see run_e2e in job_setup).
if: needs.job_setup.outputs.run_e2e == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
Expand Down Expand Up @@ -1071,6 +1096,9 @@ jobs:
job_build_e2e_image:
name: Build E2E Docker Image
needs: [job_setup, job_build_e2e_public_apps, job_build_artifacts]
# Inherits the run_e2e gate transitively — runs only when both upstream
# builds ran and succeeded (they are skipped when run_e2e is false).
if: needs.job_build_artifacts.result == 'success' && needs.job_build_e2e_public_apps.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -1181,6 +1209,8 @@ jobs:
name: E2E Tests (${{ matrix.projectName }} ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
runs-on: ubuntu-latest
needs: [job_build_e2e_image, job_setup]
# Inherits the run_e2e gate transitively via job_build_e2e_image.
if: needs.job_build_e2e_image.result == 'success'
strategy:
fail-fast: true
matrix:
Expand Down Expand Up @@ -1318,7 +1348,7 @@ jobs:
path: e2e/test-results
retention-days: 7

- uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
Expand Down Expand Up @@ -1498,7 +1528,7 @@ jobs:
]
name: Build ${{ matrix.package_name }}
runs-on: ubuntu-latest
if: always() && github.repository == 'TryGhost/Ghost' && needs.job_setup.result == 'success' && needs.job_lint.result == 'success' && needs.job_unit-tests.result == 'success'
if: always() && github.repository == 'TryGhost/Ghost' && needs.job_setup.result == 'success' && needs.job_lint.result == 'success' && needs.job_unit-tests.result == 'success' && needs.job_setup.outputs.run_e2e == 'true'
permissions:
contents: read
strategy:
Expand Down Expand Up @@ -1778,7 +1808,7 @@ jobs:
- name: Publish to npm
run: npm publish ghost-*.tgz --access public

- uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
if: failure()
with:
status: ${{ job.status }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/label-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'TryGhost'
steps:
- uses: tryghost/actions/actions/label-actions@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
- uses: tryghost/actions/actions/label-actions@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:

- name: Notify on failure
if: failure()
uses: tryghost/actions/actions/slack-build@bf96db0e7f57eb048ead40594f6c222b4a7e3e93 # main
uses: tryghost/actions/actions/slack-build@598d6328d89dbd796aa02ae2ea66308f9d942224 # main
with:
status: ${{ job.status }}
env:
Expand Down
10 changes: 9 additions & 1 deletion .secretlintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
{
"rules": [
{
"id": "@secretlint/secretlint-rule-preset-recommend"
"id": "@secretlint/secretlint-rule-preset-recommend",
"rules": [
{
"id": "@secretlint/secretlint-rule-privatekey",
"options": {
"allows": ["/MIICWwIBAAKBgQCea7oriNoFgxnY/"]
}
}
]
},
{
"id": "@secretlint/secretlint-rule-pattern",
Expand Down
2 changes: 1 addition & 1 deletion compose.dev.analytics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ services:
condition: service_completed_successfully

tinybird-local:
image: tinybirdco/tinybird-local:latest@sha256:39209d37844d613b57d499052341fc457da62cc1770fe0c5efaee94a88e5399a
image: tinybirdco/tinybird-local:latest@sha256:9d9c77fe276920acb56f0405ef36393a4831c3c193b221313157203901cab586
container_name: ghost-dev-tinybird
platform: linux/amd64
stop_grace_period: 2s
Expand Down
2 changes: 1 addition & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"dotenv": "catalog:",
"eslint": "catalog:",
"eslint-plugin-no-relative-import-paths": "1.6.1",
"eslint-plugin-playwright": "2.10.1",
"eslint-plugin-playwright": "2.10.2",
"express": "4.21.2",
"knex": "3.1.0",
"mysql2": "3.18.1",
Expand Down
1 change: 1 addition & 0 deletions ghost/core/core/shared/config/overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"audio/wav",
"audio/x-wav",
"audio/ogg",
"application/ogg",
"audio/mp4",
"audio/x-m4a"
]
Expand Down
4 changes: 2 additions & 2 deletions ghost/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"test:all": "pnpm test:unit && pnpm test:integration && pnpm test:e2e && pnpm lint",
"test:debug": "DEBUG=ghost:test* pnpm test",
"test:unit": "c8 pnpm test:unit:${GHOST_UNIT_TEST_VARIANT:-base}",
"test:unit:base": "pnpm test:base ./test/unit/server/services ./test/unit/server/notify.test.js ./test/unit/server/overrides.test.js ./test/unit/server/adapters/scheduling/scheduling-default.test.js --timeout=2000",
"test:unit:base": "pnpm test:base ./test/unit/server/notify.test.js ./test/unit/server/overrides.test.js ./test/unit/server/adapters/scheduling/scheduling-default.test.js --timeout=2000",
"test:unit:ci": "pnpm test:unit:base --reporter=min",
"test:integration": "pnpm test:base './test/integration' --timeout=10000",
"test:e2e": "pnpm test:base ./test/e2e-* --timeout=15000",
Expand Down Expand Up @@ -184,7 +184,7 @@
"heic-convert": "2.1.0",
"html-to-text": "5.1.1",
"html5parser": "2.0.2",
"human-number": "2.0.10",
"human-number": "2.0.12",
"iconv-lite": "0.7.2",
"image-size": "1.2.1",
"intl": "1.2.5",
Expand Down
15 changes: 15 additions & 0 deletions ghost/core/test/e2e-api/admin/media.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ describe('Media API', function () {
media.push(new URL(res.body.media[0].url).pathname);
});

it('Can upload an Ogg audio with application/ogg content type', async function () {
const res = await request.post(localUtils.API.getApiQuery('media/upload'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.field('ref', 'https://ghost.org/sample.ogg')
.attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample.ogg'), {filename: 'sample.ogg', contentType: 'application/ogg'})
.attach('thumbnail', path.join(__dirname, '/../../utils/fixtures/images/ghost-logo.png'))
.expect(201);

assert.match(new URL(res.body.media[0].url).pathname, /\/content\/media\/\d+\/\d+\/sample\.ogg/);
assert.equal(res.body.media[0].ref, 'https://ghost.org/sample.ogg');

media.push(new URL(res.body.media[0].url).pathname);
});

it('Can upload an mp3', async function () {
const res = await request.post(localUtils.API.getApiQuery('media/upload'))
.set('Origin', config.get('url'))
Expand Down
44 changes: 43 additions & 1 deletion ghost/core/test/e2e-api/admin/members.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1059,14 +1059,50 @@ describe('Members API', function () {

await DomainEvents.allSettled();

// Crossing the admin threshold must flip email verification on.
//
// The verification trigger and the members_created_events writer are
// two independent MemberCreatedEvent subscribers (VerificationTrigger
// and EventStorage) with no ordering guarantee between them. When the
// trigger's count query beats EventStorage's insert, it misses the
// row for the member that just crossed the threshold, undercounts by
// one, and fires one member creation late instead of on the boundary.
// This is a known, low-impact off-by-one with no user-facing effect
// (the next member creation re-counts and triggers). See BER-3507.
//
// To stay deterministic the test adds a second member past the
// boundary: by the time its event is handled the two earlier members'
// rows are guaranteed committed, so the trigger reliably counts past
// the threshold however the race landed. This member only needs to
// emit a MemberCreatedEvent, so it skips the response-body snapshot.
//
// TODO: once the trigger no longer depends on a sibling subscriber's
// write (e.g. it counts signup events excluding the current member,
// then adds a deterministic +1), restore the precise assertion:
// create exactly one member past the threshold, assert verification
// triggered on that member, and assert the webhook reported
// amountTriggered === 2.
const {body: recoveryMemberBody} = await agent
.post('/members/')
.body({members: [{
name: 'fail webhook verification recovery',
email: 'memberFailWebhookVerificationRecovery@test.com'
}]})
.expectStatus(201);
const recoveryMember = recoveryMemberBody.members[0];

await DomainEvents.allSettled();

assert.equal(settingsCache.get('email_verification_required'), true, 'After exceeding limit: Email verification should be required');

emailMockReceiver.assertSentEmailCount(0, 'No verification email to be sent when webhook verification is enabled');

// Verification triggers at most once: once email_verification_required
// is set, later member creations short-circuit, so exactly one webhook
// is sent regardless of which member crossed the boundary.
const matchingRequests = receivedWebhookRequests.filter((request) => {
return request.body.type === 'mock_verification_event' &&
request.body.siteId === '1' &&
request.body.amountTriggered === 2 &&
request.body.threshold === 1 &&
request.body.method === 'admin';
});
Expand All @@ -1075,6 +1111,11 @@ describe('Members API', function () {

const matchingRequest = matchingRequests[0];

// amountTriggered is the member count observed when verification fired.
// It is past the threshold (1); the exact value (2 or 3) depends on the
// off-by-one race described above.
assert.ok(matchingRequest.body.amountTriggered >= 2, 'Expected the webhook to report a member count past the threshold');

const requestTimestamp = Array.isArray(matchingRequest.headers['x-ghost-request-timestamp']) ?
matchingRequest.headers['x-ghost-request-timestamp'][0] :
matchingRequest.headers['x-ghost-request-timestamp'];
Expand All @@ -1090,6 +1131,7 @@ describe('Members API', function () {

await agent.delete(`/members/${passVerificationMember.id}`);
await agent.delete(`/members/${triggerVerificationMember.id}`);
await agent.delete(`/members/${recoveryMember.id}`);
});
});

Expand Down
3 changes: 2 additions & 1 deletion ghost/core/test/unit/server/data/importer/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe('Importer', function () {

it('gets the correct types', function () {
assert(Array.isArray(ImportManager.getContentTypes()));
assert.equal(ImportManager.getContentTypes().length, 23);
assert.equal(ImportManager.getContentTypes().length, 24);
assert(ImportManager.getContentTypes().includes('image/jpeg'));
assert(ImportManager.getContentTypes().includes('image/png'));
assert(ImportManager.getContentTypes().includes('image/gif'));
Expand All @@ -89,6 +89,7 @@ describe('Importer', function () {
assert(ImportManager.getContentTypes().includes('audio/wav'));
assert(ImportManager.getContentTypes().includes('audio/x-wav'));
assert(ImportManager.getContentTypes().includes('audio/ogg'));
assert(ImportManager.getContentTypes().includes('application/ogg'));
assert(ImportManager.getContentTypes().includes('audio/x-m4a'));

assert(ImportManager.getContentTypes().includes('application/octet-stream'));
Expand Down
Loading