Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions .github/workflows/cut-rc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -673,14 +673,38 @@ jobs:
echo "template surfaces declared by stampedPaths() (${#TEMPLATE_SURFACES[@]}):"
sed 's/^/ /' "$TEMPLATE_LIST"

# The release-index surface `sync-release-index-currency.mjs` stamps, read
# from `syncedPaths()` — which derives it from `INDEX_PATH` in the gate
# whose finding that rewriter clears. Same terms as the two lists above, and
# for the same reason: a third literal here would be a third contract. On an
# RC cut this rewriter writes NOTHING (an `-rc` heading is not a GA version,
# so the newest GA of the major does not move), so this list normally stages
# nothing — it is declared so that the one cut where the index IS stale gets
# a complete commit instead of the refusal below.
INDEX_LIST="${RUNNER_TEMP:-/tmp}/cut-rc-release-index-surfaces.txt"
if ! node --input-type=module \
-e 'import { syncedPaths } from "./scripts/sync-release-index-currency.mjs"; for (const p of syncedPaths()) console.log(p);' \
> "$INDEX_LIST"; then
echo "::error::could not resolve syncedPaths() from scripts/sync-release-index-currency.mjs, so the release-index half of the release file surface is unknown. Refusing to push."
exit 1
fi
if [ ! -s "$INDEX_LIST" ]; then
echo "::error::syncedPaths() in scripts/sync-release-index-currency.mjs resolved EMPTY, so the release index would go unstaged even though the version pass can rewrite it. Refusing to push."
exit 1
fi
mapfile -t INDEX_SURFACES < "$INDEX_LIST"
echo "release-index surfaces declared by syncedPaths() (${#INDEX_SURFACES[@]}):"
sed 's/^/ /' "$INDEX_LIST"

git add -A -- \
'*package.json' \
'*CHANGELOG.md' \
.changeset \
.objectui-sha \
packages/spec/src/kernel/protocol-version.ts \
"${TEMPLATE_SURFACES[@]}" \
"${DOCS_SURFACES[@]}"
"${DOCS_SURFACES[@]}" \
"${INDEX_SURFACES[@]}"

STAGED="$(git diff --cached --name-only)"
if [ -z "$STAGED" ]; then
Expand All @@ -689,16 +713,17 @@ jobs:
fi

# Re-check every staged path against the allowlist. The pathspec above is
# convenience; THIS is the guarantee. Three filters, same allowlist the
# convenience; THIS is the guarantee. Four filters, same allowlist the
# pathspec used: the fixed release paths by pattern, then the declared
# template and doc surfaces by WHOLE-LINE EXACT match (`-xF`) against the
# very lists that were staged — so neither derived filter can accept a path
# its declaration does not name, and neither needs regex-escaping of the
# paths to stay exact.
# template, doc and release-index surfaces by WHOLE-LINE EXACT match
# (`-xF`) against the very lists that were staged — so no derived filter
# can accept a path its declaration does not name, and none needs
# regex-escaping of the paths to stay exact.
BAD="$(printf '%s\n' "$STAGED" \
| grep -vE '(^|/)package\.json$|(^|/)CHANGELOG\.md$|^\.changeset/|^\.objectui-sha$|^packages/spec/src/kernel/protocol-version\.ts$' \
| grep -vxF -f "$TEMPLATE_LIST" \
| grep -vxF -f "$SURFACE_LIST" || true)"
| grep -vxF -f "$SURFACE_LIST" \
| grep -vxF -f "$INDEX_LIST" || true)"
if [ -n "$BAD" ]; then
echo "::error::the version commit would carry paths outside the release file surface. Refusing to push. Offending paths follow; if the version pass legitimately grew a new output, widen the allowlist in this workflow deliberately."
printf '%s\n' "$BAD" | sed 's/^/::error:: unexpected: /'
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1958,6 +1958,50 @@ jobs:
- name: Template version-time rewriter self-test
run: pnpm check:template-version-sync

