Skip to content
Merged
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
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI

on:
pull_request:
branches: [main]

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint:js
- run: npm run lint:md
- run: npm run typecheck

unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:unit

build-and-html-lint:
name: Build and HTML Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- run: npm run lint:html

accessibility-e2e:
name: Accessibility and E2E Tests
runs-on: ubuntu-latest
needs: build-and-html-lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run build
- run: npm run test:e2e
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules/
dist/
.astro/
.wrangler/
.env
.env.*
!.env.example
playwright-report/
test-results/
coverage/
3 changes: 3 additions & 0 deletions .htmlvalidate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["html-validate:recommended", "html-validate:a11y"]
}
9 changes: 9 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"config": {
"default": true,
"MD013": false,
"MD033": false
},
"globs": ["**/*.md"],
"ignores": ["node_modules/**", "dist/**"]
}
46 changes: 31 additions & 15 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ team-maintained commercial product.
Tech-Clusive Solutions, LLC.
* Primary contributor: Jad Wauthier (the Blind Tech Mage).
* GitHub account: BlindTechMage.
* Contact email: info@BlindTechMage.com.
* Contact email: <info@BlindTechMage.com>.
* SSH key for GitHub operations: `C:/Users/jwauthier/.ssh/btm_cynthus_ed25519`.

## Working Agreement
Expand Down Expand Up @@ -68,17 +68,21 @@ below) rather than following a full formal field checklist.
requests. Merge instead requires an explicit verbal "proceed" from the primary
contributor in conversation, given each time, regardless of CI status.
* No semantic-versioning or release-branch machinery. This is a live site, not a
versioned distributed product. Deploys happen on merge to `main` via Cloudflare
Pages' automatic deployment.
versioned distributed product. Deploys happen on merge to `main` via a
dedicated deploy workflow that runs `wrangler deploy` (see Environmental
Conditions — this project deploys to Cloudflare Workers with static assets,
not classic Cloudflare Pages, which does not have the same git-integration
auto-deploy Pages offered; deploy automation is a separate, not-yet-built CI
workflow).

## CI Policy

* A GitHub Actions workflow runs on every pull request targeting `main`, executing:
- Linting for Markdown, JavaScript/TypeScript, and HTML.
- Unit tests for all executable/functional code, regardless of language
* Linting for Markdown, JavaScript/TypeScript, and HTML.
* Unit tests for all executable/functional code, regardless of language
(TypeScript, Python, or anything else introduced later). Every function or
module implementing real behavior must have a corresponding unit test that
verifies its functionality. This applies to Pages Functions/Worker logic (e.g.
verifies its functionality. This applies to Cloudflare Worker route logic (e.g.
the contact form handler, resource search/filter API) as much as to frontend
code. A pull request introducing functional code with no corresponding test is
considered incomplete.
Expand Down Expand Up @@ -126,7 +130,7 @@ below) rather than following a full formal field checklist.
* A linter is configured from the start for each language in use (ESLint for
TypeScript/JavaScript, markdownlint for Markdown, an HTML linter/validator), run
locally and enforced in CI.
* TypeScript is used for all functional/executable code — Pages Functions/Worker
* TypeScript is used for all functional/executable code — Cloudflare Worker route
logic, interactive frontend components — not plain JavaScript.
* Every first-order module/file has exactly one primary responsibility, unless
additional code is a directly related helper or configuration type.
Expand All @@ -146,15 +150,25 @@ below) rather than following a full formal field checklist.

## Environmental Conditions

