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
2 changes: 2 additions & 0 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ jobs:
PUBLIC_CWA_TOKEN: ${{ secrets.PUBLIC_CWA_TOKEN }}
PRODUCT_REPOS_PAT: ${{ secrets.PRODUCT_REPOS_PAT }}
VERIFIED_AT: ${{ vars.VERIFIER_LAST_OK_AT }}
VERIFIED_AT_DPF: ${{ vars.VERIFIER_DPF_VERIFIED_AT }}
VERIFIED_AT_CWA: ${{ vars.VERIFIER_CWA_VERIFIED_AT }}
run: npm run build

- name: Deploy to Cloudflare Workers (production)
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ jobs:
env:
PRODUCT_REPOS_PAT: ${{ secrets.PRODUCT_REPOS_PAT }}
VERIFIED_AT: ${{ vars.VERIFIER_LAST_OK_AT }}
VERIFIED_AT_DPF: ${{ vars.VERIFIER_DPF_VERIFIED_AT }}
VERIFIED_AT_CWA: ${{ vars.VERIFIER_CWA_VERIFIED_AT }}
run: npm run build

- name: Deploy to Cloudflare Workers (staging)
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/rebuild-nightly-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ jobs:
PRODUCT_REPOS_PAT: ${{ secrets.PRODUCT_REPOS_PAT }}
PUBLIC_CWA_TOKEN: ${{ secrets.PUBLIC_CWA_TOKEN }}
VERIFIED_AT: ${{ vars.VERIFIER_LAST_OK_AT }}
VERIFIED_AT_DPF: ${{ vars.VERIFIER_DPF_VERIFIED_AT }}
VERIFIED_AT_CWA: ${{ vars.VERIFIER_CWA_VERIFIED_AT }}
run: npm run build

- name: Deploy to Cloudflare
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/rebuild-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ jobs:
PRODUCT_REPOS_PAT: ${{ secrets.PRODUCT_REPOS_PAT }}
PUBLIC_CWA_TOKEN: ${{ secrets.PUBLIC_CWA_TOKEN }}
VERIFIED_AT: ${{ vars.VERIFIER_LAST_OK_AT }}
VERIFIED_AT_DPF: ${{ vars.VERIFIER_DPF_VERIFIED_AT }}
VERIFIED_AT_CWA: ${{ vars.VERIFIER_CWA_VERIFIED_AT }}
run: npm run build

- name: Deploy to Cloudflare
Expand Down
30 changes: 28 additions & 2 deletions .github/workflows/verify-cloudflare-facts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ jobs:
node scripts/run-verifier.mjs --dry-run
echo "dry_run=true" >> "$GITHUB_OUTPUT"
# In dry-run the orchestrator does not advance GH outputs that
# gate live channel steps; explicitly pin all three flags to
# false so the variable-update, PR, and Issue gates are skipped.
# gate live channel steps; explicitly pin all flags to false
# so the variable-update, PR, and Issue gates all skip.
echo "all_ok=false" >> "$GITHUB_OUTPUT"
echo "has_value_diff=false" >> "$GITHUB_OUTPUT"
echo "has_failure=false" >> "$GITHUB_OUTPUT"
echo "dpf_ok=false" >> "$GITHUB_OUTPUT"
echo "cwa_retention_ok=false" >> "$GITHUB_OUTPUT"
exit 0
fi
node scripts/run-verifier.mjs
Expand All @@ -97,6 +99,30 @@ jobs:
"/repos/${{ github.repository }}/actions/variables/VERIFIER_LAST_OK_AT" \
-f name=VERIFIER_LAST_OK_AT -f value="$NOW"

- name: Update VERIFIER_DPF_VERIFIED_AT (when DPF check returned ok)
if: ${{ steps.run.outputs.dpf_ok == 'true' && steps.run.outputs.dry_run == 'false' }}
env:
GH_TOKEN: ${{ secrets.VERIFIER_VARIABLE_TOKEN }}
run: |
set -euo pipefail
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "[workflow] updating VERIFIER_DPF_VERIFIED_AT to $NOW"
gh api -X PATCH \
"/repos/${{ github.repository }}/actions/variables/VERIFIER_DPF_VERIFIED_AT" \
-f name=VERIFIER_DPF_VERIFIED_AT -f value="$NOW"

