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

@@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import {
getAbsolutePublicAssetUrl,
getSkillMarkdownCandidateUrls,
getSkillsIndexCandidateUrls,
normalizeBasePath,
} from '../publicAssetUrls';
describe('public asset URL helpers', () => {
it('normalizes dot-relative BASE_URL values', () => {
expect(normalizeBasePath('./')).toBe('/');
expect(normalizeBasePath('/antigravity-awesome-skills/')).toBe('/antigravity-awesome-skills/');
});
it('builds stable skills index candidates for gh-pages routes', () => {
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('builds stable markdown candidates for gh-pages routes', () => {
expect(
getSkillMarkdownCandidateUrls({
baseUrl: '/antigravity-awesome-skills/',
origin: 'https://sickn33.github.io',
pathname: '/antigravity-awesome-skills/skill/react-patterns',
documentBaseUrl: 'https://sickn33.github.io/antigravity-awesome-skills/',
skillPath: 'skills/react-patterns',
}),
).toEqual([
'https://sickn33.github.io/antigravity-awesome-skills/skills/react-patterns/SKILL.md',
'https://sickn33.github.io/skills/react-patterns/SKILL.md',
'https://sickn33.github.io/antigravity-awesome-skills/skill/skills/react-patterns/SKILL.md',
'https://sickn33.github.io/antigravity-awesome-skills/skill/react-patterns/skills/react-patterns/SKILL.md',
]);
});
it('resolves absolute public asset URLs from the shared base path logic', () => {
expect(
getAbsolutePublicAssetUrl('/skill/react-patterns', {
baseUrl: '/antigravity-awesome-skills/',
origin: 'https://sickn33.github.io',
}),
).toBe('https://sickn33.github.io/antigravity-awesome-skills/skill/react-patterns');
});
});

View File

@@ -28,8 +28,8 @@ describe('SEO helpers', () => {
it('builds homepage metadata with the canonical catalog message', () => {
const meta = buildHomeMeta(10);
expect(meta.title).toContain('1,326+');
expect(meta.description).toContain('1,326+ installable agentic skills');
expect(meta.title).toContain('10 installable AI skills');
expect(meta.description).toContain('10 installable agentic skills');
expect(meta.canonicalPath).toBe('/');
expect(meta.ogTitle).toBe(meta.title);
expect(meta.ogImage).toBe(DEFAULT_SOCIAL_IMAGE);

View File

@@ -0,0 +1,119 @@
export interface PublicAssetUrlInput {
baseUrl: string;
origin: string;
pathname: string;
documentBaseUrl?: string;
}
export type SkillsIndexUrlInput = PublicAssetUrlInput;
export interface SkillMarkdownUrlInput extends PublicAssetUrlInput {
skillPath: string;
}
function stripLeadingSlashes(path: string): string {
return path.replace(/^\/+/, '');
}
function normalizePathname(pathname: string): string {
return pathname.startsWith('/') ? pathname : `/${pathname}`;
}
function getResolvedDocumentBaseUrl({
baseUrl,
origin,
documentBaseUrl,
}: Pick<PublicAssetUrlInput, 'baseUrl' | 'origin' | 'documentBaseUrl'>): URL {
if (documentBaseUrl) {
return new URL(documentBaseUrl);
}
return new URL(normalizeBasePath(baseUrl), origin);
}
function getPathCandidateUrls(pathname: string, assetPath: string, origin: string): string[] {
const pathSegments = normalizePathname(pathname).split('/').filter(Boolean);
return pathSegments.map((_, index) => {
const prefix = `/${pathSegments.slice(0, index + 1).join('/')}/`;
return `${origin}${prefix}${assetPath}`;
});
}
function uniqueUrls(urls: string[]): string[] {
return Array.from(new Set(urls));
}
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 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}/`;
}
export function getAbsolutePublicAssetUrl(
assetPath: string,
{
baseUrl,
origin,
}: Pick<PublicAssetUrlInput, 'baseUrl' | 'origin'>,
): string {
const resolvedAssetPath = stripLeadingSlashes(assetPath.trim());
return new URL(resolvedAssetPath || '.', new URL(normalizeBasePath(baseUrl), origin)).href;
}
export function getSkillsIndexCandidateUrls({
baseUrl,
origin,
pathname,
documentBaseUrl,
}: SkillsIndexUrlInput): string[] {
const assetPath = 'skills.json';
return appendBackupCandidates(uniqueUrls([
new URL(assetPath, getResolvedDocumentBaseUrl({ baseUrl, origin, documentBaseUrl })).href,
new URL(assetPath, new URL(normalizeBasePath(baseUrl), origin)).href,
`${origin}/${assetPath}`,
...getPathCandidateUrls(pathname, assetPath, origin),
]));
}
export function getSkillMarkdownCandidateUrls({
baseUrl,
origin,
pathname,
documentBaseUrl,
skillPath,
}: SkillMarkdownUrlInput): string[] {
const normalizedSkillPath = skillPath
.replace(/^\/+/, '')
.replace(/\/SKILL\.md$/i, '');
const assetPath = `${normalizedSkillPath}/SKILL.md`;
return uniqueUrls([
new URL(assetPath, getResolvedDocumentBaseUrl({ baseUrl, origin, documentBaseUrl })).href,
new URL(assetPath, new URL(normalizeBasePath(baseUrl), origin)).href,
`${origin}/${assetPath}`,
...getPathCandidateUrls(pathname, assetPath, origin),
]);
}

View File

@@ -1,6 +1,6 @@
import type { SeoJsonLdValue, SeoMeta, TwitterCard, Skill } from '../types';
import { getAbsolutePublicAssetUrl } from './publicAssetUrls';
export const APP_HOME_CATALOG_COUNT = 1326;
export const DEFAULT_TOP_SKILL_COUNT = 40;
export const DEFAULT_SOCIAL_IMAGE = 'social-card.svg';
const SITE_NAME = 'Antigravity Awesome Skills';
@@ -46,15 +46,17 @@ export function getCanonicalUrl(canonicalPath: string, siteBaseUrl?: string): st
}
export function getAssetCanonicalUrl(canonicalPath: string): string {
const baseUrl = import.meta.env.BASE_URL || '/';
const normalizedBase = toCanonicalPath(baseUrl);
const appBase = normalizedBase === '/' ? '' : normalizedBase;
return `${window.location.origin}${appBase}${toCanonicalPath(canonicalPath)}`;
return getAbsolutePublicAssetUrl(toCanonicalPath(canonicalPath), {
baseUrl: import.meta.env.BASE_URL || '/',
origin: window.location.origin,
});
}
export function getAbsoluteAssetUrl(assetPath: string): string {
const normalizedAsset = toCanonicalPath(assetPath);
return getAssetCanonicalUrl(normalizedAsset);
return getAbsolutePublicAssetUrl(toCanonicalPath(assetPath), {
baseUrl: import.meta.env.BASE_URL || '/',
origin: window.location.origin,
});
}
function getCatalogBaseUrl(canonicalUrl: string): string {
@@ -93,12 +95,15 @@ function buildWebSiteSchema(canonicalUrl: string): Record<string, unknown> {
}
function buildSoftwareSourceCodeSchema(canonicalUrl: string, visibleCount: number): Record<string, unknown> {
const visibleCountLabel = `${visibleCount.toLocaleString('en-US')}+`;
const visibleCountLabel = visibleCount > 0
? `${visibleCount.toLocaleString('en-US')} agentic skills`
: 'agentic skills';
return {
'@context': 'https://schema.org',
'@type': 'SoftwareSourceCode',
name: SITE_NAME,
description: `Installable GitHub library of ${visibleCountLabel} agentic skills for AI coding assistants.`,
description: `Installable GitHub library of ${visibleCountLabel} for AI coding assistants.`,
url: canonicalUrl,
codeRepository: 'https://github.com/sickn33/antigravity-awesome-skills',
programmingLanguage: {
@@ -275,10 +280,14 @@ export function isTopSkill(skillId: string, skills: ReadonlyArray<Skill>, limit
}
export function buildHomeMeta(skillCount: number): SeoMeta {
const visibleCount = Math.max(skillCount, APP_HOME_CATALOG_COUNT);
const visibleCountLabel = `${visibleCount.toLocaleString('en-US')}+`;
const title = `Antigravity Awesome Skills | ${visibleCountLabel} installable AI skills catalog`;
const description = `Explore ${visibleCountLabel} installable agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, and Antigravity. Browse bundles, workflows, FAQs, and integration guides in one place.`;
const visibleCount = Math.max(skillCount, 0);
const visibleCountLabel = visibleCount > 0
? `${visibleCount.toLocaleString('en-US')} installable AI skills`
: 'installable AI skills';
const title = `Antigravity Awesome Skills | ${visibleCountLabel} catalog`;
const description = visibleCount > 0
? `Explore ${visibleCount.toLocaleString('en-US')} installable agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, and Antigravity. Browse bundles, workflows, FAQs, and integration guides in one place.`
: 'Explore installable agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, and Antigravity. Browse bundles, workflows, FAQs, and integration guides in one place.';
return {
title,
description,