feat: Phase 1 hardening — retry, rate limits, tests, docs#8
Conversation
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
1 similar comment
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
1 similar comment
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
- 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.
bac74c9 to
a43ebca
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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 | 🟡 MinorAdd 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
📒 Files selected for processing (13)
.github/workflows/release.ymlAGENTS.mdREADME.mdaction.ymldocs/ROADMAP.mdpackage.jsonsrc/ghost.test.tssrc/ghost.tssrc/github.test.tssrc/github.tssrc/http.test.tssrc/http.tssrc/index.ts
| # 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* |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
AGENTS.md was explicitly requested by the repo owner as part of this PR. Not reverting.
There was a problem hiding this comment.
@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.
( ˶ᵔ ᵕ ᵔ˶ )∫
| 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; |
There was a problem hiding this comment.
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.
…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.
Summary
Phase 1 hardening for ghost-github-portfolio, covering reliability, testing, documentation, and fixes.
Changes
Reliability
x-ratelimit-remaining: 0, waits for reset, retries automaticallyTesting (26 → 48 tests)
github.test.ts— 13 tests covering sort, filter, pagination, exclude, banner detectionhttp.test.ts— 8 tests covering retry logic, backoff, rate limit handling, passthroughghost.test.ts— imports realgenerateJwtinstead of duplicating it, adds invalid key testFixes
generateJwtexported fromghost.ts(was private, duplicated in tests)action.ymlinputs now wired (ghost-url, page-slug, username, min-stars were declared but unused):latestDocker tag from release workflowDocumentation
AGENTS.md— architecture docs, design decisions, CI/CD guide, code conventions, config schema, pitfallsdocs/ROADMAP.md— phase-based roadmap (11 phases + stretch goals, ~100 items)New files
src/http.ts— fetchWithRetry, parseRateLimitHeaderssrc/http.test.ts— 8 testssrc/github.test.ts— 13 testsAGENTS.mddocs/ROADMAP.mdTest plan
npm run lintpassesnpm run buildpassesnpm testpasses (48/48)Summary by CodeRabbit
New Features
Documentation
Chores
Tests