Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ jobs:
- name: Run fixture demo
run: node dist/index.js demo

- name: Exercise deployment adapter
run: |
node dist/index.js deploy --data data --output output --target "$RUNNER_TEMP/deploy-target"
test -f "$RUNNER_TEMP/deploy-target/index.html"

- name: Store demo output
if: success()
uses: actions/upload-artifact@v4
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ on:
schedule:
- cron: "17 5 * * 1"
workflow_dispatch:
inputs:
deploy_target:
description: Local deploy target directory (optional)
required: false
type: string
default: ""

permissions:
contents: read
Expand Down Expand Up @@ -42,3 +48,16 @@ jobs:
name: engineer-profile-refresh
path: output/
if-no-files-found: error

- name: Deploy to requested target
if: inputs.deploy_target != ''
run: |
node dist/index.js deploy --data data --output output --target "${{ inputs.deploy_target }}"
test -d "${{ inputs.deploy_target }}"

- name: Store deployed target
if: inputs.deploy_target != ''
uses: actions/upload-artifact@v4
with:
name: engineer-profile-deploy
path: ${{ inputs.deploy_target }}
11 changes: 10 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@ npm test
Use fixtures for changes that need repeatable data.
Do not add credentials, private repository data, or generated output.

For changes to themes, add or update tests in `tests/theme.test.ts`.
For changes to deployment, add or update tests in `tests/deploy.test.ts`.
Verify the deploy path with a temporary target:

```bash
node dist/index.js deploy -d data -o output -t <target-dir>
```

## Pull requests

Explain the user value and the data path.
List the checks that you ran.
Keep public claims tied to repository evidence.
Keep public claims tied to repository evidence.
Do not invent users, adoption numbers, or benchmarks.
85 changes: 71 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
# EngineerProfile

EngineerProfile builds a local engineering portfolio from public repository data.
It stores repository metadata, commits, releases, privacy settings, and preview
paths in SQLite. It publishes a static site from these records.
It stores repository metadata, commits, releases, privacy settings, and preview paths in SQLite.
It publishes a static site from these records.
You select a built-in theme.
You choose a deployment adapter for the finished site.

## Value

- Keep portfolio facts close to their source data.
- Refresh project cards from public GitHub repositories.
- Build release notes from releases or conventional commits.
- Capture repeatable project previews with Playwright.
- Select a validated site theme from the built-in registry.
- Copy the published site to a local target with an adapter.
- Hide projects and redact author emails before publication.
- Run one configured refresh from a scheduled workflow.

Expand All @@ -30,20 +34,24 @@ flowchart LR
L --> W[Publisher]
P --> W
S --> W
T[Theme] --> W
W --> O[Static output]
O --> E[Deploy]
```

| Area | Responsibility |
| --- | --- |
| `engineer-profile.config.json` | Store owner, presentation, refresh, paths, and privacy settings. |
| `engineer-profile.config.json` | Store owner, presentation, theme, deploy, refresh, paths, and privacy settings. |
| `src/config/` | Validate checked-in JSON and merge safe defaults. |
| `src/refresh/` | Coordinate ingest, best-effort capture, and static publishing. |
| `src/theme/` | Select a validated site theme from the built-in registry. |
| `src/deploy/` | Copy the published site to a configured target. |
| `src/refresh/` | Coordinate ingest, best-effort capture, publishing, and deploy. |
| `src/ingest/` | Fetch public GitHub data and map it to records. |
| `src/db/` | Store projects, commits, changelogs, and audit events. |
| `src/changelog/` | Prefer release notes and fall back to commit groups. |
| `src/privacy/` | Hide projects and block sensitive commit messages. |
| `src/preview/` | Capture fixed viewport screenshots with Playwright. |
| `src/publish/` | Render HTML, changelog files, and preview assets. |
| `src/publish/` | Render themed HTML, changelog files, and preview assets. |
| `fixtures/` | Provide deterministic demo data and local preview pages. |

The refresh command runs each stage in a fixed order.
Expand All @@ -68,7 +76,7 @@ Both directories are ignored by Git.
## Configuration

`engineer-profile.config.json` is the checked-in source for scheduled refreshes.
It sets the GitHub owner, site presentation, repository limit, paths, and privacy controls.
It sets the GitHub owner, site presentation, repository limit, theme, deploy, and privacy controls.

The loader accepts repository limits from 1 through 100.
It rejects malformed values before network access.
Expand All @@ -80,8 +88,7 @@ Run a network-backed refresh with the checked-in settings:
npm run refresh
```

