Skip to content

feat: Phase 1 hardening — retry, rate limits, tests, docs#8

Merged
GeiserX merged 3 commits intomainfrom
phase-1/hardening
Apr 16, 2026
Merged

feat: Phase 1 hardening — retry, rate limits, tests, docs#8
GeiserX merged 3 commits intomainfrom
phase-1/hardening

Conversation

@GeiserX
Copy link
Copy Markdown
Owner

@GeiserX GeiserX commented Apr 16, 2026

Summary

Phase 1 hardening for ghost-github-portfolio, covering reliability, testing, documentation, and fixes.

Changes

Reliability

  • HTTP retry with exponential backoff for all API calls (GitHub, Ghost, banner detection)
  • GitHub API rate limit awareness — detects x-ratelimit-remaining: 0, waits for reset, retries automatically
  • Verbose mode shows rate limit status per request

Testing (26 → 48 tests)

  • New github.test.ts — 13 tests covering sort, filter, pagination, exclude, banner detection
  • New http.test.ts — 8 tests covering retry logic, backoff, rate limit handling, passthrough
  • Refactored ghost.test.ts — imports real generateJwt instead of duplicating it, adds invalid key test

Fixes

  • Version 1.0.0 → 0.1.0 (no prior releases exist)
  • generateJwt exported from ghost.ts (was private, duplicated in tests)
  • All action.yml inputs now wired (ghost-url, page-slug, username, min-stars were declared but unused)
  • Removed :latest Docker tag from release workflow

Documentation

  • AGENTS.md — architecture docs, design decisions, CI/CD guide, code conventions, config schema, pitfalls
  • docs/ROADMAP.md — phase-based roadmap (11 phases + stretch goals, ~100 items)
  • README links to roadmap, Docker tag references updated to 0.1.0

New files

  • src/http.ts — fetchWithRetry, parseRateLimitHeaders
  • src/http.test.ts — 8 tests
  • src/github.test.ts — 13 tests
  • AGENTS.md
  • docs/ROADMAP.md

Test plan

  • npm run lint passes
  • npm run build passes
  • npm test passes (48/48)
  • Docker build succeeds
  • No secrets in committed files
  • AGENTS.md ends with LynxPrompt attribution

Summary by CodeRabbit

  • New Features

    • Action inputs now support runtime config overrides (Ghost URL, page slug, GitHub username, min-stars).
    • Network requests now use retry logic with exponential backoff and optional verbose retry logging; GitHub rate-limit info can be surfaced.
  • Documentation

    • Added a detailed roadmap and contributor/agent guidance.
    • Updated README Docker examples to match current image version.
  • Chores

    • Project version updated to 0.1.0.
  • Tests

    • Added/expanded test coverage for HTTP, GitHub, and Ghost behaviors.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a retry-capable HTTP helper with rate-limit handling, refactors Ghost/GitHub API calls to use it, expands test coverage, updates action inputs and config-override step, introduces agent and roadmap docs, and bumps the project version from 1.0.0 → 0.1.0.

Changes

Cohort / File(s) Summary
Version & docs
package.json, src/index.ts, README.md
Version changed from 1.0.00.1.0 and README updated with new image tag and Roadmap link.
CI / Release workflow
.github/workflows/release.yml
Docker publish step now emits a single versioned image tag (removed :latest).
HTTP retry utilities
src/http.ts, src/http.test.ts
New fetchWithRetry with exponential backoff, rate-limit-aware retrying, parseRateLimitHeaders, and comprehensive tests validating retry, backoff, rate-limit handling, and request forwarding.
Ghost integration
src/ghost.ts, src/ghost.test.ts
Exported generateJwt; Ghost calls (fetchPage, updatePage) now use fetchWithRetry; tests updated to import generateJwt and assert invalid-key error.
GitHub integration & banner detection
src/github.ts, src/github.test.ts
fetchRepos now uses fetchWithRetry, accepts verbose, parses rate-limit headers, and reports rate-limit info; detectBanner uses retries and safe error handling; extensive tests added for sorting, filtering, pagination, banner resolution, and error cases.
Action inputs & overrides
action.yml
min-stars default changed to ""; added composite step Generate config overrides to emit an overridden config path when inputs provided, wired into sync step.
Agent & roadmap docs
AGENTS.md, docs/ROADMAP.md
Added AGENTS.md with agent instructions/constraints and docs/ROADMAP.md defining phased roadmap and stretch goals.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant GitHubModule as GitHub Module
  participant HTTP as fetchWithRetry
  participant GitHubAPI as GitHub API
  participant GhostModule as Ghost Module
  participant GhostAPI as Ghost API

  CLI->>GitHubModule: request fetchRepos(config, verbose)
  GitHubModule->>HTTP: HEAD/GET repo pages (with onRetry)
  HTTP->>GitHubAPI: HTTP request
  GitHubAPI-->>HTTP: response (+rate-limit headers)
  HTTP-->>GitHubModule: response (or retry)
  GitHubModule->>GitHubModule: parse/filter/sort repos
  GitHubModule->>HTTP: HEAD banner URL checks
  HTTP->>GitHubAPI: HEAD request
  GitHubAPI-->>HTTP: 200/404/etc
  HTTP-->>GitHubModule: banner exists? or null
  CLI->>GhostModule: updatePage(payload)
  GhostModule->>HTTP: POST/PUT (with generateJwt auth)
  HTTP->>GhostAPI: request
  GhostAPI-->>HTTP: response
  HTTP-->>GhostModule: response (or retry)
  GhostModule-->>CLI: result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main changes: Phase 1 hardening work including HTTP retry/backoff, rate limit handling, comprehensive test additions, and documentation updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase-1/hardening

