fix(cli): fix patched workflow files and examples #18
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 | |
| deploy: | |
| name: Deploy Preview | |
| needs: [build] | |
| uses: ./.github/workflows/deploy.yml | |
| with: | |
| basename: dotns-example | |
| artifact-name: example-site | |
| register-base: true | |
| secrets: | |
| mnemonic: ${{ secrets.DOTNS_MNEMONIC }} | |
| 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: artifacts | |
| 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 }} | |
| BUILD_RESULT: ${{ needs.build.result }} | |
| 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 MAX_OUTPUT_CHARS = 60000; | |
| function readArtifact(filename) { | |
| try { | |
| return fs.readFileSync(`artifacts/${filename}`, "utf8"); | |
| } catch (_) { | |
| return ""; | |
| } | |
| } | |
| function truncate(text) { | |
| if (!text) return "(no output captured)"; | |
| if (text.length > MAX_OUTPUT_CHARS) { | |
| return "(truncated; showing last " + MAX_OUTPUT_CHARS + " chars)\n\n" + text.slice(-MAX_OUTPUT_CHARS); | |
| } | |
| return text; | |
| } | |
| const marker = "<!-- ci-summary -->"; | |
| const detailsMarker = "<!-- details-section -->"; | |
| const section = process.env.SECTION; | |
| const runUrl = process.env.RUN_URL; | |
| const buildStatus = process.env.BUILD_STATUS; | |
| const buildDetails = process.env.BUILD_DETAILS; | |
| const buildResult = process.env.BUILD_RESULT; | |
| 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 buildPassed = buildStatus === "Passed"; | |
| const deployPassed = deployResult === "success"; | |
| const allPassed = buildPassed && deployPassed; | |
| const stages = []; | |
| stages.push({ | |
| name: "Site validation", | |
| passed: buildPassed, | |
| detail: buildPassed | |
| ? "Site validated" | |
| : (buildDetails || "Validation failed"), | |
| }); | |
| if (deployResult === "skipped") { | |
| stages.push({ | |
| name: "Deploy", | |
| passed: false, | |
| detail: "Skipped (prerequisite job failed)", | |
| }); | |
| } else { | |
| stages.push({ | |
| name: "Deploy", | |
| passed: deployPassed, | |
| detail: deployPassed | |
| ? (cacheHit ? "Deployed (cached)" : "Deployed") | |
| : "Deploy workflow failed — see run logs for upload/register/contenthash details", | |
| }); | |
| } | |
| let failOutput = ""; | |
| const failedStage = stages.find(s => !s.passed); | |
| if (failedStage) { | |
| if (failedStage.name === "Site validation") { | |
| failOutput = readArtifact("deploy-example-output.txt"); | |
| } | |
| } | |
| 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 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 = allPassed ? "Passed" : "Failed"; | |
| rows[section] = `[${resultText}](${runUrl})`; | |
| const stageRows = stages | |
| .map(s => `| ${s.passed ? "✓" : "✗"} ${s.name} | ${s.detail} |`) | |
| .join("\n"); | |
| const stageTable = [ | |
| "| Stage | Status |", | |
| "|:------|:-------|", | |
| stageRows, | |
| ].join("\n"); | |
| if (allPassed && cid && fqdn) { | |
| const cidDisplay = cacheHit ? `\`${cid}\` (cached)` : `\`${cid}\``; | |
| existingDetails[section] = [ | |
| `<details>`, | |
| `<summary><strong>${section}</strong> — ${resultText}</summary>`, | |
| "", | |
| stageTable, | |
| "", | |
| `| Property | Value |`, | |
| `|----------|-------|`, | |
| `| Domain | \`${fqdn}\` |`, | |
| `| CID | ${cidDisplay} |`, | |
| `| URL | [${url}](${url}) |`, | |
| "", | |
| `[View run](${runUrl})`, | |
| "", | |
| "</details>", | |
| ].join("\n"); | |
| } else { | |
| const headline = failedStage | |
| ? `**Failed at: ${failedStage.name}** — ${failedStage.detail}` | |
| : "Unknown failure"; | |
| const outputBlock = failOutput | |
| ? ["", "```text", truncate(failOutput), "```"].join("\n") | |
| : ""; | |
| existingDetails[section] = [ | |
| `<details>`, | |
| `<summary><strong>${section}</strong> — ${resultText}</summary>`, | |
| "", | |
| headline, | |
| "", | |
| stageTable, | |
| "", | |
| `[View run](${runUrl})`, | |
| outputBlock, | |
| "", | |
| "</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 }); | |
| } |