diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 0036ad4dc..9f9c4f4bd 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -13,6 +13,11 @@ on: required: true type: string workflow_dispatch: + inputs: + build_main: + description: Build a web hotfix from main without changing the release tag + type: boolean + default: false permissions: contents: read @@ -38,6 +43,7 @@ jobs: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name }} RELEASE_COMMIT: ${{ inputs.commit }} + BUILD_MAIN: ${{ github.event_name == 'workflow_dispatch' && inputs.build_main }} run: | if [ -z "$RELEASE_TAG" ]; then RELEASE_TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName) @@ -47,6 +53,13 @@ jobs: echo "::error::Release tag and commit do not match" exit 1 fi + if [ "$BUILD_MAIN" = "true" ]; then + if [ "$GITHUB_REF" != "refs/heads/main" ]; then + echo "::error::Web hotfixes must run on main" + exit 1 + fi + COMMIT="$GITHUB_SHA" + fi echo "tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" echo "commit=$COMMIT" >> "$GITHUB_OUTPUT" @@ -61,6 +74,7 @@ jobs: env: RELEASE_TAG: ${{ steps.source.outputs.tag }} run: | + git merge-base --is-ancestor "$RELEASE_TAG" HEAD if [ -f scripts/release.py ]; then python3 scripts/release.py versions --tag "$RELEASE_TAG" else diff --git a/docs/releases.md b/docs/releases.md index c9940c696..9eab2edef 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -31,4 +31,7 @@ To preview release notes, manually run **Weekly release** on `main` with **publish** unchecked. Check **publish** to release immediately. Rerunning within the same week reuses the original tag and commit. To rebuild just a failed target, use GitHub's **Re-run failed jobs**. Manual web deployment -always uses the latest published release rather than unreleased `main`. +defaults to the latest published release. For a web hotfix, run **Deploy web** +on `main` with **build_main** checked. The commit must descend from that release +and keep its package version; `/app/release.json` records the actual build commit +without moving the release tag. diff --git a/scripts/test_release.py b/scripts/test_release.py index 777a23657..753c09595 100644 --- a/scripts/test_release.py +++ b/scripts/test_release.py @@ -8,6 +8,7 @@ import os from pathlib import Path import subprocess import tempfile +import textwrap import unittest from unittest.mock import patch @@ -15,6 +16,36 @@ import release class ReleaseTests(unittest.TestCase): + def test_web_source_selection(self): + workflow = (Path(__file__).resolve().parents[1] / ".github/workflows/pages.yml").read_text() + source = workflow.split(" - name: Resolve release source\n", 1)[1] + script = textwrap.dedent(source.split(" run: |\n", 1)[1].split("\n - uses:", 1)[0]) + mock_gh = 'gh() { if [ "$1" = release ]; then echo v2026.37; else echo release-sha; fi; }\n' + cases = ( + ({}, "release-sha", None), + ({"RELEASE_TAG": "v2026.37", "RELEASE_COMMIT": "release-sha"}, "release-sha", None), + ({"RELEASE_COMMIT": "wrong-sha"}, None, "Release tag and commit do not match"), + ({"BUILD_MAIN": "true"}, "main-sha", None), + ({"BUILD_MAIN": "true", "GITHUB_REF": "refs/heads/other"}, None, "Web hotfixes must run on main"), + ) + with tempfile.TemporaryDirectory() as temp: + output = Path(temp) / "output" + for overrides, commit, error in cases: + with self.subTest(overrides=overrides): + output.write_text("") + result = subprocess.run(["bash", "-e", "-c", mock_gh + script], text=True, capture_output=True, env={ + **os.environ, "GITHUB_REPOSITORY": "owner/repo", "GITHUB_REF": "refs/heads/main", + "GITHUB_SHA": "main-sha", "GITHUB_OUTPUT": str(output), + "RELEASE_TAG": "", "RELEASE_COMMIT": "", "BUILD_MAIN": "false", **overrides, + }) + if error: + self.assertNotEqual(result.returncode, 0) + self.assertIn(error, result.stdout) + self.assertEqual(output.read_text(), "") + else: + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(output.read_text(), f"tag=v2026.37\ncommit={commit}\n") + def test_build_metadata(self): with tempfile.TemporaryDirectory() as temp: binary = str(Path(temp) / "build-script") diff --git a/site/README.md b/site/README.md index 516104a16..750975c05 100644 --- a/site/README.md +++ b/site/README.md @@ -18,3 +18,4 @@ The drawing screenshot also supplies the social preview; website image URLs chan GitHub Pages runs on release publication, a call from the weekly release workflow, or a manual run. Commits and pushes do not start workflows. Deployments use website files from `main` and build the app from the release, recording both sources separately. +Manual runs can select **build_main** to publish a web hotfix; see `docs/releases.md`. diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index d173960ec..dee88a495 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -249,16 +249,25 @@ impl OpenCADStudio { .map_or(1, |h| h.show_solid_history), } }, - crate::ui::window::options::Folders { - config: crate::config::config_dir() - .map(|p| p.display().to_string()), - plot_styles: crate::io::plot_style::plot_styles_dir() - .ok() - .map(|p| p.display().to_string()), - plugins: crate::plugin::external::plugins_dir() - .map(|p| p.display().to_string()), - autosave: crate::config::config_dir() - .map(|_| std::env::temp_dir().display().to_string()), + { + #[cfg(not(target_arch = "wasm32"))] + { + crate::ui::window::options::Folders { + config: crate::config::config_dir() + .map(|p| p.display().to_string()), + plot_styles: crate::io::plot_style::plot_styles_dir() + .ok() + .map(|p| p.display().to_string()), + plugins: crate::plugin::external::plugins_dir() + .map(|p| p.display().to_string()), + autosave: crate::config::config_dir() + .map(|_| std::env::temp_dir().display().to_string()), + } + } + #[cfg(target_arch = "wasm32")] + { + crate::ui::window::options::Folders::default() + } }, self.double_click_block_refedit, self.double_click_block_attedit,