Repository navigation
Site: sync from product-site-template (star CTA, npm downloads, privacy module) #546
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Site Preview | |
| # Per-PR preview deploys of site/ to Cloudflare Pages. | |
| # | |
| # PRODUCTION IS NOT TOUCHED BY THIS WORKFLOW. netscli.com is served from | |
| # GitHub Pages via pages.yml, which stays manual-only. This deploys to a | |
| # separate Cloudflare Pages project and always passes an explicit | |
| # `--branch=pr-<N>`, which Cloudflare treats as a *preview* deployment. | |
| # There is no code path here that produces a production deployment. | |
| # | |
| # A missing secret fails this workflow rather than skipping it. | |
| # | |
| # It used to skip and report success. Fork PRs cannot see secrets, so | |
| # skipping looked like the considerate choice -- but the job-level `if` | |
| # below already excludes forks, so the only way to reach the check with no | |
| # token is a misconfigured repository. That state produced a green "Site | |
| # Preview" tick on every PR for weeks while deploying nothing, and the | |
| # notice explaining why was visible only inside the job log. A check that | |
| # passes without doing its job is worse than one that is absent. | |
| on: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - 'site/**' | |
| - 'CHANGELOG.md' | |
| - 'scripts/install.ps1' | |
| - 'scripts/install.sh' | |
| - 'apps/netscli-gui/package.json' | |
| - '.github/workflows/site-preview.yml' | |
| # Keep a `main` preview that tracks main, so there is always somewhere to | |
| # see the site as it currently is. It cannot come from the PR path: that | |
| # deploys `--branch=pr-<N>`, so the main alias only ever moved when | |
| # someone ran wrangler by hand, and it silently fell behind between times. | |
| # | |
| # This is still a preview, not production. netscli.com is served from | |
| # GitHub Pages by pages.yml and is untouched here. | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'site/**' | |
| - 'CHANGELOG.md' | |
| - 'scripts/install.ps1' | |
| - 'scripts/install.sh' | |
| - 'apps/netscli-gui/package.json' | |
| - '.github/workflows/site-preview.yml' | |
| concurrency: | |
| # `pull_request.number` is empty on a push, which would put every push in | |
| # one unnamed group with the PR runs. `ref_name` is defined for both. | |
| group: site-preview-${{ github.event.pull_request.number || github.ref_name }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| preview: | |
| name: Deploy preview | |
| runs-on: ubuntu-latest | |
| # Secrets are not exposed to pull_request runs from forks, so a fork PR | |
| # would otherwise fail on an empty token. Skip rather than fail; the | |
| # maintainer can deploy a preview from a local branch if needed. | |
| # | |
| # A push to main is always same-repo, and `github.event.pull_request` is | |
| # null there -- so without the first clause this guard is false and the | |
| # main preview never runs. | |
| # | |
| # Dependabot PRs are same-repo but run with Dependabot's own secret | |
| # store, not the repository's, so CLOUDFLARE_API_TOKEN is empty there | |
| # too and "Check configuration" failed on all eleven of them (#311-#319, | |
| # #334, #335). A dependency bump does not need a preview. | |
| if: >- | |
| github.actor != 'dependabot[bot]' && | |
| (github.event_name == 'push' || | |
| github.event.pull_request.head.repo.full_name == github.repository) | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| cache-dependency-path: site/package-lock.json | |
| - name: Check configuration | |
| env: | |
| CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| run: | | |
| missing=() | |
| [[ -z "$CF_TOKEN" ]] && missing+=("CLOUDFLARE_API_TOKEN") | |
| [[ -z "$CF_ACCOUNT" ]] && missing+=("CLOUDFLARE_ACCOUNT_ID") | |
| if (( ${#missing[@]} > 0 )); then | |
| echo "::error::Cloudflare preview is not configured: ${missing[*]} not set. Add the secret, or delete this workflow if previews are not wanted. See docs/PUBLISHING.md." | |
| exit 1 | |
| fi | |
| echo "Cloudflare preview configured." | |
| - name: npm ci | |
| run: npm ci | |
| working-directory: site | |
| # NETSCLI_PREVIEW=1 makes the build emit `robots: noindex, nofollow` | |
| # and suppress the Web Analytics beacon. A preview is still a | |
| # production Astro build, so without it every PR deploy would report | |
| # into netscli.com's real analytics property and be crawlable. | |
| - name: Build site (preview mode) | |
| run: npm run build | |
| working-directory: site | |
| env: | |
| NETSCLI_PREVIEW: '1' | |
| - name: Verify the preview build is not indexable | |
| working-directory: site | |
| run: | | |
| set -euo pipefail | |
| missing=0 | |
| while IFS= read -r f; do | |
| grep -q 'name="robots" content="noindex' "$f" || { echo "not noindexed: $f"; missing=1; } | |
| done < <(find dist -name '*.html') | |
| if [[ "$missing" -ne 0 ]]; then | |
| echo "::error::Preview build produced indexable pages — refusing to deploy." | |
| exit 1 | |
| fi | |
| if grep -rq "cloudflareinsights" dist/ 2>/dev/null; then | |
| echo "::error::Preview build still loads the analytics beacon — refusing to deploy." | |
| exit 1 | |
| fi | |
| echo "All $(find dist -name '*.html' | wc -l) pages are noindex, and no analytics beacon is present." | |
| - name: Deploy to Cloudflare Pages (preview) | |
| id: deploy | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| working-directory: site | |
| run: | | |
| set -euo pipefail | |
| # --branch is what makes this a preview rather than a production | |
| # deployment. Never pass the project's production branch here. | |
| # | |
| # `main` is safe to pass *for this project* because | |
| # netscli-site-preview has no production branch configured -- its | |
| # apex netscli-site-preview.pages.dev returns 404 while the branch | |
| # aliases resolve, which is what an all-preview project looks like. | |
| # If a production branch is ever set on it, and it is set to main, | |
| # this line starts producing production deployments of the preview | |
| # project. Point it at a name that is not the production branch. | |
| npx --yes wrangler@4 pages deploy dist \ | |
| --project-name=netscli-site-preview \ | |
| --branch="${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'main' }}" \ | |
| --commit-dirty=true \ | |
| | tee deploy.log | |
| url=$(grep -oE 'https://[a-z0-9.-]+\.pages\.dev' deploy.log | tail -1) | |
| echo "url=${url}" >> "$GITHUB_OUTPUT" | |
| - name: Comment the preview URL | |
| # Only a pull_request run has somewhere to put this. | |
| if: github.event_name == 'pull_request' && steps.deploy.outputs.url != '' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| PREVIEW_URL: ${{ steps.deploy.outputs.url }} | |
| with: | |
| script: | | |
| const url = process.env.PREVIEW_URL; | |
| const marker = '<!-- netscli-site-preview -->'; | |
| const body = `${marker}\n**Site preview:** ${url}\n\n` + | |
| `Built from ${context.sha.slice(0, 7)} with \`NETSCLI_PREVIEW=1\` — ` + | |
| `noindex, and analytics disabled so it does not report into netscli.com's numbers.\n\n` + | |
| `Production is unaffected: netscli.com is served from GitHub Pages via \`pages.yml\`, which is manual-only.`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| ...context.repo, issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find((c) => c.body?.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body }); | |
| } |