Comment @coderabbitai help to get the list of available commands and usage tips.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 16, 2026

@coderabbitai review

1 similar comment
@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 16, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 16, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- Set version to 0.1.0 (no prior releases)
- Add HTTP retry with exponential backoff for all API calls
- Add GitHub rate limit awareness (wait-then-retry on 403/429)
- Export generateJwt from ghost.ts, remove duplicated function in tests
- Add github.test.ts (13 tests: sort, filter, pagination, exclude, banner)
- Add http.test.ts (8 tests: retry logic, backoff, rate limit, passthrough)
- Wire all action.yml inputs (ghost-url, page-slug, username, min-stars)
- Remove :latest Docker tag from release workflow
- Add AGENTS.md with architecture docs and project conventions
- Add phase-based ROADMAP.md (11 phases + stretch goals)
- Link roadmap from README
- Update README Docker tag references to 0.1.0

48 tests passing across 5 test files.
@GeiserX GeiserX force-pushed the phase-1/hardening branch from bac74c9 to a43ebca Compare April 16, 2026 11:41
@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 16, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 16, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 16, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 16, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 16, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ghost.ts (1)

6-14: ⚠️ Potential issue | 🟡 Minor

Add hex validation before Buffer.from(..., "hex") to catch malformed secrets early.

Buffer.from() silently truncates invalid hex characters and odd-length strings instead of throwing. This causes malformed JWTs to be created with opaque auth failures. Add explicit validation before line 13.

