Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/changelogs/@tryghost!adapter-base-sso@0.1.2.md

This file was deleted.

11 changes: 0 additions & 11 deletions .changeset/changelogs/@tryghost!kg-default-nodes@2.2.0.md

This file was deleted.

12 changes: 0 additions & 12 deletions .changeset/changelogs/@tryghost!kg-html-to-lexical@1.4.0.md

This file was deleted.

12 changes: 0 additions & 12 deletions .changeset/changelogs/@tryghost!kg-lexical-html-renderer@1.5.0.md

This file was deleted.

17 changes: 0 additions & 17 deletions .changeset/changelogs/@tryghost!koenig-lexical@1.9.2.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/clean-flags-retire.md

This file was deleted.

13 changes: 0 additions & 13 deletions .changeset/easy-wasps-appear.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/funky-bikes-chew.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/funky-rice-shine.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/loose-pans-argue.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/olive-regions-stop.md

This file was deleted.

6 changes: 0 additions & 6 deletions .changeset/open-shirts-arrive.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/petite-forks-lick.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/young-spiders-request.md

This file was deleted.

70 changes: 45 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1031,39 +1031,59 @@ jobs:
- name: Setup Playwright
uses: ./.github/actions/setup-playwright

# The Koenig editor suite is the only one that needs apt packages on top
# of the runner image (setup-playwright deliberately avoids apt-get, see
# its README note about hangs — scoping this to one matrix leg contains
# that risk):
# - MS core fonts: caret-position assertions depend on where text wraps,
# which depends on Arial's font metrics
# - playwright firefox deps: system media codecs so firefox can decode
# the H.264 fixtures in the video card tests
# msttcorefonts downloads the font files from SourceForge at install time;
# mirror-redirect roulette there can hang indefinitely. timeout-minutes is
# the backstop, and each network op is bounded + retried so a stuck fetch
# fails fast and re-rolls onto a different mirror instead of sitting.
- name: Install Koenig editor test dependencies (fonts + media codecs)
# Cache the msttcorefonts .ttf files so only a cache miss hits the
# SourceForge download that regularly hangs the koenig-lexical job. Save
# is a separate step right after the fonts are in place, so a later codec
# or test failure on a cache-miss run still populates the cache.
- name: Restore MS core fonts cache
if: matrix.app == '@tryghost/koenig-lexical'
id: mscorefonts-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.cache/msttcorefonts
key: msttcorefonts-ttf-v1
- name: Install Koenig editor test fonts
if: matrix.app == '@tryghost/koenig-lexical'
timeout-minutes: 10
env:
DEBIAN_FRONTEND: noninteractive
DEBCONF_NONINTERACTIVE_SEEN: "true"
run: |
sudo sh -c "echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | debconf-set-selections"
sudo apt-get update -yq -o Acquire::Retries=3 -o Acquire::http::Timeout=30
installed=
for i in 1 2 3; do
if sudo timeout -k 5 120 apt-get install -yq \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 msttcorefonts; then
installed=1; break
FONT_CACHE="$HOME/.cache/msttcorefonts"
FONT_DIR=/usr/share/fonts/truetype/msttcorefonts
sudo mkdir -p "$FONT_DIR"
if ls "$FONT_CACHE"/*.ttf >/dev/null 2>&1; then
echo "Restoring MS core fonts from cache"
sudo cp "$FONT_CACHE"/*.ttf "$FONT_DIR"/
else
echo "Cache miss — installing msttcorefonts from apt"
sudo sh -c "echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | debconf-set-selections"
sudo apt-get update -yq -o Acquire::Retries=3 -o Acquire::http::Timeout=30
installed=
for i in 1 2 3; do
if sudo timeout -k 5 120 apt-get install -yq \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 msttcorefonts; then
installed=1; break
fi
echo "msttcorefonts attempt $i failed/stuck, retrying..."; sleep 5
done
if [ -z "$installed" ]; then
echo "msttcorefonts failed after 3 attempts"; exit 1
fi
echo "msttcorefonts attempt $i failed/stuck, retrying..."; sleep 5
done
if [ -z "$installed" ]; then
echo "msttcorefonts failed after 3 attempts"; exit 1
mkdir -p "$FONT_CACHE"
cp "$FONT_DIR"/*.ttf "$FONT_CACHE"/
fi
pnpm exec playwright install-deps firefox
sudo fc-cache -f
- name: Save MS core fonts cache
if: matrix.app == '@tryghost/koenig-lexical' && steps.mscorefonts-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.cache/msttcorefonts
key: msttcorefonts-ttf-v1
- name: Install Koenig editor test media codecs
if: matrix.app == '@tryghost/koenig-lexical'
timeout-minutes: 10
run: pnpm exec playwright install-deps firefox

- name: Run Playwright tests
run: pnpm nx run ${{ matrix.app }}:test:acceptance
Expand Down
12 changes: 11 additions & 1 deletion apps/admin-x-framework/src/api/automations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export type Automation = {
status: AutomationStatus;
}

export type AutomationStats = {
last_run_created_at: string | null;
total_run_count: number;
in_progress_run_count: number;
}

export type AutomationBrowseItem = Automation & {
stats: AutomationStats;
}

export type AutomationWaitAction = {
id: string;
type: 'wait';
Expand Down Expand Up @@ -62,7 +72,7 @@ export type EditAutomationPayload = {

export interface AutomationsResponseType {
meta?: Meta;
automations: Automation[];
automations: AutomationBrowseItem[];
}

export interface AutomationDetailResponseType {
Expand Down
31 changes: 28 additions & 3 deletions apps/admin/src/automations/automations.acceptance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,50 @@ import { automationsScreen } from "./automations.screen";

// Automations ships behind the `automations` beta labs flag.
const AUTOMATIONS_ENABLED = { labs: { automations: true } };
const RUN_ANALYTICS_ENABLED = { labs: { automations: true, automationRunAnalytics: true } };

describe("Automations list", () => {
it("renders the automations page", async () => {
fakeAutomations([]);
await renderAdminApp("/automations", AUTOMATIONS_ENABLED);

await expect.element(automationsScreen.heading()).toBeVisible();
await expect.element(automationsScreen.columnHeader("Last entry")).not.toBeInTheDocument();
});

it("lists the welcome automations", async () => {
fakeAutomations([
automation({ name: "Free member welcome flow", slug: "member-welcome-email-free", status: "active" }),
automation({ name: "Paid member welcome flow", slug: "member-welcome-email-paid", status: "inactive" }),
automation({
name: "Free member welcome flow",
slug: "member-welcome-email-free",
status: "active",
stats: {
last_run_created_at: "2026-07-21T07:12:00.000Z",
total_run_count: 1432,
in_progress_run_count: 118,
},
}),
automation({
name: "Paid member welcome flow",
slug: "member-welcome-email-paid",
status: "inactive",
stats: {
last_run_created_at: null,
total_run_count: 0,
in_progress_run_count: 0,
},
}),
]);
await renderAdminApp("/automations", AUTOMATIONS_ENABLED);
await renderAdminApp("/automations", RUN_ANALYTICS_ENABLED);

await expect.element(automationsScreen.link("Free member welcome flow")).toBeVisible();
await expect.element(automationsScreen.columnHeader("Last entry")).toBeVisible();
await expect.element(automationsScreen.columnHeader("Total entries")).toBeVisible();
await expect.element(automationsScreen.columnHeader("In progress")).toBeVisible();
const row = automationsScreen.rows();
await expect.element(row).toHaveTextContent("Welcome new free members after they sign up.");
await expect.element(row).toHaveTextContent("1,432");
await expect.element(row).toHaveTextContent("118");
await expect.element(row).toHaveTextContent("Live");
// Stripe is disconnected in the default boot, which hides the paid welcome flow.
await expect(automationsScreen.rows()).toHaveCount(1);
Expand Down
1 change: 1 addition & 0 deletions apps/admin/src/automations/automations.screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { automationListRow, automationsList, automationsPage } from "@tryghost/t
export const automationsScreen = {
heading: () => page.getByTestId(automationsPage).getByRole("heading", { name: "Automations" }),
list: () => page.getByTestId(automationsList),
columnHeader: (name: string) => page.getByTestId(automationsList).getByRole("columnheader", { name }),
rows: () => page.getByTestId(automationListRow),
link: (name: string) => page.getByRole("link", { name, exact: true }),
};
36 changes: 34 additions & 2 deletions apps/admin/src/automations/automations.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import {MemoryRouter} from 'react-router';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {render, screen} from '@testing-library/react';

const mockRunAnalyticsFlag = vi.hoisted(() => ({enabled: true}));

vi.mock('@tryghost/admin-x-framework/hooks', async () => {
const actual = await vi.importActual<typeof import('@tryghost/admin-x-framework/hooks')>('@tryghost/admin-x-framework/hooks');
return {
...actual,
useFeatureFlag: () => mockRunAnalyticsFlag.enabled
};
});

const {mockUseBrowseAutomations, mockUseBrowseSettings, mockUseBrowseConfig, mockUseCurrentUser} = vi.hoisted(() => ({
mockUseBrowseAutomations: vi.fn(),
mockUseBrowseSettings: vi.fn(),
Expand Down Expand Up @@ -65,12 +75,22 @@ const automations = [{
id: 'automation-id-1',
name: 'Free member welcome flow',
slug: 'member-welcome-email-free',
status: 'active' as const
status: 'active' as const,
stats: {
last_run_created_at: '2026-07-21T07:12:00.000Z',
total_run_count: 1432,
in_progress_run_count: 118
}
}, {
id: 'automation-id-2',
name: 'Paid member welcome flow',
slug: 'member-welcome-email-paid',
status: 'inactive' as const
status: 'inactive' as const,
stats: {
last_run_created_at: null,
total_run_count: 0,
in_progress_run_count: 0
}
}];

const stripeConnectedSettings = {
Expand All @@ -85,12 +105,24 @@ const renderPage = () => render(<MemoryRouter><Automations /></MemoryRouter>);
describe('Automations', () => {
beforeEach(() => {
vi.clearAllMocks();
mockRunAnalyticsFlag.enabled = true;
mockUseBrowseAutomations.mockReturnValue({data: {automations}, isError: false, isLoading: false});
mockUseBrowseSettings.mockReturnValue({data: stripeConnectedSettings, isLoading: false});
mockUseBrowseConfig.mockReturnValue({data: {config: {}}, isLoading: false});
mockUseCurrentUser.mockReturnValue({data: {id: 'user-1', roles: [{name: 'Owner'}]}});
});

it('hides run analytics when the private feature is disabled', () => {
mockRunAnalyticsFlag.enabled = false;

renderPage();

expect(screen.queryByRole('columnheader', {name: 'Last entry'})).not.toBeInTheDocument();
expect(screen.queryByRole('columnheader', {name: 'Total entries'})).not.toBeInTheDocument();
expect(screen.queryByRole('columnheader', {name: 'In progress'})).not.toBeInTheDocument();
expect(screen.queryByText('1,432')).not.toBeInTheDocument();
});

it('shows free and paid sequences when Stripe is connected', () => {
renderPage();

Expand Down
4 changes: 3 additions & 1 deletion apps/admin/src/automations/automations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {Box, Container} from '@tryghost/shade/primitives';
import {ListPage} from '@tryghost/shade/page-templates';
import {PageHeader} from '@tryghost/shade/patterns';
import {useVisibleAutomations} from './hooks/use-visible-automations';
import {useFeatureFlag} from '@tryghost/admin-x-framework/hooks';

const Automations: React.FC = () => {
const {automations, error, isError, isLoading} = useVisibleAutomations();
const showRunAnalytics = useFeatureFlag('automationRunAnalytics');

if (isError) {
throw error instanceof Error ? error : new Error('Failed to load automations');
Expand All @@ -31,7 +33,7 @@ const Automations: React.FC = () => {
</PageHeader>
</ListPage.Header>
<ListPage.Body>
<AutomationsList automations={automations} isLoading={isLoading} />
<AutomationsList automations={automations} isLoading={isLoading} showRunAnalytics={showRunAnalytics} />
<AutomationsHelpCards />
</ListPage.Body>
</ListPage>
Expand Down
Loading
Loading