|
25 | 25 | outputs: |
26 | 26 | docs: ${{ steps.changes.outputs.docs }} |
27 | 27 | core: ${{ steps.changes.outputs.core }} |
| 28 | + console: ${{ steps.changes.outputs.console }} |
28 | 29 | steps: |
29 | 30 | - name: Checkout repository |
30 | 31 | uses: actions/checkout@v7 |
|
46 | 47 | - 'pnpm-lock.yaml' |
47 | 48 | - 'tsconfig.json' |
48 | 49 | - '.github/workflows/ci.yml' |
| 50 | + # Build inputs of the vendored Console SPA — see the Console Pin Gate |
| 51 | + # job at the bottom of this file. `.objectui-sha` is a ROOT DOTFILE, so |
| 52 | + # it matches neither filter above: a pin-only diff skipped `core` and |
| 53 | + # `docs` alike, which is how #4288 moved the pin 76 commits with six of |
| 54 | + # fourteen checks skipped and nothing anywhere building the new SHA. |
| 55 | + # The last two entries are the "a change to the guard runs the guard" |
| 56 | + # rule the repo applies to every other filtered gate; they are close to |
| 57 | + # free here because an unmoved pin hits the dist cache. |
| 58 | + console: |
| 59 | + - '.objectui-sha' |
| 60 | + - 'scripts/build-console.sh' |
| 61 | + - 'scripts/check-console-sha.mjs' |
| 62 | + - '.github/workflows/ci.yml' |
49 | 63 |
|
50 | 64 | test: |
51 | 65 | name: Test Core |
@@ -704,3 +718,170 @@ jobs: |
704 | 718 |
|
705 | 719 | - name: Build Docs |
706 | 720 | run: pnpm --filter @objectstack/docs build |
| 721 | + |
| 722 | + # ── The pinned objectui SHA actually builds (#4290) ─────────────────────── |
| 723 | + # |
| 724 | + # `.objectui-sha` is the single source of truth for the vendored Console SPA: |
| 725 | + # release.yml reads it, clones objectstack-ai/objectui at that commit, builds |
| 726 | + # @object-ui/console, and copies the dist into packages/console/. Editing that |
| 727 | + # one line replaces the entire frontend the platform ships. |
| 728 | + # |
| 729 | + # Nothing on the PR side ever built it. The pin appeared in exactly two |
| 730 | + # workflows, neither of them a gate: release.yml (post-merge, on the main push) |
| 731 | + # and showcase-smoke.yml (manual + nightly, whose own header says "it never |
| 732 | + # gates PRs"). Combined with the filter hole noted above, #4288 merged a |
| 733 | + # 76-commit pin bump green with six of fourteen checks skipped. A SHA that |
| 734 | + # cannot build — a typo, a commit that was force-pushed away, a genuinely |
| 735 | + # broken objectui, a dead bundle canary — reached main unchallenged and would |
| 736 | + # have surfaced at PUBLISH time. The only thing standing between the two was |
| 737 | + # the author remembering to run scripts/build-console.sh locally: discipline, |
| 738 | + # not a gate. |
| 739 | + # |
| 740 | + # `pnpm objectui:refresh` does include the build, so the normal path was |
| 741 | + # already covered in practice; the hole is a hand-edited pin, or that single |
| 742 | + # line cherry-picked out of another branch. |
| 743 | + # |
| 744 | + # Cost is real but narrowly aimed. A moved pin is the only trigger that pays |
| 745 | + # the full clone + vite build, and #4288's 76 commits of staleness are the |
| 746 | + # measure of how rare that is. The other triggers (this file, the drift guard) |
| 747 | + # leave the pin untouched, so they hit the dist cache and cost about a minute. |
| 748 | + # |
| 749 | + # Scope, stated plainly: this proves the PINNED SHA builds. It does NOT cover a |
| 750 | + # packages/client change breaking the injected-client bundle — that input lives |
| 751 | + # under `packages/**`, and watching it here would rebuild the console on a large |
| 752 | + # fraction of every PR. release.yml stays the only check for that direction. |
| 753 | + # |
| 754 | + # The NAME is the required-check contract — the same trap the dogfood shards |
| 755 | + # note above: renaming it silently drops the gate wherever branch protection |
| 756 | + # lists it, with every PR still green. |
| 757 | + console-pin: |
| 758 | + name: Console Pin Gate |
| 759 | + needs: filter |
| 760 | + if: needs.filter.outputs.console == 'true' |
| 761 | + runs-on: ubuntu-latest |
| 762 | + timeout-minutes: 45 |
| 763 | + permissions: |
| 764 | + contents: read |
| 765 | + |
| 766 | + steps: |
| 767 | + - name: Checkout repository |
| 768 | + uses: actions/checkout@v7 |
| 769 | + |
| 770 | + - name: Setup Node.js |
| 771 | + uses: actions/setup-node@v7 |
| 772 | + with: |
| 773 | + node-version: '22' |
| 774 | + |
| 775 | + - name: Enable Corepack |
| 776 | + run: corepack enable |
| 777 | + |
| 778 | + - name: Verify pnpm version |
| 779 | + run: pnpm --version |
| 780 | + |
| 781 | + - name: Get pnpm store directory |
| 782 | + shell: bash |
| 783 | + run: | |
| 784 | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV |
| 785 | +
|
| 786 | + - name: Setup pnpm cache |
| 787 | + uses: actions/cache@v6 |
| 788 | + with: |
| 789 | + path: ${{ env.STORE_PATH }} |
| 790 | + key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} |
| 791 | + restore-keys: | |
| 792 | + ${{ runner.os }}-pnpm-store-v3- |
| 793 | +
|
| 794 | + # Restore-only, and with no save step at all: this job runs a handful of |
| 795 | + # times a month, so its own namespace would almost always be cold. The |
| 796 | + # build-core fallbacks below are the ones that actually hit — that job |
| 797 | + # builds a superset of the client closure needed here and is seeded from |
| 798 | + # main (same reasoning as the Temporal Conformance job's fallback). |
| 799 | + - name: Restore Turbo cache |
| 800 | + uses: actions/cache/restore@v6 |
| 801 | + with: |
| 802 | + path: .turbo/cache |
| 803 | + key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }} |
| 804 | + restore-keys: | |
| 805 | + ${{ runner.os }}-turbo-${{ github.job }}-${{ github.ref_name }}- |
| 806 | + ${{ runner.os }}-turbo-${{ github.job }}- |
| 807 | + ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- |
| 808 | + ${{ runner.os }}-turbo-build-core- |
| 809 | +
|
| 810 | + - name: Install dependencies |
| 811 | + run: pnpm install --frozen-lockfile |
| 812 | + |
| 813 | + # build-console.sh bundles THIS tree's @objectstack/client into the console |
| 814 | + # (via OBJECTSTACK_CLIENT_DIST) and falls back to building it in place when |
| 815 | + # the dist is missing — but that fallback is a bare `pnpm build` inside |
| 816 | + # packages/client, which tsup cannot finish unless @objectstack/core and |
| 817 | + # @objectstack/spec are built already. Build the closure through turbo |
| 818 | + # instead: cacheable, and near-free off the restore above. Both of the |
| 819 | + # script's existing callers (release.yml, showcase-smoke.yml) pre-build for |
| 820 | + # the same reason. |
| 821 | + - name: Build @objectstack/client and its dependencies |
| 822 | + run: pnpm exec turbo run build --filter=@objectstack/client... --concurrency=4 |
| 823 | + |
| 824 | + # Same key as release.yml's "Cache vendored Console dist" step. Actions |
| 825 | + # caches are repo-scoped, so a PR reads whatever main already built for this |
| 826 | + # pin — that is what makes the non-pin triggers cheap. Only a pin the repo |
| 827 | + # has never built misses, and a miss is exactly when this gate has work to do. |
| 828 | + # |
| 829 | + # Split restore/save where release.yml uses the combined action, because the |
| 830 | + # combined form's post-step saves even when the job FAILED. build-console.sh |
| 831 | + # writes the SHA stamp before it asserts the bundle canary, so a canary |
| 832 | + # failure leaves a stamped-but-broken dist on disk — cache that and the next |
| 833 | + # run restores it, skips the build, and sails through the stamp check. Saving |
| 834 | + # only once every assertion below is green is what lets a restored dist carry |
| 835 | + # the same guarantees as a freshly built one. |
| 836 | + - name: Restore vendored Console dist (keyed on the objectui pin) |
| 837 | + id: console-dist |
| 838 | + uses: actions/cache/restore@v6 |
| 839 | + with: |
| 840 | + path: packages/console/dist |
| 841 | + key: ${{ runner.os }}-console-dist-${{ hashFiles('.objectui-sha', 'scripts/build-console.sh') }} |
| 842 | + |
| 843 | + # The gate. Shallow-clones objectui at the pinned SHA, builds |
| 844 | + # @object-ui/console against this tree's client, copies the dist into |
| 845 | + # packages/console/, and asserts the bundle canary — every failure mode a |
| 846 | + # bad pin can carry, surfaced on the PR instead of at publish time. |
| 847 | + - name: Build the Console SPA at the pinned objectui SHA |
| 848 | + if: steps.console-dist.outputs.cache-hit != 'true' |
| 849 | + run: bash scripts/build-console.sh |
| 850 | + |
| 851 | + # check:console-sha exits 0 — deliberately — when there is no dist at all |
| 852 | + # (published installs and package-only CI legitimately have none), and warns |
| 853 | + # without failing when a dist carries no stamp. Both are right for that |
| 854 | + # script and both are VACUOUS here, where producing a dist is the entire |
| 855 | + # point: an empty or partial cache restore would otherwise reach the drift |
| 856 | + # check with nothing to check and report success. Prove one is really in |
| 857 | + # place before letting that check speak for the job. |
| 858 | + - name: Assert a real Console dist is in place |
| 859 | + env: |
| 860 | + CACHE_HIT: ${{ steps.console-dist.outputs.cache-hit }} |
| 861 | + run: | |
| 862 | + dist=packages/console/dist |
| 863 | + if [ "$CACHE_HIT" = "true" ]; then origin="the dist cache restore"; else origin="scripts/build-console.sh"; fi |
| 864 | + fail=0 |
| 865 | + [ -s "$dist/index.html" ] || { echo "::error::$dist/index.html is missing or empty"; fail=1; } |
| 866 | + [ -n "$(ls -A "$dist/assets" 2>/dev/null || true)" ] || { echo "::error::$dist/assets is missing or empty"; fail=1; } |
| 867 | + [ -s "$dist/.objectui-sha" ] || { echo "::error::$dist/.objectui-sha stamp is missing — check:console-sha would pass vacuously"; fail=1; } |
| 868 | + if [ "$fail" -ne 0 ]; then |
| 869 | + echo "::error::No usable Console dist after $origin." |
| 870 | + exit 1 |
| 871 | + fi |
| 872 | + echo "✓ Console dist present ($(du -sk "$dist" | awk '{print $1}') KB, via $origin) from objectui@$(cut -c1-12 "$dist/.objectui-sha")" |
| 873 | +
|
| 874 | + - name: Verify the Console dist stamp matches the pin |
| 875 | + run: pnpm check:console-sha |
| 876 | + |
| 877 | + # Reached only with every assertion above green (see the split-restore note). |
| 878 | + # This deviates from the workflow's restore-only-on-PRs policy on purpose: |
| 879 | + # that policy targets the ~5 turbo entries every push writes, whereas this |
| 880 | + # key moves only when the pin does. A pin-bump PR writes one entry, and the |
| 881 | + # rest of that PR's pushes reuse it instead of rebuilding the console again. |
| 882 | + - name: Save vendored Console dist |
| 883 | + if: steps.console-dist.outputs.cache-hit != 'true' |
| 884 | + uses: actions/cache/save@v6 |
| 885 | + with: |
| 886 | + path: packages/console/dist |
| 887 | + key: ${{ runner.os }}-console-dist-${{ hashFiles('.objectui-sha', 'scripts/build-console.sh') }} |
0 commit comments