Skip to content

feat(cli): add cli package #27

feat(cli): add cli package

feat(cli): add cli package #27

Workflow file for this run

name: Release
on:
release:
types: [created]
pull_request:
branches: [main]
paths:
- "packages/cli/**"
- ".github/workflows/release.yml"
permissions:
contents: write
pull-requests: write
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2.6"
- name: Install dependencies
shell: bash
run: |
set -euo pipefail
bun install --frozen-lockfile
for pkg in packages/*; do
[ -f "$pkg/package.json" ] || continue
grep -q '"file:' "$pkg/package.json" || continue
grep -oP '"[^"]+": "file:\K[^"]+' "$pkg/package.json" | while read -r dep; do
name=$(jq -r .name "$pkg/$dep/package.json")
scope_dir="$pkg/node_modules/$(dirname "$name")"
mkdir -p "$scope_dir"
ln -sfn "$(cd "$pkg/$dep" && pwd)" "$pkg/node_modules/$name"
done
done
- name: Extract version
id: version
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "release" ]]; then
ref="${{ github.event.release.tag_name }}"
if [[ -z "${ref}" ]]; then
ref="${GITHUB_REF_NAME}"
fi
VERSION="${ref#v}"
TAG="v${VERSION}"
else
VERSION="0.0.0-pr.${{ github.event.pull_request.number }}"
TAG=""
fi
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
echo "tag=${TAG}" >> "${GITHUB_OUTPUT}"
- name: Build CLI (capture output)
id: build
continue-on-error: true
working-directory: packages/cli
shell: bash
run: |
set -o pipefail
bun run build 2>&1 | tee "$GITHUB_WORKSPACE/release-build-output.txt"
- name: Upload build output
if: always()
uses: actions/upload-artifact@v4
with:
name: release-build-output
path: release-build-output.txt
retention-days: 7
- name: Prepare package
if: steps.build.outcome == 'success'
working-directory: packages/cli
shell: bash
run: |
set -euo pipefail
npm version "${{ steps.version.outputs.version }}" --no-git-tag-version --allow-same-version
jq 'del(.private)' package.json > package.json.tmp
mv package.json.tmp package.json
- name: Pack
if: steps.build.outcome == 'success'
working-directory: packages/cli
run: npm pack
- name: Upload artifact
if: github.event_name == 'pull_request' && steps.build.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: cli-preview-${{ steps.version.outputs.version }}
path: packages/cli/*.tgz
retention-days: 7
- name: Process results
id: result
shell: bash
run: |
set -euo pipefail
if [[ "${{ steps.build.outcome }}" == "success" ]]; then
echo "status=Passed" >> "${GITHUB_OUTPUT}"
echo "details=Build succeeded" >> "${GITHUB_OUTPUT}"
else
echo "status=Failed" >> "${GITHUB_OUTPUT}"
echo "details=Package build failed" >> "${GITHUB_OUTPUT}"
fi
- name: Update PR comment
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
env:
SECTION: Release
STATUS: ${{ steps.result.outputs.status }}
DETAILS: ${{ steps.result.outputs.details }}
VERSION: ${{ steps.version.outputs.version }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
RUN_ID: ${{ github.run_id }}
REPO: ${{ github.repository }}
with:
script: |
const fs = require("fs");
const marker = "<!-- ci-summary -->";
const detailsMarker = "<!-- details-section -->";
const section = process.env.SECTION;
const status = process.env.STATUS;
const details = process.env.DETAILS;
const version = process.env.VERSION;
const runUrl = process.env.RUN_URL;
const runId = process.env.RUN_ID;
const repoFull = process.env.REPO;
const { owner, repo } = context.repo;
const issue_number = context.payload.pull_request.number;
let output = "";
try {
output = fs.readFileSync("release-build-output.txt", "utf8");
} catch (_) {
output = "(release-build-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 = status === "Passed" ? "Passed" : "Failed";
rows[section] = `[${resultText}](${runUrl})`;
if (status === "Passed") {
const installInstructions = [
"**Test this PR**",
"",
"Download artifact (GitHub CLI required):",
"```bash",
`gh run download ${runId} -n cli-preview-${version} -R ${repoFull}`,
"```",
"",
"Install globally:",
"",
"| Package Manager | Command |",
"|:----------------|:--------|",
`| npm | \`npm install -g ./dotns-cli-${version}.tgz\` |`,
`| yarn | \`yarn global add ./dotns-cli-${version}.tgz\` |`,
`| bun (macOS/Linux) | \`bun add -g "$(pwd)/dotns-cli-${version}.tgz"\` |`,
`| bun (Windows) | \`bun add -g "$PWD\\dotns-cli-${version}.tgz"\` |`,
"",
"Verify:",
"```bash",
"dotns --version",
"```"
].join("\n");
existingDetails[section] = [
`<details>`,
`<summary><strong>${section}</strong> - ${resultText}</summary>`,
"",
installInstructions,
"",
"</details>",
].join("\n");
} else {
existingDetails[section] = [
`<details>`,
`<summary><strong>${section}</strong> - ${resultText}</summary>`,
"",
details || "Failed",
"",
`[View run](${runUrl})`,
"",
"```text",
output,
"```",
"</details>",
].join("\n");
}
const order = ["Lint", "Format", "Typecheck", "Build", "Release", "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 detailsOrder = ["Lint", "Format", "Typecheck", "Build", "Release", "PR Title", "Labels"];
const sortedDetails = Object.keys(existingDetails).sort((a, b) => {
const ai = detailsOrder.indexOf(a), bi = detailsOrder.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 });
}
- name: Create release body
if: github.event_name == 'release' && steps.build.outcome == 'success'
id: release_body
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
TAG="${{ steps.version.outputs.tag }}"
REPO="${{ github.repository }}"
cat > release-body.md <<EOF
## Installation
Download:
\`\`\`bash
gh release download ${TAG} -p "*.tgz" -R ${REPO}
\`\`\`
Install:
| Package Manager | Command |
|:----------------|:--------|
| npm | \`npm install -g ./dotns-cli-${VERSION}.tgz\` |
| yarn | \`yarn global add ./dotns-cli-${VERSION}.tgz\` |
| bun (macOS/Linux) | \`bun add -g "\$(pwd)/dotns-cli-${VERSION}.tgz"\` |
| bun (Windows) | \`bun add -g "\$PWD\\dotns-cli-${VERSION}.tgz"\` |
Verify:
\`\`\`bash
dotns --version
\`\`\`
EOF
- name: Attach to release
if: github.event_name == 'release' && steps.build.outcome == 'success'
uses: softprops/action-gh-release@v2
with:
files: packages/cli/*.tgz
append_body: true
body_path: release-body.md
- name: Fail if build failed
if: steps.build.outcome == 'failure'
run: exit 1