From 4cbda38fd8a1a4eec71a313118c239e3dd1b9feb Mon Sep 17 00:00:00 2001 From: Bhavesh Patel Date: Tue, 18 Aug 2026 12:49:03 +0530 Subject: [PATCH 1/4] ci: automate sandbox image push and SANDBOX_IMAGE_URI pin --- .github/workflows/push-sandbox-image.yml | 77 +++++++++++++++++++++--- RELEASING.md | 6 +- scripts/changeset-sandbox-image.sh | 27 +++++++++ 3 files changed, 100 insertions(+), 10 deletions(-) create mode 100755 scripts/changeset-sandbox-image.sh diff --git a/.github/workflows/push-sandbox-image.yml b/.github/workflows/push-sandbox-image.yml index 544efa0c1..67edd8657 100644 --- a/.github/workflows/push-sandbox-image.yml +++ b/.github/workflows/push-sandbox-image.yml @@ -1,20 +1,41 @@ name: Push sandbox image -# Build and push the Daytona sandbox base image to JFrog, tagged with the -# commit SHA of the ref selected at dispatch time. +# PR: docker build only. main/dispatch: push :, then pin SANDBOX_IMAGE_URI. on: workflow_dispatch: + push: + branches: [main] + paths: + - 'packages/trueforge-core/scripts/sandbox/**' + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'packages/trueforge-core/scripts/sandbox/**' concurrency: - group: push-sandbox-image - cancel-in-progress: false + group: push-sandbox-image-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read jobs: + verify: + name: Build sandbox image (no push) + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - run: | + docker build \ + --platform linux/amd64 \ + -f packages/trueforge-core/scripts/sandbox/sandbox.Dockerfile \ + packages/trueforge-core/scripts/sandbox + build: name: Build and push sandbox image + if: github.event_name != 'pull_request' uses: truefoundry/github-workflows-public/.github/workflows/build.yml@main permissions: id-token: write @@ -39,11 +60,51 @@ jobs: needs: [build] runs-on: ubuntu-latest steps: - - name: Print image URI - env: + - env: IMAGE_URI: ${{ vars.TRUEFOUNDRY_ARTIFACTORY_PUBLIC_REPOSITORY }}/trueforge-sandbox:${{ github.sha }} run: | - set -euo pipefail echo "Pushed image URI: $IMAGE_URI" - echo "### Sandbox image" >> "$GITHUB_STEP_SUMMARY" echo "\`$IMAGE_URI\`" >> "$GITHUB_STEP_SUMMARY" + + pin: + name: Open SANDBOX_IMAGE_URI pin PR + if: github.ref == 'refs/heads/main' + needs: [build] + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + env: + IMAGE_URI: ${{ vars.TRUEFOUNDRY_ARTIFACTORY_PUBLIC_REPOSITORY }}/trueforge-sandbox:${{ github.sha }} + steps: + - uses: actions/checkout@v4 + + - name: Write SANDBOX_IMAGE_URI + run: | + python3 <<'PY' + import os, re + from pathlib import Path + uri = os.environ["IMAGE_URI"] + if not re.fullmatch(r"[A-Za-z0-9._:/-]+", uri): + raise SystemExit(f"refusing IMAGE_URI: {uri!r}") + Path("packages/trueforge-core/src/core/sandbox/sandboxImage.ts").write_text( + "/** Release-owned sandbox image URI; rewritten by push-sandbox-image.yml. */\n" + f"export const SANDBOX_IMAGE_URI = '{uri}';\n" + ) + PY + + - run: bash scripts/changeset-sandbox-image.sh + + - uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: release-sandbox-image + base: main + commit-message: 'chore: pin sandbox image ${{ github.sha }}' + title: 'chore: pin sandbox image ${{ github.sha }}' + body: | + Pushed `${{ env.IMAGE_URI }}`. Merge so `release.yml` can publish the pin. + Existing tenants stay on `build_metadata`. + add-paths: | + packages/trueforge-core/src/core/sandbox/sandboxImage.ts + .changeset diff --git a/RELEASING.md b/RELEASING.md index f159af5e4..f3ddacfc4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,13 +1,14 @@ # Releasing -This repo ships npm packages, a production container image, a Helm chart, and -optional from-source **dev** images. +This repo ships npm packages, a production container image, a Helm chart, a +sandbox image, and optional from-source **dev** images. | What | Trigger | Workflow | | ----------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | npm packages | Push to `main` (Changesets) | [`release.yml`](.github/workflows/release.yml) | | Prod image + chart-release PR | After `@truefoundry/trueforge` npm publish (reusable workflow), or manual dispatch | [`build-and-prepare-chart-release.yml`](.github/workflows/build-and-prepare-chart-release.yml) | | Chart tag, GitHub Release, OCI push | Merge of `release-chart/trueforge`, or push/dispatch of `charts/trueforge@*` | [`release-chart.yml`](.github/workflows/release-chart.yml) | +| Sandbox image + pin PR | Push to `main` when `scripts/sandbox/**` changes, or dispatch | [`push-sandbox-image.yml`](.github/workflows/push-sandbox-image.yml) | | Dev (from-source) image | Manual `workflow_dispatch` | [`build-dev-image.yml`](.github/workflows/build-dev-image.yml) | ## Versioning @@ -19,6 +20,7 @@ optional from-source **dev** images. | Prod image | Root [`Dockerfile`](Dockerfile): `npm install @truefoundry/trueforge@$APP_VERSION` | | Prod image tag | `{appVersion}-{shortSha}` (shortSha of the build commit) | | Chart `version` | Independent SemVer; git tag `charts/trueforge@A.B.C` must match | +| Sandbox image | [`sandbox.Dockerfile`](packages/trueforge-core/scripts/sandbox/sandbox.Dockerfile); tag = full commit SHA | | Dev image | [`Dockerfile.dev`](Dockerfile.dev); tag = full commit SHA | Install a published chart: diff --git a/scripts/changeset-sandbox-image.sh b/scripts/changeset-sandbox-image.sh new file mode 100755 index 000000000..bd9cfcd2e --- /dev/null +++ b/scripts/changeset-sandbox-image.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Patch-changeset @truefoundry/trueforge-core unless a pending file already names it. +set -euo pipefail +cd "$(dirname "$0")/.." + +shopt -s nullglob +for file in .changeset/*.md; do + if [[ "$(basename "$file")" == "README.md" ]]; then + continue + fi + if grep -Eq "^['\"]@truefoundry/trueforge-core['\"]:" "$file"; then + echo "Pending changeset already names @truefoundry/trueforge-core ($(basename "$file")); skipping." + exit 0 + fi +done + +slug="pin-sandbox-image" +outfile=".changeset/$(date -u +%Y%m%d%H%M%S)-${slug}.md" +cat >"$outfile" <<'EOF' +--- +"@truefoundry/trueforge-core": patch +--- + +Pin SANDBOX_IMAGE_URI to the image pushed by CI. +EOF + +echo "Wrote $outfile" From 749ca57386d594e41e7d81fc3f103b1f915b9942 Mon Sep 17 00:00:00 2001 From: Bhavesh Patel Date: Tue, 18 Aug 2026 15:05:01 +0530 Subject: [PATCH 2/4] format fixes --- RELEASING.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index f3ddacfc4..596b17280 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -13,15 +13,15 @@ sandbox image, and optional from-source **dev** images. ## Versioning -| Artifact | Identity | -| ---------------------------- | ---------------------------------------------------------------------------------- | -| npm `@truefoundry/trueforge` | SemVer `X.Y.Z` — source of truth for app bits | -| Chart `appVersion` | A **published** npm version | -| Prod image | Root [`Dockerfile`](Dockerfile): `npm install @truefoundry/trueforge@$APP_VERSION` | -| Prod image tag | `{appVersion}-{shortSha}` (shortSha of the build commit) | -| Chart `version` | Independent SemVer; git tag `charts/trueforge@A.B.C` must match | +| Artifact | Identity | +| ---------------------------- | --------------------------------------------------------------------------------------------------------- | +| npm `@truefoundry/trueforge` | SemVer `X.Y.Z` — source of truth for app bits | +| Chart `appVersion` | A **published** npm version | +| Prod image | Root [`Dockerfile`](Dockerfile): `npm install @truefoundry/trueforge@$APP_VERSION` | +| Prod image tag | `{appVersion}-{shortSha}` (shortSha of the build commit) | +| Chart `version` | Independent SemVer; git tag `charts/trueforge@A.B.C` must match | | Sandbox image | [`sandbox.Dockerfile`](packages/trueforge-core/scripts/sandbox/sandbox.Dockerfile); tag = full commit SHA | -| Dev image | [`Dockerfile.dev`](Dockerfile.dev); tag = full commit SHA | +| Dev image | [`Dockerfile.dev`](Dockerfile.dev); tag = full commit SHA | Install a published chart: From a810950de9b05a37ed67bffd648359f9149a514b Mon Sep 17 00:00:00 2001 From: Bhavesh Patel Date: Tue, 18 Aug 2026 17:22:05 +0530 Subject: [PATCH 3/4] fix: always write a changeset for SANDBOX_IMAGE_URI pin PRs --- scripts/changeset-sandbox-image.sh | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/scripts/changeset-sandbox-image.sh b/scripts/changeset-sandbox-image.sh index bd9cfcd2e..07eb51923 100755 --- a/scripts/changeset-sandbox-image.sh +++ b/scripts/changeset-sandbox-image.sh @@ -1,19 +1,8 @@ #!/usr/bin/env bash -# Patch-changeset @truefoundry/trueforge-core unless a pending file already names it. +# Always write: this lands on a separate pin PR; pending main files may be consumed first. set -euo pipefail cd "$(dirname "$0")/.." -shopt -s nullglob -for file in .changeset/*.md; do - if [[ "$(basename "$file")" == "README.md" ]]; then - continue - fi - if grep -Eq "^['\"]@truefoundry/trueforge-core['\"]:" "$file"; then - echo "Pending changeset already names @truefoundry/trueforge-core ($(basename "$file")); skipping." - exit 0 - fi -done - slug="pin-sandbox-image" outfile=".changeset/$(date -u +%Y%m%d%H%M%S)-${slug}.md" cat >"$outfile" <<'EOF' From 010602055c4beedb4deff60d27031d2772ee8852 Mon Sep 17 00:00:00 2001 From: Bhavesh Patel Date: Wed, 19 Aug 2026 11:08:24 +0530 Subject: [PATCH 4/4] refactor: update push-sandbox-image workflow to use github.ref for concurrency and improve SANDBOX_IMAGE_URI handling --- .github/workflows/push-sandbox-image.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/push-sandbox-image.yml b/.github/workflows/push-sandbox-image.yml index 67edd8657..752634d4f 100644 --- a/.github/workflows/push-sandbox-image.yml +++ b/.github/workflows/push-sandbox-image.yml @@ -13,7 +13,9 @@ on: - 'packages/trueforge-core/scripts/sandbox/**' concurrency: - group: push-sandbox-image-${{ github.event_name }}-${{ github.ref }} + # github.ref (not event_name): push and dispatch on main share a group so they + # cannot pin in parallel. Do not cancel an in-flight JFrog push. + group: push-sandbox-image-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: @@ -80,20 +82,33 @@ jobs: - uses: actions/checkout@v4 - name: Write SANDBOX_IMAGE_URI + id: pin-uri run: | python3 <<'PY' import os, re from pathlib import Path + uri = os.environ["IMAGE_URI"] if not re.fullmatch(r"[A-Za-z0-9._:/-]+", uri): raise SystemExit(f"refusing IMAGE_URI: {uri!r}") - Path("packages/trueforge-core/src/core/sandbox/sandboxImage.ts").write_text( + + path = Path("packages/trueforge-core/src/core/sandbox/sandboxImage.ts") + text = path.read_text() + match = re.search(r"export const SANDBOX_IMAGE_URI = '([^']+)';", text) + changed = match is None or match.group(1) != uri + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh: + fh.write(f"changed={'true' if changed else 'false'}\n") + if not changed: + print(f"already pinned to {uri}") + raise SystemExit(0) + path.write_text( "/** Release-owned sandbox image URI; rewritten by push-sandbox-image.yml. */\n" f"export const SANDBOX_IMAGE_URI = '{uri}';\n" ) PY - - run: bash scripts/changeset-sandbox-image.sh + - if: steps.pin-uri.outputs.changed == 'true' + run: bash scripts/changeset-sandbox-image.sh - uses: peter-evans/create-pull-request@v7 with: