fix(r2): upload and sign with s3 keys

This commit is contained in:
Ben Allfree authored and GitHub committed 2026-09-28 05:07:33 -07:00
1 parent b55aafb742
commit c84bf442d3
4 files changed
+75 -42

No files matched your search

+33 -30
View File
@@ -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 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` ## Convex dashboard
- `AUTH_GITHUB_SECRET`
- `AUTH_GOOGLE_ID`
- `AUTH_GOOGLE_SECRET`
- `JWKS`
- `JWT_PRIVATE_KEY`
## 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`) | Name | Workflow |
- `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` | `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` ## When adding a variable
- `R2_SECRET_ACCESS_KEY`
- `R2_ACCOUNT_ID`
- `R2_BUCKET_NAME`
- `R2_ENDPOINT_URL`
- `R2_PUBLIC_URL`
- `R2_CLOUDFLARE_TOKEN`
**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. Add the name here in the same change as the code that reads it.
## 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.
+4 -3
View File
@@ -169,7 +169,8 @@ jobs:
id: pio id: pio
env: env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 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 }} R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
shell: bash shell: bash
run: | run: |
@@ -179,8 +180,8 @@ jobs:
BUILD_DIR_ABS="$ROOT/.pio/build/${{ inputs.target_env }}" BUILD_DIR_ABS="$ROOT/.pio/build/${{ inputs.target_env }}"
ARTIFACT_NAME="firmware-${{ inputs.build_key }}-${{ github.run_id }}.tar.gz" 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" 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}" bun install --frozen-lockfile --production
bunx wrangler r2 object put "$OBJECT_PATH" --file "/tmp/$ARTIFACT_NAME" --remote 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" STEP_INDEX=10 LABEL="Firmware bundle uploaded" python3 "${{ github.workspace }}/scripts/ci/report-convex-ci-progress.py"
echo "r2_key=${ARTIFACT_NAME}" >> "$GITHUB_OUTPUT" echo "r2_key=${ARTIFACT_NAME}" >> "$GITHUB_OUTPUT"
+17 -9
View File
@@ -1,21 +1,19 @@
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3" import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner" import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
export async function generateSignedDownloadUrl( export function createR2Client(): { client: S3Client; bucketName: string } {
objectKey: string, const accountId = process.env.CLOUDFLARE_ACCOUNT_ID
filename: string,
contentType: string = "application/octet-stream"
): Promise<string> {
const accountId = process.env.R2_ACCOUNT_ID
const accessKeyId = process.env.R2_ACCESS_KEY_ID const accessKeyId = process.env.R2_ACCESS_KEY_ID
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY
const bucketName = process.env.R2_BUCKET_NAME const bucketName = process.env.R2_BUCKET_NAME
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) { 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", region: "auto",
endpoint: `https://${accountId}.r2.cloudflarestorage.com`, endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
credentials: { 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<string> {
const { client, bucketName } = createR2Client()
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: bucketName, Bucket: bucketName,
Key: objectKey, Key: objectKey,
@@ -32,5 +40,5 @@ export async function generateSignedDownloadUrl(
}) })
// URL expires in 1 hour (3600 seconds) // URL expires in 1 hour (3600 seconds)
return await getSignedUrl(s3, command, { expiresIn: 3600 }) return await getSignedUrl(client, command, { expiresIn: 3600 })
} }
+21
View File
@@ -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 <file> <objectKey>")
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}`)