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
This commit is contained in:
Ed Zynda
2026-02-27 18:27:06 +03:00
parent 6d18ded8f9
commit f6ddd0b785
7 changed files with 448 additions and 45 deletions
+21 -6
View File
@@ -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
go-version: "1.26"
- run: go test -race ./...
+77 -9
View File
@@ -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
go-version: "1.26"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
- 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 }}
+48 -25
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# Downloaded binaries (only wrapper script should be committed)
bin/kit-bin
bin/kit.exe
Executable
+100
View File
@@ -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();
+153
View File
@@ -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();
+41
View File
@@ -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"
]
}