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
16 changes: 15 additions & 1 deletion .github/scripts/verify-firebase-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,24 @@ export function firestoreReleaseNames(project) {
};
}

function summarizeFunction(id, value) {
export function summarizeFunction(id, value) {
if (!value) return null;
// The v1 API's min_instances is an implicit-presence proto3 int32: its
// zero value can be omitted from JSON. Normalize only that omission and
// preserve its provenance; null/malformed values and other gaps still fail.
// https://protobuf.dev/programming-guides/json/#presence-and-default-values
const hasMinInstances = Object.hasOwn(value, 'minInstances');
return {
id,
name: value.name,
status: value.status,
entryPoint: value.entryPoint,
runtime: value.runtime,
minInstances: hasMinInstances ? value.minInstances : 0,
minInstancesReadback: hasMinInstances ? 'explicit' : 'proto3-default-zero',
maxInstances: value.maxInstances,
availableMemoryMb: value.availableMemoryMb,
timeout: value.timeout,
updateTime: value.updateTime,
versionId: value.versionId,
buildId: value.buildId,
Expand Down Expand Up @@ -129,6 +139,10 @@ export function validateBackendState(before, after, expectedRulesDigest) {
if (current.runtime !== RUNTIME || current.entryPoint !== spec.entryPoint) {
errors.push(`${id} has the wrong runtime or entry point.`);
}
if (current.minInstances !== 0 || current.maxInstances !== 2
|| current.availableMemoryMb !== 256 || current.timeout !== '30s') {
errors.push(`${id} has unverified or different profile resource limits.`);
}
if (current.trigger !== spec.trigger) {
errors.push(`${id} has the wrong trigger type.`);
}
Expand Down
2 changes: 2 additions & 0 deletions GITHUB_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ Do not redesign routing or hosting in this issue; WEB-001 owns that. WEB-001A1 [

## CI-001 — Repair test gates and secure the deployment pipeline

**COST-001 source child:** [#683](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/683) binds only the two profile Functions to zero reserved instances, max two each, 256 MB and 30 seconds, with actual SDK metadata tests and matching fail-closed provider readback validation. It records the community-first minimal-service direction and officer review procedure without changing schema, privileges, deployment scope, billing, providers, or public pages. This is source preparation, not reduced-bill or hosted quality evidence; #113/#133/#136 retain release prerequisites, synthetic cold-start/burst/recovery proof and compatible rollback.

**Labels:** `priority:P0`, `type:security`, `type:testing`, `area:ci`, `size:L`, `needs-external-config`
**Status:** Published as tracker [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105). [#103](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/103), [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124), and [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126) provide the local/hosted frontend and SPA baselines. CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) owns the dedicated hosted command-journal Firestore transaction gate. CI-001B4 [#186](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/186) merged through [PR #189](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/189) as exact main `bec7d5e365eacb418563a172029f241f660d9768`; exact PR run `29291402007` and post-merge run `29291515653` passed. CI-001B4A [#227](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/227), CI-001B4B [#239](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/239), and PAY-004C1 [#359](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/359) are focused reviewed reductions of that ledger. [PR #391](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/391) and [PR #392](https://github.com/Run-MPRC/Run-MPRC.github.io/pull/392) add three TypeScript files while retiring four TSX errors during functional work. CI-001B4C [#449](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/449) retires four stale unknown-rule directive records through comment-only same-line substitutions. MEMBERS-CONTENT-001B [#492](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/492) removes one finding-free dormant JSX file. The gate now records 110 files, 113 configured errors, and 6 warnings. [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135) owns the manual exact-commit, backend-first source gate and Git-triggered Netlify production containment. [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133) owns protected OIDC/environment configuration, and [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136) owns the actual profile-recovery release. Branch-required-check configuration, scanning, staging/live evidence, remaining lint-debt cleanup, and protected Netlify publication remain open, so the pipeline is still non-compliant overall.
**Depends on:** SAFETY-001
Expand Down
1 change: 1 addition & 0 deletions OFFICER_START_HERE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Attach the exact wording, public link, or approved photo when you have it. The A
| Approve a change and check whether it is really live | [Publish and check](./docs/officers/PUBLISH_AND_CHECK.md) |
| The site is wrong, down, unsafe, or showing private information | [Emergency and recovery](./docs/officers/EMERGENCY_AND_RECOVERY.md) |
| Prepare backup officers and account access | [Access continuity](./docs/officers/ACCESS_CONTINUITY.md) |
| Keep the community website simple and review costs | [Low-cost community scope](./docs/officers/LOW_COST_COMMUNITY.md) |
| Understand the pages and services | [Simple system maps](./docs/officers/SYSTEM_MAPS.md) |
| Understand an unfamiliar word | [Plain-language glossary](./docs/officers/GLOSSARY.md) |

Expand Down
4 changes: 4 additions & 0 deletions OPERATIONS_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,10 @@ Never use destructive Git or Firestore reset operations on production as a rollb

### Missing member profile repair — NOT AVAILABLE YET

COST-001 [#683](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/683) adds source-only resource limits for this pair: `minInstances: 0`, `maxInstances: 2`, `memory: '256MB'`, and `timeoutSeconds: 30`. The actual SDK endpoint metadata is tested. The backend verifier reads the v1 provider fields `minInstances`, `maxInstances`, `availableMemoryMb`, and `timeout`. The API defines minimum instances as a proto3 implicit-presence integer, so an omitted field represents zero; the summary records `minInstancesReadback: 'proto3-default-zero'` instead of claiming an explicit value was returned. Null, malformed or differing values and missing maximum/memory/timeout evidence are rejected. An unreadable or incomplete Function cannot pass through this normalization. Existing project/revision/Rules/trigger checks remain required. See the [API schema](https://github.com/googleapis/googleapis/blob/master/google/cloud/functions/v1/functions.proto) and [JSON presence semantics](https://protobuf.dev/programming-guides/json/#presence-and-default-values).

No migration is needed: profile schema, create-once transactions, claims, App Check/auth behavior, and trigger identities are unchanged. Execution timeouts and saturation may produce uncertain/busy results; the existing recovery path must preserve already committed data. Test cold starts and a small synthetic concurrent burst in staging before approving these initial limits for production. Max instances is not a hard monetary ceiling and may be briefly exceeded by the provider. Budgets/eligible spend caps, artifact retention, minimal runtime IAM, callable App Check enforcement, and the Node 20 maintenance deadline must be handled and verified in the provider release. Do not deploy extra Functions to make unrelated screens work. See the no-terminal [low-cost community review](./docs/officers/LOW_COST_COMMUNITY.md).

Issue [#118](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/118) replaces manual repair with create-once signup/recovery Functions and a fail-closed account page. Do not use it as a production procedure until #105 supplies a protected staging and deployment path.

Source guard #674 may be reviewed before billing exists, but it is not deployment evidence. Stop before any provider mutation unless the board/platform owner separately approves a Cloud Billing account and spending controls. A billing link changes the project to pay-as-you-go even when expected Function usage is within no-cost quotas.
Expand Down
6 changes: 6 additions & 0 deletions SYSTEM_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ The repository already contains a substantial prototype: a public React site, Fi

## 1. Goals and non-goals

### Community-first cost direction — 2026-09-11

The owner requests a simpler, low-cost community experience and is willing to supply a personal card as fallback. Prioritize public run/event/join/contact information and optional secure accounts; prefer static delivery and bounded data reads. The first profile release remains only `createMemberOnSignUp` plus `ensureMemberProfile`, not the prototype's commerce, mail, photo-directory, or synchronization services. Preserve their source and security requirements for separately approved later work; this is not permission to weaken trust boundaries or remove existing club services.

COST-001 [#683](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/683) explicitly binds both profile Functions to zero reserved instances, maximum two instances each, 256 MB memory and a 30-second execution timeout. The release verifier requires matching provider readback. It records the v1 API's documented omitted-zero encoding for minimum instances; missing nonzero settings and malformed evidence remain unverified. Source and tests do not make these settings live or guarantee a bill ceiling. Cold-start, saturation, uncertain-timeout/retry and public-page availability checks remain required before release. The [officer guide](./docs/officers/LOW_COST_COMMUNITY.md) records scope, cost-control limits, proof, and recovery. Node runtime maintenance and existing provider/release gates remain open; no schema, role, price, domain, or deployed service changes in this slice.

### Goals

- Publish public club content, events, and merchandise.
Expand Down
79 changes: 79 additions & 0 deletions docs/officers/LOW_COST_COMMUNITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# A simple, low-cost community website

**Purpose:** keep the community experience useful while limiting services, recurring work, and spending exposure.

**Approver:** Dave Liu for the current simplification request. Any release still uses the named platform/security reviewers. Regular club expenditure needs the club's finance approval; willingness to supply a personal card is not an unlimited club budget.

**Checked:** 2026-09-11. This is a scope and source-review guide, not proof that the new settings are live.

## What we are prioritizing

- Clear information about regular runs, meeting places, joining, and contacts.
- A useful events/calendar experience on phones as well as computers.
- Public information without requiring an account.
- Optional secure accounts for features that actually need them.
- Existing club-approved external links when an outside service already does the job.

Prefer static pages and published public content: visitors can receive files from the host without running a server operation for each page view. Where events need database content, use bounded reads and avoid unnecessary polling. This is the implementation direction, not a claim that the Events service has been repaired. Production Events/Calendar still have the recorded unavailable state pending their own reviewed release.

The first profile backend remains exactly two server operations: create a missing account profile and recover it safely. Keep custom checkout, refunds, inventory, automatic email, photo uploads, directory search, and Strava synchronization out of that deployment. Their existing code and future requirements remain preserved. Connecting any of them needs its own useful community outcome, cost review, tests, and approval. Do not remove a current external membership/payment service or change club policies through this guide.

## Cost controls in source — NOT AVAILABLE YET in hosted Functions

[Issue #683](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/683) sets the same resource options on both profile operations:

| Setting | Meaning |
| --- | --- |
| Minimum instances: 0 | No server instance is reserved to sit idle. A first request after inactivity can take longer. |
| Maximum instances: 2 per operation | Limit normal automatic scaling. This is not two users or two requests per month. |
| Memory: 256 MB | Use the existing small memory size explicitly. |
| Execution timeout: 30 seconds | Stop unusually long execution; a timed-out request can have an uncertain result. |

These are resource settings, **not a maximum monthly bill**. Google can briefly exceed the instance limit during traffic surges. A busy HTTP operation can queue requests and return a temporary busy response. Persistent storage, deployment files, database use, and other services can still cost money. The profile remains create-once so retrying an uncertain result does not overwrite existing information or grant membership.

No budget, spending cap, artifact-cleanup policy, billing account, Function, or new host is configured by this source change. Billing was separately checked as disabled/unlinked on 2026-09-11. A personal card has been offered as fallback; card details belong only in Google's secure payment form.

## Review before a low-cost release

**Prerequisites:** issue #683 and its reviewed pull request; the exact release source and test results; a named release approver; a private billing/payment-owner decision; and a platform maintainer. The existing release dependencies still apply. Never put payment details, passwords, or member records in an issue.

```mermaid
flowchart LR
Scope["Confirm useful community scope"] --> Source["Review paired limits and tests"]
Source --> Provider["Maintainer verifies actual settings and cost controls"]
Provider --> Quality["Check made-up accounts, cold start, burst and recovery"]
Quality --> Decision{"All proofs and approvals present?"}
Decision -- Yes --> Release["Use approved backend-first release"]
Decision -- No --> Hold["Keep release on hold"]
```

Text alternative: confirm the small scope, review source, verify provider controls, test quality with made-up accounts, and release only when every proof and approval is present.

1. Confirm the public pages to keep and the two profile operations to deploy.
2. Confirm the release adds no payment, email, photo, or synchronization service.
3. Ask the maintainer for the exact merged source and passing tests.
4. Ask for separate provider evidence of all four resource settings on each Function. Google may leave out a zero minimum-instance value; the maintainer must show that this was decoded using Google's documented format. Other missing or malformed settings are not proof of safe defaults.
5. Ask for the agreed alert recipients and budget amount. Alerts do not stop charges.
6. Ask whether a service spending cap is available and configured. It does not guarantee a whole-project bill ceiling or instant shutdown.
7. Ask for deployment-file cleanup and storage-retention evidence. Do not delete database backups as a cost shortcut.
8. Observe the maintainer's made-up account checks: first sign-in after inactivity, a small concurrent burst, profile recovery, retry, and the readable busy/unavailable state. Never create or edit a real member for this test.
9. Confirm public run/join/contact information stays usable when the account service is unavailable. Events data may still depend on the database; do not promise an offline calendar unless separately tested.
10. Record the date, named reviewers, source revision, fixed pass/fail results, and separate source/merge/website/Firebase/provider states.

**Expected result:** officers can identify the small launch scope, understand the limits, and verify the release without a terminal or payment details.

**Stop conditions:** unknown billing owner, unapproved service, claims of guaranteed zero cost, missing provider settings, poor cold-start/burst behavior, real-data testing, an unexpected allow, or missing deployment/rollback evidence. Stop if runtime maintenance or the existing #113/#133/#136 dependencies remain unresolved for the requested release.

**Success proof:** a reviewed source/CI record plus separate provider readback and synthetic quality evidence. Source tests alone cannot prove lower bills or a good hosted experience.

**Undo:** before release, hold the change. After release, ask the platform maintainer to use the reviewed compatible rollback or safe roll-forward. Reverting the limits can raise spending exposure and requires review. Do not edit a profile, widen permissions, switch off billing, or delete resources as an improvised fix.

**Escalation:** platform/security owner for reliability or access; treasurer/finance approver for recurring club costs; cardholder for payment authority. A specialist is still needed for provider setup, release, and rollback. This guide does not claim independent officer deployment is available.

## References

- [Firebase resource settings and saturation behavior](https://firebase.google.com/docs/functions/1st-gen/manage-functions-1st)
- [Google maximum-instance caveat](https://docs.cloud.google.com/functions/docs/reference/rest/v1/projects.locations.functions)
- [Google's zero-value response format](https://protobuf.dev/programming-guides/json/#presence-and-default-values)
- [Spending-cap coverage and overages](https://docs.cloud.google.com/billing/docs/how-to/budgets-spend-caps)
- [Firebase Hosting usage](https://firebase.google.com/docs/hosting/usage-quotas-pricing)
1 change: 1 addition & 0 deletions docs/officers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ In words: approve the merge, request one exact release, and approve its protecte
6. [Access continuity](./ACCESS_CONTINUITY.md)
7. [Simple system maps](./SYSTEM_MAPS.md)
8. [Plain-language glossary](./GLOSSARY.md)
9. [Low-cost community scope and release review](./LOW_COST_COMMUNITY.md)

## Five rules

Expand Down
3 changes: 2 additions & 1 deletion functions/ensureMemberProfile.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const profileRuntime = require('./profileRuntime');
const { requireAppCheck } = require('./stripeHelpers');
const {
ensureMemberProfileDocument,
Expand All @@ -13,7 +14,7 @@ function isEmptyRequest(data) {
&& Object.keys(data).length === 0;
}

exports.ensureMemberProfile = functions.https.onCall(async (data, context) => {
exports.ensureMemberProfile = functions.runWith(profileRuntime).https.onCall(async (data, context) => {
requireAppCheck(context);

if (!context.auth) {
Expand Down
3 changes: 2 additions & 1 deletion functions/ensureMemberProfile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,14 @@ jest.mock('firebase-functions', () => {
}
}

return {
const builder = {
auth: { user: () => ({ onCreate: (handler) => handler }) },
https: {
onCall: (handler) => handler,
HttpsError,
},
};
return { ...builder, runWith: () => builder };
});

jest.mock('./stripeHelpers', () => ({
Expand Down
8 changes: 8 additions & 0 deletions functions/profileRuntime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Resource limits for the two small account-profile operations only.
// These reduce idle/scaling exposure; they are not a monthly spending cap.
module.exports = Object.freeze({
minInstances: 0,
maxInstances: 2,
memory: '256MB',
timeoutSeconds: 30,
});
Loading