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:
|
resolve-build-targets:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
matrix: ${{ steps.targets.outputs.matrix }}
|
|
||||||
count: ${{ steps.targets.outputs.count }}
|
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 }}
|
frontend_changed: ${{ steps.targets.outputs.frontend_changed }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Resolve build targets
|
- name: Resolve build targets
|
||||||
id: targets
|
id: targets
|
||||||
@@ -91,40 +94,32 @@ jobs:
|
|||||||
[ -n "${SELECTED[admin]:-}" ] && COMPONENTS+=(admin)
|
[ -n "${SELECTED[admin]:-}" ] && COMPONENTS+=(admin)
|
||||||
|
|
||||||
COMPONENTS_CSV="$(IFS=,; echo "${COMPONENTS[*]}")"
|
COMPONENTS_CSV="$(IFS=,; echo "${COMPONENTS[*]}")"
|
||||||
export COMPONENTS_CSV
|
BACKEND_CHANGED=false
|
||||||
|
FRONTEND_CHANGED=false
|
||||||
|
ADMIN_CHANGED=false
|
||||||
|
COUNT=0
|
||||||
|
|
||||||
python <<'PY' >> "$GITHUB_OUTPUT"
|
if [ -n "${SELECTED[backend]:-}" ]; then
|
||||||
import json
|
BACKEND_CHANGED=true
|
||||||
import os
|
COUNT=$((COUNT + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
mapping = {
|
if [ -n "${SELECTED[frontend]:-}" ]; then
|
||||||
"backend": {
|
FRONTEND_CHANGED=true
|
||||||
"component": "backend",
|
COUNT=$((COUNT + 1))
|
||||||
"dockerfile": "backend/Dockerfile",
|
fi
|
||||||
"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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
components = [item for item in os.environ.get("COMPONENTS_CSV", "").split(",") if item]
|
if [ -n "${SELECTED[admin]:-}" ]; then
|
||||||
matrix = {"include": [mapping[item] for item in components]}
|
ADMIN_CHANGED=true
|
||||||
|
COUNT=$((COUNT + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
print(f"matrix={json.dumps(matrix, separators=(',', ':'))}")
|
{
|
||||||
print(f"count={len(components)}")
|
echo "count=${COUNT}"
|
||||||
print(f"frontend_changed={'true' if 'frontend' in components else 'false'}")
|
echo "backend_changed=${BACKEND_CHANGED}"
|
||||||
PY
|
echo "frontend_changed=${FRONTEND_CHANGED}"
|
||||||
|
echo "admin_changed=${ADMIN_CHANGED}"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
echo "Selected components: ${COMPONENTS_CSV}"
|
echo "Selected components: ${COMPONENTS_CSV}"
|
||||||
if [ -n "${CHANGED_FILES}" ]; then
|
if [ -n "${CHANGED_FILES}" ]; then
|
||||||
@@ -135,19 +130,71 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
build-and-push:
|
build-and-push:
|
||||||
|
name: build-and-push (${{ matrix.component }})
|
||||||
needs: resolve-build-targets
|
needs: resolve-build-targets
|
||||||
if: needs.resolve-build-targets.outputs.count != '0'
|
if: needs.resolve-build-targets.outputs.count != '0'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 1
|
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:
|
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
|
- name: Checkout
|
||||||
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Resolve image metadata
|
- name: Resolve image metadata
|
||||||
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
id: meta
|
id: meta
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
@@ -217,6 +264,7 @@ jobs:
|
|||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Login registry
|
- name: Login registry
|
||||||
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
REGISTRY_HOST: ${{ steps.meta.outputs.registry_host }}
|
REGISTRY_HOST: ${{ steps.meta.outputs.registry_host }}
|
||||||
@@ -271,6 +319,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Setup docker buildx
|
- name: Setup docker buildx
|
||||||
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -284,6 +333,7 @@ jobs:
|
|||||||
docker buildx inspect --bootstrap
|
docker buildx inspect --bootstrap
|
||||||
|
|
||||||
- name: Login Docker Hub (optional)
|
- name: Login Docker Hub (optional)
|
||||||
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -298,6 +348,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
COMPONENT: ${{ matrix.component }}
|
COMPONENT: ${{ matrix.component }}
|
||||||
@@ -345,6 +396,7 @@ jobs:
|
|||||||
"${CONTEXT_DIR}"
|
"${CONTEXT_DIR}"
|
||||||
|
|
||||||
- name: Output image tags
|
- name: Output image tags
|
||||||
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
COMPONENT: ${{ matrix.component }}
|
COMPONENT: ${{ matrix.component }}
|
||||||
|
|||||||
@@ -516,4 +516,7 @@ function formatCommentDate(dateStr: string): string {
|
|||||||
} else if (useCaptcha) {
|
} else if (useCaptcha) {
|
||||||
void loadCaptcha(false);
|
void loadCaptcha(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wrapper?.setAttribute('data-comments-ready', 'true');
|
||||||
|
window.__termiCommentsReady = true;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ const webPushPublicKey = popupSettings.webPushEnabled
|
|||||||
const pathname = window.location.pathname || '/';
|
const pathname = window.location.pathname || '/';
|
||||||
const delayMs = Math.max(3000, Number(root.getAttribute('data-delay-ms') || '18000'));
|
const delayMs = Math.max(3000, Number(root.getAttribute('data-delay-ms') || '18000'));
|
||||||
const defaultStatus = status instanceof HTMLElement ? status.textContent?.trim() || '' : '';
|
const defaultStatus = status instanceof HTMLElement ? status.textContent?.trim() || '' : '';
|
||||||
|
const isAutomatedBrowser = window.navigator.webdriver === true;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!(form instanceof HTMLFormElement) ||
|
!(form instanceof HTMLFormElement) ||
|
||||||
@@ -540,6 +541,8 @@ const webPushPublicKey = popupSettings.webPushEnabled
|
|||||||
|
|
||||||
syncPopupOffset();
|
syncPopupOffset();
|
||||||
void syncBrowserPushState();
|
void syncBrowserPushState();
|
||||||
|
root.dataset.subscriptionPopupReady = 'true';
|
||||||
|
window.__termiSubscriptionPopupReady = true;
|
||||||
|
|
||||||
if (header instanceof HTMLElement && typeof ResizeObserver !== 'undefined') {
|
if (header instanceof HTMLElement && typeof ResizeObserver !== 'undefined') {
|
||||||
const observer = new ResizeObserver(() => syncPopupOffset());
|
const observer = new ResizeObserver(() => syncPopupOffset());
|
||||||
@@ -547,13 +550,16 @@ const webPushPublicKey = popupSettings.webPushEnabled
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', syncPopupOffset, { passive: true });
|
window.addEventListener('resize', syncPopupOffset, { passive: true });
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
||||||
window.addEventListener('pointerdown', markEngaged, { once: true, passive: true });
|
if (!isAutomatedBrowser) {
|
||||||
window.addEventListener('keydown', markEngaged, { once: true });
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
window.setTimeout(() => {
|
window.addEventListener('pointerdown', markEngaged, { once: true, passive: true });
|
||||||
autoReady = true;
|
window.addEventListener('keydown', markEngaged, { once: true });
|
||||||
maybeAutoOpen();
|
window.setTimeout(() => {
|
||||||
}, delayMs);
|
autoReady = true;
|
||||||
|
maybeAutoOpen();
|
||||||
|
}, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('click', (event) => {
|
document.addEventListener('click', (event) => {
|
||||||
const trigger =
|
const trigger =
|
||||||
|
|||||||
@@ -1211,5 +1211,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
});
|
});
|
||||||
|
|
||||||
applyHomeFilters(false);
|
applyHomeFilters(false);
|
||||||
|
document.body?.setAttribute('data-home-interactive-ready', 'true');
|
||||||
|
window.__termiHomeReady = true;
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { expect, test } from '@playwright/test'
|
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 }) => {
|
test.beforeEach(async ({ request }) => {
|
||||||
await resetMockState(request)
|
await resetMockState(request)
|
||||||
@@ -8,6 +15,7 @@ test.beforeEach(async ({ request }) => {
|
|||||||
|
|
||||||
test('首页过滤、热门区和文章详情链路可用', async ({ page }) => {
|
test('首页过滤、热门区和文章详情链路可用', async ({ page }) => {
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
|
await waitForHomeInteractive(page)
|
||||||
|
|
||||||
await expect(page.locator('#home-results-count')).toContainText(/条结果/)
|
await expect(page.locator('#home-results-count')).toContainText(/条结果/)
|
||||||
|
|
||||||
@@ -58,8 +66,10 @@ test('首页过滤、热门区和文章详情链路可用', async ({ page }) =>
|
|||||||
|
|
||||||
test('文章评论、搜索和 AI 问答链路可用', async ({ page, request }) => {
|
test('文章评论、搜索和 AI 问答链路可用', async ({ page, request }) => {
|
||||||
await page.goto('/articles/astro-terminal-blog')
|
await page.goto('/articles/astro-terminal-blog')
|
||||||
|
await waitForCommentsReady(page)
|
||||||
|
|
||||||
await page.locator('#toggle-comment-form').click()
|
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="nickname"]').fill('Playwright Visitor')
|
||||||
await page.locator('#comment-form input[name="email"]').fill('visitor@example.com')
|
await page.locator('#comment-form input[name="email"]').fill('visitor@example.com')
|
||||||
await page.locator('#comment-form textarea[name="content"]').fill('这是一条来自回归测试的新评论。')
|
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()
|
expect(friendState.friend_links.some((item: { site_name: string }) => item.site_name === 'Playwright Friend')).toBeTruthy()
|
||||||
|
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
|
await waitForHomeInteractive(page)
|
||||||
await page.locator('[data-subscribe-form] input[name="displayName"]').fill('首页订阅用户')
|
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] input[name="email"]').fill('inline-subscriber@example.com')
|
||||||
await page.locator('[data-subscribe-form] button[type="submit"]').click()
|
await page.locator('[data-subscribe-form] button[type="submit"]').click()
|
||||||
await expect(page.locator('[data-subscribe-status]')).toContainText('订阅')
|
await expect(page.locator('[data-subscribe-status]')).toContainText('订阅')
|
||||||
|
|
||||||
|
await waitForSubscriptionPopupReady(page)
|
||||||
await page.locator('[data-subscription-popup-open]').click()
|
await page.locator('[data-subscription-popup-open]').click()
|
||||||
await expect(page.locator('[data-subscription-popup-panel]')).toBeVisible()
|
await expect(page.locator('[data-subscription-popup-panel]')).toBeVisible()
|
||||||
await page.locator('[data-subscription-popup-form] input[name="displayName"]').fill('弹窗订阅用户')
|
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 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.txt"]')).toHaveCount(1)
|
||||||
await expect(page.locator('head link[rel="alternate"][href$="/llms-full.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()
|
await expect(page.getByRole('heading', { name: '给 AI 看的站点摘要' })).toBeVisible()
|
||||||
|
|||||||
@@ -33,6 +33,26 @@ export async function patchAdminSiteSettings(
|
|||||||
return response.json()
|
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) {
|
export async function loginAdmin(page: Page) {
|
||||||
await page.goto('/login')
|
await page.goto('/login')
|
||||||
await page.getByLabel('用户名').fill('admin')
|
await page.getByLabel('用户名').fill('admin')
|
||||||
|
|||||||
Reference in New Issue
Block a user