The refresh command reads public repositories, captures previews, publishes HTML,
and reports skipped captures.
The refresh command reads public repositories, captures previews, publishes HTML, and reports skipped captures.

GitHub ingestion uses the public API.
Set `GITHUB_TOKEN` for a higher rate limit.
Expand All @@ -94,6 +101,44 @@ npm run refresh
Do not put a token in repository files.
Use `.env.example` as a variable reference.

## Themes

The site ships with three built-in themes.
Each theme defines one consistent set of color tokens.

| Theme | Look |
| --- | --- |
| `aurora` | Dark blue with teal and amber accents. This is the default. |
| `terminal` | Green on black for low-light reading. |
| `paper` | Light theme with dark ink text. |

Select a theme with the `theme` field.
The loader rejects unknown theme names before network access.

## Deployment

Adapters copy the published site to a destination.
The `none` adapter is the default.
It does nothing.
The `local` adapter mirrors the output directory into a target.

Set the adapter and target in the configuration.
Run `npm run deploy` after `npm run publish`.
A refresh runs the adapter automatically when one is configured.

```json
{
"theme": "paper",
"deploy": {
"adapter": "local",
"targetDir": "site"
}
}
```

The deploy step replaces the target contents with a fresh copy.
It rejects targets that overlap the output directory.

## Sample output

