Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .github/workflows/docs-refresh-pr-dry-run.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: External docs refresh dry run (temporary)

on:
pull_request:
branches:
- main
paths:
- '.github/workflows/docs-refresh*.yml'
- 'docs-sources.yml'
- 'docs/remote-markdown.md'
- 'lib/external-docs.js'
- 'lib/generate-docs-fingerprint.js'
- 'lib/generate-external-docs-manifest.js'
- 'lib/compare-docs-fingerprint.js'
- 'package.json'
- 'package-lock.json'

permissions:
contents: read

concurrency:
group: external-docs-refresh-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
dry-run:
name: Compare external docs fingerprints without deploying
runs-on: ubuntu-latest

steps:
- name: Checkout docs repo
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Validate external docs manifest generation
run: npm run generate:external-docs

- name: Generate current docs fingerprint
run: npm run docs:fingerprint -- --output /tmp/current-docs-fingerprint.json
env:
GITHUB_TOKEN: ${{ github.token }}

- name: Download deployed docs fingerprint
env:
DOCS_FINGERPRINT_URL: https://docs.hypercerts.org/docs-fingerprint.json
run: |
set +e
http_code=$(curl -sS -L -w "%{http_code}" \
-o /tmp/deployed-docs-fingerprint.json \
"$DOCS_FINGERPRINT_URL")
curl_status=$?
set -e

if [ "$curl_status" -ne 0 ]; then
echo "::error::Failed to download deployed docs fingerprint from $DOCS_FINGERPRINT_URL. Dry-run cannot compare against deployed state."
exit 1
fi

case "$http_code" in
200)
;;
404)
echo '{}' > /tmp/deployed-docs-fingerprint.json
;;
*)
echo "::error::Unexpected HTTP $http_code while downloading $DOCS_FINGERPRINT_URL. Dry-run cannot compare against deployed state."
exit 1
;;
esac

- name: Compare fingerprints
id: diff
run: |
node lib/compare-docs-fingerprint.js \
/tmp/current-docs-fingerprint.json \
/tmp/deployed-docs-fingerprint.json >> "$GITHUB_OUTPUT"

- name: Report dry-run result
run: |
if [ "${{ steps.diff.outputs.changed }}" = "true" ]; then
echo "::notice::External docs fingerprint differs from the deployed site. The production workflow would trigger the Vercel deploy hook."
else
echo "::notice::External docs fingerprint matches the deployed site. The production workflow would do nothing."
fi

{
echo "### External docs refresh dry run"
echo ""
echo "This temporary PR-only workflow never calls the Vercel deploy hook."
echo ""
echo "Changed: ${{ steps.diff.outputs.changed || 'unknown' }}"
echo "Reason: ${{ steps.diff.outputs.reason || 'not computed' }}"
echo "Current: ${{ steps.diff.outputs.current_fingerprint || 'n/a' }}"
echo "Deployed: ${{ steps.diff.outputs.deployed_fingerprint || 'n/a' }}"
} >> "$GITHUB_STEP_SUMMARY"
121 changes: 121 additions & 0 deletions .github/workflows/docs-refresh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: Refresh external docs

on:
schedule:
- cron: '17 * * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Compare fingerprints without posting to the Vercel deploy hook.'
required: false
default: true
type: boolean
force_deploy:
description: 'Mark the run as changed even when the deployed fingerprint matches.'
required: false
default: false
type: boolean

permissions:
contents: read

concurrency:
group: docs-refresh
cancel-in-progress: false

jobs:
check-docs:
name: Check external docs fingerprint
runs-on: ubuntu-latest

steps:
- name: Checkout docs repo
uses: actions/checkout@v4
Comment on lines +32 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  ".github/workflows/docs-refresh.yml"
  ".github/workflows/docs-refresh-pr-dry-run.yml"
)

for f in "${files[@]}"; do
  echo "==== $f ===="
  if [ ! -f "$f" ]; then
    echo "MISSING: $f"
    continue
  fi
  # Show around the referenced lines
  lines=$(wc -l < "$f")
  start=1
  end=$lines
  # If files are short, just print whole; else print a window around 1-120
  if [ "$lines" -gt 120 ]; then
    end=120
  fi
  # Provide numbered view for context
  sed -n "1,${end}p" "$f" | nl -ba | sed -n "1,${end}p"
  echo
done

echo "==== Search persist-credentials in workflows ===="
rg -n "persist-credentials" .github/workflows || true

echo "==== Search checkout usage in those workflows ===="
for f in "${files[@]}"; do
  echo "--- checkout blocks in $f ---"
  rg -n "actions/checkout" "$f" || true
done

echo "==== Search for npm ci lifecycle vicinity ===="
for f in "${files[@]}"; do
  echo "--- npm ci blocks in $f ---"
  rg -n "npm ci|npm install" "$f" || true