Suggested fix
 export function generateJwt(apiKey: string): string {
   const [keyId, secretHex] = apiKey.split(":");
   if (!keyId || !secretHex) {
     throw new Error(
       'Invalid Ghost Admin API key format. Expected "KEY_ID:SECRET_HEX"',
     );
   }
+  if (!/^[0-9a-fA-F]+$/.test(secretHex) || secretHex.length % 2 !== 0) {
+    throw new Error(
+      'Invalid Ghost Admin API key secret. Expected even-length hex in "KEY_ID:SECRET_HEX"',
+    );
+  }

   const secret = Buffer.from(secretHex, "hex");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ghost.ts` around lines 6 - 14, The code currently calls
Buffer.from(secretHex, "hex") which silently truncates malformed hex; before
creating secret, validate secretHex (the part after apiKey.split in
src/ghost.ts) contains only valid hex characters and has even length (e.g.,
/^[0-9a-fA-F]+$/ and length % 2 === 0) and throw a clear Error if validation
fails; update the logic around the keyId/secretHex parsing and the creation of
secret to perform this explicit validation and throw a descriptive "Invalid
Ghost Admin API secret hex" error when checks fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@action.yml`:
- Around line 47-49: The "Generate config overrides" step (id: config) runs from
the repo workspace by default but must run from the action install location so
its node -e blocks can require('yaml'); add working-directory: ${{
github.action_path }} to that step (the step with id: config) so the four node
-e invocations that call require('yaml') resolve the package installed by the
earlier npm ci step.
- Line 51: The condition that checks inputs (including inputs.min-stars) always
triggers because inputs.min-stars has a default of "2"; change the input default
for min-stars to an empty string so the override block only runs when the user
explicitly sets inputs.min-stars; update the action input definition for
min-stars and ensure the code path that assigns portfolio.minStars (the override
block using inputs.min-stars) only executes when inputs.min-stars is non-empty
so it no longer unconditionally overwrites config values.
- Around line 56-63: The inline node -e invocation is vulnerable because it
interpolates inputs directly into a double-quoted JS string; replace direct
interpolation by passing inputs as environment variables and reading them in the
script via process.env (e.g., GHOST_URL, GHOST_PAGE_SLUG, GITHUB_USERNAME,
MIN_STARS) and perform validation/coercion for expected types (string URL
validation for ghost-url, slug pattern for ghost-page-slug, string for
github-username, integer parsing and bounds check for min-stars) before writing
to the YAML at $CONFIG; update the step that sets up the node command to export
these env vars instead of embedding them and adjust the inline script (the node
-e snippet) to read from process.env and validate values.

In `@AGENTS.md`:
- Around line 1-235: The PR modified AGENTS.md which by policy must not be
changed unless explicitly authorized; revert the AGENTS.md file to its prior
state by restoring the file from the main branch (or the commit before this PR),
remove it from the current commit/changeset (unstage/amend or create a revert
commit), update the PR so AGENTS.md shows no diffs, and push the updated branch;
reference AGENTS.md to locate the change.

In `@src/github.ts`:
- Around line 129-137: The urlExists function currently converts any fetch
failure into false; change it so only a 404 yields false and all other failures
surface with context: in urlExists, call fetchWithRetry (the existing call) and
if the response is ok return true, if response.status === 404 return false,
otherwise throw a descriptive Error that includes the URL, response.status, and
a short preview of the response body; also catch exceptions from fetchWithRetry
only to rethrow a new Error with the URL and the original error message so
transient network/DNS/TLS failures are not swallowed.

In `@src/http.ts`:
- Around line 18-59: The current loop calls fetch(url, init) without catching
rejections, so transient transport errors never hit the retry/backoff logic;
wrap the await fetch(url, init) inside a try/catch in the retry loop inside the
same block that uses maxRetries/baseDelay and sleep so that thrown errors are
treated like server errors (call opts?.onRetry with attempt+1 and computed
delay, await sleep, continue) and only escalate after maxRetries; on the final
failure throw a descriptive Error that includes context (URL, attempt count,
error.message and, when available from a non-2xx response, status and a short
body preview) to satisfy the error-reporting guideline.

---

Outside diff comments:
In `@src/ghost.ts`:
- Around line 6-14: The code currently calls Buffer.from(secretHex, "hex") which
silently truncates malformed hex; before creating secret, validate secretHex
(the part after apiKey.split in src/ghost.ts) contains only valid hex characters
and has even length (e.g., /^[0-9a-fA-F]+$/ and length % 2 === 0) and throw a
clear Error if validation fails; update the logic around the keyId/secretHex
parsing and the creation of secret to perform this explicit validation and throw
a descriptive "Invalid Ghost Admin API secret hex" error when checks fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e8f5e9af-7622-4293-9d45-470e0daadfa6

📥 Commits

Reviewing files that changed from the base of the PR and between e04db3f and a43ebca.

📒 Files selected for processing (13)
  • .github/workflows/release.yml
  • AGENTS.md
  • README.md
  • action.yml
  • docs/ROADMAP.md
  • package.json
  • src/ghost.test.ts
  • src/ghost.ts
  • src/github.test.ts
  • src/github.ts
  • src/http.test.ts
  • src/http.ts
  • src/index.ts

Comment thread action.yml
Comment thread action.yml Outdated
Comment thread action.yml Outdated
Comment thread AGENTS.md
Comment on lines +1 to +235
# AGENTS.md - AI Agent Instructions for ghost-github-portfolio

> ⚠️ **IMPORTANT**: Do NOT update this file unless the user explicitly says to. Only the user can authorize changes to AGENTS.md.

> 📦 **RELEASE REMINDER**: npm publishing and Docker push are handled by GitHub Actions on tag push (`v*`). Do NOT run `npm publish` locally. Do NOT create git tags manually. Push a tag and the workflow handles npm + Docker Hub + GitHub Release.

> 🔒 **SECURITY WARNING**: This repository is PUBLIC at [github.com/GeiserX/ghost-github-portfolio](https://github.com/GeiserX/ghost-github-portfolio). **NEVER commit secrets, API keys, Ghost admin tokens, or any sensitive data.** All secrets must go through:
> - GitHub Secrets (for CI/CD and GitHub Action inputs)
> - Environment variables (`GHOST_GITHUB_TOKEN`, `GHOST_ADMIN_API_KEY`)
> - Local `config.yml` (gitignored)

---

## Project Overview

**ghost-github-portfolio** is a CLI tool, Docker image, and GitHub Action that auto-syncs GitHub repositories to a Ghost CMS portfolio page. It fetches repos via the GitHub REST API, sorts by stars, generates HTML cards with dynamic banners and shields.io badges, and updates a Ghost page via the Admin API using the lexical editor format.

- **npm**: `ghost-github-portfolio`
- **Docker Hub**: `drumsergio/ghost-github-portfolio`
- **Repository**: https://github.com/GeiserX/ghost-github-portfolio
- **License**: GPL-3.0

---

## Tech Stack

| Technology | Purpose |
|------------|---------|
| TypeScript | Language (strict mode, ES2022, NodeNext) |
| Node.js 18+ | Runtime (native `fetch`, `crypto`) |
| Commander | CLI framework |
| YAML | Config file parsing |
| Vitest | Test framework |
| Docker | Multi-stage Alpine container |
| GitHub Actions | CI (Node 18/20/22 matrix), Release (npm + Docker + GH Release) |

**Zero external HTTP dependencies** - uses only Node.js native `fetch` and `crypto`. No axios, no node-fetch, no jsonwebtoken.

---

## Architecture

```
src/
├── index.ts # CLI entry point (commander: sync + init commands)
├── config.ts # YAML config loader, defaults, env var overrides, validation
├── github.ts # GitHub REST API: fetch all repos (paginated), sort client-side, detect banners via HEAD
├── ghost.ts # Ghost Admin API: JWT generation (HS256), fetch page, update page (lexical format)
├── generator.ts # HTML card generation: banners, badges, footer, lexical document builder
└── types.ts # TypeScript interfaces: Config, GitHubRepo, LexicalDocument, CustomBadge
```

### Key Design Decisions

1. **Client-side star sorting**: GitHub REST API `/users/{user}/repos` does NOT support `sort=stars`. All pages are fetched, then sorted in memory. Do NOT add `sort=stars` to the API URL - it is an invalid parameter that causes unpredictable ordering.

2. **`GHOST_GITHUB_TOKEN` only**: The env var is intentionally NOT `GITHUB_TOKEN`. The standard `GITHUB_TOKEN` env var is often set by CI runners or `gh` CLI and may hold tokens with wrong scopes. Using a dedicated name avoids accidental token pickup.

3. **Ghost lexical format**: Ghost uses lexical (NOT mobiledoc) for its editor. The document is a JSON AST with `html` nodes and `horizontalrule` nodes. See `types.ts` for the schema.

4. **JWT authentication**: Ghost Admin API uses HS256 JWT with the key ID in the `kid` header field. The secret is hex-decoded. Tokens expire in 5 minutes. Implementation is in `ghost.ts` using only `node:crypto`.

5. **Banner detection**: Checks multiple candidate paths via HEAD requests to `raw.githubusercontent.com`. Config overrides take priority, then default path, then candidates list. All checks are parallel per repo.

6. **Dynamic badges**: All shields.io badges are live URLs — stars, forks, Docker pulls update on every page view without re-running the tool. Only the page structure (which repos, order, banners) requires re-syncing.

---

## CI/CD

### Workflows

| Workflow | Trigger | What it does |
|----------|---------|--------------|
| `ci.yml` | Push to main, PRs | Build + lint + test on Node 18/20/22; Docker build + verify |
| `release.yml` | Tag push `v*` | npm publish, Docker multi-arch build (amd64+arm64) to Docker Hub, GitHub Release |
| `stale.yml` | Daily schedule | Auto-close stale issues (14d stale + 14d close) |

### Release Process

```bash
# 1. Bump version in package.json
npm version minor --no-git-tag-version # or patch/major

# 2. Commit
git add package.json package-lock.json
git commit -m "feat: description of changes"

# 3. Create and push tag
git tag v1.1.0
git push origin main --tags

# 4. release.yml handles: npm publish + Docker push + GH Release
```

**NEVER** run `npm publish` locally or create GitHub Releases manually.

---

## Code Conventions

### General Rules
- TypeScript strict mode (`strict: true` in tsconfig)
- ESM modules (`"type": "module"` in package.json)
- All imports use `.js` extension (NodeNext resolution)
- No external HTTP libraries — native `fetch` only
- No JWT libraries — manual HS256 via `node:crypto`
- Tests use Vitest with `.test.ts` suffix
- Config file is YAML (not JSON, not TOML)

### File Naming
- Source: `src/*.ts` (flat structure, no nested directories)
- Tests: `src/*.test.ts` (co-located with source)
- Config: `config.yml` (gitignored, user-provided)

### Error Handling
- Throw descriptive `Error` with context (API status, body preview)
- CLI catches at top level and exits with code 1
- No silent failures — banner detection returns `null` on failure, but HTTP errors throw

### HTML Generation
- All user-provided strings go through `escapeHtml()` (XSS prevention)
- Badges use shields.io dynamic URLs (not static SVGs)
- Cards use inline styles (Ghost strips `<style>` blocks and classes)
- Ghost lexical format requires specific node types — do NOT invent new node types

---

## Config Schema

The config YAML has three top-level keys:

```yaml
github:
username: string # Required
token: string # Optional (env: GHOST_GITHUB_TOKEN)

ghost:
url: string # Required (trailing slash stripped)
adminApiKey: string # Required, format "KEY_ID:SECRET_HEX" (env: GHOST_ADMIN_API_KEY)
pageId: string # One of pageId or pageSlug required
pageSlug: string # One of pageId or pageSlug required

portfolio: # All optional, has defaults
minStars: 2
maxRepos: 50
includeForked: false
badgeStyle: for-the-badge
showBanner: true
centerContent: true
defaultBannerPath: docs/images/banner.svg
bannerPaths: {} # repo-name: path overrides
excludeRepos: []
repos: {} # Per-repo overrides (description, dockerImage, badges, techStack, keyFeatures)
footer:
showStats: true
showViewAll: true
```

---

## Testing

```bash
npm test # Run all tests (vitest)
npm run test:watch # Watch mode
```

### Test Files
- `src/config.test.ts` — Config loading, defaults, validation errors
- `src/generator.test.ts` — Card generation, badges, banners, centering, escaping, footer, lexical structure
- `src/ghost.test.ts` — JWT structure, header kid, payload aud, signature verification

### What to Test
- Config validation (missing fields throw descriptive errors)
- Badge generation for all types (stars, forks, license, docker, website, awesome-list, custom)
- HTML escaping of user-provided content
- Lexical document structure matches Ghost's expected format
- Banner URL construction with overrides

---

## Docker

Multi-stage build:
1. **Builder**: `node:22-alpine`, `npm ci`, `tsc`
2. **Runtime**: `node:22-alpine`, production deps only, copies `dist/`

```bash
# Build
docker build -t ghost-github-portfolio .

# Run
docker run --rm -v /path/to/config.yml:/config/config.yml ghost-github-portfolio
```

Entrypoint is `node dist/index.js`, default CMD is `sync --config /config/config.yml`.

---

## GitHub Action

Composite action that installs Node 22, builds from source, and runs sync. Inputs:
- `config-path` (default: `config.yml`)
- `ghost-url`, `ghost-admin-api-key`, `ghost-page-slug` (override config)
- `github-username`, `min-stars`

---

## Common Pitfalls

1. **Ghost redirects to canonical URL**: Ghost 301-redirects all API requests to its configured canonical URL. Always use the public Ghost URL, not localhost.
2. **`updated_at` concurrency**: Ghost uses optimistic concurrency. The PUT request must include the current `updated_at` from a fresh GET. Stale values cause 409 errors.
3. **GitHub pagination**: The API returns max 100 repos per page. Must loop until `repos.length < perPage`. Never assume a single page is enough.
4. **Inline styles only**: Ghost strips CSS classes and `<style>` tags. All styling must use inline `style=""` attributes.
5. **Banner HEAD requests**: `raw.githubusercontent.com` returns 404 for missing files — no error page. HEAD requests are cheap and reliable for detection.

---

## Checklist for AI Agents

Before completing a task, verify:

- [ ] TypeScript compiles without errors (`npm run lint`)
- [ ] All tests pass (`npm test`)
- [ ] No secrets in code or config files
- [ ] HTML output uses `escapeHtml()` for user content
- [ ] Ghost lexical format is valid (html + horizontalrule nodes only)
- [ ] Changes work with Node 18, 20, and 22
- [ ] Docker build succeeds
- [ ] README updated if public API changed

---

*Generated by [LynxPrompt](https://lynxprompt.com) CLI*
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Revert AGENTS.md from this PR.

This repository explicitly disallows modifying AGENTS.md unless the user asked for it, so this file should not ship as part of the change set. As per coding guidelines, "AGENTS.md: Do NOT update AGENTS.md unless the user explicitly authorizes changes".

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 4-4: Blank line inside blockquote

(MD028, no-blanks-blockquote)


[warning] 6-6: Blank line inside blockquote

(MD028, no-blanks-blockquote)


[warning] 43-43: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 1 - 235, The PR modified AGENTS.md which by policy
must not be changed unless explicitly authorized; revert the AGENTS.md file to
its prior state by restoring the file from the main branch (or the commit before
this PR), remove it from the current commit/changeset (unstage/amend or create a
revert commit), update the PR so AGENTS.md shows no diffs, and push the updated
branch; reference AGENTS.md to locate the change.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENTS.md was explicitly requested by the repo owner as part of this PR. Not reverting.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GeiserX, understood — since you're the repo owner and have explicitly authorized this addition, the AGENTS.md file is valid as part of this PR. I'll note this for future reviews.

( ˶ᵔ ᵕ ᵔ˶ )∫

Comment thread src/github.ts Outdated
Comment on lines 129 to 137
async function urlExists(url: string): Promise<boolean> {
try {
const res = await fetch(url, { method: "HEAD", redirect: "follow" });
const res = await fetchWithRetry(url, {
method: "HEAD",
redirect: "follow",
});
return res.ok;
} catch {
return false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not turn every banner-fetch failure into “not found”.

A rejected request at Line 131 is swallowed at Line 136 and converted into false, so transient DNS/TLS/network errors after all retries become indistinguishable from a real 404 and quietly drop the banner. Only treat 404 as "missing"; unexpected failures should throw with context.

🛠️ Proposed fix
 async function urlExists(url: string): Promise<boolean> {
-  try {
-    const res = await fetchWithRetry(url, {
-      method: "HEAD",
-      redirect: "follow",
-    });
-    return res.ok;
-  } catch {
-    return false;
-  }
+  const res = await fetchWithRetry(url, {
+    method: "HEAD",
+    redirect: "follow",
+  });
+  if (res.status === 404) return false;
+  if (!res.ok) {
+    throw new Error(`Banner HEAD failed for ${url}: ${res.status}`);
+  }
+  return true;
 }

As per coding guidelines, "src/github.ts: Banner detection via HEAD requests to raw.githubusercontent.com must handle 404 responses; prioritize config overrides, then default path, then candidates list" and "**/*.ts: Throw descriptive Error with context (API status, body preview) for debugging; do NOT fail silently".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/github.ts` around lines 129 - 137, The urlExists function currently
converts any fetch failure into false; change it so only a 404 yields false and
all other failures surface with context: in urlExists, call fetchWithRetry (the
existing call) and if the response is ok return true, if response.status === 404
return false, otherwise throw a descriptive Error that includes the URL,
response.status, and a short preview of the response body; also catch exceptions
from fetchWithRetry only to rethrow a new Error with the URL and the original
error message so transient network/DNS/TLS failures are not swallowed.

Comment thread src/http.ts Outdated
GeiserX added 2 commits April 16, 2026 14:06
…nputs

- http.ts: wrap fetch() in try/catch so network errors (ECONNRESET, etc.)
  are retried with exponential backoff instead of failing immediately
- github.ts: urlExists now distinguishes 404 from unexpected HTTP errors;
  detectBanner catches persistent errors gracefully
- action.yml: replace unsafe string interpolation with env vars + process.env
  to prevent code injection; add working-directory for yaml module resolution;
  change min-stars default to empty string to avoid overriding config
- Added 2 new tests for network error retry behavior (50 total)
Move Ghost and GitHub icons above the title text so they don't overlap.
Replace phantom ghost shape with official Ghost CMS-style logo (rounded
dome, wavy bottom, friendly eyes). Icons are now clearly separated with
a connector arrow between them.
@GeiserX GeiserX merged commit 0ce3615 into main Apr 16, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant