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
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:
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,13 +550,16 @@ const webPushPublicKey = popupSettings.webPushEnabled
|
||||
}
|
||||
|
||||
window.addEventListener('resize', syncPopupOffset, { passive: true });
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('pointerdown', markEngaged, { once: true, passive: true });
|
||||
window.addEventListener('keydown', markEngaged, { once: true });
|
||||
window.setTimeout(() => {
|
||||
autoReady = true;
|
||||
maybeAutoOpen();
|
||||
}, delayMs);
|
||||
|
||||
if (!isAutomatedBrowser) {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('pointerdown', markEngaged, { once: true, passive: true });
|
||||
window.addEventListener('keydown', markEngaged, { once: true });
|
||||
window.setTimeout(() => {
|
||||
autoReady = true;
|
||||
maybeAutoOpen();
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const trigger =
|
||||
|
||||
@@ -1211,5 +1211,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
});
|
||||
|
||||
applyHomeFilters(false);
|
||||
document.body?.setAttribute('data-home-interactive-ready', 'true');
|
||||
window.__termiHomeReady = true;
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user