Added Dev Container setup (#21279)

no issue

- Dev Containers let you work on Ghost in a consistent, isolated
environment with all the necessary development dependencies
pre-installed. VSCode (or Cursor) can effectively run _inside_ the
container, providing a local quality development environment while
working in a well-defined, isolated environment.
- For now the default setup only works with "Clone repository in
Container Volume" or "Clone PR in Container Volume" — this allows for a
super quick and simple setup. We can also introduce another
configuration to allow opening an existing local checkout in a Dev
Container, but that's not quite ready yet.
- This PR also added the `yarn clean:hard` command which: deletes all
node_modules, cleans the yarn cache, and cleans the NX cache. This will
be necessary for opening a local checkout in a Dev Container.
- To learn more about Dev Containers, read this guide from VSCode:
https://code.visualstudio.com/docs/devcontainers/containers#_personalizing-with-dotfile-repositories

---------

Co-authored-by: Joe Grigg <joe@ghost.org>
Co-authored-by: Steve Larson <9larsons@gmail.com>
This commit is contained in:
Chris Raible 2024-10-24 11:15:08 -07:00 committed by GitHub
parent 3f9fd2ad3f
commit af0f26c75f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 616 additions and 11 deletions

View File

@ -0,0 +1,64 @@
ARG NODE_VERSION=20.15.1
## Base Image used for all targets
FROM node:$NODE_VERSION-bullseye-slim AS base
RUN apt-get update && \
apt-get install -y \
build-essential \
curl \
jq \
libjemalloc2 \
python3 \
tar
# Base DevContainer: for use in a Dev Container where your local code is mounted into the container
### Adding code and installing dependencies gets overridden by your local code/dependencies, so this is done in onCreateCommand
FROM base AS base-devcontainer
# Install Stripe CLI, zsh, playwright
RUN curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | tee /usr/share/keyrings/stripe.gpg && \
echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | tee -a /etc/apt/sources.list.d/stripe.list && \
apt update && \
apt install -y \
git \
stripe \
zsh \
default-mysql-client && \
npx -y playwright@1.46.1 install --with-deps
ENV NX_DAEMON=true
ENV YARN_CACHE_FOLDER=/workspaces/ghost/.yarncache
EXPOSE 2368
EXPOSE 4200
EXPOSE 4173
EXPOSE 41730
EXPOSE 4175
EXPOSE 4176
EXPOSE 4177
EXPOSE 4178
EXPOSE 6174
EXPOSE 7173
EXPOSE 7174
# Full Devcontainer Stage: Add the code and install dependencies
### This is a full devcontainer with all the code and dependencies installed
### Useful in an environment like Github Codespaces where you don't mount your local code into the container
FROM base-devcontainer AS full-devcontainer
WORKDIR /workspaces/ghost
COPY ../../ .
RUN yarn install --frozen-lockfile --prefer-offline --cache-folder $YARN_CACHE_FOLDER
# Development Stage: alternative entrypoint for development with some caching optimizations
FROM base-devcontainer AS development
WORKDIR /workspaces/ghost
COPY ../../ .
RUN yarn install --frozen-lockfile --prefer-offline --cache-folder $YARN_CACHE_FOLDER && \
cp -r .yarncache .yarncachecopy && \
rm -Rf .yarncachecopy/.tmp && \
yarn cache clean
ENTRYPOINT ["./.devcontainer/.docker/development.entrypoint.sh"]
CMD ["yarn", "dev"]

View File

@ -0,0 +1,13 @@
# For use in a Dev Container where your local code is mounted into the container
name: ghost-devcontainer
services:
ghost:
image: ghost-base-devcontainer
build:
target: base-devcontainer
command: ["sleep", "infinity"]
environment:
- DEVCONTAINER=true
- DISPLAY=host.docker.internal:0
volumes:
- /tmp/.X11-unix:/tmp/.X11-unix

View File

@ -0,0 +1,47 @@
# Base container and services for running Ghost
## Intended to be extended by another compose file
## e.g. docker compose -f base.compose.yml -f development.compose.yml up
## Does not include development dependencies, Ghost code, or any other dependencies
name: ghost-base
services:
ghost:
image: ghost-base
build:
context: ../../
dockerfile: .devcontainer/.docker/Dockerfile
target: base
pull_policy: never
tty: true
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
mysql:
image: mysql:8.0.35
# We'll need to look into how we can further fine tune the memory usage/performance here
command: --innodb-buffer-pool-size=1G --innodb-log-buffer-size=500M --innodb-change-buffer-max-size=50 --innodb-flush-log-at-trx_commit=0 --innodb-flush-method=O_DIRECT
ports:
- "3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ghost
restart: always
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: "mysql -uroot -proot ghost -e 'select 1'"
interval: 1s
retries: 120
redis:
image: redis:7.0
ports:
- "6379"
restart: always
healthcheck:
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
interval: 1s
retries: 120
volumes:
mysql-data:

View File

@ -0,0 +1,38 @@
# Development container with Ghost code and dependencies pre-installed
## Watches your local filesystem and syncs changes to the container
## Intended for use with raw docker compose commands
name: ghost-development
services:
ghost:
image: ghost-development
build:
target: development
command: ["yarn", "dev"]
volumes:
- ../../.yarncache:/workspaces/ghost/.yarncache
develop:
watch:
- path: ../../
action: sync
target: /workspaces/ghost
ignore:
- node_modules/
- .yarncache/
- path: yarn.lock
action: rebuild
ports:
- 2368:2368
- 4200:4200
- 4173:4173
- 41730:41730
- 4175:4175
- 4176:4176
- 4177:4177
- 4178:4178
- 6174:6174
- 7173:7173
- 7174:7174
- 9174:9174
environment:
- DEBUG=${DEBUG:-}
- APP_FLAGS=${APP_FLAGS:-}

View File

@ -0,0 +1,7 @@
#!/bin/bash
if [ -z "$(ls -A ".yarncache")" ]; then
cp -r /workspaces/ghost/.yarncachecopy/* /workspaces/ghost/.yarncache/
fi
exec "$@"

View File

@ -0,0 +1,26 @@
# Dev Container with all Ghost code and dependencies pre-installed
name: ghost-full-devcontainer
services:
ghost:
image: ghost-full-devcontainer
build:
target: full-devcontainer
command: ["sleep", "infinity"]
environment:
- DEVCONTAINER=true
- DISPLAY=host.docker.internal:0
volumes:
- /tmp/.X11-unix:/tmp/.X11-unix
ports:
- 2368:2368
- 4200:4200
- 4173:4173
- 41730:41730
- 4175:4175
- 4176:4176
- 4177:4177
- 4178:4178
- 6174:6174
- 7173:7173
- 7174:7174
- 9174:9174

24
.devcontainer/README.md Normal file
View File

@ -0,0 +1,24 @@
# Dev Container Setup
## devcontainer.json
This file contains the configuration for the dev container. It is used to define the setup of the container, including things like port bindings, environment variables, and other dev container specific features.
It points to a docker compose file in the `.devcontainer/.docker` directory, which in turn relies on a Dockerfile in the same directory.
## Dockerfile
The Dockerfile in this directory uses a multi-stage build to allow for multiple types of builds without duplicating code and ensuring maximum consistency. The following targets are available:
- `base`: The bare minimum base image used to build and run Ghost. Includes the operating system, node, and some build dependencies, but does not include any Ghost code or dependencies.
- `base-devcontainer`: everything from `base`, plus additional development dependencies like the stripe-cli and playwright. No code or node dependencies.
- `full-devcontainer`: everything from `base-devcontainer`, plus Ghost's code and all node dependencies
- `development`: an alternative to `full-devcontainer` intended for manual development e.g. with docker compose. Add Ghost's code and installs dependencies with some optimizations for the yarn cache
## Docker Compose
Similarly, the docker compose configuration relies on merging compose files to create the final configuration. The `base.compose.yml` file contains the bare minimum configuration, and can be extended by specifying additional services or modifying the existing ones by supplying additional compose files. For example, to run the `development.compose.yml` file, you would use the following command:
```
docker compose -f .devcontainer/docker/base.compose.yml -f .devcontainer/docker/development.compose.yml up
```
There is an alias `yarn compose` script in the top level `package.json` which points to the appropriate `compose.yml` files for local development.
This setup gives us the flexibility to create multiple different docker compose configurations, while ensuring a base level of consistency across configurations.

View File

@ -0,0 +1,92 @@
const fs = require('fs');
const path = require('path');
const assert = require('node:assert/strict');
// Reads the config.local.json file and updates it with environments variables for devcontainer setup
const configBasePath = path.join(__dirname, '..', 'ghost', 'core');
const configFile = path.join(configBasePath, 'config.local.json');
let originalConfig = {};
if (fs.existsSync(configFile)) {
try {
// Backup the user's config.local.json file just in case
// This won't be used by Ghost but can be useful to switch back to local development
const backupFile = path.join(configBasePath, 'config.local-backup.json');
fs.copyFileSync(configFile, backupFile);
// Read the current config.local.json file into memory
const fileContent = fs.readFileSync(configFile, 'utf8');
originalConfig = JSON.parse(fileContent);
} catch (error) {
console.error('Error reading or parsing config file:', error);
process.exit(1);
}
} else {
console.log('Config file does not exist. Creating a new one.');
}
let newConfig = {};
// Change the url if we're in a codespace
if (process.env.CODESPACES === 'true') {
assert.ok(process.env.CODESPACE_NAME, 'CODESPACE_NAME is not defined');
assert.ok(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN, 'GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN is not defined');
const url = `https://${process.env.CODESPACE_NAME}-2368.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
newConfig.url = url;
}
newConfig.database = {
client: 'mysql2',
connection: {
host: 'mysql',
user: 'root',
password: 'root',
database: 'ghost'
}
}
newConfig.adapters = {
Redis: {
host: 'redis',
port: 6379
}
}
// Only update the mail settings if they aren't already set
if (!originalConfig.mail && process.env.MAILGUN_SMTP_PASS && process.env.MAILGUN_SMTP_USER) {
newConfig.mail = {
transport: 'SMTP',
options: {
service: 'Mailgun',
host: 'smtp.mailgun.org',
secure: true,
port: 465,
auth: {
user: process.env.MAILGUN_SMTP_USER,
pass: process.env.MAILGUN_SMTP_PASS
}
}
}
}
// Only update the bulk email settings if they aren't already set
if (!originalConfig.bulkEmail && process.env.MAILGUN_API_KEY && process.env.MAILGUN_DOMAIN) {
newConfig.bulkEmail = {
mailgun: {
baseUrl: 'https://api.mailgun.net/v3',
apiKey: process.env.MAILGUN_API_KEY,
domain: process.env.MAILGUN_DOMAIN,
tag: 'bulk-email'
}
}
}
// Merge the original config with the new config
const config = {...originalConfig, ...newConfig};
// Write the updated config.local.json file
try {
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
console.log('Config file updated successfully.');
} catch (error) {
console.error('Error writing config file:', error);
process.exit(1);
}

View File

@ -0,0 +1,139 @@
{
"name": "Ghost Local DevContainer",
"dockerComposeFile": ["./.docker/base.compose.yml", "./.docker/base-devcontainer.compose.yml"],
"service": "ghost",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"shutdownAction": "stopCompose",
"onCreateCommand": ["./.devcontainer/onCreateCommand.sh"],
"updateContentCommand": ["git", "submodule", "update", "--init", "--recursive"],
"postCreateCommand": ["yarn", "knex-migrator", "init"],
"remoteEnv": {
"STRIPE_SECRET_KEY": "${localEnv:STRIPE_SECRET_KEY}",
"STRIPE_API_KEY": "${localEnv:STRIPE_SECRET_KEY}",
"STRIPE_PUBLISHABLE_KEY": "${localEnv:STRIPE_PUBLISHABLE_KEY}",
"STRIPE_ACCOUNT_ID": "${localEnv:STRIPE_ACCOUNT_ID}",
"MAILGUN_SMTP_USER": "${localEnv:MAILGUN_SMTP_USER}",
"MAILGUN_SMTP_PASS": "${localEnv:MAILGUN_SMTP_PASS}",
"MAILGUN_API_KEY": "${localEnv:MAILGUN_API_KEY}",
"MAILGUN_DOMAIN": "${localEnv:MAILGUN_DOMAIN}"
},
"forwardPorts": [2368,4200],
"portsAttributes": {
"80": {
"onAutoForward": "ignore"
},
"2368": {
"label": "Ghost"
},
"2369": {
"label": "Ghost (Test Server)",
"onAutoForward": "silent"
},
"2370": {
"label": "Ghost (Test Server)",
"onAutoForward": "silent"
},
"2371": {
"label": "Ghost (Test Server)",
"onAutoForward": "silent"
},
"2372": {
"label": "Ghost (Test Server)",
"onAutoForward": "silent"
},
"2373": {
"label": "Ghost (Test Server)",
"onAutoForward": "silent"
},
"4200": {
"label": "Admin",
"onAutoForward": "silent"
},
"4201": {
"label": "Admin Live Reload",
"onAutoForward": "silent"
},
"4175": {
"label": "Portal",
},
"4176": {
"label": "Portal (HTTPS)",
"protocol": "https"
},
"4177": {
"label": "Announcement Bar"
},
"4178": {
"label": "Search"
},
"4173": {
"label": "Lexical"
},
"41730": {
"label": "Lexical (HTTPS)",
"protocol": "https"
},
"6174": {
"label": "Signup Form",
"onAutoForward": "silent"
},
"7173": {
"label": "Comments"
},
"7174": {
"label": "Comments (HTTPS)",
"protocol": "https"
},
"9174": {
"label": "Prometheus Metrics Exporter",
"onAutoForward": "silent"
},
"5173": {
"onAutoForward": "silent"
},
"5368": {
"onAutoForward": "silent"
}
},
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": { "zsh": { "path": "/bin/zsh" } }
},
"extensions": [
"ms-azuretools.vscode-docker"
]
}
},
"secrets": {
"STRIPE_SECRET_KEY": {
"description": "Your Stripe account's test secret API key",
"documentationUrl": "https://dashboard.stripe.com/test/apikeys"
},
"STRIPE_PUBLISHABLE_KEY": {
"description": "Your Stripe account's test publishable key",
"documentationUrl": "https://dashboard.stripe.com/test/apikeys"
},
"STRIPE_ACCOUNT_ID": {
"description": "Your Stripe Account ID",
"documentationUrl": "https://dashboard.stripe.com/settings/account"
},
"MAILGUN_SMTP_USER": {
"description": "Your Mailgun account's SMTP username, e.g. postmaster@sandbox1234567890.mailgun.org. You can find this in the Mailgun dashboard under Sending -> Domains -> Select your domain -> SMTP.",
"documentationUrl": "https://app.mailgun.com/mg/sending/domains"
},
"MAILGUN_SMTP_PASS": {
"description": "Your Mailgun account's SMTP password",
"documentationUrl": "https://app.mailgun.com/mg/sending/domains"
},
"MAILGUN_API_KEY": {
"description": "Your Mailgun account's API key",
"documentationUrl": ""
},
"MAILGUN_DOMAIN": {
"description": "Your Mailgun account's domain, e.g. sandbox1234567890.mailgun.org",
"documentationUrl": ""
}
}
}

View File

@ -0,0 +1,18 @@
#!/bin/bash
set -e
echo "Setting up local config file..."
node .devcontainer/createLocalConfig.js
echo "Cleaning up any previous installs..."
yarn clean:hard
echo "Installing dependencies..."
yarn install
echo "Updating git submodules..."
git submodule update --init --recursive
echo "Building typescript packages..."
yarn nx run-many -t build:ts

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules/
.nxcache
.nx/cache
.nx/workspace-data
**/*.log

26
.github/scripts/clean.sh vendored Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
set -e
# Clean yarn cache
echo "Cleaning yarn cache..."
if [ "$DEVCONTAINER" = "true" ]; then
# In devcontainer, these directories are mounted from the host so we can't delete them — only their contents
rm -rf .yarncache/* .yarncachecopy/*
else
yarn cache clean
fi
# Reset Nx
echo "Resetting NX cache..."
rm -rf .nxcache .nx
# Recursively delete all node_modules directories
echo "Deleting all node_modules directories..."
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
echo "Deleting all build artifacts..."
find ./ghost -type d -name "build" -exec rm -rf '{}' +
find ./ghost -type f -name "tsconfig.tsbuildinfo" -delete
echo "Cleanup complete!"

View File

@ -36,6 +36,11 @@ async function runAndStream(command, args, options) {
return; return;
} }
if (process.env.DEVCONTAINER === 'true') {
console.log(chalk.yellow(`Devcontainer detected, skipping setup`));
return;
}
const coreFolder = path.join(__dirname, '../../ghost/core'); const coreFolder = path.join(__dirname, '../../ghost/core');
const rootFolder = path.join(__dirname, '../..'); const rootFolder = path.join(__dirname, '../..');
const config = require('../../ghost/core/core/shared/config/loader').loadNconf({ const config = require('../../ghost/core/core/shared/config/loader').loadNconf({

5
.gitignore vendored
View File

@ -128,6 +128,7 @@ Caddyfile
# Playwright state with cookies it keeps across tests # Playwright state with cookies it keeps across tests
/ghost/core/playwright-state.json /ghost/core/playwright-state.json
/ghost/core/playwright-report
# Admin # Admin
/ghost/admin/dist /ghost/admin/dist
@ -174,3 +175,7 @@ tsconfig.tsbuildinfo
.tinyb .tinyb
.venv .venv
.diff_tmp .diff_tmp
# Docker Yarn Cache
.yarncache
.yarncachecopy

View File

@ -17,6 +17,7 @@ export default defineConfig((config) => {
'process.env.NODE_ENV': JSON.stringify(config.mode) 'process.env.NODE_ENV': JSON.stringify(config.mode)
}, },
preview: { preview: {
host: '0.0.0.0',
port: 4177 port: 4177
}, },
plugins: [ plugins: [

View File

@ -20,6 +20,7 @@ export default (function viteConfig() {
'process.env.VITEST_SEGFAULT_RETRY': 3 'process.env.VITEST_SEGFAULT_RETRY': 3
}, },
preview: { preview: {
host: '0.0.0.0',
port: 7173 port: 7173
}, },
server: { server: {

View File

@ -21,6 +21,7 @@ export default defineConfig((config) => {
REACT_APP_VERSION: JSON.stringify(process.env.npm_package_version) REACT_APP_VERSION: JSON.stringify(process.env.npm_package_version)
}, },
preview: { preview: {
host: '0.0.0.0',
port: 4175 port: 4175
}, },
server: { server: {

View File

@ -20,6 +20,7 @@ export default (function viteConfig() {
'process.env.VITEST_SEGFAULT_RETRY': 3 'process.env.VITEST_SEGFAULT_RETRY': 3
}, },
preview: { preview: {
host: '0.0.0.0',
port: 6174 port: 6174
}, },
build: { build: {

View File

@ -17,6 +17,7 @@ export default defineConfig((config) => {
'process.env.NODE_ENV': JSON.stringify(config.mode) 'process.env.NODE_ENV': JSON.stringify(config.mode)
}, },
preview: { preview: {
host: '0.0.0.0',
port: 4178 port: 4178
}, },
plugins: [ plugins: [

3
ghost/core/nodemon.json Normal file
View File

@ -0,0 +1,3 @@
{
"ignore": ["*.test.js", "**/content/public/**", "**/core/built/**", ".yarncache/**"]
}

View File

@ -21,7 +21,7 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"archive": "npm pack", "archive": "npm pack",
"dev": "node --watch index.js", "dev": "nodemon index.js",
"build:assets": "postcss core/frontend/public/ghost.css --no-map --use cssnano -o core/frontend/public/ghost.min.css", "build:assets": "postcss core/frontend/public/ghost.css --no-map --use cssnano -o core/frontend/public/ghost.min.css",
"test": "yarn test:unit", "test": "yarn test:unit",
"test:base": "mocha --reporter dot --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js", "test:base": "mocha --reporter dot --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js",
@ -250,6 +250,7 @@
"mocha-slow-test-reporter": "0.1.2", "mocha-slow-test-reporter": "0.1.2",
"mock-knex": "TryGhost/mock-knex#d8b93b1c20d4820323477f2c60db016ab3e73192", "mock-knex": "TryGhost/mock-knex#d8b93b1c20d4820323477f2c60db016ab3e73192",
"nock": "13.3.3", "nock": "13.3.3",
"nodemon": "^3.1.7",
"papaparse": "5.3.2", "papaparse": "5.3.2",
"parse-prometheus-text-format": "1.1.1", "parse-prometheus-text-format": "1.1.1",
"postcss": "8.4.39", "postcss": "8.4.39",
@ -319,17 +320,20 @@
}, },
"test:browser": { "test:browser": {
"dependsOn": [ "dependsOn": [
"^build:ts" "^build:ts",
"ghost-admin:build"
] ]
}, },
"test:browser:admin": { "test:browser:admin": {
"dependsOn": [ "dependsOn": [
"^build:ts" "^build:ts",
"ghost-admin:build"
] ]
}, },
"test:browser:portal": { "test:browser:portal": {
"dependsOn": [ "dependsOn": [
"^build:ts" "^build:ts",
"ghost-admin:build"
] ]
}, },
"test:e2e": { "test:e2e": {

View File

@ -1,4 +1,24 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const os = require('os');
const getWorkerCount = () => {
if (process.env.CI) {
return '100%';
}
if (process.env.PLAYWRIGHT_SLOWMO) {
return 1;
}
let cpuCount;
try {
cpuCount = os.cpus().length;
} catch (err) {
cpuCount = 1;
}
// Stripe limits to 5 new accounts per second
// If we go higher than 5, we'll get rate limited and tests will fail
return Math.min(5, cpuCount - 1);
};
const config = { const config = {
timeout: 75 * 1000, timeout: 75 * 1000,
@ -7,7 +27,7 @@ const config = {
}, },
// save trace on fail // save trace on fail
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? '100%' : (process.env.PLAYWRIGHT_SLOWMO ? 1 : undefined), workers: getWorkerCount(),
reporter: process.env.CI ? [['list', {printSteps: true}], ['html']] : [['list', {printSteps: true}]], reporter: process.env.CI ? [['list', {printSteps: true}], ['html']] : [['list', {printSteps: true}]],
use: { use: {
trace: 'retain-on-failure', trace: 'retain-on-failure',

View File

@ -363,12 +363,13 @@ test.describe('Publishing', () => {
await sharedPage.goto('/ghost'); await sharedPage.goto('/ghost');
await createPostDraft(sharedPage, postData); await createPostDraft(sharedPage, postData);
const editorUrl = await sharedPage.url();
// Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future) // Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future)
await publishPost(sharedPage, {time: '00:00', type: 'publish+send'}); await publishPost(sharedPage, {time: '00:00', type: 'publish+send'});
await closePublishFlow(sharedPage); await closePublishFlow(sharedPage);
await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be published and sent'); // Member count can differ, hence not included here await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be published and sent'); // Member count can differ, hence not included here
await checkPostStatus(sharedPage, 'Scheduled', 'in a few seconds'); // Extra test for suffix on hover await checkPostStatus(sharedPage, 'Scheduled', 'in a few seconds'); // Extra test for suffix on hover
const editorUrl = await sharedPage.url();
// Go to the homepage and check if the post is not yet visible there // Go to the homepage and check if the post is not yet visible there
await checkPostNotPublished(sharedPage, postData); await checkPostNotPublished(sharedPage, postData);
@ -394,11 +395,11 @@ test.describe('Publishing', () => {
await sharedPage.goto('/ghost'); await sharedPage.goto('/ghost');
await createPostDraft(sharedPage, postData); await createPostDraft(sharedPage, postData);
const editorUrl = await sharedPage.url();
// Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future) // Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future)
await publishPost(sharedPage, {time: '00:00'}); await publishPost(sharedPage, {time: '00:00'});
await closePublishFlow(sharedPage); await closePublishFlow(sharedPage);
await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be published in a few seconds'); await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be published in a few seconds');
const editorUrl = await sharedPage.url();
// Check not published yet // Check not published yet
await checkPostNotPublished(sharedPage, postData); await checkPostNotPublished(sharedPage, postData);
@ -425,12 +426,12 @@ test.describe('Publishing', () => {
await sharedPage.goto('/ghost'); await sharedPage.goto('/ghost');
await createPostDraft(sharedPage, postData); await createPostDraft(sharedPage, postData);
const editorUrl = await sharedPage.url();
// Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future) // Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future)
await publishPost(sharedPage, {type: 'send', time: '00:00'}); await publishPost(sharedPage, {type: 'send', time: '00:00'});
await closePublishFlow(sharedPage); await closePublishFlow(sharedPage);
await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be sent in a few seconds'); await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be sent in a few seconds');
const editorUrl = await sharedPage.url();
// Check not published yet // Check not published yet
await checkPostNotPublished(sharedPage, postData); await checkPostNotPublished(sharedPage, postData);

View File

@ -23,6 +23,7 @@
"archive": "nx run ghost:archive", "archive": "nx run ghost:archive",
"build": "nx run-many -t build", "build": "nx run-many -t build",
"build:clean": "nx reset && rimraf -g 'ghost/*/build' && rimraf -g 'ghost/*/tsconfig.tsbuildinfo'", "build:clean": "nx reset && rimraf -g 'ghost/*/build' && rimraf -g 'ghost/*/tsconfig.tsbuildinfo'",
"clean:hard": "./.github/scripts/clean.sh",
"dev:debug": "DEBUG_COLORS=true DEBUG=@tryghost*,ghost:* yarn dev", "dev:debug": "DEBUG_COLORS=true DEBUG=@tryghost*,ghost:* yarn dev",
"dev:admin": "node .github/scripts/dev.js --admin", "dev:admin": "node .github/scripts/dev.js --admin",
"dev:ghost": "node .github/scripts/dev.js --ghost", "dev:ghost": "node .github/scripts/dev.js --ghost",
@ -34,6 +35,8 @@
"reset:data:empty": "cd ghost/core && node index.js generate-data --clear-database --quantities members:0,posts:0 --seed 123", "reset:data:empty": "cd ghost/core && node index.js generate-data --clear-database --quantities members:0,posts:0 --seed 123",
"reset:data:xxl": "cd ghost/core && node index.js generate-data --clear-database --quantities members:2000000,posts:0,emails:0,members_stripe_customers:0,members_login_events:0,members_status_events:0 --seed 123", "reset:data:xxl": "cd ghost/core && node index.js generate-data --clear-database --quantities members:2000000,posts:0,emails:0,members_stripe_customers:0,members_login_events:0,members_status_events:0 --seed 123",
"docker:reset": "docker-compose -f .github/scripts/docker-compose.yml down -v && docker-compose -f .github/scripts/docker-compose.yml up -d --wait", "docker:reset": "docker-compose -f .github/scripts/docker-compose.yml down -v && docker-compose -f .github/scripts/docker-compose.yml up -d --wait",
"docker:down": "docker-compose -f .github/scripts/docker-compose.yml down",
"compose": "docker compose -f .devcontainer/.docker/base.compose.yml -f .devcontainer/.docker/development.compose.yml",
"lint": "nx run-many -t lint", "lint": "nx run-many -t lint",
"test": "nx run-many -t test", "test": "nx run-many -t test",
"test:unit": "nx run-many -t test:unit", "test:unit": "nx run-many -t test:unit",

View File

@ -12484,6 +12484,21 @@ chokidar@^2.1.8:
optionalDependencies: optionalDependencies:
fsevents "^1.2.7" fsevents "^1.2.7"
chokidar@^3.5.2:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
chownr@^1.1.1: chownr@^1.1.1:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
@ -13875,7 +13890,7 @@ debug@3.2.7, debug@^3.0.1, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@~4.3.1, debug@~4.3.2, debug@~4.3.6: debug@4, debug@^4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@~4.3.1, debug@~4.3.2, debug@~4.3.6:
version "4.3.7" version "4.3.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
@ -19407,6 +19422,11 @@ iferr@^0.1.5:
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA== integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
ignore@5.3.2, ignore@^5.0.4, ignore@^5.1.1, ignore@^5.2.0, ignore@^5.2.4: ignore@5.3.2, ignore@^5.0.4, ignore@^5.1.1, ignore@^5.2.0, ignore@^5.2.4:
version "5.3.2" version "5.3.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
@ -24033,6 +24053,22 @@ nodemailer@6.9.15, nodemailer@^6.6.3:
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.15.tgz#57b79dc522be27e0e47ac16cc860aa0673e62e04" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.15.tgz#57b79dc522be27e0e47ac16cc860aa0673e62e04"
integrity sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ== integrity sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==
nodemon@^3.1.7:
version "3.1.7"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.7.tgz#07cb1f455f8bece6a499e0d72b5e029485521a54"
integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==
dependencies:
chokidar "^3.5.2"
debug "^4"
ignore-by-default "^1.0.1"
minimatch "^3.1.2"
pstree.remy "^1.1.8"
semver "^7.5.3"
simple-update-notifier "^2.0.0"
supports-color "^5.5.0"
touch "^3.1.0"
undefsafe "^2.0.5"
nopt@^3.0.6: nopt@^3.0.6:
version "3.0.6" version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@ -26560,6 +26596,11 @@ psl@^1.1.28, psl@^1.1.33:
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
pstree.remy@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
public-encrypt@^4.0.0: public-encrypt@^4.0.0:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@ -28401,6 +28442,13 @@ simple-swizzle@^0.2.2:
dependencies: dependencies:
is-arrayish "^0.3.1" is-arrayish "^0.3.1"
simple-update-notifier@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb"
integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==
dependencies:
semver "^7.5.3"
sinon-chai@4.0.0: sinon-chai@4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-4.0.0.tgz#77d59d9f4a833f0d3a88249b4637acc72656fdfa" resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-4.0.0.tgz#77d59d9f4a833f0d3a88249b4637acc72656fdfa"
@ -29491,7 +29539,7 @@ supports-color@^2.0.0:
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==
supports-color@^5.3.0: supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0" version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
@ -30177,6 +30225,11 @@ totalist@^3.0.0:
resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.0.tgz#4ef9c58c5f095255cdc3ff2a0a55091c57a3a1bd" resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.0.tgz#4ef9c58c5f095255cdc3ff2a0a55091c57a3a1bd"
integrity sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw== integrity sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==
touch@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694"
integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==
tough-cookie@4.1.4, tough-cookie@^4.0.0, tough-cookie@^4.1.4: tough-cookie@4.1.4, tough-cookie@^4.0.0, tough-cookie@^4.1.4:
version "4.1.4" version "4.1.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
@ -30545,6 +30598,11 @@ unc-path-regex@^0.1.2:
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==
undefsafe@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
underscore.string@^3.2.2, underscore.string@~3.3.4: underscore.string@^3.2.2, underscore.string@~3.3.4:
version "3.3.6" version "3.3.6"
resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.6.tgz#ad8cf23d7423cb3b53b898476117588f4e2f9159" resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.6.tgz#ad8cf23d7423cb3b53b898476117588f4e2f9159"