Skip to content

fix(cli): fix patched workflow files and examples #20

fix(cli): fix patched workflow files and examples

fix(cli): fix patched workflow files and examples #20

# 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-site
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 });
}