# #15332 — the THIRD version-time rewriter, and the third self-test beside
# the two above. scripts/sync-release-index-currency.mjs joins the root
# `version` chain and stamps the release index's "current series: X.Y.Z,
# released YYYY-MM-DD" status field from packages/spec/CHANGELOG.md, so the
# sentence cannot go stale the way it did on three consecutive minors
# (#10232, #11649, #15332). The gate that names the staleness,
# check-release-section-coverage, structurally could not fire on the change
# that causes it: the version commit is opened by changesets/action with the
# default GITHUB_TOKEN and gets NO CI, and the step further down in this file
# runs that gate WITHOUT --strict, where a finding is advisory and the job is
# green by design.
#
# ⚠️ This step is NOT that gate's --strict arm, and must never become it.
# Promoting --strict here would also red a release page that has no section
# for a just-published train, i.e. every PR between a version commit and its
# release-notes PR — a policy question, deliberately not decided by adding a
# rewriter. What runs here is ONLY --self-test, whose cases are string
# fixtures: it never reads the live index for currency and never reads a
# release page at all, so no corpus state can red it. The advisory,
# non-strict `Release section-coverage guard` step below is untouched.
#
# Why it must run at PR time and not only in release.yml's post-version lane,
# where the rewriter's own corpus is — two gates in this repo say so, and both
# name a PR-TIME caller:
# * check-self-test-wired — a script CI runs that ships a --self-test must
# have that self-test run by CI.
# * scripts/pm/dispatch-gates.mjs --self-test — "a deferred pair defers the
# LEAD, not the load break": a family no every-PR workflow runs leaves its
# import edges unwatched at PR time, so a change breaking the MODULE LOAD
# of check-release-section-coverage.mjs (which this rewriter imports its
# surface, scope and verdict from) would not redden the PR that made it.
# Measured: with release.yml as the only caller, that self-test is
# 2-of-1382 RED naming this family; with this step, 1382/1382.
#
# Only the --self-test runs here, for the same reason the two steps above
# give: on a corpus that is already current the rewriter has nothing to do, so
# CI can never observe it working. The self-test is where a STALE entry is
# observed being stamped through the gate's own verdict, and — the control
# that matters just as much — where a CURRENT index is observed left
# byte-identical and UNWRITTEN, because this rewriter writes into curated,
# reader-facing prose.
- name: Release-index currency version-time rewriter self-test
run: pnpm check:release-index-currency-sync

# #4851: the docs-accuracy-audit workflow's default scope is a generated list
# (a workflow script runs in a vm with no filesystem, so it cannot enumerate
# content/docs/ itself — the caller hands the list in). Hand-kept, that list
Expand Down
34 changes: 29 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,12 @@ jobs:
# nothing to do cannot be observed working.
# check:template-version-sync the template stamper's, for the same
# reason.
# check:release-index-currency-sync
# the release-index stamper's (#15332), for the
# same reason again — on a train whose index is
# already current it rewrites nothing, and a
# rewriter with nothing to do cannot be observed
# working.
# check:nul-bytes lint.yml's one UNCONDITIONAL gate whose
# corpus is every byte in the tree. The 76
# generated CHANGELOGs are the only prose
Expand Down Expand Up @@ -539,13 +545,30 @@ jobs:
exit 1
fi