done

Repository: hypercerts-org/documentation

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  ".github/workflows/docs-refresh.yml"
  ".github/workflows/docs-refresh-pr-dry-run.yml"
)

for f in "${files[@]}"; do
  echo "==== $f ===="
  if [ ! -f "$f" ]; then
    echo "MISSING: $f"
    continue
  fi
  lines=$(wc -l < "$f")
  end=$lines
  if [ "$lines" -gt 160 ]; then end=160; fi
  # numbered view using cat -n + limit
  sed -n "1,${end}p" "$f" | cat -n
  echo
done

echo "==== Search persist-credentials in workflows ===="
rg -n "persist-credentials" .github/workflows || true

echo "==== Search checkout blocks in those workflows ===="
for f in "${files[@]}"; do
  echo "--- checkout blocks in $f ---"
  rg -n "uses:\s*actions/checkout" "$f" || true
  rg -n "with:" "$f" || true
done

echo "==== Search for npm ci / npm install in those workflows ===="
for f in "${files[@]}"; do
  echo "--- npm ci blocks in $f ---"
  rg -n "npm (ci|install)" "$f" || true
done

Repository: hypercerts-org/documentation

Length of output: 9735


🌐 Web query:

actions/checkout@v4 persist-credentials default value stores github.token in git config

💡 Result:

The persist-credentials input for actions/checkout defaults to true [1][2][3]. When set to true (the default), the action configures the authentication token (typically github.token) or SSH key to allow your workflow scripts to run authenticated Git commands [3][4]. While the documentation historically stated this token was persisted directly in the local .git/config file [3][4], recent updates (as of the main branch) have improved this behavior by storing credentials in a separate file under $RUNNER_TEMP and using Git's includeIf functionality to reference them, rather than placing them directly in .git/config [5][6]. Regardless of the exact storage mechanism, the action automatically removes these credentials during the post-job cleanup phase to ensure security [3][4]. You can opt out of this behavior by setting persist-credentials: false in your workflow configuration [3][4].

Citations:


Disable persisted checkout credentials in both docs refresh workflows
Both workflows use actions/checkout@v4 without with: persist-credentials: false; the default persists/configures github.token for authenticated git commands during the job, so later steps (including npm ci / npm lifecycle scripts) can access it. Add with: persist-credentials: false to the actions/checkout@v4 steps in .github/workflows/docs-refresh.yml and .github/workflows/docs-refresh-pr-dry-run.yml.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 32-33: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 33-33: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docs-refresh.yml around lines 32 - 33, Add the checkout
steps' "with: persist-credentials: false" option to the actions/checkout@v4
invocation(s) so the "Checkout docs repo" step (and the equivalent checkout step
in the docs-refresh-pr-dry-run workflow) does not persist the GITHUB_TOKEN to
job workspace; locate the actions/checkout@v4 usage in
.github/workflows/docs-refresh.yml and
.github/workflows/docs-refresh-pr-dry-run.yml and add with: persist-credentials:
false under that step.

Source: Linters/SAST tools


- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
Comment on lines +32 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List the workflow files to ensure they exist
ls -la .github/workflows | sed -n '1,200p'

# Show the relevant sections with line numbers
echo "---- docs-refresh.yml (lines 1-120) ----"
nl -ba .github/workflows/docs-refresh.yml | sed -n '1,140p'

echo "---- docs-refresh-pr-dry-run.yml (lines 1-140) ----"
nl -ba .github/workflows/docs-refresh-pr-dry-run.yml | sed -n '1,160p'

# Confirm all usages of actions/checkout and actions/setup-node in workflows
echo "---- occurrences of actions/checkout ----"
rg -n --hidden --glob '.github/workflows/**/*.yml' "uses:\s*actions/checkout@" .github/workflows || true

echo "---- occurrences of actions/setup-node ----"
rg -n --hidden --glob '.github/workflows/**/*.yml' "uses:\s*actions/setup-node@" .github/workflows || true

Repository: hypercerts-org/documentation

Length of output: 428


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- docs-refresh.yml (cat -n lines 25-60) ----"
cat -n .github/workflows/docs-refresh.yml | sed -n '25,70p'

echo "---- docs-refresh-pr-dry-run.yml (cat -n lines 25-70) ----"
cat -n .github/workflows/docs-refresh-pr-dry-run.yml | sed -n '25,80p'

echo "---- occurrences of actions/checkout@ ----"
rg -n --glob '.github/workflows/**/*.yml' "uses:\s*actions/checkout@" .github/workflows || true

echo "---- occurrences of actions/setup-node@ ----"
rg -n --glob '.github/workflows/**/*.yml' "uses:\s*actions/setup-node@" .github/workflows || true