* Target platform: Cloudflare Pages (static frontend) plus Cloudflare Pages
Functions/Workers (TypeScript) plus D1 (SQLite-compatible database).
* Target platform: Cloudflare Workers with static assets (via
`@astrojs/cloudflare`, which targets Workers rather than classic Cloudflare
Pages), plus D1 (SQLite-compatible database). This is a correction from an
earlier assumption that the platform would be classic Cloudflare Pages —
the Astro adapter's current majors generate a Workers-style deployment
(`wrangler.toml` with an `[assets]` binding), not a Pages-style one. The
practical effect is the same (Cloudflare-hosted, TypeScript route handlers,
D1-backed), but deployment is via `wrangler deploy` in CI rather than Pages'
built-in git-integration auto-deploy.
* Frontend framework: Astro, chosen for its island-based interactivity model, which
fits a mostly-static site with a small number of interactive pages (resource
search/filter).
search/filter). The site builds in Astro's `server` output mode (required for
Worker route/API handling), with individual static content pages opted into
prerendering (`export const prerender = true`) so they build to plain HTML
rather than being server-rendered per request.
* Language: TypeScript for all functional/executable code.
* No self-managed server, no OS/runtime matrix beyond what Cloudflare's platform
dictates. The previously used server (`fornax.techclusivesolutions.com`) is
retired from serving this site once cutover to Cloudflare Pages is complete.
retired from serving this site once cutover to Cloudflare Workers is complete.
* Credential storage uses Cloudflare's native secrets/bindings mechanism. Never
plaintext, never committed.
* Any dependency that is unmaintained, deprecated, or otherwise at risk is flagged
Expand Down Expand Up @@ -201,7 +215,9 @@ Plan → Design → Track → Implement → Test process before work begins in t
blindness-related resources, project details) in D1.
* Contact form implementation details (delivery mechanism, spam/abuse mitigation).
* Cloudflare Access configuration for any admin/write-gated routes.
* DNS cutover from `fornax.techclusivesolutions.com` to Cloudflare Pages — decided
in principle, but not yet executed pending a working deployment to point to. This
requires explicit confirmation before being carried out, as a live change to a
working domain.
* A deploy workflow (GitHub Actions running `wrangler deploy` on merge to `main`,
using a Cloudflare API token stored as a repository secret) — not yet built.
* DNS cutover from `fornax.techclusivesolutions.com` to the deployed Cloudflare
Worker — decided in principle, but not yet executed pending a working deployment
to point to. This requires explicit confirmation before being carried out, as a
live change to a working domain.
9 changes: 9 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
output: 'server',
adapter: cloudflare({
imageService: 'cloudflare',
}),
});
14 changes: 7 additions & 7 deletions docs/ACCESSIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,21 @@ that no feature bypasses.

## Specific Requirements Carried From Project Discussion

