diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1f8af61 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,76 @@ +name: release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + build: + name: Build ${{ matrix.goos }}-${{ matrix.goarch }} + runs-on: ubuntu-latest + strategy: + matrix: + include: + - goos: darwin + goarch: amd64 + asset: taco-darwin-amd64 + - goos: darwin + goarch: arm64 + asset: taco-darwin-arm64 + - goos: linux + goarch: amd64 + asset: taco-linux-amd64 + - goos: linux + goarch: arm64 + asset: taco-linux-arm64 + - goos: windows + goarch: amd64 + asset: taco-windows-amd64.exe + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: Build binary + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: "0" + run: go build -ldflags="-s -w" -o ${{ matrix.asset }} ./cmd/taco + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.asset }} + path: ${{ matrix.asset }} + + release: + name: Create Release + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist/ + merge-multiple: true + + - name: Generate checksums + run: cd dist && sha256sum * > checksums.txt + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} diff --git a/.gitignore b/.gitignore index 0400c5c..08015fe 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,7 @@ Thumbs.db .env.development .env.production .env.test + +# Downloaded binaries in create-taco-app +packages/create-taco-app/taco +packages/create-taco-app/taco.exe diff --git a/Makefile b/Makefile index d73abb6..954fdbc 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ lint-fix: @hack/lint-fix.sh build-all: - go build -o npm/bin/taco-windows-amd64.exe ./cmd/taco + GOOS=windows GOARCH=amd64 go build -o npm/bin/taco-windows-amd64.exe ./cmd/taco GOOS=darwin GOARCH=amd64 go build -o npm/bin/taco-darwin-amd64 ./cmd/taco GOOS=darwin GOARCH=arm64 go build -o npm/bin/taco-darwin-arm64 ./cmd/taco GOOS=linux GOARCH=amd64 go build -o npm/bin/taco-linux-amd64 ./cmd/taco diff --git a/packages/create-taco-app/create-taco-app.js b/packages/create-taco-app/create-taco-app.js new file mode 100644 index 0000000..ef77c46 --- /dev/null +++ b/packages/create-taco-app/create-taco-app.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node +"use strict"; + +const path = require("path"); +const { execFileSync } = require("child_process"); +const os = require("os"); + +const ext = os.platform() === "win32" ? ".exe" : ""; +const binaryPath = path.join(__dirname, "taco" + ext); + +const userArgs = process.argv.slice(2); + +try { + execFileSync(binaryPath, ["init", ...userArgs], { stdio: "inherit" }); +} catch (err) { + process.exit(err.status || 1); +} diff --git a/packages/create-taco-app/package.json b/packages/create-taco-app/package.json new file mode 100644 index 0000000..464f5bf --- /dev/null +++ b/packages/create-taco-app/package.json @@ -0,0 +1,30 @@ +{ + "name": "create-taco-app", + "version": "0.1.0", + "description": "Scaffold a full-stack project with Taco CLI", + "bin": { + "create-taco-app": "create-taco-app.js" + }, + "scripts": { + "postinstall": "node postinstall.js" + }, + "files": [ + "create-taco-app.js", + "postinstall.js" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/b-jonathan/taco" + }, + "keywords": [ + "scaffold", + "fullstack", + "cli", + "create", + "taco" + ], + "engines": { + "node": ">=16.0.0" + } +} diff --git a/packages/create-taco-app/postinstall.js b/packages/create-taco-app/postinstall.js new file mode 100644 index 0000000..f954655 --- /dev/null +++ b/packages/create-taco-app/postinstall.js @@ -0,0 +1,147 @@ +#!/usr/bin/env node +"use strict"; + +const os = require("os"); +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const https = require("https"); +const http = require("http"); + +const REPO = "b-jonathan/taco"; +const VERSION = require("./package.json").version; +const MAX_REDIRECTS = 5; + +const PLATFORM_MAP = { + darwin: "darwin", + linux: "linux", + win32: "windows", +}; + +const ARCH_MAP = { + x64: "amd64", + arm64: "arm64", +}; + +const SUPPORTED = [ + "darwin-amd64", + "darwin-arm64", + "linux-amd64", + "linux-arm64", + "windows-amd64", +]; + +function getBinaryName(platform, arch) { + const goOS = PLATFORM_MAP[platform]; + const goArch = ARCH_MAP[arch]; + + if (!goOS || !goArch) return null; + + const key = `${goOS}-${goArch}`; + if (!SUPPORTED.includes(key)) return null; + + const ext = goOS === "windows" ? ".exe" : ""; + return `taco-${key}${ext}`; +} + +function downloadFile(url, redirectCount) { + if (redirectCount === undefined) redirectCount = 0; + + return new Promise((resolve, reject) => { + if (redirectCount > MAX_REDIRECTS) { + return reject(new Error("Too many redirects")); + } + + const client = url.startsWith("https") ? https : http; + + client + .get(url, (res) => { + if ( + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + return downloadFile(res.headers.location, redirectCount + 1).then( + resolve, + reject + ); + } + + if (res.statusCode !== 200) { + return reject( + new Error(`Download failed: HTTP ${res.statusCode} from ${url}`) + ); + } + + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => resolve(Buffer.concat(chunks))); + res.on("error", reject); + }) + .on("error", reject); + }); +} + +async function main() { + const platform = os.platform(); + const arch = os.arch(); + const binaryName = getBinaryName(platform, arch); + + if (!binaryName) { + console.error( + `[create-taco-app] Unsupported platform: ${platform}-${arch}\n` + + `Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64, win32-x64` + ); + process.exit(1); + } + + const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${binaryName}`; + const ext = platform === "win32" ? ".exe" : ""; + const dest = path.join(__dirname, "taco" + ext); + + console.log( + `[create-taco-app] Downloading taco v${VERSION} for ${platform}-${arch}...` + ); + + try { + const checksumsUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt`; + const [data, checksumsRaw] = await Promise.all([ + downloadFile(url), + downloadFile(checksumsUrl), + ]); + + const actualHash = crypto.createHash("sha256").update(data).digest("hex"); + const checksums = checksumsRaw.toString(); + const expectedLine = checksums + .split("\n") + .find((line) => line.includes(binaryName)); + + if (!expectedLine) { + throw new Error(`No checksum found for ${binaryName} in checksums.txt`); + } + + const expectedHash = expectedLine.split(/\s+/)[0]; + if (actualHash !== expectedHash) { + throw new Error( + `Checksum mismatch for ${binaryName}\n` + + ` expected: ${expectedHash}\n` + + ` got: ${actualHash}` + ); + } + + fs.writeFileSync(dest, data); + fs.chmodSync(dest, 0o755); + console.log(`[create-taco-app] Binary installed and verified successfully`); + } catch (err) { + console.error( + `[create-taco-app] Failed to download binary: ${err.message}` + ); + console.error( + `[create-taco-app] Ensure that release v${VERSION} exists at:\n` + + ` https://github.com/${REPO}/releases/tag/v${VERSION}` + ); + process.exit(1); + } +} + +main();