diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..52b21cd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm typecheck + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build diff --git a/src/sync/github-sync.ts b/src/sync/github-sync.ts index ca2ad02..a5365c8 100644 --- a/src/sync/github-sync.ts +++ b/src/sync/github-sync.ts @@ -102,13 +102,24 @@ export class GitHubSync { return { synced, errors, total: synced + errors }; } + /** Build auth headers from GITHUB_TOKEN env var (optional). */ + private authHeaders(): Record { + const token = process.env.GITHUB_TOKEN; + const headers: Record = { + Accept: "application/vnd.github+json", + }; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + return headers; + } + private async fetchTree(): Promise { const url = `https://api.github.com/repos/${this.repo}/git/trees/main?recursive=1`; - const res = await fetch(url, { - headers: { Accept: "application/vnd.github+json" }, - }); + const res = await fetch(url, { headers: this.authHeaders() }); if (!res.ok) { - throw new Error(`GitHub tree API returned ${res.status}`); + const detail = await res.text().catch(() => ""); + throw new Error(`GitHub tree API returned ${res.status}: ${detail.slice(0, 200)}`); } const data = (await res.json()) as { tree: TreeEntry[] }; return data.tree; @@ -116,9 +127,10 @@ export class GitHubSync { private async fetchRawContent(path: string): Promise { const url = `https://raw.githubusercontent.com/${this.repo}/main/${path}`; - const res = await fetch(url); + const res = await fetch(url, { headers: this.authHeaders() }); if (!res.ok) { - throw new Error(`Failed to fetch ${path}: ${res.status}`); + const detail = await res.text().catch(() => ""); + throw new Error(`Failed to fetch ${path}: ${res.status} — ${detail.slice(0, 200)}`); } return res.text(); } @@ -126,12 +138,22 @@ export class GitHubSync { parseSkillFile(raw: string, skillId: string): Skill | null { // Extract YAML frontmatter between --- markers const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); - if (!fmMatch) return null; + if (!fmMatch) { + this.ctx.logger.warn( + `Skill "${skillId}" has no valid YAML frontmatter (expected ---\nname: ...\n---). Skipping.`, + ); + return null; + } const frontmatter = fmMatch[1]; const body = fmMatch[2].trim(); const fm = this.parseSimpleYaml(frontmatter); - if (!fm.name) return null; + if (!fm.name) { + this.ctx.logger.warn( + `Skill "${skillId}" frontmatter is missing required "name" field. Skipping.`, + ); + return null; + } return { id: skillId,