diff --git a/.changeset/sandbox-image-json-pin.md b/.changeset/sandbox-image-json-pin.md new file mode 100644 index 000000000..dff6e92c6 --- /dev/null +++ b/.changeset/sandbox-image-json-pin.md @@ -0,0 +1,5 @@ +--- +'@truefoundry/trueforge-core': patch +--- + +Load SANDBOX_IMAGE_URI from sandboxImage.json so CI can rewrite the pin. diff --git a/.github/workflows/push-sandbox-image.yml b/.github/workflows/push-sandbox-image.yml index 544efa0c1..30d85a737 100644 --- a/.github/workflows/push-sandbox-image.yml +++ b/.github/workflows/push-sandbox-image.yml @@ -1,9 +1,12 @@ 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. +# main / dispatch: push trueforge-sandbox:, then open a PR that updates sandboxImage.json. on: workflow_dispatch: + push: + branches: [main] + paths: + - 'packages/trueforge-core/scripts/sandbox/**' concurrency: group: push-sandbox-image @@ -39,11 +42,67 @@ 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 + steps: + - id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.TRUEFORGE_GENERATE_SDK_APP_ID }} + private-key: ${{ secrets.TRUEFORGE_GENERATE_SDK_APP_PRIVATE_KEY }} + + - uses: actions/checkout@v4 + with: + token: ${{ steps.app-token.outputs.token }} + + - id: image + env: + REPO: ${{ vars.TRUEFOUNDRY_ARTIFACTORY_PUBLIC_REPOSITORY }} + SHA: ${{ github.sha }} + run: echo "uri=${REPO}/trueforge-sandbox:${SHA}" >> "$GITHUB_OUTPUT" + + - name: Write sandboxImage.json + id: pin-uri + env: + IMAGE_URI: ${{ steps.image.outputs.uri }} + run: | + set -euo pipefail + path=packages/trueforge-core/src/core/sandbox/sandboxImage.json + jq -n --arg uri "$IMAGE_URI" '{uri: $uri}' >"$path" + if git diff --quiet -- "$path"; then + echo "changed=false" >>"$GITHUB_OUTPUT" + else + echo "changed=true" >>"$GITHUB_OUTPUT" + fi + + - if: steps.pin-uri.outputs.changed == 'true' + run: bash scripts/changeset-sandbox-image.sh + + - uses: peter-evans/create-pull-request@v7 + with: + # GitHub will not start new workflow runs from GITHUB_TOKEN (anti-recursion). + # ci.yml would never run (or sit approval-required); required checks stay + # pending and this pin PR cannot merge. An App token is treated as a user. + token: ${{ steps.app-token.outputs.token }} + branch: release-sandbox-image + base: main + commit-message: 'chore: update sandbox image ${{ github.sha }}' + title: 'chore: update sandbox image ${{ github.sha }}' + body: | + Pushed `${{ steps.image.outputs.uri }}`. Merge so `release.yml` can publish the pin. + Existing tenants stay on `build_metadata`. + add-paths: | + packages/trueforge-core/src/core/sandbox/sandboxImage.json + .changeset diff --git a/RELEASING.md b/RELEASING.md index f159af5e4..596b17280 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,25 +1,27 @@ # 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 -| 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 | -| Dev image | [`Dockerfile.dev`](Dockerfile.dev); tag = full commit SHA | +| 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 | Install a published chart: diff --git a/packages/trueforge-core/src/core/sandbox/sandboxImage.json b/packages/trueforge-core/src/core/sandbox/sandboxImage.json new file mode 100644 index 000000000..c822466f0 --- /dev/null +++ b/packages/trueforge-core/src/core/sandbox/sandboxImage.json @@ -0,0 +1,3 @@ +{ + "uri": "tfy.jfrog.io/tfy-images/trueforge-sandbox:ce9e802ff68514dfc6ff138ff81c8f26dbaaf724" +} diff --git a/packages/trueforge-core/src/core/sandbox/sandboxImage.ts b/packages/trueforge-core/src/core/sandbox/sandboxImage.ts index 82ec08f96..8097348de 100644 --- a/packages/trueforge-core/src/core/sandbox/sandboxImage.ts +++ b/packages/trueforge-core/src/core/sandbox/sandboxImage.ts @@ -1,5 +1,4 @@ -/** - * Release-owned sandbox image. In-memory for now; CI will later replace this - * with the tag it pushes on each release. - */ -export const SANDBOX_IMAGE_URI = 'tfy.jfrog.io/tfy-images/trueforge-sandbox:ce9e802ff68514dfc6ff138ff81c8f26dbaaf724'; +import sandboxImage from './sandboxImage.json' with { type: 'json' }; + +/** Release-owned sandbox image URI; CI rewrites `sandboxImage.json`. */ +export const SANDBOX_IMAGE_URI = sandboxImage.uri; diff --git a/packages/trueforge-core/tsup.config.ts b/packages/trueforge-core/tsup.config.ts index da5c9d611..e8a76c1f1 100644 --- a/packages/trueforge-core/tsup.config.ts +++ b/packages/trueforge-core/tsup.config.ts @@ -1,4 +1,5 @@ import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions'; +import { join } from 'node:path'; import { defineConfig } from 'tsup'; // every src module compiles to its own .js (CJS) + .mjs (ESM) pair so consumers can deep-import real file paths. @@ -9,7 +10,19 @@ export default defineConfig({ // specifiers per format ('./foo' -> './foo.mjs' / './foo.js'). It requires // bundle mode, but with every file as an entry nothing actually inlines. bundle: true, - esbuildPlugins: [esbuildPluginFilePathExtensions({ esmExtension: 'mjs', cjsExtension: 'js' })], + esbuildPlugins: [ + // file-path-extensions treats .json as extensionless and would emit + // `./sandboxImage.json.mjs` (broken); resolve to an absolute path first so esbuild inlines it. + { + name: 'bundle-sandbox-image-json', + setup(build) { + build.onResolve({ filter: /^\.\/sandboxImage\.json$/ }, args => ({ + path: join(args.resolveDir, args.path), + })); + }, + }, + esbuildPluginFilePathExtensions({ esmExtension: 'mjs', cjsExtension: 'js' }), + ], outExtension({ format }) { return { js: format === 'cjs' ? '.js' : '.mjs' }; }, diff --git a/scripts/changeset-sandbox-image.sh b/scripts/changeset-sandbox-image.sh new file mode 100755 index 000000000..10ef74f42 --- /dev/null +++ b/scripts/changeset-sandbox-image.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Always write: this lands on a separate pin PR; pending main files may be consumed first. +set -euo pipefail +cd "$(dirname "$0")/.." + +slug="update-sandbox-image" +outfile=".changeset/$(date -u +%Y%m%d%H%M%S)-${slug}.md" +cat >"$outfile" <<'EOF' +--- +"@truefoundry/trueforge-core": patch +--- + +Update SANDBOX_IMAGE_URI to the image pushed by CI. +EOF + +echo "Wrote $outfile"