Skip to content

Commit fadaa60

Browse files
authored
Merge pull request #18 from omdsh-dev/iteration/iter-20260814-fallbacks-release-usability
feat: npm release pipeline (PR-driven + Trusted Publishing) + consumer API/service
2 parents 449f6b0 + 8e19f82 commit fadaa60

26 files changed

Lines changed: 1844 additions & 8 deletions

.changes/archive/.gitkeep

Whitespace-only changes.

.changes/unreleased/.gitkeep

Whitespace-only changes.

.changes/unreleased/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Change fragments
2+
3+
Each file in this directory is a changelog fragment for the next release.
4+
`pnpm release:prepare` collects them into the next `## [<version>] - <date>`
5+
section of `CHANGELOG.md` (directly under `## [Unreleased]`), then moves the
6+
consumed fragments to `.changes/archive/<version>/`.
7+
8+
## Format
9+
10+
- Filename: any slug ending in `.md` (e.g. `add-foo.md`). `README.md` and
11+
`.gitkeep` are ignored.
12+
- Frontmatter (optional): a `category:` key groups the fragment's bullets
13+
under a `### <category>` heading in the changelog (default: `Changed`).
14+
- Body: one or more English bullet lines (`- ` prefix), rendered verbatim.
15+
16+
```markdown
17+
---
18+
category: Added
19+
---
20+
- Describe the change in one concise English bullet.
21+
- A second bullet if needed.
22+
```
23+
24+
Keep each fragment focused on a single user-visible change.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: Changed
3+
---
4+
- In-conversation fallback switch notice now reads 模型已降级 / Model downgraded with a warn-tone title (was neutral 模型切换 / Model switch).
5+
- Declared roles must configure a model chain: the settings card blocks saving a chain-less role (inline hint + banner), and host config validation warns on a missing/empty role chain (never crashes; runtime fallback to `rootChain` preserved).
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
category: Added
3+
---
4+
- PR-driven npm release pipeline: GitHub Actions `release-prep` (changelog fragments → `release vX.Y.Z` PR) + `release` (Trusted Publishing publish with provenance, tag, GitHub Release), zero long-term secrets.
5+
- Consumer surface: full runtime library API re-exported from the package root (`resolveRole` / `resolveChain` / `validateFallbacksConfig` / `detectLegacyKeys` / types) plus a named cordis service `llm-fallbacks` (`ctx.get('llm-fallbacks')` capability probe).
6+
- GitHub Actions CI verify pipeline (tests + full build) on PRs and `main` pushes.
7+
- Changelog fragment mechanism (`.changes/unreleased/`) with English `CHANGELOG.md`.