# Same three filters cut-rc.yml applies to the same surface: the fixed
# release paths by pattern, then the two declared lists by WHOLE-LINE
# EXACT match, so neither derived filter can accept a path its
# declaration does not name and neither needs regex-escaping.
# The release-index surface the currency stamper writes, on the same terms
# again: resolved from `syncedPaths()` in the rewriter, which derives it
# from `INDEX_PATH` in the gate whose finding it clears. Third rewriter,
# third resolved list, zero restated literals.
INDEX_LIST="${RUNNER_TEMP}/post-version-release-index-surfaces.txt"
if ! node --input-type=module \
-e 'import { syncedPaths } from "./scripts/sync-release-index-currency.mjs"; for (const p of syncedPaths()) console.log(p);' \
> "${INDEX_LIST}"; then
echo "::error::could not resolve syncedPaths() from scripts/sync-release-index-currency.mjs, so the release-index half of the post-version surface is unknown. Refusing to call this tree validated."
exit 1
fi
if [ ! -s "${INDEX_LIST}" ]; then
echo "::error::syncedPaths() in scripts/sync-release-index-currency.mjs resolved EMPTY, so the release index the version pass rewrites would read as an unexpected path. Refusing to call this tree validated."
exit 1
fi

# Same four filters cut-rc.yml applies to the same surface: the fixed
# release paths by pattern, then the three declared lists by WHOLE-LINE
# EXACT match, so no derived filter can accept a path its declaration
# does not name and none needs regex-escaping.
UNEXPECTED="$(grep -vE '(^|/)package\.json$|(^|/)CHANGELOG\.md$|^\.changeset/|^packages/spec/src/kernel/protocol-version\.ts$' "${MOVED_FILE}" \
| grep -vxF -f "${TEMPLATE_LIST}" \
| grep -vxF -f "${SURFACE_LIST}" || true)"
| grep -vxF -f "${SURFACE_LIST}" \
| grep -vxF -f "${INDEX_LIST}" || true)"
if [ -n "${UNEXPECTED}" ]; then
printf '%s\n' "${UNEXPECTED}" | sed 's/^/::error:: unexpected: /'
echo "::error::the version pass wrote outside the reviewed post-version surface (paths above). This is the treadmill guard: a new version-time output must arrive together with the gate that judges it. Add the surface to the declaration its rewriter reads, and its gate to the content half below — deliberately, in one reviewed diff."
Expand Down Expand Up @@ -591,6 +614,7 @@ jobs:
run_gate pnpm check:docs-image-tag
run_gate pnpm check:docs-image-tag-sync
run_gate pnpm check:template-version-sync
run_gate pnpm check:release-index-currency-sync
run_gate pnpm check:nul-bytes
run_gate pnpm check:release-notes
run_gate pnpm check:release-page-status
Expand Down
2 changes: 1 addition & 1 deletion content/docs/releases/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ migration steps, then covers new capabilities and notable fixes.

## Versions

