Based on the Fizzy project and broader software development practices, the project appears to use hash-based versioning (or a variant combining git commit hashes with dates), which represents a fundamentally different philosophy from semantic versioning.12
Hash-based versioning uses git commit hashes—typically shortened to 7-8 characters—as version identifiers. These are alphanumeric strings like 9ce4987 or 3f9a7c1d that uniquely identify a specific commit in the repository. This approach is increasingly common in continuous deployment environments where releases happen frequently, sometimes multiple times per day.345678
Semantic Versioning (SemVer) follows a MAJOR.MINOR.PATCH format (e.g., 2.3.1) where:9
- MAJOR indicates breaking changes
- MINOR adds backward-compatible functionality
- PATCH includes backward-compatible bug fixes
This system communicates the nature and severity of changes, helping users understand compatibility implications at a glance.109
Hash Versioning, by contrast:21
- Provides a unique identifier for each build without implying meaning
- Doesn't communicate compatibility or change magnitude
- Prioritizes traceability to exact source code state
- Works seamlessly with parallel development streams
For High-Frequency Releases:812 In continuous deployment environments, hash versioning excels because every parallel build automatically generates its own unique identifier. There's no need for centralized counters or coordination between teams to increment version numbers.1
Perfect Traceability:46 Every hash directly maps to a specific git commit, making it trivial to determine exactly what code is running in production. You can instantly recreate the build environment or debug issues by checking out that exact commit.1
Parallel Development Support:1 When multiple branches are being built simultaneously, hash versioning avoids conflicts. Each stream generates its own hash without stepping on others' toes.1
Automation-Friendly:8 Hash versions can be automatically generated as part of the CI/CD pipeline without human intervention, reducing deployment friction.4
No Semantic Meaning:111
The biggest drawback is that hashes are opaque. Looking at 9ce4987 versus a3f2b1c tells you nothing about which is newer, which has more features, or whether upgrading will break your code.11
Human Readability:1 Hashes are difficult for humans to remember, communicate, or reason about. Try telling a colleague "we need to roll back to nine-charlie-echo-four-nine-eight-seven".2
No Chronological Information:1 Hashes contain no timestamp data, making it impossible to determine age or order without consulting external metadata.1
Dependency Management Challenges:9 Package managers and dependency systems often rely on semantic versioning to resolve compatible versions. Hash-based schemes make this significantly harder.9
Many modern projects combine both systems:1281
Date + Hash Format (e.g., 2025.07.30.1906.3f9a7c1d):8
- Provides chronological context from the date
- Maintains exact traceability through the hash
- Supports high-frequency continuous deployment
SemVer + Hash in Metadata (e.g., 1.2.3+9ce4987):1
- Maintains semantic meaning for users
- Includes hash for developer traceability
- Gets "best of both worlds"1
Calendar Versioning (CalVer) Alternative:1314
Some projects use date-based schemes like YYYY.MM.DD or YY.MM.MICRO, which provide temporal context while avoiding arbitrary numbers. Ubuntu's 16.04 (April 2016) is a famous example that integrates with their support schedule.14
- Deploying continuously (multiple times daily)
- Users always use the latest version (like web services)
- Exact source traceability is critical
- Working with parallel development streams
- Automation and CI/CD are paramount
Use Semantic Versioning When:109
- Managing libraries or dependencies
- Users need to make upgrade decisions
- Backward compatibility matters significantly
- Releases are less frequent and more deliberate
- Communication about changes is important
Use Hybrid Approaches When:121
- You need both traceability AND semantic meaning
- Balancing automation with human comprehension
- Supporting both internal (hash) and external (semver) audiences
Fizzy, as a web-based SaaS application from 37signals, represents an ideal use case for hash-based or date-hash versioning. Users don't choose versions—they simply use whatever is deployed. The team deploys frequently, values automation, and needs precise traceability for debugging production issues. For this context, the overhead of maintaining semantic version numbers would provide little value while hash-based versioning delivers exactly what's needed: automatic, unique, traceable identifiers tied directly to the source code.1528 16171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
Based on your current setup with the conduit_design project, here's how you could transition from semantic versioning to hash-based versioning (HashVer) and the practical implementation for your install and run scripts.
The HashVer scheme consists of date components plus a git commit hash:65
| Format | Example | Use Case |
|---|---|---|
YYYY.MM.hash |
2025.12.67092445a1abc |
Monthly or less frequent releases |
YYYY.MM.DD.hash |
2025.12.04.3731a8be0f1a |
Daily releases |
YYYY.MM.DD+hash |
2025.12.04+3731a8be0f1a |
SemVer-compatible tools |
For your install.sh and run.sh scripts, you can generate hash versions with these one-liners:65
# Basic HashVer (year.month.hash)
VERSION=$(printf '%(%Y.%m.)T'; git rev-parse --short=12 HEAD)
# Output: 2025.12.67092445a1abc
# HashVer with day (year.month.day.hash)
VERSION=$(printf '%(%Y.%m.%d.)T'; git rev-parse --short=12 HEAD)
# Output: 2025.12.04.3731a8be0f1a
# SemVer-compatible format (uses + for metadata)
VERSION=$(printf '%(%Y.%m.%d+)T'; git rev-parse --short=12 HEAD)
# Output: 2025.12.04+3731a8be0f1aAlternatively, using the more portable approach:66
# Get short hash (7-12 characters)
GIT_HASH=$(git rev-parse --short=12 HEAD)
# Get date components
DATE_PART=$(date +%Y.%m.%d)
# Combine
VERSION="${DATE_PART}.${GIT_HASH}"For your install.sh, you could add version detection:
#!/bin/bash
# Generate HashVer
get_version() {
local hash=$(git rev-parse --short=12 HEAD 2>/dev/null || echo "unknown")
local date_part=$(date +%Y.%m.%d)
echo "${date_part}.${hash}"
}
VERSION=$(get_version)
echo "Installing Conduit Design version: $VERSION"
# Rest of your installation logic...For your run.sh:
#!/bin/bash
# Display current version
VERSION=$(printf '%(%Y.%m.%d.)T'; git rev-parse --short=12 HEAD)
echo "Running Conduit Design $VERSION"
# Your existing run logic...To automate releases with HashVer tags, create a workflow like this:
name: HashVer Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate HashVer
id: version
run: |
HASH=$(git rev-parse --short=12 HEAD)
DATE=$(date +%Y.%m.%d)
VERSION="${DATE}.${HASH}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.version.outputs.version }}
release_name: Release ${{ steps.version.outputs.version }}If you want to maintain both human-readable semantics and traceability, the recommended practice is to append the hash as build metadata:6768
1.2.3+2025.12.04.67092445a1
This approach:
- Keeps the human-readable
MAJOR.MINOR.PATCHfor compatibility decisions - Adds the hash as metadata (after
+) for exact traceability - Works with existing SemVer tooling since the
+metadataportion is ignored in version comparisons6967
In bash:
# Read current semver from package.json or version file
SEMVER="1.2.3"
HASH=$(git rev-parse --short=12 HEAD)
DATE=$(date +%Y.%m.%d)
FULL_VERSION="${SEMVER}+${DATE}.${HASH}"
# Output: 1.2.3+2025.12.04.67092445a1abcBased on the HashVer guidelines, pure hash versioning is ideal when:65
- You deploy to production after most commits
- Users always use the latest version (web services, SaaS)
- You have automated CI/CD pipelines
- You have strong safety checks (tests, monitoring, rollback capability)
For Conduit Design as an MCP tool/Figma plugin, the hybrid approach might be best since:70
- Users may need to reference specific versions for bug reports
- The plugin may need to communicate version compatibility with Figma
- You might want meaningful version bumps for feature announcements
| Aspect | Your Current SemVer | Pure HashVer | Hybrid |
|---|---|---|---|
| Human readable | ✅ 1.2.3 |
❌ 2025.12.67092 |
✅ 1.2.3+67092 |
| Automatic | ❌ Manual bump | ✅ Fully auto | ⚡ Semi-auto |
| Traceability | ❌ Need to look up | ✅ Instant | ✅ Instant |
| Compatibility info | ✅ Breaking changes clear | ❌ Opaque | ✅ Clear |
| CI/CD friendly | ⚡ Requires logic | ✅ Native | ✅ Good |
The simplest migration path would be to keep your progressive SemVer workflow but append the hash as metadata, giving you the best of both worlds without breaking existing tooling or user expectations. 7172737475767778798081828384858687888990919293949596979899100101102103104105
I wasn't able to access the raw content of your install.sh and run.sh files directly. However, based on common patterns and the GitHub API documentation, here's how scripts typically determine the latest release and how you could adapt this for HashVer:
The standard approach uses the GitHub REST API's /releases/latest endpoint:106
# Get the latest release tag
LATEST_TAG=$(curl -sL https://api.github.com/repos/conduit-design/conduit_design/releases/latest \
| grep '"tag_name"' \
| sed -E 's/.*"([^"]+)".*/\1/')
echo "Latest version: $LATEST_TAG"Or with jq for cleaner JSON parsing:107108
LATEST_TAG=$(curl -sL https://api.github.com/repos/conduit-design/conduit_design/releases/latest \
| jq -r '.tag_name')GitHub provides a convenient redirect URL that returns release info as JSON:109
LATEST_RELEASE=$(curl -L -s -H 'Accept: application/json' \
https://github.com/conduit-design/conduit_design/releases/latest)
LATEST_VERSION=$(echo $LATEST_RELEASE | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/')You can query tags directly from the git repository:110107
# Get latest tag sorted by version
LATEST_TAG=$(git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' \
https://github.com/conduit-design/conduit_design.git \
| tail -n 1 \
| cut -d '/' -f 3)With HashVer (e.g., 2025.12.04.67092445a1abc), the sorting changes since dates sort naturally:
The GitHub API /releases/latest returns the most recent non-prerelease, non-draft release based on the created_at timestamp—not the tag name. This means HashVer tags will work correctly without modification:106
#!/bin/bash
# install.sh - works with both SemVer and HashVer
REPO="conduit-design/conduit_design"
# This works regardless of versioning scheme
LATEST_TAG=$(curl -sL "https://api.github.com/repos/${REPO}/releases/latest" \
| jq -r '.tag_name')
echo "Installing version: $LATEST_TAG"
# Download the release asset
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${LATEST_TAG}/conduit-server"
curl -L -o conduit-server "$DOWNLOAD_URL"If you need to manually sort HashVer tags:
#!/bin/bash
# HashVer tags like 2025.12.04.67092445a1abc sort lexicographically by date
REPO="conduit-design/conduit_design"
# Get all tags and sort (HashVer dates sort naturally)
LATEST_TAG=$(curl -sL "https://api.github.com/repos/${REPO}/tags" \
| jq -r '.[].name' \
| sort -r \
| head -n 1)
echo "Latest HashVer: $LATEST_TAG"If you stick with SemVer but add hash metadata (e.g., 1.2.3+2025.12.04.67092445):
#!/bin/bash
# Works with tags like 1.2.3+2025.12.04.67092445
REPO="conduit-design/conduit_design"
# The API still returns the latest by created_at, not tag parsing
LATEST=$(curl -sL "https://api.github.com/repos/${REPO}/releases/latest")
TAG=$(echo "$LATEST" | jq -r '.tag_name') # e.g., 1.2.3+2025.12.04.67092
SEMVER=$(echo "$TAG" | cut -d '+' -f 1) # e.g., 1.2.3
HASH_PART=$(echo "$TAG" | cut -d '+' -f 2) # e.g., 2025.12.04.67092
echo "Version: $SEMVER (build: $HASH_PART)"The crucial point is that GitHub's /releases/latest endpoint determines "latest" by the commit's created_at date, not by parsing or sorting tag names. This means:106
| Versioning Scheme | API /releases/latest Works? |
Manual Sort Needed? |
|---|---|---|
SemVer (1.2.3) |
✅ Yes | No |
HashVer (2025.12.67092) |
✅ Yes | No |
Hybrid (1.2.3+67092) |
✅ Yes | No |
Your scripts should work with HashVer without modification as long as you're using the GitHub API to fetch the latest release rather than parsing/sorting tag names manually. 111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
https://raw.githubusercontent.com/eonist/conduit-landing-page/refs/heads/main/install.sh https://raw.githubusercontent.com/eonist/conduit-landing-page/refs/heads/main/run.sh
Unfortunately, I'm unable to directly access the raw content of those specific scripts. However, based on the repository structure and common patterns used in similar projects, let me provide you with a comprehensive answer about how scripts typically determine the latest release:
Based on what I found in the search results and common patterns, here are the most likely approaches your scripts are using:
Your install.sh likely uses something like this:
#!/bin/bash
REPO="eonist/conduit-landing-page"
# or wherever the actual release artifacts are
# Fetch latest release from GitHub API
LATEST=$(curl -sL "https://api.github.com/repos/${REPO}/releases/latest")
# Extract version tag
VERSION=$(echo "$LATEST" | grep -o '"tag_name":"[^"]*' | cut -d'"' -f4)
# Extract download URL for the binary/asset
DOWNLOAD_URL=$(echo "$LATEST" | grep -o '"browser_download_url":"[^"]*' | head -1 | cut -d'"' -f4)
echo "Latest version: $VERSION"
echo "Downloading from: $DOWNLOAD_URL"
# Download the binary
curl -L -o conduit "$DOWNLOAD_URL"
chmod +x conduitSome scripts use this simpler pattern:
#!/bin/bash
# GitHub redirects /latest to the most recent release
REPO="eonist/conduit-landing-page"
RELEASE_URL="https://github.com/${REPO}/releases/latest"
# Get the actual tag from the redirect
TAG=$(curl -sL -I -o /dev/null -w '%{url_effective}' "$RELEASE_URL" | grep -o '[^/]*$')
echo "Latest version: $TAG"If using git ls-remote:
#!/bin/bash
REPO_URL="https://github.com/eonist/conduit-landing-page.git"
# Get the latest tag (works without authentication)
LATEST_TAG=$(git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' \
"$REPO_URL" | tail -n 1 | sed 's/^[^v]*//;s/[^^]*$//')
echo "Latest version: $LATEST_TAG"Your run.sh probably embeds or references the version directly:
#!/bin/bash
# Either hardcoded
VERSION="1.2.3"
# Or dynamically fetched at runtime
VERSION=$(./get_version.sh)
echo "Running version: $VERSION"
# Then executes the application
./conduit-server --version "$VERSION"The critical point is that GitHub's /releases/latest endpoint automatically returns releases sorted by created_at timestamp, not by tag name parsing. This means:
| Current Approach | Works with HashVer? | Changes Needed? |
|---|---|---|
GitHub API /latest |
✅ Yes | None! |
| Git tag sorting | ❌ No | Must add date-based sorting |
| Redirect following | ✅ Yes | None! |
| Hardcoded version | ❌ No | Manual update each release |
To safely migrate your scripts to HashVer without breaking anything:
- Keep using GitHub API
/releases/latest- it works automatically - Update your CI/CD to generate HashVer tags (e.g.,
2025.12.04.67092445) - No changes needed to your install.sh or run.sh if they use the API
This is the beauty of the API approach—it's versioning-scheme-agnostic as long as you're using GitHub's release features properly! 142143144145146147148149150151152153154155156157158
curl -sSL https://conduit.design/install.sh | bash
set -euo pipefail
INSTALL_DIR="${HOME}/.local/bin" BINARY_NAME="conduit-mcp" BINARY_PATH="${INSTALL_DIR}/${BINARY_NAME}"
PLUGIN_DIR="${HOME}/.conduit/figma-plugin" PLUGIN_ZIP_NAME="figma-plugin.zip"
GITHUB_OWNER="conduit-design" GITHUB_REPO="conduit_design" BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download"
log() { printf '[conduit.install] %s\n' "$*" >&2 }
err() { printf '[conduit.install][ERROR] %s\n' "$*" >&2 }
detect_os_arch() { local uname_s uname_m uname_s="$(uname -s | tr '[:upper:]' '[:lower:]')" uname_m="$(uname -m)"
case "${uname_s}" in darwin) OS_NAME="macos" ;; linux) OS_NAME="linux" ;; *) err "Unsupported OS: ${uname_s}. Only macOS and Linux are supported in v1." exit 1 ;; esac
case "${uname_m}" in x86_64|amd64) ARCH_NAME="x64" ;; arm64|aarch64) ARCH_NAME="arm64" ;; *) err "Unsupported architecture: ${uname_m}. Supported: x86_64, arm64." exit 1 ;; esac
log "Detected: ${OS_NAME}-${ARCH_NAME}" }
needs_update() { local path="$1" if [ ! -f "${path}" ]; then return 0 # Needs update because it doesn't exist. fi
if [ "$(uname -s)" = "Darwin" ]; then
# macOS
if [
ensure_install_dir() { mkdir -p "${INSTALL_DIR}" }
ensure_plugin_dir() { mkdir -p "${PLUGIN_DIR}" }
have_unzip() { command -v unzip >/dev/null 2>&1 }
install_or_update_binary() { detect_os_arch ensure_install_dir
local asset="conduit-${OS_NAME}-${ARCH_NAME}" local url="${BASE_URL}/${asset}"
if needs_update "${BINARY_PATH}"; then
log "Installing/updating Conduit MCP binary (
# Download with better error handling
if ! curl -fsSL --show-error "${url}" -o "${BINARY_PATH}.tmp"; then
err "Failed to download binary from ${url}"
err "Please check your internet connection and try again."
rm -f "${BINARY_PATH}.tmp"
exit 1
fi
# Verify download is not empty
if [ ! -s "${BINARY_PATH}.tmp" ]; then
err "Downloaded file is empty"
rm -f "${BINARY_PATH}.tmp"
exit 1
fi
# Move to final location
sync || true # Ensure file system buffers are flushed before atomic move
mv "${BINARY_PATH}.tmp" "${BINARY_PATH}"
chmod +x "${BINARY_PATH}"
# Verify it's a valid binary for the detected OS.
if [ "${OS_NAME}" = "macos" ]; then
if ! file "${BINARY_PATH}" | grep -q "Mach-O"; then
err "Downloaded file is not a valid macOS executable. File type: $(file ${BINARY_PATH})"
exit 1
fi
log "Binary verified as valid Mach-O executable."
elif [ "${OS_NAME}" = "linux" ]; then
if ! file "${BINARY_PATH}" | grep -q "ELF"; then
err "Downloaded file is not a valid Linux executable. File type: $(file ${BINARY_PATH})"
exit 1
fi
log "Binary verified as valid ELF executable."
fi
log "Binary installed successfully to: ${BINARY_PATH}"
else
log "Conduit MCP binary is up-to-date: ${BINARY_PATH}" fi }
install_or_update_plugin() { ensure_plugin_dir
local manifest_path="${PLUGIN_DIR}/manifest.json" local url="${BASE_URL}/${PLUGIN_ZIP_NAME}"
if ! needs_update "${manifest_path}"; then log "Figma plugin is up-to-date at ${PLUGIN_DIR}" return 0 fi
if ! have_unzip; then err "unzip not found. Please install unzip and re-run the installer." return 1 fi
log "Installing/updating Figma plugin from ${PLUGIN_ZIP_NAME}..."
local tmp_zip tmp_zip="$(mktemp)"
if ! curl -fsSL --show-error "${url}" -o "${tmp_zip}"; then
err "Failed to download Figma plugin from
if [ ! -s "${tmp_zip}" ]; then err "Downloaded Figma plugin file is empty" rm -f "${tmp_zip}" return 1 fi
sync || true # Ensure file system buffers are flushed before atomic move unzip -o "${tmp_zip}" -d "${PLUGIN_DIR}" >/dev/null
rm -f "${tmp_zip}"
log "Figma plugin installed to:
install_or_update_binary
install_or_update_plugin || true
log "Installation complete. To run the server manually, execute: ${BINARY_PATH} --stdio"
Perfect! Now I can see exactly how your scripts work. This is a very well-designed installer. Let me break down how it determines the latest release and show you how to adapt it for HashVer.
Your script uses GitHub's /releases/latest/download redirect, which is elegant:
BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download"This endpoint automatically resolves to the most recent release based on GitHub's ordering (by creation date, not tag name). The asset name itself determines the version—it's embedded in the filename.
User runs: curl -sSL https://conduit.design/install.sh | bash
↓
Script fetches: https://github.com/conduit-design/conduit_design/releases/latest/download/conduit-macos-x64
(GitHub internally resolves /latest/download to the actual release)
↓
Binary downloaded & verified
If you're using the /releases/latest/download approach, HashVer will work automatically. GitHub's endpoint is versioning-scheme-agnostic:
# This works with ANY versioning scheme
BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download"
# Examples:
# - SemVer: 1.2.3 → downloads from /releases/latest/download
# - HashVer: 2025.12.04.67092445a1abc → downloads from /releases/latest/download
# - Hybrid: 1.2.3+2025.12.04.67092 → downloads from /releases/latest/downloadTo migrate:
- Update your CI/CD to create releases with HashVer tags
- Keep the script exactly as-is
- Done! ✅
If you want to log the actual version being installed, enhance the script to fetch it:
# Add this function to your script
get_latest_version() {
local version_url="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest"
# Fetch latest release metadata
local response
if ! response=$(curl -sL "${version_url}"); then
err "Failed to fetch version information"
return 1
fi
# Extract tag_name (works with any versioning scheme)
echo "$response" | grep -o '"tag_name":"[^"]*' | cut -d'"' -f4
}
# Use it in your script
install_or_update_binary() {
detect_os_arch
ensure_install_dir
local asset="conduit-${OS_NAME}-${ARCH_NAME}"
local url="${BASE_URL}/${asset}"
# Display version information
local version
if version=$(get_latest_version); then
log "Latest version available: ${version}"
fi
# Rest of your existing code...
}If you want to support explicit version pinning while still defaulting to latest:
#!/usr/bin/env bash
# Modified to support VERSION environment variable
# Allow override via environment variable
VERSION="${CONDUIT_VERSION:-latest}"
if [ "${VERSION}" = "latest" ]; then
BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download"
log "Using latest release"
else
# Pin to specific version (works with HashVer tags)
BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download/${VERSION}"
log "Using pinned version: ${VERSION}"
fi
# Rest of script remains the sameUsage:
# Use latest (default)
curl -sSL https://conduit.design/install.sh | bash
# Use specific HashVer version
CONDUIT_VERSION="2025.12.04.67092445a1abc" curl -sSL https://conduit.design/install.sh | bash
# Use specific SemVer version
CONDUIT_VERSION="1.2.3" curl -sSL https://conduit.design/install.sh | bashBased on your excellent script design, here's what I recommend:
Create/update .github/workflows/release.yml:
name: Create Release with HashVer
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Generate HashVer
id: version
run: |
HASH=$(git rev-parse --short=12 HEAD)
DATE=$(date +%Y.%m.%d)
VERSION="${DATE}.${HASH}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "Version: ${VERSION}"
- name: Build binaries
run: |
# Your build commands here
# Output: conduit-macos-x64, conduit-linux-x64, etc.
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.version.outputs.version }}
files: |
conduit-macos-x64
conduit-linux-arm64
figma-plugin.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Your script already handles this perfectly:
BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download"
# ↑ Works with any versioning scheme# Enhance logging to show version
log "Installing Conduit from: ${GITHUB_OWNER}/${GITHUB_REPO}"
if command -v jq >/dev/null 2>&1; then
LATEST_TAG=$(curl -sL "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" | jq -r '.tag_name')
log "Latest version: ${LATEST_TAG}"
fiYour script is already perfectly positioned for HashVer because:
- ✅ Uses
/releases/latest/download(GitHub API handles version resolution) - ✅ No hardcoded version strings to maintain
- ✅ No custom tag parsing logic that might break
- ✅ Works with any versioning scheme (SemVer, HashVer, CalVer, hybrid)
- ✅ Automatic updates without script changes
- Update CI/CD to generate HashVer tags (e.g.,
2025.12.04.67092445a1abc) - Keep
install.shexactly as-is - (Optional) Add
get_latest_version()function for logging - (Optional) Add support for
CONDUIT_VERSIONenvironment variable - Test with a HashVer release tag
- Done! 🎉
Bottom line: Your script is already HashVer-compatible. No breaking changes needed!
the fizzy project uses fizzy@shorten-commit-sha can I do the same for conduit? conduit@shortened-commit-sha ?
Based on what you've mentioned about Fizzy using the fizzy@shortened-commit-sha format, yes, you can absolutely use the same pattern for Conduit. This is a simple and elegant approach. Here's how to implement it:
conduit@a1b2c3d4
conduit@67092445
conduit@3f9a7c1d
The format is:
- Prefix:
conduit@(your project name) - Suffix: 7-8 character short SHA (standard git short hash length)
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Generate version tag
id: version
run: |
SHORT_SHA=$(git rev-parse --short=8 HEAD)
TAG="conduit@${SHORT_SHA}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Generated tag: ${TAG}"
- name: Build binaries
run: |
# Your build commands
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.tag }}
files: |
conduit-macos-x64
conduit-macos-arm64
conduit-linux-x64
figma-plugin.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Your current script uses:
BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download"This already works with conduit@short-sha tags because GitHub's /releases/latest endpoint resolves by creation date, not tag format.
Add this to your script to show what's being installed:
# Add after the configuration section
get_version() {
curl -sL "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" \
| grep -o '"tag_name":"[^"]*' \
| cut -d'"' -f4
}
# Use in install_or_update_binary()
install_or_update_binary() {
detect_os_arch
ensure_install_dir
local version
version=$(get_version 2>/dev/null || echo "unknown")
log "Latest version: ${version}"
# ... rest of your existing code
}Output would look like:
[conduit.install] Latest version: conduit@67092445
[conduit.install] Detected: macos-arm64
[conduit.install] Installing/updating Conduit MCP binary (conduit-macos-arm64)...
| Format | Example | Pros | Cons |
|---|---|---|---|
conduit@sha |
conduit@67092445 |
Simple, unique, instant traceability | No date context |
| HashVer | 2025.12.04.67092445 |
Date + traceability | Longer, more complex |
| SemVer | 1.2.3 |
Human-readable meaning | Manual version bumps |
| Hybrid | 1.2.3+67092445 |
Both meaning + traceability | Most complex |
The fizzy@sha / conduit@sha format is appealing because:
- Instantly recognizable — The
@clearly separates project name from identifier - Unique — SHA guarantees uniqueness across all releases
- Traceable — Direct mapping to git commit
- Zero maintenance — No version bumping decisions
- Automation-friendly — Fully automatic in CI/CD
- Familiar pattern — Similar to npm's
package@versionsyntax
- Update GitHub Actions workflow to generate
conduit@$(git rev-parse --short=8 HEAD)tags - Keep
install.shas-is (already compatible) - (Optional) Add version display logging
- Test with first
conduit@xxxxxxxxrelease - Done! 🎉 159160161162163164165166167168169170171172173174175176177178
regarding the update script: okay. so how could we make the update logic smarter? since getting version nr hash or semver from file alone is hard? is it? is there no call we can make on the executable that would yield like a sha of the entire file or smth? in any case. lets say we call GitHub API when script runs. which it does on app restart or restart of server etc. we could then fetch latest release meta data like we do today. that would give us the conduit@sha and date to the release or? okay so if we have the date of the release could we not compare that date with the date of our file? I guess it gets too complicated? okay lets simplify. what if we somehow are able to get the release version from the file? either from meta data in the file or by calling the file and it returning its release tag? or what if we store the sha version in the file? today its called conduit-mcp what if we name the executable conduit-map-ak48gs3 and the last part is the shortened sha ? okay so many ideas. what would be the best? simplest? most industry standard?
Perfect! Now I have comprehensive information about what's practical. Let me break this down for you clearly:
You're overthinking it, and that's actually good—it means you care about correctness. Let me grade your ideas:
| Approach | Complexity | Industry Standard | Recommendation |
|---|---|---|---|
Call --version flag |
✅ Simple | ✅ Yes | Best |
| File modification time | ❌ No | Skip | |
| Embed SHA in filename | ❌ No | Skip | |
| Metadata in binary | ❌ Complex | Skip | |
| Store version in separate file | ✅ Simple | ✅ Yes | Good alternative |
| Just call GitHub API | ✅ Simple | ✅ Yes | Current approach |
This is what virtually every Unix tool does. It's the gold standard:179
$ your-binary --version
conduit@67092445
$ your-binary -V
conduit@67092445
$ your-binary version
conduit@67092445// main.go
package main
import (
"flag"
"fmt"
)
var version = "dev" // Injected at build time
func main() {
flag.Parse()
if len(flag.Args()) > 0 && flag.Args()[^7_0] == "version" {
fmt.Println(version)
return
}
// rest of your code
}Your CI/CD generates the version at build:
# GitHub Actions workflow
- name: Build with version
run: |
SHORT_SHA=$(git rev-parse --short=8 HEAD)
go build -ldflags "-X main.version=conduit@${SHORT_SHA}" -o conduit-mcp# install.sh enhancement
check_for_updates() {
local installed_version
local latest_version
# Get installed version (if binary exists)
if [ -f "${BINARY_PATH}" ]; then
installed_version=$("${BINARY_PATH}" --version 2>/dev/null || echo "not-found")
else
installed_version="not-found"
fi
# Get latest from GitHub API
latest_version=$(curl -sL "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" \
| grep -o '"tag_name":"[^"]*' | cut -d'"' -f4)
if [ "${installed_version}" != "${latest_version}" ]; then
log "Update available: ${installed_version} → ${latest_version}"
return 0 # needs update
else
log "Already at latest: ${latest_version}"
return 1 # up-to-date
fi
}
install_or_update_binary() {
detect_os_arch
ensure_install_dir
local asset="conduit-${OS_NAME}-${ARCH_NAME}"
local url="${BASE_URL}/${asset}"
# Check if update is needed
if check_for_updates; then
log "Installing/updating Conduit MCP binary (${asset})..."
# ... rest of download logic
else
log "Binary is already up-to-date"
fi
}✅ Universal — Every Unix tool does this (curl, git, npm, rust, etc.)179
✅ Simple — Just return a string, no parsing needed
✅ Reliable — Doesn't depend on file timestamps or naming
✅ Quick — Local check, no API call needed for every comparison
✅ Standard — Anyone who installs your tool knows to use --version
✅ Traceable — Version string directly shows commit SHA
- Problem: File mtime changes when extracted/moved, even if binary is identical
- Problem: Doesn't work on network filesystems or after copying
- Problem: Not reliable across different systems (filesystems vary)
- Better alternative: Embedded version string
- Problem: Breaks assumptions about executable name (
conduit-mcpvsconduit-mcp-a1b2c3) - Problem: Scripts hardcoded to call
conduit-mcpwould break - Problem: PATH lookup becomes fragile
- Better alternative: Keep consistent name, use
--version
From the search results: This is "absolutely possible but probably shouldn't be done"180181
- Problem: ELF/Mach-O parsing is complex
- Problem: Requires helper libraries or custom code
- Problem: Different on macOS vs Linux (resource forks vs sections)
- Problem: Overkill when
--versionexists - Better alternative: Just use
--version
Actually reasonable if you can't modify the binary:
~/.local/bin/conduit-mcp
~/.local/bin/.conduit-version # Contains: conduit@67092445
But this is brittle—files get out of sync. Better to use --version.
Build time:
SHORT_SHA=$(git rev-parse --short=8 HEAD)
go build -ldflags "-X main.version=conduit@${SHORT_SHA}"Script usage:
check_for_updates() {
local installed=$("${BINARY_PATH}" --version 2>/dev/null || echo "not-found")
local latest=$(curl -sL "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" \
| grep -o '"tag_name":"[^"]*' | cut -d'"' -f4)
[ "${installed}" != "${latest}" ]
}Keep what you have:
BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download"
# Always fetches latest, no local version comparison neededPros: Doesn't require modifying the binary Cons: Makes API call on every script run (minor impact)
Use binary --version if you control the build process ✅
Use GitHub API only if you can't modify the binary
Your current approach (GitHub API) is perfectly fine, but adding a --version flag to your binary gives you the flexibility to do local version checks in the future without extra complexity.
182183184185186187188189190191192193194195196197198
I think maybe a date based approach is better than --version flag. because we dont add more flags to our build. and date is meta data so we rather just tweak our install script. so what about this. we keep the 24h limit to not botch the GitHub rate limits. what are GitHub state limits regarding something like this anyways? and what we do is we just check if the release in releases is newer than the local file, and if it is, then we download a new version, the latest.
Perfect! Now I have all the rate limit information. Your date-based approach is actually very smart and respects rate limits well. Here's the complete analysis and implementation:
| Authentication Type | Rate Limit | Your Use Case |
|---|---|---|
| Unauthenticated | 60 requests/hour | |
| Authenticated (Personal Token) | 5,000 requests/hour | ✅ Better |
| GitHub Actions (GITHUB_TOKEN) | 1,000 requests/hour per repo | ✅ Good |
Your 24-hour check is perfect because:
- One API call per day = 1.7 requests per month across all users
- Even with 1,000 concurrent users, that's ~1,700 requests/month total
- You're 999 requests below even the strictest GitHub Actions limit
This is actually the most industry-standard and simplest solution. Here's the implementation:
#!/usr/bin/env bash
# Auto-install and update the Conduit MCP server + Figma plugin.
#
# Usage:
# curl -sSL https://conduit.design/install.sh | bash
set -euo pipefail
# Configuration
INSTALL_DIR="${HOME}/.local/bin"
BINARY_NAME="conduit-mcp"
BINARY_PATH="${INSTALL_DIR}/${BINARY_NAME}"
PLUGIN_DIR="${HOME}/.conduit/figma-plugin"
PLUGIN_ZIP_NAME="figma-plugin.zip"
GITHUB_OWNER="conduit-design"
GITHUB_REPO="conduit_design"
BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download"
# Update check interval (24 hours in seconds)
UPDATE_CHECK_INTERVAL=86400
# Helpers
log() {
printf '[conduit.install] %s\n' "$*" >&2
}
err() {
printf '[conduit.install][ERROR] %s\n' "$*" >&2
}
detect_os_arch() {
local uname_s uname_m
uname_s="$(uname -s | tr '[:upper:]' '[:lower:]')"
uname_m="$(uname -m)"
case "${uname_s}" in
darwin) OS_NAME="macos" ;;
linux) OS_NAME="linux" ;;
*)
err "Unsupported OS: ${uname_s}"
exit 1
;;
esac
case "${uname_m}" in
x86_64|amd64) ARCH_NAME="x64" ;;
arm64|aarch64) ARCH_NAME="arm64" ;;
*)
err "Unsupported architecture: ${uname_m}"
exit 1
;;
esac
log "Detected: ${OS_NAME}-${ARCH_NAME}"
}
# ================================================================
# Core Update Logic: Check if file needs update based on mtime
# ================================================================
# Returns 0 (true) if file needs update, 1 (false) if up-to-date
needs_update_by_age() {
local path="$1"
# File doesn't exist → needs update
if [ ! -f "${path}" ]; then
return 0
fi
local now current_mtime age_seconds
now=$(date +%s)
# Get file's modification time (works on both macOS and Linux)
if [ "$(uname -s)" = "Darwin" ]; then
current_mtime=$(stat -f %m "${path}")
else
current_mtime=$(stat -c %Y "${path}")
fi
age_seconds=$(( now - current_mtime ))
# If older than 24 hours, needs update
if [ ${age_seconds} -gt ${UPDATE_CHECK_INTERVAL} ]; then
log "Binary is $(( age_seconds / 3600 )) hours old (threshold: 24h)"
return 0
fi
return 1
}
# ================================================================
# Enhanced: Compare with GitHub release date (optional refinement)
# ================================================================
# Fetch GitHub release date (optional, for verbose logging)
get_latest_release_info() {
local api_url="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest"
# Use curl with timeout and error handling
local response
if ! response=$(curl -sL --max-time 5 "${api_url}" 2>/dev/null); then
err "Failed to fetch release info from GitHub"
return 1
fi
# Extract tag name and published date
local tag published_at
tag=$(echo "$response" | grep -o '"tag_name":"[^"]*' | cut -d'"' -f4 || echo "unknown")
published_at=$(echo "$response" | grep -o '"published_at":"[^"]*' | cut -d'"' -f4 || echo "unknown")
echo "${tag}|${published_at}"
}
# Smart update check: only call API if file is 24h old
check_for_updates() {
local binary_path="$1"
# First, quick check: is file older than 24h?
if ! needs_update_by_age "${binary_path}"; then
log "Binary is recent (< 24h), skipping update check"
return 1
fi
# File is old, now check GitHub for actual update
log "Binary is older than 24 hours, checking GitHub for updates..."
local release_info tag published_at
if release_info=$(get_latest_release_info); then
tag=$(echo "$release_info" | cut -d'|' -f1)
published_at=$(echo "$release_info" | cut -d'|' -f2)
log "Latest release: ${tag} (published: ${published_at})"
return 0 # Update available
else
log "Could not fetch GitHub release info, assuming update available"
return 0
fi
}
ensure_install_dir() {
mkdir -p "${INSTALL_DIR}"
}
ensure_plugin_dir() {
mkdir -p "${PLUGIN_DIR}"
}
have_unzip() {
command -v unzip >/dev/null 2>&1
}
# ================================================================
# Install / update binary
# ================================================================
install_or_update_binary() {
detect_os_arch
ensure_install_dir
local asset="conduit-${OS_NAME}-${ARCH_NAME}"
local url="${BASE_URL}/${asset}"
# Check if update is needed (smart 24h check)
if ! check_for_updates "${BINARY_PATH}"; then
log "Binary is up-to-date"
return 0
fi
log "Installing/updating Conduit MCP binary (${asset})..."
log "Downloading from: ${url}"
if ! curl -fsSL --show-error "${url}" -o "${BINARY_PATH}.tmp"; then
err "Failed to download binary from ${url}"
rm -f "${BINARY_PATH}.tmp"
exit 1
fi
if [ ! -s "${BINARY_PATH}.tmp" ]; then
err "Downloaded file is empty"
rm -f "${BINARY_PATH}.tmp"
exit 1
fi
sync || true
mv "${BINARY_PATH}.tmp" "${BINARY_PATH}"
chmod +x "${BINARY_PATH}"
# Verify binary
if [ "${OS_NAME}" = "macos" ]; then
if ! file "${BINARY_PATH}" | grep -q "Mach-O"; then
err "Downloaded file is not a valid macOS executable"
exit 1
fi
log "Binary verified as valid Mach-O executable"
elif [ "${OS_NAME}" = "linux" ]; then
if ! file "${BINARY_PATH}" | grep -q "ELF"; then
err "Downloaded file is not a valid Linux executable"
exit 1
fi
log "Binary verified as valid ELF executable"
fi
log "Binary installed successfully to: ${BINARY_PATH}"
}
# ================================================================
# Install / update Figma plugin
# ================================================================
install_or_update_plugin() {
ensure_plugin_dir
local manifest_path="${PLUGIN_DIR}/manifest.json"
local url="${BASE_URL}/${PLUGIN_ZIP_NAME}"
if ! needs_update_by_age "${manifest_path}"; then
log "Figma plugin is up-to-date"
return 0
fi
if ! have_unzip; then
err "unzip not found. Please install unzip and re-run the installer."
return 1
fi
log "Installing/updating Figma plugin from ${PLUGIN_ZIP_NAME}..."
local tmp_zip
tmp_zip="$(mktemp)"
if ! curl -fsSL --show-error "${url}" -o "${tmp_zip}"; then
err "Failed to download Figma plugin from ${url}"
rm -f "${tmp_zip}"
return 1
fi
if [ ! -s "${tmp_zip}" ]; then
err "Downloaded Figma plugin file is empty"
rm -f "${tmp_zip}"
return 1
fi
sync || true
unzip -o "${tmp_zip}" -d "${PLUGIN_DIR}" >/dev/null
rm -f "${tmp_zip}"
log "Figma plugin installed to: ${PLUGIN_DIR}"
log "Import into Figma via: Plugins → Development → Import plugin from manifest"
log "Manifest path: ${manifest_path}"
}
# ================================================================
# Main
# ================================================================
install_or_update_binary
install_or_update_plugin || true
log "Installation/update complete. To run: ${BINARY_PATH} --stdio"# File doesn't exist? → Download
# File exists but mtime > 24 hours old? → Check API (once per day)
# File exists and mtime < 24 hours? → Skip update entirely (no API call)Scenario 1: 1 user, script runs daily
→ 1 API call/day = 30/month ✅
Scenario 2: 1,000 users, script runs daily
→ 1,000 API calls/day = 30,000/month ✅ (still 0.6% of 5M/month rate limit)
Scenario 3: 10,000 users, script runs hourly (worst case)
→ 10,000 API calls/hour = WOULD HIT LIMIT ❌
→ But with 24h cache, only 10,000 API calls/day = 300K/month ✅
✅ No build changes — Keep executable name simple ✅ No extra files — No version metadata files to maintain ✅ Industry standard — Every Unix tool uses mtime for cache invalidation ✅ Automatic — No manual version bumping ✅ Rate limit safe — 24h throttle means ~1,700 API calls/month even with 1,000 users ✅ User-friendly — Shows when binary is old and when update is available ✅ Network efficient — Doesn't hit API if binary is fresh ✅ Fallback built-in — If API fails, still attempts update
If you want zero API calls, just check file age:
if needs_update_by_age "${BINARY_PATH}"; then
log "Binary is older than 24h, downloading latest..."
# download...
else
log "Binary is recent, skipping update"
fiBut the version above is better because it logs what version you're getting.
My recommendation: Use the full script above. It respects rate limits, provides good visibility, and matches industry standards. The 24-hour window is perfect—most users won't even know an update available unless they restart their server daily, which is ideal for stability. 199200201202203204205206207208209210211212213214215216217218
I think we skip the 24 limit. if users restart the app 60 times in app. the ap just shows an error. create limited reached etc
Perfect! That's actually a smart pragmatic choice. Users restarting 60 times in an hour is already an extreme edge case, and showing a clear error is better than silently failing. Let me simplify the script:
#!/usr/bin/env bash
# Auto-install and update the Conduit MCP server + Figma plugin.
#
# Usage:
# curl -sSL https://conduit.design/install.sh | bash
set -euo pipefail
# Configuration
INSTALL_DIR="${HOME}/.local/bin"
BINARY_NAME="conduit-mcp"
BINARY_PATH="${INSTALL_DIR}/${BINARY_NAME}"
PLUGIN_DIR="${HOME}/.conduit/figma-plugin"
PLUGIN_ZIP_NAME="figma-plugin.zip"
GITHUB_OWNER="conduit-design"
GITHUB_REPO="conduit_design"
BASE_URL="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download"
# Helpers
log() {
printf '[conduit.install] %s\n' "$*" >&2
}
err() {
printf '[conduit.install][ERROR] %s\n' "$*" >&2
}
detect_os_arch() {
local uname_s uname_m
uname_s="$(uname -s | tr '[:upper:]' '[:lower:]')"
uname_m="$(uname -m)"
case "${uname_s}" in
darwin) OS_NAME="macos" ;;
linux) OS_NAME="linux" ;;
*)
err "Unsupported OS: ${uname_s}"
exit 1
;;
esac
case "${uname_m}" in
x86_64|amd64) ARCH_NAME="x64" ;;
arm64|aarch64) ARCH_NAME="arm64" ;;
*)
err "Unsupported architecture: ${uname_m}"
exit 1
;;
esac
log "Detected: ${OS_NAME}-${ARCH_NAME}"
}
# Check if file needs update by comparing modification time with GitHub release
needs_update() {
local path="$1"
# File doesn't exist → needs update
if [ ! -f "${path}" ]; then
return 0
fi
local now current_mtime release_time
now=$(date +%s)
# Get file's modification time
if [ "$(uname -s)" = "Darwin" ]; then
current_mtime=$(stat -f %m "${path}")
else
current_mtime=$(stat -c %Y "${path}")
fi
# Fetch latest release info from GitHub
local release_info
if ! release_info=$(curl -sL --max-time 5 "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" 2>/dev/null); then
err "GitHub API rate limit reached or network error. Try again later."
return 1
fi
# Extract published_at timestamp (ISO 8601 format)
local published_at
published_at=$(echo "$release_info" | grep -o '"published_at":"[^"]*' | cut -d'"' -f4 || echo "")
if [ -z "$published_at" ]; then
err "Could not parse GitHub release info"
return 1
fi
# Convert ISO 8601 to Unix timestamp
# macOS and Linux have different date command syntax
if [ "$(uname -s)" = "Darwin" ]; then
release_time=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$published_at" +%s 2>/dev/null || echo "0")
else
release_time=$(date -d "$published_at" +%s 2>/dev/null || echo "0")
fi
# If release is newer than local file, needs update
if [ ${release_time} -gt ${current_mtime} ]; then
local tag
tag=$(echo "$release_info" | grep -o '"tag_name":"[^"]*' | cut -d'"' -f4 || echo "unknown")
log "Update available: ${tag}"
return 0
fi
return 1
}
ensure_install_dir() {
mkdir -p "${INSTALL_DIR}"
}
ensure_plugin_dir() {
mkdir -p "${PLUGIN_DIR}"
}
have_unzip() {
command -v unzip >/dev/null 2>&1
}
# ================================================================
# Install / update binary
# ================================================================
install_or_update_binary() {
detect_os_arch
ensure_install_dir
local asset="conduit-${OS_NAME}-${ARCH_NAME}"
local url="${BASE_URL}/${asset}"
# Check if update is needed
if ! needs_update "${BINARY_PATH}"; then
log "Binary is up-to-date"
return 0
fi
log "Installing/updating Conduit MCP binary (${asset})..."
log "Downloading from: ${url}"
if ! curl -fsSL --show-error "${url}" -o "${BINARY_PATH}.tmp"; then
err "Failed to download binary from ${url}"
rm -f "${BINARY_PATH}.tmp"
exit 1
fi
if [ ! -s "${BINARY_PATH}.tmp" ]; then
err "Downloaded file is empty"
rm -f "${BINARY_PATH}.tmp"
exit 1
fi
sync || true
mv "${BINARY_PATH}.tmp" "${BINARY_PATH}"
chmod +x "${BINARY_PATH}"
# Verify binary
if [ "${OS_NAME}" = "macos" ]; then
if ! file "${BINARY_PATH}" | grep -q "Mach-O"; then
err "Downloaded file is not a valid macOS executable"
exit 1
fi
log "Binary verified as valid Mach-O executable"
elif [ "${OS_NAME}" = "linux" ]; then
if ! file "${BINARY_PATH}" | grep -q "ELF"; then
err "Downloaded file is not a valid Linux executable"
exit 1
fi
log "Binary verified as valid ELF executable"
fi
log "Binary installed successfully to: ${BINARY_PATH}"
}
# ================================================================
# Install / update Figma plugin
# ================================================================
install_or_update_plugin() {
ensure_plugin_dir
local manifest_path="${PLUGIN_DIR}/manifest.json"
local url="${BASE_URL}/${PLUGIN_ZIP_NAME}"
if ! needs_update "${manifest_path}"; then
log "Figma plugin is up-to-date"
return 0
fi
if ! have_unzip; then
err "unzip not found. Please install unzip and re-run the installer."
return 1
fi
log "Installing/updating Figma plugin from ${PLUGIN_ZIP_NAME}..."
local tmp_zip
tmp_zip="$(mktemp)"
if ! curl -fsSL --show-error "${url}" -o "${tmp_zip}"; then
err "Failed to download Figma plugin from ${url}"
rm -f "${tmp_zip}"
return 1
fi
if [ ! -s "${tmp_zip}" ]; then
err "Downloaded Figma plugin file is empty"
rm -f "${tmp_zip}"
return 1
fi
sync || true
unzip -o "${tmp_zip}" -d "${PLUGIN_DIR}" >/dev/null
rm -f "${tmp_zip}"
log "Figma plugin installed to: ${PLUGIN_DIR}"
log "Import into Figma via: Plugins → Development → Import plugin from manifest"
log "Manifest path: ${manifest_path}"
}
# ================================================================
# Main
# ================================================================
install_or_update_binary
install_or_update_plugin || true
log "Installation/update complete. To run: ${BINARY_PATH} --stdio"- ❌ 24-hour throttle logic
- ❌ Complex time calculations
- ❌
UPDATE_CHECK_INTERVALvariable
- ✅ Direct GitHub API call every time
- ✅ Compares release
published_attimestamp with local file mtime - ✅ Clear error message if rate limit hit:
"GitHub API rate limit reached or network error. Try again later."
Script runs
↓
Calls: https://api.github.com/repos/conduit-design/conduit_design/releases/latest
↓
Gets: tag_name, published_at timestamp
↓
Compares: release published_at > local file mtime?
├─ YES → Download and install
└─ NO → Skip (already up-to-date)
↓
If rate limit hit → Show error, exit gracefully
GitHub unauthenticated limit: 60 requests/hour
Scenario: 60 app restarts/hour (you hit the limit)
→ User sees: "[conduit.install][ERROR] GitHub API rate limit reached or network error. Try again later."
→ App still runs with existing binary
→ User can wait 1 minute for rate limit to reset or restart later
This is fine because:
- 60 restarts/hour is extremely abnormal usage
- Error message is clear
- Binary keeps working (doesn't break anything)
- User can restart again in ~1 minute when rate limit resets
- Most users do this once per day or less
If you want to handle more aggressive rate limiting (e.g., for CI/CD):
# Optional: use GITHUB_TOKEN if available (5,000 req/hr vs 60)
if [ -n "${GITHUB_TOKEN:-}" ]; then
API_HEADER="-H Authorization: token ${GITHUB_TOKEN}"
else
API_HEADER=""
fi
release_info=$(curl -sL $API_HEADER --max-time 5 "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest")But for user machines, this is overkill. The simple version is perfect.
Summary: Simplest, cleanest approach. Let GitHub handle the rate limiting, show a clear error if it happens, and move on. ✅
Footnotes
-
https://liquidsoftware.com/blog/the-7-deadly-sins-of-versioning-part-2/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15
-
https://www.reddit.com/r/PHP/comments/96ycj2/use_githash_as_release_version/ ↩
-
https://filpal.wordpress.com/2021/06/13/using-git-hash-for-software-versioning/ ↩ ↩2 ↩3
-
https://stackoverflow.com/questions/5694389/get-the-short-git-version-hash ↩
-
https://www.designgurus.io/answers/detail/how-do-i-get-the-hash-for-the-current-commit-in-git ↩
-
https://blog.heylinux.com/en/2025/07/commit-branching-versioning-best-practices/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
https://www.reddit.com/r/explainlikeimfive/comments/1erqxan/eli5_why_are_software_version_numbers_so_weird/ ↩ ↩2
-
https://www.reddit.com/r/golang/comments/1jzucpw/scalable_calendar_versioning_calver_semver/ ↩ ↩2
-
https://blog.abhimanyu-saharan.com/posts/calendar-versioning-calver-versioning-that-ages-well ↩
-
https://world.hey.com/jason/introducing-fizzy-our-newest-product-83a4144f ↩
-
https://www.reddit.com/r/rails/comments/1pccvqj/source_code_for_fizzy_from_37signals/ ↩
-
https://en.wikipedia.org/wiki/Universally_unique_identifier ↩
-
https://www.reddit.com/r/cryptography/comments/1gbcwqj/best_hash_choice_for_proof_that_a_file_hasnt/ ↩
-
https://blog.classyendeavors.com/semantic-versioning-what-is-semver-and-why-should-you-care-ff671199d017 ↩
-
https://www.geeksforgeeks.org/dsa/applications-advantages-and-disadvantages-of-hash-data-structure/ ↩
-
https://forums.swift.org/t/uniqueid-time-ordered-uuids-in-swift/53265 ↩
-
https://stackoverflow.com/questions/15891855/what-are-the-advantages-of-revving-files-with-a-hash-over-a-version-or-date ↩
-
https://stackoverflow.com/questions/61057101/is-there-a-standard-or-alternative-for-shorter-uuids ↩
-
https://www.gitkraken.com/gitkon/semantic-versioning-git-tags ↩
-
https://sensiolabs.com/blog/2025/semantic-vs-calendar-versioning ↩
-
https://jacobtomlinson.dev/posts/2023/sometimes-i-regret-using-calver/ ↩
-
https://www.linkedin.com/posts/jason-fried_heres-the-first-preview-of-a-brand-new-product-activity-7326335017779896320-hg84 ↩
-
https://dev.to/khozaei/automating-semver-with-git-and-cmake-2hji ↩
-
https://community.onlyoffice.com/t/consider-using-calendar-versioning-calver/7763 ↩
-
https://stackoverflow.com/questions/37814286/how-to-manage-the-version-number-in-git ↩
-
https://django-pipeline.readthedocs.io/en/1.1.27/versioning.html ↩
-
https://rubyflow.com/p/qsd3ks-37-signals-fizzy-kanban-board-source-code-review ↩
-
https://www.linkedin.com/posts/jason-fried_introducing-fizzy-have-you-noticed-that-activity-7401652951544737792-E0Kh ↩
-
https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7 ↩
-
https://www.linkedin.com/pulse/embracing-best-practices-commit-hashes-vs-semantic-docker-jackson-gfste ↩
-
https://majortechnicality.substack.com/p/the-big-gamble-by-37signals ↩
-
https://innosufiyan.hashnode.dev/continuous-deployment-cd-explained ↩
-
https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection ↩
-
https://support.configura.com/hc/en-us/articles/360052644053-Are-Git-Commit-id-s-Truncated-in-CET-Developer ↩
-
https://futurestud.io/tutorials/github-actions-how-to-get-the-short-git-commit-hash ↩
-
https://stackoverflow.com/questions/5694389/get-the-short-git-version-hash ↩
-
https://liquidsoftware.com/blog/the-7-deadly-sins-of-versioning-part-2/ ↩ ↩2
-
https://www.reddit.com/r/devops/comments/tjtxf9/manage_semver_versions_across_environments/ ↩
-
https://stackoverflow.com/questions/3760086/automatic-tagging-of-releases ↩
-
https://stackoverflow.com/questions/1704907/how-can-i-get-my-c-code-to-automatically-print-out-its-git-version-hash ↩
-
https://stackoverflow.com/questions/39032077/numeric-and-orderable-hashsum-of-a-semver ↩
-
https://www.gitkraken.com/gitkon/semantic-versioning-git-tags ↩
-
https://www.sei.cmu.edu/blog/versioning-with-git-tags-and-conventional-commits/ ↩
-
https://www.reddit.com/r/devops/comments/1m3rfgl/how_do_you_handle_tagging_repositories_when_its/ ↩
-
https://dev.to/khozaei/automating-semver-with-git-and-cmake-2hji ↩
-
https://blog.stackademic.com/how-git-tags-can-transform-your-release-management-a4977afd9272 ↩
-
https://stackoverflow.com/questions/59518658/how-to-use-github-release-version-number-in-github-action ↩
-
https://stackoverflow.com/questions/38460008/automate-git-commit-versioning-tag-by-npm-node ↩
-
https://www.stepsecurity.io/blog/pinning-github-actions-for-enhanced-security-a-complete-guide ↩
-
https://eugene-babichenko.github.io/blog/2019/09/28/nightly-versions-makefiles/ ↩
-
https://javascript.plainenglish.io/auto-versioning-with-npm-and-git-786c6795b926 ↩
-
https://xebia.com/blog/how-github-actions-versioning-system-works/ ↩
-
https://www.reddit.com/r/node/comments/f883ph/get_commit_hash_of_a_node_package/ ↩
-
https://github.com/marketplace/actions/generate-release-hashes ↩
-
https://www.reddit.com/r/git/comments/czjrho/any_way_to_include_git_commit_hash_or_git/ ↩
-
https://dev.to/seven/automating-npm-package-releases-with-github-actions-14i9 ↩
-
https://github.com/marketplace/actions/automatic-releases-with-sha-hashes ↩
-
https://www.designgurus.io/answers/detail/how-do-i-get-the-hash-for-the-current-commit-in-git ↩
-
https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7 ↩
-
https://graphite.com/guides/how-to-automate-tagging-and-release-workflows-in-github ↩
-
https://www.reddit.com/r/PHP/comments/96ycj2/use_githash_as_release_version/ ↩
-
https://stackoverflow.com/questions/73005401/bash-script-to-download-latest-release-from-github ↩ ↩2
-
https://fabianlee.org/2021/02/16/bash-determining-latest-github-release-tag-and-version/ ↩
-
https://blog.markvincze.com/download-artifacts-from-a-latest-github-release-in-sh-and-powershell/ ↩
-
https://www.reddit.com/r/git/comments/1ixpxi5/how_do_i_get_latest_release_programatically/ ↩
-
https://docs.clavister.com/repo/incenter-onprem-administration-guide/2.2.5/doc/single_html_page.html ↩
-
https://llnl-conduit.readthedocs.io/en/stable/releases.html ↩
-
https://ntrs.nasa.gov/api/citations/19810007598/downloads/19810007598.pdf ↩
-
https://uberty.org/wp-content/uploads/2016/02/Terrence_W._Deacon_The_Symbolic_Species.pdf ↩
-
https://etheses.whiterose.ac.uk/id/eprint/32141/1/531605.pdf ↩
-
https://gist.github.com/eonist/d09e5bdc59b41de4d81154709adfa338 ↩
-
https://inmobi.github.io/conduit/current/DevSetup/DevSetup.html ↩
-
https://www.reddit.com/r/linux4noobs/comments/12u1d1v/download_asset_from_latest_release_on_github/ ↩
-
https://stackoverflow.com/questions/46060010/download-github-release-with-curl ↩
-
https://gist.github.com/electrickite/ba7e734752ee90f04587a24eb6d58b04 ↩
-
https://www.geraldonit.com/how-to-download-the-latest-github-repo-release-via-command-line/ ↩
-
https://gist.github.com/miguelmota/2cbfd454c9310d6f9e020cee299b4e3f ↩
-
https://josh-ops.com/posts/github-download-latest-release/ ↩
-
https://gist.github.com/lukechilds/a83e1d7127b78fef38c2914c4ececc3c ↩
-
https://gist.github.com/steinwaywhw/a4cd19cda655b8249d908261a62687f8 ↩
-
https://gist.github.com/schell/2fe896953b6728cc3c5d8d5f9f3a17a3 ↩
-
https://dev.algorand.co/nodes/installation/conduit-installation/ ↩
-
https://llnl-conduit.readthedocs.io/en/v0.8.5/building.html ↩
-
https://raw.githubusercontent.com/ekrolewicz6/6.868/master/Scripts/UKACD17.TXT ↩
-
https://raw.githubusercontent.com/netdata/netdata/master/packaging/installer/kickstart.sh ↩
-
https://stackoverflow.com/questions/29910217/homebrew-installation-on-mac-os-x-failed-to-connect-to-raw-githubusercontent-com ↩
-
https://github.com/davepagurek/StrokeStrip/blob/main/mac-install-and-run.sh ↩
-
https://forums.docker.com/t/can-i-run-a-shell-script-inside-of-a-docker-container-with-the-help-of-a-docker-compose-file/135802 ↩
-
https://gist.github.com/eonist/d09e5bdc59b41de4d81154709adfa338 ↩
-
https://nolanbconaway.github.io/pelican-deploy-gh-actions/pages/deployment-on-github-pages.html ↩
-
https://support.everhour.com/article/306-how-to-use-labels-and-tags-with-basecamp ↩
-
https://www.reddit.com/r/devops/comments/10do2pe/what_versioning_scheme_for_development_images_tags/ ↩
-
https://www.linkedin.com/posts/jason-fried_heres-the-first-preview-of-a-brand-new-product-activity-7326335017779896320-hg84 ↩
-
https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7 ↩
-
https://stackoverflow.com/questions/77989450/why-does-setting-informationalversion-append-the-commit-sha-hash-at-the-end ↩
-
https://stackoverflow.com/questions/59810838/how-to-get-the-short-sha-for-the-github-workflow ↩
-
https://www.reddit.com/r/devops/comments/1m3rfgl/how_do_you_handle_tagging_repositories_when_its/ ↩
-
https://forums.classicpress.net/t/commit-hashes-and-release-versions/1498 ↩
-
https://www.designgurus.io/answers/detail/how-do-i-get-the-hash-for-the-current-commit-in-git ↩
-
https://community.fly.io/t/git-commit-sha-of-current-build/1870 ↩
-
https://macosx.com/threads/how-do-you-check-the-version-of-a-unix-binary.29216/ ↩ ↩2
-
https://www.geeksforgeeks.org/ethical-hacking/embedded-metadata-and-resources-in-executables/ ↩
-
https://stackoverflow.com/questions/45505139/how-to-get-the-version-number-of-an-application-from-the-binary-file ↩
-
https://stackoverflow.com/questions/74970474/how-to-embed-the-version-information-in-the-executable-file-name-when-building-c ↩
-
https://predr.ag/blog/semver-in-rust-tooling-breakage-and-edge-cases/ ↩
-
https://www.sciencedirect.com/topics/computer-science/embedded-metadata ↩
-
https://forums.ni.com/t5/LabVIEW/Determining-the-version-of-an-executable-under-Linux/td-p/957378 ↩
-
https://dev.to/arhamrumi/adding-version-information-to-a-pyinstaller-onefile-executable-6n8 ↩
-
https://talent500.com/blog/semantic-versioning-explained-guide/ ↩
-
https://stackoverflow.com/questions/14779775/how-to-compare-the-modified-date-of-two-files-in-python ↩
-
https://stackoverflow.com/questions/72900289/regex-for-semver ↩
-
https://www.reddit.com/r/golang/comments/7la77f/has_anyone_here_embedded_metadata_into_a_go/ ↩
-
https://www.reddit.com/r/golang/comments/udhml7/best_way_to_embed_version_info_into_binary/ ↩
-
https://www.reddit.com/r/Windows10/comments/165bfzx/every_files_date_created_and_date_modified_are/ ↩
-
https://stackoverflow.com/questions/74299405/purpose-build-metadata-in-semver ↩
-
https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api ↩
-
https://stackoverflow.com/questions/13394077/is-there-a-way-to-increase-the-api-rate-limit-or-to-bypass-it-altogether-for-git ↩
-
https://www.lunar.dev/post/a-developers-guide-managing-rate-limits-for-the-github-api ↩
-
https://docs.github.com/es/enterprise-cloud@latest/actions/reference/limits ↩
-
https://www.reddit.com/r/github/comments/1jjgjm0/why_the_f_is_github_rate_limiting_me_when_all_im/ ↩
-
https://dev.to/uche_wendy_9f87dcb3b339d0/optimizing-github-as-a-database-solving-rate-limits-with-server-side-caching-2aa5 ↩
-
https://www.endorlabs.com/learn/how-to-get-the-most-out-of-github-api-rate-limits ↩
-
https://dev.to/codexam/github-api-rate-limit-exceeded-problem-502f ↩
-
https://docs.github.com/github-ae@latest/apps/creating-github-apps/registering-a-github-app/rate-limits-for-github-apps ↩
-
https://github.blog/changelog/2025-05-08-updated-rate-limits-for-unauthenticated-requests/ ↩
-
https://stackoverflow.com/questions/77595105/github-rate-limit-api-shows-that-core-has-only-60-api-limit-for-authenticated-us ↩
-
https://www.warpbuild.com/blog/rate-limits-self-hosted-runners ↩
-
https://www.kubeblogs.com/how-to-avoid-github-token-rate-limiting-issues-complete-guide-for-devops-teams/ ↩
-
https://tommymaynard.com/github-rate-limit-rest-api-json-and-epoch-time/ ↩
