Fixed unhandled promise rejection in Redis adapter _get timeout path #36394
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| push: | |
| # Ref: GHA Filter pattern syntax: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#filter-pattern-cheat-sheet | |
| # Run on pushes to main, release branches, and previous/future major version branches | |
| branches: | |
| - main | |
| - 'v[0-9]+.*' # Matches any release branch, e.g. v6.0.3, v12.1.0 | |
| - '[0-9]+.x' # Matches any major version branch, e.g. 5.x, 23.x | |
| env: | |
| FORCE_COLOR: 1 | |
| HEAD_COMMIT: ${{ github.sha }} | |
| CACHED_DEPENDENCY_PATHS: | | |
| ${{ github.workspace }}/node_modules | |
| ${{ github.workspace }}/apps/*/node_modules | |
| ${{ github.workspace }}/ghost/*/node_modules | |
| ${{ github.workspace }}/e2e/node_modules | |
| ~/.cache/ms-playwright/ | |
| NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 | |
| NODE_VERSION: 22.18.0 | |
| # Disable v8-compile-cache to prevent intermittent V8 deserializer crashes | |
| # when multiple parallel Nx workers race to read/write shared bytecode cache | |
| # files. The cache lives in /tmp and is discarded after each run anyway, | |
| # so disabling it has no meaningful performance impact in CI. | |
| # See: https://github.com/nodejs/node/issues/51555 | |
| DISABLE_V8_COMPILE_CACHE: 1 | |
| concurrency: | |
| group: ${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| job_setup: | |
| name: Setup | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| env: | |
| IS_MAIN: ${{ github.ref == 'refs/heads/main' }} | |
| IS_DEVELOPMENT: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/5.x' || github.ref == 'refs/heads/6.x' }} | |
| IS_SIX: ${{ github.ref == 'refs/heads/6.x' }} | |
| IS_SIX_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref == '6.x' }} | |
| IS_FIVE: ${{ github.ref == 'refs/heads/5.x' }} | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| - name: Checkout current commit | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ env.HEAD_COMMIT }} | |
| fetch-depth: 2 | |
| - name: Output GitHub context | |
| if: env.RUNNER_DEBUG == '1' | |
| run: | | |
| echo "GITHUB_EVENT_NAME: ${{ github.event_name }}" | |
| echo "GITHUB_CONTEXT: ${{ toJson(github.event) }}" | |
| - name: Get metadata (push) | |
| if: github.event_name == 'push' | |
| run: | | |
| NUMBER_OF_COMMITS=$(printf "%s\n" '${{ toJson(github.event.commits.*.id) }}' | jq length) | |
| echo "There are $NUMBER_OF_COMMITS commits in this push." | |
| echo "BASE_COMMIT=$(git rev-parse HEAD~$NUMBER_OF_COMMITS)" >> $GITHUB_ENV | |
| - name: Get metadata (pull_request) | |
| if: github.event_name == 'pull_request' | |
| run: | | |
| BASE_COMMIT=$(curl --location --request GET 'https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}' --header 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' | jq -r .base.sha) | |
| echo "Setting BASE_COMMIT to $BASE_COMMIT" | |
| echo "BASE_COMMIT=$BASE_COMMIT" >> $GITHUB_ENV | |
| - name: Check user org membership | |
| id: check_user_org_membership | |
| if: github.event_name == 'pull_request' | |
| run: | | |
| echo "Looking up: ${{ github.triggering_actor }}" | |
| ENCODED_USERNAME=$(printf '%s' '${{ github.triggering_actor }}' | jq -sRr @uri) | |
| LOOKUP_USER=$(curl --write-out "%{http_code}" --silent --output /dev/null --location "https://api.github.com/orgs/tryghost/members/$ENCODED_USERNAME" --header "Authorization: Bearer ${{ secrets.CANARY_DOCKER_BUILD }}") | |
| if [ "$LOOKUP_USER" == "204" ]; then | |
| echo "User is in the org" | |
| echo "is_member=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "User is not in the org" | |
| echo "is_member=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Determine added packages | |
| uses: dorny/paths-filter@v3.0.2 | |
| id: added | |
| with: | |
| filters: | | |
| new-package: | |
| - added: 'ghost/**/package.json' | |
| - name: Determine changed packages | |
| uses: AurorNZ/paths-filter@v4.0.0 | |
| id: changed | |
| with: | |
| filters: | | |
| shared: &shared | |
| - '.github/**' | |
| - 'package.json' | |
| - 'yarn.lock' | |
| core: | |
| - *shared | |
| - 'ghost/**' | |
| - '!ghost/admin/**' | |
| - '!ghost/core/core/server/data/tinybird/**' | |
| admin: | |
| - *shared | |
| - 'ghost/admin/**' | |
| admin-x-settings: | |
| - *shared | |
| - 'apps/admin-x-settings/**' | |
| - 'apps/admin-x-design-system/**' | |
| - 'apps/admin-x-framework/**' | |
| - 'apps/shade/**' | |
| activitypub: | |
| - *shared | |
| - 'apps/shade/**' | |
| - 'apps/admin-x-framework/**' | |
| - 'apps/activitypub/**' | |
| announcement-bar: | |
| - *shared | |
| - 'apps/announcement-bar/**' | |
| comments-ui: | |
| - *shared | |
| - 'apps/comments-ui/**' | |
| portal: | |
| - *shared | |
| - 'apps/portal/**' | |
| signup-form: | |
| - *shared | |
| - 'apps/signup-form/**' | |
| sodo-search: | |
| - *shared | |
| - 'apps/sodo-search/**' | |
| tinybird: | |
| - 'ghost/core/core/server/data/tinybird/**' | |
| - '!ghost/core/core/server/data/tinybird/**/*.md' | |
| any-code: | |
| - '!**/*.md' | |
| - name: Compute lockfile hash | |
| run: echo "hash=lockfile-${{ hashFiles('yarn.lock') }}" >> "$GITHUB_ENV" | |
| - name: Compute dependency cache key | |
| run: echo "cachekey=dep-cache-${{ hashFiles('yarn.lock') }}-${{ env.HEAD_COMMIT }}" >> "$GITHUB_ENV" | |
| - name: Compute dependency cache restore key | |
| run: | | |
| EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) | |
| echo "DEPENDENCY_CACHE_RESTORE_KEYS<<$EOF" >> "$GITHUB_ENV" | |
| echo "dep-cache-${{ env.hash }}-${{ github.sha }}" >> "$GITHUB_ENV" | |
| echo "dep-cache-${{ env.hash }}" >> "$GITHUB_ENV" | |
| echo "dep-cache" >> "$GITHUB_ENV" | |
| echo "$EOF" >> "$GITHUB_ENV" | |
| - name: Nx cache | |
| uses: actions/cache@v4 | |
| id: cache_nx | |
| with: | |
| path: .nxcache | |
| key: nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT }} | |
| restore-keys: | | |
| nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT }} | |
| nx-Linux-${{ github.ref }} | |
| nx-Linux | |
| - name: Check dependency cache | |
| uses: actions/cache@v4 | |
| id: cache_dependencies | |
| with: | |
| path: ${{ env.CACHED_DEPENDENCY_PATHS }} | |
| key: ${{ env.cachekey }} | |
| restore-keys: ${{ env.IS_DEVELOPMENT == 'false' && env.DEPENDENCY_CACHE_RESTORE_KEYS || 'dep-never-restore'}} | |
| - name: Define Node test matrix | |
| id: node_matrix | |
| run: | | |
| echo 'matrix=["22.18.0"]' >> $GITHUB_OUTPUT | |
| - name: Set up Node | |
| uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Install dependencies | |
| run: bash .github/scripts/install-deps.sh | |
| outputs: | |
| changed_admin: ${{ steps.changed.outputs.admin }} | |
| changed_core: ${{ steps.changed.outputs.core }} | |
| changed_admin_x_settings: ${{ steps.changed.outputs.admin-x-settings }} | |
| changed_activitypub: ${{ steps.changed.outputs.activitypub }} | |
| changed_announcement_bar: ${{ steps.changed.outputs.announcement-bar }} | |
| changed_comments_ui: ${{ steps.changed.outputs.comments-ui }} | |
| changed_portal: ${{ steps.changed.outputs.portal }} | |
| changed_signup_form: ${{ steps.changed.outputs.signup-form }} | |
| changed_sodo_search: ${{ steps.changed.outputs.sodo-search }} | |
| changed_tinybird: ${{ steps.changed.outputs.tinybird }} | |
| changed_any_code: ${{ steps.changed.outputs.any-code }} | |
| changed_new_package: ${{ steps.added.outputs.new-package }} | |
| base_commit: ${{ env.BASE_COMMIT }} | |
| is_main: ${{ env.IS_MAIN }} | |
| is_development: ${{ env.IS_DEVELOPMENT }} | |
| is_six: ${{ env.IS_SIX }} | |
| is_six_pr: ${{ env.IS_SIX_PR }} | |
| is_five: ${{ env.IS_FIVE }} | |
| member_is_in_org: ${{ steps.check_user_org_membership.outputs.is_member }} | |
| dependency_cache_key: ${{ env.cachekey }} | |
| node_version: ${{ env.NODE_VERSION }} | |
| node_test_matrix: ${{ steps.node_matrix.outputs.matrix }} | |
| job_app_version_bump_check: | |
| name: Check app version bump | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Checkout PR head commit | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| fetch-depth: 0 | |
| - name: Fetch main branch | |
| run: git fetch --no-tags origin main | |
| - name: Check app version bump | |
| env: | |
| PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| PR_COMPARE_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: node .github/scripts/check-app-version-bump.js | |
| job_lint: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_any_code == 'true' | |
| name: Lint | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1000 | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - uses: actions/cache@v4 | |
| with: | |
| path: ghost/**/.eslintcache | |
| key: eslint-cache | |
| - run: yarn nx affected -t lint --base=${{ needs.job_setup.outputs.BASE_COMMIT }} | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_i18n: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| name: i18n | |
| if: | | |
| needs.job_setup.outputs.changed_comments_ui == 'true' | |
| || needs.job_setup.outputs.changed_signup_form == 'true' | |
| || needs.job_setup.outputs.changed_sodo_search == 'true' | |
| || needs.job_setup.outputs.changed_portal == 'true' | |
| || needs.job_setup.outputs.changed_core == 'true' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Run i18n tests | |
| run: yarn nx run @tryghost/i18n:test | |
| job_admin-tests: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_admin == 'true' | |
| name: Admin tests - Chrome | |
| env: | |
| MOZ_HEADLESS: 1 | |
| JOBS: 1 | |
| CI: true | |
| COVERAGE: true | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - run: yarn nx run ghost-admin:test | |
| env: | |
| BROWSER: Chrome | |
| # Merge coverage reports and upload | |
| - name: Merge Admin test coverage | |
| run: yarn ember coverage-merge | |
| working-directory: ghost/admin | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: admin-coverage | |
| path: ghost/*/coverage/cobertura-coverage.xml | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_browser-tests: | |
| name: Browser tests | |
| timeout-minutes: 60 | |
| runs-on: | |
| labels: ubuntu-latest | |
| needs: [job_setup] | |
| # Skip on forked PRs - Stripe secrets are not available | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| submodules: true | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: yarn | |
| - name: Install Stripe-CLI | |
| run: | | |
| export VERSION=1.13.5 | |
| wget "https://github.com/stripe/stripe-cli/releases/download/v$VERSION/stripe_${VERSION}_linux_x86_64.tar.gz" | |
| tar -zxvf "stripe_${VERSION}_linux_x86_64.tar.gz" | |
| mv stripe /usr/local/bin | |
| stripe -v | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Run migrations | |
| working-directory: ghost/core | |
| run: yarn knex-migrator init | |
| env: | |
| NODE_OPTIONS: "--import=tsx" | |
| - name: Setup Playwright | |
| uses: ./.github/actions/setup-playwright | |
| - name: Build TS packages | |
| run: yarn nx run-many -t build --exclude=ghost-admin | |
| - name: Build Admin | |
| run: yarn nx run ghost-admin:build:dev | |
| - name: Run Playwright tests locally | |
| run: yarn test:browser | |
| env: | |
| CI: true | |
| STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }} | |
| STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: browser-tests-playwright-report | |
| path: ghost/core/playwright-report | |
| retention-days: 30 | |
| - name: View Test Report command | |
| if: failure() | |
| run: | | |
| echo -e "::notice::To view the Playwright report locally, run:\n\nREPORT_DIR=\$(mktemp -d) && gh run download ${{ github.run_id }} -n browser-tests-playwright-report -D \"\$REPORT_DIR\" && npx playwright show-report \"\$REPORT_DIR\"" | |
| job_unit-tests: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_any_code == 'true' | |
| strategy: | |
| matrix: | |
| node: ${{ fromJSON(needs.job_setup.outputs.node_test_matrix) }} | |
| name: Unit tests (Node ${{ matrix.node }}) | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1000 | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Set timezone (non-UTC) | |
| uses: szenius/set-timezone@v2.0 | |
| with: | |
| timezoneLinux: "America/New_York" | |
| - name: Determine affected unit-test projects | |
| id: affected_unit_projects | |
| run: | | |
| PROJECTS=$(yarn -s nx show projects --affected --withTarget=test:unit --base=${{ needs.job_setup.outputs.BASE_COMMIT }} --sep=, | tr -d '\n') | |
| echo "projects=$PROJECTS" >> "$GITHUB_OUTPUT" | |
| # Build only projects that will run unit tests | |
| - name: Build assets for affected unit tests | |
| if: steps.affected_unit_projects.outputs.projects != '' | |
| run: yarn nx run-many -t build --projects="${{ steps.affected_unit_projects.outputs.projects }}" | |
| - name: Run unit tests | |
| run: yarn nx affected -t test:unit --base=${{ needs.job_setup.outputs.BASE_COMMIT }} | |
| env: | |
| FORCE_COLOR: 0 | |
| GHOST_UNIT_TEST_VARIANT: ci | |
| NX_SKIP_LOG_GROUPING: true | |
| logging__level: fatal | |
| - uses: actions/upload-artifact@v4 | |
| if: matrix.node == env.NODE_VERSION | |
| with: | |
| name: unit-coverage | |
| path: ghost/*/coverage/cobertura-coverage.xml | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_acceptance-tests: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_core == 'true' | |
| services: | |
| mysql: | |
| image: ${{ matrix.env.DB == 'mysql8' && 'mysql:8.0' || '' }} | |
| env: | |
| MYSQL_DATABASE: ghost_testing | |
| MYSQL_ROOT_PASSWORD: root | |
| ports: | |
| - 3306 | |
| options: >- | |
| --tmpfs /var/lib/mysql | |
| --health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -proot" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=12 | |
| strategy: | |
| matrix: | |
| node: ${{ fromJSON(needs.job_setup.outputs.node_test_matrix) }} | |
| env: | |
| - DB: mysql8 | |
| NODE_ENV: testing-mysql | |
| include: | |
| - node: ${{ needs.job_setup.outputs.node_version }} | |
| env: | |
| DB: sqlite3 | |
| NODE_ENV: testing | |
| env: | |
| DB: ${{ matrix.env.DB }} | |
| NODE_ENV: ${{ matrix.env.NODE_ENV }} | |
| name: Acceptance tests (Node ${{ matrix.node }}, ${{ matrix.env.DB }}) | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Build TS packages | |
| run: yarn nx run-many -t build --exclude=ghost-admin | |
| - name: Set timezone (non-UTC) | |
| uses: szenius/set-timezone@v2.0 | |
| with: | |
| timezoneLinux: "America/New_York" | |
| - name: Set env vars (SQLite) | |
| if: contains(matrix.env.DB, 'sqlite') | |
| run: echo "database__connection__filename=/dev/shm/ghost-test.db" >> $GITHUB_ENV | |
| - name: Set env vars (MySQL) | |
| if: contains(matrix.env.DB, 'mysql') | |
| run: | | |
| echo "database__connection__host=127.0.0.1" >> $GITHUB_ENV | |
| echo "database__connection__port=${{ job.services.mysql.ports['3306'] }}" >> $GITHUB_ENV | |
| echo "database__connection__password=root" >> $GITHUB_ENV | |
| - name: E2E tests | |
| working-directory: ghost/core | |
| run: yarn test:ci:e2e | |
| - name: Integration tests | |
| working-directory: ghost/core | |
| run: yarn test:ci:integration | |
| - uses: actions/upload-artifact@v4 | |
| if: matrix.node == env.NODE_VERSION && contains(matrix.env.DB, 'mysql') | |
| with: | |
| name: e2e-coverage | |
| path: | | |
| ghost/*/coverage-e2e/cobertura-coverage.xml | |
| ghost/*/coverage-integration/cobertura-coverage.xml | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_legacy-tests: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_core == 'true' | |
| services: | |
| mysql: | |
| image: ${{ matrix.env.DB == 'mysql8' && 'mysql:8.0' || '' }} | |
| env: | |
| MYSQL_DATABASE: ghost_testing | |
| MYSQL_ROOT_PASSWORD: root | |
| ports: | |
| - 3306 | |
| options: >- | |
| --tmpfs /var/lib/mysql | |
| --health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -proot" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=12 | |
| strategy: | |
| matrix: | |
| include: | |
| - node: ${{ needs.job_setup.outputs.node_version }} | |
| env: | |
| DB: mysql8 | |
| NODE_ENV: testing-mysql | |
| - node: ${{ needs.job_setup.outputs.node_version }} | |
| env: | |
| DB: sqlite3 | |
| NODE_ENV: testing | |
| env: | |
| DB: ${{ matrix.env.DB }} | |
| NODE_ENV: ${{ matrix.env.NODE_ENV }} | |
| name: Legacy tests (Node ${{ matrix.node }}, ${{ matrix.env.DB }}) | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| submodules: true | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Build TS packages | |
| run: yarn nx run-many -t build --exclude=ghost-admin | |
| - name: Set env vars (SQLite) | |
| if: contains(matrix.env.DB, 'sqlite') | |
| run: echo "database__connection__filename=/dev/shm/ghost-test.db" >> $GITHUB_ENV | |
| - name: Set env vars (MySQL) | |
| if: contains(matrix.env.DB, 'mysql') | |
| run: | | |
| echo "database__connection__host=127.0.0.1" >> $GITHUB_ENV | |
| echo "database__connection__port=${{ job.services.mysql.ports['3306'] }}" >> $GITHUB_ENV | |
| echo "database__connection__password=root" >> $GITHUB_ENV | |
| - name: Legacy tests | |
| working-directory: ghost/core | |
| run: yarn test:ci:legacy | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_admin_x_settings: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_admin_x_settings == 'true' | |
| name: Admin-X Settings tests | |
| env: | |
| CI: true | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Setup Playwright | |
| uses: ./.github/actions/setup-playwright | |
| - run: yarn nx run @tryghost/admin-x-settings:test:acceptance | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: admin-x-settings-playwright-report | |
| path: apps/admin-x-settings/playwright-report | |
| retention-days: 30 | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_activitypub: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_activitypub == 'true' | |
| name: ActivityPub tests | |
| env: | |
| CI: true | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Setup Playwright | |
| uses: ./.github/actions/setup-playwright | |
| - run: yarn nx run @tryghost/activitypub:test:acceptance | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: activitypub-playwright-report | |
| path: apps/activitypub/playwright-report | |
| retention-days: 30 | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_comments_ui: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_comments_ui == 'true' | |
| name: Comments-UI tests | |
| env: | |
| CI: true | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Setup Playwright | |
| uses: ./.github/actions/setup-playwright | |
| - run: yarn nx run @tryghost/comments-ui:test | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: comments-ui-playwright-report | |
| path: apps/comments-ui/playwright-report | |
| retention-days: 30 | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_signup_form: | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_signup_form == 'true' | |
| name: Signup-form tests | |
| env: | |
| CI: true | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Setup Playwright | |
| uses: ./.github/actions/setup-playwright | |
| - run: yarn nx run @tryghost/signup-form:test:e2e | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: signup-form-playwright-report | |
| path: apps/signup-form/playwright-report | |
| retention-days: 30 | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_tinybird-tests: | |
| name: Tinybird Tests | |
| runs-on: ubuntu-latest | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_tinybird == 'true' | |
| defaults: | |
| run: | |
| working-directory: ghost/core/core/server/data/tinybird | |
| services: | |
| tinybird: | |
| image: tinybirdco/tinybird-local:latest | |
| ports: | |
| - 7181:7181 | |
| env: | |
| TINYBIRD_HOST: ${{ secrets.TINYBIRD_HOST }} | |
| TINYBIRD_TOKEN: ${{ secrets.TINYBIRD_TOKEN }} | |
| TINYBIRD_HOST_STAGING: ${{ secrets.TINYBIRD_HOST_STAGING }} | |
| TINYBIRD_TOKEN_STAGING: ${{ secrets.TINYBIRD_TOKEN_STAGING }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Tinybird CLI | |
| run: curl -fsSL https://tinybird.co/install.sh | sh | |
| - name: Build project | |
| run: tb build | |
| - name: Test project | |
| run: tb test run | |
| - name: Deployment check - Staging | |
| run: tb --cloud --host ${{ env.TINYBIRD_HOST_STAGING }} --token ${{ env.TINYBIRD_TOKEN_STAGING }} deploy --check | |
| - name: Deployment check - Production | |
| run: tb --cloud --host ${{ env.TINYBIRD_HOST }} --token ${{ env.TINYBIRD_TOKEN }} deploy --check | |
| job_ghost-cli: | |
| name: Ghost-CLI tests | |
| needs: [job_setup] | |
| if: needs.job_setup.outputs.changed_core == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| submodules: true | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Install Ghost-CLI | |
| run: npm install -g ghost-cli@latest | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - run: node .github/scripts/bump-version.js canary | |
| - run: yarn archive | |
| - run: mv ghost-*.tgz ghost.tgz | |
| working-directory: ghost/core | |
| - name: Save Ghost CLI Debug Logs | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ghost-cli-debug-logs | |
| path: /home/runner/.ghost/logs/ | |
| - name: Clean Install | |
| run: | | |
| DIR=$(mktemp -d) | |
| ghost install local -d $DIR --archive $(pwd)/ghost/core/ghost.tgz | |
| - name: Latest Release | |
| run: | | |
| DIR=$(mktemp -d) | |
| ghost install local -d $DIR | |
| ghost update -d $DIR --archive $(pwd)/ghost/core/ghost.tgz | |
| - name: Print debug logs | |
| if: failure() | |
| run: | | |
| [ -f ~/.ghost/logs/*.log ] && cat ~/.ghost/logs/*.log | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_docker_build_production: | |
| name: Build & Push Production Docker Images | |
| needs: [job_setup] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| - uses: actions/setup-node@v4 | |
| env: | |
| FORCE_COLOR: 0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Build server and admin assets | |
| run: | | |
| PKG_VERSION=$(node -p "require('./ghost/core/package.json').version") | |
| SHORT_SHA="${GITHUB_SHA:0:7}" | |
| if [ "${{ github.ref_type }}" != "tag" ]; then | |
| export GHOST_BUILD_VERSION="${PKG_VERSION}+${SHORT_SHA}" | |
| echo "GHOST_BUILD_VERSION=${GHOST_BUILD_VERSION}" >> $GITHUB_ENV | |
| fi | |
| yarn build:production | |
| - name: Pack standalone distribution | |
| run: yarn workspace ghost pack:standalone | |
| - name: Prepare Docker build context | |
| run: mv ghost/core/package/ /tmp/ghost-production/ | |
| - name: Determine push strategy | |
| id: strategy | |
| run: | | |
| # Same-org repos (e.g. TryGhost/Ghost, TryGhost/Ghost-Security) push to GHCR. | |
| # External forks and cross-repo PRs use artifact-based image transfer instead. | |
| USE_ARTIFACT="false" | |
| if [ "${{ github.repository_owner }}" != "TryGhost" ]; then | |
| # External fork — no GHCR push | |
| USE_ARTIFACT="true" | |
| elif [ "${{ github.event_name }}" = "pull_request" ] && \ | |
| [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then | |
| # Cross-repo PR (fork PR into this repo) — no GHCR push | |
| USE_ARTIFACT="true" | |
| fi | |
| OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| # Derive GHCR image names from repository name so each repo gets its own namespace | |
| # TryGhost/Ghost → ghost-core / ghost, TryGhost/Ghost-Security → ghost-security-core / ghost-security | |
| REPO_NAME=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]') | |
| if [ "$REPO_NAME" = "ghost" ]; then | |
| IMAGE_CORE_NAME="ghcr.io/${OWNER}/ghost-core" | |
| IMAGE_FULL_NAME="ghcr.io/${OWNER}/ghost" | |
| else | |
| IMAGE_CORE_NAME="ghcr.io/${OWNER}/${REPO_NAME}-core" | |
| IMAGE_FULL_NAME="ghcr.io/${OWNER}/${REPO_NAME}" | |
| fi | |
| echo "use-artifact=$USE_ARTIFACT" >> $GITHUB_OUTPUT | |
| echo "should-push=$( [ "$USE_ARTIFACT" = "false" ] && echo "true" || echo "false" )" >> $GITHUB_OUTPUT | |
| echo "owner=$OWNER" >> $GITHUB_OUTPUT | |
| echo "image-core-name=$IMAGE_CORE_NAME" >> $GITHUB_OUTPUT | |
| echo "image-full-name=$IMAGE_FULL_NAME" >> $GITHUB_OUTPUT | |
| - name: Upload admin artifact for CD | |
| id: upload-admin | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: admin-build-cd | |
| path: apps/admin/dist | |
| retention-days: 7 | |
| if-no-files-found: error | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| if: steps.strategy.outputs.should-push == 'true' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Docker meta (core) | |
| id: meta-core | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ steps.strategy.outputs.image-core-name }} | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=sha | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| labels: | | |
| org.opencontainers.image.title=Ghost Core | |
| org.opencontainers.image.description=Ghost production build (server only, no admin) | |
| org.opencontainers.image.vendor=TryGhost | |
| - name: Docker meta (full) | |
| id: meta-full | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ steps.strategy.outputs.image-full-name }} | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=sha | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| labels: | | |
| org.opencontainers.image.title=Ghost | |
| org.opencontainers.image.description=Ghost production build (server + admin) | |
| org.opencontainers.image.vendor=TryGhost | |
| - name: Build & push core image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: /tmp/ghost-production | |
| file: Dockerfile.production | |
| target: core | |
| build-args: | | |
| NODE_VERSION=${{ env.NODE_VERSION }} | |
| GHOST_BUILD_VERSION=${{ env.GHOST_BUILD_VERSION }} | |
| push: ${{ steps.strategy.outputs.should-push }} | |
| load: ${{ steps.strategy.outputs.should-push == 'false' }} | |
| tags: ${{ steps.meta-core.outputs.tags }} | |
| labels: ${{ steps.meta-core.outputs.labels }} | |
| cache-from: type=registry,ref=${{ steps.strategy.outputs.image-core-name }}:cache-main | |
| cache-to: ${{ steps.strategy.outputs.should-push == 'true' && format('type=registry,ref={0}:cache-{1},mode=max', steps.strategy.outputs.image-core-name, github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'main') || '' }} | |
| - name: Build & push full image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: /tmp/ghost-production | |
| file: Dockerfile.production | |
| target: full | |
| build-args: | | |
| NODE_VERSION=${{ env.NODE_VERSION }} | |
| GHOST_BUILD_VERSION=${{ env.GHOST_BUILD_VERSION }} | |
| push: ${{ steps.strategy.outputs.should-push }} | |
| load: true | |
| tags: ${{ steps.meta-full.outputs.tags }} | |
| labels: ${{ steps.meta-full.outputs.labels }} | |
| cache-from: type=registry,ref=${{ steps.strategy.outputs.image-full-name }}:cache-main | |
| cache-to: ${{ steps.strategy.outputs.should-push == 'true' && format('type=registry,ref={0}:cache-{1},mode=max', steps.strategy.outputs.image-full-name, github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'main') || '' }} | |
| - name: Save full image as artifact | |
| run: | | |
| IMAGE_TAG=$(echo "${{ steps.meta-full.outputs.tags }}" | head -n1) | |
| echo "Saving image: $IMAGE_TAG" | |
| docker save "$IMAGE_TAG" | gzip > docker-image-production.tar.gz | |
| echo "Image saved as docker-image-production.tar.gz" | |
| ls -lh docker-image-production.tar.gz | |
| - name: Upload image artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: docker-image-production | |
| path: docker-image-production.tar.gz | |
| retention-days: 1 | |
| - name: Inspect image size and layers | |
| shell: bash | |
| run: | | |
| IMAGE_TAG=$(echo "${{ steps.meta-full.outputs.tags }}" | head -n1) | |
| echo "Analyzing Docker image: $IMAGE_TAG" | |
| # Get the image size in bytes | |
| IMAGE_SIZE_BYTES=$(docker inspect "$IMAGE_TAG" --format='{{.Size}}') | |
| # Convert to human readable format | |
| IMAGE_SIZE_MB=$(( IMAGE_SIZE_BYTES / 1024 / 1024 )) | |
| IMAGE_SIZE_GB=$(echo "scale=2; $IMAGE_SIZE_BYTES / 1024 / 1024 / 1024" | bc) | |
| # Format size display based on magnitude | |
| if [ $IMAGE_SIZE_MB -ge 1024 ]; then | |
| IMAGE_SIZE_DISPLAY="${IMAGE_SIZE_GB} GB" | |
| else | |
| IMAGE_SIZE_DISPLAY="${IMAGE_SIZE_MB} MB" | |
| fi | |
| echo "Image size: ${IMAGE_SIZE_DISPLAY}" | |
| # Write to GitHub Step Summary | |
| { | |
| echo "# Docker Image Analysis" | |
| echo "" | |
| echo "**Image:** \`$IMAGE_TAG\`" | |
| echo "" | |
| echo "**Total Size:** ${IMAGE_SIZE_DISPLAY}" | |
| echo "" | |
| echo "## Image Layers" | |
| echo "" | |
| echo "| Size | Layer |" | |
| echo "|------|-------|" | |
| # Get all layers (including 0B ones) | |
| docker history "$IMAGE_TAG" --format "{{.Size}}@@@{{.CreatedBy}}" --no-trunc | \ | |
| while IFS='@@@' read -r size cmd; do | |
| # Clean up the command for display | |
| cmd_clean=$(echo "$cmd" | sed 's/^\/bin\/sh -c //' | sed 's/^#(nop) //' | sed 's/^@@//' | sed 's/|/\\|/g' | cut -c1-80) | |
| if [ ${#cmd} -gt 80 ]; then | |
| cmd_clean="${cmd_clean}..." | |
| fi | |
| echo "| $size | \`${cmd_clean}\` |" | |
| done | |
| } >> $GITHUB_STEP_SUMMARY | |
| outputs: | |
| image-tags: ${{ steps.meta-full.outputs.tags }} | |
| use-artifact: ${{ steps.strategy.outputs.use-artifact }} | |
| admin-artifact-id: ${{ steps.upload-admin.outputs.artifact-id }} | |
| job_e2e_tests: | |
| name: E2E Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) | |
| runs-on: ubuntu-latest | |
| needs: [job_docker_build_production, job_setup] | |
| strategy: | |
| fail-fast: true | |
| matrix: | |
| shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] | |
| shardTotal: [8] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Pull or build Tinybird CLI Image | |
| run: | | |
| COMPOSE_IMAGE="${COMPOSE_PROJECT_NAME:-ghost-dev}-tb-cli" | |
| # Try pulling pre-built image from GHCR first (fast path) | |
| if docker pull ghcr.io/tryghost/tb-cli:latest 2>/dev/null; then | |
| echo "Pulled tb-cli from GHCR" | |
| docker tag ghcr.io/tryghost/tb-cli:latest "$COMPOSE_IMAGE" | |
| else | |
| echo "GHCR image not available, building from source" | |
| docker buildx build --load -t "$COMPOSE_IMAGE" -f docker/tb-cli/Dockerfile . | |
| fi | |
| - name: Load Image | |
| uses: ./.github/actions/load-docker-image | |
| id: load | |
| with: | |
| use-artifact: ${{ needs.job_docker_build_production.outputs.use-artifact }} | |
| image-tags: ${{ needs.job_docker_build_production.outputs.image-tags }} | |
| artifact-name: docker-image-production | |
| - name: Setup Docker Registry Mirrors | |
| uses: ./.github/actions/setup-docker-registry-mirrors | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Prepare E2E CI job | |
| env: | |
| GHOST_E2E_BASE_IMAGE: ${{ steps.load.outputs.image-tag }} | |
| run: bash ./e2e/scripts/prepare-ci-e2e-job.sh | |
| - name: Run e2e tests in Playwright container | |
| env: | |
| TEST_WORKERS_COUNT: 1 | |
| GHOST_E2E_MODE: build | |
| GHOST_E2E_IMAGE: ghost-e2e:local | |
| E2E_SHARD_INDEX: ${{ matrix.shardIndex }} | |
| E2E_SHARD_TOTAL: ${{ matrix.shardTotal }} | |
| E2E_RETRIES: 2 | |
| run: bash ./e2e/scripts/run-playwright-container.sh | |
| - name: Dump E2E docker logs | |
| if: failure() | |
| run: bash ./e2e/scripts/dump-e2e-docker-logs.sh | |
| - name: Stop E2E infra | |
| if: always() | |
| run: yarn workspace @tryghost/e2e infra:down | |
| - name: Upload blob report to GitHub Actions Artifacts | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: blob-report-${{ matrix.shardIndex }} | |
| path: e2e/blob-report | |
| retention-days: 1 | |
| - name: Upload test results artifacts | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results-${{ matrix.shardIndex }} | |
| path: e2e/test-results | |
| retention-days: 7 | |
| - uses: tryghost/actions/actions/slack-build@main | |
| if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| with: | |
| status: ${{ job.status }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| job_merge_e2e_reports: | |
| name: Merge Reports | |
| if: always() | |
| needs: [job_e2e_tests, job_setup] | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Download blob reports from GitHub Actions Artifacts | |
| uses: actions/download-artifact@v4 | |
| continue-on-error: true | |
| with: | |
| path: e2e/all-blob-reports | |
| pattern: blob-report-* | |
| merge-multiple: true | |
| - name: Check for blob reports | |
| id: check | |
| run: | | |
| if [ -d "e2e/all-blob-reports" ] && [ -n "$(ls -A e2e/all-blob-reports 2>/dev/null)" ]; then | |
| echo "has_reports=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_reports=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Download test results from GitHub Actions Artifacts | |
| if: steps.check.outputs.has_reports == 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: e2e/all-test-results | |
| pattern: test-results-* | |
| merge-multiple: true | |
| - name: Merge into HTML Report | |
| if: steps.check.outputs.has_reports == 'true' | |
| run: npx playwright merge-reports --reporter html ./all-blob-reports | |
| working-directory: e2e | |
| - name: Upload HTML report | |
| if: steps.check.outputs.has_reports == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report | |
| path: e2e/playwright-report | |
| retention-days: 14 | |
| - name: Upload merged test results | |
| if: steps.check.outputs.has_reports == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results | |
| path: e2e/all-test-results | |
| retention-days: 7 | |
| - name: View Test Report command | |
| if: steps.check.outputs.has_reports == 'true' | |
| run: | | |
| echo -e "::notice::To view the Playwright report locally, run:\n\nREPORT_DIR=\$(mktemp -d) && gh run download ${{ github.run_id }} -n playwright-report -D \"\$REPORT_DIR\" && npx playwright show-report \"\$REPORT_DIR\"" | |
| - name: Comment on PR with test report command | |
| if: github.event_name == 'pull_request' && steps.check.outputs.has_reports == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh pr comment ${{ github.event.pull_request.number }} --body "## E2E Tests Failed | |
| To view the Playwright test report locally, run: | |
| \`\`\`bash | |
| REPORT_DIR=\$(mktemp -d) && gh run download ${{ github.run_id }} -n playwright-report -D \"\$REPORT_DIR\" && npx playwright show-report \"\$REPORT_DIR\" | |
| \`\`\`" | |
| job_coverage: | |
| name: Coverage | |
| needs: [ | |
| job_admin-tests, | |
| job_acceptance-tests, | |
| job_unit-tests | |
| ] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Restore Admin coverage | |
| if: contains(needs.job_admin-tests.result, 'success') | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: admin-coverage | |
| - name: Move coverage | |
| if: contains(needs.job_admin-tests.result, 'success') | |
| run: | | |
| rsync -av --remove-source-files admin/* ghost/admin | |
| - name: Upload Admin test coverage | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| flags: admin-tests | |
| - name: Restore E2E coverage | |
| if: contains(needs.job_acceptance-tests.result, 'success') | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: e2e-coverage | |
| - name: Move coverage | |
| if: contains(needs.job_acceptance-tests.result, 'success') | |
| run: | | |
| rsync -av --remove-source-files core/* ghost/core | |
| - name: Upload E2E test coverage | |
| if: contains(needs.job_acceptance-tests.result, 'success') | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| flags: e2e-tests | |
| job_required_tests: | |
| name: All required tests passed or skipped | |
| needs: | |
| [ | |
| job_setup, | |
| job_app_version_bump_check, | |
| job_lint, | |
| job_i18n, | |
| job_ghost-cli, | |
| job_admin-tests, | |
| job_unit-tests, | |
| job_acceptance-tests, | |
| job_legacy-tests, | |
| job_browser-tests, | |
| job_admin_x_settings, | |
| job_activitypub, | |
| job_comments_ui, | |
| job_signup_form, | |
| job_tinybird-tests, | |
| job_e2e_tests | |
| ] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Output needs | |
| run: echo "${{ toJson(needs) }}" | |
| - name: Check if any required jobs failed or been cancelled | |
| if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') | |
| run: | | |
| echo "One of the dependent jobs have failed or been cancelled. You may need to re-run it." && exit 1 | |
| publish_packages: | |
| needs: [ | |
| job_setup, | |
| job_lint, | |
| job_unit-tests | |
| ] | |
| name: Publish ${{ 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' && (needs.job_setup.outputs.is_main == 'true' || needs.job_setup.outputs.is_five == 'true') | |
| permissions: | |
| id-token: write | |
| strategy: | |
| matrix: | |
| include: | |
| - package_name: '@tryghost/activitypub' | |
| package_path: 'apps/activitypub' | |
| cdn_paths: 'https://cdn.jsdelivr.net/ghost/activitypub@CURRENT_MAJOR/dist/activitypub.js' | |
| - package_name: '@tryghost/portal' | |
| package_path: 'apps/portal' | |
| cdn_paths: 'https://cdn.jsdelivr.net/ghost/portal@~CURRENT_MINOR/umd/portal.min.js' | |
| - package_name: '@tryghost/sodo-search' | |
| package_path: 'apps/sodo-search' | |
| cdn_paths: | | |
| https://cdn.jsdelivr.net/ghost/sodo-search@~CURRENT_MINOR/umd/sodo-search.min.js | |
| https://cdn.jsdelivr.net/ghost/sodo-search@~CURRENT_MINOR/umd/main.css | |
| - package_name: '@tryghost/comments-ui' | |
| package_path: 'apps/comments-ui' | |
| cdn_paths: 'https://cdn.jsdelivr.net/ghost/comments-ui@~CURRENT_MINOR/umd/comments-ui.min.js' | |
| - package_name: '@tryghost/signup-form' | |
| package_path: 'apps/signup-form' | |
| cdn_paths: 'https://cdn.jsdelivr.net/ghost/signup-form@~CURRENT_MINOR/umd/signup-form.min.js' | |
| - package_name: '@tryghost/announcement-bar' | |
| package_path: 'apps/announcement-bar' | |
| cdn_paths: 'https://cdn.jsdelivr.net/ghost/announcement-bar@~CURRENT_MINOR/umd/announcement-bar.min.js' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Restore caches | |
| uses: ./.github/actions/restore-cache | |
| env: | |
| DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} | |
| - name: Check if version changed | |
| id: version_check | |
| working-directory: ${{ matrix.package_path }} | |
| run: | | |
| CURRENT_VERSION=$(cat package.json | jq -r .version) | |
| echo "Current version: $CURRENT_VERSION" | |
| CURRENT_MINOR=$(cat package.json | jq -r .version | awk -F. '{print $1"."$2}') | |
| echo "current_minor=$CURRENT_MINOR" >> $GITHUB_OUTPUT | |
| CURRENT_MAJOR=$(cat package.json | jq -r .version | awk -F. '{print $1}') | |
| echo "current_major=$CURRENT_MAJOR" >> $GITHUB_OUTPUT | |
| if [ "${{ needs.job_setup.outputs.is_five }}" = "true" ]; then | |
| # For 5.x branch, check if this exact version exists on npm | |
| if npm show ${{ matrix.package_name }}@$CURRENT_VERSION version > /dev/null 2>&1; then | |
| echo "Version $CURRENT_VERSION already exists on npm." | |
| echo "version_changed=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "Version $CURRENT_VERSION does not exist on npm." | |
| echo "version_changed=true" >> $GITHUB_OUTPUT | |
| fi | |
| else | |
| # For main branch, compare against latest | |
| PUBLISHED_VERSION=$(npm show ${{ matrix.package_name }} version || echo "0.0.0") | |
| echo "Published version (latest): $PUBLISHED_VERSION" | |
| if [ "$CURRENT_VERSION" = "$PUBLISHED_VERSION" ]; then | |
| echo "Version is unchanged." | |
| echo "version_changed=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "Version has changed." | |
| echo "version_changed=true" >> $GITHUB_OUTPUT | |
| fi | |
| fi | |
| - name: Build the package | |
| if: steps.version_check.outputs.version_changed == 'true' | |
| run: yarn run nx build ${{ matrix.package_name }} | |
| - name: Configure .npmrc | |
| if: steps.version_check.outputs.version_changed == 'true' | |
| run: | | |
| echo "@tryghost:registry=https://registry.npmjs.org/" >> ~/.npmrc | |
| # TODO: Check we can remove this once we update Node to v24 | |
| - name: Install v11 of NPM # We need this to install packages via OIDC. | |
| if: steps.version_check.outputs.version_changed == 'true' | |
| run: npm install -g npm@11 | |
| - name: Publish to npm | |
| if: steps.version_check.outputs.version_changed == 'true' | |
| working-directory: ${{ matrix.package_path }} | |
| run: | | |
| if [ "${{ needs.job_setup.outputs.is_five }}" = "true" ]; then | |
| npm publish --access public --tag ghost-5x | |
| else | |
| npm publish --access public | |
| fi | |
| - name: Replace version placeholders in cdn-paths | |
| id: cdn_paths | |
| if: steps.version_check.outputs.version_changed == 'true' | |
| run: | | |
| cdn_paths="${{ matrix.cdn_paths }}" | |
| echo "cdn_paths<<EOF" >> $GITHUB_OUTPUT | |
| echo "$cdn_paths" | sed -e 's/CURRENT_MINOR/${{ steps.version_check.outputs.current_minor }}/g' -e 's/CURRENT_MAJOR/${{ steps.version_check.outputs.current_major }}/g' >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Print cdn_paths | |
| if: steps.version_check.outputs.version_changed == 'true' | |
| run: echo "${{ steps.cdn_paths.outputs.cdn_paths }}" | |
| - name: Wait before purging jsDelivr cache | |
| if: steps.version_check.outputs.version_changed == 'true' && matrix.package_name == '@tryghost/activitypub' | |
| run: | | |
| echo "Purging jsDelivr cache immediately after publishing a new version on NPM is unreliable. Waiting 1 minute before purging cache..." | |
| sleep 60 | |
| - name: Purge jsDelivr cache | |
| if: steps.version_check.outputs.version_changed == 'true' | |
| uses: gacts/purge-jsdelivr-cache@v1 | |
| with: | |
| url: ${{ steps.cdn_paths.outputs.cdn_paths }} | |
| deploy_tinybird_staging: | |
| name: Deploy Tinybird - Staging | |
| runs-on: ubuntu-latest | |
| needs: [ | |
| job_setup, | |
| job_tinybird-tests | |
| ] | |
| if: always() && github.repository == 'TryGhost/Ghost' && github.event_name == 'push' && needs.job_setup.outputs.changed_tinybird == 'true' && needs.job_setup.result == 'success' && needs.job_tinybird-tests.result == 'success' && needs.job_setup.outputs.is_main == 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Deploy to Staging | |
| uses: ./.github/actions/deploy-tinybird | |
| with: | |
| host: ${{ secrets.TINYBIRD_HOST_STAGING }} | |
| token: ${{ secrets.TINYBIRD_TOKEN_STAGING }} | |
| workspace: 'Staging' | |
| slack-webhook: ${{ secrets.ANALYTICS_SLACK_WEBHOOK_URL }} | |
| deploy_tinybird_production: | |
| name: Deploy Tinybird - Production | |
| runs-on: ubuntu-latest | |
| needs: [ | |
| job_setup, | |
| job_tinybird-tests, | |
| deploy_tinybird_staging | |
| ] | |
| if: always() && github.repository == 'TryGhost/Ghost' && github.event_name == 'push' && needs.job_setup.outputs.changed_tinybird == 'true' && needs.job_setup.result == 'success' && needs.job_tinybird-tests.result == 'success' && needs.deploy_tinybird_staging.result == 'success' && needs.job_setup.outputs.is_main == 'true' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Deploy to Production | |
| uses: ./.github/actions/deploy-tinybird | |
| with: | |
| host: ${{ secrets.TINYBIRD_HOST }} | |
| token: ${{ secrets.TINYBIRD_TOKEN }} | |
| workspace: 'Production' | |
| slack-webhook: ${{ secrets.ANALYTICS_SLACK_WEBHOOK_URL }} | |
| # --------------------------------------------------------------------------- # | |
| # Trigger Pro CD — dispatch to Ghost-Moya cd.yml (runs on main + PRs) | |
| # --------------------------------------------------------------------------- # | |
| trigger_cd: | |
| needs: [job_setup, job_docker_build_production] | |
| name: Trigger Pro CD | |
| runs-on: ubuntu-latest | |
| if: | | |
| always() | |
| && github.repository == 'TryGhost/Ghost' | |
| && needs.job_setup.result == 'success' | |
| && needs.job_docker_build_production.result == 'success' | |
| && needs.job_docker_build_production.outputs.use-artifact != 'true' | |
| steps: | |
| - name: Determine dispatch parameters | |
| id: params | |
| run: | | |
| if [ "${{ needs.job_setup.outputs.is_main }}" = "true" ]; then | |
| echo "pr_number=" >> $GITHUB_OUTPUT | |
| echo "deploy=" >> $GITHUB_OUTPUT | |
| elif [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT | |
| # DISABLED: deploy-to-staging label detection is disabled. | |
| # The label workflow has fundamental problems — admin deploys are global | |
| # (not per-site) and main merges overwrite the deployment immediately. | |
| # See deploy-to-staging.yml for details. | |
| echo "deploy=" >> $GITHUB_OUTPUT | |
| else | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| - name: Dispatch to Ghost-Moya cd.yml | |
| if: steps.params.outputs.skip != 'true' | |
| uses: peter-evans/repository-dispatch@v3 | |
| with: | |
| token: ${{ secrets.CANARY_DOCKER_BUILD }} | |
| repository: TryGhost/Ghost-Moya | |
| event-type: ghost-artifacts-ready | |
| client-payload: >- | |
| { | |
| "ref": "${{ github.sha }}", | |
| "source_repo": "${{ github.repository }}", | |
| "pr_number": "${{ steps.params.outputs.pr_number }}", | |
| "deploy": "${{ steps.params.outputs.deploy }}", | |
| "admin_artifact_id": "${{ needs.job_docker_build_production.outputs.admin-artifact-id }}", | |
| "admin_artifact_run_id": "${{ github.run_id }}" | |
| } |