.github/workflows/release-prep.yml

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
name: Release prep
2+
3+
# Manual entry point: assemble changelog fragments, bump the version, build,
4+
# and open a `release vX.Y.Z` PR. Merging that PR runs the Release workflow
5+
# (publish + tag + GitHub Release). No NPM_TOKEN: npm auth in the Release
6+
# workflow is Trusted Publishing once configured, with an optional
7+
# NODE_AUTH_TOKEN bootstrap secret for the first publish; this workflow uses
8+
# only the built-in GITHUB_TOKEN.
9+
#
10+
# Trigger: Actions -> "Release prep" -> Run workflow. Pass an explicit target
11+
# version (e.g. 0.1.0-alpha.2) or leave the input empty for an auto bump
12+
# (`--patch`: X.Y.Z-pre.N -> N+1, stays in the prerelease line).
13+
14+
on:
15+
workflow_dispatch:
16+
inputs:
17+
version:
18+
description: "Target version (e.g. 0.1.0-alpha.2). Leave empty for auto pre-release bump."
19+
required: false
20+
type: string
21+
22+
permissions:
23+
contents: write
24+
pull-requests: write
25+
26+
concurrency:
27+
group: release-prep
28+
cancel-in-progress: false
29+
30+
jobs:
31+
prepare:
32+
runs-on: ubuntu-latest
33+
steps:
34+
- name: Checkout
35+
uses: actions/checkout@v4
36+
with:
37+
fetch-depth: 0
38+
39+
- name: Setup pnpm
40+
uses: pnpm/action-setup@v4
41+
with:
42+
version: 11.21.0
43+
44+
- name: Setup Node
45+
uses: actions/setup-node@v4
46+
with:
47+
node-version: "24.19.0"
48+
registry-url: https://registry.npmjs.org
49+
cache: pnpm
50+
51+
- name: Install dependencies
52+
run: pnpm install --frozen-lockfile
53+
54+
# Validate the input format BEFORE writing it to GITHUB_OUTPUT: a
55+
# malformed (e.g. multiline) value must fail here instead of injecting
56+
# extra output lines that downstream steps would read as the version.
57+
- name: Reject already-released version
58+
id: want
59+
env:
60+
INPUT_VERSION: ${{ inputs.version }}
61+
run: |
62+
V="$INPUT_VERSION"
63+
if [ -z "$V" ]; then V="auto"; fi
64+
if [ "$V" != "auto" ]; then
65+
case "$V" in
66+
*$'\n'*|*$'\r'*)
67+
echo "::error::Invalid version \"$V\" (must be a single line)."
68+
exit 1
69+
;;
70+
esac
71+
if ! printf '%s' "$V" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
72+
echo "::error::Invalid version \"$V\". Expected X.Y.Z or X.Y.Z-pre.N (e.g. 0.1.0-alpha.2)."
73+
exit 1
74+
fi
75+
if git rev-parse "v$V" >/dev/null 2>&1; then
76+
echo "::error::Tag v$V already exists."
77+
exit 1
78+
fi
79+
fi
80+
echo "version=$V" >> "$GITHUB_OUTPUT"
81+
82+
- name: Assemble fragments + bump version
83+
run: |
84+
V="${{ steps.want.outputs.version }}"
85+
if [ "$V" = "auto" ]; then
86+
pnpm release:prepare -- --patch
87+
else
88+
pnpm release:prepare -- "$V"
89+
fi
90+
91+
- name: Resolve resulting version
92+
id: ver
93+
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
94+
95+
- name: Validate release version
96+
run: pnpm release:validate -- "v${{ steps.ver.outputs.version }}"
97+
98+
- name: Build (release smoke)
99+
run: pnpm build
100+
101+
- name: Extract changelog notes
102+
run: |
103+
awk -v v="${{ steps.ver.outputs.version }}" '
104+
/^## \[/ { if (found) exit }
105+
index($0, "## [" v "]") == 1 { found = 1 }
106+
found { print }
107+
' CHANGELOG.md > /tmp/notes.md
108+
109+
- name: Commit prepared release
110+
run: |
111+
git config user.name "github-actions[bot]"
112+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
113+
BRANCH="release/v${{ steps.ver.outputs.version }}"
114+
git checkout -B "$BRANCH"
115+
# Explicit paths only: the release PR must contain exactly the
116+
# version/changelog/fragment changes, never stray files.
117+
git add package.json CHANGELOG.md .changes/
118+
git commit -m "chore(release): prepare v${{ steps.ver.outputs.version }}"
119+
git push --force-with-lease origin "$BRANCH"
120+
121+
- name: Ensure release label
122+
env:
123+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
124+
run: gh label create release --if-not-exists --color 0E8A16 || true
125+
126+
# State-aware PR handling. `gh pr view` also matches closed PRs, and
127+
# `gh pr edit` silently keeps them closed — which would make a re-run
128+
# after rollback (close PR -> re-run prep) look successful while
129+
# leaving no open PR. Query state explicitly:
130+
# open PR -> update it;
131+
# closed PR -> reopen it, then update the body;
132+
# none -> create a new one.
133+
# A closed PR is never edited in place.
134+
- name: Open or update release PR
135+
env:
136+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
137+
VERSION: ${{ steps.ver.outputs.version }}
138+
run: |
139+
BRANCH="release/v${VERSION}"
140+
TITLE="release v${VERSION}"
141+
{
142+
echo "Generated by the **Release prep** workflow."
143+
echo
144+
echo "Merging this PR publishes dsh-llm-fallbacks@v${VERSION} to npm (--provenance; Trusted Publishing once configured, NODE_AUTH_TOKEN bootstrap), tags v${VERSION}, and creates the GitHub Release."
145+
echo
146+
echo "## Changelog"
147+
echo
148+
cat /tmp/notes.md
149+
} > /tmp/pr-body.md
150+
151+
OPEN_NUM="$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number' 2>/dev/null || true)"
152+
if [ -n "$OPEN_NUM" ]; then
153+
gh pr edit "$OPEN_NUM" --title "$TITLE" --body-file /tmp/pr-body.md
154+
echo "Updated existing open release PR #${OPEN_NUM} for $BRANCH"
155+
exit 0
156+
fi
157+
158+
CLOSED_NUM="$(gh pr list --head "$BRANCH" --state closed --json number -q '.[0].number' 2>/dev/null || true)"
159+
if [ -n "$CLOSED_NUM" ]; then
160+
# Reopen first: `gh pr reopen` fails loudly on a merged PR instead
161+
# of silently leaving the release blocked.
162+
gh pr reopen "$CLOSED_NUM"
163+
gh pr edit "$CLOSED_NUM" --title "$TITLE" --body-file /tmp/pr-body.md
164+
echo "Reopened release PR #${CLOSED_NUM} for $BRANCH"
165+
else
166+
gh pr create --base main --head "$BRANCH" --title "$TITLE" \
167+
--body-file /tmp/pr-body.md --label release
168+
echo "Created release PR for $BRANCH"
169+
fi