The fixture set contains `signal-router` and `metrics-kit`.
Expand All @@ -104,11 +149,18 @@ The second project uses commit-based notes.
Ingested 2 fixture projects.
Captured demo-engineer-signal-router.
Captured demo-engineer-metrics-kit.
Published 2 projects to output/index.html.
Published 2 projects with the aurora theme to output/index.html.
Copied 2 available preview screenshots.
Open output/index.html in a browser.
```

Point the local adapter at a target directory.
Run the deploy command after publish.

```text
Deployed 5 files with the local adapter to site.
```

The site shows project facts, source links, changelog previews, and screenshots.
The totals come from fixture fields and stored commit records.

Expand All @@ -123,7 +175,8 @@ Build before direct CLI commands.
| `npm run ingest -- --fixture` | Load fixture records only. |
| `npm run capture -- --fixture` | Capture local fixture pages. |
| `npm run publish` | Rebuild the site from SQLite. |
| `npm run refresh` | Run configured ingest, capture, and publish stages. |
| `npm run deploy` | Copy the site to the configured target. |
| `npm run refresh` | Run configured ingest, capture, publish, and deploy stages. |
| `node dist/index.js status` | Show visibility and recent operations. |
| `npm test` | Run deterministic unit and integration tests. |
| `npm run typecheck` | Validate TypeScript types. |
Expand Down Expand Up @@ -153,13 +206,14 @@ Each release keeps its tag, notes, date, and source URL.
The site displays visible projects only.
It links project cards to repositories.
It links release notes to their release pages.
It records local operations in an audit table.
It records local operations, including deploys, in an audit table.

## CI and test status

The regular CI workflow runs typecheck, build, tests, the fixture demo, and artifact upload.
The regular CI workflow runs typecheck, build, tests, the fixture demo, a deploy check, and artifact upload.
The scheduled refresh workflow runs each Monday and supports manual dispatch.
It uploads the generated site as a workflow artifact.
Manual dispatch can request a local deploy target.

The test suite covers these core behaviors:

Expand All @@ -168,6 +222,8 @@ The test suite covers these core behaviors:
- Release-first changelog generation.
- SQLite upserts and changelog replacement.
- Privacy filtering and email redaction.
- Theme registry and theme publishing.
- Deployment adapters and audit logging.
- Fixture ingestion and static publishing.
- Configured refresh orchestration.
- Release source links.
Expand Down Expand Up @@ -197,6 +253,7 @@ The fixture pipeline provides deterministic data for repeatable checks.
- External pages can fail during capture.
- Capture failures are reported and do not stop publishing.
- Publishing creates local files. It does not deploy them.
- The local adapter copies to one directory. It does not push to a remote host.
- Scheduled runs upload artifacts. They do not commit generated output.

## Roadmap
Expand All @@ -205,8 +262,8 @@ The fixture pipeline provides deterministic data for repeatable checks.
| --- | --- | --- |
| v0.1 | Complete | Fixture demo, GitHub ingest, changelog, capture, publish, and privacy controls. |
| v0.2 | Complete | Checked-in configuration, coordinated refresh command, and scheduled artifact workflow. |
| v0.3 | Next | Custom themes and deployment adapters. |
| v0.4 | Later | Commit-diff summaries and an RSS feed. |
| v0.3 | Complete | Built-in themes and a local deployment adapter. |
| v0.4 | Next | Commit-diff summaries and an RSS feed. |

## License

Expand Down
4 changes: 4 additions & 0 deletions engineer-profile.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"title": "EngineerProfile",
"tagline": "A living index of shipped systems, maintained from repository evidence",
"repositoryLimit": 5,
"theme": "aurora",
"deploy": {
"adapter": "none"
},
"dataDir": "data",
"outputDir": "output",
"privacy": {
Expand Down
6 changes: 4 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"name": "engineer-profile",
"version": "0.2.0",
"version": "0.3.0",
"description": "Self-maintaining engineering portfolio from repository activity",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"bin": {
"engineer-profile": "dist/index.js"
},
Expand All @@ -18,6 +22,7 @@
"ingest": "npm run build && node dist/index.js ingest",
"publish": "npm run build && node dist/index.js publish",
"capture": "npm run build && node dist/index.js capture",
"deploy": "npm run build && node dist/index.js deploy",
"refresh": "npm run build && node dist/index.js refresh"
},
"engines": {
Expand Down
41 changes: 39 additions & 2 deletions src/config/loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { readFileSync } from "node:fs";
import type { PortfolioConfig, PrivacyConfig } from "../types.js";
import { DEFAULT_CONFIG, DEFAULT_PRIVACY } from "../types.js";
import type { DeploymentConfig, PortfolioConfig, PrivacyConfig } from "../types.js";
import { DEFAULT_CONFIG, DEFAULT_DEPLOY, DEFAULT_PRIVACY } from "../types.js";
import { mergePrivacy } from "../privacy/controls.js";
import { isKnownAdapter, listAdapters } from "../deploy/adapters.js";
import { isKnownTheme, listThemeNames } from "../theme/registry.js";

export const DEFAULT_CONFIG_PATH = "engineer-profile.config.json";

Expand Down Expand Up @@ -29,6 +31,39 @@ function readLimit(source: ConfigValue, key: string, fallback: number): number {
return value;
}

function readTheme(source: ConfigValue, fallback: string): string {
if (!("theme" in source)) return fallback;
const value = source.theme;
if (typeof value !== "string" || value.trim() === "") {
throw new Error('Configuration field "theme" must be a non-empty string.');
}
const name = value.trim();
if (!isKnownTheme(name)) {
throw new Error(`Configuration field "theme" must be one of: ${listThemeNames().join(", ")}.`);
}
return name;
}

function readDeploy(source: ConfigValue): DeploymentConfig {
if (!("deploy" in source)) return DEFAULT_DEPLOY;
if (!isConfigValue(source.deploy)) {
throw new Error('Configuration field "deploy" must be an object.');
}

const { adapter, targetDir } = source.deploy;
if (typeof adapter !== "string" || !isKnownAdapter(adapter)) {
throw new Error(`Configuration field "deploy.adapter" must be one of: ${listAdapters().join(", ")}.`);
}
if (targetDir !== undefined && (typeof targetDir !== "string" || targetDir.trim() === "")) {
throw new Error('Configuration field "deploy.targetDir" must be a non-empty string.');
}

return {
adapter: adapter as DeploymentConfig["adapter"],
targetDir: targetDir === undefined ? undefined : targetDir.trim(),
};
}

function readPrivacy(source: ConfigValue): PrivacyConfig {
if (!("privacy" in source)) return DEFAULT_PRIVACY;
if (!isConfigValue(source.privacy)) {
Expand Down Expand Up @@ -78,6 +113,8 @@ export function loadPortfolioConfig(
repositoryLimit: readLimit(parsed, "repositoryLimit", DEFAULT_CONFIG.repositoryLimit),
dataDir: readString(parsed, "dataDir", DEFAULT_CONFIG.dataDir),
outputDir: readString(parsed, "outputDir", DEFAULT_CONFIG.outputDir),
theme: readTheme(parsed, DEFAULT_CONFIG.theme),
deploy: readDeploy(parsed),
privacy: readPrivacy(parsed),
clock,
};
Expand Down
Loading
Loading