From 38dee32d2df15d0faf00c54de9ef525ca831b988 Mon Sep 17 00:00:00 2001 From: laurenfrederick Date: Wed, 28 Oct 2026 16:38:53 -0400 Subject: [PATCH 1/4] Initial draft release GitHub workflow --- .github/workflows/draftRelease.yml | 186 +++++++++++++++++++++++++++++ Documentation~/release-guide.md | 32 ++--- package.json | 3 +- scripts/changelog.js | 143 ++++++++++++++++++++++ scripts/release-notes-template.md | 21 ++++ 5 files changed, 368 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/draftRelease.yml create mode 100644 scripts/changelog.js create mode 100644 scripts/release-notes-template.md diff --git a/.github/workflows/draftRelease.yml b/.github/workflows/draftRelease.yml new file mode 100644 index 00000000..af5dcd2c --- /dev/null +++ b/.github/workflows/draftRelease.yml @@ -0,0 +1,186 @@ +name: Draft Release +on: + push: + branches: [draft-release-workflow] + workflow_dispatch: + inputs: + version: + description: "Release version, for example 1.26.0" + required: true + type: string + source_branch: + description: "Branch whose build artifacts should be published" + required: false + default: "main" + type: string + run_id: + description: "Build run ID to publish. Defaults to the latest successful build of the source branch." + required: false + default: "" + type: string + dry_run: + description: "Preview the package and release notes without downloading the package or changing releases" + required: false + default: false + type: boolean + +jobs: + DraftRelease: + runs-on: ubuntu-latest + permissions: + contents: write + actions: read + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + VERSION: ${{ inputs.version }} + SOURCE_BRANCH: ${{ inputs.source_branch }} + RUN_ID_INPUT: ${{ inputs.run_id }} + DRY_RUN: ${{ inputs.dry_run }} + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Validate inputs + run: | + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid version '$VERSION'. Expected X.Y.Z, for example 1.26.0." + exit 1 + fi + + - name: Resolve the build to publish + run: | + if [[ -n "$RUN_ID_INPUT" ]]; then + run_id="$RUN_ID_INPUT" + conclusion=$(gh run view "$run_id" --json conclusion --jq .conclusion) + if [[ "$conclusion" != "success" ]]; then + echo "::error::Build run $run_id concluded with '$conclusion'. Only successful builds can be published." + exit 1 + fi + else + run_id=$(gh run list --workflow build.yml --branch "$SOURCE_BRANCH" --status success --limit 1 --json databaseId --jq '.[0].databaseId') + fi + + if [[ -z "$run_id" || "$run_id" == "null" ]]; then + echo "::error::No successful build.yml run found for branch '$SOURCE_BRANCH'. Wait for CI to pass, or pass an explicit run_id." + exit 1 + fi + + head_sha=$(gh run view "$run_id" --json headSha --jq .headSha) + echo "Selected package from run $run_id (commit $head_sha, requested branch $SOURCE_BRANCH)." + echo "RUN_ID=$run_id" >> "$GITHUB_ENV" + echo "HEAD_SHA=$head_sha" >> "$GITHUB_ENV" + + - name: Inspect artifacts and print the package plan + run: | + artifacts=$(gh api --paginate --slurp "repos/${GH_REPO}/actions/runs/${RUN_ID}/artifacts") + echo "--- artifacts found in run $RUN_ID ---" + jq -r '.[] | .artifacts[] | "\(.name): id=\(.id), size=\(.size_in_bytes) bytes, expired=\(.expired)"' <<< "$artifacts" + + matches=$(jq '[.[] | .artifacts[] | select(.name == "Combined Package" and .expired == false)] | length' <<< "$artifacts") + if [[ "$matches" -ne 1 ]]; then + echo "::error::Expected one available artifact named 'Combined Package', found $matches." + exit 1 + fi + + name=$(jq -r ".name" package.json) + echo "--- package plan: artifact -> release asset ---" + echo "Combined Package -> ${name}-${VERSION}.tgz" + + - name: Download and verify the release package + if: ${{ !inputs.dry_run }} + run: | + mkdir -p out + gh run download "$RUN_ID" --name "Combined Package" --dir out + + expected="$(jq -r ".name" package.json)-${VERSION}.tgz" + actual=$(find out -maxdepth 1 -name '*.tgz' | wc -l | tr -d ' ') + if [[ "$actual" -ne 1 ]]; then + echo "::error::Expected exactly one .tgz file in the Combined Package artifact, found $actual." + exit 1 + fi + + found=$(basename "$(find out -maxdepth 1 -name '*.tgz')") + if [[ "$found" != "$expected" ]]; then + echo "::error::Expected package '$expected' but the build produced '$found'. Does package.json's version match the requested release version?" + exit 1 + fi + + echo "--- SHA256 of the package to be uploaded ---" + sha256sum "out/$found" + + - name: Generate the release notes + run: | + if ! changelog=$(node scripts/changelog.js --version "$VERSION" 2> changelog-error.txt); then + reason=$(cat changelog-error.txt) + if [[ "$DRY_RUN" == "true" ]]; then + echo "::warning::Could not extract the changelog for v${VERSION}: ${reason}" + changelog="_Dry run: no changelog section for v${VERSION} was found in CHANGES.md._" + else + echo "::error::Could not extract the changelog for v${VERSION}: ${reason}" + exit 1 + fi + fi + + CHANGELOG="$changelog" python3 - > notes.md <<'PYTHON' + import os + + with open("scripts/release-notes-template.md", encoding="utf-8") as file: + notes = file.read() + + notes = notes.replace("{{CHANGELOG}}", os.environ["CHANGELOG"].strip()) + + print(notes.strip()) + PYTHON + + echo "--- release notes ---" + cat notes.md + + - name: Create or update the draft release (preview only in dry run) + run: | + tag="v${VERSION}" + title="Cesium for Unity v${VERSION}" + echo "Release title: $title" + echo "Release tag: $tag" + echo "Target commit: $HEAD_SHA" + + if gh release view "$tag" > /dev/null 2>&1; then + if [[ "$(gh release view "$tag" --json isDraft --jq .isDraft)" != "true" ]]; then + if [[ "$DRY_RUN" != "true" ]]; then + echo "::error::Release $tag already exists and has been published. Refusing to modify it." + exit 1 + fi + echo "::warning::Release $tag already exists and has been published. A full run would stop here and refuse to modify it." + else + echo "Would update the existing draft $tag with the release notes printed above." + if [[ "$DRY_RUN" != "true" ]]; then + gh release edit "$tag" --title "$title" --notes-file notes.md --target "$HEAD_SHA" + fi + fi + else + echo "Would create draft $tag with the release notes printed above." + if [[ "$DRY_RUN" != "true" ]]; then + gh release create "$tag" --draft --title "$title" --notes-file notes.md --target "$HEAD_SHA" + fi + fi + + echo "Would upload the planned package, replacing an attachment with a matching name." + if [[ "$DRY_RUN" == "true" ]]; then + echo "Dry run: no package downloaded and no release changes made." + else + gh release upload "$tag" out/*.tgz --clobber + fi + + - name: Summarize + run: | + { + echo "### Draft release v${VERSION}" + echo + echo "Selected build: [${RUN_ID}](https://github.com/${GH_REPO}/actions/runs/${RUN_ID}) at commit \`${HEAD_SHA}\`." + echo + if [[ "$DRY_RUN" == "true" ]]; then + echo "**Dry run:** no package downloaded and no release changes made. See the logs for artifact metadata, the package plan, and release notes." + else + echo "Review the draft at https://github.com/${GH_REPO}/releases before publishing." + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/Documentation~/release-guide.md b/Documentation~/release-guide.md index edcdc06e..5c67c091 100644 --- a/Documentation~/release-guide.md +++ b/Documentation~/release-guide.md @@ -18,22 +18,22 @@ This is the process we follow when releasing a new version of Cesium for Unity o Cesium for Unity's release package is fairly large, so it can take awhile to upload it to the GitHub Releases page. For that reason, it is helpful to start the process now, even though we haven't tested the release yet. -1. Go to https://github.com/CesiumGS/cesium-unity/actions. -2. Click the most recent build of the `main` branch (it should be near the top). This should be the build for the commit you pushed in the previous section. If it's still running, wait. If it failed, you'll need to fix it. -3. Scroll down to **Artifacts**. -4. Download the artifact named "Combined Package". -5. Extract the `com.cesium.unity-1.21.0.tgz` (or similar) from the `Combined Package.zip` file. On macOS this is a pain to do, because Finder's unzip insists on also extracting the contents of the .tgz file. The best approach is probably to use `unzip` from the command-line. -6. Create a new release by visiting https://github.com/CesiumGS/cesium-unity/releases/new. -7. Leave the release tag blank for now, as we haven't created it yet. -8. Set `Release Title` to "Cesium for Unity v1.21.0", updating the version number as appropriate. -9. Copy the "release notes" section from a previous release, which you can find by visiting https://github.com/CesiumGS/cesium-unity/releases/latest and clicking the Edit button. Be careful not to change or save the previous release! -10. Update the version numbers as appropriate in the top section. Replace the changelog section with the actual changelog entries from this release. Copy it from `CHANGES.md`. -11. Upload the .tgz file by dragging it into the "Attach binaries" box. -12. Click Save Draft. Be careful not to publish it yet. -13. Visit the [releases page](https://github.com/CesiumGS/cesium-unity/releases) and you'll see the new draft release at the top. Expand the "Assets" section you should see the file you uploaded above. -14. Take a moment to verify that the SHA256 listed for each file is identical to the SHA256 for the same file that was printed at the end of the build by the "Print SHA256 of combined package" job. If they are, this proves the release package hasn't been inadvertently modified while you had a copy of it on your local machine. If they're different, stop the release process immediately! - -If the uploaded package is later found to have problems during testing, this step will need to be repeated. +The `Draft Release` GitHub Actions workflow does this for you. It copies the package directly from the build artifacts to a draft release, so it never passes through your local machine. + +1. Go to the [Draft Release workflow](https://github.com/CesiumGS/cesium-unity/actions/workflows/draftRelease.yml) and click "Run workflow". +2. Enter the release version, for example `1.26.0`, leave the other inputs at their defaults, and click "Run workflow". + - The workflow publishes the most recent _successful_ build of `main`, so it will fail if CI hasn't passed yet. + - To publish a different build, enter its run ID in the `run_id` input. +3. Wait for the workflow to finish. It will: + - Download the combined package from that build, for example `com.cesium.unity-1.26.0.tgz`, and verify its name matches the requested release version. + - Build the release notes from the `CHANGES.md` section for this version. + - Create a draft release named "Cesium for Unity v1.26.0" with the package attached, tagged `v1.26.0` against the exact commit that was built. The tag itself is not created until you publish the release. +4. Visit the [releases page](https://github.com/CesiumGS/cesium-unity/releases), expand the "Assets" section of the new draft, and confirm that the package is attached and that the release notes look correct. + - To preview the notes before running the workflow, use `npm run release-notes -- --version 1.26.0`. + +If the uploaded package is later found to have problems during testing, re-run the workflow. It updates the existing draft in place, and refuses to modify the release once it has been published. + +We used to download the package, extract it by hand, and then compare the SHA256 of the uploaded asset against the build artifact, to prove that it hadn't been modified while it was on a local machine. The workflow copies the package straight from the build artifact to the release, so there is no longer a local copy to verify. ## Update the "Cesium for Unity Samples" Project diff --git a/package.json b/package.json index 6d114e8b..a054085c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ }, "scripts": { "doxygen": "doxygen ./Documentation~/Doxyfile && cd Documentation~ && node copy-images.js && cd ..", - "preinstall": "echo \"ERROR: This is a Unity package, not an npm package. Do not run npm install.\" && exit 1" + "preinstall": "echo \"ERROR: This is a Unity package, not an npm package. Do not run npm install.\" && exit 1", + "release-notes": "node scripts/changelog.js" }, "changelogUrl": "https://github.com/CesiumGS/cesium-unity/blob/main/CHANGES.md", "documentationUrl": "https://cesium.com/learn/unity/", diff --git a/scripts/changelog.js b/scripts/changelog.js new file mode 100644 index 00000000..66b51dcf --- /dev/null +++ b/scripts/changelog.js @@ -0,0 +1,143 @@ +#!/usr/bin/env node + +/* + * Extracts a single release section from CHANGES.md. + * Used by the Draft Release workflow to build the GitHub release notes. + */ + +const fs = require("fs"); +const path = require("path"); + +function usage() { + console.log(`Usage: + node scripts/changelog.js --version [--changes-file ] + npm run release-notes -- --version [--changes-file ] + +Required: + --version Release version to extract, for example 1.26.0 (no v) + +Optional: + --changes-file Path to the changelog. Defaults to CHANGES.md in the repository root. + +Prints the changelog section for the requested version, including its heading, to stdout. +`); +} + +function parseArgs(argv) { + const args = {}; + + for (let index = 2; index < argv.length; ++index) { + const value = argv[index]; + + if (value === "--help" || value === "-h") { + args.help = true; + continue; + } + + if (!value.startsWith("--")) { + throw new Error(`Unexpected argument: ${value}`); + } + + const key = value.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); + const next = argv[++index]; + if (next === undefined || next.startsWith("--")) { + throw new Error(`Missing value for --${key}`); + } + + args[key] = next; + } + + return args; +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function isRealDate(value) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + return false; + } + + const parsed = new Date(`${value}T00:00:00Z`); + return !Number.isNaN(parsed.getTime()) && parsed.toISOString().startsWith(value); +} + +function extractReleaseSection(contents, version) { + const lines = contents.split(/\r?\n/); + const heading = new RegExp(`^## v${escapeRegExp(version)} - (.+?)\\s*$`); + + let start = -1; + let date = ""; + + for (let index = 0; index < lines.length; ++index) { + const match = heading.exec(lines[index]); + if (match) { + start = index; + date = match[1]; + break; + } + } + + if (start === -1) { + throw new Error( + `Could not find a "## v${version} - " heading in the changelog. Has the release section been added yet?` + ); + } + + if (!isRealDate(date)) { + throw new Error( + `The v${version} changelog heading has an unresolved release date: "${date}". Expected YYYY-MM-DD.` + ); + } + + let end = lines.length; + for (let index = start + 1; index < lines.length; ++index) { + if (lines[index].startsWith("## ")) { + end = index; + break; + } + } + + const body = lines.slice(start + 1, end).join("\n").trim(); + if (body === "") { + throw new Error(`The v${version} changelog section is empty.`); + } + + return `${lines[start]}\n\n${body}`; +} + +function main() { + const args = parseArgs(process.argv); + if (args.help) { + usage(); + return; + } + + if (!args.version) { + throw new Error("Missing required --version argument."); + } + + if (!/^\d+\.\d+\.\d+$/.test(args.version)) { + throw new Error(`Invalid version format: ${args.version}`); + } + + const changesPath = args.changesFile + ? path.resolve(args.changesFile) + : path.resolve(__dirname, "..", "CHANGES.md"); + + const contents = fs.readFileSync(changesPath, "utf8"); + + process.stdout.write(`${extractReleaseSection(contents, args.version)}\n`); +} + +module.exports = { extractReleaseSection }; + +if (require.main === module) { + try { + main(); + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + } +} diff --git a/scripts/release-notes-template.md b/scripts/release-notes-template.md new file mode 100644 index 00000000..76c84efc --- /dev/null +++ b/scripts/release-notes-template.md @@ -0,0 +1,21 @@ +Cesium for Unity works on Windows (x86-64), macOS (x86-64 and Apple Silicon), +Android (ARM64 and x86-64), iOS, Universal Windows Platform (ARM64 and x86-64), +and the Web (Unity 6+ only). + +## 🚀 Get Started + +1. [Download Cesium for Unity Samples](https://github.com/CesiumGS/cesium-unity-samples/releases/latest). +2. Extract the `.zip` file into a suitable location on your computer. +3. If you have [Unity Hub](https://unity.com/unity-hub) installed, click the "Open" button under the "Projects" tab. Otherwise, open +the Unity Editor directly and select "Open Project". Then, point it to the +extracted directory. Unity will load the project and also download Cesium for Unity +using the Package Manager. +4. In the Asset Browser, select and load `Assets -> Scenes -> 01_CesiumWorld`. You can also select other scenes as you browse and explore the samples. + +Or, to learn how to add it to your own project, see the [Cesium for Unity Quickstart](https://cesium.com/learn/unity/unity-quickstart/). + +Have questions? Ask them on the [community forum](https://community.cesium.com/). + +## 🕙 Change Log + +{{CHANGELOG}} From 2ce52c6966d011ee949bdc8de639edccd8ed18d0 Mon Sep 17 00:00:00 2001 From: laurenfrederick Date: Wed, 28 Oct 2026 17:09:31 -0400 Subject: [PATCH 2/4] Test download and verify release package --- .github/workflows/draftRelease.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/draftRelease.yml b/.github/workflows/draftRelease.yml index af5dcd2c..f12ec06c 100644 --- a/.github/workflows/draftRelease.yml +++ b/.github/workflows/draftRelease.yml @@ -88,7 +88,6 @@ jobs: echo "Combined Package -> ${name}-${VERSION}.tgz" - name: Download and verify the release package - if: ${{ !inputs.dry_run }} run: | mkdir -p out gh run download "$RUN_ID" --name "Combined Package" --dir out From 7595528014cc0da8073d778a4ea4d6911bb1c6ac Mon Sep 17 00:00:00 2001 From: laurenfrederick Date: Wed, 28 Oct 2026 17:16:17 -0400 Subject: [PATCH 3/4] Get draftRelease.yml ready for prod --- .github/workflows/draftRelease.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/draftRelease.yml b/.github/workflows/draftRelease.yml index f12ec06c..6a8c5c5f 100644 --- a/.github/workflows/draftRelease.yml +++ b/.github/workflows/draftRelease.yml @@ -1,7 +1,5 @@ name: Draft Release on: - push: - branches: [draft-release-workflow] workflow_dispatch: inputs: version: @@ -88,6 +86,7 @@ jobs: echo "Combined Package -> ${name}-${VERSION}.tgz" - name: Download and verify the release package + if: ${{ !inputs.dry_run }} run: | mkdir -p out gh run download "$RUN_ID" --name "Combined Package" --dir out From 394417973000f47d71f7e0284eb07e8605cf7a7a Mon Sep 17 00:00:00 2001 From: laurenfrederick Date: Thu, 29 Oct 2026 12:56:29 -0400 Subject: [PATCH 4/4] Add cesium-for-unity samples script --- Documentation~/release-guide.md | 21 ++- package.json | 3 +- scripts/update-unity-samples.js | 267 ++++++++++++++++++++++++++++++++ 3 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 scripts/update-unity-samples.js diff --git a/Documentation~/release-guide.md b/Documentation~/release-guide.md index 5c67c091..ebc7fdd2 100644 --- a/Documentation~/release-guide.md +++ b/Documentation~/release-guide.md @@ -37,12 +37,9 @@ We used to download the package, extract it by hand, and then compare the SHA256 ## Update the "Cesium for Unity Samples" Project -1. Clone a fresh copy of [cesium-unity-samples](https://github.com/CesiumGS/cesium-unity-samples) to a new directory. -2. Before opening the project, go to the Packages folder and open `Packages/manifest.json` in a text editor. Find the `com.cesium.unity` dependency under the `"dependencies"` object and change the version to the version number of the in-progress release. -3. Open `ProjectSettings/ProjectSettings.asset` and find the `bundleVersion` key (under `PlayerSettings`). Set this to the version number of the in-progress release. -4. Create a new Cesium ion token for this release under the "CesiumJS" account: +1. Create a new Cesium ion token for this release under the "CesiumJS" account: - Visit https://ion.cesium.com. - - Log in using your own `@cesium.com` email address. + - Log in using your own Bentley email address. - Click your name in the top-right corner and choose "Switch to CesiumJS". If you don't see this option, ask someone on the ion team to add you to the CesiumJS account. - Click "Access Tokens". - Create a new token named "Cesium for Unity Samples vX.Y.Z - Delete on MONTH DAY, YEAR", using real values for the version number and date. The date should be the date of the release two months out, using the month's name rather than its number. @@ -50,8 +47,18 @@ We used to download the package, extract it by hand, and then compare the SHA256 - Set the other properties as follows (these should be the defaults): - `Allow URLs`: "All Urls" - `Resources`: "All assets" -5. Open `Assets/CesiumSettings/Resources/CesiumIonServers/ion.cesium.com.asset` in a text editor and change the `defaultIonAccessToken` property to the new token you created above. -6. Commit the above changes but do _not_ push it yet. If you do, anyone trying to use cesium-unity-samples will be broken until the new version is published. +2. Run the `update-unity-samples` script. The ion token is only ever read from the `CESIUM_ION_TOKEN` environment variable, never accepted as a command-line argument, so it doesn't end up in your shell history or process list: + ``` + CESIUM_ION_TOKEN= npm run update-unity-samples -- --version 1.26.0 --samples-dir /path/to/cesium-unity-samples + ``` + If `--samples-dir` doesn't exist yet, the script clones a fresh copy of [cesium-unity-samples](https://github.com/CesiumGS/cesium-unity-samples) there. If it's an existing checkout, the script updates it with `git checkout main && git pull --ff-only`, and refuses to touch it if it has uncommitted changes. + + It then updates, in the samples checkout: + - `Packages/manifest.json`: the `com.cesium.unity` dependency version. + - `ProjectSettings/ProjectSettings.asset`: `PlayerSettings.bundleVersion`. + - `Assets/CesiumSettings/Resources/CesiumIonServers/ion.cesium.com.asset`: `defaultIonAccessToken`. + + Use `--dry-run` first to preview the planned changes (the token value itself is never printed). The script commits the changes locally but does _not_ push them. Do not push until the new package version is published, or anyone trying to use cesium-unity-samples will be broken in the meantime. ## Test the release candidate diff --git a/package.json b/package.json index a054085c..03d76177 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "scripts": { "doxygen": "doxygen ./Documentation~/Doxyfile && cd Documentation~ && node copy-images.js && cd ..", "preinstall": "echo \"ERROR: This is a Unity package, not an npm package. Do not run npm install.\" && exit 1", - "release-notes": "node scripts/changelog.js" + "release-notes": "node scripts/changelog.js", + "update-unity-samples": "node scripts/update-unity-samples.js" }, "changelogUrl": "https://github.com/CesiumGS/cesium-unity/blob/main/CHANGES.md", "documentationUrl": "https://cesium.com/learn/unity/", diff --git a/scripts/update-unity-samples.js b/scripts/update-unity-samples.js new file mode 100644 index 00000000..809604ae --- /dev/null +++ b/scripts/update-unity-samples.js @@ -0,0 +1,267 @@ +#!/usr/bin/env node + +/* + * Update Samples Project Steps (see "Update the Cesium for Unity Samples Project" in + * Documentation~/release-guide.md) + * - Sets the com.cesium.unity dependency version in Packages/manifest.json. + * - Sets PlayerSettings.bundleVersion in ProjectSettings/ProjectSettings.asset. + * - Sets defaultIonAccessToken in Assets/CesiumSettings/Resources/CesiumIonServers/ion.cesium.com.asset. + * - Commits the changes locally, but does not push. + * + * Cloning the samples repo, creating the ion token on ion.cesium.com, and pushing are still manual steps. + */ + +const fs = require("fs"); +const path = require("path"); +const { execFileSync } = require("child_process"); + +const SAMPLES_REPO_URL = "https://github.com/CesiumGS/cesium-unity-samples.git"; + +function usage() { + console.log(`Usage: + CESIUM_ION_TOKEN= npm run update-unity-samples -- --version --samples-dir [--no-commit] [--dry-run] + +Required: + --version New com.cesium.unity version to set in the samples project, for example 1.26.0 + --samples-dir Path to clone cesium-unity-samples into, or to an existing checkout to update + +Required environment variable: + CESIUM_ION_TOKEN New Cesium ion access token for the samples project. Only read from the + environment, never accepted as a CLI argument, so it doesn't end up in your + shell history or process list. + +Optional: + --repo-url Override the repository to clone/pull. Defaults to ${SAMPLES_REPO_URL} + --no-commit Write the files but skip the local git commit. + --dry-run Print planned changes without writing files, cloning, pulling, or committing. + The token itself is never printed. + +If --samples-dir does not exist, it is cloned fresh. If it already exists as a checkout of the +samples repo, it is updated with "git checkout main && git pull --ff-only"; the script refuses to +touch it if it has uncommitted changes. + +Updates the following files under --samples-dir: + - Packages/manifest.json: sets the com.cesium.unity dependency version. + - ProjectSettings/ProjectSettings.asset: sets PlayerSettings.bundleVersion. + - Assets/CesiumSettings/Resources/CesiumIonServers/ion.cesium.com.asset: sets defaultIonAccessToken. + +Commits the changes locally with git, but does not push. Creating the ion token on ion.cesium.com +and pushing are still manual steps. +`); +} + +function parseArgs(argv) { + const args = { dryRun: false, commit: true }; + + for (let index = 2; index < argv.length; ++index) { + const value = argv[index]; + + if (value === "--help" || value === "-h") { + args.help = true; + continue; + } + + if (value === "--dry-run") { + args.dryRun = true; + continue; + } + + if (value === "--no-commit") { + args.commit = false; + continue; + } + + if (!value.startsWith("--")) { + throw new Error(`Unexpected argument: ${value}`); + } + + const key = value.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); + const next = argv[++index]; + if (next === undefined || next.startsWith("--")) { + throw new Error(`Missing value for --${key}`); + } + + args[key] = next; + } + + return args; +} + +function readText(filePath) { + return fs.readFileSync(filePath, "utf8"); +} + +function writeText(filePath, contents) { + const existing = readText(filePath); + if (existing === contents) { + return false; + } + + fs.writeFileSync(filePath, contents); + + return true; +} + +function replaceDependencyVersion(contents, version) { + // Matches the "com.cesium.unity" entry under the "dependencies" object in Packages/manifest.json. + const pattern = /("com\.cesium\.unity"\s*:\s*")([^"]+)(")/; + if (!pattern.test(contents)) { + throw new Error('Could not find the "com.cesium.unity" dependency in manifest.json.'); + } + + return contents.replace(pattern, `$1${version}$3`); +} + +function replaceBundleVersion(contents, version) { + const pattern = /^(\s*bundleVersion:\s*).*$/m; + if (!pattern.test(contents)) { + throw new Error("Could not find bundleVersion in ProjectSettings.asset."); + } + + return contents.replace(pattern, `$1${version}`); +} + +function replaceIonToken(contents, token) { + const pattern = /^(\s*defaultIonAccessToken:\s*).*$/m; + if (!pattern.test(contents)) { + throw new Error("Could not find defaultIonAccessToken in ion.cesium.com.asset."); + } + + return contents.replace(pattern, `$1${token}`); +} + +function runGit(cwd, args, dryRun) { + if (dryRun) { + return; + } + + execFileSync("git", ["-C", cwd, ...args], { stdio: "inherit" }); +} + +function hasUncommittedChanges(samplesDir) { + const status = execFileSync("git", ["-C", samplesDir, "status", "--porcelain"], { encoding: "utf8" }).trim(); + return status !== ""; +} + +function ensureSamplesCheckout(samplesDir, repoUrl, dryRun) { + const exists = fs.existsSync(samplesDir); + + if (!exists) { + console.log(`- git clone ${repoUrl} into ${samplesDir}`); + if (dryRun) { + return { ready: false }; + } + + fs.mkdirSync(path.dirname(samplesDir), { recursive: true }); + execFileSync("git", ["clone", repoUrl, samplesDir], { stdio: "inherit" }); + return { ready: true }; + } + + if (!fs.existsSync(path.join(samplesDir, ".git"))) { + throw new Error(`${samplesDir} exists but is not a git repository. Remove it or choose a different --samples-dir.`); + } + + if (hasUncommittedChanges(samplesDir)) { + throw new Error( + `${samplesDir} has uncommitted changes. Commit, stash, or discard them before running this script, or choose a different --samples-dir.` + ); + } + + console.log(`- git -C ${samplesDir} checkout main && git pull --ff-only`); + if (dryRun) { + return { ready: false }; + } + + execFileSync("git", ["-C", samplesDir, "checkout", "main"], { stdio: "inherit" }); + execFileSync("git", ["-C", samplesDir, "pull", "--ff-only"], { stdio: "inherit" }); + return { ready: true }; +} + +function main() { + const args = parseArgs(process.argv); + if (args.help) { + usage(); + return; + } + + if (!args.version) { + throw new Error("Missing required --version argument."); + } + + if (!/^\d+\.\d+\.\d+$/.test(args.version)) { + throw new Error(`Invalid version format: ${args.version}`); + } + + if (!args.samplesDir) { + throw new Error("Missing required --samples-dir argument."); + } + + const samplesDir = path.resolve(args.samplesDir); + const repoUrl = args.repoUrl || SAMPLES_REPO_URL; + + if (args.ionToken) { + throw new Error( + "--ion-token is not supported, so it can't end up in your shell history or process list. Set the CESIUM_ION_TOKEN environment variable instead." + ); + } + + const ionToken = process.env.CESIUM_ION_TOKEN; + if (!ionToken) { + throw new Error("Missing ion token. Set the CESIUM_ION_TOKEN environment variable before running this script."); + } + + console.log(`Samples update plan for v${args.version}`); + + const { ready } = ensureSamplesCheckout(samplesDir, repoUrl, args.dryRun); + + if (args.dryRun && !ready) { + console.log("- Dry run: skipping file-level preview because the checkout isn't available locally yet."); + return; + } + + const manifestPath = path.join(samplesDir, "Packages", "manifest.json"); + const projectSettingsPath = path.join(samplesDir, "ProjectSettings", "ProjectSettings.asset"); + const ionAssetPath = path.join( + samplesDir, + "Assets", + "CesiumSettings", + "Resources", + "CesiumIonServers", + "ion.cesium.com.asset" + ); + + const manifest = readText(manifestPath); + const projectSettings = readText(projectSettingsPath); + const ionAsset = readText(ionAssetPath); + + console.log(`- ${path.relative(samplesDir, manifestPath)}: set com.cesium.unity dependency to ${args.version}`); + console.log(`- ${path.relative(samplesDir, projectSettingsPath)}: set bundleVersion to ${args.version}`); + console.log(`- ${path.relative(samplesDir, ionAssetPath)}: set defaultIonAccessToken (value hidden)`); + console.log(args.commit ? "- git commit the changes locally (not pushed)" : "- Skipping git commit (--no-commit)"); + + const nextManifest = replaceDependencyVersion(manifest, args.version); + const nextProjectSettings = replaceBundleVersion(projectSettings, args.version); + const nextIonAsset = replaceIonToken(ionAsset, ionToken); + + if (args.dryRun) { + return; + } + + writeText(manifestPath, nextManifest); + writeText(projectSettingsPath, nextProjectSettings); + writeText(ionAssetPath, nextIonAsset); + + if (!args.commit) { + return; + } + + runGit(samplesDir, ["add", manifestPath, projectSettingsPath, ionAssetPath], false); + runGit(samplesDir, ["commit", "-m", `Prepare samples for v${args.version}`], false); +} + +try { + main(); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +}