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
35 changes: 35 additions & 0 deletions .github/actions/download-build-artifact/__tests__/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ def make_zip_bytes() -> bytes:
return mem.getvalue()


def make_zip_bytes_with_top_level_dir() -> bytes:
mem = io.BytesIO()
with zipfile.ZipFile(mem, "w") as z:
z.writestr("vercel-build-preview/.vercel/output/config.json", "{}")
return mem.getvalue()


def test_extracts_vercel_output(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
module = load_action_module()

Expand All @@ -64,3 +71,31 @@ def fake_get(url: str, **kwargs: Any) -> MockResponse:
assert failures == []
assert (tmp_path / ".vercel" / "output" / "config.json").exists()
monkeypatch.chdir(cwd)


def test_extracts_vercel_output_when_nested_in_top_level_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
module = load_action_module()

inputs = {"token": "ghs_test", "artifact-download-url": "https://api.github.com/art.zip"}
monkeypatch.setattr(module.core, "get_input", lambda name, required=False: inputs.get(name, ""))

monkeypatch.setenv("GITHUB_API_URL", "https://api.github.com")

zip_bytes = make_zip_bytes_with_top_level_dir()

def fake_get(url: str, **kwargs: Any) -> MockResponse:
return MockResponse(ok=True, status_code=200, content=zip_bytes)

monkeypatch.setattr(module.requests, "get", fake_get)

cwd = Path.cwd()
monkeypatch.chdir(tmp_path)

failures: list[str] = []
monkeypatch.setattr(module.core, "set_failed", lambda m: failures.append(m))

module.run()

assert failures == []
assert (tmp_path / ".vercel" / "output" / "config.json").exists()
monkeypatch.chdir(cwd)
32 changes: 29 additions & 3 deletions .github/actions/download-build-artifact/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ def is_allowed_fetch_url(url: str, allowed_hosts: set[str]) -> bool:
return parsed.scheme == "https" and parsed.hostname in allowed_hosts


def find_vercel_output_dir(extract_dir: Path) -> Path | None:
direct_candidates = [extract_dir / ".vercel" / "output", extract_dir / "output"]
for candidate in direct_candidates:
if candidate.is_dir():
return candidate

# Common case: artifact contains a top-level folder (e.g. "vercel-build-preview/")
# and the output is nested under it.
nested_candidates = list(extract_dir.rglob(".vercel/output"))
for candidate in nested_candidates:
if candidate.is_dir():
return candidate

# Fallback: look for directories named "output" that contain Vercel output.
for candidate in extract_dir.rglob("output"):
if candidate.is_dir() and (candidate / "config.json").is_file():
return candidate

return None


def run() -> None:
try:
token = get_input_compat("token", required=True)
Expand Down Expand Up @@ -73,10 +94,15 @@ def run() -> None:
with zipfile.ZipFile(io.BytesIO(response.content)) as zip_ref:
zip_ref.extractall(extract_dir)

candidates = [extract_dir / ".vercel" / "output", extract_dir / "output"]
source = next((p for p in candidates if p.is_dir()), None)
source = find_vercel_output_dir(extract_dir)
if not source:
raise RuntimeError("Downloaded artifact did not contain expected output directory.")
top_level = sorted(
[p.name + ("/" if p.is_dir() else "") for p in extract_dir.iterdir()]
)
raise RuntimeError(
"Downloaded artifact did not contain expected output directory. "
f"Top-level entries: {top_level}"
)

target = Path(".vercel") / "output"
if target.exists():
Expand Down
12 changes: 6 additions & 6 deletions .github/actions/keep-alive/__tests__/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ def test_executes_select_1_and_closes_client(monkeypatch: pytest.MonkeyPatch) ->
module = load_action_module()

def fake_get_input(name: str, required: bool = False) -> str:
if name == "astro-db-remote-url":
if name == "url":
return "libsql://example.turso.io"
if name == "astro-db-app-token":
if name == "token":
return "token"
return ""

Expand Down Expand Up @@ -103,9 +103,9 @@ def test_prefers_inputs_over_env_vars(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ASTRO_DB_APP_TOKEN", "env_token")

def fake_get_input(name: str, required: bool = False) -> str:
if name == "astro-db-remote-url":
if name == "url":
return "libsql://input.turso.io"
if name == "astro-db-app-token":
if name == "token":
return "input_token"
return ""

Expand Down Expand Up @@ -136,9 +136,9 @@ def test_normalizes_libsql_url_to_include_trailing_slash(monkeypatch: pytest.Mon
module = load_action_module()

def fake_get_input(name: str, required: bool = False) -> str:
if name == "astro-db-remote-url":
if name == "url":
return "libsql://example.turso.io"
if name == "astro-db-app-token":
if name == "token":
return "token"
return ""

Expand Down
8 changes: 4 additions & 4 deletions .github/actions/keep-alive/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ name: Keep Alive
description: Executes a keep-alive query (SELECT 1) against the Turso DB.

inputs:
astro-db-remote-url:
url:
description: Turso DB URL.
required: true
astro-db-app-token:
token:
description: Turso auth token.
required: true

Expand All @@ -17,5 +17,5 @@ runs:
run: python3 src/main.py
shell: bash
env:
INPUT_ASTRO_DB_REMOTE_URL: ${{ inputs.astro-db-remote-url }}
INPUT_ASTRO_DB_APP_TOKEN: ${{ inputs.astro-db-app-token }}
INPUT_URL: ${{ inputs.url }}
INPUT_ASTRO_DB_APP_TOKEN: ${{ inputs.token }}
8 changes: 4 additions & 4 deletions .github/actions/keep-alive/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ def normalize_libsql_url(url: str) -> str:

def run() -> None:
try:
url = core.get_input("astro-db-remote-url", required=True)
auth_token = core.get_input("astro-db-app-token", required=True)
url = core.get_input("url", required=True)
auth_token = core.get_input("token", required=True)

required_url = normalize_libsql_url(get_required_value(url, "astro-db-remote-url"))
required_auth_token = get_required_value(auth_token, "astro-db-app-token")
required_url = normalize_libsql_url(get_required_value(url, "url"))
required_auth_token = get_required_value(auth_token, "token")

client = create_client_sync(url=required_url, auth_token=required_auth_token)
try:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: vercel-build-preview
path: .vercel/output
path: .vercel
retention-days: 30
2 changes: 1 addition & 1 deletion .github/workflows/build-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: vercel-build-production
path: .vercel/output
path: .vercel
retention-days: 30
8 changes: 4 additions & 4 deletions .github/workflows/cron.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ jobs:
- name: Execute keep-alive query
uses: './.github/actions/keep-alive'
with:
astro-db-remote-url: ${{ vars.ASTRO_DB_REMOTE_URL }}
astro-db-app-token: ${{ secrets.ASTRO_DB_APP_TOKEN }}
url: ${{ vars.ASTRO_DB_REMOTE_URL }}
token: ${{ secrets.ASTRO_DB_APP_TOKEN }}

ping-preview:
name: Ping Turso Preview DB
Expand All @@ -58,5 +58,5 @@ jobs:
- name: Execute keep-alive query
uses: './.github/actions/keep-alive'
with:
astro-db-remote-url: ${{ vars.ASTRO_DB_REMOTE_URL }}
astro-db-app-token: ${{ secrets.ASTRO_DB_APP_TOKEN }}
url: ${{ vars.ASTRO_DB_REMOTE_URL }}
token: ${{ secrets.ASTRO_DB_APP_TOKEN }}
87 changes: 3 additions & 84 deletions _TODO.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<!-- markdownlint-disable-file -->
# TODO

## Refactor API Endpoints to Astro Actions

### Action / Domain / Responder Pattern
## Astro Actions - Action / Domain / Responder Pattern

- The action takes HTTP requests (URLs and their methods) and uses that input to interact with the domain, after which it passes the domain's output to one and only one responder.

Expand All @@ -27,48 +25,6 @@

- The responder builds the entire HTTP response from the domain's output which is given to it by the action. The Responder is responsible solely for formatting the final response (e.g., JSON, HTML) to be sent back to the client.

### Endpoints:

- cron/cleanup-confirmations β†’ GET
- cron/cleanup-dsar-requests β†’ GET
- cron/run-all β†’ GET
- social-card/ β†’ GET

- contact/ β†’ POST (contact form submission) and OPTIONS (CORS pre-flight)
- downloads/submit β†’ POST
- gdpr/consent β†’ POST, GET, DELETE
- gdpr/request-data β†’ POST
- gdpr/export β†’ GET
- gdpr/verify β†’ GET
- health/ β†’ GET
- newsletter/ β†’ POST, OPTIONS
- newsletter/confirm β†’ GET

### Files importing from `astro:db`

- _utils/rateLimit.ts
- _utils/rateLimitStore.ts
- cron/cleanup-confirmations.ts
- cron/cleanup-dsar-requests.ts
- gdpr/_utils/consentStore.ts
- gdpr/_utils/dsarStore.ts
- newsletter/_token.ts

### Cross-endpoint dependencies:

gdpr: Mostly self-contained, but `verify.ts` does import `deleteNewsletterConfirmationsByEmail` from `@pages/api/newsletter/_token` (line 15). That's a direct dependency on the newsletter code.

newsletter: `confirm.ts` pulls `markConsentRecordsVerified` from `@pages/api/gdpr/_utils/consentStore` (line 10) to mark double opt-in consent. That's the reciprocal dependency.

Newsletter hits the gdpr consent endpoint using `recordConsent` in `src/pages/api/_logger/index.ts`.

If we want to make it feel less inconsistent, we could either (a) rename `_logger` to something like `_consentClient` so its purpose is clearer, or (b) move to a microservices architecture and expose a protected `/api/gdpr/verify` endpoint and have newsletter call it over HTTP as well - but that would need additional auth to prevent abuse.

**Affected components:**

- CallToAction/Newsletter
- ContactForm

## Refactor Theme Colors

### Color vars
Expand Down Expand Up @@ -111,39 +67,6 @@ cat.text-alternatives: Rules for ensuring that text alternatives are provided fo

Implement mitigations in test/e2e/specs/07-performance/PERFORMANCE.md

## Prefetch Links

The default prefetch strategy when adding the data-astro-prefetch attribute is hover. To change it, you can configure prefetch.defaultStrategy in your astro.config.mjs file.

hover (default): Prefetch when you hover over or focus on the link.
tap: Prefetch just before you click on the link.
viewport: Prefetch as the links enter the viewport.
load: Prefetch all links on the page after the page is loaded.

```html
<a href="/about" data-astro-prefetch>
<a href="/about" data-astro-prefetch="tap">About</a>
```

If you want to prefetch all links, including those without the data-astro-prefetch attribute, you can set prefetch.prefetchAll to true:

```typescript
// astro.config.mjs
import { defineConfig } from 'astro/config'

export default defineConfig({
prefetch: {
prefetchAll: true
}
})
```

You can then opt-out of prefetching for individual links by setting data-astro-prefetch="false":

```html
<a href="/about" data-astro-prefetch="false">About</a>
```

## Email Templates

Right now we're using string literals to define HTML email templates for site mails. We should use Nunjucks with the rule-checking for valid CSS in HTML emails like we have in the corporate email footer repo.
Expand Down Expand Up @@ -175,12 +98,6 @@ Needs to add real API key and test

See the example image in Social Shares. The social shares UI on mobile should be a modal that slides in from the bottom.

## Themepicker tooltips, extra themes

- Add additional themes (high contrast)
- Add Carousel
- Add tooltip that makes use of the description field for the theme, explaining what the intent of the theme is

## Sentry feedback, chat bot tying into my phone and email

See note in src/components/scripts/sentry/client.ts - "User Feedback - allow users to report issues"
Expand All @@ -201,6 +118,8 @@ Add Upstash Search as a Vercel Marketplace Integration.

Google Calendar, Apple Calendar, Microsoft Outlook and Teams, and generate iCal/ics files (for all other calendars and cases).

## Troubleshooting deploy workflow issues

`https://github.com/add2cal/add-to-calendar-button`
`https://add-to-calendar-button.com/`

Expand Down
8 changes: 4 additions & 4 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ export default [
{
files: [
'.github/actions/**/*',
'src/actions/newsletter.ts',
'src/lib/config/pwa.ts',
'src/lib/config/serviceWorker.ts',
'src/components/scripts/store/__tests__/socialEmbeds.spec.ts',
Expand Down Expand Up @@ -422,7 +423,7 @@ export default [
'vitest.setup.ts',
'src/components/scripts/utils/environmentClient.ts',
'src/lib/config/**/*',
'src/pages/api/_environment/**/*',
'src/pages/api/_utils/environment/**/*',
'test/e2e/config/runtime/database.ts',
'test/e2e/config/global-setup.ts',
'test/e2e/config/runtime/mockState.ts',
Expand Down Expand Up @@ -468,8 +469,7 @@ export default [
'src/components/scripts/utils/siteUrlClient.ts',
'src/lib/config/environmentServer.ts',
'src/lib/config/siteUrlServer.ts',
'src/pages/api/_environment/index.ts',
'src/pages/api/_environment/environmentApi.ts',
'src/pages/api/_utils/environment/environmentApi.ts',
'test/e2e/config/global-setup.ts',
],
rules: {
Expand Down Expand Up @@ -753,7 +753,7 @@ export default [
'src/**/server/**/*',
'src/lib/**/*.ts',
'src/pages/**/*.astro',
'src/pages/api/_environment/environmentApi.ts',
'src/pages/api/_utils/environment/environmentApi.ts',
],
rules: {
'no-restricted-imports': [
Expand Down
Loading
Loading