.github/workflows/release.yml

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
name: Release
2+
3+
# PR-driven publish. Merging a `release vX.Y.Z` PR (opened by the Release prep
4+
# workflow) runs the whole release inline on the pull_request event:
5+
# validate -> build -> npm publish (provenance) -> tag -> GitHub Release.
6+
# There is deliberately NO push:tags auto-publish path: a manual
7+
# `git tag && git push --tags` does not publish.
8+
#
9+
# No NPM_TOKEN: npm auth is Trusted Publishing (OIDC id-token + registry-url)
10+
# once the package exists; during first-publish bootstrap (the package is not
11+
# on the registry yet, so TP cannot be configured) the optional
12+
# NODE_AUTH_TOKEN repo secret is used instead. The tag push and GitHub
13+
# Release use the built-in GITHUB_TOKEN.
14+
#
15+
# NOTE: Node 24 ships npm with working Trusted Publishing / provenance
16+
# (sigstore). Do not use Node 22 or `npm install -g npm@latest` on Node 22 —
17+
# that breaks provenance with MODULE_NOT_FOUND: sigstore.
18+
19+
on:
20+
pull_request:
21+
types: [closed]
22+
branches: [main]
23+
24+
permissions:
25+
contents: write
26+
id-token: write
27+
28+
concurrency:
29+
group: release
30+
cancel-in-progress: false
31+
32+
jobs:
33+
release:
34+
# Trigger only on PRs opened by the Release prep workflow (branch
35+
# `release/v*`): a same-repo PR merely *titled* `release v…` must not
36+
# publish. Merged check + title + head-ref prefix are all required.
37+
if: >
38+
github.event.pull_request.merged == true
39+
&& startsWith(github.event.pull_request.title, 'release v')
40+
&& startsWith(github.event.pull_request.head.ref, 'release/v')
41+
runs-on: ubuntu-latest
42+
timeout-minutes: 30
43+
steps:
44+
- name: Checkout merge commit
45+
uses: actions/checkout@v4
46+
with:
47+
ref: ${{ github.event.pull_request.merge_commit_sha }}
48+
fetch-depth: 0
49+
50+
- name: Setup pnpm
51+
uses: pnpm/action-setup@v4
52+
with:
53+
version: 11.21.0
54+
55+
- name: Setup Node
56+
uses: actions/setup-node@v4
57+
with:
58+
node-version: "24.19.0"
59+
registry-url: https://registry.npmjs.org
60+
cache: pnpm
61+
62+
- name: Install dependencies
63+
run: pnpm install --frozen-lockfile
64+
65+
- name: Resolve version
66+
id: ver
67+
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
68+
69+
- name: Detect prerelease
70+
id: pre
71+
run: echo "prerelease=$(node -p "/-/.test(require('./package.json').version)")" >> "$GITHUB_OUTPUT"
72+
73+
# The Release prep workflow derives the version from the PR title
74+
# ("release v<version>") and bumps package.json to match. If the merged
75+
# PR drifted from that contract (e.g. the version was edited inside the
76+
# PR without re-running prep), publishing the drifted version would
77+
# silently produce a mismatched tag and an empty GitHub Release — fail
78+
# loudly instead.
79+
- name: Cross-check version vs PR title
80+
env:
81+
VERSION: ${{ steps.ver.outputs.version }}
82+
TITLE: ${{ github.event.pull_request.title }}
83+
run: |
84+
set -euo pipefail
85+
TITLE_VER="${TITLE#release v}"
86+
if [ "$TITLE_VER" != "$VERSION" ]; then
87+
echo "::error::PR title version \"${TITLE_VER}\" does not match package.json version \"${VERSION}\". Re-run Release prep for v${VERSION} instead of hand-editing the PR."
88+
exit 1
89+
fi
90+
echo "PR title and package.json agree on ${VERSION}"
91+
92+
- name: Validate release version
93+
env:
94+
VERSION: ${{ steps.ver.outputs.version }}
95+
run: pnpm release:validate -- "v${VERSION}"
96+
97+
- name: Build
98+
run: pnpm build
99+
100+
# `--tag latest`: npm >= 11 (bundled with Node 24) hard-throws when
101+
# publishing a prerelease without an explicit --tag. The first release
102+
# (0.1.0-alpha.2) lands on the default `latest` dist-tag so
103+
# `npm i dsh-llm-fallbacks` resolves; a later stable 0.1.0 takes over
104+
# `latest` naturally.
105+
- name: Publish to npm
106+
env:
107+
# Optional bootstrap-only secret: absent once Trusted Publishing is
108+
# configured (npm prefers OIDC when available). setup-node with
109+
# registry-url writes it into .npmrc, so npm uses it automatically
110+
# for the first publish when no trusted publisher exists yet.
111+
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
112+
run: npm publish --provenance --access public --tag latest
113+
114+
- name: Configure git identity
115+
run: |
116+
git config user.name "github-actions[bot]"
117+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
118+
119+
- name: Create + push tag
120+
env:
121+
VERSION: ${{ steps.ver.outputs.version }}
122+
run: |
123+
set -euo pipefail
124+
TAG="v${VERSION}"
125+
if git rev-parse "$TAG" >/dev/null 2>&1; then
126+
echo "Tag $TAG already exists; skipping tag creation."
127+
exit 0
128+
fi
129+
# git >= 2.55 requires a message for -a tags.
130+
git tag -a -m "release v${VERSION}" "$TAG"
131+
git push origin "$TAG"
132+
133+
# Fail when no changelog section matches the published version: an empty
134+
# GitHub Release is a silent quality regression, not a recoverable one.
135+
- name: Extract changelog notes
136+
id: changelog
137+
env:
138+
VERSION: ${{ steps.ver.outputs.version }}
139+
run: |
140+
set -euo pipefail
141+
NOTES_FILE="${RUNNER_TEMP}/release-notes-${VERSION}.md"
142+
awk -v v="$VERSION" '
143+
/^## \[/ { if (found) exit }
144+
index($0, "## [" v "]") == 1 { found = 1 }
145+
found { print }
146+
' CHANGELOG.md > "$NOTES_FILE"
147+
if [ ! -s "$NOTES_FILE" ]; then
148+
echo "::error::No changelog section found for v${VERSION} in CHANGELOG.md; refusing to create an empty GitHub Release."
149+
exit 1
150+
fi
151+
echo "notes_file=${NOTES_FILE}" >> "$GITHUB_OUTPUT"
152+
153+
- name: Create GitHub Release
154+
uses: softprops/action-gh-release@v2
155+
with:
156+
tag_name: v${{ steps.ver.outputs.version }}
157+
name: v${{ steps.ver.outputs.version }}
158+
body_path: ${{ steps.changelog.outputs.notes_file }}
159+
draft: false
160+
prerelease: ${{ steps.pre.outputs.prerelease == 'true' }}

