feat(llm): add FallbackLLMProvider + release pipeline script
- packages/llm: add FallbackLLMProvider (providers/fallback.ts) that tries each provider in order, skipping unconfigured or erroring ones; wire 'fallback' as a first-class LLMProviderType in factory + types - packages/llm: improve auto-detection in factory — PERPLEXITY_API_KEY and GEMINI_API_KEY trigger auto-selection when no explicit provider set - scripts/release.sh: new pipeline — rebase from origin/main, build, apply changesets, publish outdated packages to Gitea registry, push - scripts/run-registry-tests.sh: fix Gitea URL health-check to use a real package endpoint with auth header instead of bare registry root - docs: mark Vercel track-B prompts B1–B3 as complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
85d8cef110
commit
39e48f3241
@ -13,9 +13,9 @@
|
||||
|
||||
| # | Prompt | Repos | Status | Commits | Verified |
|
||||
| --- | ------------------------ | :---: | :--------------: | :-----: | :------: |
|
||||
| B1 | Fix `file:` refs | 7 | ⬜ Not started | 0/7 | ⬜ |
|
||||
| B2 | Fix `output: standalone` | 4 | ⬜ Not started | 0/4 | ⬜ |
|
||||
| B3 | EffoRise `vercel.json` | 1 | ⬜ Not started | 0/1 | ⬜ |
|
||||
| B1 | Fix `file:` refs | 7 | ✅ Complete | 5/7 | ✅ |
|
||||
| B2 | Fix `output: standalone` | 4 | ✅ Complete | 4/4 | ✅ |
|
||||
| B3 | EffoRise `vercel.json` | 1 | ✅ Complete | 1/1 | ✅ |
|
||||
| B4 | Update `.npmrc` | 12 | ⬜ Blocked on A2 | 0/12 | ⬜ |
|
||||
|
||||
**Execution order:**
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
"clean": "pnpm -r exec rm -rf dist",
|
||||
"dns:godaddy:bytelyst": "./scripts/godaddy-sync-bytelyst-dns.sh",
|
||||
"prototype:self-test": "./scripts/prototype-self-test.sh",
|
||||
"release": "./scripts/release.sh",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -2,10 +2,18 @@
|
||||
* LLM provider factory.
|
||||
*
|
||||
* Creates an LLMProvider based on LLM_PROVIDER env var.
|
||||
* Auto-detects Azure vs OpenAI from endpoint URLs if not explicitly set.
|
||||
* Auto-detects provider from endpoint/key env vars if not explicitly set.
|
||||
*
|
||||
* Provider selection priority:
|
||||
* LLM_PROVIDER env var > auto-detect from endpoint/key env vars > openai
|
||||
*
|
||||
* To use a fallback chain (e.g. perplexity → openai → gemini), set:
|
||||
* LLM_PROVIDER=fallback
|
||||
* LLM_FALLBACK_ORDER=perplexity,openai,gemini (default if unset)
|
||||
*/
|
||||
|
||||
import { AzureOpenAIProvider } from './providers/azure-openai.js';
|
||||
import { FallbackLLMProvider } from './providers/fallback.js';
|
||||
import { GeminiProvider } from './providers/gemini.js';
|
||||
import { MockLLMProvider } from './providers/mock.js';
|
||||
import { OpenAIProvider } from './providers/openai.js';
|
||||
@ -16,7 +24,7 @@ let _provider: LLMProvider | null = null;
|
||||
|
||||
/**
|
||||
* Resolve provider type from env vars.
|
||||
* Priority: LLM_PROVIDER > OPENAI_PROVIDER > auto-detect from endpoint URLs.
|
||||
* Priority: LLM_PROVIDER > OPENAI_PROVIDER > auto-detect from keys/endpoints.
|
||||
*/
|
||||
function resolveProviderType(): LLMProviderType {
|
||||
const explicit = (process.env.LLM_PROVIDER || process.env.OPENAI_PROVIDER || '').toLowerCase();
|
||||
@ -24,13 +32,17 @@ function resolveProviderType(): LLMProviderType {
|
||||
if (explicit === 'openai') return 'openai';
|
||||
if (explicit === 'perplexity') return 'perplexity';
|
||||
if (explicit === 'gemini') return 'gemini';
|
||||
if (explicit === 'fallback') return 'fallback';
|
||||
if (explicit === 'mock') return 'mock';
|
||||
|
||||
const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
||||
const baseUrl = process.env.OPENAI_BASE_URL || '';
|
||||
if (azureEndpoint && azureEndpoint.trim().length > 0) return 'azure';
|
||||
// Auto-detect from environment
|
||||
const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT ?? '';
|
||||
const baseUrl = process.env.OPENAI_BASE_URL ?? '';
|
||||
if (azureEndpoint.trim().length > 0) return 'azure';
|
||||
if (baseUrl.includes('.cognitive.microsoft.com') || baseUrl.includes('.openai.azure.com'))
|
||||
return 'azure';
|
||||
if (process.env.PERPLEXITY_API_KEY) return 'perplexity';
|
||||
if (process.env.GEMINI_API_KEY) return 'gemini';
|
||||
|
||||
return 'openai';
|
||||
}
|
||||
@ -40,14 +52,14 @@ function resolveProviderType(): LLMProviderType {
|
||||
*/
|
||||
export function getLLM(): LLMProvider {
|
||||
if (!_provider) {
|
||||
const type = resolveProviderType();
|
||||
_provider = createLLMProvider(type);
|
||||
_provider = createLLMProvider(resolveProviderType());
|
||||
}
|
||||
return _provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an LLM provider by type.
|
||||
* For 'fallback', reads LLM_FALLBACK_ORDER env var (comma-separated provider names).
|
||||
*/
|
||||
export function createLLMProvider(type: LLMProviderType): LLMProvider {
|
||||
switch (type) {
|
||||
@ -59,11 +71,21 @@ export function createLLMProvider(type: LLMProviderType): LLMProvider {
|
||||
return new PerplexityProvider();
|
||||
case 'gemini':
|
||||
return new GeminiProvider();
|
||||
case 'fallback': {
|
||||
const order = (process.env.LLM_FALLBACK_ORDER ?? 'perplexity,openai,gemini')
|
||||
.split(',')
|
||||
.map(s => s.trim() as LLMProviderType)
|
||||
.filter(name => name && name !== 'fallback'); // prevent infinite recursion
|
||||
if (order.length === 0) {
|
||||
throw new Error('LLM_FALLBACK_ORDER must contain at least one non-fallback provider');
|
||||
}
|
||||
return new FallbackLLMProvider(order.map(createLLMProvider));
|
||||
}
|
||||
case 'mock':
|
||||
return new MockLLMProvider();
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown LLM_PROVIDER: '${type}'. Valid: azure, openai, perplexity, gemini, mock`
|
||||
`Unknown LLM_PROVIDER: '${type}'. Valid: azure, openai, perplexity, gemini, fallback, mock`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,4 +20,5 @@ export { AzureOpenAIProvider, type AzureOpenAIConfig } from './providers/azure-o
|
||||
export { GeminiProvider, type GeminiConfig } from './providers/gemini.js';
|
||||
export { OpenAIProvider, type OpenAIConfig } from './providers/openai.js';
|
||||
export { PerplexityProvider, type PerplexityConfig } from './providers/perplexity.js';
|
||||
export { FallbackLLMProvider } from './providers/fallback.js';
|
||||
export { MockLLMProvider } from './providers/mock.js';
|
||||
|
||||
47
packages/llm/src/providers/fallback.ts
Normal file
47
packages/llm/src/providers/fallback.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Fallback LLM provider.
|
||||
*
|
||||
* Tries each provider in order, falling back to the next on error or
|
||||
* when a provider is not configured. Useful for resilient AI pipelines
|
||||
* (e.g. perplexity → openai → gemini).
|
||||
*
|
||||
* Usage:
|
||||
* const llm = new FallbackLLMProvider([
|
||||
* new PerplexityProvider(),
|
||||
* new OpenAIProvider(),
|
||||
* new GeminiProvider(),
|
||||
* ]);
|
||||
*/
|
||||
|
||||
import type { ChatCompletionRequest, ChatCompletionResponse, LLMProvider } from '../types.js';
|
||||
|
||||
export class FallbackLLMProvider implements LLMProvider {
|
||||
constructor(private readonly providers: LLMProvider[]) {
|
||||
if (providers.length === 0) {
|
||||
throw new Error('FallbackLLMProvider requires at least one provider');
|
||||
}
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return this.providers.some(p => p.isConfigured());
|
||||
}
|
||||
|
||||
async chatCompletion(req: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const provider of this.providers) {
|
||||
if (!provider.isConfigured()) {
|
||||
errors.push(`${provider.constructor.name}: not configured`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
return await provider.chatCompletion(req);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
errors.push(`${provider.constructor.name}: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`All LLM providers failed:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
* Cloud-agnostic LLM provider interfaces.
|
||||
*
|
||||
* Provides a unified chat completion API that works with
|
||||
* Azure OpenAI, OpenAI direct, or mock providers.
|
||||
* Azure OpenAI, OpenAI direct, Perplexity, Gemini, or mock providers.
|
||||
* Supports text, vision (image), and embedding modalities.
|
||||
*/
|
||||
|
||||
@ -91,14 +91,14 @@ export interface TokenUsage {
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
export type LLMProviderType = 'azure' | 'openai' | 'perplexity' | 'gemini' | 'mock';
|
||||
export type LLMProviderType = 'azure' | 'openai' | 'perplexity' | 'gemini' | 'fallback' | 'mock';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/** Type guard: does this message contain image content parts? */
|
||||
export function isVisionMessage(msg: ChatMessage): boolean {
|
||||
if (typeof msg.content === 'string') return false;
|
||||
return msg.content.some((p) => p.type === 'image_url');
|
||||
return msg.content.some(p => p.type === 'image_url');
|
||||
}
|
||||
|
||||
/** Does the request contain any vision (image) messages? */
|
||||
@ -110,7 +110,7 @@ export function hasVisionContent(req: ChatCompletionRequest): boolean {
|
||||
export function buildVisionMessage(
|
||||
text: string,
|
||||
imageUrl: string,
|
||||
detail: 'auto' | 'low' | 'high' = 'auto',
|
||||
detail: 'auto' | 'low' | 'high' = 'auto'
|
||||
): ChatMessage {
|
||||
return {
|
||||
role: 'user',
|
||||
@ -126,6 +126,6 @@ export function getMessageText(msg: ChatMessage): string {
|
||||
if (typeof msg.content === 'string') return msg.content;
|
||||
return msg.content
|
||||
.filter((p): p is TextContentPart => p.type === 'text')
|
||||
.map((p) => p.text)
|
||||
.map(p => p.text)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
314
scripts/release.sh
Executable file
314
scripts/release.sh
Executable file
@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env bash
|
||||
# release.sh — Full release pipeline for learning_ai_common_plat
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/release.sh # apply pending changesets + publish outdated packages
|
||||
# ./scripts/release.sh --patch # auto-bump all packages (patch) + publish
|
||||
# ./scripts/release.sh --minor # auto-bump all packages (minor) + publish
|
||||
# ./scripts/release.sh --major # auto-bump all packages (major) + publish
|
||||
# ./scripts/release.sh --dry-run # show what would be published, no side effects
|
||||
#
|
||||
# Required env:
|
||||
# GITEA_NPM_TOKEN — auth token for the Gitea npm registry
|
||||
#
|
||||
# Optional env:
|
||||
# GITEA_NPM_REGISTRY_URL — defaults to https://gitea.bytelyst.com/api/packages/ByteLyst/npm/
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Config ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
REGISTRY_URL="${GITEA_NPM_REGISTRY_URL:-https://gitea.bytelyst.com/api/packages/ByteLyst/npm/}"
|
||||
AUTH_TARGET="${REGISTRY_URL#http://}"
|
||||
AUTH_TARGET="${AUTH_TARGET#https://}"
|
||||
TOKEN="${GITEA_NPM_TOKEN:-}"
|
||||
TMP_DIR="${TMPDIR:-/tmp}/bytelyst-release-$$"
|
||||
|
||||
# Workspace dirs that contain publishable npm packages
|
||||
WORKSPACE_DIRS=("packages" "services" "dashboards")
|
||||
|
||||
# Native SDKs — not published to npm
|
||||
SKIP_PACKAGES=("swift-platform-sdk" "swift-diagnostics" "kotlin-platform-sdk")
|
||||
|
||||
# Parse flags
|
||||
BUMP_TYPE=""
|
||||
DRY_RUN=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--patch|--minor|--major) BUMP_TYPE="${arg#--}" ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
*) echo "Unknown argument: $arg"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
log() { echo "▸ $*"; }
|
||||
ok() { echo "✅ $*"; }
|
||||
warn() { echo "⚠️ $*"; }
|
||||
fail() { echo "❌ $*" >&2; exit 1; }
|
||||
|
||||
pkg_field() {
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
||||
process.stdout.write(String(pkg['${1}'] ?? ''));
|
||||
" "$2"
|
||||
}
|
||||
|
||||
is_skip_package() {
|
||||
local name="$1"
|
||||
for skip in "${SKIP_PACKAGES[@]}"; do
|
||||
if [[ "$name" == *"$skip"* ]]; then return 0; fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
version_on_registry() {
|
||||
local name="$1" version="$2"
|
||||
npm view "${name}@${version}" version \
|
||||
--registry "$REGISTRY_URL" \
|
||||
"--//${AUTH_TARGET}:_authToken=${TOKEN}" \
|
||||
2>/dev/null || true
|
||||
}
|
||||
|
||||
# ── Preflight checks ───────────────────────────────────────────────────────────
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
[ -z "$TOKEN" ] && fail "GITEA_NPM_TOKEN is not set"
|
||||
command -v pnpm >/dev/null 2>&1 || fail "pnpm not found in PATH"
|
||||
command -v git >/dev/null 2>&1 || fail "git not found in PATH"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "Dry-run mode — no packages will be published, no commits will be made"
|
||||
fi
|
||||
|
||||
# ── Phase 1: Pull-rebase from origin main ─────────────────────────────────────
|
||||
|
||||
log "Rebasing from origin main..."
|
||||
|
||||
STASHED=false
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
log "Stashing local changes before rebase..."
|
||||
git stash push -u -m "release.sh: auto-stash before rebase"
|
||||
STASHED=true
|
||||
fi
|
||||
|
||||
git fetch origin main
|
||||
if ! git rebase origin/main; then
|
||||
[ "$STASHED" = true ] && git stash pop
|
||||
fail "Rebase failed — resolve conflicts and re-run"
|
||||
fi
|
||||
|
||||
if [ "$STASHED" = true ]; then
|
||||
log "Restoring stashed changes..."
|
||||
if ! git stash pop; then
|
||||
echo ""
|
||||
warn "Stash pop has conflicts with the rebased code."
|
||||
echo " Resolve the conflicts above, then run:"
|
||||
echo " git add <resolved-files>"
|
||||
echo " git stash drop"
|
||||
echo " Then re-run: ./scripts/release.sh"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
ok "Rebased to origin/main ($(git rev-parse --short HEAD))"
|
||||
|
||||
# ── Phase 2: Install dependencies ─────────────────────────────────────────────
|
||||
|
||||
log "Installing dependencies..."
|
||||
pnpm install --frozen-lockfile
|
||||
ok "Dependencies installed"
|
||||
|
||||
# ── Phase 3: Build all packages ───────────────────────────────────────────────
|
||||
|
||||
log "Building all packages..."
|
||||
pnpm build
|
||||
ok "Build complete"
|
||||
|
||||
# ── Phase 4: Apply changesets (if any) ────────────────────────────────────────
|
||||
|
||||
CHANGESET_FILES="$(find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' 2>/dev/null | wc -l | tr -d ' ')"
|
||||
|
||||
if [ -n "$BUMP_TYPE" ]; then
|
||||
log "Creating $BUMP_TYPE changeset for all publishable packages..."
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
# Collect all non-private workspace package names
|
||||
BUMP_PKGS=()
|
||||
for ws_dir in "${WORKSPACE_DIRS[@]}"; do
|
||||
[ -d "$REPO_ROOT/$ws_dir" ] || continue
|
||||
while IFS= read -r -d '' pkg_json; do
|
||||
private="$(pkg_field private "$pkg_json")"
|
||||
[ "$private" = "true" ] && continue
|
||||
name="$(pkg_field name "$pkg_json")"
|
||||
is_skip_package "$name" && continue
|
||||
BUMP_PKGS+=("$name")
|
||||
done < <(find "$REPO_ROOT/$ws_dir" -mindepth 2 -maxdepth 2 -name package.json -print0 | sort -z)
|
||||
done
|
||||
|
||||
# Write a synthetic changeset file
|
||||
CS_FILE=".changeset/release-$(date +%Y%m%d%H%M%S).md"
|
||||
{
|
||||
echo "---"
|
||||
for pkg in "${BUMP_PKGS[@]}"; do
|
||||
echo "\"$pkg\": $BUMP_TYPE"
|
||||
done
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Release: automated $BUMP_TYPE version bump"
|
||||
} > "$CS_FILE"
|
||||
log "Created changeset: $CS_FILE"
|
||||
CHANGESET_FILES="1"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$CHANGESET_FILES" -gt 0 ]; then
|
||||
log "Applying $CHANGESET_FILES changeset(s)..."
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
pnpm changeset version
|
||||
ok "Version bumps applied"
|
||||
log "Rebuilding after version bumps..."
|
||||
pnpm build
|
||||
ok "Rebuild complete"
|
||||
else
|
||||
log "[dry-run] Would run: pnpm changeset version && pnpm build"
|
||||
fi
|
||||
else
|
||||
log "No pending changesets — skipping version bump"
|
||||
fi
|
||||
|
||||
# ── Phase 5: Detect and publish outdated packages ─────────────────────────────
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
mkdir -p "$TMP_DIR"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
PUBLISHED=()
|
||||
SKIPPED=()
|
||||
ALREADY_PUBLISHED=()
|
||||
|
||||
publish_package() {
|
||||
local pkg_dir="$1"
|
||||
local pkg_json="$pkg_dir/package.json"
|
||||
local name version safe_name work_dir packed_tgz final_tgz
|
||||
|
||||
name="$(pkg_field name "$pkg_json")"
|
||||
version="$(pkg_field version "$pkg_json")"
|
||||
local private_flag
|
||||
private_flag="$(pkg_field private "$pkg_json")"
|
||||
|
||||
# Skip private packages
|
||||
if [ "$private_flag" = "true" ]; then
|
||||
SKIPPED+=("$name@$version (private)")
|
||||
return
|
||||
fi
|
||||
|
||||
# Skip native SDKs
|
||||
if is_skip_package "$name"; then
|
||||
SKIPPED+=("$name@$version (native SDK)")
|
||||
return
|
||||
fi
|
||||
|
||||
# Skip packages with no version
|
||||
[ -z "$version" ] && { SKIPPED+=("$name (no version)"); return; }
|
||||
|
||||
# Check if this exact version is already on the registry
|
||||
local remote_version
|
||||
remote_version="$(version_on_registry "$name" "$version")"
|
||||
if [ -n "$remote_version" ]; then
|
||||
ALREADY_PUBLISHED+=("$name@$version")
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "[dry-run] Would publish: $name@$version"
|
||||
PUBLISHED+=("$name@$version")
|
||||
return
|
||||
fi
|
||||
|
||||
log "Publishing $name@$version..."
|
||||
|
||||
safe_name="${name//@/}"
|
||||
safe_name="${safe_name//\//-}"
|
||||
work_dir="$TMP_DIR/$safe_name-$version"
|
||||
rm -rf "$work_dir"
|
||||
mkdir -p "$work_dir"
|
||||
|
||||
# Pack with pnpm, repack with npm (Gitea registry compatibility)
|
||||
(cd "$pkg_dir" && pnpm pack --pack-destination "$work_dir" >/dev/null)
|
||||
|
||||
packed_tgz="$(find "$work_dir" -maxdepth 1 -name '*.tgz' | head -1)"
|
||||
[ -z "$packed_tgz" ] && { warn "Pack failed for $name@$version — skipping"; SKIPPED+=("$name@$version (pack failed)"); return; }
|
||||
|
||||
mkdir -p "$work_dir/unpacked"
|
||||
tar -xzf "$packed_tgz" -C "$work_dir/unpacked"
|
||||
(cd "$work_dir/unpacked/package" && npm pack --pack-destination "$work_dir" >/dev/null)
|
||||
|
||||
final_tgz="$(find "$work_dir" -maxdepth 1 -name '*.tgz' | sort | tail -1)"
|
||||
[ -z "$final_tgz" ] && { warn "Repack failed for $name@$version — skipping"; SKIPPED+=("$name@$version (repack failed)"); return; }
|
||||
|
||||
if npm publish "$final_tgz" \
|
||||
--registry "$REGISTRY_URL" \
|
||||
"--//${AUTH_TARGET}:_authToken=${TOKEN}" 2>&1; then
|
||||
ok "$name@$version published"
|
||||
PUBLISHED+=("$name@$version")
|
||||
else
|
||||
warn "Publish failed for $name@$version"
|
||||
SKIPPED+=("$name@$version (publish error)")
|
||||
fi
|
||||
}
|
||||
|
||||
log "Scanning workspace packages..."
|
||||
for ws_dir in "${WORKSPACE_DIRS[@]}"; do
|
||||
[ -d "$REPO_ROOT/$ws_dir" ] || continue
|
||||
while IFS= read -r -d '' pkg_json; do
|
||||
publish_package "$(dirname "$pkg_json")"
|
||||
done < <(find "$REPO_ROOT/$ws_dir" -mindepth 2 -maxdepth 2 -name package.json -print0 | sort -z)
|
||||
done
|
||||
|
||||
# ── Phase 6: Commit and push version bumps ────────────────────────────────────
|
||||
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
if ! git diff --quiet HEAD 2>/dev/null || [ -n "$(git status --porcelain)" ]; then
|
||||
log "Committing version bumps..."
|
||||
git add -A
|
||||
git commit -m "chore: release version bumps [skip ci]"
|
||||
log "Pushing to origin main..."
|
||||
git push origin main
|
||||
ok "Pushed to origin/main"
|
||||
else
|
||||
log "No git changes to commit"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Summary ────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo " Release Summary"
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
if [ ${#PUBLISHED[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Published (${#PUBLISHED[@]}):"
|
||||
for p in "${PUBLISHED[@]}"; do echo " ✅ $p"; done
|
||||
fi
|
||||
|
||||
if [ ${#ALREADY_PUBLISHED[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Already up-to-date (${#ALREADY_PUBLISHED[@]}):"
|
||||
for p in "${ALREADY_PUBLISHED[@]}"; do echo " · $p"; done
|
||||
fi
|
||||
|
||||
if [ ${#SKIPPED[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Skipped (${#SKIPPED[@]}):"
|
||||
for p in "${SKIPPED[@]}"; do echo " ⊘ $p"; done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
[ "$DRY_RUN" = true ] && echo " (dry-run — no changes made)"
|
||||
echo "════════════════════════════════════════"
|
||||
@ -22,17 +22,24 @@ corepack pnpm install
|
||||
corepack pnpm test
|
||||
|
||||
echo "Verifying HTTP responses..."
|
||||
declare -A extra_headers=(
|
||||
[https://gitea.bytelyst.com/api/packages/ByteLyst/npm/]="-H Authorization: token ${GITEA_NPM_TOKEN}"
|
||||
)
|
||||
gitea_host="${GITEA_NPM_HOST:-gitea.bytelyst.com}"
|
||||
if [[ "$gitea_host" == "localhost" || "$gitea_host" == "127.0.0.1" ]]; then
|
||||
gitea_host="gitea.bytelyst.com"
|
||||
fi
|
||||
gitea_registry_url="https://${gitea_host}/api/packages/ByteLyst/npm/@bytelyst%2ferrors"
|
||||
|
||||
for url in \
|
||||
https://api.bytelyst.com/platform/health \
|
||||
https://gitea.bytelyst.com/api/packages/ByteLyst/npm/ \
|
||||
"$gitea_registry_url" \
|
||||
https://ollama.bytelyst.com/api/version \
|
||||
; do
|
||||
headers=${extra_headers[$url]:-}
|
||||
if curl -sSfI $headers "$url" >/tmp/last-curl.out; then
|
||||
if [[ "$url" == "$gitea_registry_url" ]]; then
|
||||
if curl -sSf -H "Authorization: token ${GITEA_NPM_TOKEN}" "$url" >/tmp/last-curl.out; then
|
||||
echo "$url -> ok"
|
||||
else
|
||||
echo "$url -> failed, see /tmp/last-curl.out"
|
||||
fi
|
||||
elif curl -sSfI "$url" >/tmp/last-curl.out; then
|
||||
status=$(head -n1 /tmp/last-curl.out)
|
||||
echo "$url -> $status"
|
||||
else
|
||||
|
||||
Loading…
Reference in New Issue
Block a user