feat: enhance build process and add readiness checks for components
Some checks failed
docker-images / resolve-build-targets (push) Successful in 6s
ui-regression / playwright-regression (push) Failing after 13m44s
docker-images / build-and-push (admin) (push) Successful in 1m13s
docker-images / build-and-push (backend) (push) Successful in 45m36s
docker-images / build-and-push (frontend) (push) Successful in 1m29s
docker-images / submit-indexnow (push) Successful in 18s

This commit is contained in:
2026-04-02 14:57:01 +08:00
parent 3628a46ed1
commit ebfb9c7838
6 changed files with 137 additions and 40 deletions

View File

@@ -21,13 +21,16 @@ jobs:
resolve-build-targets:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.targets.outputs.matrix }}
count: ${{ steps.targets.outputs.count }}
backend_changed: ${{ steps.targets.outputs.backend_changed }}
admin_changed: ${{ steps.targets.outputs.admin_changed }}
frontend_changed: ${{ steps.targets.outputs.frontend_changed }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve build targets
id: targets
@@ -91,40 +94,32 @@ jobs:
[ -n "${SELECTED[admin]:-}" ] && COMPONENTS+=(admin)
COMPONENTS_CSV="$(IFS=,; echo "${COMPONENTS[*]}")"
export COMPONENTS_CSV
BACKEND_CHANGED=false
FRONTEND_CHANGED=false
ADMIN_CHANGED=false
COUNT=0
python <<'PY' >> "$GITHUB_OUTPUT"
import json
import os
if [ -n "${SELECTED[backend]:-}" ]; then
BACKEND_CHANGED=true
COUNT=$((COUNT + 1))
fi
mapping = {
"backend": {
"component": "backend",
"dockerfile": "backend/Dockerfile",
"context": "backend",
"default_image_name": "termi-astro-backend",
},
"frontend": {
"component": "frontend",
"dockerfile": "frontend/Dockerfile",
"context": "frontend",
"default_image_name": "termi-astro-frontend",
},
"admin": {
"component": "admin",
"dockerfile": "admin/Dockerfile",
"context": "admin",
"default_image_name": "termi-astro-admin",
},
}
if [ -n "${SELECTED[frontend]:-}" ]; then
FRONTEND_CHANGED=true
COUNT=$((COUNT + 1))
fi
components = [item for item in os.environ.get("COMPONENTS_CSV", "").split(",") if item]
matrix = {"include": [mapping[item] for item in components]}
if [ -n "${SELECTED[admin]:-}" ]; then
ADMIN_CHANGED=true
COUNT=$((COUNT + 1))
fi
print(f"matrix={json.dumps(matrix, separators=(',', ':'))}")
print(f"count={len(components)}")
print(f"frontend_changed={'true' if 'frontend' in components else 'false'}")
PY
{
echo "count=${COUNT}"
echo "backend_changed=${BACKEND_CHANGED}"
echo "frontend_changed=${FRONTEND_CHANGED}"
echo "admin_changed=${ADMIN_CHANGED}"
} >> "$GITHUB_OUTPUT"
echo "Selected components: ${COMPONENTS_CSV}"
if [ -n "${CHANGED_FILES}" ]; then
@@ -135,19 +130,71 @@ jobs:
fi
build-and-push:
name: build-and-push (${{ matrix.component }})
needs: resolve-build-targets
if: needs.resolve-build-targets.outputs.count != '0'
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1
matrix: ${{ fromJson(needs.resolve-build-targets.outputs.matrix) }}
matrix:
include:
- component: backend
dockerfile: backend/Dockerfile
context: backend
default_image_name: termi-astro-backend
- component: frontend
dockerfile: frontend/Dockerfile
context: frontend
default_image_name: termi-astro-frontend
- component: admin
dockerfile: admin/Dockerfile
context: admin
default_image_name: termi-astro-admin
steps:
- name: Decide whether to build current component
id: should_build
shell: bash
env:
COMPONENT: ${{ matrix.component }}
BACKEND_CHANGED: ${{ needs.resolve-build-targets.outputs.backend_changed }}
FRONTEND_CHANGED: ${{ needs.resolve-build-targets.outputs.frontend_changed }}
ADMIN_CHANGED: ${{ needs.resolve-build-targets.outputs.admin_changed }}
run: |
set -euo pipefail
SHOULD_BUILD=false
case "${COMPONENT}" in
backend)
SHOULD_BUILD="${BACKEND_CHANGED}"
;;
frontend)
SHOULD_BUILD="${FRONTEND_CHANGED}"
;;
admin)
SHOULD_BUILD="${ADMIN_CHANGED}"
;;
esac
echo "should_build=${SHOULD_BUILD}" >> "$GITHUB_OUTPUT"
echo "component=${COMPONENT}"
echo "should_build=${SHOULD_BUILD}"
- name: Skip unchanged component
if: steps.should_build.outputs.should_build != 'true'
shell: bash
env:
COMPONENT: ${{ matrix.component }}
run: echo "No changes detected for ${COMPONENT}, skipping docker build."
- name: Checkout
if: steps.should_build.outputs.should_build == 'true'
uses: actions/checkout@v4
- name: Resolve image metadata
if: steps.should_build.outputs.should_build == 'true'
id: meta
shell: bash
env:
@@ -217,6 +264,7 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: Login registry
if: steps.should_build.outputs.should_build == 'true'
shell: bash
env:
REGISTRY_HOST: ${{ steps.meta.outputs.registry_host }}
@@ -271,6 +319,7 @@ jobs:
fi
- name: Setup docker buildx
if: steps.should_build.outputs.should_build == 'true'
shell: bash
run: |
set -euo pipefail
@@ -284,6 +333,7 @@ jobs:
docker buildx inspect --bootstrap
- name: Login Docker Hub (optional)
if: steps.should_build.outputs.should_build == 'true'
shell: bash
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -298,6 +348,7 @@ jobs:
fi
- name: Build and push image
if: steps.should_build.outputs.should_build == 'true'
shell: bash
env:
COMPONENT: ${{ matrix.component }}
@@ -345,6 +396,7 @@ jobs:
"${CONTEXT_DIR}"
- name: Output image tags
if: steps.should_build.outputs.should_build == 'true'
shell: bash
env:
COMPONENT: ${{ matrix.component }}