Repository: hypercerts-org/documentation

Length of output: 4282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- docs-refresh.yml triggers + job context (head 1-120) ----"
cat -n .github/workflows/docs-refresh.yml | sed -n '1,140p'

echo "---- docs-refresh.yml uses: lines (entire file) ----"
rg -n --glob '.github/workflows/docs-refresh.yml' "^\s*uses:\s*[^ ]+" .github/workflows/docs-refresh.yml || true

echo "---- docs-refresh-pr-dry-run.yml triggers + job context (head 1-140) ----"
cat -n .github/workflows/docs-refresh-pr-dry-run.yml | sed -n '1,160p'

echo "---- docs-refresh-pr-dry-run.yml uses: lines (entire file) ----"
rg -n --glob '.github/workflows/docs-refresh-pr-dry-run.yml' "^\s*uses:\s*[^ ]+" .github/workflows/docs-refresh-pr-dry-run.yml || true

echo "---- all occurrences of actions/checkout@v4 across workflows ----"
rg -n --glob '.github/workflows/**/*.yml' "uses:\s*actions/checkout@v4" .github/workflows || true

echo "---- all occurrences of actions/setup-node@v4 across workflows ----"
rg -n --glob '.github/workflows/**/*.yml' "uses:\s*actions/setup-node@v4" .github/workflows || true

Repository: hypercerts-org/documentation

Length of output: 9903


Pin actions/checkout and actions/setup-node to immutable SHAs in both docs workflows. Both .github/workflows/docs-refresh.yml (scheduled, triggers the Vercel deploy hook) and .github/workflows/docs-refresh-pr-dry-run.yml use the floating tags actions/checkout@v4 and actions/setup-node@v4, weakening the supply-chain boundary; pin these actions to exact commit SHAs.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 32-33: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 33-33: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 36-36: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docs-refresh.yml around lines 32 - 39, The workflow
currently uses floating tags actions/checkout@v4 and actions/setup-node@v4;
replace those with their corresponding immutable commit SHAs (pin the exact
commit for actions/checkout and actions/setup-node) in this file and also mirror
the same change in .github/workflows/docs-refresh-pr-dry-run.yml so both docs
workflows use exact SHAs instead of version tags; update the uses lines for
those two steps (the entries referencing actions/checkout and
actions/setup-node) to the exact commit identifiers.

Source: Linters/SAST tools


- name: Install dependencies
run: npm ci

- name: Generate current docs fingerprint
run: npm run docs:fingerprint -- --output current-docs-fingerprint.json
env:
DOCS_SOURCE_TOKEN: ${{ secrets.DOCS_SOURCE_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}

- name: Download deployed docs fingerprint
env:
DOCS_FINGERPRINT_URL: https://docs.hypercerts.org/docs-fingerprint.json
run: |
set +e
http_code=$(curl -sS -L -w "%{http_code}" \
-o deployed-docs-fingerprint.json \
"$DOCS_FINGERPRINT_URL")
curl_status=$?
set -e

if [ "$curl_status" -ne 0 ]; then
echo "::error::Failed to download deployed docs fingerprint from $DOCS_FINGERPRINT_URL. Refusing to deploy on an unknown diff."
exit 1
fi

case "$http_code" in
200)
;;
404)
echo '{}' > deployed-docs-fingerprint.json
;;
*)
echo "::error::Unexpected HTTP $http_code while downloading $DOCS_FINGERPRINT_URL. Refusing to deploy on an unknown diff."
exit 1
;;
esac

- name: Check if external docs changed
id: diff
env:
FORCE_DEPLOY: ${{ github.event_name == 'workflow_dispatch' && inputs.force_deploy == true }}
run: |
node lib/compare-docs-fingerprint.js \
current-docs-fingerprint.json \
deployed-docs-fingerprint.json >> "$GITHUB_OUTPUT"

if [ "$FORCE_DEPLOY" = "true" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "reason=forced by workflow_dispatch input" >> "$GITHUB_OUTPUT"
fi

- name: Trigger Vercel deployment
if: steps.diff.outputs.changed == 'true'
env:
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_URL }}
run: |
if [ "$DRY_RUN" = "true" ]; then
echo "workflow_dispatch dry_run=true; skipping Vercel deploy hook."
exit 0
fi

if [ -z "$VERCEL_DEPLOY_HOOK_URL" ]; then
echo "::error::VERCEL_DEPLOY_HOOK_URL is required when external docs have changed. Create a Vercel Deploy Hook for the production branch and store its URL as a GitHub Actions secret."
exit 1
fi

curl -fsS -X POST "$VERCEL_DEPLOY_HOOK_URL"