* No custom-styled interactive control (dropdowns, filter panels, etc.) built as
- No custom-styled interactive control (dropdowns, filter panels, etc.) built as
divs pretending to be native form controls. Real semantic elements (`<select>`,
checkboxes, buttons) are used, styled as needed, rather than reimplemented.
* Dynamic content updates (e.g. resource search/filter results changing without a
- Dynamic content updates (e.g. resource search/filter results changing without a
page reload) are announced to screen reader users via an ARIA live region.
* Any element that triggers navigation must be a real link (`<a href>`); any
- Any element that triggers navigation must be a real link (`<a href>`); any
element that triggers an in-page action must be a real button. Interactive
elements must not carry mismatched semantics (e.g. a `<button>` used to navigate
to another page via an `href` attribute, which is invalid and was a defect
identified in the prior version of this site).
* Text content must not contain encoding artifacts (mojibake) — verified as part of
- Text content must not contain encoding artifacts (mojibake) — verified as part of
normal QA, not just visually but by inspecting rendered text output, since
visual review alone can miss characters that render acceptably in one font but
fail elsewhere.
* No link points to a placeholder destination (`href="#"` with no real target) in
- No link points to a placeholder destination (`href="#"` with no real target) in
shipped content.

## Possible Future Content Element
Expand All @@ -58,8 +58,8 @@ content/IA decision, not yet finalized — see `docs/PRD.md`, Open Items.

## Open Items

* Whether any component will target AAA specifically, and which.
* Full accessibility test checklist / acceptance-criteria template for feature
- Whether any component will target AAA specifically, and which.
- Full accessibility test checklist / acceptance-criteria template for feature
issues (each feature issue's acceptance criteria should include at least one
accessibility-focused scenario, per the lightweight issue tracking approach in
`PROJECT.md`).
49 changes: 28 additions & 21 deletions docs/CI_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ This document consolidates the CI and testing requirements established in

## What Runs, and When

A single GitHub Actions workflow runs on every pull request targeting `main`. It
executes, in this order:

1. Linting:
- Markdown (markdownlint or equivalent).
- JavaScript/TypeScript (ESLint).
- HTML (an HTML linter/validator).
2. Accessibility:
- axe-core, run against the built site output.
3. Unit tests:
- All executable/functional code, regardless of language.
A single GitHub Actions workflow (`.github/workflows/ci.yml`) runs on every pull
request targeting `main`, as four parallel-where-possible jobs:

1. **Lint** — ESLint (JS/TS/Astro), markdownlint-cli2 (Markdown), and a TypeScript
typecheck (`tsc --noEmit`).
2. **Unit Tests** — Vitest, covering all executable/functional code regardless of
language (Cloudflare Worker route logic first; any other language introduced
later is added as its own job/step).
3. **Build and HTML Lint** — Astro build (`npm run build`), then html-validate run
against the built `dist/client/` output, since that is the actual shipped
markup, not the source templates.
4. **Accessibility and E2E Tests** — Playwright, with `@axe-core/playwright`
run against the built site (via `npm run preview`) for every page under test.
Depends on the build job so it always tests real built output.

No workflow runs on push to `main`. Direct pushes to `main` are disabled at the
repository level, so every change reaching `main` has already passed this workflow
Expand All @@ -28,7 +31,7 @@ at pull-request time. A duplicate post-merge run would be redundant.
Every function or module implementing real behavior must have a corresponding unit
test that verifies its functionality. This applies to:

* TypeScript logic in Cloudflare Pages Functions/Workers (contact form handler,
* TypeScript logic in Cloudflare Worker routes (contact form handler,
resource search/filter API, any admin-route logic).
* Any interactive frontend component with non-trivial logic (e.g. the resource
search/filter UI's client-side behavior).
Expand All @@ -53,15 +56,19 @@ project's scope grows significantly.

## Accessibility Testing Note

axe-core results in CI are a floor, not proof of compliance. Manual verification
(keyboard-only navigation, a JAWS pass) is still required before a feature is
considered done, per `docs/ACCESSIBILITY.md`. CI does not and cannot enforce the
manual verification step — it is a process requirement tracked via the working
agreement, not a tooling gate.
`@axe-core/playwright` results in CI are a floor, not proof of compliance. Manual
verification (keyboard-only navigation, a JAWS pass) is still required before a
feature is considered done, per `docs/ACCESSIBILITY.md`. CI does not and cannot
enforce the manual verification step — it is a process requirement tracked via the
working agreement, not a tooling gate. Playwright's browser-driven testing does
additionally allow interaction-based accessibility checks (e.g. verifying live
region announcements after a client-side filter action), not just static-markup
scanning.

## Open Items

* Specific linter configurations (rule sets) for ESLint, markdownlint, and the HTML
validator — to be established when CI is actually scaffolded.
* Test framework selection for TypeScript (e.g. Vitest) — to be decided when the
first functional code (contact form handler) is built.
* Linter rule-set tuning (ESLint, markdownlint, html-validate) beyond the
recommended defaults currently configured — to be refined as real content and
code are added.
* Expanding the Playwright accessibility suite to cover new pages/features as they
are built (currently covers only the placeholder home page).
8 changes: 4 additions & 4 deletions docs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ is a separate design decision to be made and confirmed before implementation.

## Functional Requirements (established so far)

* A contact form, backed by a TypeScript Pages Function.
* A contact form, backed by a TypeScript Cloudflare Worker route.
* Domain-specific, searchable/filterable resource tables, stored in Cloudflare D1,
each in its own table (not a single shared table across domains):
- Ham radio resources.
- Blindness-related resources.
- Project-specific details.
* Ham radio resources.
* Blindness-related resources.
* Project-specific details.
* Public tables are queryable via a public API route and browsable/searchable on
corresponding site pages, filterable by fields appropriate to each domain (e.g.
category, tags via a join table rather than a delimited string column).
Expand Down
6 changes: 3 additions & 3 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ relying on a filter to exclude private rows.

* The contact form is a public, unauthenticated endpoint and is treated as an
abuse surface. Requirements:
- Server-side input validation on all fields.
- Rate limiting to prevent bulk submission abuse.
- Basic bot mitigation (e.g. a honeypot field or equivalent low-friction
* Server-side input validation on all fields.
* Rate limiting to prevent bulk submission abuse.
* Basic bot mitigation (e.g. a honeypot field or equivalent low-friction
measure) — full CAPTCHA is avoided if possible, given accessibility concerns
with CAPTCHA and this site's accessibility positioning; if a bot-mitigation
measure with accessibility implications is ever considered, it is a
Expand Down
12 changes: 12 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import astroPlugin from 'eslint-plugin-astro';

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...astroPlugin.configs.recommended,
{
ignores: ['dist/**', 'node_modules/**', '.astro/**'],
}
);
Loading
Loading