View File

@@ -516,4 +516,7 @@ function formatCommentDate(dateStr: string): string {
} else if (useCaptcha) {
void loadCaptcha(false);
}
wrapper?.setAttribute('data-comments-ready', 'true');
window.__termiCommentsReady = true;
</script>

View File

@@ -273,6 +273,7 @@ const webPushPublicKey = popupSettings.webPushEnabled
const pathname = window.location.pathname || '/';
const delayMs = Math.max(3000, Number(root.getAttribute('data-delay-ms') || '18000'));
const defaultStatus = status instanceof HTMLElement ? status.textContent?.trim() || '' : '';
const isAutomatedBrowser = window.navigator.webdriver === true;
if (
!(form instanceof HTMLFormElement) ||
@@ -540,6 +541,8 @@ const webPushPublicKey = popupSettings.webPushEnabled
syncPopupOffset();
void syncBrowserPushState();
root.dataset.subscriptionPopupReady = 'true';
window.__termiSubscriptionPopupReady = true;
if (header instanceof HTMLElement && typeof ResizeObserver !== 'undefined') {
const observer = new ResizeObserver(() => syncPopupOffset());
@@ -547,6 +550,8 @@ const webPushPublicKey = popupSettings.webPushEnabled
}
window.addEventListener('resize', syncPopupOffset, { passive: true });
if (!isAutomatedBrowser) {
window.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('pointerdown', markEngaged, { once: true, passive: true });
window.addEventListener('keydown', markEngaged, { once: true });
@@ -554,6 +559,7 @@ const webPushPublicKey = popupSettings.webPushEnabled
autoReady = true;
maybeAutoOpen();
}, delayMs);
}
document.addEventListener('click', (event) => {
const trigger =

View File

@@ -1211,5 +1211,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
});
applyHomeFilters(false);
document.body?.setAttribute('data-home-interactive-ready', 'true');
window.__termiHomeReady = true;
})();
</script>

