diff --git a/.cursor/rules/convex-env.mdc b/.cursor/rules/convex-env.mdc index 3141b27..5b73961 100644 --- a/.cursor/rules/convex-env.mdc +++ b/.cursor/rules/convex-env.mdc @@ -1,45 +1,48 @@ --- -description: Convex deployment environment variable names (MeshEnvy mesh-forge); values are secrets — never commit them. +description: Environment variable names for MeshForge. Values are secrets. Never commit them. alwaysApply: true --- -# Convex environment variables (this deployment) +# Environment variables -These names are configured in the Convex dashboard for this project. **Do not put real values in the repo or in rules.** +Names only. The Convex dashboard and GitHub Actions secrets are separate stores. A name that appears in both must hold the same value. -## Auth (OAuth / Convex Auth) +Convex injects `CONVEX_SITE_URL`. `convex deploy` injects `VITE_CONVEX_URL` at build time. Do not add those to either secret list. -- `AUTH_GITHUB_ID` -- `AUTH_GITHUB_SECRET` -- `AUTH_GOOGLE_ID` -- `AUTH_GOOGLE_SECRET` -- `JWKS` -- `JWT_PRIVATE_KEY` +## Convex dashboard -## App / Convex Auth config +| Name | Read by | +| --- | --- | +| `AUTH_GITHUB_ID` | Convex Auth | +| `AUTH_GITHUB_SECRET` | Convex Auth | +| `JWKS` | Convex Auth | +| `JWT_PRIVATE_KEY` | Convex Auth | +| `SITE_URL` | Convex Auth | +| `GITHUB_TOKEN` | `convex/actions.ts`, `repoScans.ts`, `repoTags.ts` | +| `CONVEX_BUILD_TOKEN` | `convex/http.ts` | +| `CLOUDFLARE_ACCOUNT_ID` | `convex/lib/r2.ts` | +| `R2_ACCESS_KEY_ID` | `convex/lib/r2.ts` | +| `R2_SECRET_ACCESS_KEY` | `convex/lib/r2.ts` | +| `R2_BUCKET_NAME` | `convex/lib/r2.ts` | -- `SITE_URL` — canonical site URL (often used by auth / redirects; set in dashboard as provided) +`CONVEX_ENV=dev` makes `dispatchRepoBuild` use workflow ref `develop`. Unset means `main`. Both use `.github/workflows/custom_build.yml`. -**Code note:** Convex code in this repo also reads `CONVEX_SITE_URL` for `auth.config.ts` (JWT domain) and `actions.ts` (GitHub Actions `convex_url` input). If something breaks around auth domain or workflow callbacks, confirm both dashboard vars match what Convex and the frontend expect. +`CLOUDFLARE_ACCOUNT_ID` is the Cloudflare account id. `r2.ts` uses it as the R2 S3 endpoint host. -## CI / GitHub integration +## GitHub Actions secrets -- `GITHUB_TOKEN` — GitHub API (repo scans, branches, dispatching `custom_build*.yml`) -- `CONVEX_ENV` — when `dev`, workflow dispatch uses **`custom_build_test.yml`** on git ref **`v2`** on `MeshEnvy/mesh-forge`; otherwise **`custom_build.yml`** on **`main`** -- `CONVEX_BUILD_TOKEN` — shared with GitHub Actions secret of the same name; bearer auth for `POST /ingest-repo-build` +| Name | Workflow | +| --- | --- | +| `CLOUDFLARE_ACCOUNT_ID` | `custom_build.yml` | +| `CLOUDFLARE_API_TOKEN` | `deploy.yml` | +| `CONVEX_BUILD_TOKEN` | `custom_build.yml` | +| `CONVEX_DEPLOY_KEY` | `deploy.yml` | +| `R2_ACCESS_KEY_ID` | `custom_build.yml` | +| `R2_SECRET_ACCESS_KEY` | `custom_build.yml` | +| `R2_BUCKET_NAME` | `custom_build.yml` | -## R2 (firmware artifacts / signed downloads) +Shared with Convex, same value: `CLOUDFLARE_ACCOUNT_ID`, `CONVEX_BUILD_TOKEN`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET_NAME`. -- `R2_ACCESS_KEY_ID` -- `R2_SECRET_ACCESS_KEY` -- `R2_ACCOUNT_ID` -- `R2_BUCKET_NAME` -- `R2_ENDPOINT_URL` -- `R2_PUBLIC_URL` -- `R2_CLOUDFLARE_TOKEN` +## When adding a variable -**Code note:** `convex/lib/r2.ts` currently uses `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET_NAME` and builds the S3 endpoint from account id. Extra R2-related vars may be used elsewhere or reserved for future use. - -## When editing Convex code - -Prefer reading env vars that already exist in dashboard; if you introduce a new required variable, document it here and in any operator-facing setup notes. +Add the name here in the same change as the code that reads it. diff --git a/.github/workflows/custom_build.yml b/.github/workflows/custom_build.yml index 37a3d5b..8439f1e 100644 --- a/.github/workflows/custom_build.yml +++ b/.github/workflows/custom_build.yml @@ -169,7 +169,8 @@ jobs: id: pio env: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }} shell: bash run: | @@ -179,8 +180,8 @@ jobs: BUILD_DIR_ABS="$ROOT/.pio/build/${{ inputs.target_env }}" ARTIFACT_NAME="firmware-${{ inputs.build_key }}-${{ github.run_id }}.tar.gz" MESHTASTIC_OTA=1 bash "${{ github.workspace }}/scripts/ci/common/fw-bundle-pipeline.sh" "$ROOT" "$BUILD_DIR_ABS" "/tmp/$ARTIFACT_NAME" - OBJECT_PATH="${R2_BUCKET_NAME}/${ARTIFACT_NAME}" - bunx wrangler r2 object put "$OBJECT_PATH" --file "/tmp/$ARTIFACT_NAME" --remote + bun install --frozen-lockfile --production + bun scripts/ci/upload-firmware-r2.ts "/tmp/$ARTIFACT_NAME" "$ARTIFACT_NAME" STEP_INDEX=10 LABEL="Firmware bundle uploaded" python3 "${{ github.workspace }}/scripts/ci/report-convex-ci-progress.py" echo "r2_key=${ARTIFACT_NAME}" >> "$GITHUB_OUTPUT" diff --git a/convex/lib/r2.ts b/convex/lib/r2.ts index 8086348..70ab81d 100644 --- a/convex/lib/r2.ts +++ b/convex/lib/r2.ts @@ -1,21 +1,19 @@ import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3" import { getSignedUrl } from "@aws-sdk/s3-request-presigner" -export async function generateSignedDownloadUrl( - objectKey: string, - filename: string, - contentType: string = "application/octet-stream" -): Promise { - const accountId = process.env.R2_ACCOUNT_ID +export function createR2Client(): { client: S3Client; bucketName: string } { + const accountId = process.env.CLOUDFLARE_ACCOUNT_ID const accessKeyId = process.env.R2_ACCESS_KEY_ID const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY const bucketName = process.env.R2_BUCKET_NAME if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) { - throw new Error("R2 credentials are not set") + throw new Error( + "CLOUDFLARE_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and R2_BUCKET_NAME must be set" + ) } - const s3 = new S3Client({ + const client = new S3Client({ region: "auto", endpoint: `https://${accountId}.r2.cloudflarestorage.com`, credentials: { @@ -24,6 +22,16 @@ export async function generateSignedDownloadUrl( }, }) + return { client, bucketName } +} + +export async function generateSignedDownloadUrl( + objectKey: string, + filename: string, + contentType: string = "application/octet-stream" +): Promise { + const { client, bucketName } = createR2Client() + const command = new GetObjectCommand({ Bucket: bucketName, Key: objectKey, @@ -32,5 +40,5 @@ export async function generateSignedDownloadUrl( }) // URL expires in 1 hour (3600 seconds) - return await getSignedUrl(s3, command, { expiresIn: 3600 }) + return await getSignedUrl(client, command, { expiresIn: 3600 }) } diff --git a/scripts/ci/upload-firmware-r2.ts b/scripts/ci/upload-firmware-r2.ts new file mode 100644 index 0000000..1abc76d --- /dev/null +++ b/scripts/ci/upload-firmware-r2.ts @@ -0,0 +1,21 @@ +import { createReadStream } from "node:fs" +import { PutObjectCommand } from "@aws-sdk/client-s3" +import { createR2Client } from "../../convex/lib/r2" + +const filePath = process.argv[2] +const objectKey = process.argv[3] +if (!filePath || !objectKey) { + console.error("usage: upload-firmware-r2.ts ") + process.exit(1) +} + +const { client, bucketName } = createR2Client() +await client.send( + new PutObjectCommand({ + Bucket: bucketName, + Key: objectKey, + Body: createReadStream(filePath), + ContentType: "application/gzip", + }) +) +console.log(`uploaded ${objectKey} to ${bucketName}`)