.mstar/knowledge/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
| Document | Source Plan | Description | Status |
44
|----------|-------------|-------------|--------|
5-
| [architecture-patterns/dsh-llm-fallbacks.md](architecture-patterns/dsh-llm-fallbacks.md) | llm-fallbacks-plugin | dsh LLM fallback 双 waterfall 恢复架构(ADR-1..4、两块制配置模型 rootChain + roles.list/rules + inherit 语义、append-not-replace、legacy 三通道与 schemastery 未知键保留、warn-not-crash 校验、单遍历决策、冷却/安全阀、always-cap、状态机、gateway 通道与 KD-G3/种子不变量、入口面、已知限制) | Active |
6-
| [best-practices/dsh-cordis-plugin-authoring.md](best-practices/dsh-cordis-plugin-authoring.md) | llm-fallbacks-plugin | dsh 第三方 cordis 插件创作 playbook(bundle/client/真实包链接 DSH_HOME/构建/设置入口两形态与 gateway 数据面/remote events 失效刷新/schemastery 组合未知键保留与 schema-breaking 迁移三通道/事件监听组合顺序与 persona 可读性/关键坑) | Active |
5+
| [architecture-patterns/dsh-llm-fallbacks.md](architecture-patterns/dsh-llm-fallbacks.md) | llm-fallbacks-plugin | dsh LLM fallback 双 waterfall 恢复架构(ADR-1..4、两块制配置模型 rootChain + roles.list/rules + inherit 语义、append-not-replace、legacy 三通道与 schemastery 未知键保留、warn-not-crash 校验、单遍历决策、冷却/安全阀、always-cap、状态机、gateway 通道与 KD-G3/种子不变量、入口面、**消费面(库 API re-export + 具名 service llm-fallbacks)**已知限制) | Active |
6+
| [best-practices/dsh-cordis-plugin-authoring.md](best-practices/dsh-cordis-plugin-authoring.md) | llm-fallbacks-plugin | dsh 第三方 cordis 插件创作 playbook(bundle/client/真实包链接 DSH_HOME/构建/设置入口两形态与 gateway 数据面/remote events 失效刷新/schemastery 组合未知键保留与 schema-breaking 迁移三通道/事件监听组合顺序与 persona 可读性/关键坑/**具名 cordis 服务注册(值形式 ctx.provide + 多 fiber dedupe + Context merge + createRequire version)**| Active |
77
| [workflow-patterns/harness-sandbox-verification.md](workflow-patterns/harness-sandbox-verification.md) | llm-fallbacks-plugin | dsh 沙箱兼容验证模式(scratch DSH_HOME / 只读 git apply --check / 编译级验证) | Active |
88
| [build-errors/css-modules-hash-invalid-selector.md](build-errors/css-modules-hash-invalid-selector.md) | llm-fallbacks-settings-style | CSS Modules 哈希类名数字开头 → 浏览器静默丢弃样式规则(构建根因 + 双位置契约断言 + CSSOM 验证模式) | Active |
99
| [build-errors/dsh-client-bundle-purity-gate.md](build-errors/dsh-client-bundle-purity-gate.md) | fallbacks-plugin-config-card | Client bundle purity 门失明缺口:alwaysBundle 静默内联使 require-only 断言失明(94 kB 负向探针实证);resolveId 门 + emitted-surface token 扫描双层修复 | Active |
@@ -13,3 +13,4 @@
1313
| [architecture-patterns/dsh-conversation-surface-mounting.md](architecture-patterns/dsh-conversation-surface-mounting.md) | fallbacks-aux-seams | 会话转录挂载模式:conversationEvents 注册表 + conversation.chat.node keyed 座位双段挂载;纯渲染纪律与 degrade-never-crash(W-001,引擎无 try/catch) | Active |
1414
| [best-practices/dsh-settings-ui-fidelity.md](best-practices/dsh-settings-ui-fidelity.md) | llm-fallbacks-settings-ui-fidelity | dsh web 设置 UI 保真参考(参照文件地图含插件配置卡 chrome、几何/token 词表、逐维度对照方法、用户可见差异裁决) | Active |
1515
| [developer-experience/pnpm11-workspace-config-and-windows-link-farm.md](developer-experience/pnpm11-workspace-config-and-windows-link-farm.md) | fix/install 自检(dsh-advisor PR #11 经验 commit 294aff1b → ac994ea) | pnpm 11 workspace 配置迁移 + Windows 安全 link farm + cordis scoped 对齐(.npmrc 静默忽略/死 token、prerelease-tuple 规则实证、requiredPeers 豁免、legacy 清理、junction/file 分型、USERPROFILE) | Active |
16+
| [best-practices/npm-release-pipeline.md](best-practices/npm-release-pipeline.md) | fallbacks-npm-release | npm Release Pipeline(PR-driven + Trusted Publishing):4 坑(TP 仅已存在包可配/无 pre-registration、node 24 provenance、npm≥11 prerelease 必须 --tag、closed-PR 重跑 state-aware)+ bootstrap 路径 + changelog fragments | Active |

0 commit comments

Comments
 (0)