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:
@@ -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>({});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user