View File

@@ -1,6 +1,13 @@
import { expect, test } from '@playwright/test'
import { getDebugState, patchAdminSiteSettings, resetMockState } from './helpers'
import {
getDebugState,
patchAdminSiteSettings,
resetMockState,
waitForCommentsReady,
waitForHomeInteractive,
waitForSubscriptionPopupReady,
} from './helpers'
test.beforeEach(async ({ request }) => {
await resetMockState(request)
@@ -8,6 +15,7 @@ test.beforeEach(async ({ request }) => {
test('首页过滤、热门区和文章详情链路可用', async ({ page }) => {
await page.goto('/')
await waitForHomeInteractive(page)
await expect(page.locator('#home-results-count')).toContainText(/条结果/)
@@ -58,8 +66,10 @@ test('首页过滤、热门区和文章详情链路可用', async ({ page }) =>
test('文章评论、搜索和 AI 问答链路可用', async ({ page, request }) => {
await page.goto('/articles/astro-terminal-blog')
await waitForCommentsReady(page)
await page.locator('#toggle-comment-form').click()
await expect(page.locator('#comment-form input[name="nickname"]')).toBeVisible()
await page.locator('#comment-form input[name="nickname"]').fill('Playwright Visitor')
await page.locator('#comment-form input[name="email"]').fill('visitor@example.com')
await page.locator('#comment-form textarea[name="content"]').fill('这是一条来自回归测试的新评论。')
@@ -94,11 +104,13 @@ test('友链申请与订阅确认/偏好/退订链路可用', async ({ page, req
expect(friendState.friend_links.some((item: { site_name: string }) => item.site_name === 'Playwright Friend')).toBeTruthy()
await page.goto('/')
await waitForHomeInteractive(page)
await page.locator('[data-subscribe-form] input[name="displayName"]').fill('首页订阅用户')
await page.locator('[data-subscribe-form] input[name="email"]').fill('inline-subscriber@example.com')
await page.locator('[data-subscribe-form] button[type="submit"]').click()
await expect(page.locator('[data-subscribe-status]')).toContainText('订阅')
await waitForSubscriptionPopupReady(page)
await page.locator('[data-subscription-popup-open]').click()
await expect(page.locator('[data-subscription-popup-panel]')).toBeVisible()
await page.locator('[data-subscription-popup-form] input[name="displayName"]').fill('弹窗订阅用户')
@@ -136,6 +148,8 @@ test('GEO 分享面板、AI 摘要块与 llms 入口可用', async ({ page, requ
})
await page.goto('/')
await waitForHomeInteractive(page)
await waitForSubscriptionPopupReady(page)
await expect(page.locator('head link[rel="alternate"][href$="/llms.txt"]')).toHaveCount(1)
await expect(page.locator('head link[rel="alternate"][href$="/llms-full.txt"]')).toHaveCount(1)
await expect(page.getByRole('heading', { name: '给 AI 看的站点摘要' })).toBeVisible()

View File

@@ -33,6 +33,26 @@ export async function patchAdminSiteSettings(
return response.json()
}
export async function waitForHomeInteractive(page: Page) {
await page.waitForFunction(
() => (window as Window & { __termiHomeReady?: boolean }).__termiHomeReady === true,
)
}
export async function waitForCommentsReady(page: Page) {
await page.waitForFunction(
() => (window as Window & { __termiCommentsReady?: boolean }).__termiCommentsReady === true,
)
}
export async function waitForSubscriptionPopupReady(page: Page) {
await page.waitForFunction(
() =>
(window as Window & { __termiSubscriptionPopupReady?: boolean })
.__termiSubscriptionPopupReady === true,
)
}
export async function loginAdmin(page: Page) {
await page.goto('/login')
await page.getByLabel('用户名').fill('admin')