diff --git a/.github/scripts/create-github-app.sh b/.github/scripts/create-github-app.sh new file mode 100755 index 0000000..e4e5271 --- /dev/null +++ b/.github/scripts/create-github-app.sh @@ -0,0 +1,140 @@ +#!/bin/bash +# This script helps create a GitHub App for homebrew automation + +echo "================================================================" +echo "GitHub App Creation Helper for Stacks Homebrew Automation" +echo "================================================================" +echo + +# Try to load from .env file if it exists +if [ -f ".env" ] || [ -f "../../.env" ]; then + source .env 2>/dev/null || source ../../.env 2>/dev/null +fi + +if [ -z "$GITHUB_TOKEN" ]; then + echo "❌ Error: GITHUB_TOKEN environment variable is not set" + echo + echo "To fix this, you need to:" + echo "1. Create a Personal Access Token (PAT):" + echo " - Go to: https://github.com/settings/tokens/new" + echo " - Click 'Generate new token (classic)'" + echo " - Give it a name like 'Stacks Homebrew Automation'" + echo " - Set expiration (recommended: 90 days)" + echo " - Select these scopes:" + echo " ✓ admin:org (Full control of orgs and teams)" + echo " ✓ repo (Full control of private repositories)" + echo " - Click 'Generate token' and copy the token" + echo + echo "2. Set the token in one of these ways:" + echo " a. Export it directly:" + echo " export GITHUB_TOKEN=your_token_here" + echo " b. Or add it to your .env file:" + echo " echo 'GITHUB_TOKEN=your_token_here' >> .env" + echo + echo "3. Run this script again:" + echo " ./github/scripts/create-github-app.sh" + echo + echo "Note: The token will only be shown once. Make sure to copy it!" + echo " You can revoke it anytime at: https://github.com/settings/tokens" + exit 1 +fi + +echo "1. Going to help you create a GitHub App..." + +# Check if gh CLI is installed +if ! command -v gh &> /dev/null; then + echo "GitHub CLI is not installed. Please install it:" + echo " - macOS: brew install gh" + echo " - Linux: https://github.com/cli/cli#installation" + exit 1 +fi + +# Authenticate with GitHub CLI if needed +gh auth status || (echo "Authenticating with GitHub CLI..." && echo "$GITHUB_TOKEN" | gh auth login --with-token) + +echo +echo "2. Opening GitHub App creation page in your browser..." +echo " IMPORTANT: You need to create this app in the stacksjs organization!" +echo " - Go to: https://github.com/organizations/stacksjs/settings/apps/new" +echo " - If that doesn't work, go to github.com/stacksjs, click Settings → Developer settings → GitHub Apps → New GitHub App" +echo +echo " Please fill in the following details:" +echo " - GitHub App name: Stacks Homebrew Automation" +echo " - Homepage URL: https://github.com/stacksjs/stacks" +echo " - Callback URL: (leave blank)" +echo " - Description: App for managing Homebrew tap for Stacks" +echo +echo " - Webhook section:" +echo " ✗ Active: UNCHECK this box (this makes the URL and secret optional)" +echo " (With Active unchecked, you can ignore the Webhook URL and Secret fields)" +echo +echo " - Under Repository permissions, select:" +echo " ✓ Contents: Read & write" +echo " ✓ Metadata: Read-only" +echo +echo " - Under Organization permissions, select:" +echo " ✓ Administration: Read & write" +echo " (Note: GitHub App can't create repositories, we'll use PAT for that)" +echo +echo " - At the bottom, select 'Only on this account' for installation" +echo + +# Open the URL in the browser for the stacksjs organization +open "https://github.com/organizations/stacksjs/settings/apps/new" 2>/dev/null || xdg-open "https://github.com/organizations/stacksjs/settings/apps/new" 2>/dev/null || echo "Please open this URL in your browser: https://github.com/organizations/stacksjs/settings/apps/new" + +echo +echo "3. After clicking 'Create GitHub App':" +echo " a. You'll be redirected to the App settings page" +echo " b. Note your App ID (shown at the top)" +echo " c. Generate a private key by clicking 'Generate a private key'" +echo " d. Save the downloaded .pem file" +echo + +echo "4. Please enter the App ID from the settings page: " +read APP_ID + +if [ -z "$APP_ID" ]; then + echo "Error: No App ID provided" + exit 1 +fi + +echo +echo "5. Please enter the path to the downloaded .pem private key file: " +read PEM_PATH + +if [ ! -f "$PEM_PATH" ]; then + echo "Error: Private key file not found at $PEM_PATH" + exit 1 +fi + +PEM_KEY=$(cat "$PEM_PATH") + +echo "6. Successfully registered GitHub App with ID: $APP_ID" + +# Create GitHub repository secrets +echo "7. Creating GitHub repository and organization secrets..." + +# Create secrets in the repository +echo "Creating GH_APP_ID secret..." +gh secret set GH_APP_ID -b"$APP_ID" -R stacksjs/stacks + +echo "Creating GH_APP_PRIVATE_KEY secret..." +gh secret set GH_APP_PRIVATE_KEY -b"$PEM_KEY" -R stacksjs/stacks + +echo "Creating GH_PAT as an ORGANIZATION secret (available to all stacksjs repos)..." +gh secret set GH_PAT -b"$GITHUB_TOKEN" -o stacksjs --visibility all + +echo +echo "8. Installation:" +echo " - On the App settings page, click 'Install App' in the sidebar" +echo " - You should see the stacksjs organization listed" +echo " - Click 'Install' next to stacksjs" +echo " - On the next page, choose 'All repositories' or 'Only select repositories'" +echo " - If 'Only select repositories', make sure to select 'stacks'" +echo " - Click 'Install'" +echo + +echo "Setup completed successfully!" +echo "The GitHub App ID is: $APP_ID" +echo "These values have been set as repository and organization secrets." +echo "================================================================" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 167c9ad..b85479c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,24 +1,30 @@ -name: CI +name: Release on: push: tags: - 'v*' +permissions: + contents: write + packages: write + id-token: write + jobs: - release: - name: release + npm: runs-on: ubuntu-latest - + outputs: + version: ${{ env.VERSION }} steps: - - uses: actions/checkout@v4 + - name: Checkout Code + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Install Bun + - name: Setup Bun uses: oven-sh/setup-bun@v2 - - name: Use cached node_modules + - name: Use Cached node_modules uses: actions/cache@v4 with: path: node_modules @@ -29,17 +35,17 @@ jobs: - name: Install Dependencies run: bun install + - name: Extract tag version + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + - name: Publish to npm run: bun publish --access public env: BUN_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - - name: Create GitHub release - run: bunx changelogithub - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - - - name: Attach Binaries + - name: Create GitHub Release + id: create_release uses: stacksjs/action-releaser@v1.0.0 with: files: | @@ -50,3 +56,135 @@ jobs: ./packages/rpx/bin/rpx-darwin-arm64.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Calculate SHA256 checksums + run: | + mkdir -p checksums + sha256sum ./packages/rpx/bin/rpx-linux-x64.zip > checksums/linux-x64.sha256 + sha256sum ./packages/rpx/bin/rpx-linux-arm64.zip > checksums/linux-arm64.sha256 + sha256sum ./packages/rpx/bin/rpx-darwin-x64.zip > checksums/darwin-x64.sha256 + sha256sum ./packages/rpx/bin/rpx-darwin-arm64.zip > checksums/darwin-arm64.sha256 + + - name: Upload Checksums Artifact + uses: actions/upload-artifact@v4 + with: + name: checksums + path: checksums/ + retention-days: 1 + + homebrew: + needs: npm + runs-on: macos-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Generate GitHub App Token + id: generate-token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.GH_APP_ID }} + private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - name: Setup GitHub CLI + run: | + echo "${{ steps.generate-token.outputs.token }}" | gh auth login --with-token + + - name: Ensure Homebrew Tap Repository Exists + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + run: | + if ! gh repo view stacksjs/homebrew-tap &>/dev/null; then + echo "Creating homebrew-tap repository..." + gh repo create stacksjs/homebrew-tap --public --description "Homebrew tap for Stacks packages" + else + echo "homebrew-tap repository already exists" + fi + + - name: Wait for Release Assets + run: | + VERSION=${GITHUB_REF#refs/tags/} + echo "Waiting for release assets to be available..." + + # Wait for assets to be available (max 10 minutes) + TIMEOUT=600 + START_TIME=$(date +%s) + + while [ $(( $(date +%s) - START_TIME )) -lt $TIMEOUT ]; do + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://github.com/stacksjs/rpx/releases/download/$VERSION/rpx-darwin-arm64.zip) + echo "HTTP response code: $HTTP_CODE" + + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then + echo "Assets are available!" + break + fi + + echo "Assets not ready yet, waiting 10 seconds..." + sleep 10 + done + + if [ $(( $(date +%s) - START_TIME )) -ge $TIMEOUT ]; then + echo "Timed out waiting for assets, proceeding anyway..." + fi + + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + name: checksums + path: checksums + + - name: Update Homebrew Formula + run: | + VERSION=${GITHUB_REF#refs/tags/v} + LINUX_X64_SHA=$(cat checksums/linux-x64.sha256 | awk '{print $1}') + LINUX_ARM64_SHA=$(cat checksums/linux-arm64.sha256 | awk '{print $1}') + DARWIN_X64_SHA=$(cat checksums/darwin-x64.sha256 | awk '{print $1}') + DARWIN_ARM64_SHA=$(cat checksums/darwin-arm64.sha256 | awk '{print $1}') + + # Check if tap/rpx.rb exists + if [ ! -f "tap/rpx.rb" ]; then + echo "Error: tap/rpx.rb file is missing" + exit 1 + fi + + # Update checksums in the formula + sed -i '' "s|sha256 \".*\" # darwin-arm64|sha256 \"${DARWIN_ARM64_SHA}\" # darwin-arm64|g" tap/rpx.rb + sed -i '' "s|sha256 \".*\" # darwin-x64|sha256 \"${DARWIN_X64_SHA}\" # darwin-x64|g" tap/rpx.rb + sed -i '' "s|sha256 \".*\" # linux-arm64|sha256 \"${LINUX_ARM64_SHA}\" # linux-arm64|g" tap/rpx.rb + sed -i '' "s|sha256 \".*\" # linux-x64|sha256 \"${LINUX_X64_SHA}\" # linux-x64|g" tap/rpx.rb + + # Verify that the version in tap/rpx.rb matches our release version + if ! grep -q "version \"${VERSION}\"" tap/rpx.rb; then + echo "Warning: Version mismatch in tap/rpx.rb. Setting to ${VERSION}" + sed -i '' "s|version \".*\"|version \"${VERSION}\"|g" tap/rpx.rb + fi + + - name: Push to Homebrew Tap Repository + run: | + # Clone the tap repo using GitHub App token + git clone https://x-access-token:${{ steps.generate-token.outputs.token }}@github.com/stacksjs/homebrew-tap.git + + # Set up Git config + cd homebrew-tap + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Copy the formula to the tap repo + mkdir -p Formula + cp ../tap/rpx.rb Formula/ + + # Check if there are changes to commit + if git status --porcelain | grep -q .; then + # Push the changes to the tap repo + git add Formula/rpx.rb + git commit -m "chore: update rpx formula to ${GITHUB_REF#refs/tags/}" + git push + echo "Successfully updated homebrew formula!" + else + echo "No changes to commit" + fi diff --git a/.gitignore b/.gitignore index 3250d73..0d61eee 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ node_modules temp docs/.vitepress/cache storage +.env diff --git a/package.json b/package.json index 788f178..8d76eb9 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "lint:fix": "bunx --bun eslint . --fix", "fresh": "bunx rimraf node_modules/ bun.lock && bun i", "changelog": "changelogen --output CHANGELOG.md", - "release": "bun run changelog && bumpp package.json --all", + "release": "bun run changelog && bumpp -r --all --execute \"bun scripts/update-tap-version.ts\"", "test": "bun test", "typecheck": "bunx tsc --noEmit", "build": "for dir in packages/*; do if [ -f \"$dir/package.json\" ]; then echo \"Building $dir\" && bun run --cwd $dir build; fi; done", diff --git a/scripts/update-tap-version.ts b/scripts/update-tap-version.ts new file mode 100755 index 0000000..b8663c7 --- /dev/null +++ b/scripts/update-tap-version.ts @@ -0,0 +1,102 @@ +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +// Get the package.json version +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const packageJsonPath = path.resolve(__dirname, '..', 'package.json') +const tapFilePath = path.resolve(__dirname, '..', 'tap', 'rpx.rb') + +// Ensure tap directory exists +const tapDir = path.resolve(__dirname, '..', 'tap') +if (!fs.existsSync(tapDir)) + fs.mkdirSync(tapDir, { recursive: true }) + +async function main() { + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) + const newVersion = packageJson.version + + console.log(`Updating tap/rpx.rb to version ${newVersion}`) + + // Check if rpx.rb exists + if (!fs.existsSync(tapFilePath)) { + // Create a basic template if it doesn't exist + const template = getTemplate(newVersion) + fs.writeFileSync(tapFilePath, template, 'utf8') + console.log('Created new tap/rpx.rb file') + return + } + + // Update the version in the existing file + let content = fs.readFileSync(tapFilePath, 'utf8') + + // Update version line + content = content.replace( + /version\s+["'](.+?)["']/, + `version "${newVersion}"`, + ) + + fs.writeFileSync(tapFilePath, content, 'utf8') + console.log('Updated tap/rpx.rb file successfully') + } + catch (error) { + console.error('Error updating tap/rpx.rb:', error) + process.exit(1) + } +} + +// Template for creating a new rpx.rb file if it doesn't exist +function getTemplate(version: string) { + return `class Rpx < Formula + desc "A modern and smart reverse proxy for local development" + homepage "https://github.com/stacksjs/rpx" + version "${version}" + license "MIT" + + on_macos do + if Hardware::CPU.arm? + url "https://github.com/stacksjs/rpx/releases/download/v#{version}/rpx-darwin-arm64.zip" + sha256 "PLACEHOLDER" # darwin-arm64 + else + url "https://github.com/stacksjs/rpx/releases/download/v#{version}/rpx-darwin-x64.zip" + sha256 "PLACEHOLDER" # darwin-x64 + end + end + + on_linux do + if Hardware::CPU.arm? + url "https://github.com/stacksjs/rpx/releases/download/v#{version}/rpx-linux-arm64.zip" + sha256 "PLACEHOLDER" # linux-arm64 + else + url "https://github.com/stacksjs/rpx/releases/download/v#{version}/rpx-linux-x64.zip" + sha256 "PLACEHOLDER" # linux-x64 + end + end + + depends_on "unzip" => :build + + def install + binary_name = Hardware::CPU.arm? ? + (OS.mac? ? "rpx-darwin-arm64" : "rpx-linux-arm64") : + (OS.mac? ? "rpx-darwin-x64" : "rpx-linux-x64") + + # Extract the zip file + system "unzip", "-o", "#{binary_name}.zip" + + # Install the binary + bin.install binary_name => "rpx" + + # Create symlink for 'reverse-proxy' command + bin.install_symlink "rpx" => "reverse-proxy" + end + + test do + assert_match version.to_s, shell_output("#{bin}/rpx --version") + end +end +` +} + +main().catch(console.error) diff --git a/tap/rpx.rb b/tap/rpx.rb new file mode 100644 index 0000000..ac1f2a7 --- /dev/null +++ b/tap/rpx.rb @@ -0,0 +1,47 @@ +class Rpx < Formula + desc "A modern and smart reverse proxy for local development" + homepage "https://github.com/stacksjs/rpx" + version "0.10.0" + license "MIT" + + on_macos do + if Hardware::CPU.arm? + url "https://github.com/stacksjs/rpx/releases/download/v#{version}/rpx-darwin-arm64.zip" + sha256 "PLACEHOLDER" # darwin-arm64 + else + url "https://github.com/stacksjs/rpx/releases/download/v#{version}/rpx-darwin-x64.zip" + sha256 "PLACEHOLDER" # darwin-x64 + end + end + + on_linux do + if Hardware::CPU.arm? + url "https://github.com/stacksjs/rpx/releases/download/v#{version}/rpx-linux-arm64.zip" + sha256 "PLACEHOLDER" # linux-arm64 + else + url "https://github.com/stacksjs/rpx/releases/download/v#{version}/rpx-linux-x64.zip" + sha256 "PLACEHOLDER" # linux-x64 + end + end + + depends_on "unzip" => :build + + def install + binary_name = Hardware::CPU.arm? ? + (OS.mac? ? "rpx-darwin-arm64" : "rpx-linux-arm64") : + (OS.mac? ? "rpx-darwin-x64" : "rpx-linux-x64") + + # Extract the zip file + system "unzip", "-o", "#{binary_name}.zip" + + # Install the binary + bin.install binary_name => "rpx" + + # Create symlink for 'reverse-proxy' command + bin.install_symlink "rpx" => "reverse-proxy" + end + + test do + assert_match version.to_s, shell_output("#{bin}/rpx --version") + end +end