Skip to content

Convert and Update Tool #171

Convert and Update Tool

Convert and Update Tool #171

name: Convert and Update Tool
on:
workflow_dispatch:
inputs:
tool_id:
description: "Tool ID (npm package name)"
required: true
type: string
version:
description: "Version to convert"
required: true
type: string
authors:
description: "Authors of the tool"
required: true
type: string
repository:
description: "Repository URL"
required: false
type: string
website:
description: "Website URL"
required: false
type: string
csp_exceptions:
description: "CSP Exceptions"
required: false
type: string
features:
description: "Features"
required: false
type: string
branch:
description: "Branch to commit to (optional, defaults to current branch)"
required: false
type: string
jobs:
convert:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ inputs.branch || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Get tool metadata from npm package
id: metadata
run: |
TOOL_ID=$(echo "${{ inputs.tool_id }}" | sed 's/@//g' | sed 's/\//-/g')
TOOL_PACKAGE_NAME="${{ inputs.tool_id }}"
# Fetch metadata from npm registry
NPM_INFO=$(npm view "${{ inputs.tool_id }}@${{ inputs.version }}" --json || echo '{}')
TOOL_NAME=$(echo "$NPM_INFO" | jq -r '.displayName // .name // "${{ inputs.tool_id }}"')
TOOL_DESC=$(echo "$NPM_INFO" | jq -r '.description // ""')
# Extract icon and readmeUrl from configurations section
ICON=$(echo "$NPM_INFO" | jq -r '.icon // empty')
README_URL=$(echo "$NPM_INFO" | jq -r '.configurations.readmeUrl // empty')
LICENSE=$(echo "$NPM_INFO" | jq -r '(.license // .licenses // "") | (if type=="string" then . else (.[0].type // "") end) // ""')
REPOSITORY=$(echo "$NPM_INFO" | jq -r '.configurations.repository // empty')
WEBSITE=$(echo "$NPM_INFO" | jq -r '.configurations.website // empty')
CSP_EXCEPTIONS=$(echo "$NPM_INFO" | jq -c '.csp_exceptions // .cspexception // .configurations.csp_exceptions // null')
FEATURES=$(echo "$NPM_INFO" | jq -c '.features // .configurations.features // null')
echo "id=$TOOL_ID" >> $GITHUB_OUTPUT
echo "package_name=$TOOL_PACKAGE_NAME" >> $GITHUB_OUTPUT
echo "name=$TOOL_NAME" >> $GITHUB_OUTPUT
echo "description=$TOOL_DESC" >> $GITHUB_OUTPUT
echo "readme_url=$README_URL" >> $GITHUB_OUTPUT
echo "icon=$ICON" >> $GITHUB_OUTPUT
echo "license=$LICENSE" >> $GITHUB_OUTPUT
echo "repository=$REPOSITORY" >> $GITHUB_OUTPUT
echo "website=$WEBSITE" >> $GITHUB_OUTPUT
echo "csp_exceptions=$CSP_EXCEPTIONS" >> $GITHUB_OUTPUT
echo "features=$FEATURES" >> $GITHUB_OUTPUT
- name: Download and build tool
id: build
run: |
# Download specific version from npm
npm pack ${{ inputs.tool_id }}@${{ inputs.version }}
# Extract
tar -xzf *.tgz
cd package
# Verify version
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
# Install production dependencies (skip lifecycle scripts to avoid husky issues)
npm install --production --no-optional --ignore-scripts
# Run build if script exists
npm run --if-present build || echo "No build script found or build failed"
cd ..
- name: Set tool environment
run: |
echo "TOOL_ID=${{ steps.metadata.outputs.id }}" >> $GITHUB_ENV
echo "TOOL_VERSION=${{ inputs.version }}" >> $GITHUB_ENV
- name: Create distribution archive
run: |
cd package
# Remove unnecessary files
rm -rf .git .github node_modules/*/test node_modules/*/tests
rm -rf node_modules/*/*.md node_modules/*/.npmignore
find . -name "*.map" -delete
find . -name "*.ts" ! -name "*.d.ts" -delete
# Create archive
tar -czf ../${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz \
--exclude=node_modules/.bin \
.
cd ..
- name: Generate checksum
id: checksum
run: |
CHECKSUM=$(sha256sum ${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz | awk '{print $1}')
SIZE=$(stat -f%z ${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz 2>/dev/null || stat -c%s ${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz)
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
echo "size=$SIZE" >> $GITHUB_OUTPUT
- name: Extract or download icon
id: icon
if: ${{ steps.metadata.outputs.icon != '' }}
run: |
ICON="${{ steps.metadata.outputs.icon }}"
if [ -n "$ICON" ]; then
echo "Using bundled icon: $ICON"
# Check if icon exists in package/dist
if [ -f "package/dist/$ICON" ]; then
# Extract extension
EXTENSION="${ICON##*.}"
EXTENSION=$(echo "$EXTENSION" | tr '[:upper:]' '[:lower:]')
# Copy icon to release directory
ICON_FILENAME="${{ steps.metadata.outputs.id }}-${{ inputs.version }}-icon.${EXTENSION}"
cp "package/dist/$ICON" "$ICON_FILENAME"
echo "✅ Bundled icon extracted successfully"
echo "filename=$ICON_FILENAME" >> $GITHUB_OUTPUT
echo "has_icon=true" >> $GITHUB_OUTPUT
echo "icon_path=$ICON" >> $GITHUB_OUTPUT
else
echo "⚠️ Bundled icon not found at package/dist/$ICON"
echo "has_icon=false" >> $GITHUB_OUTPUT
fi
fi
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Upload tool package and icon to Azure Blob
run: |
set -euo pipefail
FOLDER="${TOOL_ID}-${TOOL_VERSION}"
az storage blob upload \
--account-name ${{ secrets.AZURE_STORAGE_ACCOUNT }} \
--container-name ${{ secrets.AZURE_STORAGE_CONTAINER }} \
--name "packages/${FOLDER}/${FOLDER}.tar.gz" \
--file "${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz" \
--auth-mode login \
--overwrite true
if [ "${{ steps.icon.outputs.has_icon }}" = "true" ]; then
az storage blob upload \
--account-name ${{ secrets.AZURE_STORAGE_ACCOUNT }} \
--container-name ${{ secrets.AZURE_STORAGE_CONTAINER }} \
--name "packages/${FOLDER}/${{ steps.icon.outputs.filename }}" \
--file "${{ steps.icon.outputs.filename }}" \
--auth-mode login \
--overwrite true
fi
- name: Prepare release files
id: release_files
run: |
# Build files list
FILES="${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz"
if [ "${{ steps.icon.outputs.has_icon }}" = "true" ]; then
FILES="${FILES}"$'\n'"${{ steps.icon.outputs.filename }}"
fi
# Output as multiline string
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Build tool metadata JSON
env:
INPUT_FEATURES: ${{ inputs.features }}
INPUT_CSP_EXCEPTIONS: ${{ inputs.csp_exceptions }}
METADATA_FEATURES: ${{ steps.metadata.outputs.features }}
METADATA_CSP: ${{ steps.metadata.outputs.csp_exceptions }}
run: |
set -euo pipefail
# Normalize JSON-like input safely. Accepts full JSON and object fragments like "key":"value".
normalize_json_input() {
local raw="$1"
local field_name="$2"
local trimmed
local candidate
trimmed=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$trimmed" ] || [ "$trimmed" = "null" ]; then
echo "null"
return 0
fi
if printf '%s' "$trimmed" | jq -e . >/dev/null 2>&1; then
printf '%s' "$trimmed" | jq -c .
return 0
fi
# Some callers send object members without surrounding braces.
if [[ "$trimmed" == *:* ]] && [[ "$trimmed" != \{* ]] && [[ "$trimmed" != \[* ]]; then
candidate="{$trimmed}"
if printf '%s' "$candidate" | jq -e . >/dev/null 2>&1; then
printf '%s' "$candidate" | jq -c .
return 0
fi
fi
echo "Invalid JSON for ${field_name} workflow input: $trimmed" >&2
return 1
}
# Prefer caller-provided values; otherwise fall back to npm package metadata.
# Use env vars for JSON values to avoid bash brace-expansion issues with literal JSON defaults.
FEATURES_SOURCE="${INPUT_FEATURES:-}"
if [ -z "$FEATURES_SOURCE" ] || [ "$FEATURES_SOURCE" = "null" ]; then
FEATURES_SOURCE="$METADATA_FEATURES"
fi
CSP_SOURCE="${INPUT_CSP_EXCEPTIONS:-}"
if [ -z "$CSP_SOURCE" ] || [ "$CSP_SOURCE" = "null" ]; then
CSP_SOURCE="$METADATA_CSP"
fi
FEATURES_JSON=$(normalize_json_input "${FEATURES_SOURCE}" "features")
CSP_JSON=$(normalize_json_input "${CSP_SOURCE}" "csp_exceptions")
FOLDER="${TOOL_ID}-${TOOL_VERSION}"
DOWNLOAD_URL="https://${{ secrets.AZURE_STORAGE_ACCOUNT }}.blob.core.windows.net/${{ secrets.AZURE_STORAGE_CONTAINER }}/packages/${FOLDER}/${FOLDER}.tar.gz"
ICON_URL=""
if [ "${{ steps.icon.outputs.has_icon }}" = "true" ]; then
ICON_URL="https://${{ secrets.AZURE_STORAGE_ACCOUNT }}.blob.core.windows.net/${{ secrets.AZURE_STORAGE_CONTAINER }}/packages/${FOLDER}/${{ steps.icon.outputs.filename }}"
fi
TOOL_METADATA_JSON=$(jq -c -n \
--arg tool_id "${TOOL_ID}" \
--arg tool_version "${TOOL_VERSION}" \
--arg packageName "${{ steps.metadata.outputs.package_name }}" \
--arg name "${{ steps.metadata.outputs.name }}" \
--arg description "${{ steps.metadata.outputs.description }}" \
--arg authors "${{ inputs.authors }}" \
--arg readme_url "${{ steps.metadata.outputs.readme_url }}" \
--arg icon "$ICON_URL" \
--arg downloadurl "$DOWNLOAD_URL" \
--arg checksum "${{ steps.checksum.outputs.checksum }}" \
--arg size "${{ steps.checksum.outputs.size }}" \
--arg repository "${{ inputs.repository }}" \
--arg website "${{ inputs.website }}" \
--argjson csp_exceptions "$CSP_JSON" \
--argjson features "$FEATURES_JSON" \
--arg published "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{
tool_id: $tool_id,
tool_version: $tool_version,
packagename: $packageName,
name: $name,
description: $description,
authors: $authors,
readmeurl: $readme_url,
icon: (if $icon == "" then null else $icon end),
downloadurl: $downloadurl,
checksum: $checksum,
size: ($size | tonumber),
repository: (if $repository == "" then null else $repository end),
website: (if $website == "" then null else $website end),
csp_exceptions: $csp_exceptions,
features: $features,
published_at: $published
}')
echo "TOOL_METADATA_JSON=$TOOL_METADATA_JSON" >> $GITHUB_ENV
- name: Regenerate Azure Blob registry.json
run: |
set -euo pipefail
az storage blob download \
--account-name ${{ secrets.AZURE_STORAGE_ACCOUNT }} \
--container-name ${{ secrets.AZURE_STORAGE_CONTAINER }} \
--name registry.json \
--file registry.json \
--auth-mode login \
|| echo '{"version":"1.0","tools":[]}' > registry.json
node buildScripts/updateRegistry.js "${TOOL_ID}" "${TOOL_VERSION}" "$TOOL_METADATA_JSON"
az storage blob upload \
--account-name ${{ secrets.AZURE_STORAGE_ACCOUNT }} \
--container-name ${{ secrets.AZURE_STORAGE_CONTAINER }} \
--name registry.json \
--file registry.json \
--auth-mode login \
--overwrite true
- name: Update Supabase database
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
INPUT_FEATURES: ${{ inputs.features }}
INPUT_CSP_EXCEPTIONS: ${{ inputs.csp_exceptions }}
METADATA_FEATURES: ${{ steps.metadata.outputs.features }}
METADATA_CSP: ${{ steps.metadata.outputs.csp_exceptions }}
if: env.SUPABASE_URL != '' && env.SUPABASE_SERVICE_ROLE_KEY != ''
run: |
set -euo pipefail
# Ensure required tools exist (exit 127 indicates command-not-found)
command -v curl >/dev/null 2>&1 || (sudo apt-get update && sudo apt-get install -y curl)
command -v jq >/dev/null 2>&1 || (sudo apt-get update && sudo apt-get install -y jq)
FOLDER="${TOOL_ID}-${TOOL_VERSION}"
# Prepare icon and download URLs
ICON_URL=""
if [ "${{ steps.icon.outputs.has_icon }}" = "true" ]; then
ICON_URL="https://${{ secrets.AZURE_STORAGE_ACCOUNT }}.blob.core.windows.net/${{ secrets.AZURE_STORAGE_CONTAINER }}/packages/${FOLDER}/${{ steps.icon.outputs.filename }}"
fi
# Azure Blob (preferred distribution)
BLOB_DOWNLOAD_URL="https://${{ secrets.AZURE_STORAGE_ACCOUNT }}.blob.core.windows.net/${{ secrets.AZURE_STORAGE_CONTAINER }}/packages/${FOLDER}/${FOLDER}.tar.gz"
# Normalize JSON-like input safely. Accepts full JSON and object fragments like "key":"value".
normalize_json_input() {
local raw="$1"
local field_name="$2"
local trimmed
local candidate
trimmed=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$trimmed" ] || [ "$trimmed" = "null" ]; then
echo "null"
return 0
fi
if printf '%s' "$trimmed" | jq -e . >/dev/null 2>&1; then
printf '%s' "$trimmed" | jq -c .
return 0
fi
# Some callers send object members without surrounding braces.
if [[ "$trimmed" == *:* ]] && [[ "$trimmed" != \{* ]] && [[ "$trimmed" != \[* ]]; then
candidate="{$trimmed}"
if printf '%s' "$candidate" | jq -e . >/dev/null 2>&1; then
printf '%s' "$candidate" | jq -c .
return 0
fi
fi
echo "Invalid JSON for ${field_name} workflow input: $trimmed" >&2
return 1
}
# Prefer caller-provided values; otherwise fall back to npm package metadata.
# Use env vars for JSON values to avoid bash brace-expansion issues with literal JSON defaults.
FEATURES_SOURCE="${INPUT_FEATURES:-}"
if [ -z "$FEATURES_SOURCE" ] || [ "$FEATURES_SOURCE" = "null" ]; then
FEATURES_SOURCE="$METADATA_FEATURES"
fi
CSP_SOURCE="${INPUT_CSP_EXCEPTIONS:-}"
if [ -z "$CSP_SOURCE" ] || [ "$CSP_SOURCE" = "null" ]; then
CSP_SOURCE="$METADATA_CSP"
fi
FEATURES_JSON=$(normalize_json_input "${FEATURES_SOURCE}" "features")
CSP_JSON=$(normalize_json_input "${CSP_SOURCE}" "csp_exceptions")
# Prepare SQL payload for tools table
TOOL_PAYLOAD=$(jq -n \
--arg packagename "${{ steps.metadata.outputs.package_name }}" \
--arg version "${{ inputs.version }}" \
--arg name "${{ steps.metadata.outputs.name }}" \
--arg description "${{ steps.metadata.outputs.description }}" \
--arg readmeurl "${{ steps.metadata.outputs.readme_url }}" \
--arg license "${{ steps.metadata.outputs.license }}" \
--arg download "$BLOB_DOWNLOAD_URL" \
--arg icon "$ICON_URL" \
--arg published "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg checksum "${{ steps.checksum.outputs.checksum }}" \
--arg size "${{ steps.checksum.outputs.size }}" \
--arg repository "${{ inputs.repository }}" \
--arg website "${{ inputs.website }}" \
--argjson csp_exceptions "$CSP_JSON" \
--argjson features "$FEATURES_JSON" \
'{
packagename: $packagename,
version: $version,
name: $name,
description: $description,
readmeurl: $readmeurl,
license: $license,
download: $download,
icon: (if $icon == "" then null else $icon end),
published_at: $published,
checksum: $checksum,
size: ($size|tonumber),
repository: (if $repository == "" then null else $repository end),
website: (if $website == "" then null else $website end),
csp_exceptions: $csp_exceptions,
features: $features
}')
# Update existing tool metadata in Supabase (falls back to upsert if missing)
ENCODED_PACKAGE=$(echo "${{ steps.metadata.outputs.package_name }}" | jq -sRr @uri)
PATCH_RESPONSE=$(curl -s -w "\n%{http_code}" -X PATCH "${{ secrets.SUPABASE_URL }}/rest/v1/tools?packagename=eq.$ENCODED_PACKAGE&select=id" \
-H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d "$TOOL_PAYLOAD")
PATCH_BODY=$(echo "$PATCH_RESPONSE" | head -n1)
PATCH_STATUS=$(echo "$PATCH_RESPONSE" | tail -n1)
echo "Supabase tools patch status: $PATCH_STATUS"
echo "Supabase tools patch body: $PATCH_BODY"
TOOL_ID=$(echo "$PATCH_BODY" | jq -r '.[0].id // empty')
# If PATCH returned nothing, try upsert
if [ -z "$TOOL_ID" ]; then
UPSERT_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${{ secrets.SUPABASE_URL }}/rest/v1/tools?on_conflict=packagename&select=id" \
-H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Content-Type: application/json" \
-H "Prefer: resolution=merge-duplicates,return=representation" \
-d "$TOOL_PAYLOAD")
UPSERT_BODY=$(echo "$UPSERT_RESPONSE" | head -n1)
UPSERT_STATUS=$(echo "$UPSERT_RESPONSE" | tail -n1)
echo "Supabase tools upsert status: $UPSERT_STATUS"
echo "Supabase tools upsert body: $UPSERT_BODY"
TOOL_ID=$(echo "$UPSERT_BODY" | jq -r '.[0].id // empty')
fi
if [ -n "$TOOL_ID" ]; then
echo "Tool metadata updated with ID: $TOOL_ID"
echo "Analytics data preserved from previous version"
AUTHOR_LIST="${{ inputs.authors }}"
NEW_CONTRIBUTOR_IDS=""
if [ -n "$AUTHOR_LIST" ]; then
IFS=',' read -ra AUTHORS <<< "$AUTHOR_LIST"
for RAW_AUTHOR in "${AUTHORS[@]}"; do
AUTHOR_NAME=$(echo "$RAW_AUTHOR" | xargs)
if [ -z "$AUTHOR_NAME" ]; then
continue
fi
ENCODED_NAME=$(jq -rn --arg v "$AUTHOR_NAME" '$v|@uri')
CONTRIBUTOR_LOOKUP=$(curl -s "${{ secrets.SUPABASE_URL }}/rest/v1/contributors?name=eq.${ENCODED_NAME}&select=id&limit=1" \
-H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Content-Type: application/json")
CONTRIBUTOR_ID=$(echo "$CONTRIBUTOR_LOOKUP" | jq -r '.[0].id // empty')
if [ -z "$CONTRIBUTOR_ID" ]; then
CONTRIBUTOR_PAYLOAD=$(jq -n --arg name "$AUTHOR_NAME" '{name: $name}')
CONTRIBUTOR_RESPONSE=$(curl -s -X POST "${{ secrets.SUPABASE_URL }}/rest/v1/contributors?select=id" \
-H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d "$CONTRIBUTOR_PAYLOAD")
CONTRIBUTOR_ID=$(echo "$CONTRIBUTOR_RESPONSE" | jq -r '.[0].id // empty')
fi
if [ -n "$CONTRIBUTOR_ID" ]; then
NEW_CONTRIBUTOR_IDS="${NEW_CONTRIBUTOR_IDS:+$NEW_CONTRIBUTOR_IDS,}$CONTRIBUTOR_ID"
TOOL_CONTRIBUTOR_PAYLOAD=$(jq -n \
--arg tool_id "$TOOL_ID" \
--arg contributor_id "$CONTRIBUTOR_ID" \
'{tool_id: $tool_id, contributor_id: ($contributor_id|tonumber)}')
curl -s -X POST "${{ secrets.SUPABASE_URL }}/rest/v1/tool_contributors" \
-H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Content-Type: application/json" \
-H "Prefer: resolution=ignore-duplicates" \
-d "$TOOL_CONTRIBUTOR_PAYLOAD" >/dev/null || true
echo "Contributor linked: $AUTHOR_NAME"
else
echo "Warning: Failed to upsert contributor for author '$AUTHOR_NAME'"
fi
done
fi
# Remove contributors that are no longer in the authors list
EXISTING_CONTRIBUTORS=$(curl -s "${{ secrets.SUPABASE_URL }}/rest/v1/tool_contributors?tool_id=eq.$TOOL_ID&select=contributor_id" \
-H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}")
for EXISTING_ID in $(echo "$EXISTING_CONTRIBUTORS" | jq -r '.[].contributor_id'); do
if ! echo ",$NEW_CONTRIBUTOR_IDS," | grep -q ",$EXISTING_ID,"; then
curl -s -X DELETE "${{ secrets.SUPABASE_URL }}/rest/v1/tool_contributors?tool_id=eq.$TOOL_ID&contributor_id=eq.$EXISTING_ID" \
-H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" >/dev/null || true
echo "Contributor removed: $EXISTING_ID"
fi
done
else
echo "Warning: Could not extract tool ID from Supabase response"
fi