feat(cli): use @ensdomains/content-hash for content hashes + workflow tweaks #13
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
| # Dogfooding workflow that exercises the reusable deploy.yml workflow on every PR | |
| # that touches the example site or either workflow file. It validates the static | |
| # site in examples/deploy/site/, uploads it to Bulletin via the DotNS CLI, and | |
| # registers a preview subname (e.g. pr42.dotns-example.dot). Results are posted | |
| # to the PR's CI Summary comment alongside other checks. | |
| # | |
| # The CLI is built from source (build-cli job) because no release exists yet. | |
| # Once a release is published, replace cli-artifact-name with cli-version and | |
| # remove the build-cli job. | |
| name: Deploy Example | |
| on: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - "examples/deploy/**" | |
| - ".github/workflows/deploy-example.yml" | |
| - ".github/workflows/deploy.yml" | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| build: | |
| name: Build Example | |
| runs-on: ubuntu-latest | |
| outputs: | |
| status: ${{ steps.result.outputs.status }} | |
| details: ${{ steps.result.outputs.details }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Validate site | |
| id: validate | |
| continue-on-error: true | |
| run: | | |
| set -o pipefail | |
| { | |
| echo "Validating examples/deploy/site/..." | |
| if [ ! -d "examples/deploy/site" ]; then | |
| echo "ERROR: examples/deploy/site/ directory not found" | |
| exit 1 | |
| fi | |
| if [ ! -f "examples/deploy/site/index.html" ]; then | |
| echo "ERROR: index.html not found" | |
| exit 1 | |
| fi | |
| FILE_COUNT=$(find examples/deploy/site -type f | wc -l) | |
| TOTAL_SIZE=$(du -sh examples/deploy/site | cut -f1) | |
| echo "Files: $FILE_COUNT" | |
| echo "Size: $TOTAL_SIZE" | |
| echo "" | |
| echo "Contents:" | |
| find examples/deploy/site -type f | sort | |
| echo "" | |
| echo "Validation passed" | |
| } 2>&1 | tee "$GITHUB_WORKSPACE/deploy-example-output.txt" | |
| - name: Upload build output | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: deploy-example-output | |
| path: deploy-example-output.txt | |
| retention-days: 7 | |
| - name: Upload site artifact | |
| if: steps.validate.outcome == 'success' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: example-site | |
| path: examples/deploy/site/ | |
| retention-days: 7 | |
| - name: Process results | |
| id: result | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "$VALIDATE_OUTCOME" == "success" ]]; then | |
| echo "status=Passed" >> "$GITHUB_OUTPUT" | |
| echo "details=Site validated" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "status=Failed" >> "$GITHUB_OUTPUT" | |
| echo "details=Site validation failed" >> "$GITHUB_OUTPUT" | |
| fi | |
| env: | |
| VALIDATE_OUTCOME: ${{ steps.validate.outcome }} | |
| - name: Fail if validation failed | |
| if: steps.validate.outcome == 'failure' | |
| run: exit 1 | |
| build-cli: | |
| name: Build CLI | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: '1.2.6' | |
| - name: Build and pack | |
| run: | | |
| cd packages/cli | |
| bun install | |
| bun run build | |
| npm pack | |
| - name: Upload CLI artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: cli-tgz | |
| path: packages/cli/dotns-cli-*.tgz | |
| retention-days: 7 | |
| deploy: | |
| name: Deploy Preview | |
| needs: [build, build-cli] | |
| uses: ./.github/workflows/deploy.yml | |
| with: | |
| basename: dotns-example | |
| artifact-name: example-site | |
| register-base: true | |
| cli-artifact-name: cli-tgz | |
| key-uri: '//Alice' | |
| comment: | |
| name: Update PR Comment | |
| needs: [build, deploy] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download build output | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: deploy-example-output | |
| path: . | |
| continue-on-error: true | |
| - name: Update PR comment | |
| uses: actions/github-script@v7 | |
| env: | |
| SECTION: Deploy Example | |
| BUILD_STATUS: ${{ needs.build.outputs.status }} | |
| BUILD_DETAILS: ${{ needs.build.outputs.details }} | |
| DEPLOY_RESULT: ${{ needs.deploy.result }} | |
| DEPLOY_CID: ${{ needs.deploy.outputs.cid }} | |
| DEPLOY_FQDN: ${{ needs.deploy.outputs.fqdn }} | |
| DEPLOY_URL: ${{ needs.deploy.outputs.url }} | |
| DEPLOY_CACHE_HIT: ${{ needs.deploy.outputs.cache-hit }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| const marker = "<!-- ci-summary -->"; | |
| const detailsMarker = "<!-- details-section -->"; | |
| const section = process.env.SECTION; | |
| const buildStatus = process.env.BUILD_STATUS; | |
| const buildDetails = process.env.BUILD_DETAILS; | |
| const deployResult = process.env.DEPLOY_RESULT; | |
| const cid = process.env.DEPLOY_CID; | |
| const fqdn = process.env.DEPLOY_FQDN; | |
| const url = process.env.DEPLOY_URL; | |
| const cacheHit = process.env.DEPLOY_CACHE_HIT === "true"; | |
| const runUrl = process.env.RUN_URL; | |
| const passed = buildStatus === "Passed" && deployResult === "success"; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.payload.pull_request.number; | |
| let output = ""; | |
| try { | |
| output = fs.readFileSync("deploy-example-output.txt", "utf8"); | |
| } catch (_) { | |
| output = "(deploy-example-output.txt not found)"; | |
| } | |
| const MAX_CHARS = 60000; | |
| if (output.length > MAX_CHARS) { | |
| output = ["(truncated; showing last " + MAX_CHARS + " chars)", "", output.slice(-MAX_CHARS)].join("\n"); | |
| } | |
| 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 existingDetails = {}; | |
| if (existing?.body) { | |
| const parts = existing.body.split(detailsMarker); | |
| const tableSection = parts[0] || ""; | |
| 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(); | |
| } | |
| } | |
| } | |
| const detailsRegex = /<details>\s*<summary><strong>([^<]+)<\/strong>.*?<\/summary>([\s\S]*?)<\/details>/g; | |
| let detailMatch; | |
| while ((detailMatch = detailsRegex.exec(existing.body)) !== null) { | |
| existingDetails[detailMatch[1].trim()] = detailMatch[0]; | |
| } | |
| } | |
| const resultText = passed ? "Passed" : "Failed"; | |
| rows[section] = `[${resultText}](${runUrl})`; | |
| if (passed && cid && fqdn) { | |
| const cidDisplay = cacheHit ? `\`${cid}\` (cached)` : `\`${cid}\``; | |
| existingDetails[section] = [ | |
| `<details>`, | |
| `<summary><strong>${section}</strong> - ${resultText}</summary>`, | |
| "", | |
| `| Property | Value |`, | |
| `|----------|-------|`, | |
| `| Domain | \`${fqdn}\` |`, | |
| `| CID | ${cidDisplay} |`, | |
| `| URL | [${url}](${url}) |`, | |
| "", | |
| `[View run](${runUrl})`, | |
| "", | |
| "</details>", | |
| ].join("\n"); | |
| } else { | |
| const failReason = buildStatus !== "Passed" | |
| ? (buildDetails || "Build validation failed") | |
| : "Deployment failed"; | |
| existingDetails[section] = [ | |
| `<details>`, | |
| `<summary><strong>${section}</strong> - ${resultText}</summary>`, | |
| "", | |
| failReason, | |
| "", | |
| `[View run](${runUrl})`, | |
| "", | |
| "```text", | |
| output, | |
| "```", | |
| "</details>", | |
| ].join("\n"); | |
| } | |
| const order = ["Lint", "Format", "Typecheck", "Build", "Release", "Deploy Example", "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`; | |
| } | |
| const sortedDetails = Object.keys(existingDetails).sort((a, b) => { | |
| const ai = order.indexOf(a), bi = order.indexOf(b); | |
| return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); | |
| }); | |
| let body = `${marker}\n## CI Summary\n\n${table}`; | |
| if (sortedDetails.length > 0) { | |
| body += `\n${detailsMarker}\n\n---\n\n${sortedDetails.map(k => existingDetails[k]).join("\n\n")}`; | |
| } | |
| 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 }); | |
| } |