- name: Write summary
if: always()
run: |
{
echo "### External docs refresh"
echo ""
echo "Changed: ${{ steps.diff.outputs.changed || 'unknown' }}"
echo "Reason: ${{ steps.diff.outputs.reason || 'not computed' }}"
echo "Dry run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}"
echo "Current: ${{ steps.diff.outputs.current_fingerprint || 'n/a' }}"
echo "Deployed: ${{ steps.diff.outputs.deployed_fingerprint || 'n/a' }}"
} >> "$GITHUB_STEP_SUMMARY"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ out/

# Generated at build time by lib/generate-*.js — no need to track
public/raw/
public/external-docs.json
public/docs-fingerprint.json
public/search-index.json
public/sitemap.xml
current-docs-fingerprint.json
deployed-docs-fingerprint.json
lib/lastUpdated.json
49 changes: 45 additions & 4 deletions components/CopyRawButton.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,59 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';

function getRawUrl(currentPath) {
const EXTERNAL_DOCS_MANIFEST_URL = '/external-docs.json';

function getGeneratedRawUrl(currentPath) {
if (currentPath === '/') return '/raw/index.md';
return `/raw${currentPath}.md`;
}

export function CopyRawButton() {
async function getExternalDocRawUrl(externalDoc, signal) {
const response = await fetch(EXTERNAL_DOCS_MANIFEST_URL, {
cache: 'no-store',
signal,
});

if (!response.ok) {
throw new Error(`Could not load ${EXTERNAL_DOCS_MANIFEST_URL}. Run npm run generate:external-docs before starting the docs site.`);
}

const manifest = await response.json();
const rawUrl = manifest?.sources?.[externalDoc]?.rawUrl;
if (!rawUrl) {
throw new Error(`External docs source "${externalDoc}" does not define a raw Markdown URL.`);
}

return rawUrl;
}

/**
* Render page-level actions for copying or viewing the Markdown source for the current docs page.
* Pages that render canonical Markdown from another repository can set `externalDoc` in frontmatter so these actions use the registry source instead of the generated local fallback.
*/
export function CopyRawButton({ frontmatter }) {
const [copied, setCopied] = useState(false);
const [copyError, setCopyError] = useState(false);
const [isCopying, setIsCopying] = useState(false);
const [externalRawUrl, setExternalRawUrl] = useState(null);
const router = useRouter();
const currentPath = router.asPath.split('#')[0].split('?')[0] || '/';
const rawUrl = getRawUrl(currentPath);
const generatedRawUrl = getGeneratedRawUrl(currentPath);
const rawUrl = frontmatter?.rawUrl || externalRawUrl || generatedRawUrl;

useEffect(() => {
if (!frontmatter?.externalDoc || frontmatter?.rawUrl) {
setExternalRawUrl(null);
return undefined;
}

const controller = new AbortController();
getExternalDocRawUrl(frontmatter.externalDoc, controller.signal)
.then(setExternalRawUrl)
.catch(() => setExternalRawUrl(null));

return () => controller.abort();
}, [frontmatter?.externalDoc, frontmatter?.rawUrl]);
Comment on lines +42 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prefer the generated /raw/... cache before the remote registry URL.

This now routes external-doc pages through the live rawUrl first, so Copy raw / View raw can fail on GitHub availability, rate limits, or browser cross-origin fetch behavior even when the build already produced a same-origin raw cache for the page. The local generated raw page should stay the primary source, with the registry URL used only as a fallback.

Based on learnings from the PR objective and review stack context: external docs raw pages are generated during builds and are intended to back this runtime flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/CopyRawButton.js` around lines 42 - 56, The code currently prefers
the external registry URL over the local generated cache for rawUrl; change the
selection to prefer the generated raw cache first by updating the rawUrl
assignment to use frontmatter?.rawUrl || generatedRawUrl || externalRawUrl
(instead of placing externalRawUrl before generatedRawUrl). Also update the
useEffect that calls getExternalDocRawUrl: add generatedRawUrl to its dependency
list and short-circuit (return) if generatedRawUrl exists so we do not fetch the
external registry when a same-origin generated raw page is available; keep using
getExternalDocRawUrl(frontmatter.externalDoc, controller.signal) and
setExternalRawUrl only when generatedRawUrl is absent. Ensure references:
rawUrl, generatedRawUrl, externalRawUrl, useEffect, getExternalDocRawUrl,
setExternalRawUrl, frontmatter.


const handleCopy = async () => {
setIsCopying(true);
Expand Down
2 changes: 1 addition & 1 deletion components/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export default function Layout({ children, frontmatter }) {

<main className="layout-content" id="main-content">
<Breadcrumbs />
{frontmatter && <CopyRawButton />}
{frontmatter && <CopyRawButton frontmatter={frontmatter} />}
<LastUpdated />
<article>{children}</article>

Expand Down
Loading
Loading