- [v17.0.0](/docs/releases/v17) — Files become owned `sys_file` records with server-enforced `accept`/`maxSize` and a governed download path, bulk export becomes its own opt-in privilege, the SDK is reconciled against the routes the server actually mounts (21 dead methods out, 40+ real ones in), approval nodes route approvers dynamically via CEL expressions and decision outputs, a datasource that cannot connect fails the boot, and Node 22 becomes the supported floor; 17.1 adds partial field masking, record-view auditing on `sys_audit_log`, and a per-object read-only approval visibility tier — and makes a deactivated permission set or position actually stop granting access, withdraws the bulk-export wildcard from the shipped admin sets, and gives all three flow doors one honest HTTP status table; 17.2 tightens by-id `update`/`delete` against a silently-dropped `where` predicate or a mismatched id, retires `sys_position.permissions` and other dead ADR-0049 surfaces, and stops analytics from answering the wrong number on a cross-object filter (current series: 17.2.0, released 2026-08-23).
- [v17.0.0](/docs/releases/v17) — Files become owned `sys_file` records with server-enforced `accept`/`maxSize` and a governed download path, bulk export becomes its own opt-in privilege, the SDK is reconciled against the routes the server actually mounts (21 dead methods out, 40+ real ones in), approval nodes route approvers dynamically via CEL expressions and decision outputs, a datasource that cannot connect fails the boot, and Node 22 becomes the supported floor; 17.1 adds partial field masking, record-view auditing on `sys_audit_log`, and a per-object read-only approval visibility tier — and makes a deactivated permission set or position actually stop granting access, withdraws the bulk-export wildcard from the shipped admin sets, and gives all three flow doors one honest HTTP status table; 17.2 tightens by-id `update`/`delete` against a silently-dropped `where` predicate or a mismatched id, retires `sys_position.permissions` and other dead ADR-0049 surfaces, and stops analytics from answering the wrong number on a cross-object filter (current series: 17.3.0, released 2026-09-04).
- [v16.0.0](/docs/releases/v16) — One org identifier (`organizationId`) across hooks and actions, quorum + per-group sign-off (会签) approvals with metadata-declared decision actions, time-relative automations, filtered roll-ups, strict dashboard widgets, an identity-scoped MCP stdio transport, and a platform-wide enforce-or-remove sweep that makes dead metadata loud; 16.1 adds a `requires` capability-provider preflight, two more dashboard build gates, and `runAs:'user'` automations that run with the triggering user's real grants (final release: 16.1.0).
- [v15.0.0](/docs/releases/v15) — Explain record access layer by layer, a docked AI workspace in the Console, project-ready Gantt charts, and phone sign-in; 15.1 adds permission-following attachments, no-code third-party connectors, dashboard-wide filters, pinyin search, and whole-record inline editing — with materially safer multi-tenant and write-path defaults (final release: 15.1.1).
- [v14.0.0](/docs/releases/v14) — ADR-0090 vocabulary convergence completed, object `enable.*` flags become real gates, admin user management, phone/SMS auth, book-audience enforcement, data-lifecycle contract, and effective-dated grants (final release: 14.8.0).
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"setup": "pnpm install && pnpm --filter @objectstack/spec build",
"prepare": "node scripts/setup-git-hooks.mjs",
"check:merge-driver": "node scripts/git-merge-regen.mjs --self-test && node scripts/check-regen-pending.mjs --self-test",
"version": "changeset version && node scripts/sync-protocol-version.mjs && node scripts/sync-template-versions.mjs && node scripts/sync-docs-image-tags.mjs",
"version": "changeset version && node scripts/sync-protocol-version.mjs && node scripts/sync-template-versions.mjs && node scripts/sync-docs-image-tags.mjs && node scripts/sync-release-index-currency.mjs",
"release": "pnpm run build && bash scripts/build-console.sh && bash scripts/release-publish.sh",
"docs:dev": "pnpm --filter @objectstack/docs dev",
"docs:build": "pnpm --filter @objectstack/docs build",
Expand Down Expand Up @@ -124,6 +124,7 @@
"check:release-notes": "node scripts/check-release-notes.mjs",
"check:release-page-status": "node scripts/check-release-page-status.mjs --self-test && node scripts/check-release-page-status.mjs",
"check:release-body": "node scripts/release-github-releases.mjs --self-test",
"check:release-index-currency-sync": "node scripts/sync-release-index-currency.mjs --self-test",
"check:node-version": "node scripts/check-node-version.mjs",
"check:pnpm-acquisition": "node scripts/check-pnpm-acquisition.mjs --self-test && node scripts/check-pnpm-acquisition.mjs",
"check:workflow-status-functions": "node scripts/check-workflow-status-functions.mjs --self-test && node scripts/check-workflow-status-functions.mjs",
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-release-section-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,9 @@ import { dirname, isAbsolute, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isEntrypoint } from './invoked-as.mjs';

const SPEC_CHANGELOG = 'packages/spec/CHANGELOG.md';
export const SPEC_CHANGELOG = 'packages/spec/CHANGELOG.md';
const RELEASES_DIR = 'content/docs/releases';
const INDEX_PATH = `${RELEASES_DIR}/index.mdx`;
export const INDEX_PATH = `${RELEASES_DIR}/index.mdx`;

/**
* The sibling gate, as the repo-relative name every MESSAGE in this file names
Expand Down
Loading
Loading