feat: price dotNS names on a scarcity curve #290
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: Documentation | |
| # SECURITY ATTENTION | |
| # This workflow uses `pull_request_target` for the on-close cleanup job | |
| # only. `pull_request_target` runs in the BASE branch's workflow context | |
| # with repository secrets and a write-scoped GITHUB_TOKEN. The cleanup | |
| # job is gated on `github.event.action == 'closed'` and does NOT check | |
| # out PR-head code, so no untrusted code executes under that context. | |
| # | |
| # Adding other `pull_request_target` event types or any step that | |
| # checks out PR-head code under that trigger would let a malicious PR | |
| # exfiltrate repo secrets. Re-review carefully if widened. | |
| on: | |
| push: | |
| branches: [master] | |
| paths: ["src/**", "contracts/**", "**.sol", "foundry.toml"] | |
| pull_request: | |
| branches: [master] | |
| paths: ["src/**", "contracts/**", "**.sol", "foundry.toml"] | |
| pull_request_target: | |
| types: [closed] | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| concurrency: | |
| group: pages-deploy | |
| cancel-in-progress: false | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| if: github.event.action != 'closed' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - uses: foundry-rs/foundry-toolchain@v1 | |
| with: | |
| version: stable | |
| - name: Install mdbook | |
| run: | | |
| MDBOOK_VERSION="0.4.40" | |
| curl -sSL "https://github.com/rust-lang/mdBook/releases/download/v${MDBOOK_VERSION}/mdbook-v${MDBOOK_VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin | |
| - run: forge install | |
| - name: Apply OZ payable patches | |
| run: bash scripts/shell/apply-oz-patches.sh | |
| - run: forge build | |
| - name: Generate docs | |
| id: docs | |
| env: | |
| FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" | |
| run: | | |
| REPO_NAME="${{ github.event.repository.name }}" | |
| # Step 1: Generate mdBook source (without building) | |
| if ! forge doc --out docs-output 2>&1 | tee docs.log; then | |
| echo "success=false" >> "$GITHUB_OUTPUT" | |
| echo "error=forge doc failed" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Step 2: Determine the deploy subpath and inject site-url | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| PR_NUM="${{ github.event.pull_request.number }}" | |
| SITE_URL="/${REPO_NAME}/docs/preview/pr-${PR_NUM}/" | |
| else | |
| SITE_URL="/${REPO_NAME}/docs/" | |
| fi | |
| # Inject site-url into book.toml so assets resolve correctly on gh-pages | |
| sed -i '/^\[output\.html\]/a site-url = "'"${SITE_URL}"'"' docs-output/book.toml | |
| # Step 3: Build with mdbook | |
| if ! mdbook build docs-output 2>&1 | tee -a docs.log; then | |
| echo "success=false" >> "$GITHUB_OUTPUT" | |
| echo "error=mdbook build failed" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ -d "docs-output/book" ]; then | |
| touch docs-output/book/.nojekyll | |
| COUNT=$(find docs-output/book -name "*.html" | wc -l | tr -d ' ') | |
| echo "success=true" >> "$GITHUB_OUTPUT" | |
| echo "count=${COUNT}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "success=false" >> "$GITHUB_OUTPUT" | |
| echo "error=No documentation generated" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Copy diagrams into doc/src | |
| if: steps.docs.outputs.success == 'true' | |
| run: | | |
| # Expects a repo-level ./diagrams directory. | |
| # Copies into the mdBook source tree (docs-output/src) as requested. | |
| # Also copies into the built site (docs-output/book) so assets are served even if build already ran. | |
| if [ -d "diagrams" ]; then | |
| mkdir -p docs-output/src | |
| rm -rf docs-output/src/diagrams | |
| cp -R diagrams docs-output/src/diagrams | |
| mkdir -p docs-output/book/diagrams | |
| rm -rf docs-output/book/diagrams | |
| cp -R diagrams docs-output/book/diagrams | |
| else | |
| echo "No ./diagrams directory found; skipping copy." | |
| fi | |
| - name: Set deploy path and URL | |
| id: path | |
| run: | | |
| OWNER="${{ github.repository_owner }}" | |
| REPO_NAME="${{ github.event.repository.name }}" | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| PR_NUM="${{ github.event.pull_request.number }}" | |
| echo "target=docs/preview/pr-${PR_NUM}" >> "$GITHUB_OUTPUT" | |
| echo "url=https://${OWNER}.github.io/${REPO_NAME}/docs/preview/pr-${PR_NUM}/" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "target=docs" >> "$GITHUB_OUTPUT" | |
| echo "url=https://${OWNER}.github.io/${REPO_NAME}/docs/" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Prepare result | |
| id: run | |
| run: | | |
| if [ "${{ steps.docs.outputs.success }}" = "true" ]; then | |
| COUNT="${{ steps.docs.outputs.count }}" | |
| URL="${{ steps.path.outputs.url }}" | |
| echo "result=Passed - ${COUNT} pages generated - [View Docs](${URL})" >> "$GITHUB_OUTPUT" | |
| else | |
| ERROR="${{ steps.docs.outputs.error }}" | |
| echo "result=Failed - ${ERROR}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: docs | |
| path: | | |
| docs-output/ | |
| docs.log | |
| retention-days: 7 | |
| - name: Deploy to GitHub Pages | |
| if: steps.docs.outputs.success == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: bash scripts/shell/deploy-to-gh-pages.sh ./docs-output/book ${{ steps.path.outputs.target }} | |
| - name: Update PR comment | |
| if: always() && github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| env: | |
| SECTION: Documentation | |
| RESULT: ${{ steps.run.outputs.result }} | |
| with: | |
| script: | | |
| const marker = "<!-- ci-summary -->"; | |
| const detailsMarker = "<!-- details-section -->"; | |
| const section = process.env.SECTION; | |
| const result = process.env.RESULT || 'Unknown'; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.payload.pull_request.number; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, repo, issue_number, per_page: 100, | |
| }); | |
| const existing = comments.find(c => | |
| c.user?.login === "github-actions[bot]" && c.body?.includes(marker) | |
| ); | |
| let rows = {}; | |
| let detailsSection = ''; | |
| if (existing?.body) { | |
| const parts = existing.body.split(detailsMarker); | |
| const tableSection = parts[0] || ''; | |
| if (parts[1]) detailsSection = `\n${detailsMarker}${parts[1]}`; | |
| const lines = tableSection.split('\n'); | |
| for (const line of lines) { | |
| const match = line.match(/^\| ([^|]+) \| ([^|]+) \|$/); | |
| if (match) { | |
| const name = match[1].trim(); | |
| if (name && name !== 'Check' && !name.startsWith(':')) { | |
| rows[name] = match[2].trim(); | |
| } | |
| } | |
| } | |
| } | |
| rows[section] = result; | |
| const order = ['4naly3er Analysis', 'Slither Analysis', 'Contract Tests (Unit + Fuzz)', 'Contract Tests (Invariant)', 'Coverage', 'Documentation', 'Format & Lint', 'Deploy Contracts', 'PR Title', 'Labels']; | |
| const sortedKeys = Object.keys(rows).sort((a, b) => { | |
| const ai = order.indexOf(a), bi = order.indexOf(b); | |
| return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); | |
| }); | |
| let table = `| Check | Result |\n|:------|:-------|\n`; | |
| for (const key of sortedKeys) { | |
| table += `| ${key} | ${rows[key]} |\n`; | |
| } | |
| let body = `${marker}\n## CI Summary\n\n${table}${detailsSection}`; | |
| // GitHub issue/PR comments are capped at 65536 characters. Fall back | |
| // to the summary table only when the cumulative body would overflow; | |
| // per-section reports remain reachable via row `[View Report]` links. | |
| const MAX_BODY = 65000; | |
| if (body.length > MAX_BODY) { | |
| body = `${marker}\n## CI Summary\n\n${table}\n\n_Per-section details omitted: combined body exceeded the ${MAX_BODY}-char comment limit. Follow the **View Report** links above for each section's full output._`; | |
| } | |
| if (existing) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ owner, repo, issue_number, body }); | |
| } | |
| - if: steps.docs.outputs.success != 'true' | |
| run: exit 1 | |
| cleanup: | |
| runs-on: ubuntu-latest | |
| if: github.event.action == 'closed' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: gh-pages | |
| fetch-depth: 0 | |
| - run: | | |
| DIR="docs/preview/pr-${{ github.event.pull_request.number }}" | |
| [ -d "$DIR" ] || exit 0 | |
| rm -rf "$DIR" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add -A | |
| git commit -m "Clean up preview for PR #${{ github.event.pull_request.number }}" || true | |
| git push |