ci: add GitHub Release workflow with npm publish
This commit is contained in:
parent
795d00302e
commit
d5e15c6e71
|
|
@ -0,0 +1,198 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
mkdir -p dist
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
|
||||
for pair in "darwin amd64" "darwin arm64" "linux amd64" "linux arm64"; do
|
||||
GOOS=$(echo $pair | cut -d' ' -f1)
|
||||
GOARCH=$(echo $pair | cut -d' ' -f2)
|
||||
echo "Building ${GOOS}-${GOARCH}..."
|
||||
GOOS=$GOOS GOARCH=$GOARCH go build -ldflags "-s -w -X 'github.com/gitlink-org/gitlink-cli/cmd.Version=${VERSION}'" -o dist/gitlink-cli .
|
||||
cd dist
|
||||
tar -czf "gitlink-cli_${VERSION}_${GOOS}_${GOARCH}.tar.gz" gitlink-cli
|
||||
rm gitlink-cli
|
||||
cd ..
|
||||
done
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: dist/*.tar.gz
|
||||
generate_release_notes: true
|
||||
|
||||
- name: Build npm package
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
mkdir -p npm-pkg/bin npm-pkg/scripts npm-pkg/skills
|
||||
|
||||
# Copy skills
|
||||
cp -r skills/* npm-pkg/skills/
|
||||
|
||||
# Copy bin wrappers
|
||||
cp bin/cli.js npm-pkg/bin/ 2>/dev/null || cat > npm-pkg/bin/cli.js << 'NODEEOF'
|
||||
#!/usr/bin/env node
|
||||
const {execFileSync} = require("child_process");
|
||||
const path = require("path");
|
||||
const bin = path.join(__dirname, "..", "bin", "gitlink-cli");
|
||||
try { process.exit(execFileSync(bin, process.argv.slice(2), {stdio:"inherit"}).status); }
|
||||
catch(e) { process.exit(e.status || 1); }
|
||||
NODEEOF
|
||||
|
||||
cat > npm-pkg/bin/install-skills.js << 'NODEEOF'
|
||||
#!/usr/bin/env node
|
||||
const {execSync} = require("child_process");
|
||||
const path = require("path");
|
||||
const skillsDir = path.join(__dirname, "..", "skills");
|
||||
try {
|
||||
execSync(`npx skills add "${skillsDir}" -y -g`, {stdio:"inherit"});
|
||||
} catch(e) {
|
||||
console.error("Failed to install skills:", e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
NODEEOF
|
||||
|
||||
# Copy install.js
|
||||
cp scripts/install.js npm-pkg/scripts/ 2>/dev/null || cat > npm-pkg/scripts/install.js << 'NODEEOF'
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const {execSync} = require("child_process");
|
||||
|
||||
const PACKAGE = require("../package.json");
|
||||
const VERSION = PACKAGE.version;
|
||||
const BINARY_NAME = "gitlink-cli";
|
||||
const RELEASE_BASE = "https://github.com";
|
||||
const REPO_OWNER = "ccfos";
|
||||
const REPO_NAME = "gitlink-cli";
|
||||
|
||||
function getPlatformInfo() {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
const pmap = {darwin:"darwin",linux:"linux",win32:"windows"};
|
||||
const amap = {x64:"amd64",arm64:"arm64"};
|
||||
const p=pmap[platform], a=amap[arch];
|
||||
if(!p||!a) throw new Error(`Unsupported: ${platform}-${arch}`);
|
||||
return {platform:p,arch:a};
|
||||
}
|
||||
|
||||
function fetch(url) {
|
||||
return new Promise((resolve,reject) => {
|
||||
const mod=url.startsWith("https")?https:http;
|
||||
let count=0;
|
||||
function req(u) {
|
||||
if(++count>5) return reject(new Error("Too many redirects"));
|
||||
mod.get(u,(res) => {
|
||||
if([301,302,307,308].includes(res.statusCode)&&res.headers.location){
|
||||
let loc=res.headers.location;
|
||||
if(loc.startsWith("/")){const p=new URL(u);loc=p.protocol+"//"+p.host+loc}
|
||||
return req(loc);
|
||||
}
|
||||
if(res.statusCode!==200) return reject(new Error(`HTTP ${res.statusCode}`));
|
||||
const c=[];res.on("data",d=>c.push(d));res.on("end",()=>resolve(Buffer.concat(c)));
|
||||
}).on("error",reject);
|
||||
}
|
||||
req(url);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const {platform,arch} = getPlatformInfo();
|
||||
const binDir = path.join(__dirname,"..","bin");
|
||||
if(!fs.existsSync(binDir)) fs.mkdirSync(binDir,{recursive:true});
|
||||
const binaryPath = path.join(binDir,BINARY_NAME);
|
||||
|
||||
if(fs.existsSync(binaryPath)) {
|
||||
try {
|
||||
const out = execSync(`"${binaryPath}" version`,{encoding:"utf-8",timeout:5000});
|
||||
if(out.includes(VERSION)) { console.log(`${BINARY_NAME} v${VERSION} already installed.`); return; }
|
||||
} catch(e) {}
|
||||
fs.unlinkSync(binaryPath);
|
||||
}
|
||||
|
||||
const assetName = `gitlink-cli_${VERSION}_${platform}_${arch}.tar.gz`;
|
||||
const url = `${RELEASE_BASE}/${REPO_OWNER}/${REPO_NAME}/releases/download/v${VERSION}/${assetName}`;
|
||||
console.log(`Downloading ${url}...`);
|
||||
const data = await fetch(url);
|
||||
const tmp = path.join(binDir,"dl.tar.gz");
|
||||
fs.writeFileSync(tmp,data);
|
||||
execSync(`tar -xzf "${tmp}" -C "${binDir}"`,{stdio:"pipe"});
|
||||
fs.unlinkSync(tmp);
|
||||
fs.chmodSync(binaryPath,0o755);
|
||||
console.log(`${BINARY_NAME} v${VERSION} installed.`);
|
||||
} catch(err) {
|
||||
console.warn(`⚠ Binary download failed: ${err.message}`);
|
||||
console.warn(`Skills are installed. Install binary manually:`);
|
||||
console.warn(`npm run postinstall`);
|
||||
}
|
||||
}
|
||||
main();
|
||||
NODEEOF
|
||||
|
||||
# Create README
|
||||
cp README.md npm-pkg/
|
||||
|
||||
# Create package.json
|
||||
cat > npm-pkg/package.json << PKGEOF
|
||||
{
|
||||
"name": "@gitlink-ai/cli",
|
||||
"version": "${VERSION}",
|
||||
"description": "GitLink CLI — 面向 AI Agent 的 GitLink 命令行工具",
|
||||
"main": "bin/cli.js",
|
||||
"bin": {
|
||||
"gitlink-cli": "./bin/cli.js",
|
||||
"gitlink-cli-install-skills": "./bin/install-skills.js"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node scripts/install.js"
|
||||
},
|
||||
"files": [
|
||||
"bin/",
|
||||
"scripts/",
|
||||
"skills/",
|
||||
"README.md",
|
||||
"package.json"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ccfos/gitlink-cli.git"
|
||||
},
|
||||
"keywords": ["gitlink", "cli", "ai-agent", "skills"],
|
||||
"author": "",
|
||||
"license": "MulanPSL-2.0"
|
||||
}
|
||||
PKGEOF
|
||||
|
||||
cd npm-pkg
|
||||
npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
"use strict";
|
||||
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const PACKAGE = require("../package.json");
|
||||
const VERSION = PACKAGE.version;
|
||||
const BINARY_NAME = "gitlink-cli";
|
||||
|
||||
const RELEASE_BASE = "https://www.gitlink.org.cn";
|
||||
const REPO_OWNER = "Gitlink";
|
||||
const REPO_NAME = "gitlink-cli";
|
||||
|
||||
function getPlatformInfo() {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
const platformMap = { darwin: "darwin", linux: "linux", win32: "windows" };
|
||||
const archMap = { x64: "amd64", arm64: "arm64" };
|
||||
const goPlatform = platformMap[platform];
|
||||
const goArch = archMap[arch];
|
||||
if (!goPlatform || !goArch) {
|
||||
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
||||
}
|
||||
return { platform: goPlatform, arch: goArch, isWindows: platform === "win32" };
|
||||
}
|
||||
|
||||
function fetch(url, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const maxRedirects = options.maxRedirects || 5;
|
||||
let redirectCount = 0;
|
||||
function doRequest(currentUrl) {
|
||||
const mod = currentUrl.startsWith("https") ? https : http;
|
||||
const req = mod.get(currentUrl, (res) => {
|
||||
if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
|
||||
redirectCount++;
|
||||
if (redirectCount > maxRedirects) { reject(new Error("Too many redirects")); return; }
|
||||
let redirectUrl = res.headers.location;
|
||||
if (redirectUrl.startsWith("/")) {
|
||||
const parsed = new URL(currentUrl);
|
||||
redirectUrl = `${parsed.protocol}//${parsed.host}${redirectUrl}`;
|
||||
}
|
||||
doRequest(redirectUrl);
|
||||
return;
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
if (options.json) {
|
||||
let body = "";
|
||||
res.on("data", (chunk) => (body += chunk));
|
||||
res.on("end", () => {
|
||||
try { resolve(JSON.parse(body)); } catch (e) { reject(new Error("JSON parse failed")); }
|
||||
});
|
||||
} else {
|
||||
const chunks = [];
|
||||
res.on("data", (chunk) => chunks.push(chunk));
|
||||
res.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
}
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.setTimeout(30000, () => { req.destroy(); reject(new Error("Timeout")); });
|
||||
}
|
||||
doRequest(url);
|
||||
});
|
||||
}
|
||||
|
||||
async function findReleaseAsset(platform, arch) {
|
||||
const archiveName = `gitlink-cli_${VERSION}_${platform}_${arch}.tar.gz`;
|
||||
const tagName = `v${VERSION}`;
|
||||
const apiUrl = `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases.json`;
|
||||
|
||||
try {
|
||||
const releases = await fetch(apiUrl, { json: true });
|
||||
const rlist = Array.isArray(releases) ? releases : (releases && releases.releases ? releases.releases : []);
|
||||
let release = rlist.find(r => r.tag_name === tagName || r.tag_name === VERSION);
|
||||
if (!release && rlist.length > 0) release = rlist[0];
|
||||
|
||||
if (release && release.attachments) {
|
||||
let asset = release.attachments.find(a => a.title === archiveName || a.filename === archiveName);
|
||||
if (!asset) {
|
||||
const pattern = `_${platform}_${arch}.tar.gz`;
|
||||
asset = release.attachments.find(a => (a.title || a.filename || "").endsWith(pattern));
|
||||
}
|
||||
if (asset) {
|
||||
let url = asset.url || `${RELEASE_BASE}/api/attachments/${asset.id}`;
|
||||
if (url.startsWith("/")) url = RELEASE_BASE + url;
|
||||
return url;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases/${tagName}/assets/${archiveName}`;
|
||||
}
|
||||
|
||||
async function downloadAndExtract(url, destDir, platform) {
|
||||
const data = await fetch(url);
|
||||
const archivePath = path.join(destDir, "download.tar.gz");
|
||||
fs.writeFileSync(archivePath, data);
|
||||
execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: "pipe" });
|
||||
fs.unlinkSync(archivePath);
|
||||
|
||||
const binaryPath = path.join(destDir, BINARY_NAME);
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
const files = fs.readdirSync(destDir);
|
||||
for (const file of files) {
|
||||
const subPath = path.join(destDir, file, BINARY_NAME);
|
||||
if (fs.existsSync(subPath)) { fs.renameSync(subPath, binaryPath); break; }
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(binaryPath)) throw new Error("Binary not found after extraction");
|
||||
fs.chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { platform, arch } = getPlatformInfo();
|
||||
const binDir = path.join(__dirname, "..", "bin");
|
||||
if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); }
|
||||
|
||||
const binaryPath = path.join(binDir, BINARY_NAME);
|
||||
if (fs.existsSync(binaryPath)) {
|
||||
try {
|
||||
const output = execSync(`"${binaryPath}" version`, { encoding: "utf-8", stdio: "pipe", timeout: 5000 });
|
||||
if (output.includes(VERSION)) {
|
||||
console.log(`${BINARY_NAME} v${VERSION} already installed.`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
fs.unlinkSync(binaryPath);
|
||||
}
|
||||
|
||||
try {
|
||||
const downloadUrl = await findReleaseAsset(platform, arch);
|
||||
await downloadAndExtract(downloadUrl, binDir, platform);
|
||||
console.log(`${BINARY_NAME} v${VERSION} installed.`);
|
||||
} catch (err) {
|
||||
// Don't fail npm install — binary can be installed later
|
||||
console.warn(`⚠ ${BINARY_NAME} binary download failed: ${err.message}`);
|
||||
console.warn(` Skills are installed. You can install the binary manually later:`);
|
||||
console.warn(` npm run postinstall`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
Reference in New Issue