1839 lines
70 KiB
YAML
1839 lines
70 KiB
YAML
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
|
|
tags-ignore:
|
|
- '**' # Tags handled by ci-release.yml
|
|
workflow_call: # Called by ci-release.yml for uninterruptible release CI
|
|
|
|
env:
|
|
FORCE_COLOR: 1
|
|
HEAD_COMMIT: ${{ github.sha }}
|
|
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.ref }}
|
|
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_TAG: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
IS_DEVELOPMENT: ${{ github.ref == 'refs/heads/main' || 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' }}
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
# Required by dorny/paths-filter, which calls pulls.listFiles on
|
|
# pull_request events. Private forks of this repo don't grant this
|
|
# implicitly when an explicit permissions block is present, so it must
|
|
# be listed here for the Setup job to succeed on those forks.
|
|
pull-requests: read
|
|
steps:
|
|
- name: Checkout current commit
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: ${{ env.HEAD_COMMIT }}
|
|
fetch-depth: 0
|
|
# fetch a treeless clone to improve checkout speed, the job will fetch contents later if needed
|
|
filter: 'tree:0'
|
|
|
|
- name: Output GitHub context
|
|
if: env.RUNNER_DEBUG == '1'
|
|
run: |
|
|
echo "GITHUB_EVENT_NAME: ${{ github.event_name }}"
|
|
echo "GITHUB_CONTEXT: ${{ toJson(github.event) }}"
|
|
|
|
- name: Set SHAs for Nx Commands
|
|
if: env.IS_TAG != 'true'
|
|
uses: nrwl/nx-set-shas@afb73a62d26e41464e9254689e1fd6122ee683c1 # v5.0.1
|
|
with:
|
|
main-branch-name: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }}
|
|
error-on-no-successful-workflow: ${{ env.IS_MAIN == 'true' }}
|
|
|
|
- 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
|
|
if: env.IS_TAG != 'true'
|
|
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
|
id: added
|
|
with:
|
|
base: ${{ env.NX_BASE }}
|
|
filters: |
|
|
new-package:
|
|
- added: 'ghost/**/package.json'
|
|
|
|
- name: Determine changed packages
|
|
if: env.IS_TAG != 'true'
|
|
uses: AurorNZ/paths-filter@c9dd42e99db87803313ff6f4b1150cc9f6c836af # v5.0.0
|
|
id: changed
|
|
with:
|
|
base: ${{ env.NX_BASE }}
|
|
filters: |
|
|
shared: &shared
|
|
- '.github/**'
|
|
- '.npmrc'
|
|
- 'package.json'
|
|
- 'pnpm-lock.yaml'
|
|
- 'pnpm-workspace.yaml'
|
|
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:
|
|
- '.github/workflows/ci.yml'
|
|
- 'compose.dev.analytics.yaml'
|
|
- 'ghost/core/core/server/data/tinybird/**'
|
|
- '!ghost/core/core/server/data/tinybird/**/*.md'
|
|
tinybird-datafiles:
|
|
- 'ghost/core/core/server/data/tinybird/**'
|
|
- '!ghost/core/core/server/data/tinybird/**/*.md'
|
|
any-code:
|
|
- '!**/*.md'
|
|
|
|
- name: Define Node test matrix
|
|
id: node_matrix
|
|
run: |
|
|
echo 'matrix=["22.18.0"]' >> $GITHUB_OUTPUT
|
|
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- name: Set up Node
|
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Determine Affected Projects
|
|
if: env.IS_TAG != 'true'
|
|
id: affected
|
|
run: |
|
|
AFFECTED_PROJECTS=$(pnpm -s nx show projects --affected --json | tr -d '\n')
|
|
echo "affected_projects=$AFFECTED_PROJECTS" >> "$GITHUB_OUTPUT"
|
|
|
|
outputs:
|
|
affected_projects: ${{ steps.affected.outputs.affected_projects }}
|
|
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_tinybird_datafiles: ${{ steps.changed.outputs.tinybird-datafiles }}
|
|
changed_any_code: ${{ steps.changed.outputs.any-code }}
|
|
changed_new_package: ${{ steps.added.outputs.new-package }}
|
|
is_main: ${{ env.IS_MAIN }}
|
|
is_tag: ${{ env.IS_TAG }}
|
|
is_development: ${{ env.IS_DEVELOPMENT }}
|
|
is_six: ${{ env.IS_SIX }}
|
|
is_six_pr: ${{ env.IS_SIX_PR }}
|
|
member_is_in_org: ${{ steps.check_user_org_membership.outputs.is_member }}
|
|
node_version: ${{ env.NODE_VERSION }}
|
|
node_test_matrix: ${{ steps.node_matrix.outputs.matrix }}
|
|
nx_base: ${{ env.NX_BASE }}
|
|
|
|
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
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.is_tag == 'true' || needs.job_setup.outputs.changed_any_code == 'true'
|
|
name: Lint
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 1000
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
|
with:
|
|
path: ghost/**/.eslintcache
|
|
key: eslint-cache
|
|
|
|
- name: Lint all (tags)
|
|
if: needs.job_setup.outputs.is_tag == 'true'
|
|
run: pnpm nx run-many -t lint
|
|
|
|
- name: Lint affected (branches)
|
|
if: needs.job_setup.outputs.is_tag != 'true'
|
|
run: pnpm nx affected -t lint
|
|
env:
|
|
NX_BASE: ${{ needs.job_setup.outputs.nx_base }}
|
|
NX_HEAD: ${{ env.HEAD_COMMIT }}
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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.is_tag == 'true'
|
|
|| 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Run i18n tests
|
|
run: pnpm nx run @tryghost/i18n:test
|
|
|
|
job_admin-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: [job_setup]
|
|
if: needs.job_setup.outputs.is_tag == 'true' || 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- run: pnpm nx run ghost-admin:test
|
|
env:
|
|
BROWSER: Chrome
|
|
|
|
# Merge coverage reports and upload
|
|
- name: Merge Admin test coverage
|
|
run: pnpm ember coverage-merge
|
|
working-directory: ghost/admin
|
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: admin-coverage
|
|
path: ghost/*/coverage/cobertura-coverage.xml
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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_unit-tests:
|
|
runs-on: ubuntu-latest
|
|
needs: [job_setup]
|
|
if: needs.job_setup.outputs.is_tag == 'true' || 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 1000
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ matrix.node }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
# sqlite3 is an optionalDependency. Without --force, pnpm may skip
|
|
# installing/linking it when restoring from a cached store. --force
|
|
# ensures all optional deps are installed regardless.
|
|
# (ghost core's test:unit job requires sqlite3)
|
|
run: pnpm install --frozen-lockfile --force
|
|
|
|
- name: Set timezone (non-UTC)
|
|
uses: szenius/set-timezone@1f9716b0f7120e344f0c62bb7b1ee98819aefd42 # v2.0
|
|
with:
|
|
timezoneLinux: "America/New_York"
|
|
|
|
- name: Run unit tests (tags — all)
|
|
if: needs.job_setup.outputs.is_tag == 'true'
|
|
run: pnpm nx run-many -t test:unit
|
|
env:
|
|
FORCE_COLOR: 0
|
|
GHOST_UNIT_TEST_VARIANT: ci
|
|
NX_SKIP_LOG_GROUPING: true
|
|
logging__level: fatal
|
|
|
|
- name: Run unit tests (branches — affected)
|
|
if: needs.job_setup.outputs.is_tag != 'true'
|
|
run: pnpm nx affected -t test:unit
|
|
env:
|
|
FORCE_COLOR: 0
|
|
GHOST_UNIT_TEST_VARIANT: ci
|
|
NX_SKIP_LOG_GROUPING: true
|
|
logging__level: fatal
|
|
NX_BASE: ${{ needs.job_setup.outputs.nx_base }}
|
|
NX_HEAD: ${{ env.HEAD_COMMIT }}
|
|
|
|
- name: Check for unexpected file changes
|
|
run: |
|
|
if [ -n "$(git status --porcelain)" ]; then
|
|
echo "Tests generated unexpected file changes. Commit them before merging:"
|
|
git status
|
|
git diff
|
|
exit 1
|
|
fi
|
|
|
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
if: matrix.node == env.NODE_VERSION
|
|
with:
|
|
name: unit-coverage
|
|
path: ghost/*/coverage/cobertura-coverage.xml
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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.is_tag == 'true' || 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
|
|
redis:
|
|
image: redis:7.0
|
|
ports:
|
|
- 6379:6379
|
|
options: >-
|
|
--health-cmd "redis-cli ping"
|
|
--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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ matrix.node }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
# sqlite3 is an optionalDependency. Without --force, pnpm may skip
|
|
# installing/linking it when restoring from a cached store. --force
|
|
# ensures all optional deps are installed regardless.
|
|
run: |
|
|
if [ "${{ matrix.env.DB }}" = "sqlite3" ]; then
|
|
pnpm install --frozen-lockfile --force
|
|
else
|
|
pnpm install --frozen-lockfile
|
|
fi
|
|
|
|
- name: Set timezone (non-UTC)
|
|
uses: szenius/set-timezone@1f9716b0f7120e344f0c62bb7b1ee98819aefd42 # 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
|
|
run: pnpm nx run ghost:test:ci:e2e
|
|
|
|
- name: Integration tests
|
|
run: pnpm nx run ghost:test:ci:integration
|
|
|
|
- name: Check for unexpected file changes
|
|
run: |
|
|
if [ -n "$(git status --porcelain)" ]; then
|
|
echo "Tests generated unexpected file changes. Commit them before merging:"
|
|
git status
|
|
git diff
|
|
exit 1
|
|
fi
|
|
|
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
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@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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.is_tag == 'true' || 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
submodules: true
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ matrix.node }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
# sqlite3 is an optionalDependency. Without --force, pnpm may skip
|
|
# installing/linking it when restoring from a cached store. --force
|
|
# ensures all optional deps are installed regardless.
|
|
run: |
|
|
if [ "${{ matrix.env.DB }}" = "sqlite3" ]; then
|
|
pnpm install --frozen-lockfile --force
|
|
else
|
|
pnpm install --frozen-lockfile
|
|
fi
|
|
|
|
- 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
|
|
run: pnpm nx run ghost:test:ci:legacy
|
|
|
|
- name: Check for unexpected file changes
|
|
run: |
|
|
if [ -n "$(git status --porcelain)" ]; then
|
|
echo "Tests generated unexpected file changes. Commit them before merging:"
|
|
git status
|
|
git diff
|
|
exit 1
|
|
fi
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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.is_tag == 'true' || needs.job_setup.outputs.changed_admin_x_settings == 'true'
|
|
name: Admin-X Settings tests
|
|
env:
|
|
CI: true
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Setup Playwright
|
|
uses: ./.github/actions/setup-playwright
|
|
|
|
- run: pnpm nx run @tryghost/admin-x-settings:test:acceptance
|
|
|
|
- name: Upload test results
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: admin-x-settings-playwright-report
|
|
path: apps/admin-x-settings/playwright-report
|
|
retention-days: 30
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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.is_tag == 'true' || needs.job_setup.outputs.changed_activitypub == 'true'
|
|
name: ActivityPub tests
|
|
env:
|
|
CI: true
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Setup Playwright
|
|
uses: ./.github/actions/setup-playwright
|
|
|
|
- run: pnpm nx run @tryghost/activitypub:test:acceptance
|
|
|
|
- name: Upload test results
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: activitypub-playwright-report
|
|
path: apps/activitypub/playwright-report
|
|
retention-days: 30
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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.is_tag == 'true' || needs.job_setup.outputs.changed_comments_ui == 'true'
|
|
name: Comments-UI tests
|
|
env:
|
|
CI: true
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Setup Playwright
|
|
uses: ./.github/actions/setup-playwright
|
|
|
|
- run: pnpm nx run @tryghost/comments-ui:test
|
|
|
|
- name: Upload test results
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: comments-ui-playwright-report
|
|
path: apps/comments-ui/playwright-report
|
|
retention-days: 30
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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.is_tag == 'true' || needs.job_setup.outputs.changed_signup_form == 'true'
|
|
name: Signup-form tests
|
|
env:
|
|
CI: true
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Setup Playwright
|
|
uses: ./.github/actions/setup-playwright
|
|
|
|
- run: pnpm nx run @tryghost/signup-form:test:e2e
|
|
|
|
- name: Upload test results
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: signup-form-playwright-report
|
|
path: apps/signup-form/playwright-report
|
|
retention-days: 30
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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.is_tag == 'true' || needs.job_setup.outputs.changed_tinybird == 'true'
|
|
defaults:
|
|
run:
|
|
working-directory: ghost/core/core/server/data/tinybird
|
|
services:
|
|
tinybird:
|
|
image: tinybirdco/tinybird-local:latest@sha256:52ea15fc337547b13d06069c23479c293e23074d4e4a6be21253e4bd57ad12be
|
|
ports:
|
|
- 7181:7181
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- 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: Trigger and watch traffic analytics infra Tinybird workflow
|
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
|
env:
|
|
GH_TOKEN: ${{ secrets.TRAFFIC_ANALYTICS_GITHUB_TOKEN }}
|
|
uses: ./.github/actions/dispatch-workflow
|
|
with:
|
|
repo: TryGhost/traffic-analytics-infra
|
|
workflow: tinybird.yml
|
|
branch: main
|
|
dispatch-inputs: >-
|
|
{
|
|
"ghost_ref": "${{ github.sha }}",
|
|
"caller_run_id": "${{ github.run_id }}",
|
|
"run_local_tests": false
|
|
}
|
|
|
|
job_ghost-cli:
|
|
name: Ghost-CLI tests
|
|
needs: [job_setup]
|
|
if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_core == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 0
|
|
submodules: true
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install Ghost-CLI
|
|
run: npm install -g ghost-cli@latest
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- run: node .github/scripts/bump-version.js canary
|
|
|
|
- run: pnpm archive
|
|
|
|
- run: mv ghost-*.tgz ghost.tgz
|
|
working-directory: ghost/core
|
|
|
|
- name: Save Ghost CLI Debug Logs
|
|
if: failure()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
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@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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_build_artifacts:
|
|
name: Build & Publish Artifacts
|
|
needs: [job_setup]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
submodules: true
|
|
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- 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
|
|
pnpm build:production
|
|
|
|
- name: Verify tag matches package.json
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
working-directory: ghost/core
|
|
run: |
|
|
PKG_VERSION=$(node -p "require('./package.json').version")
|
|
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
|
echo "::error::Tag ${GITHUB_REF_NAME} doesn't match package.json version ${PKG_VERSION}"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Pack standalone distribution
|
|
run: pnpm --filter ghost pack:standalone
|
|
|
|
- name: Create npm tarball
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
run: pnpm --filter ghost pack:tarball
|
|
|
|
- name: Upload npm tarball
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: ghost-npm-tarball
|
|
path: ghost/core/ghost-*.tgz
|
|
retention-days: 7
|
|
if-no-files-found: error
|
|
|
|
- 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
|
|
|
|
# Force push on tag pushes (release images must always be published)
|
|
IS_TAG="${{ startsWith(github.ref, 'refs/tags/v') }}"
|
|
if [ "$IS_TAG" = "true" ]; then
|
|
USE_ARTIFACT="false"
|
|
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
|
|
echo "image-e2e-name=${IMAGE_FULL_NAME}-e2e" >> $GITHUB_OUTPUT
|
|
|
|
- name: Upload admin artifact for CD
|
|
id: upload-admin
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
if: steps.strategy.outputs.should-push == 'true'
|
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Docker meta (core)
|
|
id: meta-core
|
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
|
with:
|
|
images: ${{ steps.strategy.outputs.image-core-name }}
|
|
tags: |
|
|
type=ref,event=branch
|
|
type=ref,event=pr
|
|
type=sha
|
|
type=semver,pattern=v{{version}}
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
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@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
|
with:
|
|
images: ${{ steps.strategy.outputs.image-full-name }}
|
|
tags: |
|
|
type=ref,event=branch
|
|
type=ref,event=pr
|
|
type=sha
|
|
type=semver,pattern=v{{version}}
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
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@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
|
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@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
|
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
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 }}
|
|
image-e2e-name: ${{ steps.strategy.outputs.image-e2e-name }}
|
|
|
|
job_build_e2e_public_apps:
|
|
name: Build E2E Public App Assets
|
|
needs: [job_setup]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
env:
|
|
FORCE_COLOR: 0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Build public apps for E2E
|
|
run: pnpm --filter @tryghost/e2e build:apps
|
|
|
|
- name: Pack public app artifacts
|
|
run: |
|
|
tar -czf e2e-public-apps.tar.gz \
|
|
apps/portal/umd \
|
|
apps/comments-ui/umd \
|
|
apps/sodo-search/umd \
|
|
apps/signup-form/umd \
|
|
apps/announcement-bar/umd
|
|
ls -lh e2e-public-apps.tar.gz
|
|
|
|
- name: Upload public app artifacts
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: e2e-public-apps
|
|
path: e2e-public-apps.tar.gz
|
|
retention-days: 1
|
|
|
|
job_build_e2e_image:
|
|
name: Build E2E Docker Image
|
|
needs: [job_setup, job_build_e2e_public_apps, job_build_artifacts]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Download public app artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
with:
|
|
name: e2e-public-apps
|
|
|
|
- name: Extract public app artifacts
|
|
run: tar -xzf e2e-public-apps.tar.gz
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
|
with:
|
|
# Fork/cross-repo PRs use artifact transfer (no GHCR push). The default
|
|
# docker-container driver runs in an isolated BuildKit container that
|
|
# cannot see locally loaded images, so we fall back to the docker driver
|
|
# which shares the host daemon's image store.
|
|
driver: ${{ needs.job_build_artifacts.outputs.use-artifact == 'true' && 'docker' || '' }}
|
|
|
|
- name: Load base Ghost image
|
|
uses: ./.github/actions/load-docker-image
|
|
id: load-base
|
|
with:
|
|
use-artifact: ${{ needs.job_build_artifacts.outputs.use-artifact }}
|
|
image-tags: ${{ needs.job_build_artifacts.outputs.image-tags }}
|
|
artifact-name: docker-image-production
|
|
|
|
- name: Determine E2E image distribution strategy
|
|
id: strategy
|
|
run: |
|
|
USE_ARTIFACT="${{ needs.job_build_artifacts.outputs.use-artifact }}"
|
|
SHOULD_PUSH="true"
|
|
if [ "$USE_ARTIFACT" = "true" ]; then
|
|
SHOULD_PUSH="false"
|
|
fi
|
|
|
|
echo "use-artifact=$USE_ARTIFACT" >> $GITHUB_OUTPUT
|
|
echo "should-push=$SHOULD_PUSH" >> $GITHUB_OUTPUT
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
if: steps.strategy.outputs.should-push == 'true'
|
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Docker meta (e2e)
|
|
id: meta-e2e
|
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
|
with:
|
|
images: ${{ needs.job_build_artifacts.outputs.image-e2e-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 E2E
|
|
org.opencontainers.image.description=Ghost production build with public E2E app bundles
|
|
org.opencontainers.image.vendor=TryGhost
|
|
|
|
- name: Build & push E2E image
|
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
|
with:
|
|
context: .
|
|
file: e2e/Dockerfile.e2e
|
|
build-args: |
|
|
GHOST_IMAGE=${{ steps.load-base.outputs.image-tag }}
|
|
push: ${{ steps.strategy.outputs.should-push }}
|
|
load: ${{ steps.strategy.outputs.use-artifact == 'true' }}
|
|
tags: ${{ steps.meta-e2e.outputs.tags }}
|
|
labels: ${{ steps.meta-e2e.outputs.labels }}
|
|
cache-from: ${{ steps.strategy.outputs.should-push == 'true' && format('type=registry,ref={0}:cache-main', needs.job_build_artifacts.outputs.image-e2e-name) || '' }}
|
|
cache-to: ${{ steps.strategy.outputs.should-push == 'true' && format('type=registry,ref={0}:cache-{1},mode=max', needs.job_build_artifacts.outputs.image-e2e-name, github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'main') || '' }}
|
|
|
|
- name: Save E2E image as artifact
|
|
if: steps.strategy.outputs.use-artifact == 'true'
|
|
run: |
|
|
IMAGE_TAG=$(echo "${{ steps.meta-e2e.outputs.tags }}" | head -n1)
|
|
echo "Saving image: $IMAGE_TAG"
|
|
docker save "$IMAGE_TAG" | gzip > docker-image-e2e.tar.gz
|
|
echo "Image saved as docker-image-e2e.tar.gz"
|
|
ls -lh docker-image-e2e.tar.gz
|
|
|
|
- name: Upload E2E image artifact
|
|
if: steps.strategy.outputs.use-artifact == 'true'
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: docker-image-e2e
|
|
path: docker-image-e2e.tar.gz
|
|
retention-days: 1
|
|
|
|
outputs:
|
|
image-tags: ${{ steps.meta-e2e.outputs.tags }}
|
|
use-artifact: ${{ steps.strategy.outputs.use-artifact }}
|
|
|
|
job_e2e_tests:
|
|
name: E2E Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
|
runs-on: ubuntu-latest
|
|
needs: [job_build_e2e_image, job_setup]
|
|
strategy:
|
|
fail-fast: true
|
|
matrix:
|
|
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
|
|
shardTotal: [8]
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
|
|
|
- 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_build_e2e_image.outputs.use-artifact }}
|
|
image-tags: ${{ needs.job_build_e2e_image.outputs.image-tags }}
|
|
artifact-name: docker-image-e2e
|
|
|
|
- name: Setup Docker Registry Mirrors
|
|
uses: ./.github/actions/setup-docker-registry-mirrors
|
|
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Prepare E2E CI job
|
|
env:
|
|
GHOST_E2E_IMAGE: ${{ steps.load.outputs.image-tag }}
|
|
GHOST_E2E_SKIP_IMAGE_BUILD: 'true'
|
|
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: ${{ steps.load.outputs.image-tag }}
|
|
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: pnpm --filter @tryghost/e2e infra:down
|
|
|
|
- name: Upload blob report to GitHub Actions Artifacts
|
|
if: failure()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: blob-report-${{ matrix.shardIndex }}
|
|
path: e2e/blob-report
|
|
retention-days: 1
|
|
|
|
- name: Upload test results artifacts
|
|
if: failure()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: test-results-${{ matrix.shardIndex }}
|
|
path: e2e/test-results
|
|
retention-days: 7
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Download blob reports from GitHub Actions Artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Restore Admin coverage
|
|
if: contains(needs.job_admin-tests.result, 'success')
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
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@75cd11691c0faa626561e295848008c8a7dddffe # v5
|
|
with:
|
|
flags: admin-tests
|
|
|
|
- name: Restore E2E coverage
|
|
if: contains(needs.job_acceptance-tests.result, 'success')
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
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@75cd11691c0faa626561e295848008c8a7dddffe # 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_admin_x_settings,
|
|
job_activitypub,
|
|
job_comments_ui,
|
|
job_signup_form,
|
|
job_tinybird-tests,
|
|
job_build_e2e_public_apps,
|
|
job_build_e2e_image,
|
|
job_e2e_tests,
|
|
publish_packages
|
|
]
|
|
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'
|
|
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: pnpm
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Check if version changed
|
|
if: needs.job_setup.outputs.is_main == 'true'
|
|
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
|
|
|
|
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
|
|
|
|
- name: Build the package
|
|
if: steps.version_check.outputs.version_changed == 'true' || github.event_name == 'pull_request'
|
|
run: pnpm nx build ${{ matrix.package_name }}
|
|
|
|
- name: Configure .npmrc
|
|
if: needs.job_setup.outputs.is_main == 'true' && 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: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true'
|
|
run: npm install -g npm@11
|
|
|
|
- name: Publish to npm
|
|
if: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true'
|
|
working-directory: ${{ matrix.package_path }}
|
|
run: |
|
|
npm publish --access public
|
|
|
|
- name: Replace version placeholders in cdn-paths
|
|
id: cdn_paths
|
|
if: needs.job_setup.outputs.is_main == 'true' && 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: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true'
|
|
run: echo "${{ steps.cdn_paths.outputs.cdn_paths }}"
|
|
|
|
- name: Wait before purging jsDelivr cache
|
|
if: needs.job_setup.outputs.is_main == 'true' && 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: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true'
|
|
uses: gacts/purge-jsdelivr-cache@8d92aea944f1a3e8ad70505379e1a8ac72d56b73 # v1
|
|
with:
|
|
url: ${{ steps.cdn_paths.outputs.cdn_paths }}
|
|
|
|
deploy_tinybird:
|
|
name: Deploy Tinybird
|
|
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_datafiles == 'true' && needs.job_setup.result == 'success' && needs.job_tinybird-tests.result == 'success' && needs.job_setup.outputs.is_main == 'true'
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
- name: Trigger and watch traffic analytics infra Tinybird workflow
|
|
if: github.repository == 'TryGhost/Ghost'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.TRAFFIC_ANALYTICS_GITHUB_TOKEN }}
|
|
uses: ./.github/actions/dispatch-workflow
|
|
with:
|
|
repo: TryGhost/traffic-analytics-infra
|
|
workflow: tinybird.yml
|
|
branch: main
|
|
dispatch-inputs: >-
|
|
{
|
|
"ghost_ref": "${{ github.sha }}",
|
|
"caller_run_id": "${{ github.run_id }}",
|
|
"run_local_tests": false,
|
|
"deploy_staging": true,
|
|
"deploy_production": true
|
|
}
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Trigger Pro CD — dispatch to Ghost-Moya cd.yml (runs on main + PRs)
|
|
# --------------------------------------------------------------------------- #
|
|
trigger_cd:
|
|
needs: [job_setup, job_build_artifacts]
|
|
name: Trigger Pro CD
|
|
runs-on: ubuntu-latest
|
|
if: |
|
|
always()
|
|
&& github.repository == 'TryGhost/Ghost'
|
|
&& needs.job_setup.result == 'success'
|
|
&& needs.job_build_artifacts.result == 'success'
|
|
&& needs.job_build_artifacts.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 [ "${{ needs.job_setup.outputs.is_tag }}" = "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@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
|
with:
|
|
token: ${{ secrets.CANARY_DOCKER_BUILD }}
|
|
repository: TryGhost/Ghost-Moya
|
|
event-type: ghost-artifacts-ready
|
|
client-payload: >-
|
|
{
|
|
"ref": "${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || github.sha }}",
|
|
"source_repo": "${{ github.repository }}",
|
|
"pr_number": "${{ steps.params.outputs.pr_number }}",
|
|
"deploy": "${{ steps.params.outputs.deploy }}",
|
|
"admin_artifact_id": "${{ needs.job_build_artifacts.outputs.admin-artifact-id }}",
|
|
"admin_artifact_run_id": "${{ github.run_id }}"
|
|
}
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Publish Ghost npm package — runs on version tags only (OIDC, no stored token)
|
|
# --------------------------------------------------------------------------- #
|
|
publish_ghost:
|
|
needs: [job_setup, job_build_artifacts]
|
|
name: Publish Ghost to npm
|
|
runs-on: ubuntu-latest
|
|
if: |
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
&& github.repository == 'TryGhost/Ghost'
|
|
&& needs.job_build_artifacts.result == 'success'
|
|
environment: npm-release
|
|
permissions:
|
|
id-token: write
|
|
steps:
|
|
- name: Download npm tarball
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
with:
|
|
name: ghost-npm-tarball
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
package-manager-cache: false
|
|
|
|
# TODO: Remove once Node v24 ships with npm >= 11
|
|
- name: Install npm v11 (required for OIDC publishing)
|
|
run: npm install -g npm@11
|
|
|
|
- name: Verify tarball contents
|
|
run: |
|
|
echo "Tarball contents:"
|
|
tar -tzf ghost-*.tgz | head -20
|
|
tar -tzf ghost-*.tgz | grep -q 'package/.npmrc' || { echo "::error::.npmrc not found in tarball"; exit 1; }
|
|
tar -tzf ghost-*.tgz | grep -q 'package/pnpm-lock.yaml' || { echo "::error::pnpm-lock.yaml not found in tarball"; exit 1; }
|
|
tar -xOf ghost-*.tgz package/package.json | jq -e '.packageManager' >/dev/null || { echo "::error::packageManager not found in packaged package.json"; exit 1; }
|
|
|
|
- name: Publish to npm
|
|
run: npm publish ghost-*.tgz --access public
|
|
|
|
- uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main
|
|
if: failure()
|
|
with:
|
|
status: ${{ job.status }}
|
|
env:
|
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Create GitHub Release — runs after successful npm publish
|
|
# --------------------------------------------------------------------------- #
|
|
create_github_release:
|
|
needs: [publish_ghost]
|
|
name: Create GitHub Release
|
|
runs-on: ubuntu-latest
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
permissions:
|
|
contents: write
|
|
env:
|
|
GH_TOKEN: ${{ secrets.CANARY_DOCKER_BUILD }}
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve previous tag
|
|
id: prev_tag
|
|
run: |
|
|
CURRENT_TAG="${GITHUB_REF_NAME}"
|
|
# Find the tag immediately before this one (excluding pre-releases)
|
|
PREV_TAG=$(git tag --list 'v[0-9]*' --sort=-version:refname | grep -v '-' | grep -v "^${CURRENT_TAG}$" | head -n 1)
|
|
if [ -z "$PREV_TAG" ]; then
|
|
echo "::warning::No previous stable tag found — release notes will use fallback message"
|
|
fi
|
|
echo "tag=${PREV_TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "Previous tag: ${PREV_TAG:-<none>}"
|
|
|
|
- name: Generate release notes
|
|
id: notes
|
|
run: |
|
|
PREV_TAG="${{ steps.prev_tag.outputs.tag }}"
|
|
if [ -n "$PREV_TAG" ]; then
|
|
node scripts/lib/release-notes.js "$PREV_TAG" "${GITHUB_REF_NAME}" > /tmp/release-notes.md
|
|
else
|
|
echo "This release contains fixes for minor bugs and issues reported by Ghost users." > /tmp/release-notes.md
|
|
fi
|
|
cat /tmp/release-notes.md
|
|
|
|
- name: Create GitHub Release
|
|
run: |
|
|
gh release create "${GITHUB_REF_NAME}" \
|
|
--title "${GITHUB_REF_NAME}" \
|
|
--notes-file /tmp/release-notes.md
|
|
|
|
- name: Notify Slack
|
|
if: always() && steps.notes.outcome == 'success'
|
|
run: |
|
|
VERSION="${GITHUB_REF_NAME}"
|
|
RELEASE_URL="https://github.com/TryGhost/Ghost/releases/tag/${VERSION}"
|
|
CHANGELOG=$(cat /tmp/release-notes.md | head -c 3000)
|
|
|
|
# Build Slack payload — use --rawfile so newlines in release notes are preserved
|
|
PAYLOAD=$(jq -n \
|
|
--arg header ":ghost: Ghost ${VERSION} is loose! - ${RELEASE_URL}" \
|
|
--rawfile notes /tmp/release-notes.md \
|
|
'{text: ($header + "\n\n" + $notes)}')
|
|
|
|
curl -sf -X POST \
|
|
-H 'Content-type: application/json' \
|
|
--data "${PAYLOAD}" \
|
|
"${{ secrets.RELEASE_NOTIFICATION_URL }}" || echo "Slack notification failed (non-fatal)"
|