- name: Update VERIFIER_CWA_VERIFIED_AT (when CWA check returned ok)
if: ${{ steps.run.outputs.cwa_retention_ok == 'true' && steps.run.outputs.dry_run == 'false' }}
env:
GH_TOKEN: ${{ secrets.VERIFIER_VARIABLE_TOKEN }}
run: |
set -euo pipefail
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "[workflow] updating VERIFIER_CWA_VERIFIED_AT to $NOW"
gh api -X PATCH \
"/repos/${{ github.repository }}/actions/variables/VERIFIER_CWA_VERIFIED_AT" \
-f name=VERIFIER_CWA_VERIFIED_AT -f value="$NOW"

- name: Open auto-PR for value change
if: ${{ steps.run.outputs.has_value_diff == 'true' && steps.run.outputs.dry_run == 'false' }}
env:
Expand Down
16 changes: 14 additions & 2 deletions scripts/run-verifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,24 +212,36 @@ async function main() {
const previousJson = await readFile(DATA_PATH, 'utf8');
const nextJson = JSON.stringify(data, null, 2) + '\n';

// Per-fact ok flags for independent per-fact variable updates
// (G D.12). Allows VERIFIER_DPF_VERIFIED_AT / VERIFIER_CWA_VERIFIED_AT
// to advance whenever the corresponding fact returned 'ok', even on
// mixed-status runs where one fact is changed/failing and the other
// is fine. Independent of the top-level `all_ok` gate.
const dpfOk = results.dpf.status === 'ok';
const cwaRetentionOk = results.cwa_retention.status === 'ok';

// Emit GH outputs only on real (non-dry-run) execution. Dry-run is the
// synthetic / dispatch path; the workflow explicitly pins flags to
// false in that branch so live PR / Issue steps stay quiet.
// false in that branch so live PR / Issue / variable steps stay quiet.
if (!DRY_RUN) {
await emitGithubOutput('all_ok', allOk);
await emitGithubOutput('has_value_diff', hasValueDiff);
await emitGithubOutput('has_failure', hasFailure);
await emitGithubOutput('dpf_ok', dpfOk);
await emitGithubOutput('cwa_retention_ok', cwaRetentionOk);
}

console.log(
`[verifier:orchestrator] summary: all_ok=${allOk} has_value_diff=${hasValueDiff} has_failure=${hasFailure}`,
`[verifier:orchestrator] summary: all_ok=${allOk} has_value_diff=${hasValueDiff} has_failure=${hasFailure} dpf_ok=${dpfOk} cwa_retention_ok=${cwaRetentionOk}`,
);

if (DRY_RUN) {
console.log('\n[verifier:orchestrator] [DRY-RUN] would write src/data/cloudflare-facts.json:');
console.log(nextJson);
const wouldDo = [];
if (allOk) wouldDo.push('update VERIFIER_LAST_OK_AT variable');
if (dpfOk) wouldDo.push('update VERIFIER_DPF_VERIFIED_AT variable');
if (cwaRetentionOk) wouldDo.push('update VERIFIER_CWA_VERIFIED_AT variable');
if (hasValueDiff) wouldDo.push('open auto-PR');
if (hasFailure) wouldDo.push('open verifier-alert Issue');
console.log(
Expand Down
90 changes: 61 additions & 29 deletions src/lib/cloudflare-facts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,29 @@
* Schema (verifier-era, schema_version=1):
* - `_meta.last_check_attempt` is updated every run (success or failure)
* and drives the build-time freshness gate.
* - Per-fact `verified_at` is updated only on successful checks. This is
* the date users see on the privacy pages.
* - Per-fact `verified_at` is updated only when a value change forces a
* JSON commit (the verifier-bot PR). For the silent-when-healthy
* weekly cron path, JSON stays untouched and the displayed dates flow
* from GitHub Actions repo variables instead — see below.
* - Per-fact `last_known_good_at` mirrors `verified_at` on success and is
* left untouched on failure — useful for "stale" detection.
*
* G D.11 display-date sourcing note: the top-level `bbl-verified-at`
* meta marker + visible "zuletzt geprüft am ..." prose on the privacy
* pages flows through `getEffectiveVerifiedDate()` (env-preferred, JSON
* fallback). The per-fact `bbl-verified-at-dpf` / `bbl-verified-at-cwa`
* markers in `<head>` deliberately read JSON's per-fact `verified_at`
* fields directly, NOT this helper — they're diagnostic markers
* tracking the JSON-side ground truth per fact, separate from the
* unified display date. Don't "fix" the asymmetry without revisiting
* the verifier post-deploy smoke design.
* G D.11 + G D.12 display-date sourcing — all three rendered "verified
* at" surfaces on the privacy pages flow through
* `getEffectiveVerifiedDate()` with a `source` argument, preferring
* GitHub Actions repo variables (set by the verifier's clean-run path)
* over JSON:
* - top-level `<meta name="bbl-verified-at">` + visible "zuletzt geprüft
* am ..." prose → source `'top'` → env `VERIFIED_AT` ← variable
* `VERIFIER_LAST_OK_AT` (advances on all-ok runs).
* - `<meta name="bbl-verified-at-dpf">` → source `'dpf'` → env
* `VERIFIED_AT_DPF` ← variable `VERIFIER_DPF_VERIFIED_AT` (advances
* whenever the DPF check returns ok, independent of CWA).
* - `<meta name="bbl-verified-at-cwa">` → source `'cwa'` → env
* `VERIFIED_AT_CWA` ← variable `VERIFIER_CWA_VERIFIED_AT` (advances
* whenever the CWA check returns ok, independent of DPF).
* JSON's per-fact `verified_at` is the fallback if any env var is
* unset or unparseable (local dev, first deploy, reset scenarios).
*/

import facts from '../data/cloudflare-facts.json';
Expand Down Expand Up @@ -59,30 +68,53 @@ export interface CloudflareFacts {
export const cloudflareFacts: CloudflareFacts = facts as CloudflareFacts;

/**
* Effective verified-date for the privacy pages' single-date contract.
* Source selector for `getEffectiveVerifiedDate`.
* - `'top'` (default): unified display date for the headline "zuletzt
* geprüft am ..." prose + `<meta name="bbl-verified-at">`. Sourced
* from `VERIFIED_AT` env (← repo var `VERIFIER_LAST_OK_AT`); JSON
* fallback is the older of the two per-fact dates.
* - `'dpf'`: per-fact diagnostic marker for the DPF claim. Sourced from
* `VERIFIED_AT_DPF` env (← repo var `VERIFIER_DPF_VERIFIED_AT`); JSON
* fallback is `facts.dpf.verified_at`.
* - `'cwa'`: per-fact diagnostic marker for the CWA retention claim.
* Sourced from `VERIFIED_AT_CWA` env (← repo var
* `VERIFIER_CWA_VERIFIED_AT`); JSON fallback is
* `facts.cwa_retention.verified_at`.
*/
export type VerifiedAtSource = 'top' | 'dpf' | 'cwa';

/**
* Effective verified-date for the privacy pages.
*
* Resolution order (G D.11):
* 1. `import.meta.env.VERIFIED_AT` — set from the `VERIFIER_LAST_OK_AT`
* GitHub Actions repo variable by every build workflow. Wins when
* present and parseable so prod/staging refresh the displayed date
* from the verifier's last clean run without any git operations.
* (Astro/Vite exposes non-public env vars at build time through
* `import.meta.env`; this is the project-wide pattern — see
* `src/lib/github-api.ts`, `src/lib/env.ts`.)
* 2. Otherwise, the older of the two per-fact `verified_at` values in
* `src/data/cloudflare-facts.json` (worst-case freshness signal:
* "the data is at most this stale"). This is the local-dev path and
* the safe fallback for any first-deploy / reset scenario before
* the verifier has run.
* Resolution order (G D.11 + G D.12): env var matching `source` wins
* when present and parseable so prod/staging refresh the displayed date
* from the verifier's last clean run without any git operations. Otherwise
* fall back to JSON's per-fact `verified_at` (per-fact sources) or the
* older of the two (top-level — worst-case freshness signal).
*
* The `Date.parse()` guard ensures malformed env input falls through to
* JSON rather than rendering a literal "Invalid Date" string.
* Vite replaces `import.meta.env.X` at build time only when `X` is a
* literal property access — computed property access wouldn't be
* replaced — so the three sources branch explicitly. The `Date.parse()`
* guard ensures malformed env input falls through to JSON rather than
* rendering a literal "Invalid Date" string.
*/
export function getEffectiveVerifiedDate(facts: CloudflareFacts): string {
const envValue = import.meta.env.VERIFIED_AT as string | undefined;
export function getEffectiveVerifiedDate(
facts: CloudflareFacts,
source: VerifiedAtSource = 'top',
): string {
let envValue: string | undefined;
if (source === 'dpf') {
envValue = import.meta.env.VERIFIED_AT_DPF as string | undefined;
} else if (source === 'cwa') {
envValue = import.meta.env.VERIFIED_AT_CWA as string | undefined;
} else {
envValue = import.meta.env.VERIFIED_AT as string | undefined;
}
if (envValue && !Number.isNaN(Date.parse(envValue))) {
return envValue;
}
if (source === 'dpf') return facts.dpf.verified_at;
if (source === 'cwa') return facts.cwa_retention.verified_at;
const a = facts.dpf.verified_at;
const b = facts.cwa_retention.verified_at;
return a < b ? a : b;
Expand Down
13 changes: 7 additions & 6 deletions src/pages/datenschutz.astro
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ const description =
const standDate = 'April 2026';
const cwaMonths = cloudflareFacts.cwa_retention.aggregated_retention_months;
const verifiedHuman = formatLocaleDate(getEffectiveVerifiedDate(cloudflareFacts), 'de');
// ISO-date markers for the verifier workflow's post-deploy smoke step
// (mode #8 v1). Locale-independent; invisible to users; greppable from
// the deployed HTML. Format: YYYY-MM-DD. See
// .github/workflows/verify-cloudflare-facts.yml ("Post-deploy smoke").
// Hidden ISO-date markers for diagnostics / external audit tooling.
// Locale-independent (YYYY-MM-DD); invisible to users; greppable from
// the deployed HTML. All three flow through getEffectiveVerifiedDate
// with a source argument; see src/lib/cloudflare-facts.ts for the
// env-var → repo-variable → JSON fallback chain (G D.12).
const verifiedAtEffective = getEffectiveVerifiedDate(cloudflareFacts).slice(0, 10);
const verifiedAtDpf = cloudflareFacts.dpf.verified_at.slice(0, 10);
const verifiedAtCwa = cloudflareFacts.cwa_retention.verified_at.slice(0, 10);
const verifiedAtDpf = getEffectiveVerifiedDate(cloudflareFacts, 'dpf').slice(0, 10);
const verifiedAtCwa = getEffectiveVerifiedDate(cloudflareFacts, 'cwa').slice(0, 10);

const tocItems = [
{ num: '1', label: 'Verantwortlicher' },
Expand Down
13 changes: 7 additions & 6 deletions src/pages/en/privacy.astro
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ const description =
const lastUpdated = 'April 2026';
const cwaMonths = cloudflareFacts.cwa_retention.aggregated_retention_months;
const verifiedHuman = formatLocaleDate(getEffectiveVerifiedDate(cloudflareFacts), 'en');
// ISO-date markers for the verifier workflow's post-deploy smoke step
// (mode #8 v1). Locale-independent; invisible to users; greppable from
// the deployed HTML. Format: YYYY-MM-DD. See
// .github/workflows/verify-cloudflare-facts.yml ("Post-deploy smoke").
// Hidden ISO-date markers for diagnostics / external audit tooling.
// Locale-independent (YYYY-MM-DD); invisible to users; greppable from
// the deployed HTML. All three flow through getEffectiveVerifiedDate
// with a source argument; see src/lib/cloudflare-facts.ts for the
// env-var → repo-variable → JSON fallback chain (G D.12).
const verifiedAtEffective = getEffectiveVerifiedDate(cloudflareFacts).slice(0, 10);
const verifiedAtDpf = cloudflareFacts.dpf.verified_at.slice(0, 10);
const verifiedAtCwa = cloudflareFacts.cwa_retention.verified_at.slice(0, 10);
const verifiedAtDpf = getEffectiveVerifiedDate(cloudflareFacts, 'dpf').slice(0, 10);
const verifiedAtCwa = getEffectiveVerifiedDate(cloudflareFacts, 'cwa').slice(0, 10);

const tocItems = [
{ num: '1', label: 'Controller' },
Expand Down
Loading