fix(repo): Harden catalog sync and release integrity

Tighten the repo-state automation so canonical bot commits remain
predictable while leaving main clean after each sync.

Make the public catalog UI more honest by hiding dev-only sync,
turning stars into explicit browser-local saves, aligning risk types,
and removing hardcoded catalog counts.

Add shared public asset URL helpers, risk suggestion plumbing,
safer unpack/sync guards, and CI coverage gates so release and
maintainer workflows catch drift earlier.
This commit is contained in:
sickn33
2026-03-29 09:22:09 +02:00
parent 141fd58568
commit 08a31cacf5
46 changed files with 1903 additions and 523 deletions

View File

@@ -1,6 +1,7 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
import type { Skill, StarMap } from '../types';
import { supabase } from '../lib/supabase';
import { getSkillsIndexCandidateUrls } from '../utils/publicAssetUrls';
interface SkillContextType {
skills: Skill[];
@@ -10,67 +11,8 @@ interface SkillContextType {
refreshSkills: () => Promise<void>;
}
interface SkillsIndexUrlInput {
baseUrl: string;
origin: string;
pathname: string;
documentBaseUrl?: string;
}
const SkillContext = createContext<SkillContextType | undefined>(undefined);
function normalizeBasePath(baseUrl: string): string {
const normalizedSegments = baseUrl
.trim()
.split('/')
.filter((segment) => segment.length > 0 && segment !== '.');
const normalizedPath = normalizedSegments.length > 0
? `/${normalizedSegments.join('/')}`
: '/';
return normalizedPath.endsWith('/') ? normalizedPath : `${normalizedPath}/`;
}
function appendBackupCandidates(urls: string[]): string[] {
const candidates = new Set<string>();
urls.forEach((url) => {
candidates.add(url);
if (url.endsWith('skills.json')) {
candidates.add(`${url}.backup`);
}
});
return Array.from(candidates);
}
export function getSkillsIndexCandidateUrls({
baseUrl,
origin,
pathname,
documentBaseUrl,
}: SkillsIndexUrlInput): string[] {
const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
const baseCandidate = new URL(
'skills.json',
documentBaseUrl || new URL(normalizeBasePath(baseUrl), origin),
).href;
const pathSegments = normalizedPathname.split('/').filter(Boolean);
const pathCandidates = pathSegments.map((_, index) => {
const prefix = `/${pathSegments.slice(0, index + 1).join('/')}/`;
return `${origin}${prefix}skills.json`;
});
return appendBackupCandidates([
baseCandidate,
new URL('skills.json', new URL(normalizeBasePath(baseUrl), origin)).href,
`${origin}/skills.json`,
...pathCandidates,
]);
}
export function SkillProvider({ children }: { children: React.ReactNode }) {
const [skills, setSkills] = useState<Skill[]>([]);
const [stars, setStars] = useState<StarMap>({});

View File

@@ -1,7 +1,7 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import { getSkillsIndexCandidateUrls, SkillProvider, useSkills } from '../SkillContext';
import { SkillProvider, useSkills } from '../SkillContext';
// Keep tests deterministic by skipping real Supabase requests.
vi.mock('../../lib/supabase', () => ({
@@ -23,44 +23,6 @@ function SkillsProbe() {
);
}
describe('getSkillsIndexCandidateUrls', () => {
it('keeps stable candidates for gh-pages base path', () => {
expect(
getSkillsIndexCandidateUrls({
baseUrl: '/antigravity-awesome-skills/',
origin: 'https://sickn33.github.io',
pathname: '/antigravity-awesome-skills/skill/some-id',
documentBaseUrl: 'https://sickn33.github.io/antigravity-awesome-skills/',
}),
).toEqual([
'https://sickn33.github.io/antigravity-awesome-skills/skills.json',
'https://sickn33.github.io/antigravity-awesome-skills/skills.json.backup',
'https://sickn33.github.io/skills.json',
'https://sickn33.github.io/skills.json.backup',
'https://sickn33.github.io/antigravity-awesome-skills/skill/skills.json',
'https://sickn33.github.io/antigravity-awesome-skills/skill/skills.json.backup',
'https://sickn33.github.io/antigravity-awesome-skills/skill/some-id/skills.json',
'https://sickn33.github.io/antigravity-awesome-skills/skill/some-id/skills.json.backup',
]);
});
it('normalizes dot-relative BASE_URL values', () => {
expect(
getSkillsIndexCandidateUrls({
baseUrl: './',
origin: 'https://sickn33.github.io',
pathname: '/antigravity-awesome-skills/',
documentBaseUrl: 'https://sickn33.github.io/antigravity-awesome-skills/',
}),
).toEqual([
'https://sickn33.github.io/antigravity-awesome-skills/skills.json',
'https://sickn33.github.io/antigravity-awesome-skills/skills.json.backup',
'https://sickn33.github.io/skills.json',
'https://sickn33.github.io/skills.json.backup',
]);
});
});
describe('SkillProvider', () => {
beforeEach(() => {
(global.fetch as Mock).mockReset();