Deploy product website #241
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: Deploy product website | |
| on: | |
| push: | |
| branches: [ main ] | |
| paths: | |
| - "landing/**" | |
| - "scripts/validate-product-source.py" | |
| - "scripts/build-product-site.py" | |
| - "scripts/validate-product-build.py" | |
| - "scripts/apply-search-authority.py" | |
| - "scripts/validate-search-authority.py" | |
| - "scripts/build-responsive-media.py" | |
| - "scripts/validate-responsive-media.py" | |
| - "scripts/validate-adoption-proof.py" | |
| - "scripts/verify-adoption-deployment.py" | |
| - "scripts/submit-indexnow.py" | |
| - "scripts/inject-site-measurement.py" | |
| - "scripts/validate-site-measurement.py" | |
| - "scripts/check-site-health.py" | |
| - "scripts/generate-privacy-pages.py" | |
| - "scripts/stamp-site-build.py" | |
| - "scripts/verify-pages-deployment.py" | |
| - "scripts/resolve-site-source-commit.py" | |
| - ".github/ISSUE_TEMPLATE/**" | |
| - ".github/workflows/pages.yml" | |
| - ".github/workflows/release-supply-chain.yml" | |
| pull_request: | |
| branches: [ main ] | |
| paths: | |
| - "landing/**" | |
| - "scripts/validate-product-source.py" | |
| - "scripts/build-product-site.py" | |
| - "scripts/validate-product-build.py" | |
| - "scripts/apply-search-authority.py" | |
| - "scripts/validate-search-authority.py" | |
| - "scripts/build-responsive-media.py" | |
| - "scripts/validate-responsive-media.py" | |
| - "scripts/validate-adoption-proof.py" | |
| - "scripts/verify-adoption-deployment.py" | |
| - "scripts/submit-indexnow.py" | |
| - "scripts/inject-site-measurement.py" | |
| - "scripts/validate-site-measurement.py" | |
| - "scripts/check-site-health.py" | |
| - "scripts/generate-privacy-pages.py" | |
| - "scripts/stamp-site-build.py" | |
| - "scripts/verify-pages-deployment.py" | |
| - "scripts/resolve-site-source-commit.py" | |
| - ".github/ISSUE_TEMPLATE/**" | |
| - ".github/workflows/pages.yml" | |
| - ".github/workflows/release-supply-chain.yml" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: pages | |
| cancel-in-progress: true | |
| env: | |
| CANONICAL_ROOT: https://masarray.github.io/arsas/ | |
| jobs: | |
| validate: | |
| name: Validate ARSAS product website | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout complete history | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| show-progress: false | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install responsive media dependency | |
| run: python -m pip install --disable-pip-version-check --quiet pillow | |
| - name: Resolve deployable website source | |
| id: site_source | |
| run: python scripts/resolve-site-source-commit.py --output _validation/site-source.json | |
| - name: Prepare verified stable release evidence | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then | |
| cp landing/latest.json /tmp/arsas-published.json | |
| else | |
| gh api "repos/$GITHUB_REPOSITORY/contents/published.json?ref=release-evidence" --jq .content | base64 -d > /tmp/arsas-published.json | |
| fi | |
| python - <<'PY' | |
| import json | |
| import re | |
| from pathlib import Path | |
| evidence = json.loads(Path('/tmp/arsas-published.json').read_text()) | |
| notes = json.loads(Path('landing/release-notes.json').read_text()) | |
| if evidence.get('product') not in (None, 'ARSAS'): | |
| raise SystemExit('Stable release evidence is for another product') | |
| if evidence.get('channel') != 'stable': | |
| raise SystemExit('Only stable release evidence may be deployed') | |
| if evidence.get('version') != notes.get('version'): | |
| raise SystemExit('Release notes and stable evidence versions differ') | |
| if not re.fullmatch(r'\d+\.\d+\.\d+', str(evidence.get('version', ''))): | |
| raise SystemExit('Stable release version is invalid') | |
| for name in ('installer', 'portable'): | |
| package = evidence.get(name, {}) | |
| if not re.fullmatch(r'[0-9a-fA-F]{64}', str(package.get('sha256', ''))): | |
| raise SystemExit(f'{name} SHA-256 is invalid') | |
| if int(package.get('sizeBytes', 0)) < 1_000_000: | |
| raise SystemExit(f'{name} size is invalid') | |
| PY | |
| - name: Validate product source, SEO, adoption and field-proof contracts | |
| id: source_validation | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| mkdir -p _validation | |
| python scripts/validate-product-source.py > _validation/source.log 2>&1 | |
| python scripts/validate-adoption-proof.py >> _validation/source.log 2>&1 | |
| - name: Upload source validation report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: product-source-validation | |
| path: | | |
| _validation/source.log | |
| _validation/site-source.json | |
| if-no-files-found: error | |
| - name: Enforce source validation | |
| if: steps.source_validation.outcome != 'success' | |
| shell: bash | |
| run: | | |
| cat _validation/source.log | |
| exit 1 | |
| - name: Build deterministic product website | |
| run: python scripts/build-product-site.py --output _site --release-evidence /tmp/arsas-published.json | |
| - name: Apply contextual search authority graph | |
| run: python scripts/apply-search-authority.py _site | |
| - name: Generate bilingual privacy pages | |
| env: | |
| GA4_MEASUREMENT_ID: ${{ vars.GA4_MEASUREMENT_ID }} | |
| run: python scripts/generate-privacy-pages.py --output _site --release-evidence /tmp/arsas-published.json --measurement-id "$GA4_MEASUREMENT_ID" | |
| - name: Generate responsive screenshot variants | |
| run: python scripts/build-responsive-media.py _site | |
| - name: Configure optional client measurement | |
| env: | |
| GA4_MEASUREMENT_ID: ${{ vars.GA4_MEASUREMENT_ID }} | |
| run: python scripts/inject-site-measurement.py _site --measurement-id "$GA4_MEASUREMENT_ID" | |
| - name: Stamp source and workflow attestation | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python scripts/stamp-site-build.py _site \ | |
| --source-commit "${{ steps.site_source.outputs.site_source_commit }}" \ | |
| --source-ref "$GITHUB_REF" \ | |
| --commit-timestamp "${{ steps.site_source.outputs.site_source_timestamp }}" \ | |
| --workflow-run-id "$GITHUB_RUN_ID" \ | |
| --workflow-run-attempt "$GITHUB_RUN_ATTEMPT" | |
| - name: Validate consent, privacy and measurement contract | |
| env: | |
| GA4_MEASUREMENT_ID: ${{ vars.GA4_MEASUREMENT_ID }} | |
| run: python scripts/validate-site-measurement.py _site --measurement-id "$GA4_MEASUREMENT_ID" | |
| - name: Validate contextual authority and guide discovery | |
| run: python scripts/validate-search-authority.py _site | |
| - name: Validate adoption and field-proof output | |
| run: python scripts/validate-adoption-proof.py --site _site | |
| - name: Validate responsive screenshot output | |
| run: python scripts/validate-responsive-media.py _site | |
| - name: Check internal links and fragments | |
| run: python scripts/check-site-health.py --site _site --output _validation/site-health | |
| - name: Validate IndexNow payload without network submission | |
| run: python scripts/submit-indexnow.py --sitemap _site/sitemap.xml --dry-run | |
| - name: Validate rendered product website | |
| id: build_validation | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| mkdir -p _validation | |
| python scripts/validate-product-build.py _site > _validation/build.log 2>&1 | |
| - name: Upload rendered validation report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: product-build-validation | |
| path: | | |
| _validation/build.log | |
| _validation/site-health/ | |
| if-no-files-found: warn | |
| - name: Enforce rendered validation | |
| if: steps.build_validation.outcome != 'success' | |
| shell: bash | |
| run: | | |
| cat _validation/build.log | |
| exit 1 | |
| - name: Verify public website boundaries | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ! grep -R --line-number --fixed-strings 'LICENSE-APACHE-2.0' _site | |
| ! grep -R --line-number -E '(href|src|content)="http://' _site --include='*.html' | |
| ! grep -R --line-number --fixed-strings 'raw.githubusercontent.com/masarray/arsas/main/Assets/screenshot' _site --include='*.html' | |
| ! grep -R --line-number --fixed-strings '__ARSAS_GA4_MEASUREMENT_ID__' _site --include='*.html' | |
| test ! -e _site/search-authority.json | |
| - name: Upload website artifact | |
| if: github.event_name != 'pull_request' | |
| uses: actions/upload-pages-artifact@v3 | |
| with: | |
| path: _site | |
| deploy: | |
| if: github.event_name != 'pull_request' | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pages: write | |
| id-token: write | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v4 | |
| verify-production: | |
| name: Verify public Pages attestation | |
| if: github.event_name != 'pull_request' | |
| needs: deploy | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout complete history | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| show-progress: false | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Resolve deployed website source | |
| id: site_source | |
| run: python scripts/resolve-site-source-commit.py --output _validation/site-source.json | |
| - name: Verify deployed commit, privacy, authority and measurement state | |
| env: | |
| GA4_MEASUREMENT_ID: ${{ vars.GA4_MEASUREMENT_ID }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| stable_version="$(python -c 'import json; print(json.load(open("landing/latest.json"))["version"])')" | |
| if [ -n "${GA4_MEASUREMENT_ID:-}" ]; then measurement_enabled=true; else measurement_enabled=false; fi | |
| python scripts/verify-pages-deployment.py \ | |
| --base-url "$CANONICAL_ROOT" \ | |
| --source-commit "${{ steps.site_source.outputs.site_source_commit }}" \ | |
| --stable-version "$stable_version" \ | |
| --measurement-enabled "$measurement_enabled" \ | |
| --output _validation/production-attestation.md | |
| - name: Verify public adoption, evidence and responsive media surfaces | |
| run: python scripts/verify-adoption-deployment.py --base-url "$CANONICAL_ROOT" --source-commit "${{ steps.site_source.outputs.site_source_commit }}" | |
| - name: Upload production attestation | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: production-pages-attestation | |
| path: | | |
| _validation/production-attestation.* | |
| _validation/site-source.json | |
| if-no-files-found: error | |
| retention-days: 90 | |
| notify-indexing: | |
| name: Notify search engines | |
| if: github.event_name != 'pull_request' | |
| needs: verify-production | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| show-progress: false | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Prepare stable release evidence | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: gh api "repos/$GITHUB_REPOSITORY/contents/published.json?ref=release-evidence" --jq .content | base64 -d > /tmp/arsas-published.json | |
| - name: Build current sitemap | |
| run: python scripts/build-product-site.py --output _site --release-evidence /tmp/arsas-published.json | |
| - name: Submit deployed URLs through IndexNow | |
| id: indexnow | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| mkdir -p _validation | |
| python scripts/submit-indexnow.py --sitemap _site/sitemap.xml > _validation/indexnow.log 2>&1 | |
| - name: Upload IndexNow submission report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: indexnow-submission | |
| path: _validation/indexnow.log | |
| if-no-files-found: warn | |
| - name: Report non-blocking submission result | |
| if: steps.indexnow.outcome != 'success' | |
| run: echo "IndexNow submission was not accepted; deployment remains valid and the generated sitemap is still available to crawlers." |