* refactor(release): extract the patch-cut pre-flight out of inline shell
Faithful port, no behavior change: resolve-patch-cut.ts computes the same
version and the same X.Y.0 gate target the shell did, and runs the same
gh release lookup. Moving it out of YAML is what makes the gate decision
testable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(release): pin the patch cut on the previous patch, not the minor base
Red against the current guard: with release/v0.22.2 cut and unshipped, the
pre-flight gates open-design-v0.22.0, finds it published, and cuts v0.22.3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(release): gate the Thursday patch cut on the previous patch
The guard checked the minor base X.Y.0, so it only ever guarded a line's
first patch. Once X.Y.0 shipped, X.Y.2, X.Y.3, ... all passed no matter how
many unshipped releases had stacked up beneath them.
Gate on the release the cut actually stacks on — the highest release/vX.Y.Z
below it — which is the same target cut-release.yml uses for the Tuesday
minor, and still gives the answer the old minor-base comment wanted (cutting
0.15.1 checks open-design-v0.15.0, not the older 0.14.0).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(release): surface patch gate lookup failures
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverses the "withhold the card until the first artifact publishes" rule.
The card is now posted on the watcher's first poll, while every platform is
still queued, and every later state edits that same message.
The reason the card was withheld — a "构建中…" message left standing forever
when a build dies — has since been answered by the machinery that grew around
the card: a dead lane renders its own terminal state, a release where nothing
shipped turns the card red, the watcher's 140-minute budget stamps the card
instead of abandoning it, and a watcher that never delivered draws the
fallback bot. Silence for the whole build window bought nothing.
`shouldPostFirstCard` conflated two predicates. One half was already
`anyPackagePublished`; the other is now `allPlatformsFailed`, and it moves
from gating the post to being the card's red verdict — the header's old
"some platform reported and none shipped" proxy is only equivalent while the
card does not exist before total failure, and with the card posted early it
reads 全部平台构建失败 the moment one lane fails, or red at t=0 because
`build_linux` is skipped on every run.
Each platform row now carries its build time (`用时 11m42s` once finished,
`已用 18m` while running) and the footer carries the whole round's clock, both
read from the `started_at` / `completed_at` already in the polled job list
plus the origin run's `created_at`. Live numbers are quantized to the minute
so a 30-second poll does not PATCH the card on every cycle.
The `[P0] cold first Home run` packaged mac case has been red on almost
every beta build for eleven days (1 pass in 12 attempts) and nobody saw
it, because release-beta's smoke step is `continue-on-error: true`.
Four changes, in the order they matter:
1. Capture evidence before cleanup. A failing case uninstalled the app
and the next case deleted the runtime namespace, so the only surviving
trace was `logs: {skipped: true}`. The failure path now writes the
desktop log, a raw `/api/runs` diagnostics snapshot, and the error into
the release report before anything is torn down.
2. Give the wait a budget that matches the chain, split in two. 15s had to
cover cold project creation through rendered output; it is now 60s for
"the daemon owns a finished run" plus 30s for "the output surfaced", so
a timeout says which half broke instead of leaving three observations
that cannot falsify each other. Polling moved inside the page: every
sample used to cost a `tools-pack mac inspect` process, which ate about
a third of the old budget on its own.
3. Drop the injected 503. `POST /api/runs` does not answer
WORKSPACE_AUTHORITY_UNAVAILABLE — local run creation is deliberately
independent of Workspace membership availability — so the case spent
an extra create round trip plus a 500ms backoff on a branch production
cannot reach. Run-create retry semantics stay covered by
apps/web/tests/providers/sse.test.ts.
4. Make the exempted red audible. The exemption stays (at a 1/12 pass rate
a blocking gate would stall the daily beta), but `tools-release
write-report` now turns a failed smoke into a run annotation and a
caution banner at the top of the step summary.
* feat(ci): publish the What's New card from the repository
Move the post-update highlight document into the repo and publish it from
GitHub Actions, so changing the card is a pull request instead of a task
that needs personal Cloudflare credentials.
- docs/whats-new.json is the source of truth, next to its spec doc.
- pnpm guard validates it through the daemon's own parser on every PR, so a
document that would silently show no card cannot merge.
- whats-new-publish.yml uploads on merge to main (and on manual dispatch,
with a read-only dry-run mode) and verifies the object by reading it back
from the origin the daemon fetches, past the edge cache.
* docs: name the CI job that actually runs the guard check
* fix(ci): gate the What's New publish behind a main-only environment
A workflow_dispatch runs the workflow file from the ref it is dispatched
against, so the in-workflow main-only assertion was editable by whoever
triggered it: anyone with write access could publish unreviewed content to
every installed client from their own branch.
Split the workflow so credentials live in one place and that place is gated by
GitHub rather than by YAML. `validate` holds no secrets and runs on any ref;
`publish` declares the `whats-new-publish` environment, whose deployment-branch
policy allows main only and which owns the R2 secrets. Dropping the environment
declaration to escape the branch policy also drops the secrets.
Also points the card at the new Arena URLs supplied by product.
* fix(ci): pin the What's New publish trust boundary in a check that runs
The missing-credentials error still told operators the R2 keys are
repository secrets. Following that string is the exact setup the previous
revision closed: repository secrets are readable from any job on any
branch, so a modified workflow dispatched from an unreviewed ref could
publish to every installed client — or just take the keys. Point the
error at the `whats-new-publish` environment and refuse the repository
path in the same breath.
The boundary itself had no coverage: nothing read the workflow, so moving
`environment:` or a `secrets.*` expression back onto the arbitrary-ref
job would stay green while still publishing correctly from main.
That coverage cannot be lane-routed. Running the planner over
`.github/workflows/whats-new-publish.yml` selects neither
`web_tests_required` nor `ui_p0_validation_required`, so the `e2e_vitest`
workload does not run for a workflow-only edit — an assertion living only
there would be skipped for exactly the change class it guards. The check
therefore lives in `pnpm guard`, which CI invokes from the unconditional
preflight job, same reasoning as the document guard next to it.
It asserts that the credential-bearing job declares the environment by
its exact name, needs `validate` and is gated on its output with no
`always()`/`cancelled()` escape, reads all four bucket bindings, and that
no other job attaches an environment or reads any secret at all.
The e2e suite drives it over the real workflow mutated into each of those
regressions in turn, so every assertion is proven able to fail; gutting
the checker turns all eleven red.
* chore(whats-new): seed the document from the live card
Bringing the workflow in with new copy attached ties the infrastructure to
a content decision. The Arena CTA this document pointed at is not live —
`/llm-arena-for-design/` and `/zh/llm-arena/llm-arena-for-design/` both
404 — and the card renders regardless, so merging would have put a dead
button on every client's Home.
Seed the repository copy from the object that is already serving instead:
byte-identical to `https://whatsnew.open-design.ai/whats-new.json`, id
`0.21.0`. That puts the current state under version control and makes the
first automatic publish idempotent — it uploads exactly what is already
there, so the merge exercises the whole chain (validate, upload, read-back
against the origin the daemon fetches) without changing anything a user
sees.
The Arena copy is not lost; it moves to the PR body as a follow-up for the
release owner, who can now change the card through this workflow without
holding personal Cloudflare credentials.
* fix(ci): detect secrets by context, not by access shape
The guard added last round matched `${{ secrets.NAME }}` with a regex over
the raw workflow text. A guard whose whole job is to stop a secret reaching
the arbitrary-ref job had a blind spot for most of the ways to read one.
Verified against the previous implementation, each returning zero
violations while a secret reached `validate`: `secrets['NAME']`,
`secrets["NAME"]`, `secrets [ 'NAME' ]`, `secrets . NAME`,
`toJSON(secrets)`, a bare `secrets`, and `fromJSON(toJSON(secrets)).NAME`.
Extending the regex would have repeated the mistake that caused it. One
bypass proves why: a double-quoted YAML scalar may carry a backslash line
continuation, so
STOLEN: "${{ sec\
rets.NAME }}"
parses to a live secret read while the source text contains no such
substring. No text scanner can see that, however many shapes it lists.
So the workflow is read through a YAML parser, and detection is a single
property instead of an enumeration: GitHub's expression language has no
string-to-context evaluation, so a secret cannot be reached without naming
the `secrets` context as an identifier. Finding that identifier inside any
`${{ … }}` expression covers every access form, including ones nobody has
thought of.
Parsing also retires the hand-rolled job splitter and its own failure
modes, and lets the check distinguish "no violations" from "nothing was
examined" — an unparseable document, a flow-style jobs mapping, or a
renamed publish job now fail closed rather than passing quietly.
Two holes the shape list never covered are closed as a side effect: a
job-level `secrets:` key passes credentials to a called workflow with no
expression at all, and workflow-level `env:` is inherited by every job.
`yaml@2.9.0` becomes a root devDependency. It was already in the lockfile
via `tools/release` and pinned repository-wide by the existing pnpm
override, so the lockfile gains only the root importer entry.
Test coverage moves with it: 26 of 46 cases now go red when the checker is
gutted, up from 11.
---------
Co-authored-by: lefarcen <ontf116@gmail.com>
* feat(release): alert through the other Feishu bot when the prerelease card goes silent
The progressive card is now the only prerelease notification, and every way it
breaks is silent: the pipeline stays green, the packages ship, and the channel
hears nothing. The watcher catches Feishu failures on purpose — one hiccup must
not end a two-hour watch — so expired application-bot credentials look exactly
like a healthy run.
Adds a last-resort notice delivered through the custom-bot webhook, which is a
different bot with a different secret, so a credential failure on the card side
cannot take the alert down with it. It fires only when the card did not reach
the group: the watcher now reports `card_delivered` for a card whose FINAL state
is in the chat, and the gate reads that rather than the job result.
Two callers, mutually exclusive so a release can never draw both:
release-prerelease-card.yml's `fallback_notice` job covers a watcher that died
or never delivered; steps at the end of release-prerelease.yml's
`dispatch_validation` cover a card that was never dispatched. The latter are
steps, not a job, because nothing may appear in a `needs:` on the dispatchers.
* fix(release): keep the fallback notice reachable past the dispatch it reports
The two steps that actually deliver the notice carried no status function, so
GitHub's implicit success() skipped them whenever anything earlier in the job
had failed. `dispatch-validation.sh` exits non-zero when it cannot dispatch the
card on any ref — which marks the job failed — so on the one path the
dispatch-side fallback exists for, it was silent. This PR was written to close
the implicit-success() trap at the job level and walked into its step-level twin.
Both consumers now carry `always() && steps.notice.outcome == 'success'`: the
status function so a failed dispatch still reaches them, and the outcome check
so neither a crashed nor a skipped composer can hand feishu-notice.ts an empty
body. The composer writes `alert` last, after every field a poster reads, so
`alert=true` in an append-only $GITHUB_OUTPUT means the whole notice is on disk.
Adds the step-level reachability rule as a spec beside the existing job-level
one, and puts the dispatched prerelease lanes into that job-level sweep —
release-prerelease-card.yml was outside it, so `fallback_notice`, whose `if`
hand-checks `needs.card.result`, was uncovered by the rule it has to obey.
A stable release with no docs/CHANGELOG/v<version>/ directory hard-failed at
the very first job. Fixing it mid-release is not "write two markdown files":
the stable promotion gate pins github.commit to the promoted prerelease, so the
changelog commit invalidates that artifact and forces a fresh ~27-minute
prerelease round trip.
Missing notes are now a loud warning instead of a failure. reviewReleaseNotePlanPolicy
returns warnings for a stable release with no notes, or one missing zh-CN, and
prepare-release-note surfaces them as a ::warning:: annotation plus a job
summary entry. Notes that exist but cannot be consumed still throw: a channel
mismatch, or a plan that omits the default locale every consumer falls back to.
publish-metadata carried a second stable gate on the same condition. Removing
only the first would have moved the hard failure past the GitHub draft release
and asset upload, which is strictly worse, so both are gone. The rest of the
chain already treated "absent" as a complete outcome — it is the beta and
prerelease path — so publication, verification, and metadata assembly are
unchanged.
Non-stable channels behave exactly as before.
The progressive Feishu card (release-prerelease-card.yml) has carried four
real prereleases: correct rendering, per-platform refresh, correct changelog,
smoke wired through. The one-shot custom-bot webhook card it superseded was
kept alongside it on purpose — a gap in release notification is worse than a
duplicate — and its own TRANSITIONAL comment named the retirement scope.
Deletes notify-release-feishu.yml's `notify` job, leaving that workflow as
what its remaining job already is: the push-to-release/** prerelease build
entry point.
tools/release/src/notifications/feishu.ts stays. notify-daily-feishu.yml
renders the beta download card through it and `tools-release notify feishu`
exposes it, so only the prerelease call path is removed. The
FEISHU_RELEASE_WEBHOOK / FEISHU_RELEASE_SIGN_SECRET secrets stay too — six
other workflows post through them, and keeping them leaves a rollback open.
`dispatch_smoke` has never run. release-prerelease-smoke.yml has zero runs
since it shipped, while release-prerelease-tests.yml runs on every prerelease.
build_linux is skipped on every run — the repository has no
ENABLE_STABLE_LINUX variable — and GitHub applies a skip "to all jobs in the
dependency chain from the point of failure or skip onwards" unless a job uses
a conditional expression that causes it to continue. That break is per job and
is not inherited: `publish` carries `always() && !cancelled()` and so runs,
but `dispatch_smoke` sits downstream of the same skip with no break of its
own, so GitHub's implicit success() skips it before its condition is ever
evaluated. In run 34149795952 publish succeeded, version_metadata_url was
published, and enable_smoke arrived as true; the job was still skipped with
zero steps.
`dispatch_validation` is unaffected because it needs only `metadata`, which
has no skippable ancestor — which is why the tests lane worked and the smoke
lane did not.
The progressive Feishu card then reported the three packaged-smoke rows as
⏳ 排队中 forever. The watcher already computed that the discovery window had
closed without a run appearing — it used that to stop waiting, and threw it
away when deciding what the lane said. The lane model had no state between
"a result is coming" and "a switch turned it off", so a lane that should have
run and never did rendered as the former.
Adds `never_started`: terminal, rendered 🚨 未触发, counted separately from
failures so the header says "未触发" rather than claiming a check ran and
disagreed. The same rule now covers the code test lanes, which have the same
latent hole.
The topology test previously pinned dispatch_smoke's exact `if` string, so it
agreed with the bug. It now asserts the substance of the condition plus the
reachability invariant: a job that hand-checks needs.<x>.result and has a
skippable ancestor must break the skip chain. That flags exactly one job
across the four release workflows.
* perf(ci): make prerelease mac Intel opt-in
build_mac_intel is the prerelease critical path: ~33 min on the
macos-15-intel fleet against ~21 min for mac arm64 and Windows, which
stretches every release-branch push to ~56 min end to end.
Gate it behind a new `enable_mac_x64` input that defaults to false, the
same shape build_linux already uses, and let publish tolerate a skipped
Intel job. Push-triggered prereleases now finish in ~21 min; a manual
dispatch can still turn Intel back on.
The stable promotion gate is unchanged and still hard-requires
platforms.macIntel in the promoted prerelease metadata, so an Intel-less
prerelease fails promotion loudly rather than shipping unvalidated.
* perf(ci): stop the prerelease pipeline waiting on work it does not need
Run 34102146769 took 56m, 42m of which survived turning macOS Intel off.
Four things in that 42m were wall clock nobody was buying anything with.
Tests no longer sit in front of packaging. functional_e2e, e2e_vitest,
daemon_unit_tests and verify take ~11 min; packaging takes ~21 min, and the
build jobs needed all four before they could start. They now need only
metadata and run alongside the tests. Nothing about the safety of a release
changes: `publish` is what writes metadata.json, and it already required all
four gates to be `success` — a red suite still cannot ship. What it costs is
CI minutes, since a failing gate now stops builds that have already run.
The Feishu card stops cloning the repository to read one `git log`. That
checkout was `fetch-depth: 0` against a 2.2 GiB pack and took 4m09s to
produce a `PREV..CUR` commit list. The list now comes from the compare API,
reproducing `git log --no-merges --pretty=format:'%s (%h)'` line for line —
verified against 35 commits of real history, where the only difference was a
leading space that feishu.ts trims anyway. Every fallback the old script had
(no baseline, unreachable commit) still produces an empty changelog rather
than a failed notification.
Packaged smoke and Apple notarization become opt-in, like Intel already is.
Smoke is advisory — `continue-on-error`, so it has never blocked publication
— and costs ~2.5 min on the Windows critical path. Notarization is a
round-trip to Apple that no prerelease consumer needs. Signing is NOT
optional and stays on: prepare-stable.ts requires platforms.mac.signed ==
true, so an unsigned prerelease could never be promoted. `mac_sign_mode: no`
is offered for parity with beta and now correctly reports signed=false
rather than lying in the metadata.
Windows drops setup-node's pnpm store cache and every job checks out
shallow. Measured across four prerelease runs the store cache missed 3 times
out of 4, costing 100s/66s/43s to save with no install speedup, and on its
one hit traded a 32s restore for a 36s faster install — while competing for
the 10 GB cache budget against tools-pack caches worth ~11 min each.
Nothing in this workflow reads git history.
* perf(ci): let a prerelease ship on the build's schedule, not the tests'
prerelease is the internal channel. Its job is to put a package in a
colleague's hands so a human can test it; automated CI is a second, parallel
opinion about the same commit. Wiring the two in series meant a single flaky
suite cost the team a whole day's package — the expensive failure, and the
one this pipeline kept producing.
The four test jobs (functional_e2e, e2e_vitest, daemon_unit_tests, verify)
now gate nothing. They are absent from `build_*` and from `publish`. They
still run on every prerelease and their results leave the workflow as
outputs — functional_e2e_result and friends, plus a tests_all_green /
failed_test_jobs rollup — so the notification layer can raise the alarm
without holding the package hostage. Stable is untouched: its promotion gate
reads the prerelease metadata and runs its own checks.
The packaged smoke suites move for the same reason. They were already
`continue-on-error`, so a smoke failure never failed the job — but they sat
between the build and the R2 upload, which meant the package waited on them
and a hung smoke could still eat the job. Every smoke now runs AFTER
"Publish <platform> platform" and "Upload <platform> publish manifest", and
every step after that manifest upload is continue-on-error, so nothing
post-delivery can fail a build job and retroactively block publish.
The results travel through a `test_signals` job rather than the
`jobs.<job_id>.result` expression GitHub documents for workflow_call outputs,
because actionlint has rejected that property since 2023
(rhysd/actionlint#343) and ci.yml runs actionlint at a pinned version.
One consequence to hand to the notification work: a failed advisory job now
makes the overall workflow conclusion red even though the package shipped, so
`needs.build.result` is no longer the right thing to colour a card by.
`version_metadata_url` is the authoritative "did a package ship?" signal and
is documented as such.
* perf(ci): move prerelease validation out of the pipeline it validates
release-prerelease.yml holds one repository-wide concurrency group with
cancel-in-progress: false, so any job that outlives `publish` keeps that
group held and stops the NEXT prerelease from starting. Making the test
jobs advisory did not fix that: they still held the group for 45 minutes
after the package had shipped.
The pipeline is now metadata -> build_* -> publish plus two dispatcher
jobs that nothing depends on. The four code suites, the packaged
mac/Windows smoke, and the release card each run in a workflow of their
own, keyed on the origin run.
The packaged smoke no longer installs electron-builder's output
directory on the machine that produced it. It reads the published
version metadata, downloads the DMG / setup.exe out of R2, verifies its
sha256 sidecar, and writes it to the single path
`tools-pack <platform> install` reads - which is why the specs need no
changes. That is both a better test and what lets the smoke live on a
different runner at all.
The release card becomes progressive. A Feishu application bot can PATCH
a message it sent, so one card is created when the first platform lands
in R2 and edited in place as the rest of the platforms, the code tests,
and the smoke report in. A PATCH replaces the whole card, so exactly one
job may write it: the watcher owns the message and polls the three runs.
The card is withheld until a package exists, because a "building..."
card posted at dispatch time becomes a permanent lie when a build dies -
with total build failure as the one exception, where silence would hide
an incident.
macOS Intel goes back to on-by-default: stable ships Intel and stable's
gate is a validated prerelease artifact, so an Intel-less prerelease
cannot be promoted at all. The switch stays, forwarded as
`github.event_name != 'workflow_dispatch' || inputs.enable_mac_x64` -
`inputs.enable_mac_x64 || true` would be unturnoffable, because a
dispatch with the box unchecked also yields true.
Also fixes the signal batch 2 left wrong: a reusable workflow reports
failure if any job inside it failed, including work that runs after
publication, so `needs.build.result` painted the download card red while
its own buttons worked. `version_metadata_url` is non-empty only when
publish wrote metadata, and now drives both the header colour and the
smoke annotations.
Two topology tests were already red on this branch - batch 2 changed the
workflow without updating them. Both now assert the new invariant.
The existing webhook card stays until the new one has carried a couple
of real releases; a gap in release notification is worse than a
duplicate.
* chore: remove extracted landing-page from the monorepo
The marketing site now lives in a separate repository. Drop the
Astro app, its GitHub workflows/scripts, and the remaining
monorepo coupling so this workspace no longer builds or deploys it.
* fix: drop leftover landing-page coupling from Teams and skills
Point development enterprise URLs at the deployed marketing origin,
remove nextjs-app skill outputs that no longer emit a page, and stop
advertising apps/landing-page in plugin marketplace metadata.
Generated-By: looper 0.12.0 (runner=fixer, agent=omp)
* fix(ci): restore DSH installer E2E Vitest routing
Removing dsh-bootstrap-identity dropped the only planner route from
tools/release/resources/dsh-bootstrap/ to web_tests_required, so
installer-only PRs skipped e2e/tests/dsh-installer-version-policy.test.ts.
Generated-By: looper 0.12.0 (runner=fixer, agent=omp)
* docs: drop leftover landing-page paths from template assets
The skill contract no longer advertises the extracted Astro app, but
styles.css, compose.ts, and example.html still named deleted
apps/landing-page sources. Describe the standalone template behavior
instead and keep the bundled plugin example in sync.
Generated-By: looper 0.12.0 (runner=fixer, agent=omp)
* docs: keep landing-page off review and test surfaces
Add apps/landing-page to the forbidden-surface review list and stop naming the extracted path in plugin registry acceptance tables.
Generated-By: looper 0.12.0 (runner=fixer, agent=omp)
* fix: drop leftover landing-page refs and bump plugin version
Stop naming the deleted apps/landing-page in the landing-deck demo and
plugin-registry plan. Bump example-open-design-landing to 0.1.1 and keep
the official catalog version aligned with the bundled manifest.
Generated-By: looper 0.12.0 (runner=fixer, agent=omp)
* fix(release): make catalog pack git-safe in the Playwright container
Checkout's set-safe-directory did not cover later git calls, and Meta
swallowed a failed rev-parse under sh -e, so export ran with an empty
source commit. Mark GITHUB_WORKSPACE safe and pin CATALOG_SOURCE_COMMIT
to github.sha.
* fix(release): temporarily allow catalog-publish on the git-safe branch
Let pack and publish run on fix/catalog-publish-container-git so the
container git fix can be verified end-to-end before reverting the guard.
* fix(release): restore catalog-publish main-only job guards
Drop the temporary fix-branch allowlist now that the container git
workaround is verified end-to-end.
* fix(runtimes): support the dsh release line users are actually served
Two days after #7153 widened the version policy from one release candidate
to the `0.1.0-rc.N` line, upstream published `0.1.1-rc.1` and then
`0.1.1-rc.2`, and made the latter the `latest` tag. Scoping to `0.1.0-rc.N`
was still a pin, just a longer one: every user on what npm now serves is
told their CLI is untested, our installer hands out a version two releases
behind, and the connection component's peers do not resolve against the
host they land on.
That last one is not a warning. semver only lets a prerelease satisfy a
range when some comparator carries the SAME major.minor.patch tuple AND a
prerelease tag, so `>=0.1.0-rc.6` matches nothing on the 0.1.1 line — a
verified `satisfies('0.1.1-rc.2', '>=0.1.0-rc.6') === false`, the same rule
that made the original pin uninstallable.
Move all three to the 0.1.x line: the policy accepts any `0.1.N`, release
candidate or stable; both installers pin `0.1.1-rc.2` with the resolution
window reset to just after its publish time; the peers gain a comparator
for the new line.
This will expire again. A comparator per prerelease line is what semver
allows, so `0.1.2-rc.1` will not resolve until someone adds one, and
nothing in the repo notices when upstream moves. The check that catches
installer/policy drift only compares the two against each other.
* ci: watch upstream dsh releases instead of waiting for a bug report
Twice in one week `@deepseek-ai/dsh` published a new release line while
this repo kept pinning the old one, and both times the detection mechanism
was a person reporting that their install did not work.
The check added with the first fix compares our installers against our
agent def. Both are our own config, so it stays green precisely when they
are equally out of date — which is the state that shipped, twice. Nothing
here looks at the registry.
Add a daily scheduled comparison against npm's `latest` that separates the
two failures worth telling apart: `behind` means our installer hands out an
older version than the registry serves, everything still works;
`unsupported` means a user who has what npm serves is outside what we claim
to support, which is an "untested" warning in Settings and peer ranges that
do not resolve. The card names all three files that must move together,
because forgetting the peer ranges is the one that breaks an install rather
than warning about it.
Standalone and outside the merge gate, like nix.yml and docker-image.yml:
an upstream release is not a reason to fail someone's PR. The workflow runs
`self-check` before touching the registry so a broken checker is loud
rather than quietly in-sync.
* fix(ci,runtimes): stop the drift watch losing what it exists to report
Four defects from review, each of which would have made this quieter than
having built nothing.
The widened policy over-claimed. `/^0\.1\.\d+(?:-rc\.\d+)?$/` accepts
`0.1.2-rc.1`, which the peer ranges cannot install — so the daemon would
suppress the untested warning for a version whose companion fails to
resolve, which is worse than the warning and silent. Narrow prerelease
acceptance to the tuples and floors the peers actually carry; stable
releases on the line need no comparator and stay accepted. This also
retires an over-claim that predates the branch: `0.1.0-rc.2` and `rc.3`
are published and below the `>=0.1.0-rc.6` floor.
`e2e/tests/dsh-installer-version-policy.test.ts` now runs both authorities
over a version matrix and fails when the def accepts anything the peers
reject, so the two cannot drift apart again.
Importing the script ran the CLI. Collecting the new test fetched the live
registry — measured at 354ms with the verdict printed — and once drift
existed would have attempted a real Feishu post before any assertion. Move
the dispatch behind a direct-entry check and prove it with a test that
stubs fetch and re-imports.
Feishu answers a rejected webhook with HTTP 200 and a nonzero code, so
treating every 2xx as delivered discards the alert and logs success doing
it. Mirror the landing notifier's contract: code 0 or absent is delivered,
429/5xx/9499 retry, anything else raises.
The workflow chose the landing webhook but signed with an unpaired secret,
so a bot that verifies signatures would reject the card. Pair them the way
the landing workflow does, and pin the mapping in the topology test.
* test(e2e): check each dsh peer range on its own
The compatibility guard scanned the whole manifest and merged every
comparator into one tuple table, so a single updated peer answered for the
other four. A bump that updates one range and forgets the rest — the
realistic way this goes wrong — read as compatible, which is the partial
state the guard exists to catch.
Parse `peerDependencies`, keep the `@deepseek-ai/dsh-*` entries, and require
every one of them to admit a version before calling it installable.
`cordis` is excluded: it has its own version line and never moves with dsh.
Two cases cover it: a fixture where one peer carries the new comparator and
the others do not, and the floor itself, since a comparator for the right
tuple still rejects a candidate below it. Verified against the real
manifest by downgrading four of the five ranges, which reds the matrix with
`expected [ '0.1.1-rc.1', '0.1.1-rc.2' ] to deeply equal []`.
* ci: surface merge-queue CI failure ejections as a PR comment
A failed workload in a merge_group run ejects the queued PR just as
invisibly as a blocking label does: the required check fails on the
queue's transient ref, the PR's own checks stay green, and the queue
entry silently disappears. The needs-validation gate (#5519) announces
its own ejections, but every other ejection left no trace on the PR.
The validate job now produces a handoff/comment artifact after the gate
has already failed in a merge group, naming the failed jobs and steps, a
log excerpt of the failing assertions, the run, and the PRs batched ahead
in the group. comment.atom.yml already consumes merge_group handoffs.
merge_policy exposes whether it already announced a label ejection so a
label-only ejection still gets exactly one comment.
* ci: keep the merge-queue log excerpt working on gh >= 2.97
gh 2.97.0 (GHSA-3m3g-3wcr-px46) refuses to print an API body that contains
terminal escape sequences unless --allow-escape-sequences is passed, and
CI job logs are full of them, so the excerpt fetch returned nothing on a
current gh. Older gh rejects the flag as unknown. Probe the flag once and
pass it only when supported; the fake gh in the topology test now enforces
both behaviors so dropping the probe goes red.
* ci: make the merge-queue notice probe survive pipefail and replays
Capturing gh api --help before grepping keeps an early grep exit from
SIGPIPE-ing gh and reading as "flag unsupported" under pipefail; strip
CRLF from job logs; never list the gate job itself as a failed job when
a completed run is replayed. The fake gh pads its help text past the pipe
buffer so the old probe shape stays red.
The production landing-page promotion pinned DSH_BOOTSTRAP_VERSION: v1 in
the workflow, while the publisher refuses to overwrite an already-published
immutable version with different bytes. PR #6983 renamed the brand inside
install-dsh.ps1 and install-dsh.sh without bumping that pin, so the next
production deploy failed closed on
immutable bootstrap object already exists with different content:
bootstrap/dsh/v1/install-dsh.ps1
and skipped every later step, including the Cloudflare Pages deploy. The
version is a function of the installer bytes, so maintaining it as a
hand-written constant guarantees this drift.
The publisher now resolves the version itself: it probes published versions
in order using SHA256SUMS as the fingerprint for the whole set, reuses the
version that already holds these exact bytes, and mints the next one when
the bytes changed. Every published version keeps if-none-match: * and the
immutable cache header, so history stays permanent and unoverwritable --
only the manual bump is gone. A mutable bootstrap/dsh/latest.json pointer
records the current version, per-file sha256, and the publishing run, and
the verify step reads the resolved version instead of naming one.
DSH_BOOTSTRAP_VERSION survives as an explicit escape hatch: setting it
forces one version and still fails closed on a content mismatch.
* chore: unify product name display to OpenDesign repo-wide
Rename the user-facing product name from "Open Design" to "OpenDesign"
across documentation, web/desktop UI strings, i18n locales, prompts,
plugin/skill/design-system content, changelogs, and specs, matching the
landing-page rename already in flight.
Deliberately excluded so installed-app identity stays untouched:
- packages/release PRODUCT_NAME and everything derived from it
(app bundle names, installer/registry identity, updater feeds)
- tools/pack, tools/release, apps/packaged, landing page (PR #6983)
- mocks/ recorded traces and LICENSE
- test fixtures and comments that encode real on-disk install paths
(Open Design.app, Program Files\Open Design, AppData paths)
Validated with pnpm guard, pnpm typecheck, and targeted web/desktop/host
test runs over the renamed strings.
* fix(web): keep two-letter author monogram for CamelCase brand names
authorInitials split words on whitespace only, so the renamed single-token
author name "OpenDesign" produced a one-letter monogram. Treat lower-to-upper
CamelCase boundaries as word breaks so the avatar fallback stays "OD".
* fix(release): restore spaced Windows source artifact names in win.ps1
tools/pack still derives builder outputs from the unchanged PRODUCT_NAME
("Open Design"), so the asset script must look for the spaced filenames.
Matches the mac/mac-intel/linux asset scripts, which were already kept on
the original names.
---------
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
* fix(landing): shorten HTML edge TTL and host-purge after production deploy
Long s-maxage=3600 + stale-while-revalidate=86400 left locale HTML
(/zh/pricing/, /de/pricing/, …) on different edge objects after
landing-page-production, so some languages looked deployed and others
did not. Cap HTML/edge docs at s-maxage=60 without multi-hour SWR, and
purge open-design.ai by hostname after pages deploy (token needs Zone
Cache Purge). Hashed _astro assets stay immutable.
* ci(landing): add manual host-purge workflow for edge cache recovery
Allows testing CLOUDFLARE_API_TOKEN Cache Purge permission and one-click
recovery without a full production rebuild. Production deploy still purges
after pages deploy.
* ci(landing): support purge-only production workflow mode
Lets us exercise CLOUDFLARE_API_TOKEN Cache Purge without a full rebuild,
and keeps host purge as a post-deploy job after a real promote.
* ci(landing): move edge-cache purge helper to workflow-owned TypeScript
pnpm guard rejects project-owned .mjs, so the new purge helper blocked
Validate workspace. Convert it to TypeScript and move it next to the other
landing workflow helpers under .github/scripts/, invoked with
--experimental-strip-types like landing-page-deploy-feishu.ts.
Also drop the purge-only production mode: blog-indexing-on-deploy listens on
successful landing-page-production runs and cannot tell a cache-only recovery
run from a real deploy. Purging without a rebuild already has a dedicated
entry point in landing-edge-cache-purge, so production purge now runs only
after a successful deploy.
* test(e2e): harden coverage and runtime isolation
* test(e2e): gate prerelease on full functional suite
* test(daemon): enforce prerelease coverage
* test(daemon): stabilize full-suite fixtures
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* test(e2e): preserve interaction regression coverage
Keep deck navigation subject to Playwright actionability checks and restore the current rail updater/composer stacking witness as an explicit expected failure.
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(ci): close conformance validation gaps
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* test(e2e): restore MCP snippet style guard
Restore the browser-computed style witness that was over-deleted when the OAuth flow moved to Integrations, and navigate through the current Settings MCP server entry.\n\nGenerated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(ci): close directive and job-boundary gaps
Compare normalized MUST_FIX directives as sets so identical multi-item rounds remain weak debates, and bound workflow topology assertions to their immediate sibling jobs.\n\nGenerated-By: looper 0.11.8 (runner=fixer, agent=codex)
Measured UI P0 shards fit nexu-runners-medium; drop the blanket xlarge
assignment and keep only project-collab on ui_p0_heavy (xlarge) because
multi-client collab has hit large/xlarge memory ceilings.
* test(e2e): pin that an empty windows smoke mode means core
Two independent holes let an empty value read as a different profile:
The workflow-level one — a workflow_call `default:` applies only when an
input is OMITTED, so forwarding '' defeats the declared `core` default.
The spec-level one — `??` falls back only on null/undefined, so an empty
environment variable survives it and `smokeProfile === 'core'` is false.
Together they made release/v0.18.1's first prerelease take the `full` path,
demand the updater fixture only a genuine `full` request configures, and die
before the smoke started — on the branch-cut commit, before anything else had
landed there. release/v0.18.0 never showed it because its branch name matched
a special case that produced `skip`, so its smoke never ran at all.
Red on both layers: the resolver mirrors main's `?? 'core'` verbatim.
* fix(release): treat an empty windows smoke mode as core
Close both layers that let an empty value read as a different profile.
notify-release-feishu now names `core` as its final arm instead of ''. A
workflow_call `default:` applies only when an input is omitted, so forwarding
an empty string defeated the callee's declared default.
win.spec.ts resolves the profile through `resolvePackagedSmokeProfile`, which
treats empty, whitespace and unset alike as `core` and rejects anything it does
not recognise — a typo must not select the updater path the way '' did.
Either fix alone would unblock release/v0.18.1, but they guard different holes:
one stops an empty value being produced, the other stops one being misread.
* ci: auto-rerun once when runner/spot cancel kills ci jobs
Add rerun.atom.yml as a trusted workflow_run consumer that requests one
`gh run rerun --failed` when completed pull_request/merge_group ci runs
lose leaf jobs to infra cancel (shutdown signal / cancelled runner),
while skipping ordinary assertion failures, stale heads, and ejected
merge-queue entries.
* fix(ci): grant rerun atom read scopes and refuse mixed failures
Add checks/pull-requests read permissions so the helper can fingerprint
infra cancels, and require every non-success leaf to be infra-classified
before authorizing gh run rerun --failed.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* fix(ci): require open PR for live-head stale gate
Stop treating a same-repo branch tip as live-PR evidence. Closed PRs can
leave the source branch at the failed run SHA with no open association;
query open pulls by repository-qualified head ref instead, and cover the
closed-PR + unchanged tip shape in self-check.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* fix(ci): require infra marker for cancel rerun
Bare cancelled jobs without shutdown/spot annotations are ordinary
(manual cancel) and must not authorize gh run rerun --failed.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* fix(ci): ignore aggregate gate jobs in infra-cancel classify
Validate workspace and Runtime summary fail as always() mirrors of leaf
outcomes and never carry runner/spot markers. Counting them as ordinary
blocked pure infra-cancel reruns whenever the workflow still reached the
gate. Exclude those names from classification and cover the shape in
self-check.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* fixer: address PR #6453 follow-up items
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* fix(ci): refuse mixed ordinary+infra job annotations
Jobs that record both a real failure annotation and a later runner
shutdown marker must classify as ordinary, not pure infra-cancel, so
gh run rerun --failed cannot auto-retry assertion failures.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* fix(ci): paginate check-run annotations before infra classify
GitHub defaults annotations to 30 results; a later ordinary failure must still
block gh run rerun --failed. Walk pages like paginate_jobs and cover the split
with a multi-page request fixture.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* fix(ci): drop tracked pycache from infra-cancel helper
PerishCode BLOCKING on #6453: remove the accidental CPython 3.14
bytecode under .github/scripts/__pycache__/ and ignore __pycache__/
plus *.py[cod] so local self-check/compile output stays out of git.
* fix(ci): open-PR merge_group freshness; ignore warning annotations
Merge-group infra-cancel retries no longer require the synthetic head to
still sit in the merge queue — workflow_run completed fires after required
checks already ejected it. Freshness is an open PR still associated with
the run (commit→pulls or gh-readonly-queue pr-N ref).
Warning-level annotations (e.g. scopes.ts full-plan fallback) no longer
count as ordinary failures that suppress pure infra-cancel retry.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* fix(ci): match merge_group PR heads and arm rerun e2e Vitest
Compare the live open PR head with the PR head SHA encoded in the
merge-queue branch before authorizing infra-cancel retry, and enroll the
rerun atom surfaces in scopes so PR CI runs the topology self-check.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
Brings back the commits that landed directly on the release branch and exist
nowhere else: #6480 (guard the windows e2e report download so
win_x64_smoke_mode: skip is usable on the stable lane), #6452 (retry NSIS
install and fail the setup step loudly), #6440 and #6437 (release/v0.18.0
auto-build defaults, both scoped by their own github.ref_name conditions),
#6435 (the amr_profile input that enables the Workspace Team transport), plus
the release-cut chores.
apps/packaged/package.json keeps main's 0.16.2 rather than taking the branch's
0.18.0: this repo advances main's version through its own
'chore(release): bump main to X.Y.Z ahead of stable' PRs (#6000, #5661, #5441),
never through a back-merge.
release-stable.yml offers win_x64_smoke_mode: skip, but choosing it makes the
release impossible to finish. The windows smoke uploads its e2e report with
if-no-files-found: warn, so a skipped smoke leaves the report directory empty
and publishes no artifact; finalize then downloads that artifact
unconditionally and fails — after every platform has already built, signed and
notarized, and after the pre-flight tag check has passed. Twenty-five minutes
of work is thrown away for a report nothing downstream reads.
Guard the download on the same condition that gates the smoke itself, matching
how the linux report is guarded by its build result. The mac report stays
unconditional because the mac smoke has no skip switch.
release-stable.yml and release-preview.yml never set OPEN_DESIGN_AMR_PROFILE
or OD_VELA_WEB_URL. Neither has a default in tools-pack, so velaWebUrl reached
the packaged config as undefined, workspaceTeamTransportEnv returned an empty
record, and the four vela transports were never injected into the daemon spawn
env. Both channels therefore shipped clients with Workspace Team silently
dormant — the build succeeds, the app installs and starts, and the absence only
surfaces once a package is in a user's hands.
beta and prerelease already inject the pair from their amr_profile input, which
is why prerelease validation looked correct: the artifact under test had the
feature and the stable artifact promoted from it would not have.
Stable and preview are production channels, so the pair is pinned instead of
dispatched — there is no legitimate stable build aimed at the test backend.
Naming prod explicitly is a no-op for AMR itself (the daemon already defaults to
prod in integrations/vela-profile.ts); it exists to satisfy the profile half of
the gate. A fork or PR build without the secret leaves the origin empty, so the
transports stay dormant rather than half-enabled.
(cherry picked from commit 3371ce4b2a)
Co-authored-by: lefarcen <935902669@qq.com>
* ci: shard web tests and retain visual failure artifacts
Split the 6m web workspace job into two vitest shards and keep
Playwright trace/screenshot on visual failure for diagnosis.
* test(e2e): pin visual-pr-capture retain-on-failure path
Assert playwright_visual uploads keep e2e/ui/reports/visual-test-results
for both visual-pr-capture and visual-ci artifacts so report-consumed
diagnostics cannot silently regress.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
* test(e2e): pin web shards and baseline visual diagnostics
Lock the two-way web_workspace_tests vitest matrix in the CI topology
contract and upload retain-on-failure traces from visual-baseline.yml,
the second consumer of playwright.visual.config.ts.
Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)
release-stable.yml and release-preview.yml never set OPEN_DESIGN_AMR_PROFILE
or OD_VELA_WEB_URL. Neither has a default in tools-pack, so velaWebUrl reached
the packaged config as undefined, workspaceTeamTransportEnv returned an empty
record, and the four vela transports were never injected into the daemon spawn
env. Both channels therefore shipped clients with Workspace Team silently
dormant — the build succeeds, the app installs and starts, and the absence only
surfaces once a package is in a user's hands.
beta and prerelease already inject the pair from their amr_profile input, which
is why prerelease validation looked correct: the artifact under test had the
feature and the stable artifact promoted from it would not have.
Stable and preview are production channels, so the pair is pinned instead of
dispatched — there is no legitimate stable build aimed at the test backend.
Naming prod explicitly is a no-op for AMR itself (the daemon already defaults to
prod in integrations/vela-profile.ts); it exists to satisfy the profile half of
the gate. A fork or PR build without the secret leaves the origin empty, so the
transports stay dormant rather than half-enabled.
UI P0 (especially project-collab) regularly drives the large runner
container working set to the 16Gi limit. Add a dedicated ui_p0 profile
on nexu-runners-xlarge and keep lighter browser jobs on large.
Production was deliberately held outside the workspace-team profile allowlist
until its Vela backend could actually serve the feature. It can now:
WORKSPACE_TEAM_ENABLED and the three billing-scope flags are true on prod
amr-api, and the resource hub it needs for team file sharing is configured
(S3-backed blob store instead of the chart's mock default, which would have
failed every push with 'unsupported protocol scheme mock' while comments kept
syncing through Postgres).
- Add prod to WORKSPACE_TEAM_AMR_PROFILES.
- Resolve OD_VELA_WEB_URL for prod from secrets.VELA_WEB_URL_PROD, so release
builds bake the production console origin.
- Drop the temporary amr_profile=test default on release/v0.18.0; release
builds now ship prod, with manual dispatch still able to target test or
feature-test.
The origin half of the gate is unchanged and still load-bearing: a build
without an injected origin stays dormant on every profile rather than pointing
at an unknown backend. Tests updated to lock the new prod behaviour and to keep
local/profile-less builds dormant.
(cherry picked from commit 5f5a589cd8)
Co-authored-by: lefarcen <935902669@qq.com>