From fc88a1b7a6c7a1e71d80715c6f321c55edef387f Mon Sep 17 00:00:00 2001 From: XiaoYun Zhang Date: Wed, 7 Oct 2026 13:27:55 -0700 Subject: [PATCH 1/2] Add organization Pages deployment Build the authoritative RazorConsole website at the root URL and deploy only from main while keeping pull requests build-only. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pages.yml | 168 ++++++++++++++++++++++++++++++++++++ README.md | 30 ++++++- 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pages.yml diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..1c0ff80 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,168 @@ +name: Build and deploy organization site + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + schedule: + - cron: '17 5 * * *' + +permissions: + contents: read + +concurrency: + group: pages-${{ github.workflow }} + cancel-in-progress: true + +env: + SOURCE_REPOSITORY: RazorConsole/RazorConsole + SOURCE_REF: main + VITE_SITE_URL: https://razorconsole.github.io + VITE_BASE: / + VITE_ROUTER_BASENAME: / + +jobs: + build: + name: Build source website + runs-on: ubuntu-latest + outputs: + source-sha: ${{ steps.source.outputs.sha }} + steps: + - name: Check out website source + uses: actions/checkout@v7 + with: + repository: ${{ env.SOURCE_REPOSITORY }} + ref: ${{ env.SOURCE_REF }} + path: source + fetch-depth: 1 + lfs: true + + - name: Record source revision + id: source + working-directory: source + shell: bash + run: | + source_sha="$(git rev-parse HEAD)" + echo "sha=$source_sha" >> "$GITHUB_OUTPUT" + { + echo "## Website source" + echo + echo "- Repository: \`$SOURCE_REPOSITORY\`" + echo "- Requested ref: \`$SOURCE_REF\`" + echo "- Built commit: \`$source_sha\`" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Setup .NET for documentation and WASM + uses: actions/setup-dotnet@v6 + with: + global-json-file: source/global.json + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: '20' + cache: npm + cache-dependency-path: source/website/package-lock.json + + - name: Build API documentation input + working-directory: source + run: dotnet build src/RazorConsole.Core/RazorConsole.Core.csproj --configuration Release --framework net10.0 + + - name: Install website dependencies + working-directory: source/website + run: npm ci + + - name: Setup Python for browser regressions + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install headless browser test tools + working-directory: source/website + run: | + python -m pip install -r tests/requirements.txt + python -m playwright install --with-deps chromium + + - name: Build and test root website + working-directory: source/website + run: npm run test:terminal-input && npm run test:seo && npm run build && npm run test:seo:static + + - name: Verify built tutorial navigation + working-directory: source/website + run: python tests/tutorial_navigation.py + + - name: Prepare and verify Pages artifact + working-directory: source + shell: bash + run: | + cp website/build/client/index.html website/build/client/404.html + touch website/build/client/.nojekyll + test -f website/build/client/googleddbc334a439061f3.html + python - <<'PY' + from pathlib import Path + from xml.etree import ElementTree + + output = Path("website/build/client") + sitemap = output / "sitemap.xml" + if not sitemap.is_file(): + raise SystemExit("Missing sitemap.xml") + + namespace = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"} + urls = [ + node.text + for node in ElementTree.parse(sitemap).findall(".//s:loc", namespace) + ] + if not urls: + raise SystemExit("Sitemap contains no URLs") + if len(urls) != len(set(urls)): + raise SystemExit("Sitemap contains duplicate URLs") + invalid = [ + url + for url in urls + if not url.startswith("https://razorconsole.github.io/") + or url.startswith("https://razorconsole.github.io/RazorConsole/") + ] + if invalid: + raise SystemExit(f"Invalid root-site sitemap URLs: {invalid[:5]}") + + home = (output / "index.html").read_text(encoding="utf-8") + if 'rel="canonical" href="https://razorconsole.github.io/"' not in home: + raise SystemExit("Homepage canonical is not the organization root URL") + if "/RazorConsole/" in home: + raise SystemExit("Homepage still contains the legacy project-site base") + PY + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: source/website/build/client + + deploy: + name: Deploy to GitHub Pages + if: >- + github.ref == 'refs/heads/main' && + (github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + github.event_name == 'schedule') + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Configure GitHub Pages + uses: actions/configure-pages@v6 + + - name: Deploy Pages artifact + id: deployment + uses: actions/deploy-pages@v5 diff --git a/README.md b/README.md index cc24dc2..ebf9154 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,31 @@ # RazorConsole.github.io -Organization website for [RazorConsole](https://github.com/RazorConsole/RazorConsole). +Deployment repository for the RazorConsole organization site at +[https://razorconsole.github.io/](https://razorconsole.github.io/). + +The website source remains authoritative in +[`RazorConsole/RazorConsole`](https://github.com/RazorConsole/RazorConsole/tree/main/website). +This repository only owns the organization Pages identity and deployment workflow; generated site +files are never copied into the repository. + +## Deployment + +[`pages.yml`](.github/workflows/pages.yml) checks out the source repository's `main` branch, records +the exact source commit in the workflow summary, builds the existing website with `/` as both the +Vite base and router basename, verifies the root canonical URLs, sitemap, Google verification file, +and browser navigation, then uploads the generated `website/build/client` directory unchanged apart +from the Pages `404.html` and `.nojekyll` files. + +Pull requests run the complete build and verification but never deploy. Production deployment runs +only from this repository's `main` branch after a push, a manual `workflow_dispatch`, or the daily +scheduled source synchronization. Runs are serialized so an older build cannot overtake a newer one. + +Before the first deployment, an organization owner must open **Settings → Pages → Build and +deployment** and set **Source** to **GitHub Actions**. The existing project site should not be changed +until the root URL and representative routes return HTTP 200. + +To recover or immediately pick up a source change, run **Build and deploy organization site** from +the Actions tab on the `main` branch. GitHub's repository-scoped `GITHUB_TOKEN` cannot trigger a +workflow in another repository, so immediate source-driven deployments would require an explicitly +managed GitHub App or fine-grained token. The scheduled and manual paths require no cross-repository +secret. From a0c3df0d3ce82d32952a02ec5d830327e837bdf4 Mon Sep 17 00:00:00 2001 From: XiaoYun Zhang Date: Wed, 7 Oct 2026 13:37:06 -0700 Subject: [PATCH 2/2] Narrow root URL artifact validation Validate actual links and asset references without rejecting migration URLs mentioned as page content. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pages.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 1c0ff80..2dad33c 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -106,8 +106,23 @@ jobs: test -f website/build/client/googleddbc334a439061f3.html python - <<'PY' from pathlib import Path + from html.parser import HTMLParser from xml.etree import ElementTree + class RootReferenceParser(HTMLParser): + def __init__(self): + super().__init__() + self.invalid = [] + + def handle_starttag(self, tag, attrs): + for name, value in attrs: + if name not in {"href", "src"} or not value: + continue + if value.startswith("/RazorConsole/") or value.startswith( + "https://razorconsole.github.io/RazorConsole/" + ): + self.invalid.append((tag, name, value)) + output = Path("website/build/client") sitemap = output / "sitemap.xml" if not sitemap.is_file(): @@ -134,8 +149,12 @@ jobs: home = (output / "index.html").read_text(encoding="utf-8") if 'rel="canonical" href="https://razorconsole.github.io/"' not in home: raise SystemExit("Homepage canonical is not the organization root URL") - if "/RazorConsole/" in home: - raise SystemExit("Homepage still contains the legacy project-site base") + references = RootReferenceParser() + references.feed(home) + if references.invalid: + raise SystemExit( + f"Homepage references the legacy project-site base: {references.invalid[:5]}" + ) PY - name: Upload Pages artifact