diff --git a/.devcontainer/.docker/Dockerfile b/.devcontainer/.docker/Dockerfile new file mode 100644 index 0000000000..1c41e3c2a5 --- /dev/null +++ b/.devcontainer/.docker/Dockerfile @@ -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"] diff --git a/.devcontainer/.docker/base-devcontainer.compose.yml b/.devcontainer/.docker/base-devcontainer.compose.yml new file mode 100644 index 0000000000..dc37474ce2 --- /dev/null +++ b/.devcontainer/.docker/base-devcontainer.compose.yml @@ -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 \ No newline at end of file diff --git a/.devcontainer/.docker/base.compose.yml b/.devcontainer/.docker/base.compose.yml new file mode 100644 index 0000000000..3f7839508e --- /dev/null +++ b/.devcontainer/.docker/base.compose.yml @@ -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: \ No newline at end of file diff --git a/.devcontainer/.docker/development.compose.yml b/.devcontainer/.docker/development.compose.yml new file mode 100644 index 0000000000..aff0810ddc --- /dev/null +++ b/.devcontainer/.docker/development.compose.yml @@ -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:-} \ No newline at end of file diff --git a/.devcontainer/.docker/development.entrypoint.sh b/.devcontainer/.docker/development.entrypoint.sh new file mode 100755 index 0000000000..55af95b5d0 --- /dev/null +++ b/.devcontainer/.docker/development.entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ -z "$(ls -A ".yarncache")" ]; then + cp -r /workspaces/ghost/.yarncachecopy/* /workspaces/ghost/.yarncache/ +fi + +exec "$@" diff --git a/.devcontainer/.docker/full-devcontainer.compose.yml b/.devcontainer/.docker/full-devcontainer.compose.yml new file mode 100644 index 0000000000..3a816f5be0 --- /dev/null +++ b/.devcontainer/.docker/full-devcontainer.compose.yml @@ -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 \ No newline at end of file diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 0000000000..71ea9228f1 --- /dev/null +++ b/.devcontainer/README.md @@ -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. diff --git a/.devcontainer/createLocalConfig.js b/.devcontainer/createLocalConfig.js new file mode 100644 index 0000000000..49c3098a58 --- /dev/null +++ b/.devcontainer/createLocalConfig.js @@ -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); +} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..634b0769dc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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": "" + } + } +} diff --git a/.devcontainer/onCreateCommand.sh b/.devcontainer/onCreateCommand.sh new file mode 100755 index 0000000000..a227848dc5 --- /dev/null +++ b/.devcontainer/onCreateCommand.sh @@ -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 \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..3404dc1677 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules/ + +.nxcache +.nx/cache +.nx/workspace-data + +**/*.log \ No newline at end of file diff --git a/.github/scripts/clean.sh b/.github/scripts/clean.sh new file mode 100755 index 0000000000..87b17f7483 --- /dev/null +++ b/.github/scripts/clean.sh @@ -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!" diff --git a/.github/scripts/setup.js b/.github/scripts/setup.js index f0558b298d..801002121a 100644 --- a/.github/scripts/setup.js +++ b/.github/scripts/setup.js @@ -36,6 +36,11 @@ async function runAndStream(command, args, options) { return; } + if (process.env.DEVCONTAINER === 'true') { + console.log(chalk.yellow(`Devcontainer detected, skipping setup`)); + return; + } + const coreFolder = path.join(__dirname, '../../ghost/core'); const rootFolder = path.join(__dirname, '../..'); const config = require('../../ghost/core/core/shared/config/loader').loadNconf({ diff --git a/.gitignore b/.gitignore index 2c9f7aaae7..b6a211def2 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,7 @@ Caddyfile # Playwright state with cookies it keeps across tests /ghost/core/playwright-state.json +/ghost/core/playwright-report # Admin /ghost/admin/dist @@ -174,3 +175,7 @@ tsconfig.tsbuildinfo .tinyb .venv .diff_tmp + +# Docker Yarn Cache +.yarncache +.yarncachecopy diff --git a/apps/announcement-bar/vite.config.js b/apps/announcement-bar/vite.config.js index c3a360fa43..a564850a2e 100644 --- a/apps/announcement-bar/vite.config.js +++ b/apps/announcement-bar/vite.config.js @@ -17,6 +17,7 @@ export default defineConfig((config) => { 'process.env.NODE_ENV': JSON.stringify(config.mode) }, preview: { + host: '0.0.0.0', port: 4177 }, plugins: [ diff --git a/apps/comments-ui/vite.config.ts b/apps/comments-ui/vite.config.ts index dbd41a49d6..6819b4b8eb 100644 --- a/apps/comments-ui/vite.config.ts +++ b/apps/comments-ui/vite.config.ts @@ -20,6 +20,7 @@ export default (function viteConfig() { 'process.env.VITEST_SEGFAULT_RETRY': 3 }, preview: { + host: '0.0.0.0', port: 7173 }, server: { diff --git a/apps/portal/vite.config.js b/apps/portal/vite.config.js index 8890ea371d..473464f270 100644 --- a/apps/portal/vite.config.js +++ b/apps/portal/vite.config.js @@ -21,6 +21,7 @@ export default defineConfig((config) => { REACT_APP_VERSION: JSON.stringify(process.env.npm_package_version) }, preview: { + host: '0.0.0.0', port: 4175 }, server: { diff --git a/apps/signup-form/vite.config.ts b/apps/signup-form/vite.config.ts index cc04b37d55..45c8f54cff 100644 --- a/apps/signup-form/vite.config.ts +++ b/apps/signup-form/vite.config.ts @@ -20,6 +20,7 @@ export default (function viteConfig() { 'process.env.VITEST_SEGFAULT_RETRY': 3 }, preview: { + host: '0.0.0.0', port: 6174 }, build: { diff --git a/apps/sodo-search/vite.config.js b/apps/sodo-search/vite.config.js index 30fe8cd9b1..692b14bf31 100644 --- a/apps/sodo-search/vite.config.js +++ b/apps/sodo-search/vite.config.js @@ -17,6 +17,7 @@ export default defineConfig((config) => { 'process.env.NODE_ENV': JSON.stringify(config.mode) }, preview: { + host: '0.0.0.0', port: 4178 }, plugins: [ diff --git a/ghost/core/nodemon.json b/ghost/core/nodemon.json new file mode 100644 index 0000000000..b356d5913a --- /dev/null +++ b/ghost/core/nodemon.json @@ -0,0 +1,3 @@ +{ + "ignore": ["*.test.js", "**/content/public/**", "**/core/built/**", ".yarncache/**"] +} diff --git a/ghost/core/package.json b/ghost/core/package.json index 95b33f3191..51bd68b5d8 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -21,7 +21,7 @@ "license": "MIT", "scripts": { "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", "test": "yarn test:unit", "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", "mock-knex": "TryGhost/mock-knex#d8b93b1c20d4820323477f2c60db016ab3e73192", "nock": "13.3.3", + "nodemon": "^3.1.7", "papaparse": "5.3.2", "parse-prometheus-text-format": "1.1.1", "postcss": "8.4.39", @@ -319,17 +320,20 @@ }, "test:browser": { "dependsOn": [ - "^build:ts" + "^build:ts", + "ghost-admin:build" ] }, "test:browser:admin": { "dependsOn": [ - "^build:ts" + "^build:ts", + "ghost-admin:build" ] }, "test:browser:portal": { "dependsOn": [ - "^build:ts" + "^build:ts", + "ghost-admin:build" ] }, "test:e2e": { diff --git a/ghost/core/playwright.config.js b/ghost/core/playwright.config.js index 19b016c6d2..e809a22342 100644 --- a/ghost/core/playwright.config.js +++ b/ghost/core/playwright.config.js @@ -1,4 +1,24 @@ /** @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 = { timeout: 75 * 1000, @@ -7,7 +27,7 @@ const config = { }, // save trace on fail 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}]], use: { trace: 'retain-on-failure', diff --git a/ghost/core/test/e2e-browser/admin/publishing.spec.js b/ghost/core/test/e2e-browser/admin/publishing.spec.js index 2bd54057de..8c2261800b 100644 --- a/ghost/core/test/e2e-browser/admin/publishing.spec.js +++ b/ghost/core/test/e2e-browser/admin/publishing.spec.js @@ -363,12 +363,13 @@ test.describe('Publishing', () => { await sharedPage.goto('/ghost'); 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) await publishPost(sharedPage, {time: '00:00', type: 'publish+send'}); 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', '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 await checkPostNotPublished(sharedPage, postData); @@ -394,11 +395,11 @@ test.describe('Publishing', () => { await sharedPage.goto('/ghost'); 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) await publishPost(sharedPage, {time: '00:00'}); await closePublishFlow(sharedPage); await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be published in a few seconds'); - const editorUrl = await sharedPage.url(); // Check not published yet await checkPostNotPublished(sharedPage, postData); @@ -425,12 +426,12 @@ test.describe('Publishing', () => { await sharedPage.goto('/ghost'); 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) await publishPost(sharedPage, {type: 'send', time: '00:00'}); await closePublishFlow(sharedPage); await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be sent in a few seconds'); - const editorUrl = await sharedPage.url(); // Check not published yet await checkPostNotPublished(sharedPage, postData); diff --git a/package.json b/package.json index 7c92de9716..df765a4973 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "archive": "nx run ghost:archive", "build": "nx run-many -t build", "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:admin": "node .github/scripts/dev.js --admin", "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: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: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", "test": "nx run-many -t test", "test:unit": "nx run-many -t test:unit", diff --git a/yarn.lock b/yarn.lock index 969a07a116..1410ff3364 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12484,6 +12484,21 @@ chokidar@^2.1.8: optionalDependencies: 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: version "1.1.4" 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: 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" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" 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" 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: version "5.3.2" 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" 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: version "3.0.6" 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" 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: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -28401,6 +28442,13 @@ simple-swizzle@^0.2.2: dependencies: 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: version "4.0.0" 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" integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -30177,6 +30225,11 @@ totalist@^3.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.0.tgz#4ef9c58c5f095255cdc3ff2a0a55091c57a3a1bd" 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: version "4.1.4" 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" 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: version "3.3.6" resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.6.tgz#ad8cf23d7423cb3b53b898476117588f4e2f9159"