From f6ddd0b7852e1d551f3862bdeda27de44ddd4fa1 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 27 Feb 2026 18:27:06 +0300 Subject: [PATCH] ci: add lint job, npm publish pipeline, and update goreleaser config - CI: add golangci-lint job, target master branch, use Go 1.26 - Release: add workflow_dispatch for npm-only releases, Discord notifications, and npm-publish job for @mark3labs/kit - Goreleaser: upgrade to v2 schema, lowercase archive names, structured changelog groups, release header with install instructions - npm: add package scaffolding with postinstall binary downloader --- .github/workflows/ci.yml | 33 ++++++-- .github/workflows/release.yml | 90 +++++++++++++++++--- .goreleaser.yaml | 73 ++++++++++------ npm/.gitignore | 3 + npm/bin/kit | 100 ++++++++++++++++++++++ npm/install.js | 153 ++++++++++++++++++++++++++++++++++ npm/package.json | 41 +++++++++ 7 files changed, 448 insertions(+), 45 deletions(-) create mode 100644 npm/.gitignore create mode 100755 npm/bin/kit create mode 100644 npm/install.js create mode 100644 npm/package.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21a647c4..430192e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,32 @@ -name: go +name: CI + on: push: - branches: - - main + branches: [master] pull_request: - workflow_dispatch: + branches: [master] jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.26" + + - uses: golangci/golangci-lint-action@v7 + with: + version: v2.8.0 + test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - run: go test ./... -race + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.26" + + - run: go test -race ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b14847fc..69a8f93e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,30 +4,98 @@ on: push: tags: - "v*" + workflow_dispatch: + inputs: + tag: + description: 'Tag to use for npm publish' + required: true + skip_goreleaser: + description: 'Skip goreleaser job (npm-only release)' + type: boolean + default: true permissions: contents: write + id-token: write # Required for npm trusted publishing (OIDC) jobs: goreleaser: runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' || !inputs.skip_goreleaser }} steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v4 + + - uses: actions/setup-go@v5 with: - go-version: ">=1.21.0" - cache: true - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 + go-version: "1.26" + + - uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser - version: latest + version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Send Discord Notification + if: success() + env: + DISCORD_WEBHOOK: ${{ secrets.RELEASES_WEBHOOK }} + TAG_NAME: ${{ github.ref_name }} + RELEASE_URL: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }} + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{ + \"embeds\": [{ + \"title\": \"New Release: $TAG_NAME\", + \"description\": \"A new version of kit has been released!\", + \"color\": 5814783, + \"fields\": [ + { + \"name\": \"Version\", + \"value\": \"$TAG_NAME\", + \"inline\": true + }, + { + \"name\": \"Repository\", + \"value\": \"[kit](https://github.com/${{ github.repository }})\", + \"inline\": true + } + ], + \"footer\": { + \"text\": \"Released via GitHub Actions\" + }, + \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\", + \"url\": \"$RELEASE_URL\" + }] + }" \ + $DISCORD_WEBHOOK + + npm-publish: + runs-on: ubuntu-latest + needs: goreleaser + if: ${{ always() && (needs.goreleaser.result == 'success' || needs.goreleaser.result == 'skipped') }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + + - name: Set version from tag + working-directory: npm + run: | + TAG=${{ inputs.tag || github.ref_name }} + VERSION=${TAG#v} + echo "Setting npm version to $VERSION" + npm version $VERSION --no-git-tag-version + + - name: Publish to npm + working-directory: npm + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a7dba872..1414af33 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,47 +1,70 @@ -before: - hooks: - - go mod tidy +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +version: 2 builds: - - id: kit - main: ./cmd/kit + - main: ./cmd/kit + binary: kit env: - CGO_ENABLED=0 goos: + - darwin - linux - windows - - darwin goarch: - amd64 - arm64 - ignore: - - goos: windows - goarch: arm64 - binary: kit ldflags: - - -s -w -X main.version={{.Version}} + - -s -w + - -X main.version={{.Version}} + - -X main.commit={{.Commit}} + - -X main.date={{.Date}} archives: - - format: tar.gz - name_template: >- - {{ .ProjectName }}_ - {{- title .Os }}_ - {{- if eq .Arch "amd64" }}x86_64 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} + - formats: [tar.gz] + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" format_overrides: - goos: windows - format: zip + formats: [zip] checksum: - name_template: "checksums.txt" + name_template: checksums.txt + +release: + header: | + ## Installation + + ```bash + # Bun / npm + bun add -g @mark3labs/kit + # or: npm install -g @mark3labs/kit + + # Go install (requires Go 1.26+) + go install github.com/mark3labs/kit/cmd/kit@latest + + # Or download from assets below + ``` changelog: sort: asc + use: github filters: exclude: - - "^docs:" - - "^test:" - - "^ci:" - - Merge pull request - - Merge branch + - "^Merge" + - "^merge" + - "^wip" + - "^WIP" + groups: + - title: "Features" + regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' + order: 0 + - title: "Bug Fixes" + regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' + order: 1 + - title: "Documentation" + regexp: '^.*?docs(\([[:word:]]+\))??!?:.+$' + order: 2 + - title: "Maintenance" + regexp: '^.*?(chore|refactor|style|ci|build)(\([[:word:]]+\))??!?:.+$' + order: 3 + - title: "Other Changes" + order: 999 diff --git a/npm/.gitignore b/npm/.gitignore new file mode 100644 index 00000000..39e6ae01 --- /dev/null +++ b/npm/.gitignore @@ -0,0 +1,3 @@ +# Downloaded binaries (only wrapper script should be committed) +bin/kit-bin +bin/kit.exe diff --git a/npm/bin/kit b/npm/bin/kit new file mode 100755 index 00000000..cc45b6e8 --- /dev/null +++ b/npm/bin/kit @@ -0,0 +1,100 @@ +#!/usr/bin/env node + +const { spawnSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const https = require("https"); + +const REPO = "mark3labs/kit"; +const BINARY = "kit"; + +const binDir = __dirname; +const isWindows = process.platform === "win32"; +const binaryName = isWindows ? "kit.exe" : "kit-bin"; +const binaryPath = path.join(binDir, binaryName); + +// Platform/arch mapping +const PLATFORM_MAP = { darwin: "darwin", linux: "linux", win32: "windows" }; +const ARCH_MAP = { x64: "amd64", arm64: "arm64" }; + +function download(url, dest) { + return new Promise((resolve, reject) => { + const follow = (url, redirects = 0) => { + if (redirects > 10) return reject(new Error("Too many redirects")); + https.get(url, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + return follow(res.headers.location, redirects + 1); + } + if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`)); + const file = fs.createWriteStream(dest); + res.pipe(file); + file.on("finish", () => { file.close(); resolve(); }); + file.on("error", (err) => { fs.unlinkSync(dest); reject(err); }); + }).on("error", reject); + }; + follow(url); + }); +} + +async function install() { + const platform = PLATFORM_MAP[process.platform]; + const arch = ARCH_MAP[process.arch]; + if (!platform || !arch) { + console.error(`Unsupported platform: ${process.platform}/${process.arch}`); + process.exit(1); + } + + const packageJson = require("../package.json"); + const version = packageJson.version; + const ext = platform === "windows" ? "zip" : "tar.gz"; + const filename = `${BINARY}_${version}_${platform}_${arch}.${ext}`; + const url = `https://github.com/${REPO}/releases/download/v${version}/${filename}`; + const archivePath = path.join(binDir, filename); + // Extract to temp dir to avoid overwriting this JS wrapper script + const tempDir = path.join(binDir, ".tmp-extract"); + + console.log(`Installing ${BINARY} v${version} (${platform}/${arch})...`); + + await download(url, archivePath); + + // Create temp extraction directory + fs.mkdirSync(tempDir, { recursive: true }); + + if (platform === "windows") { + spawnSync("powershell", ["-Command", `Expand-Archive -Path "${archivePath}" -DestinationPath "${tempDir}" -Force`]); + } else { + spawnSync("tar", ["-xzf", archivePath, "-C", tempDir]); + } + + // Move extracted binary to final location (from temp dir to avoid overwriting wrapper) + const extractedPath = path.join(tempDir, platform === "windows" ? `${BINARY}.exe` : BINARY); + if (fs.existsSync(extractedPath)) { + fs.renameSync(extractedPath, binaryPath); + } + + if (platform !== "windows") fs.chmodSync(binaryPath, 0o755); + fs.unlinkSync(archivePath); + fs.rmSync(tempDir, { recursive: true, force: true }); + console.log(`Installed ${BINARY} successfully`); +} + +async function main() { + // Install binary if missing + if (!fs.existsSync(binaryPath)) { + await install(); + } + + const result = spawnSync(binaryPath, process.argv.slice(2), { + stdio: "inherit", + shell: false, + }); + + if (result.error) { + console.error(`Failed to run kit: ${result.error.message}`); + process.exit(1); + } + + process.exit(result.status ?? 1); +} + +main(); diff --git a/npm/install.js b/npm/install.js new file mode 100644 index 00000000..192c9789 --- /dev/null +++ b/npm/install.js @@ -0,0 +1,153 @@ +#!/usr/bin/env node + +const { execSync, spawnSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const https = require("https"); +const { createWriteStream, mkdirSync, chmodSync, unlinkSync } = fs; + +const REPO = "mark3labs/kit"; +const BINARY = "kit"; + +// Get version from package.json +const packageJson = require("./package.json"); +const VERSION = packageJson.version; + +// Platform mapping +const PLATFORM_MAP = { + darwin: "darwin", + linux: "linux", + win32: "windows", +}; + +// Arch mapping +const ARCH_MAP = { + x64: "amd64", + arm64: "arm64", +}; + +function getPlatform() { + const platform = PLATFORM_MAP[process.platform]; + if (!platform) { + throw new Error(`Unsupported platform: ${process.platform}`); + } + return platform; +} + +function getArch() { + const arch = ARCH_MAP[process.arch]; + if (!arch) { + throw new Error(`Unsupported architecture: ${process.arch}`); + } + return arch; +} + +function getExtension(platform) { + return platform === "windows" ? "zip" : "tar.gz"; +} + +function getBinaryName(platform) { + // Use different name to avoid conflict with JS wrapper on Unix + return platform === "windows" ? `${BINARY}.exe` : `${BINARY}-bin`; +} + +function download(url, dest) { + return new Promise((resolve, reject) => { + const follow = (url, redirects = 0) => { + if (redirects > 10) { + reject(new Error("Too many redirects")); + return; + } + + https + .get(url, (response) => { + if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { + follow(response.headers.location, redirects + 1); + return; + } + + if (response.statusCode !== 200) { + reject(new Error(`Failed to download: ${response.statusCode}`)); + return; + } + + const file = createWriteStream(dest); + response.pipe(file); + file.on("finish", () => { + file.close(); + resolve(); + }); + file.on("error", (err) => { + unlinkSync(dest); + reject(err); + }); + }) + .on("error", reject); + }; + + follow(url); + }); +} + +function extract(archivePath, destDir, platform) { + if (platform === "windows") { + // Use PowerShell to extract zip on Windows + spawnSync("powershell", [ + "-Command", + `Expand-Archive -Path "${archivePath}" -DestinationPath "${destDir}" -Force`, + ]); + } else { + // Use tar for Unix systems + spawnSync("tar", ["-xzf", archivePath, "-C", destDir]); + } +} + +async function main() { + const platform = getPlatform(); + const arch = getArch(); + const ext = getExtension(platform); + const binaryName = getBinaryName(platform); + + const filename = `${BINARY}_${VERSION}_${platform}_${arch}.${ext}`; + const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${filename}`; + + const binDir = path.join(__dirname, "bin"); + const archivePath = path.join(__dirname, filename); + const binaryPath = path.join(binDir, binaryName); + + console.log(`Installing ${BINARY} v${VERSION} (${platform}/${arch})...`); + console.log(`Downloading from ${url}...`); + + try { + // Ensure bin directory exists + mkdirSync(binDir, { recursive: true }); + + // Download archive + await download(url, archivePath); + + // Extract + extract(archivePath, binDir, platform); + + // Rename binary (archive contains "kit", we want "kit-bin" on Unix) + const extractedName = platform === "windows" ? `${BINARY}.exe` : BINARY; + const extractedPath = path.join(binDir, extractedName); + if (extractedPath !== binaryPath && fs.existsSync(extractedPath)) { + fs.renameSync(extractedPath, binaryPath); + } + + // Make executable on Unix + if (platform !== "windows") { + chmodSync(binaryPath, 0o755); + } + + // Clean up archive + unlinkSync(archivePath); + + console.log(`Successfully installed ${BINARY} to ${binaryPath}`); + } catch (err) { + console.error(`Failed to install ${BINARY}: ${err.message}`); + process.exit(1); + } +} + +main(); diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 00000000..c94134cb --- /dev/null +++ b/npm/package.json @@ -0,0 +1,41 @@ +{ + "name": "@mark3labs/kit", + "version": "0.0.0", + "description": "AI-powered CLI tool", + "repository": { + "type": "git", + "url": "git+https://github.com/mark3labs/kit.git" + }, + "homepage": "https://github.com/mark3labs/kit", + "bugs": { + "url": "https://github.com/mark3labs/kit/issues" + }, + "license": "MIT", + "bin": { + "kit": "bin/kit" + }, + "scripts": { + "postinstall": "node install.js" + }, + "files": [ + "bin", + "install.js" + ], + "engines": { + "node": ">=16" + }, + "os": [ + "darwin", + "linux", + "win32" + ], + "cpu": [ + "x64", + "arm64" + ], + "keywords": [ + "cli", + "kit", + "ai" + ] +}