Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
446f27d96e | ||
|
|
ef81a1a59c | ||
|
|
d8ed8c2147 | ||
|
|
f3ae03373e |
+1
-1
@@ -2,4 +2,4 @@
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Document write verification** (#960) — avoids false partial-success results when normalized Markdown, paginated blocks, inline images, or version reverts are confirmed by server readback. Document reverts and media inserts now require explicit readback evidence and report partial success when the server cannot prove the requested result.
|
||||
- **Document write verification** (#960) — avoids false partial-success results when normalized Markdown, paginated blocks, inline images, or version reverts are confirmed by server readback.
|
||||
+1
-3
@@ -25,9 +25,7 @@ category: Added
|
||||
|
||||
发布 beta 时,`scripts/release/prepare-changelog.sh` 会按分类和文件名稳定排序,
|
||||
将未归档 fragments 汇总为唯一的版本章节,并移动到
|
||||
`.changes/released/<version>/`。beta 发布后若有新 fragments 合入并直接准备 stable,
|
||||
stable 封板会把它们追加到明确的 post-beta 小节,并归档到正式版本目录;没有新
|
||||
fragments 时仍只生成原有 beta 晋级模板。因此 release-seal PR 是唯一会修改
|
||||
`.changes/released/<version>/`。因此 release-seal PR 是唯一会修改
|
||||
`CHANGELOG.md` 的 PR;它同时归档已消费的 fragments,供审计追溯。
|
||||
归档只能在同一个 release-seal PR 中以原样移动完成;CI 会拒绝直接修改、
|
||||
删除或重写已归档文件。
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Command typo guidance** — returns a validation error with up to three nearest command suggestions and the parent `--help` entry instead of printing the full command list.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **Download host trust policy** — retires the static DingTalk/OSS download
|
||||
host allowlist, the dial-time public-IP refusal, and the IP-literal
|
||||
refusal from both the shared local download path (`drive +download`,
|
||||
`drive +version-download`, doc/minutes artifact downloads) and the chat
|
||||
message-resource path (`chat +messages-resource-download`,
|
||||
`--download-resources`). Download URLs only require HTTPS without userinfo
|
||||
and accept non-default HTTPS ports, because every dimension of a
|
||||
dedicated-deployment storage endpoint — custom domain, port, and network
|
||||
location — is decided by the customer deployment and cannot be enumerated
|
||||
or configured client-side. Verified on a dedicated deployment whose
|
||||
storage domain resolves to a customer-intranet address. Downloads align
|
||||
with the official GUI client, which applies no client-side SSRF
|
||||
interception: download URLs only ever come from authenticated service
|
||||
responses (no command accepts a user-supplied URL), TLS hostname
|
||||
verification pins the connection to the requested host, redirects are
|
||||
re-validated per hop, and service credential headers are stripped once a
|
||||
redirect leaves the original origin.
|
||||
- **Upload host trust unchanged** — upload target URLs (`drive +upload`,
|
||||
minutes audio upload) keep the pre-existing public DingTalk/OSS trusted
|
||||
host requirement through a dedicated upload validator, so removing the
|
||||
download allowlist does not widen where local file bytes can be sent;
|
||||
the validator also keeps the pre-existing default-port-only HTTPS rule
|
||||
(DingTalk/OSS upload endpoints always serve on 443, so non-default ports
|
||||
accepted for dedicated-deployment downloads stay anomalous for uploads).
|
||||
Download credential headers are issued together with the download URL by
|
||||
the same authenticated service response and follow it as-is on the first
|
||||
request; redirects leaving the original host still strip them.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Fork pull-request admission** — keeps the read-only Reviewer Router identity check fail-closed while allowing external contributors' CI to use the reviewed public App slug when GitHub withholds repository variables.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Markdown append chunking rewritten around safe split positions** — long markdown is now split so that every chunk is a complete, self-contained top-level block sequence, which is what `update_document mode=append` requires: the server inserts a brand new structure per call and cannot continue the previous one. Split points are chosen strictly by how much they change the rendered document — fully safe boundaries (blank lines, block starts that interrupt a paragraph) before boundaries that need repair (a table's rows now carry a re-emitted header and delimiter row; a fenced code block is closed and reopened with its original marker and info string) before boundaries that merely restructure (long paragraphs, list items) before a hard character cut. Within a tier the latest boundary in the window wins, since all chunks land in the same document. Every boundary that changes the rendered structure is reported in a new `degradations` field instead of being applied silently.
|
||||
- **Fixed markdown chunking dropping a newline** — the previous splitter rebuilt block text from lines and lost one `\n` whenever the content's last line began a heading, table or code fence, so `"para\n# Title"` was written as `"para# Title"` and the heading stopped being a heading. Roughly one in five randomly generated documents was affected. The new splitter slices by offset and never rebuilds text, making content preservation structural.
|
||||
- **Fixed oversized tables and code blocks being cut mid-cell and mid-fence** — the hard-split path never received the block type, so it cut at arbitrary character boundaries despite claiming to preserve table and code block integrity.
|
||||
- **Fixed readback verification comparing against content the server never receives** — `doc +create` / `doc +update` verified the readback against the raw input, so any repaired boundary (and, previously, any paragraph split) failed verification on large documents. Verification now compares against the document the chunk plan says the server should hold.
|
||||
- **Unified four markdown write paths onto one splitter** — `doc create` / `doc update`, `doc +create` / `doc +update` and `doc +checkpoint-update` now share `helpers.SplitMarkdownForAppend` and one limit constant (30000 runes), replacing two independent implementations plus one path that never chunked at all. `doc +checkpoint-update` accepts `@file` and stdin content, so oversized input was reachable there while the equivalent `doc +update` chunked. `doc +doc-append` takes `--text` from argv only and now rejects oversized input with a pointer to `doc +update` rather than sending one oversized call.
|
||||
- **`doc update --index` now fails closed when the content requires chunking** — each chunk creates an unpredictable number of blocks, so the insertion point for later chunks is unknowable; the flag was previously accepted and silently ignored.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Drive `--latest` refuses incomplete Top-N** (#899) — `dws drive list --latest` used to
|
||||
exit 0 with a "Top-N" computed over a partially scanned tree whenever a directory read
|
||||
failed mid-recursion (permission denied, API error), letting an incomplete set pose as the
|
||||
globally newest files. Truncation at the 2000-item scan cap and mid-recursion directory
|
||||
failures now both fail closed (`LATEST_SCAN_TRUNCATED` / `LATEST_SCAN_INCOMPLETE`), report
|
||||
the first failing folder with its depth and reason, and emit a recovery command that
|
||||
reproduces the original candidate set — query domain, `--folder`, `--pattern`, `--type`,
|
||||
`--start` and `--end` are all carried over. On POSIX shells each user-supplied value is
|
||||
quoted so a URL query string or a shell metacharacter cannot change how the copied command
|
||||
parses. On Windows no quoting form is safe for both `cmd.exe` and PowerShell, so values
|
||||
containing metacharacters are not inlined at all: the command carries a placeholder and the
|
||||
original value is shown on a separate line marked as data rather than an executable command.
|
||||
Unrecoverable errors under `--latest` return the root cause instead of a partial result.
|
||||
Remote-controlled folder names and server error text are stripped of ANSI escapes and
|
||||
control characters before they reach the plain-text stderr message. The internal `sortTime`
|
||||
sort key no longer leaks into `drive list --depth` output on any path.
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Drive folder synchronization** — adds `dws drive status`, `dws drive pull`,
|
||||
`dws drive push`, and `dws drive sync` for file-level comparison and transfer
|
||||
between a local folder and a Drive folder. Differences come from exact MD5 by
|
||||
default or from modification time with `--quick`; `status` is read-only, `pull`
|
||||
and `push` are one-directional with `--if-exists skip|smart|overwrite`, and
|
||||
`sync` is bidirectional with `--on-conflict remote-wins|local-wins|keep-both|ask`.
|
||||
Only regular files are transferred — online documents and shortcuts are skipped,
|
||||
neither side deletes extra files, downloads are staged through a temporary file
|
||||
and committed with an atomic rename, and remote names that would escape
|
||||
`--local-folder` are reported as failures instead of being written. Every command
|
||||
prints a structured summary on stdout and exits non-zero when any item fails.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **International DingTalk region support** — adds `.io` login and MCP routing, pre-release endpoint overrides, and profile-aware gateway selection while preserving the existing `.com` flow.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Privacy-safe CLI telemetry** (#1009) — reports reviewed command outcomes and profile identity dimensions while excluding command arguments, output, paths, device fingerprints, and automatic system dimensions; `DO_NOT_TRACK=1` disables reporting.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Feedback survey entry in root help** (#1019) — `dws --help` now closes with a Feedback section linking the user-experience survey form.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **Chat IM ID flags** (#954) — standardizes chat command entry points on `--conversation-id` for conversation IDs and `--message-id` for message IDs, so help, Schema, and Agent recommendations use the same canonical flags.
|
||||
- **Legacy chat flag compatibility** (#954) — keeps older chat IM ID flags such as `--group`, `--id`, `--chat`, `--open-conversation-id`, `--msg-id`, and `--open-message-id` working as compatibility aliases where applicable, while hiding migrated aliases from recommended help and Schema surfaces.
|
||||
- **Chat group bots target flag** (#954) — keeps `dws chat group bots` on the visible `--group` flag; this command does not register `--group-name`, and `--group` accepts either an openConversationId or a uniquely resolved group name.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Chat card update evidence** — distinguishes an accepted update request from an independently verified visible update, preserving the real `bizId` and warning callers not to repeat an unverified write.
|
||||
- **Chat command guidance** — splits message and group references by task and explains that `--from` is ambiguous between sender and time-range intent.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **Faster Schema Catalog assembly** — projects typed values into payload JSON
|
||||
without re-running a validation scan over documents `json.Marshal` has just
|
||||
produced, cutting roughly a third of the projection work across the full tool
|
||||
set. Untrusted JSON input keeps its existing validation.
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Wiki Shortcut workflows** — publishes 20 reviewed space, member, node, and
|
||||
activity shortcuts with strict collection validation, cursor handling,
|
||||
write-terminal evidence, safe read-backs where the backend supports them,
|
||||
task-oriented routing, and documented backend
|
||||
boundaries.
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Aitable pagination and Minutes unshare verification** (#1006) — keeps
|
||||
record queries on the service's 20-record page boundary so multi-page reads
|
||||
and mutation readbacks no longer report false retryable failures, preserves
|
||||
`totalCount` when supplied, validates `--dry-run` plans before transport,
|
||||
follows active deletion readback continuations before proving absence, and
|
||||
rejects Minutes unshare success until the listening note exists and the
|
||||
service acknowledges the exact task and member targets.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Robot group reference replies** (#928) — `chat message send-by-bot` supports paired `--reply` and `--ref-sender` flags for Markdown replies that quote an existing group message.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **AI Table parameter aliases** — accepts reviewed equivalent spellings for Base, table, workflow, search, pagination, and description parameters while keeping role-changing or semantically ambiguous inputs blocked.
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **AI Table server-side statistics** — adds `dws aitable record stats` for
|
||||
ungrouped record-set metrics through `query_records_stats`, plus `dws aitable
|
||||
record group-stats` for grouped, distinct, and advanced aggregation through
|
||||
`query_stats`; both commands validate their JSON aggregation contracts before
|
||||
dispatch.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Calendar event share-info** (#980) — adds `dws calendar event share-info` to fetch a calendar event's share info (title, organizer, location, join info) for sharing with others; supports `--calendar-id` and `--language`.
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Calendar and To-do Shortcut workflows** — aligns 47 public task-oriented
|
||||
entries with lark-cli where the DingTalk backend supports equivalent
|
||||
semantics, rejects malformed or missing collections instead of returning
|
||||
false empty success, preserves truthful pagination, and requires stable
|
||||
identifiers plus read-back or explicit terminal receipts for writes. Adds
|
||||
deterministic contract coverage, a PII-safe live E2E runner, and a sanitized
|
||||
capability review with documented platform boundaries.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Chat sender identity guards** — preserves unverified mixed sender inputs after exact message `senderId` matches and aligns `--sender-query` Skill guidance with fail-closed Runtime behavior.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **Doc/drive description scope** — restates the `dingtalk-doc` description as document-entity-and-content operations with an explicit exclusion list, and narrows `dingtalk-drive` to file-level management of DingTalk documents, so first-round Agent selection separates content work from file management without changing CLI behavior.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Doc and Sheet comment lifecycle commands** — adds `comment batch-query`,
|
||||
`comment resolve`, `comment restore`, and the lightweight
|
||||
`comment react-reply` to both `dws doc` and `dws sheet`. The two domains share
|
||||
the same `doc-comment` MCP capabilities; batch queries preserve input order
|
||||
for repeated `topicId:commentKey` references, while reaction replies require
|
||||
DingTalk reaction names such as `憨笑` or `鼓掌` rather than raw Unicode emoji.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Sheet SourceRange dropdowns** — supports range-backed dropdowns across direct, cell, and batch write paths, with structured readback for valid and invalid references. Batch `set-dropdown` now rejects unsupported top-level `colors` / `source-colors`; Inline colors belong in `options[].color`, while SourceRange color writes remain unsupported.
|
||||
- **Sheet read completion metadata** — documents and preserves returned ranges, truncation reasons, and partial-read status for large range and CSV reads.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Windows event bus lifecycle** — start event consumers without unsupported inherited file descriptors, stop buses through local IPC with a termination fallback, and preserve subscription cleanup when startup fails.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **Attendance and Mail Shortcuts** (#1045) — publishes only capabilities with
|
||||
strict response, identity, pagination, and real-data verification while
|
||||
retaining historical CLI discovery and argument compatibility for commands
|
||||
that remain unavailable to agents. Mailbox auto-resolution now accepts both
|
||||
reviewed string and object response shapes, and Attendance date ranges cover
|
||||
the complete requested end date without dropping cross-midnight punches whose
|
||||
actual check time is inside the requested range. The schedule query remains
|
||||
CLI-compatible but is withheld from the Agent catalog because its downstream
|
||||
service returns a successful process exit with a null body for both populated
|
||||
and empty ranges.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **Chat group roles** (#1058) — exposes the single-value `--role-id` flag for assigning one custom group role while preserving hidden `--role-ids` compatibility.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **招聘职位管理** (#976) — 新增招聘职位列表、详情查询和职位创建命令。
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Chat user mentions** — preserves literal `<@openDingTalkId>` tokens in current-user Markdown messages and rejects mismatches between message-body mentions and mention flags before sending.
|
||||
- **Chat direct media** — uses the IM upload target field for current-user direct file, audio, and video uploads, then uses the Chat receiver field for final message delivery.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **CLI compatibility governance** — adds a reviewed two-stage path for hiding retained legacy commands or optional `NoOpt=true` boolean flags from Help and Schema when their activated capability moves to a dedicated command, with legacy-leaf, complete parameter/constant mapping, durable runtime constant evidence, protected framework bridges, dry-run preservation, parameter-collision, and fail-closed required-parameter checks.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **OA admin approval query** — `oa approval list-by-admin` queries approval instances of a template with admin scope, with simple flags and an advanced `--request` mode; `startTime`/`endTime` use `yyyy-MM-dd HH:mm:ss` strings per the 2026-08 MCP contract update (ISO-8601 flag inputs auto-convert), and pageSize/time format are validated client-side with localized errors.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Shortcut functional workflows** (#1050) — fixes truthful Drive push/sync previews, strict AITable write verification and deletion accounting, lossless Wiki feeds, and false-success handling across task, Contact, Minutes, and Wiki operations.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Chat personal emotions** — adds `chat emotion list`, `chat emotion send`, and `chat emotion favorite` for current-user personal favorite emotion listing, sending, and favoriting.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Minutes, DingTalk tasks, and Wiki parameter aliases** — adds reviewed parameter-name normalization, ambiguity guards, and end-to-end payload coverage for the three products.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Calendar empty windows** (#1074) — returns a legitimate empty result when the service emits its exact exhausted empty-event sentinel.
|
||||
- **Task update verification** (#1074) — compares due-time readback as exact milliseconds so committed updates are no longer reported as failures.
|
||||
- **Comment reaction validation** (#1074) — narrows accepted reaction input to reviewed DingTalk emoji names and rejects Unicode emoji and unsupported names such as `like` and `heart` before the RPC.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **OA, DING, and Report shortcuts** — hardens response, identity, pagination, and confirmation contracts; publishes verified form search, receiver status, and report read workflows while withholding shortcuts that lack trustworthy downstream evidence.
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **OAuth refresh falls back to the organization mirror** — when the server rejects the
|
||||
current identity's `refresh_token` with the reviewed `invalidParameter.authCode.notFound`
|
||||
business code, `dws` now retries once with the still-valid token mirrored in the same
|
||||
organization's slot (same corp, matching or backfilled user identity) before giving up,
|
||||
and writes the rotated credential back to both the identity and the organization slots so
|
||||
the fallback stays usable on later refreshes. Transient failures and direct-mode HTTP
|
||||
rejections without a reviewed business code do not trigger the fallback.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **Stable release sealing** — directly preparing a stable release now renders and archives release fragments merged after its beta baseline, avoiding a forced extra beta solely to consume pending notes.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Drive permission get-setting** (#1056) — adds `dws drive permission get-setting --node <ID>` to inspect a document-space node's permission settings (permission mode, share scope, and permission policies) in one call.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Whiteboard shortcuts** (#1082) — adds strict query and confirmed update workflows with stable-target receipts and exact readback verification.
|
||||
- **Sheet shortcut hardening** (#1082) — makes worksheet listing and cell-range reads fail closed on malformed, ambiguous, or truncated responses, publishes a closed reviewed output shape, and preserves non-executing `--dry-run` previews for range reads.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **AiSearch and Contact shortcuts** (#1083) — adds strict people search and reviewed unified results; people results must use the live-reviewed `person` source, and exact mobile lookups normalize accepted formatting before calling the dedicated mobile interface. Agent/public discovery keeps `contact +list-roles`, `contact +list-roster-fields`, `contact +get-roster`, and incomplete Live routes unavailable rather than publishing ambiguous results, while the historical Contact CLI commands retain legacy MCP execution and real error propagation. The legacy role-list projection preserves the service's reviewed null placeholder without exposing that ambiguous row through Agent Result contracts.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **Permission error guidance and error rendering** (#1085) —
|
||||
permission-denied responses now exit with the `AUTH_PERMISSION_DENIED` code
|
||||
instead of a generic business-error rendering; document/wiki-specific errors
|
||||
(the drive-specific codes `forbidden.accessDenied` / `forbidden.no.auth`,
|
||||
or the role-threshold wording like
|
||||
“需要您具备 MANAGER 及以上角色”) carry apply-permission guidance
|
||||
(`dws drive permission apply-info` / `dws drive permission apply`), while
|
||||
permission failures carrying only generic code names (`FORBIDDEN`,
|
||||
`NO_PERMISSION` — also returned by attendance and event-subscription tools)
|
||||
or other products' wording keep their product-specific or
|
||||
product-neutral suggestion instead of a misleading document-permission hint;
|
||||
member-validation failures such as
|
||||
“用户不存在/不属于当前组织” are classified as tool errors with a
|
||||
`--members`-with-`corpId` suggestion instead of a misleading
|
||||
resource-not-found error; business error output now surfaces the backend
|
||||
message with `code`/`logId` appended for traceability; and the
|
||||
`update_permission` / `remove_permission` / `update_member` /
|
||||
`remove_member` tools — whose servers return a literal `null` on successful
|
||||
no-payload writes — now render `{}` so downstream JSON consumers do not fail
|
||||
parsing `null`; other tools keep raw `null` output unchanged.
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Permission and member list pagination** (#1085) — `drive/doc permission
|
||||
list` and `wiki member list` now accept `--next-token` to follow the
|
||||
server-side cursor (output carries `totalCount`/`hasMore`/`nextToken`) and
|
||||
map `--limit` to `pageSize` capped at 50 instead of the rejected `maxResults
|
||||
200` path; `permission add/update/remove` and `wiki member add/update/remove`
|
||||
additionally accept a `--members` JSON array covering USER/DEPT/CONVERSATION/TAG
|
||||
grantee types. The optional `--notify` defaults to `false` and is omitted from
|
||||
the server request unless passed explicitly, so member grants no longer notify
|
||||
recipients by default. These commands also declare cursor pagination
|
||||
(`next-token`) in the Agent schema contract, mirroring the internal CLI parity
|
||||
change. Because a single batch remove can revoke access for up to 30
|
||||
USER/DEPT/CONVERSATION/TAG members — where departments, chats, and role
|
||||
groups can indirectly affect many more users — `drive/doc permission
|
||||
remove` and `wiki member remove` now declare
|
||||
`confirmation=user_required` and gate the actual tool call behind user
|
||||
confirmation (`--yes`, an interactive yes, or `--dry-run` preview); their
|
||||
confirmation-gate failure now also passes through verbatim instead of being
|
||||
reclassified as a permission-denied or unclassified error.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Agoal scorecard search-entities** — `dws agoal scorecard search-entities` searches scorecard metrics and key items by keyword, returning matching entity info (scorecard ID, entity ID, entity type, title, owning team) with optional `--page`/`--page-size` pagination.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **AITable datasource shortcuts** — adds 7 shortcuts for datasource sync management (`+datasource-create`, `+datasource-update`, `+datasource-sync`, `+datasource-sync-status`, `+datasource-get-config`, `+datasource-list-sources`, `+datasource-get-fields`) and updates the `dingtalk-aitable` skill with routing rules and a new `aitable-datasource.md` reference guide.
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Doc public-link and historical-version reads** — `dws doc read` forwards
|
||||
the reviewed `password` (internet-public documents with password protection)
|
||||
and `historyVersion` (read content as of a listed historical version; `0`
|
||||
denotes the document's initial version) parameters on the markdown, JSONML,
|
||||
and scope read paths via `--password` / `--version`; `dws doc +fetch` gains
|
||||
`--password` and `--version` with the same `historyVersion` forwarding, while
|
||||
`--revision` stays rejected with explicit guidance: revision is the document
|
||||
edit revision returned by JSONML reads for `+update --expected-revision`
|
||||
conditional writes, not a historical version number.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Edu & College vendor extensions** — adds five hidden vendor extension commands for education scenarios: `dws edu-contact` (school/class/family/teacher contact management), `dws edu-group` (student/class group lifecycle), `dws edu-app` (homework, notices, report cards, diplomas, class circles), `dws edu-familygroup` (family group management, child binding, app permissions), and `dws college-contact` (university dept/employee/alumni/graduate management). All route to dedicated MCP servers via `callMCPToolOnServer`.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Legacy global slot recovery** — recovers a rejected identity refresh from the legacy global keychain slot when the organization mirror is absent, with strict corp/user matching so blank-user legacy tokens only recover for single-account organizations.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **OA approval attachment upload** — `dws oa approval attachment upload --file <path>` uploads a local file as an approval attachment in one command: it initializes the upload credential (MCP `oa/init_attachment_upload_info`), HTTP PUTs the file to OSS, then commits it (MCP `oa/commit_attachment_upload_info`). `--file-name` defaults to the file's base name and `--md5` is auto-computed when omitted.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Sheet floating images** — supports creating or replacing a floating image directly from a local file with `create-float-image --file` and `update-float-image --file`, while retaining the existing `--src` workflow.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Added
|
||||
---
|
||||
|
||||
- **Sheet revision changesets** — adds read-only commands for querying the current workbook revision and reviewing Agent-readable changes between revisions, with guidance for distinguishing revisions from saved history versions and safely selecting rollback targets.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Removed
|
||||
---
|
||||
|
||||
- **Education and college vendor extensions removed** — removes `dws edu-contact`, `dws edu-group`, `dws edu-app`, `dws edu-familygroup`, and `dws college-contact` from the CLI, Schema, bundled Skills, and open-edition MCP endpoint registry. Future DWS packages no longer expose these five command surfaces.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **report entry submit requires recipients** — `dws report entry submit`(及废弃别名 `dws report create`)的 `--to-user-ids` 从可选提升为必填:无接收人的日志提交在服务端仍返回成功,但日志对任何接收人都不可见。openAPI `create_report` 的 `toUserIds` 参数保持可选不动,规则仅在 dws CLI 侧收紧——Cobra required 拦截未传场景,RunE 内对空值/纯分隔符(如 `--to-user-ids ","`)同样 fail-closed 拒绝。修复 [#85724185](https://project.aone.alibaba-inc.com/v2/project/2170318/bug/85724185)。
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: Fixed
|
||||
---
|
||||
|
||||
- **Reviewer Router merge recovery** — retries exact App-owned merge intents through a SHA-bound synchronous merge after GitHub has enforced approval and nine GitHub Actions source-bound required checks.
|
||||
@@ -4,13 +4,3 @@ paths:
|
||||
# GitHub Actions added concurrency.queue in 2026. actionlint v1.7.12's
|
||||
# bundled workflow schema has not caught up with the platform syntax.
|
||||
- 'unexpected key "queue" for "concurrency" section'
|
||||
.github/workflows/coverage-baseline-promotion.yml:
|
||||
ignore:
|
||||
# Serialize every acknowledgement for one Formula target without
|
||||
# allowing Actions' default single-pending replacement to orphan a run.
|
||||
- 'unexpected key "queue" for "concurrency" section'
|
||||
.github/workflows/coverage-baseline-repair.yml:
|
||||
ignore:
|
||||
# Keep the closed-event dispatcher and its exact-SHA producer queued for
|
||||
# the same target instead of replacing either half of the repair chain.
|
||||
- 'unexpected key "queue" for "concurrency" section'
|
||||
|
||||
+70
-827
File diff suppressed because it is too large
Load Diff
@@ -1,322 +0,0 @@
|
||||
name: Coverage Baseline Promotion
|
||||
|
||||
run-name: Promote coverage baseline for ${{ github.event.client_payload.target_sha }}
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [coverage-baseline-promote]
|
||||
|
||||
# repository_dispatch loads this workflow from the protected default branch.
|
||||
# The requested target is treated as untrusted input until the validation step
|
||||
# proves it is an exact Formula-only successor already contained in main.
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: coverage-baseline-promotion-${{ github.event.client_payload.target_sha }}
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
if: github.repository == 'DingTalk-Real-AI/dingtalk-workspace-cli'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Validate Formula-only main target
|
||||
id: validate-target
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const targetSha = context.payload.client_payload?.target_sha;
|
||||
const sourceRunId = context.payload.client_payload?.source_run_id;
|
||||
const checkRunId = Number(context.payload.client_payload?.check_run_id);
|
||||
if (!/^[0-9a-f]{40}$/.test(targetSha || '')) {
|
||||
throw new Error('coverage-baseline-promote requires one full target_sha');
|
||||
}
|
||||
if (!/^[1-9][0-9]*$/.test(sourceRunId || '')) {
|
||||
throw new Error('coverage-baseline-promote requires one source_run_id');
|
||||
}
|
||||
if (!Number.isSafeInteger(checkRunId) || checkRunId <= 0) {
|
||||
throw new Error('coverage-baseline-promote requires one safe check_run_id');
|
||||
}
|
||||
|
||||
// Bind the finalizer before any target or cache validation. A
|
||||
// later failure must complete the release-created acknowledgement
|
||||
// instead of leaving Release to poll a permanently queued check.
|
||||
const promotionExternalId = `release-${sourceRunId}-${targetSha}`;
|
||||
const {data: promotionCheck} = await github.rest.checks.get({
|
||||
owner,
|
||||
repo,
|
||||
check_run_id: checkRunId,
|
||||
});
|
||||
if (
|
||||
promotionCheck.id !== checkRunId ||
|
||||
promotionCheck.head_sha !== targetSha ||
|
||||
promotionCheck.name !== 'Coverage Baseline Cache' ||
|
||||
promotionCheck.external_id !== promotionExternalId ||
|
||||
promotionCheck.app?.slug !== 'github-actions' ||
|
||||
promotionCheck.status !== 'queued' ||
|
||||
promotionCheck.conclusion !== null
|
||||
) {
|
||||
throw new Error('coverage baseline acknowledgement has an invalid identity');
|
||||
}
|
||||
core.setOutput('target_sha', targetSha);
|
||||
core.setOutput('check_run_id', String(checkRunId));
|
||||
core.setOutput('check_external_id', promotionExternalId);
|
||||
|
||||
const {data: targetCommit} = await github.rest.repos.getCommit({
|
||||
owner,
|
||||
repo,
|
||||
ref: targetSha,
|
||||
per_page: 100,
|
||||
});
|
||||
const files = targetCommit.files || [];
|
||||
const message = targetCommit.commit.message;
|
||||
const formulaPath = files[0]?.filename;
|
||||
const stableFormula =
|
||||
formulaPath === 'Formula/dingtalk-workspace-cli.rb' &&
|
||||
/^chore: update formula for v[0-9]+\.[0-9]+\.[0-9]+ \[skip ci\]$/.test(message);
|
||||
const betaFormula =
|
||||
formulaPath === 'Formula/dingtalk-workspace-cli-beta.rb' &&
|
||||
/^chore: update beta formula for v[0-9]+\.[0-9]+\.[0-9]+-beta\.[1-9][0-9]* \[skip ci\]$/.test(message);
|
||||
if (
|
||||
targetCommit.sha !== targetSha ||
|
||||
targetCommit.parents.length !== 1 ||
|
||||
targetCommit.author?.login !== 'github-actions[bot]' ||
|
||||
targetCommit.committer?.login !== 'github-actions[bot]' ||
|
||||
files.length !== 1 ||
|
||||
!['added', 'modified'].includes(files[0].status) ||
|
||||
(!stableFormula && !betaFormula)
|
||||
) {
|
||||
throw new Error(
|
||||
`${targetSha} is not an exact release-produced Formula-only commit`,
|
||||
);
|
||||
}
|
||||
|
||||
const parentSha = targetCommit.parents[0].sha;
|
||||
const requiredContexts = [
|
||||
'Lint',
|
||||
'Test',
|
||||
'Coverage',
|
||||
'Policy',
|
||||
'Edition',
|
||||
'Interface Integrity',
|
||||
'AI Behavior',
|
||||
'CLI Smoke',
|
||||
'Mock MCP',
|
||||
];
|
||||
async function requireSuccessfulAdmission(ref, label) {
|
||||
for (let attempt = 1; attempt <= 6; attempt += 1) {
|
||||
const runs = await github.paginate(github.rest.checks.listForRef, {
|
||||
owner,
|
||||
repo,
|
||||
ref,
|
||||
filter: 'latest',
|
||||
per_page: 100,
|
||||
});
|
||||
const latestByName = new Map();
|
||||
for (const run of runs) {
|
||||
if (
|
||||
run.head_sha !== ref ||
|
||||
run.app?.slug !== 'github-actions' ||
|
||||
!requiredContexts.includes(run.name)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const current = latestByName.get(run.name);
|
||||
if (!current || run.id > current.id) {
|
||||
latestByName.set(run.name, run);
|
||||
}
|
||||
}
|
||||
const invalid = requiredContexts.filter((name) => {
|
||||
const run = latestByName.get(name);
|
||||
return !run || run.conclusion !== 'success';
|
||||
});
|
||||
if (invalid.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (attempt < 6) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
continue;
|
||||
}
|
||||
throw new Error(
|
||||
`${label} ${ref} lacks successful Code Admission contexts: ${invalid.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await requireSuccessfulAdmission(parentSha, 'Formula parent');
|
||||
await requireSuccessfulAdmission(targetSha, 'Formula target');
|
||||
|
||||
const {data: branch} = await github.rest.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
branch: context.payload.repository.default_branch,
|
||||
});
|
||||
const {data: containment} =
|
||||
await github.rest.repos.compareCommitsWithBasehead({
|
||||
owner,
|
||||
repo,
|
||||
basehead: `${targetSha}...${branch.commit.sha}`,
|
||||
});
|
||||
if (!['ahead', 'identical'].includes(containment.status)) {
|
||||
throw new Error(`${targetSha} is not contained in the protected default branch`);
|
||||
}
|
||||
|
||||
core.setOutput('parent_sha', parentSha);
|
||||
core.setOutput('formula_path', formulaPath);
|
||||
|
||||
- name: Mark Formula cache promotion in progress
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.checks.update({
|
||||
...context.repo,
|
||||
check_run_id: Number('${{ steps.validate-target.outputs.check_run_id }}'),
|
||||
status: 'in_progress',
|
||||
started_at: new Date().toISOString(),
|
||||
output: {
|
||||
title: 'Producing exact-SHA coverage baseline',
|
||||
summary: 'The trusted default-branch workflow is validating or producing the main-scoped cache.',
|
||||
},
|
||||
});
|
||||
|
||||
- name: Check out validated Formula-only target
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
ref: ${{ steps.validate-target.outputs.target_sha }}
|
||||
|
||||
- name: Verify checked-out Formula-only identity
|
||||
shell: bash
|
||||
env:
|
||||
TARGET_SHA: ${{ steps.validate-target.outputs.target_sha }}
|
||||
PARENT_SHA: ${{ steps.validate-target.outputs.parent_sha }}
|
||||
FORMULA_PATH: ${{ steps.validate-target.outputs.formula_path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test "$(git rev-parse HEAD)" = "$TARGET_SHA"
|
||||
test "$(git rev-parse HEAD^)" = "$PARENT_SHA"
|
||||
test "$(git diff --name-only --no-renames "$PARENT_SHA" "$TARGET_SHA")" = "$FORMULA_PATH"
|
||||
|
||||
- name: Set up Go
|
||||
id: setup-go
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Restore existing target coverage profile
|
||||
id: target-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: coverage-cache.txt
|
||||
key: dws-coverage-full-v2-${{ steps.validate-target.outputs.target_sha }}-go${{ steps.setup-go.outputs.go-version }}
|
||||
|
||||
- name: Validate existing target coverage profile
|
||||
if: steps.target-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
set -eu
|
||||
test -s coverage-cache.txt
|
||||
test "$(head -n 1 coverage-cache.txt)" = "mode: atomic"
|
||||
|
||||
- name: Restore exact Formula parent coverage profile
|
||||
id: parent-cache
|
||||
if: steps.target-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: coverage-cache.txt
|
||||
key: dws-coverage-full-v2-${{ steps.validate-target.outputs.parent_sha }}-go${{ steps.setup-go.outputs.go-version }}
|
||||
|
||||
- name: Validate promoted Formula parent profile
|
||||
if: steps.target-cache.outputs.cache-hit != 'true' && steps.parent-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
set -eu
|
||||
test -s coverage-cache.txt
|
||||
test "$(head -n 1 coverage-cache.txt)" = "mode: atomic"
|
||||
|
||||
- name: Install archive tooling for cold Formula baseline
|
||||
if: steps.target-cache.outputs.cache-hit != 'true' && steps.parent-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
if command -v zip >/dev/null && command -v unzip >/dev/null; then
|
||||
echo "zip and unzip are already available"
|
||||
else
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y zip unzip
|
||||
fi
|
||||
|
||||
- name: Recompute cold Formula baseline
|
||||
if: steps.target-cache.outputs.cache-hit != 'true' && steps.parent-cache.outputs.cache-hit != 'true'
|
||||
env:
|
||||
DWS_PACKAGE_VERSION: 0.0.0-test
|
||||
run: |
|
||||
set -euo pipefail
|
||||
go test -count=1 -p 1 \
|
||||
-coverprofile=coverage-cache.txt \
|
||||
-covermode=atomic \
|
||||
./ ./cmd/... ./internal/... ./skills/...
|
||||
test -s coverage-cache.txt
|
||||
test "$(head -n 1 coverage-cache.txt)" = "mode: atomic"
|
||||
|
||||
- name: Save Formula main SHA coverage profile
|
||||
if: steps.target-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: coverage-cache.txt
|
||||
key: dws-coverage-full-v2-${{ steps.validate-target.outputs.target_sha }}-go${{ steps.setup-go.outputs.go-version }}
|
||||
|
||||
- name: Verify Formula main SHA coverage cache exists
|
||||
id: formula-target-cache-verification
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: coverage-cache.txt
|
||||
key: dws-coverage-full-v2-${{ steps.validate-target.outputs.target_sha }}-go${{ steps.setup-go.outputs.go-version }}
|
||||
lookup-only: true
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Require exact Formula main SHA coverage cache
|
||||
env:
|
||||
EXACT_CACHE_HIT: ${{ steps.formula-target-cache-verification.outputs.cache-hit }}
|
||||
run: test "$EXACT_CACHE_HIT" = true
|
||||
|
||||
- name: Complete Formula cache promotion acknowledgement
|
||||
if: ${{ always() && steps.validate-target.outputs.check_run_id != '' }}
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
env:
|
||||
PROMOTION_JOB_STATUS: ${{ job.status }}
|
||||
with:
|
||||
script: |
|
||||
const checkRunId = Number('${{ steps.validate-target.outputs.check_run_id }}');
|
||||
const targetSha = '${{ steps.validate-target.outputs.target_sha }}';
|
||||
const expectedExternalId = '${{ steps.validate-target.outputs.check_external_id }}';
|
||||
const {data: currentCheck} = await github.rest.checks.get({
|
||||
...context.repo,
|
||||
check_run_id: checkRunId,
|
||||
});
|
||||
if (
|
||||
currentCheck.head_sha !== targetSha ||
|
||||
currentCheck.name !== 'Coverage Baseline Cache' ||
|
||||
currentCheck.external_id !== expectedExternalId ||
|
||||
currentCheck.app?.slug !== 'github-actions'
|
||||
) {
|
||||
throw new Error('refusing to update a changed promotion acknowledgement');
|
||||
}
|
||||
const succeeded = process.env.PROMOTION_JOB_STATUS === 'success';
|
||||
await github.rest.checks.update({
|
||||
...context.repo,
|
||||
check_run_id: checkRunId,
|
||||
status: 'completed',
|
||||
conclusion: succeeded ? 'success' : 'failure',
|
||||
completed_at: new Date().toISOString(),
|
||||
output: {
|
||||
title: succeeded
|
||||
? 'Exact-SHA coverage baseline is available'
|
||||
: 'Exact-SHA coverage baseline promotion failed',
|
||||
summary: succeeded
|
||||
? `Verified the main-scoped exact cache for ${targetSha}.`
|
||||
: `Promotion failed for ${targetSha}; rerun the failed Release job after correcting the producer.`,
|
||||
},
|
||||
});
|
||||
@@ -1,560 +0,0 @@
|
||||
name: Coverage Baseline Repair
|
||||
|
||||
run-name: Repair coverage baseline from ${{ github.event_name }}
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [main]
|
||||
types: [closed]
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
repository_dispatch:
|
||||
types: [coverage-baseline-repair]
|
||||
schedule:
|
||||
- cron: "23 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
# pull_request_target and workflow_run are allowed to inspect only GitHub API
|
||||
# data and dispatch the trusted producer. GitHub deliberately makes both
|
||||
# triggers read-only for the default-branch cache, so all checkout and cache
|
||||
# writes live in repository_dispatch, schedule, or main-only workflow_dispatch.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: coverage-baseline-repair-${{ github.event_name == 'pull_request_target' && github.event.pull_request.merge_commit_sha || github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event_name == 'repository_dispatch' && github.event.client_payload.merge_commit_sha || github.sha }}
|
||||
cancel-in-progress: false
|
||||
# Retain every pending repair for one target. actionlint v1.7.12's bundled
|
||||
# schema predates GitHub's concurrency.queue support.
|
||||
queue: max
|
||||
|
||||
jobs:
|
||||
dispatch-merged-pr:
|
||||
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.merged == true && github.repository == 'DingTalk-Real-AI/dingtalk-workspace-cli' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
# Never check out or execute pull-request content in this privileged
|
||||
# base-owned event. Re-read the merged PR, bind every immutable identity,
|
||||
# prove the result is in main, and send only those values to the producer.
|
||||
- name: Dispatch trusted merged-PR repair
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const eventPull = context.payload.pull_request;
|
||||
const fullCommit = /^[0-9a-f]{40}$/;
|
||||
const pullNumber = Number(eventPull?.number);
|
||||
const headSha = eventPull?.head?.sha;
|
||||
const baseRef = eventPull?.base?.ref;
|
||||
const mergeCommitSha = eventPull?.merge_commit_sha;
|
||||
if (
|
||||
context.payload.repository?.full_name !==
|
||||
'DingTalk-Real-AI/dingtalk-workspace-cli' ||
|
||||
context.payload.repository?.default_branch !== 'main' ||
|
||||
!Number.isSafeInteger(pullNumber) ||
|
||||
pullNumber <= 0 ||
|
||||
!fullCommit.test(headSha || '') ||
|
||||
baseRef !== 'main' ||
|
||||
!fullCommit.test(mergeCommitSha || '')
|
||||
) {
|
||||
throw new Error('closed PR event has an invalid repository or revision identity');
|
||||
}
|
||||
|
||||
// REST base.sha follows the live base branch and can move after
|
||||
// merge. Bind the closed event's stable PR head snapshot and merge
|
||||
// facts, then authorize the target through main containment.
|
||||
function isStableMergedPRIdentity(
|
||||
currentPull,
|
||||
pullNumber,
|
||||
headSha,
|
||||
mergeCommitSha,
|
||||
) {
|
||||
return (
|
||||
currentPull?.number === pullNumber &&
|
||||
currentPull.state === 'closed' &&
|
||||
currentPull.merged === true &&
|
||||
typeof currentPull.merged_at === 'string' &&
|
||||
currentPull.merged_at.length > 0 &&
|
||||
currentPull.base?.ref === 'main' &&
|
||||
currentPull.head?.sha === headSha &&
|
||||
currentPull.merge_commit_sha === mergeCommitSha
|
||||
);
|
||||
}
|
||||
|
||||
const {data: currentPull} = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
});
|
||||
if (!isStableMergedPRIdentity(
|
||||
currentPull,
|
||||
pullNumber,
|
||||
headSha,
|
||||
mergeCommitSha,
|
||||
)) {
|
||||
throw new Error(`PR #${pullNumber} no longer matches the merged-main event`);
|
||||
}
|
||||
|
||||
async function requireMainContainment(targetSha) {
|
||||
let lastState = 'not checked';
|
||||
for (let attempt = 1; attempt <= 6; attempt += 1) {
|
||||
try {
|
||||
const {data: branch} = await github.rest.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
branch: 'main',
|
||||
});
|
||||
const {data: comparison} =
|
||||
await github.rest.repos.compareCommitsWithBasehead({
|
||||
owner,
|
||||
repo,
|
||||
basehead: `${targetSha}...${branch.commit.sha}`,
|
||||
});
|
||||
lastState = comparison.status;
|
||||
if (['ahead', 'identical'].includes(comparison.status)) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
lastState = error.message;
|
||||
}
|
||||
if (attempt < 6) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`${targetSha} is not contained in protected main after retries: ${lastState}`,
|
||||
);
|
||||
}
|
||||
await requireMainContainment(mergeCommitSha);
|
||||
|
||||
// Normal App or human merges emit a protected-main push run whose
|
||||
// CI producer owns this exact key. Give Actions event delivery a
|
||||
// short visibility window and avoid a duplicate full-suite repair.
|
||||
// A workflow-skip directive or suppressed built-in-token event has
|
||||
// no such run, so only that missing-event path reaches dispatch.
|
||||
const {data: ciWorkflow} = await github.rest.actions.getWorkflow({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: '.github/workflows/ci.yml',
|
||||
});
|
||||
if (
|
||||
ciWorkflow.name !== 'CI' ||
|
||||
ciWorkflow.path !== '.github/workflows/ci.yml' ||
|
||||
ciWorkflow.state !== 'active'
|
||||
) {
|
||||
throw new Error('protected CI workflow identity is not active or exact');
|
||||
}
|
||||
for (let attempt = 1; attempt <= 12; attempt += 1) {
|
||||
const {data: workflowRuns} =
|
||||
await github.rest.actions.listWorkflowRunsForRepo({
|
||||
owner,
|
||||
repo,
|
||||
branch: 'main',
|
||||
event: 'push',
|
||||
per_page: 100,
|
||||
});
|
||||
const exactPushRun = workflowRuns.workflow_runs.find(run =>
|
||||
run.name === 'CI' &&
|
||||
run.workflow_id === ciWorkflow.id &&
|
||||
run.path === ciWorkflow.path &&
|
||||
run.event === 'push' &&
|
||||
run.head_sha === mergeCommitSha &&
|
||||
run.head_branch === 'main' &&
|
||||
['queued', 'in_progress', 'completed'].includes(run.status),
|
||||
);
|
||||
if (exactPushRun) {
|
||||
core.info(
|
||||
`CI push run ${exactPushRun.id} already owns the exact-SHA producer for ${mergeCommitSha}; repair dispatch is unnecessary.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (attempt < 12) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
|
||||
// repository_dispatch is one of GitHub's explicit GITHUB_TOKEN
|
||||
// recursion exceptions and receives default-branch cache-write scope.
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner,
|
||||
repo,
|
||||
event_type: 'coverage-baseline-repair',
|
||||
client_payload: {
|
||||
source: 'merged_pr',
|
||||
pull_number: String(pullNumber),
|
||||
head_sha: headSha,
|
||||
merge_commit_sha: mergeCommitSha,
|
||||
source_run_id: String(context.runId),
|
||||
},
|
||||
});
|
||||
core.info(
|
||||
`Dispatched exact-SHA coverage repair for merged PR #${pullNumber} at ${mergeCommitSha}.`,
|
||||
);
|
||||
|
||||
dispatch-failed-ci:
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.repository == 'DingTalk-Real-AI/dingtalk-workspace-cli' &&
|
||||
github.event.workflow_run.name == 'CI' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch == 'main' &&
|
||||
github.event.workflow_run.status == 'completed' &&
|
||||
github.event.workflow_run.conclusion != 'success'
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
steps:
|
||||
# workflow_run cannot write the default-branch cache. Re-read the exact
|
||||
# completed CI run from Actions, bind it to the protected CI workflow and
|
||||
# main revision, then use the repository_dispatch recursion exception.
|
||||
- name: Dispatch trusted failed-CI repair
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const upstream = 'DingTalk-Real-AI/dingtalk-workspace-cli';
|
||||
const eventRun = context.payload.workflow_run;
|
||||
const fullCommit = /^[0-9a-f]{40}$/;
|
||||
const runID = Number(eventRun?.id);
|
||||
const runAttempt = Number(eventRun?.run_attempt);
|
||||
const headSha = eventRun?.head_sha;
|
||||
const conclusion = eventRun?.conclusion;
|
||||
if (
|
||||
context.payload.repository?.full_name !== upstream ||
|
||||
context.payload.repository?.default_branch !== 'main' ||
|
||||
!Number.isSafeInteger(runID) ||
|
||||
runID <= 0 ||
|
||||
!Number.isSafeInteger(runAttempt) ||
|
||||
runAttempt <= 0 ||
|
||||
eventRun?.name !== 'CI' ||
|
||||
eventRun?.event !== 'push' ||
|
||||
eventRun?.head_branch !== 'main' ||
|
||||
eventRun?.status !== 'completed' ||
|
||||
typeof conclusion !== 'string' ||
|
||||
conclusion.length === 0 ||
|
||||
conclusion === 'success' ||
|
||||
!fullCommit.test(headSha || '')
|
||||
) {
|
||||
throw new Error('workflow_run event is not one completed non-success main CI push');
|
||||
}
|
||||
|
||||
const {data: ciWorkflow} = await github.rest.actions.getWorkflow({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: '.github/workflows/ci.yml',
|
||||
});
|
||||
const {data: currentRun} = await github.rest.actions.getWorkflowRun({
|
||||
owner,
|
||||
repo,
|
||||
run_id: runID,
|
||||
});
|
||||
if (
|
||||
ciWorkflow.name !== 'CI' ||
|
||||
ciWorkflow.path !== '.github/workflows/ci.yml' ||
|
||||
eventRun.workflow_id !== ciWorkflow.id ||
|
||||
currentRun.id !== runID ||
|
||||
currentRun.workflow_id !== ciWorkflow.id ||
|
||||
currentRun.name !== 'CI' ||
|
||||
currentRun.event !== 'push' ||
|
||||
currentRun.head_branch !== 'main' ||
|
||||
currentRun.head_sha !== headSha ||
|
||||
currentRun.run_attempt !== runAttempt ||
|
||||
currentRun.status !== 'completed' ||
|
||||
currentRun.conclusion !== conclusion ||
|
||||
currentRun.conclusion === 'success' ||
|
||||
currentRun.repository?.full_name !== upstream ||
|
||||
currentRun.head_repository?.full_name !== upstream
|
||||
) {
|
||||
throw new Error(`CI workflow run ${runID} no longer matches the completed event`);
|
||||
}
|
||||
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner,
|
||||
repo,
|
||||
event_type: 'coverage-baseline-repair',
|
||||
client_payload: {
|
||||
source: 'failed_ci',
|
||||
workflow_run_id: String(runID),
|
||||
workflow_run_attempt: String(runAttempt),
|
||||
workflow_conclusion: conclusion,
|
||||
merge_commit_sha: headSha,
|
||||
source_run_id: String(context.runId),
|
||||
},
|
||||
});
|
||||
core.info(
|
||||
`Dispatched exact-SHA coverage repair for ${conclusion} CI run ${runID} at ${headSha}.`,
|
||||
);
|
||||
|
||||
repair:
|
||||
if: ${{ github.event_name == 'repository_dispatch' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 35
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Resolve trusted main repair target
|
||||
id: resolve-target
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const fullCommit = /^[0-9a-f]{40}$/;
|
||||
if (
|
||||
context.payload.repository?.full_name !==
|
||||
'DingTalk-Real-AI/dingtalk-workspace-cli' ||
|
||||
context.payload.repository?.default_branch !== 'main'
|
||||
) {
|
||||
throw new Error('coverage repair is restricted to the protected upstream repository');
|
||||
}
|
||||
|
||||
async function requireMainContainment(targetSha) {
|
||||
let lastState = 'not checked';
|
||||
for (let attempt = 1; attempt <= 6; attempt += 1) {
|
||||
try {
|
||||
const {data: branch} = await github.rest.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
branch: 'main',
|
||||
});
|
||||
const {data: comparison} =
|
||||
await github.rest.repos.compareCommitsWithBasehead({
|
||||
owner,
|
||||
repo,
|
||||
basehead: `${targetSha}...${branch.commit.sha}`,
|
||||
});
|
||||
lastState = comparison.status;
|
||||
if (['ahead', 'identical'].includes(comparison.status)) {
|
||||
return branch.commit.sha;
|
||||
}
|
||||
} catch (error) {
|
||||
lastState = error.message;
|
||||
}
|
||||
if (attempt < 6) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`${targetSha} is not contained in protected main after retries: ${lastState}`,
|
||||
);
|
||||
}
|
||||
|
||||
// The dispatcher froze the stable PR head snapshot in this payload.
|
||||
// Do not re-read mutable base.sha; bind the head and stable merge
|
||||
// facts, then prove protected-main containment below.
|
||||
function isStableMergedPRIdentity(
|
||||
currentPull,
|
||||
pullNumber,
|
||||
headSha,
|
||||
mergeCommitSha,
|
||||
) {
|
||||
return (
|
||||
currentPull?.number === pullNumber &&
|
||||
currentPull.state === 'closed' &&
|
||||
currentPull.merged === true &&
|
||||
typeof currentPull.merged_at === 'string' &&
|
||||
currentPull.merged_at.length > 0 &&
|
||||
currentPull.base?.ref === 'main' &&
|
||||
currentPull.head?.sha === headSha &&
|
||||
currentPull.merge_commit_sha === mergeCommitSha
|
||||
);
|
||||
}
|
||||
|
||||
let targetSha;
|
||||
if (context.eventName === 'repository_dispatch') {
|
||||
const payload = context.payload.client_payload || {};
|
||||
const sourceRunIDText = String(payload.source_run_id || '');
|
||||
if (!/^[1-9][0-9]*$/.test(sourceRunIDText)) {
|
||||
throw new Error('coverage-baseline-repair payload has an invalid source run');
|
||||
}
|
||||
if (payload.source === 'merged_pr') {
|
||||
const rawPullNumber = String(payload.pull_number || '');
|
||||
const pullNumber = Number(rawPullNumber);
|
||||
const headSha = payload.head_sha;
|
||||
targetSha = payload.merge_commit_sha;
|
||||
if (
|
||||
!/^[1-9][0-9]*$/.test(rawPullNumber) ||
|
||||
!Number.isSafeInteger(pullNumber) ||
|
||||
!fullCommit.test(headSha || '') ||
|
||||
!fullCommit.test(targetSha || '')
|
||||
) {
|
||||
throw new Error('coverage-baseline-repair payload has an invalid PR identity');
|
||||
}
|
||||
const {data: currentPull} = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
});
|
||||
if (!isStableMergedPRIdentity(
|
||||
currentPull,
|
||||
pullNumber,
|
||||
headSha,
|
||||
targetSha,
|
||||
)) {
|
||||
throw new Error(
|
||||
`repair payload no longer matches merged PR #${pullNumber}`,
|
||||
);
|
||||
}
|
||||
} else if (payload.source === 'failed_ci') {
|
||||
const rawWorkflowRunID = String(payload.workflow_run_id || '');
|
||||
const workflowRunID = Number(rawWorkflowRunID);
|
||||
const rawWorkflowRunAttempt = String(payload.workflow_run_attempt || '');
|
||||
const workflowRunAttempt = Number(rawWorkflowRunAttempt);
|
||||
const workflowConclusion = payload.workflow_conclusion;
|
||||
targetSha = payload.merge_commit_sha;
|
||||
if (
|
||||
!/^[1-9][0-9]*$/.test(rawWorkflowRunID) ||
|
||||
!Number.isSafeInteger(workflowRunID) ||
|
||||
!/^[1-9][0-9]*$/.test(rawWorkflowRunAttempt) ||
|
||||
!Number.isSafeInteger(workflowRunAttempt) ||
|
||||
typeof workflowConclusion !== 'string' ||
|
||||
workflowConclusion.length === 0 ||
|
||||
workflowConclusion === 'success' ||
|
||||
!fullCommit.test(targetSha || '')
|
||||
) {
|
||||
throw new Error('coverage-baseline-repair payload has an invalid CI identity');
|
||||
}
|
||||
const {data: ciWorkflow} = await github.rest.actions.getWorkflow({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: '.github/workflows/ci.yml',
|
||||
});
|
||||
const {data: currentRun} = await github.rest.actions.getWorkflowRun({
|
||||
owner,
|
||||
repo,
|
||||
run_id: workflowRunID,
|
||||
});
|
||||
if (
|
||||
ciWorkflow.name !== 'CI' ||
|
||||
ciWorkflow.path !== '.github/workflows/ci.yml' ||
|
||||
currentRun.id !== workflowRunID ||
|
||||
currentRun.workflow_id !== ciWorkflow.id ||
|
||||
currentRun.name !== 'CI' ||
|
||||
currentRun.event !== 'push' ||
|
||||
currentRun.head_branch !== 'main' ||
|
||||
currentRun.head_sha !== targetSha ||
|
||||
currentRun.run_attempt !== workflowRunAttempt ||
|
||||
currentRun.status !== 'completed' ||
|
||||
currentRun.conclusion !== workflowConclusion ||
|
||||
currentRun.conclusion === 'success' ||
|
||||
currentRun.repository?.full_name !==
|
||||
'DingTalk-Real-AI/dingtalk-workspace-cli' ||
|
||||
currentRun.head_repository?.full_name !==
|
||||
'DingTalk-Real-AI/dingtalk-workspace-cli'
|
||||
) {
|
||||
throw new Error(
|
||||
`repair payload no longer matches failed CI run ${workflowRunID}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error('coverage-baseline-repair payload has an unknown source');
|
||||
}
|
||||
await requireMainContainment(targetSha);
|
||||
} else {
|
||||
if (context.ref !== 'refs/heads/main') {
|
||||
throw new Error('scheduled and manual repair must run from refs/heads/main');
|
||||
}
|
||||
// github.sha is the default-branch tip that keyed this workflow's
|
||||
// concurrency group. Keep the producer bound to that exact
|
||||
// event-time target even if main advances while this run queues.
|
||||
targetSha = context.sha;
|
||||
if (!fullCommit.test(targetSha || '')) {
|
||||
throw new Error('protected main did not resolve to one full commit SHA');
|
||||
}
|
||||
await requireMainContainment(targetSha);
|
||||
}
|
||||
core.setOutput('target_sha', targetSha);
|
||||
core.info(`Resolved protected-main coverage repair target ${targetSha}.`);
|
||||
|
||||
- name: Check out exact protected-main target
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
ref: ${{ steps.resolve-target.outputs.target_sha }}
|
||||
|
||||
- name: Verify checked-out repair target
|
||||
env:
|
||||
TARGET_SHA: ${{ steps.resolve-target.outputs.target_sha }}
|
||||
run: test "$(git rev-parse HEAD)" = "$TARGET_SHA"
|
||||
|
||||
- name: Set up Go
|
||||
id: setup-go
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Restore exact target coverage profile
|
||||
id: target-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: coverage-cache.txt
|
||||
key: dws-coverage-full-v2-${{ steps.resolve-target.outputs.target_sha }}-go${{ steps.setup-go.outputs.go-version }}
|
||||
|
||||
- name: Validate existing exact target profile
|
||||
if: steps.target-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
set -eu
|
||||
test -s coverage-cache.txt
|
||||
test "$(head -n 1 coverage-cache.txt)" = "mode: atomic"
|
||||
|
||||
- name: Install archive tooling for cold repair
|
||||
if: steps.target-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
if command -v zip >/dev/null && command -v unzip >/dev/null; then
|
||||
echo "zip and unzip are already available"
|
||||
else
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y zip unzip
|
||||
fi
|
||||
|
||||
- name: Recompute complete target coverage profile
|
||||
if: steps.target-cache.outputs.cache-hit != 'true'
|
||||
env:
|
||||
DWS_PACKAGE_VERSION: 0.0.0-test
|
||||
run: |
|
||||
set -euo pipefail
|
||||
go test -count=1 -p 1 \
|
||||
-coverprofile=coverage-cache.txt \
|
||||
-covermode=atomic \
|
||||
./ ./cmd/... ./internal/... ./skills/...
|
||||
test -s coverage-cache.txt
|
||||
test "$(head -n 1 coverage-cache.txt)" = "mode: atomic"
|
||||
|
||||
- name: Save exact protected-main coverage profile
|
||||
if: steps.target-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: coverage-cache.txt
|
||||
key: dws-coverage-full-v2-${{ steps.resolve-target.outputs.target_sha }}-go${{ steps.setup-go.outputs.go-version }}
|
||||
|
||||
# Cache uploads are fail-open warnings. A lookup-only restore plus the
|
||||
# explicit cache-hit assertion makes an absent or partial key fail hard.
|
||||
- name: Verify exact protected-main coverage cache exists
|
||||
id: target-cache-verification
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: coverage-cache.txt
|
||||
key: dws-coverage-full-v2-${{ steps.resolve-target.outputs.target_sha }}-go${{ steps.setup-go.outputs.go-version }}
|
||||
lookup-only: true
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Require exact protected-main coverage cache
|
||||
env:
|
||||
EXACT_CACHE_HIT: ${{ steps.target-cache-verification.outputs.cache-hit }}
|
||||
run: test "$EXACT_CACHE_HIT" = true
|
||||
@@ -14,12 +14,9 @@ jobs:
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const webhooks = [
|
||||
process.env.DINGTALK_WEBHOOK,
|
||||
process.env.DINGTALK_WEBHOOK_SECONDARY
|
||||
].filter(Boolean);
|
||||
if (webhooks.length === 0) {
|
||||
console.log('⚠️ No DingTalk webhook configured, skipping notification');
|
||||
const webhook = process.env.DINGTALK_WEBHOOK;
|
||||
if (!webhook) {
|
||||
console.log('⚠️ DINGTALK_WEBHOOK not set, skipping notification');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,15 +39,12 @@ jobs:
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all(webhooks.map(webhook =>
|
||||
fetch(webhook, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(message)
|
||||
})
|
||||
));
|
||||
await fetch(webhook, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(message)
|
||||
});
|
||||
|
||||
console.log(`✅ DingTalk notification sent to ${webhooks.length} webhook(s)`);
|
||||
console.log('✅ DingTalk notification sent');
|
||||
env:
|
||||
DINGTALK_WEBHOOK: ${{ secrets.DINGTALK_WEBHOOK }}
|
||||
DINGTALK_WEBHOOK_SECONDARY: ${{ secrets.DINGTALK_WEBHOOK_SECONDARY }}
|
||||
|
||||
@@ -1112,9 +1112,6 @@ jobs:
|
||||
needs: [release-contract, release-validation, release, verify-darwin-signatures]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
coverage_baseline_required: ${{ steps.seal-formula.outputs.coverage_baseline_required }}
|
||||
coverage_baseline_commit: ${{ steps.seal-formula.outputs.coverage_baseline_commit }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
@@ -1498,7 +1495,6 @@ jobs:
|
||||
DWS_GIT_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
|
||||
|
||||
- name: Seal Formula-only Code Admission contexts
|
||||
id: seal-formula
|
||||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' }}
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
@@ -1519,8 +1515,6 @@ jobs:
|
||||
const sourcePath = channel === "stable"
|
||||
? "dist/homebrew/dingtalk-workspace-cli.rb"
|
||||
: "dist/homebrew/dingtalk-workspace-cli-beta.rb";
|
||||
core.setOutput("coverage_baseline_required", "false");
|
||||
core.setOutput("coverage_baseline_commit", "");
|
||||
const expectedMessage = channel === "stable"
|
||||
? `chore: update formula for ${version} [skip ci]`
|
||||
: `chore: update beta formula for ${version} [skip ci]`;
|
||||
@@ -1656,11 +1650,6 @@ jobs:
|
||||
},
|
||||
});
|
||||
}
|
||||
core.setOutput("coverage_baseline_required", "true");
|
||||
core.setOutput("coverage_baseline_commit", commit);
|
||||
core.info(
|
||||
`Formula-only Code Admission is sealed for ${commit}; the independent confirmation job will dispatch its exact-SHA cache producer.`,
|
||||
);
|
||||
|
||||
- name: Reverify exact immutable npm package
|
||||
run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$RELEASE_VERSION"
|
||||
@@ -2188,117 +2177,6 @@ jobs:
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
coverage-baseline-confirmation:
|
||||
name: Confirm Formula coverage baseline
|
||||
# Once Formula sealing has exposed a target SHA, later publication
|
||||
# verification failures must not orphan its exact-main cache producer.
|
||||
if: ${{ !cancelled() && (needs.publish-release.result == 'success' || needs.publish-release.outputs.coverage_baseline_required == 'true') }}
|
||||
needs: publish-release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 35
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Require exact Formula cache acknowledgement
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
env:
|
||||
BASELINE_REQUIRED: ${{ needs.publish-release.outputs.coverage_baseline_required }}
|
||||
FORMULA_COMMIT: ${{ needs.publish-release.outputs.coverage_baseline_commit }}
|
||||
with:
|
||||
script: |
|
||||
const rawRequired = process.env.BASELINE_REQUIRED;
|
||||
if (!['true', 'false'].includes(rawRequired)) {
|
||||
throw new Error(`Formula baseline requirement is invalid: ${rawRequired || 'empty'}`);
|
||||
}
|
||||
const required = rawRequired === 'true';
|
||||
const targetSha = process.env.FORMULA_COMMIT;
|
||||
if (!required) {
|
||||
if (targetSha) {
|
||||
throw new Error('Formula baseline outputs are inconsistent for a no-op publication');
|
||||
}
|
||||
core.info('Formula was already current; no new exact-SHA cache acknowledgement is required.');
|
||||
return;
|
||||
}
|
||||
if (!/^[0-9a-f]{40}$/.test(targetSha)) {
|
||||
throw new Error('Formula baseline target output is malformed');
|
||||
}
|
||||
const expectedExternalId = `release-${context.runId}-${targetSha}`;
|
||||
let promotionCheck;
|
||||
try {
|
||||
const created = await github.rest.checks.create({
|
||||
...context.repo,
|
||||
name: 'Coverage Baseline Cache',
|
||||
head_sha: targetSha,
|
||||
status: 'queued',
|
||||
external_id: expectedExternalId,
|
||||
output: {
|
||||
title: 'Waiting for exact-SHA baseline promotion',
|
||||
summary:
|
||||
'The independent release governance job is waiting for the default-branch cache producer.',
|
||||
},
|
||||
});
|
||||
promotionCheck = created.data;
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
...context.repo,
|
||||
event_type: 'coverage-baseline-promote',
|
||||
client_payload: {
|
||||
target_sha: targetSha,
|
||||
source_run_id: String(context.runId),
|
||||
check_run_id: String(promotionCheck.id),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (promotionCheck) {
|
||||
try {
|
||||
await github.rest.checks.update({
|
||||
...context.repo,
|
||||
check_run_id: promotionCheck.id,
|
||||
status: 'completed',
|
||||
conclusion: 'failure',
|
||||
completed_at: new Date().toISOString(),
|
||||
output: {
|
||||
title: 'Coverage baseline dispatch failed',
|
||||
summary: `Release could not dispatch the exact-SHA producer: ${error.message}`,
|
||||
},
|
||||
});
|
||||
} catch (cleanupError) {
|
||||
core.error(
|
||||
`Could not close failed cache acknowledgement ${promotionCheck.id}: ${cleanupError.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const checkRunId = promotionCheck.id;
|
||||
for (let attempt = 1; attempt <= 180; attempt += 1) {
|
||||
const {data: currentCheck} = await github.rest.checks.get({
|
||||
...context.repo,
|
||||
check_run_id: checkRunId,
|
||||
});
|
||||
if (
|
||||
currentCheck.head_sha !== targetSha ||
|
||||
currentCheck.name !== 'Coverage Baseline Cache' ||
|
||||
currentCheck.external_id !== expectedExternalId ||
|
||||
currentCheck.app?.slug !== 'github-actions'
|
||||
) {
|
||||
throw new Error('Formula baseline promotion acknowledgement changed identity');
|
||||
}
|
||||
if (currentCheck.status === 'completed') {
|
||||
if (currentCheck.conclusion !== 'success') {
|
||||
throw new Error(
|
||||
`Formula baseline promotion failed with ${currentCheck.conclusion || 'unknown'}`,
|
||||
);
|
||||
}
|
||||
core.info(`Formula baseline promotion completed for ${targetSha}.`);
|
||||
return;
|
||||
}
|
||||
if (attempt < 180) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
}
|
||||
}
|
||||
throw new Error(`Formula baseline promotion timed out for ${targetSha}`);
|
||||
|
||||
release-delivery-gate:
|
||||
name: Release delivery gate
|
||||
if: ${{ !cancelled() }}
|
||||
@@ -2311,7 +2189,6 @@ jobs:
|
||||
- verify-darwin-signatures
|
||||
- publish-release
|
||||
- publish-channels
|
||||
- coverage-baseline-confirmation
|
||||
- mirror-gitee-release
|
||||
- repair-npm
|
||||
- repair-channel
|
||||
@@ -2334,7 +2211,6 @@ jobs:
|
||||
DARWIN_SIGNATURE_RESULT: ${{ needs.verify-darwin-signatures.result }}
|
||||
PUBLISH_RELEASE_RESULT: ${{ needs.publish-release.result }}
|
||||
PUBLISH_CHANNELS_RESULT: ${{ needs.publish-channels.result }}
|
||||
COVERAGE_BASELINE_CONFIRMATION_RESULT: ${{ needs.coverage-baseline-confirmation.result }}
|
||||
MIRROR_GITEE_RESULT: ${{ needs.mirror-gitee-release.result }}
|
||||
REPAIR_NPM_RESULT: ${{ needs.repair-npm.result }}
|
||||
REPAIR_CHANNEL_RESULT: ${{ needs.repair-channel.result }}
|
||||
@@ -2358,7 +2234,6 @@ jobs:
|
||||
require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" success
|
||||
require_result publish-release "$PUBLISH_RELEASE_RESULT" success
|
||||
require_result publish-channels "$PUBLISH_CHANNELS_RESULT" success
|
||||
require_result coverage-baseline-confirmation "$COVERAGE_BASELINE_CONFIRMATION_RESULT" success
|
||||
if test "$GITEE_FALLBACK_ENABLED" = true; then
|
||||
require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" success
|
||||
else
|
||||
@@ -2398,7 +2273,6 @@ jobs:
|
||||
require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped
|
||||
require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped
|
||||
require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped
|
||||
require_result coverage-baseline-confirmation "$COVERAGE_BASELINE_CONFIRMATION_RESULT" skipped
|
||||
require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped
|
||||
require_result repair-npm "$REPAIR_NPM_RESULT" skipped
|
||||
require_result repair-channel "$REPAIR_CHANNEL_RESULT" skipped
|
||||
@@ -2420,7 +2294,6 @@ jobs:
|
||||
require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped
|
||||
require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped
|
||||
require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped
|
||||
require_result coverage-baseline-confirmation "$COVERAGE_BASELINE_CONFIRMATION_RESULT" skipped
|
||||
require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped
|
||||
require_result repair-npm "$REPAIR_NPM_RESULT" skipped
|
||||
require_result repair-channel "$REPAIR_CHANNEL_RESULT" skipped
|
||||
@@ -2436,7 +2309,6 @@ jobs:
|
||||
require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped
|
||||
require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped
|
||||
require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped
|
||||
require_result coverage-baseline-confirmation "$COVERAGE_BASELINE_CONFIRMATION_RESULT" skipped
|
||||
require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped
|
||||
require_result repair-channel "$REPAIR_CHANNEL_RESULT" skipped
|
||||
require_cloud_jobs_skipped
|
||||
@@ -2451,7 +2323,6 @@ jobs:
|
||||
require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped
|
||||
require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped
|
||||
require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped
|
||||
require_result coverage-baseline-confirmation "$COVERAGE_BASELINE_CONFIRMATION_RESULT" skipped
|
||||
require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped
|
||||
require_result repair-npm "$REPAIR_NPM_RESULT" skipped
|
||||
require_cloud_jobs_skipped
|
||||
@@ -2466,7 +2337,6 @@ jobs:
|
||||
require_result verify-darwin-signatures "$DARWIN_SIGNATURE_RESULT" skipped
|
||||
require_result publish-release "$PUBLISH_RELEASE_RESULT" skipped
|
||||
require_result publish-channels "$PUBLISH_CHANNELS_RESULT" skipped
|
||||
require_result coverage-baseline-confirmation "$COVERAGE_BASELINE_CONFIRMATION_RESULT" skipped
|
||||
require_result mirror-gitee-release "$MIRROR_GITEE_RESULT" skipped
|
||||
require_result repair-npm "$REPAIR_NPM_RESULT" skipped
|
||||
require_cloud_jobs_skipped
|
||||
@@ -2828,11 +2698,9 @@ jobs:
|
||||
;;
|
||||
compatibility)
|
||||
test -n "$PREVIOUS_STABLE"
|
||||
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/check-release-compatibility.sh" \
|
||||
--repo-root "$GITHUB_WORKSPACE" \
|
||||
./scripts/policy/check-command-compatibility.sh \
|
||||
--base-ref HEAD \
|
||||
--stable-ref "$PREVIOUS_STABLE" \
|
||||
--candidate-ref HEAD
|
||||
--stable-ref "$PREVIOUS_STABLE"
|
||||
;;
|
||||
e2e)
|
||||
bash scripts/dev/test-multi-profile-e2e.sh
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Reviewer Router approval signal
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
|
||||
# This workflow only converts an approval-state change into a trusted
|
||||
# workflow_run event. It must never read secrets, check out code, or mutate the
|
||||
# pull request; the default-branch Reviewer routing workflow owns reconciliation.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
signal:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Signal approval-state change
|
||||
run: echo "Review state changed; default-branch reconciliation will re-evaluate App-owned merge intents."
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,10 +20,6 @@ test/cli_compat/testdata/
|
||||
.gitignore
|
||||
.worktrees/
|
||||
.qoder/
|
||||
_logs/
|
||||
_docs/
|
||||
_output/
|
||||
vendor/
|
||||
|
||||
# Secrets & credentials
|
||||
.env
|
||||
|
||||
@@ -44,8 +44,7 @@ Schema contract) keep separate authorities — do not merge them with
|
||||
## Command framework declaration
|
||||
|
||||
- Framework definition: `docs/rfc-command-framework-convergence.md` **§5.0**
|
||||
- Today (leaf): `helpers.LeafSpec` / `shortcut.Shortcut` → `corecmd.Spec` (+ optional `Contract`) → `corecmd.New`
|
||||
- Today (non-leaf): owning Cobra command → complete `corecmd.GroupPolicy{Mode, Positionals, Recovery}` → `corecmd.ApplyGroupPolicy`; the final assembled-tree gate rejects undeclared groups and stale group declarations on leaves
|
||||
- Today: `helpers.LeafSpec` / `shortcut.Shortcut` → `corecmd.Spec` (+ optional `Contract`) → `corecmd.New`
|
||||
- **Declare = final Schema source**: `Flags` / `Constraints` / `Safety` / `ConstParams` / `Contract` (`corecmd.ContractDecl`; nested fields are `contract.*`)
|
||||
- Naming: `ContractDecl` is the authoring leaf declaration. "Schema" means Catalog / `ToolSpec` delivery — do not reintroduce `SchemaDecl`.
|
||||
- `Safety` uses `contract.SafetySpec` (`internal/corecmd/contract` only — no `cli.*` type alias). Its `confirmation` drives the runtime gate; `effect` / `risk` / `idempotency` are published unchanged. When `Contract` is set, convert once via `contractfinal.RegisterRuntimeContractFinal` (all callers — `corecmd.New` registers internally); assembly **pass-throughs** Final.
|
||||
@@ -61,7 +60,6 @@ Schema contract) keep separate authorities — do not merge them with
|
||||
- **Tier2** — `DeclareLeafMetadata` (helpers migration; **Shortcut may also use this path — acceptable**)
|
||||
- **Tier3** — bare Cobra (should shrink over time; reviewed exclusions where needed)
|
||||
- Long-term outlook only: broader mcpbind / fewer hand-written `Execute` bodies. **Not** a current hard requirement to delete `Shortcut.Execute` or force mcpbind.
|
||||
- Group policy is separate from the leaf tiers: `corecmd.Spec` remains leaf-only. `ApplyGroupPolicy` must not infer or enable `TraverseChildren`; parent local-flag inheritance remains an explicit owning-command surface.
|
||||
- Description declare vs delivery: construction requires `ContractDecl.Description` (evidence). Catalog delivery prefers Cobra Long → provenance `cobra_help`; without Long, declared text → `contract_final`. Title: declared first, then Short, then MCP. Do **not** read this as "declare = wire final" or dual authority.
|
||||
- **Execute** = hooks (`Validate` / `Call` / `RunE` / `PostMount`) — not a second surface authority
|
||||
- Declaration path has **no reviewed parallel fields**; migration-only `runtime_gate` annotate until `Safety` is declared
|
||||
|
||||
-311
File diff suppressed because one or more lines are too long
+2
-2
@@ -74,9 +74,9 @@ coverage is additionally selected for platform-sensitive code.
|
||||
`make authoritative-interface-integrity BASE_REF=<merge-base> STABLE_REF=<latest-GA-tag> CANDIDATE_REF=<candidate-sha>`.
|
||||
The Make target delegates to the authoritative wrapper; CI does not invoke a
|
||||
second comparator or the legacy fixture checker. See
|
||||
[CLI Help / Schema compatibility migration governance](docs/cli-interface-flag-migrations.md)
|
||||
[CLI flag compatibility migration governance](docs/cli-interface-flag-migrations.md)
|
||||
for the reviewed two-stage `pending` → `consumed` lifecycle.
|
||||
Agent-visible flag or command-path migrations must also run
|
||||
Agent-visible flag migrations must also run
|
||||
`make schema-compatibility BASE_REF=<merge-base> STABLE_REF=<latest-GA-tag> CANDIDATE_REF=<candidate-sha>`;
|
||||
it consumes the same base-owned ledger rather than a second exception list.
|
||||
5. Run `./scripts/policy/check-generated-drift.sh` when generated artifacts may
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
class DingtalkWorkspaceCliBeta < Formula
|
||||
desc "Automate DingTalk workspace tasks from the terminal (beta channel)"
|
||||
homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
|
||||
version "1.0.60-beta.2"
|
||||
version "1.0.58-beta.6"
|
||||
license "Apache-2.0"
|
||||
keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli"
|
||||
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.60-beta.2/dws-darwin-arm64.tar.gz"
|
||||
sha256 "e7776807f0664cbf0d0728cc236f2415c0981eb8d6557a897d2eeee708641b1d"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-darwin-arm64.tar.gz"
|
||||
sha256 "8f55497b84113f81b318e087c723016a02eded1cb5784cbd0811fe527d5852ca"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.60-beta.2/dws-darwin-amd64.tar.gz"
|
||||
sha256 "3004474df3cfb529719348f02c9f2f39afa88f0fca469fe8303a9ebe0f3a0034"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-darwin-amd64.tar.gz"
|
||||
sha256 "b1762f1640310fb4100634fe54d9baace46384bb270c59148fe306275554d491"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.60-beta.2/dws-linux-arm64.tar.gz"
|
||||
sha256 "6386885d10f149c8c555031dda4cf07bf34e1e9daad61d4cd948b92d3c7b7bad"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-linux-arm64.tar.gz"
|
||||
sha256 "55393310ef0e1f24ea2c0dc22f00c0eb3b343edc2deb18d0db7838cda65af25d"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.60-beta.2/dws-linux-amd64.tar.gz"
|
||||
sha256 "5c94c2af269d2fe5a79a400d4fa3af267a86d6ab21b01a24ede1d29514a6eaef"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-linux-amd64.tar.gz"
|
||||
sha256 "3830f77d09b4da4aa39f0c08d772ff3fa1ffb837eb15bb417f97edbccb073b7d"
|
||||
end
|
||||
end
|
||||
|
||||
resource "skills" do
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.60-beta.2/dws-skills.zip"
|
||||
sha256 "c3bd917f1b44a978ba2a9fbe95c5d0910ccf75f870f1c9b0dc356262ab1080c5"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-skills.zip"
|
||||
sha256 "f304a883a4f9e938b26a44692cd5a8d3d8704ba70ee7f33ba7c288434da72b6f"
|
||||
end
|
||||
|
||||
def install
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
class DingtalkWorkspaceCli < Formula
|
||||
desc "Automate DingTalk workspace tasks from the terminal"
|
||||
homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
|
||||
version "1.0.59"
|
||||
version "1.0.58"
|
||||
license "Apache-2.0"
|
||||
|
||||
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59/dws-darwin-arm64.tar.gz"
|
||||
sha256 "61135a2a9286204ce060847e653c63c1e9784a0fa631bb7e0563b90628762a35"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-darwin-arm64.tar.gz"
|
||||
sha256 "7d98599f90cae9d42b51ff2863efc87dbfb4a3176ff3c84fc2216110c0157a70"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59/dws-darwin-amd64.tar.gz"
|
||||
sha256 "fd14b0b1a1475891fb243bf6453857a1044ab5a40bcf7dc1c7c795f57e5b03ba"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-darwin-amd64.tar.gz"
|
||||
sha256 "4c12e35e5bf7e0905812cd42dc94a5345068a2c16e306bb50b13c5c78b5cb95d"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59/dws-linux-arm64.tar.gz"
|
||||
sha256 "5bfe9ac7d1798b028f0fad579bbdffec5898e2fb16ee36f5766ab58e208abd50"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-linux-arm64.tar.gz"
|
||||
sha256 "5ef6bde24bc3db6a11a0f1d0b3343a048956b2cbcf6cd3409a037fb6ba425489"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59/dws-linux-amd64.tar.gz"
|
||||
sha256 "be1eb9a1f8fc5048e578b5b0bde212fc90baca0f289236c7c333d824bd869cf3"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-linux-amd64.tar.gz"
|
||||
sha256 "3ccadcc6f070a39d2b2ba20429a4fcdc2f21639bf79f34361dc7d16f501bfda6"
|
||||
end
|
||||
end
|
||||
|
||||
resource "skills" do
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59/dws-skills.zip"
|
||||
sha256 "7ce5c3ab6f6a367407f64971bc5ff96cfcdfade2c1a10d326144b17c7b25a57e"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-skills.zip"
|
||||
sha256 "2626debc21c3daadfd155b4c167b2219b97e801398fe4441a8b48138960ab264"
|
||||
end
|
||||
|
||||
def install
|
||||
|
||||
@@ -10,7 +10,7 @@ SCHEMA_META_INDEX_OUTPUT ?= artifacts/schema_meta_index.gob
|
||||
POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)"
|
||||
GO_SOURCE_LIST = git ls-files -z --cached --others --exclude-standard -- '*.go'
|
||||
|
||||
.PHONY: all help build rebuild test test-plan test-auth-legacy-compat shortcut-public-e2e-proof lint format-check fmt policy edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity skill-context-budget multi-im-skill-chain-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema fetch-mcp-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
||||
.PHONY: all help build rebuild test test-plan test-auth-legacy-compat lint format-check fmt policy edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity skill-context-budget multi-im-skill-chain-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema fetch-mcp-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
||||
|
||||
all: setup-hooks fmt lint build test rebuild
|
||||
|
||||
@@ -18,9 +18,8 @@ help:
|
||||
@printf "Available targets:\n"
|
||||
@printf " make build - Build the dws CLI binary\n"
|
||||
@printf " make test - Run the Go test suite\n"
|
||||
@printf " make test-plan - Verify CI test and full-suite coverage package plans cover their scopes exactly once\n"
|
||||
@printf " make test-plan - Verify every default Go package belongs to one CI test shard\n"
|
||||
@printf " make test-auth-legacy-compat - Run stable legacy authentication compatibility regressions\n"
|
||||
@printf " make shortcut-public-e2e-proof - Prove every reviewed Devdoc/HRbrain/PAT public Shortcut through exact and owning raw execution\n"
|
||||
@printf " make lint - Run formatting checks, go vet, and staticcheck\n"
|
||||
@printf " make format-check - Check all repository Go source files with gofmt\n"
|
||||
@printf " make fmt - Format all repository Go source files\n"
|
||||
@@ -63,9 +62,6 @@ test-auth-legacy-compat:
|
||||
@mkdir -p "$(POLICY_GOTMPDIR)"
|
||||
@GO="$(GO)" $(POLICY_ENV) ./scripts/policy/check-auth-legacy-compat.sh
|
||||
|
||||
shortcut-public-e2e-proof: build
|
||||
@GO="$(GO)" DWS_PACKAGE_VERSION="$(DWS_PACKAGE_VERSION)" ./scripts/policy/check-shortcut-public-e2e-proof.sh
|
||||
|
||||
lint:
|
||||
@./scripts/dev/lint.sh
|
||||
|
||||
@@ -88,7 +84,7 @@ fmt:
|
||||
$(GO_SOURCE_LIST) > "$$go_files"; \
|
||||
xargs -0 sh -c 'if [ "$$#" -gt 0 ]; then exec gofmt -w -- "$$@"; fi' sh < "$$go_files"
|
||||
|
||||
policy: test-auth-legacy-compat shortcut-public-e2e-proof
|
||||
policy: test-auth-legacy-compat
|
||||
@mkdir -p "$(POLICY_GOTMPDIR)"
|
||||
@$(POLICY_ENV) ./scripts/policy/check-open-source-assets.sh
|
||||
@$(POLICY_ENV) ./scripts/policy/check-skill-context-budget.sh
|
||||
|
||||
@@ -210,7 +210,7 @@ The verifier uses isolated directories and does not replace the `dws` on the cur
|
||||
The upgrade process follows a two-phase atomic flow to ensure consistency:
|
||||
|
||||
1. **Prepare** — downloads the platform-specific binary and skill packages to a temporary directory, verifies SHA256 checksums, and extracts/validates all files. If any step fails, the upgrade aborts without modifying the existing installation.
|
||||
2. **Apply** — only after all preparations succeed, the binary is replaced and skills are flattened into the canonical `~/.agents/skills` root. Agents classified by the pinned compatibility registry as supporting the universal root read it directly; other detected Agents receive links to the canonical copy, with a direct-copy fallback when links are unavailable. Older DWS-managed agent-specific copies are backed up and retired so the same Skill is not discovered twice.
|
||||
2. **Apply** — only after all preparations succeed, the binary is replaced and skills are flattened into detected agent-specific roots (for example `~/.codex/skills/dingtalk-chat`). `~/.agents/skills` is used only when no specific Agent is detected; once a specific root is active, older DWS-managed generic copies are backed up and retired so the same Skill is not discovered twice.
|
||||
|
||||
A backup of the current version is automatically created before each upgrade. Use `dws upgrade --rollback` to restore the previous version if needed.
|
||||
|
||||
@@ -405,7 +405,7 @@ After installing, AI tools like Claude Code / Cursor can operate DingTalk direct
|
||||
curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install-skills.sh | sh
|
||||
```
|
||||
|
||||
> Installers use `$HOME/.agents/skills/` as the canonical global store, following the universal `.agents/skills` convention. Agents classified by the pinned compatibility registry as universal read that root directly; detected non-universal Agents receive links to it (or copies when links are unavailable). Multi layout is per-product siblings, while mono uses the `dws/` subdirectory.
|
||||
> Installers prefer detected agent-specific roots such as `$HOME/.codex/skills/`. They use `.agents/skills/` only as the generic fallback when no specific Agent is detected; multi layout is per-product siblings, while mono uses the `dws/` subdirectory.
|
||||
>
|
||||
> China users: prefix `DWS_GITEE_REPO` to use the Gitee mirror — see [China mirror](#china-mirror).
|
||||
|
||||
@@ -482,7 +482,7 @@ Env vars: `DWS_SKILL_MODE=mono|multi` (also honored by `install.sh` / `install.p
|
||||
<details>
|
||||
<summary><strong>Personal Event Subscription</strong> — real-time DingTalk messages for event-driven agents</summary>
|
||||
|
||||
`dws event consume` subscribes as the currently logged-in user over a managed Stream WebSocket and emits each event as one NDJSON line on stdout. The public catalog covers scoped and all one-to-one/group messages, specified senders, read/recall/reaction events, group lifecycle events, and seven OA approval task/instance events.
|
||||
`dws event consume` subscribes as the currently logged-in user over a managed Stream WebSocket and emits each event as one NDJSON line on stdout. The public catalog covers scoped and all one-to-one/group messages, specified senders, read/recall/reaction events, group lifecycle events, and six OA approval task/instance events.
|
||||
|
||||
The default `ndjson`, `json`, and `pretty` output preserves the transport envelope (`type`, `event_type`, string `data`, and `headers`) for existing scripts; `compact` retains its existing processor. Add `--flatten` to emit the stable top-level business fields used by Agent workflows. `--format` controls JSON serialization; `--flatten` controls the data structure and cannot be combined with `-f raw` or `--debug-raw-events`.
|
||||
|
||||
@@ -530,13 +530,12 @@ dws event consume user_im_group_disbanded --group <openConversationId> --flatten
|
||||
dws event +listen-im --kind sender --user <userId> \
|
||||
--events message,read,recall -f ndjson
|
||||
|
||||
# Listen for all seven public OA approval events in one process
|
||||
# Listen for all six public OA approval events in one process
|
||||
dws event consume \
|
||||
user_oa_approval_task_created \
|
||||
user_oa_approval_task_finished \
|
||||
user_oa_approval_task_redirected \
|
||||
user_oa_approval_instance_started \
|
||||
user_oa_approval_instance_cc \
|
||||
user_oa_approval_instance_terminated \
|
||||
user_oa_approval_instance_finished \
|
||||
--flatten -f ndjson
|
||||
@@ -787,7 +786,6 @@ See [`docs/robot-quickstart.md`](./docs/robot-quickstart.md) for the full 4-step
|
||||
|
||||
## Reference & Docs
|
||||
|
||||
- [International DingTalk (`.io`) guide](./docs/international-region-guide.md) — international login, domestic/international profile switching, isolated testing, and troubleshooting
|
||||
- [Command Index](./docs/command-index.md) — every runtime command with description and when-to-use guidance
|
||||
- [Reference](./docs/reference.md) — environment variables, exit codes, output formats, shell completion
|
||||
- [Architecture](./docs/architecture.md) — static endpoint pipeline, command surface, transport layer
|
||||
|
||||
+3
-5
@@ -19,7 +19,7 @@
|
||||
</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **钉钉 DWS CLI 已全面开放,欢迎使用**:本项目涉及钉钉企业数据访问,需企业管理员授权后方可使用。欢迎加入钉钉 DWS 共创群获取支持与最新动态。详见下方 [开始使用](#开始使用)。
|
||||
> **共创阶段**:本项目涉及钉钉企业数据访问,需企业管理员授权后方可使用。欢迎加入钉钉 DWS 共创群获取支持与最新动态。详见下方 [开始使用](#开始使用)。
|
||||
>
|
||||
> <img src="https://img.alicdn.com/imgextra/i1/O1CN01WJyAsJ1prD2ovQACM_!!6000000005413-2-tps-718-720.png" alt="dws 开源沟通群二维码" width="150">
|
||||
|
||||
@@ -476,7 +476,7 @@ multi setup 或 upgrade 后,DWS 会把官方 bundle 快照和统一所有权
|
||||
<details>
|
||||
<summary><strong>个人事件订阅</strong> — 实时接收钉钉消息,驱动事件触发的 Agent</summary>
|
||||
|
||||
`dws event consume` 使用当前 OAuth 登录用户建立托管的 Stream WebSocket 长连接,并把每条事件以 NDJSON 一行输出到 stdout。当前公开目录覆盖指定范围和全量单聊/群消息、指定发送人、已读/撤回/表情回应、群生命周期,以及七个 OA 审批任务/实例事件。
|
||||
`dws event consume` 使用当前 OAuth 登录用户建立托管的 Stream WebSocket 长连接,并把每条事件以 NDJSON 一行输出到 stdout。当前公开目录覆盖指定范围和全量单聊/群消息、指定发送人、已读/撤回/表情回应、群生命周期,以及六个 OA 审批任务/实例事件。
|
||||
|
||||
默认 `ndjson`、`json`、`pretty` 输出保留兼容 transport envelope(`type`、`event_type`、字符串 `data`、`headers`),`compact` 继续沿用原 processor。Agent 或新脚本显式加 `--flatten` 后,输出稳定的顶层业务字段。`--format` 控制 JSON 序列化,`--flatten` 控制数据结构,且不能与 `-f raw` 或 `--debug-raw-events` 同时使用。
|
||||
|
||||
@@ -524,13 +524,12 @@ dws event consume user_im_group_disbanded --group <openConversationId> --flatten
|
||||
dws event +listen-im --kind sender --user <userId> \
|
||||
--events message,read,recall -f ndjson
|
||||
|
||||
# 一个进程监听全部七个公开 OA 审批事件
|
||||
# 一个进程监听全部六个公开 OA 审批事件
|
||||
dws event consume \
|
||||
user_oa_approval_task_created \
|
||||
user_oa_approval_task_finished \
|
||||
user_oa_approval_task_redirected \
|
||||
user_oa_approval_instance_started \
|
||||
user_oa_approval_instance_cc \
|
||||
user_oa_approval_instance_terminated \
|
||||
user_oa_approval_instance_finished \
|
||||
--flatten -f ndjson
|
||||
@@ -778,7 +777,6 @@ dws dev connect --channel auto --robot-client-id <id> --robot-client-secret <sec
|
||||
|
||||
## 参考与文档
|
||||
|
||||
- [国际版(`.io`)使用手册](./docs/international-region-guide.zh-CN.md) — 国际版登录、国内/国际 profile 切换、隔离验证与排障
|
||||
- [命令索引](./docs/command-index.md) — 全部运行时命令,带描述与使用场景
|
||||
- [参考手册](./docs/reference.md) — 环境变量、退出码、输出格式、Shell 补全
|
||||
- [架构设计](./docs/architecture.md) — 静态端点管道、命令面、Transport 层
|
||||
|
||||
+5
-63
@@ -13,71 +13,13 @@ if (!fs.existsSync(binaryPath)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Interactive commands must remain in the terminal's foreground session so
|
||||
// prompts can use /dev/tty. Non-interactive launches use a separate process
|
||||
// group, allowing a signal sent only to this wrapper to reach the full vendor
|
||||
// process tree exactly once.
|
||||
const isolateVendorProcessGroup = process.platform !== "win32" && !process.stdin.isTTY;
|
||||
|
||||
const child = childProcess.spawn(binaryPath, process.argv.slice(2), {
|
||||
const result = childProcess.spawnSync(binaryPath, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
detached: isolateVendorProcessGroup,
|
||||
});
|
||||
|
||||
let spawnFailed = false;
|
||||
let forwardedSignal = null;
|
||||
const forwardedSignals = ["SIGINT", "SIGTERM"];
|
||||
|
||||
function forwardSignal(signal) {
|
||||
forwardedSignal = signal;
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
if (process.platform === "win32") {
|
||||
child.kill(signal);
|
||||
return;
|
||||
}
|
||||
if (!isolateVendorProcessGroup) {
|
||||
// Ctrl-C is generated for the whole foreground process group, including
|
||||
// the vendor. SIGTERM is not terminal-generated and still needs an
|
||||
// explicit handoff when a process manager targets only this wrapper.
|
||||
if (signal === "SIGTERM") {
|
||||
child.kill(signal);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// detached makes the vendor PID the leader of its POSIX process group.
|
||||
// Signal the whole group so any subprocesses inherit the same shutdown.
|
||||
process.kill(-child.pid, signal);
|
||||
} catch (error) {
|
||||
// The group may have completed between the state check and kill.
|
||||
if (error.code !== "ESRCH") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.error) {
|
||||
console.error(result.error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const signalHandlers = new Map(
|
||||
forwardedSignals.map((signal) => [signal, () => forwardSignal(signal)]),
|
||||
);
|
||||
for (const signal of forwardedSignals) {
|
||||
process.on(signal, signalHandlers.get(signal));
|
||||
}
|
||||
|
||||
child.on("error", (error) => {
|
||||
spawnFailed = true;
|
||||
console.error(error.message);
|
||||
});
|
||||
|
||||
child.on("close", (code, signal) => {
|
||||
for (const forwarded of forwardedSignals) {
|
||||
process.removeListener(forwarded, signalHandlers.get(forwarded));
|
||||
}
|
||||
|
||||
const exitSignal = forwardedSignal || signal;
|
||||
if (exitSignal && process.platform !== "win32") {
|
||||
process.kill(process.pid, exitSignal);
|
||||
return;
|
||||
}
|
||||
process.exitCode = spawnFailed || code === null ? 1 : code;
|
||||
});
|
||||
process.exit(result.status === null ? 1 : result.status);
|
||||
|
||||
+126
-1214
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,6 @@
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.7.0"
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,16 +157,6 @@ func runCompare(args []string, stdout, stderr io.Writer) (bool, error) {
|
||||
"",
|
||||
"candidate flag migration manifest",
|
||||
)
|
||||
approvedCommandMigrationsPath := flags.String(
|
||||
"approved-command-migrations",
|
||||
"",
|
||||
"merge-base-owned approved command migration manifest",
|
||||
)
|
||||
candidateCommandMigrationsPath := flags.String(
|
||||
"candidate-command-migrations",
|
||||
"",
|
||||
"candidate command migration manifest",
|
||||
)
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -184,13 +174,8 @@ func runCompare(args []string, stdout, stderr io.Writer) (bool, error) {
|
||||
"--approved-flag-migrations and --candidate-flag-migrations must be provided together",
|
||||
)
|
||||
}
|
||||
if (*approvedCommandMigrationsPath == "") != (*candidateCommandMigrationsPath == "") {
|
||||
return false, fmt.Errorf(
|
||||
"--approved-command-migrations and --candidate-command-migrations must be provided together",
|
||||
)
|
||||
}
|
||||
if (*approvedMigrationsPath != "" || *approvedCommandMigrationsPath != "") && (*basePath == "" || *stablePath == "") {
|
||||
return false, fmt.Errorf("migration compare requires both --base and --stable")
|
||||
if *approvedMigrationsPath != "" && (*basePath == "" || *stablePath == "") {
|
||||
return false, fmt.Errorf("flag migration compare requires both --base and --stable")
|
||||
}
|
||||
|
||||
current, err := readSnapshot(*currentPath)
|
||||
@@ -212,39 +197,7 @@ func runCompare(args []string, stdout, stderr io.Writer) (bool, error) {
|
||||
}
|
||||
|
||||
report := interfacesnapshot.CompareAll(current, references)
|
||||
if *approvedCommandMigrationsPath != "" {
|
||||
flagApproved := interfacesnapshot.FlagMigrationManifest{Version: interfacesnapshot.FlagMigrationManifestVersion, Migrations: []interfacesnapshot.FlagMigration{}}
|
||||
flagCandidate := flagApproved
|
||||
if *approvedMigrationsPath != "" {
|
||||
flagApproved, err = readFlagMigrationManifest(*approvedMigrationsPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("read approved flag migrations: %w", err)
|
||||
}
|
||||
flagCandidate, err = readFlagMigrationManifest(*candidateMigrationsPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("read candidate flag migrations: %w", err)
|
||||
}
|
||||
}
|
||||
commandApproved, readErr := readCommandMigrationManifest(*approvedCommandMigrationsPath)
|
||||
if readErr != nil {
|
||||
return false, fmt.Errorf("read approved command migrations: %w", readErr)
|
||||
}
|
||||
commandCandidate, readErr := readCommandMigrationManifest(*candidateCommandMigrationsPath)
|
||||
if readErr != nil {
|
||||
return false, fmt.Errorf("read candidate command migrations: %w", readErr)
|
||||
}
|
||||
report, err = interfacesnapshot.CompareAllWithInterfaceMigrations(
|
||||
current,
|
||||
references,
|
||||
flagApproved,
|
||||
flagCandidate,
|
||||
commandApproved,
|
||||
commandCandidate,
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("validate interface migration lifecycle: %w", err)
|
||||
}
|
||||
} else if *approvedMigrationsPath != "" {
|
||||
if *approvedMigrationsPath != "" {
|
||||
approved, readErr := readFlagMigrationManifest(*approvedMigrationsPath)
|
||||
if readErr != nil {
|
||||
return false, fmt.Errorf("read approved flag migrations: %w", readErr)
|
||||
@@ -281,15 +234,6 @@ func readFlagMigrationManifest(path string) (interfacesnapshot.FlagMigrationMani
|
||||
return interfacesnapshot.ReadFlagMigrationManifest(file)
|
||||
}
|
||||
|
||||
func readCommandMigrationManifest(path string) (interfacesnapshot.CommandMigrationManifest, error) {
|
||||
file, err := os.Open(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return interfacesnapshot.CommandMigrationManifest{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
return interfacesnapshot.ReadCommandMigrationManifest(file)
|
||||
}
|
||||
|
||||
func validateHelpRendering(root *cobra.Command, snapshot interfacesnapshot.Snapshot) error {
|
||||
for _, command := range snapshot.Commands {
|
||||
path := strings.TrimPrefix(command.Path, "dws")
|
||||
@@ -336,5 +280,5 @@ func readSnapshot(path string) (interfacesnapshot.Snapshot, error) {
|
||||
func printUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "usage:")
|
||||
fmt.Fprintln(w, " interface-snapshot generate [--output FILE]")
|
||||
fmt.Fprintln(w, " interface-snapshot compare --current FILE [--base FILE] [--stable FILE] [--approved-flag-migrations FILE --candidate-flag-migrations FILE] [--approved-command-migrations FILE --candidate-command-migrations FILE]")
|
||||
fmt.Fprintln(w, " interface-snapshot compare --current FILE [--base FILE] [--stable FILE] [--approved-flag-migrations FILE --candidate-flag-migrations FILE]")
|
||||
}
|
||||
|
||||
@@ -166,102 +166,6 @@ func TestCrossPlatformCoverageRunCompareRequiresBothFlagMigrationInputs(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageRunCompareCommandMigrationInputs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
snapshotPath := writeSnapshot(t, dir, "snapshot.json", commandSnapshot("dws"))
|
||||
emptyFlag := writeManifest(t, dir, "empty-flags.json", `{"version":1,"migrations":[]}`)
|
||||
emptyCommand := writeManifest(t, dir, "empty-commands.json", `{"version":1,"migrations":[]}`)
|
||||
invalid := writeManifest(t, dir, "invalid-commands.json", `{`)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
args := []string{
|
||||
"compare",
|
||||
"--current", snapshotPath,
|
||||
"--base", snapshotPath,
|
||||
"--stable", snapshotPath,
|
||||
"--approved-flag-migrations", emptyFlag,
|
||||
"--candidate-flag-migrations", emptyFlag,
|
||||
"--approved-command-migrations", emptyCommand,
|
||||
"--candidate-command-migrations", emptyCommand,
|
||||
}
|
||||
if exitCode := run(args, &stdout, &stderr); exitCode != 0 {
|
||||
t.Fatalf("combined migration compare exit=%d stderr=%s", exitCode, stderr.String())
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
approved string
|
||||
candidate string
|
||||
want string
|
||||
}{
|
||||
{"approved flag", invalid, emptyFlag, "read approved flag migrations"},
|
||||
{"candidate flag", emptyFlag, invalid, "read candidate flag migrations"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
testArgs := []string{
|
||||
"compare", "--current", snapshotPath, "--base", snapshotPath, "--stable", snapshotPath,
|
||||
"--approved-flag-migrations", test.approved,
|
||||
"--candidate-flag-migrations", test.candidate,
|
||||
"--approved-command-migrations", emptyCommand,
|
||||
"--candidate-command-migrations", emptyCommand,
|
||||
}
|
||||
if exitCode := run(testArgs, &stdout, &stderr); exitCode != 2 || !strings.Contains(stderr.String(), test.want) {
|
||||
t.Fatalf("combined flag error exit=%d stderr=%s", exitCode, stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
approved string
|
||||
candidate string
|
||||
want string
|
||||
}{
|
||||
{"approved", invalid, emptyCommand, "read approved command migrations"},
|
||||
{"candidate", emptyCommand, invalid, "read candidate command migrations"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
testArgs := []string{
|
||||
"compare", "--current", snapshotPath, "--base", snapshotPath, "--stable", snapshotPath,
|
||||
"--approved-command-migrations", test.approved,
|
||||
"--candidate-command-migrations", test.candidate,
|
||||
}
|
||||
if exitCode := run(testArgs, &stdout, &stderr); exitCode != 2 || !strings.Contains(stderr.String(), test.want) {
|
||||
t.Fatalf("command manifest error exit=%d stderr=%s", exitCode, stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stderr.Reset()
|
||||
if exitCode := run([]string{
|
||||
"compare", "--current", snapshotPath, "--base", snapshotPath,
|
||||
"--approved-command-migrations", emptyCommand,
|
||||
}, &stdout, &stderr); exitCode != 2 || !strings.Contains(stderr.String(), "provided together") {
|
||||
t.Fatalf("one-sided command manifest exit=%d stderr=%s", exitCode, stderr.String())
|
||||
}
|
||||
|
||||
if _, err := readCommandMigrationManifest(filepath.Join(dir, "missing.json")); err == nil {
|
||||
t.Fatal("missing command migration manifest unexpectedly read")
|
||||
}
|
||||
if _, err := readCommandMigrationManifest(invalid); err == nil {
|
||||
t.Fatal("invalid command migration manifest unexpectedly read")
|
||||
}
|
||||
|
||||
pending := writeManifest(t, dir, "pending-command.json", commandMigrationManifestJSON("pending"))
|
||||
consumed := writeManifest(t, dir, "consumed-command.json", commandMigrationManifestJSON("consumed"))
|
||||
stderr.Reset()
|
||||
if exitCode := run([]string{
|
||||
"compare", "--current", snapshotPath, "--base", snapshotPath, "--stable", snapshotPath,
|
||||
"--approved-command-migrations", pending,
|
||||
"--candidate-command-migrations", consumed,
|
||||
}, &stdout, &stderr); exitCode != 2 || !strings.Contains(stderr.String(), "validate interface migration lifecycle") {
|
||||
t.Fatalf("command lifecycle error exit=%d stderr=%s", exitCode, stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageRunCompareRequiresBothReferencesForFlagMigrations(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
currentPath := writeSnapshot(t, dir, "current.json", commandSnapshot("dws"))
|
||||
@@ -663,33 +567,6 @@ func flagMigrationManifestJSON(state string) string {
|
||||
}`, "STATE", state, 1)
|
||||
}
|
||||
|
||||
func commandMigrationManifestJSON(state string) string {
|
||||
return strings.Replace(`{
|
||||
"version": 1,
|
||||
"migrations": [{
|
||||
"kind": "command_move",
|
||||
"legacy": {
|
||||
"command": "dws chat message old",
|
||||
"before": {"present": true, "runnable": true},
|
||||
"after": {"present": true, "runnable": true, "hidden": true}
|
||||
},
|
||||
"replacement": {
|
||||
"command": "dws chat topic new",
|
||||
"before": {"present": false},
|
||||
"after": {"present": true, "runnable": true}
|
||||
},
|
||||
"schema": {
|
||||
"product_id": "chat",
|
||||
"source_tool_id": "chat.move",
|
||||
"replacement_tool_id": "chat.move",
|
||||
"parameters": []
|
||||
},
|
||||
"state": "STATE",
|
||||
"reason": "reviewed command migration"
|
||||
}]
|
||||
}`, "STATE", state, 1)
|
||||
}
|
||||
|
||||
func hasFlag(flags []interfacesnapshot.Flag, name, flagType string) bool {
|
||||
for _, flag := range flags {
|
||||
if flag.Name == name && flag.Type == flagType {
|
||||
|
||||
+2
-66
@@ -15,76 +15,12 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
|
||||
"gitlab.alibaba-inc.com/aes/aem-go-sdk/clitrack"
|
||||
)
|
||||
|
||||
var (
|
||||
appExecute = app.ExecuteWithTelemetry
|
||||
resolveTelemetryIdentity = app.ResolveTelemetryIdentity
|
||||
trackRun = func(cfg clitrack.Config, execute func() error, exitCode func(error) int) {
|
||||
clitrack.New(cfg).Run(execute, exitCode)
|
||||
}
|
||||
)
|
||||
|
||||
// trackedExitError tells clitrack that the command failed without asking it to
|
||||
// print the error a second time. The already-rendered message is published via
|
||||
// ExtraFields c5, while app.Execute remains the sole owner of presentation.
|
||||
type trackedExitError struct{}
|
||||
|
||||
func (trackedExitError) Error() string { return "" }
|
||||
|
||||
func trackerConfig(identity app.TelemetryIdentity, commandPath, errorMessage *string) clitrack.Config {
|
||||
return clitrack.Config{
|
||||
PID: "wcCRwZ",
|
||||
App: "dws",
|
||||
Version: app.RawVersion(),
|
||||
UID: identity.UserID,
|
||||
Username: identity.UserName,
|
||||
NoCommandLine: true,
|
||||
NoCwd: true,
|
||||
NoAutomaticDimensions: true,
|
||||
ExtraFields: func() map[string]string {
|
||||
fields := map[string]string{"c9": *commandPath}
|
||||
if identity.CorpID != "" {
|
||||
fields["c10"] = identity.CorpID
|
||||
}
|
||||
if *errorMessage != "" {
|
||||
fields["c5"] = *errorMessage
|
||||
}
|
||||
return fields
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func telemetryOptedOut() bool {
|
||||
return strings.TrimSpace(os.Getenv("DO_NOT_TRACK")) != ""
|
||||
}
|
||||
var exit = os.Exit
|
||||
|
||||
func main() {
|
||||
optedOut := telemetryOptedOut()
|
||||
identity := app.TelemetryIdentity{}
|
||||
if !optedOut {
|
||||
identity = resolveTelemetryIdentity(os.Args[1:])
|
||||
}
|
||||
exitCode := 0
|
||||
commandPath := "dws"
|
||||
errorMessage := ""
|
||||
cfg := trackerConfig(identity, &commandPath, &errorMessage)
|
||||
if optedOut {
|
||||
cfg.PID = ""
|
||||
}
|
||||
trackRun(
|
||||
cfg,
|
||||
func() error {
|
||||
exitCode, commandPath, errorMessage = appExecute()
|
||||
if exitCode != 0 {
|
||||
return trackedExitError{}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(error) int { return exitCode },
|
||||
)
|
||||
exit(app.Execute())
|
||||
}
|
||||
|
||||
+13
-206
@@ -1,220 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
||||
"gitlab.alibaba-inc.com/aes/aem-go-sdk/clitrack"
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoverageMainRunsThroughCLITracker(t *testing.T) {
|
||||
for _, wantCode := range []int{0, 1, 3, 5} {
|
||||
t.Run(fmt.Sprintf("exit_%d", wantCode), func(t *testing.T) {
|
||||
t.Setenv("DO_NOT_TRACK", "")
|
||||
wantError := ""
|
||||
if wantCode != 0 {
|
||||
wantError = "synthetic failure"
|
||||
}
|
||||
testseam.Swap(t, &os.Args, []string{"dws", "sheet", "read", "--profile", "corp-a"})
|
||||
testseam.Swap(t, &resolveTelemetryIdentity, func(args []string) app.TelemetryIdentity {
|
||||
if strings.Join(args, " ") != "sheet read --profile corp-a" {
|
||||
t.Fatalf("telemetry identity args = %#v", args)
|
||||
}
|
||||
return app.TelemetryIdentity{UserID: "user-1", UserName: "Alice", CorpID: "corp-1"}
|
||||
})
|
||||
testseam.Swap(t, &appExecute, func() (int, string, string) { return wantCode, "sheet read", wantError })
|
||||
called := false
|
||||
testseam.Swap(t, &trackRun, func(cfg clitrack.Config, execute func() error, exitCode func(error) int) {
|
||||
called = true
|
||||
if cfg.PID != "wcCRwZ" || cfg.App != "dws" {
|
||||
t.Fatalf("tracker identity = PID %q App %q", cfg.PID, cfg.App)
|
||||
}
|
||||
if cfg.Version != app.RawVersion() {
|
||||
t.Fatalf("tracker Version = %q, want %q", cfg.Version, app.RawVersion())
|
||||
}
|
||||
if !cfg.NoCommandLine || !cfg.NoCwd || !cfg.NoAutomaticDimensions || cfg.CaptureOutput {
|
||||
t.Fatalf("tracker privacy config = NoCommandLine %v NoCwd %v NoAutomaticDimensions %v CaptureOutput %v", cfg.NoCommandLine, cfg.NoCwd, cfg.NoAutomaticDimensions, cfg.CaptureOutput)
|
||||
}
|
||||
if cfg.Env != "" || cfg.EventID != "" || cfg.Endpoint != "" || cfg.FlushTimeout != 0 || cfg.OutputMaxLen != 0 {
|
||||
t.Fatalf("tracker SDK defaults were overridden: %#v", cfg)
|
||||
}
|
||||
if cfg.UID != "user-1" || cfg.Username != "Alice" || cfg.UserType != "" {
|
||||
t.Fatalf("tracker user identity = UID %q Username %q UserType %q", cfg.UID, cfg.Username, cfg.UserType)
|
||||
}
|
||||
func TestCrossPlatformCoverageMainExitsWithSuccessfulVersionCommand(t *testing.T) {
|
||||
previousExit := exit
|
||||
previousArgs := os.Args
|
||||
t.Cleanup(func() {
|
||||
exit = previousExit
|
||||
os.Args = previousArgs
|
||||
})
|
||||
|
||||
err := execute()
|
||||
if wantCode == 0 && err != nil {
|
||||
t.Fatalf("successful tracked execute error = %v", err)
|
||||
}
|
||||
if wantCode != 0 && (err == nil || err.Error() != "") {
|
||||
t.Fatalf("failed tracked execute error = %#v, want empty sentinel", err)
|
||||
}
|
||||
if gotCode := exitCode(err); gotCode != wantCode {
|
||||
t.Fatalf("tracked exit code = %d, want %d", gotCode, wantCode)
|
||||
}
|
||||
fields := cfg.ExtraFields()
|
||||
if fields["c9"] != "sheet read" || fields["c10"] != "corp-1" || fields["c5"] != wantError {
|
||||
t.Fatalf("tracker extra fields = %#v, want command path, corp ID, and error %q", fields, wantError)
|
||||
}
|
||||
if (wantError == "" && len(fields) != 2) || (wantError != "" && len(fields) != 3) {
|
||||
t.Fatalf("tracker extra field count = %d for error %q", len(fields), wantError)
|
||||
}
|
||||
})
|
||||
|
||||
main()
|
||||
if !called {
|
||||
t.Fatalf("trackRun was not called for exit code %d", wantCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageTrackerConfigOmitsEmptyOrganization(t *testing.T) {
|
||||
commandPath := "version"
|
||||
errorMessage := ""
|
||||
cfg := trackerConfig(app.TelemetryIdentity{}, &commandPath, &errorMessage)
|
||||
if cfg.UID != "" {
|
||||
t.Fatalf("empty identity UID = %q", cfg.UID)
|
||||
}
|
||||
if cfg.Username != "" {
|
||||
t.Fatalf("empty identity Username = %q", cfg.Username)
|
||||
}
|
||||
if fields := cfg.ExtraFields(); len(fields) != 1 || fields["c9"] != "version" {
|
||||
t.Fatalf("empty organization fields = %#v", fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDefaultTrackRunNoopTracker(t *testing.T) {
|
||||
called := false
|
||||
trackRun(clitrack.Config{}, func() error {
|
||||
code := -1
|
||||
exit = func(value int) {
|
||||
called = true
|
||||
return nil
|
||||
}, nil)
|
||||
if !called {
|
||||
t.Fatal("default tracker did not execute callback")
|
||||
code = value
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageMainRespectsDoNotTrack(t *testing.T) {
|
||||
t.Setenv("DO_NOT_TRACK", "1")
|
||||
testseam.Swap(t, &os.Args, []string{"dws", "version"})
|
||||
testseam.Swap(t, &resolveTelemetryIdentity, func([]string) app.TelemetryIdentity {
|
||||
t.Fatal("DO_NOT_TRACK must skip telemetry identity reads")
|
||||
return app.TelemetryIdentity{}
|
||||
})
|
||||
testseam.Swap(t, &appExecute, func() (int, string, string) { return 0, "version", "" })
|
||||
testseam.Swap(t, &trackRun, func(cfg clitrack.Config, execute func() error, exitCode func(error) int) {
|
||||
if cfg.PID != "" || cfg.UID != "" || cfg.Username != "" {
|
||||
t.Fatalf("opted-out tracker config = %#v", cfg)
|
||||
}
|
||||
if err := execute(); err != nil {
|
||||
t.Fatalf("opted-out execution failed: %v", err)
|
||||
}
|
||||
if code := exitCode(nil); code != 0 {
|
||||
t.Fatalf("opted-out exit code = %d, want 0", code)
|
||||
}
|
||||
})
|
||||
|
||||
os.Args = []string{"dws", "version"}
|
||||
main()
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageTrackerPayloadUsesReviewedFieldWhitelist(t *testing.T) {
|
||||
testseam.Protect(t, &os.Args)
|
||||
os.Args = []string{"dws", "sheet", "read", "--access-token", "must-not-leak"}
|
||||
t.Setenv("SHELL", "/bin/zsh")
|
||||
t.Setenv("TERM_SESSION_ID", "stable-session")
|
||||
t.Setenv("TMUX_PANE", "%42")
|
||||
t.Setenv("LANG", "zh_CN.UTF-8")
|
||||
t.Setenv("LC_ALL", "zh_CN.UTF-8")
|
||||
t.Chdir(t.TempDir())
|
||||
|
||||
requestBody := make(chan []byte, 1)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
requestBody <- body
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
commandPath := "sheet read"
|
||||
errorMessage := ""
|
||||
cfg := trackerConfig(app.TelemetryIdentity{UserID: "user-1", UserName: "Alice", CorpID: "corp-1"}, &commandPath, &errorMessage)
|
||||
cfg.Endpoint = server.URL
|
||||
cfg.FlushTimeout = time.Second
|
||||
clitrack.New(cfg).Run(func() error { return nil }, nil)
|
||||
|
||||
var body []byte
|
||||
select {
|
||||
case body = <-requestBody:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for telemetry request")
|
||||
}
|
||||
var envelope map[string]string
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
t.Fatalf("decode telemetry request %q: %v", body, err)
|
||||
}
|
||||
decoded, err := url.QueryUnescape(envelope["gokey"])
|
||||
if err != nil {
|
||||
t.Fatalf("decode gokey: %v", err)
|
||||
}
|
||||
globalFields, err := url.ParseQuery(decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("parse global telemetry fields: %v", err)
|
||||
}
|
||||
eventFields, err := url.ParseQuery(globalFields.Get("msg"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse event telemetry fields: %v", err)
|
||||
}
|
||||
|
||||
assertTelemetryKeys(t, globalFields, []string{"app_name", "app_version", "env", "msg", "pid", "platform", "uid", "username", "version"})
|
||||
assertTelemetryKeys(t, eventFields, []string{"c1", "c10", "c3", "c4", "c9", "p1", "p4", "ts", "type"})
|
||||
for key, want := range map[string]string{
|
||||
"app_name": "dws", "app_version": app.RawVersion(), "env": "prod", "pid": "wcCRwZ",
|
||||
"platform": "cli", "uid": "user-1", "username": "Alice", "version": app.RawVersion(),
|
||||
} {
|
||||
if got := globalFields.Get(key); got != want {
|
||||
t.Fatalf("global telemetry field %s = %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
for key, want := range map[string]string{
|
||||
"type": "event", "p1": "cli.exec", "p4": "SYS", "c1": "dws", "c3": "0", "c9": "sheet read", "c10": "corp-1",
|
||||
} {
|
||||
if got := eventFields.Get(key); got != want {
|
||||
t.Fatalf("event telemetry field %s = %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"device_id", "ext", "os", "os_version", "pv_id", "sdk_version", "sid", "timezone_offset"} {
|
||||
if globalFields.Has(key) {
|
||||
t.Fatalf("global telemetry leaked %s: %q", key, decoded)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"c2", "c5", "c6", "c7", "c8"} {
|
||||
if eventFields.Has(key) {
|
||||
t.Fatalf("event telemetry leaked %s: %q", key, globalFields.Get("msg"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertTelemetryKeys(t *testing.T, fields url.Values, want []string) {
|
||||
t.Helper()
|
||||
got := make([]string, 0, len(fields))
|
||||
for key := range fields {
|
||||
got = append(got, key)
|
||||
}
|
||||
sort.Strings(got)
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("telemetry keys = %v, want %v", got, want)
|
||||
if !called || code != 0 {
|
||||
t.Fatalf("main exit = called %v, code %d", called, code)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-215
@@ -62,171 +62,9 @@ make lint
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Reviewer Router GitHub App
|
||||
|
||||
Reviewer requests and merge authority intentionally use different identities.
|
||||
The base-owned `pull_request_target` workflow may use its built-in
|
||||
`GITHUB_TOKEN` to request reviewers, but it must mint a dedicated GitHub App
|
||||
installation token before enabling auto-merge. GitHub suppresses most workflow
|
||||
events created by the built-in token; using it for auto-merge prevents the
|
||||
merge commit's `push` workflows from running and leaves the exact-SHA Coverage
|
||||
baseline without a trusted main-scoped producer.
|
||||
|
||||
Configure the dedicated App before merging a workflow revision that requires
|
||||
it:
|
||||
|
||||
- install it only on `DingTalk-Real-AI/dingtalk-workspace-cli`;
|
||||
- grant only `Contents: read and write` and `Pull requests: read and write`;
|
||||
- set repository variable `REVIEWER_ROUTER_APP_CLIENT_ID` to its client ID;
|
||||
- set `REVIEWER_ROUTER_APP_SLUG` to its exact lowercase slug;
|
||||
- set repository secret `REVIEWER_ROUTER_APP_PRIVATE_KEY` to its private key;
|
||||
- create one active repository branch ruleset named `main-merge-writers`,
|
||||
targeting only `refs/heads/main`, with exactly one `Restrict updates` rule
|
||||
(`update_allows_fetch_and_merge: false`). GitHub may project that strict
|
||||
value through the read APIs as `{type: "update"}` with `parameters` omitted;
|
||||
consumers accept only that exact omission or a one-field `parameters` object
|
||||
containing explicit boolean `false`, and reject every other present shape or
|
||||
value. They then bind the same ruleset node through GraphQL and require its
|
||||
non-null `updateAllowsFetchAndMerge` value to be exactly `false`;
|
||||
- give that ruleset exactly three bypass actors: the Reviewer Router App as an
|
||||
`Integration` in `pull_request` mode, plus `haofeng0705` (ID `30925823`) and
|
||||
`PeterGuy326` (ID `47820304`) in `always` mode for Formula publication and
|
||||
break-glass recovery;
|
||||
- never give the App bypass on `main-protection`, `main-quality`, or any other
|
||||
ruleset, and never reuse `HOMEBREW_PR_TOKEN`,
|
||||
`RELEASE_GOVERNANCE_TOKEN`, or a personal token for Reviewer Router.
|
||||
|
||||
The workflow limits each minted token to the current repository, requests the
|
||||
two permissions explicitly, and lets the token action revoke it at job end.
|
||||
It also requires the minted App slug to equal the reviewed repository variable;
|
||||
there is no `GITHUB_TOKEN` fallback. Before reading App credentials, the
|
||||
base-owned workflow revalidates the event's exact base/head and uses its
|
||||
built-in token only to disable an existing request owned by
|
||||
`github-actions[bot]` or one whose title or merge metadata requests that GitHub
|
||||
skip workflows. A mint or permission failure therefore leaves that PR
|
||||
manual-merge only. The built-in token's `Contents: write` permission is
|
||||
isolated to this trusted cleanup job and is never used to enable auto-merge;
|
||||
review routing keeps `Contents: read`. Existing requests owned by a human or
|
||||
another non-built-in identity are replaced with the exact dedicated-App
|
||||
request after token minting. Only an already App-owned request with the fixed
|
||||
headline/body is preserved. The required `Test` context reads the live
|
||||
repository settings and applied rulesets, verifies the exact writer-rule
|
||||
shape, and requires its own built-in Actions identity to report
|
||||
`current_user_can_bypass: never`. Before enabling or reconciling auto-merge,
|
||||
the minted App independently requires `pull_requests_only` on that writer rule
|
||||
and `never` on every other active main ruleset. These identity-relative checks
|
||||
remain available to low-privilege tokens; GitHub deliberately hides the full
|
||||
`bypass_actors` list from callers without ruleset-write access. Operators must
|
||||
therefore inspect that list during rollout and keep it at the exact three actors
|
||||
above. The required `Test` context then briefly waits for the concurrent
|
||||
router takeover and accepts only a null request or the configured App owner
|
||||
with exact fixed metadata. A null request is safe for this failure mode because
|
||||
the built-in Actions identity cannot pass the writer rule; other permitted
|
||||
identities emit either a protected-main push or the trusted closed-PR repair.
|
||||
Draft PRs skip this identity check; the explicit `ready_for_review` trigger
|
||||
reruns admission when they become merge-eligible,
|
||||
while `edited` and `auto_merge_enabled` rerun both workflows when the PR title
|
||||
or merge request changes. A human `auto_merge_disabled` event reruns CI without
|
||||
silently re-enabling the request, leaving it available only to the designated
|
||||
break-glass identity. The required `Test` context rejects GitHub workflow-skip
|
||||
directives in the PR title or an existing auto-merge request and verifies the
|
||||
repository's reviewed `MERGE_MESSAGE` title plus `PR_TITLE` or `BLANK` body
|
||||
defaults. GitHub does not expose those merge-related settings to the read-only
|
||||
admission token: the classifier accepts only both exact reviewed values or the
|
||||
complete omission of both properties, and rejects partial omission, `null`, or
|
||||
any other value. Before any enable, reconcile, or merge mutation, the dedicated
|
||||
App's current-repository token (which has `Contents: write`) must observe both
|
||||
exact reviewed values. The dedicated App binds each mutation to the exact head
|
||||
OID and supplies a fixed safe headline and body, so GitHub cannot copy an unsafe
|
||||
PR title into its merge commit.
|
||||
After enabling, the workflow requires the owner to equal the token action's
|
||||
exact `<app-slug>[bot]` output. If the event base/head changes during the
|
||||
mutation window, it removes only that App-owned request and fails the run.
|
||||
The App-owned native auto-merge request is the reviewed automation intent, not
|
||||
the sole executor: GitHub's deferred auto-merge path does not reliably apply a
|
||||
GitHub App's pull-request-only ruleset bypass. A zero-permission approval-signal
|
||||
workflow converts submitted or dismissed reviews into `workflow_run`; completed
|
||||
admission workflows use the same trusted default-branch trigger. The serialized
|
||||
reconcile job treats `workflow_run` only as a wake-up signal: it never reads the
|
||||
triggering run's pull-request payload or artifacts and never checks out code
|
||||
from that run. It enumerates open `main` PRs again through the API, then
|
||||
revalidates the safe App owner, metadata, and ruleset boundary immediately
|
||||
before calling the synchronous PR merge endpoint with the exact current head
|
||||
SHA. The preflight requires exactly one repository-owned `main-protection`
|
||||
ruleset with one latest-head approval and exactly one repository-owned
|
||||
`main-quality` ruleset with the reviewed nine strict checks. The App must report
|
||||
`never` on both and on every other non-writer ruleset. Every required context
|
||||
must be bound to the GitHub Actions App (`integration_id=15368`); a missing,
|
||||
different, or duplicate context/source entry fails closed together with
|
||||
deletion or weakening of either gate. HTTP 405 means the PR is not ready,
|
||||
while 409 means its revision
|
||||
changed; either remains open for the next event. Other failures make
|
||||
reconciliation red. A concurrent native merge is accepted only after the final
|
||||
PR state proves the exact head, App identity, and non-empty merge SHA.
|
||||
A staggered twice-hourly schedule provides eventual recovery if a webhook or
|
||||
workflow completion is delayed, and `workflow_dispatch` remains the on-demand
|
||||
repair path.
|
||||
The break-glass publisher must preserve a safe final commit message;
|
||||
`[skip ci]`, `[ci skip]`, `[no ci]`, `[skip actions]`,
|
||||
`[actions skip]`, and a `skip-checks: true` trailer are forbidden outside the
|
||||
release-controlled Formula-only path below.
|
||||
|
||||
GitHub may suppress `pull_request_target` entirely for security-sensitive head
|
||||
branch names, including names that look like commit SHAs. Such a PR receives
|
||||
neither App takeover nor the closed-event repair. Rename the head branch for
|
||||
the normal path; if break-glass merge is unavoidable, preserve a safe final
|
||||
message so the protected-main push CI remains the authoritative producer.
|
||||
|
||||
After installing the App, the protected-main push that deploys this workflow
|
||||
runs reconciliation automatically. Approval-signal and admission-workflow
|
||||
completions run the same serialized recovery path. The job enumerates open,
|
||||
ready `main` PRs
|
||||
with any non-App owner, unsafe App metadata, or workflow-skip metadata. It
|
||||
revalidates each base/head, converges a safe request to the exact dedicated-App
|
||||
owner and fixed message, and leaves a workflow-skipping request disabled for
|
||||
manual correction. It never enables auto-merge where the request was already
|
||||
null. Every exact safe App request is then attempted through the synchronous,
|
||||
SHA-bound merge endpoint; a server-declared not-ready result remains open for
|
||||
the next event. A mid-migration failure leaves the affected PR disabled for a
|
||||
fresh routing event or break-glass merge. One PR failure is recorded
|
||||
without preventing later legacy owners from being attempted; the batch ends
|
||||
red with a per-PR summary. Manually dispatch `Reviewer routing` from `main`
|
||||
until the failed count is zero.
|
||||
|
||||
Disabling the App-owned auto-merge request before the reconcile job's final PR
|
||||
read leaves that PR manual-only. That final read is the cancellation
|
||||
linearization point: GitHub's merge API can condition atomically on the head SHA
|
||||
but not on the auto-merge request itself, so a disable racing after that read may
|
||||
lose to an already-issued merge request. To stop an in-flight attempt
|
||||
before the merge endpoint accepts it, close the PR or change its head; if the
|
||||
server observes that state first, it rejects the state/SHA-bound merge. No
|
||||
client-side action can revoke a merge that GitHub has already accepted.
|
||||
The endpoint has no equivalent expected-base parameter. The workflow therefore
|
||||
checks `base=main` and the repository before and after merge and fails any
|
||||
retargeted result, but a retarget racing after the final read cannot be made
|
||||
atomic client-side. Never retarget a PR while its App-owned intent is active:
|
||||
disable the request, wait until all running `Reviewer routing` reconciliation
|
||||
jobs finish, and only then change the base. Preventing a malicious same-instant
|
||||
retarget requires a GitHub-side branch/ruleset control rather than workflow
|
||||
code.
|
||||
|
||||
A PR that introduces or rotates this identity still runs the old base-owned
|
||||
router. Install/configure the App and activate the exact writer ruleset first;
|
||||
this blocks its legacy `github-actions[bot]` request from writing `main`. After
|
||||
the governance PR's final push, disable that old request, confirm the live
|
||||
settings/ruleset contract and all required checks are green for the exact head,
|
||||
then have only `haofeng0705` or `PeterGuy326` merge that head with the
|
||||
repository-generated safe merge message. Verify the resulting merge SHA has a
|
||||
`CI` run with `event=push`,
|
||||
a successful `Coverage` context, and an exact-SHA baseline cache under
|
||||
`refs/heads/main`. Confirm automatic reconciliation reports zero failures and
|
||||
zero non-App owners. Finally use a normal canary PR to verify that the dedicated
|
||||
App is both `enabledBy` and `mergedBy`, and that the same post-merge chain
|
||||
repeats before declaring the rollout complete.
|
||||
|
||||
## Homebrew Formula Delivery
|
||||
|
||||
Official releases use the designated `HOMEBREW_PR_TOKEN` identity to update
|
||||
Official releases use the Release workflow's built-in `GITHUB_TOKEN` to update
|
||||
exactly one tracked Formula after the immutable GitHub assets and their
|
||||
checksums have passed verification. The publisher validates the rendered Ruby,
|
||||
commits only the configured Formula path, never force-pushes `main`, and retries
|
||||
@@ -234,60 +72,11 @@ from a fresh clone up to three times when `main` advances concurrently. Normal
|
||||
stable and beta releases do not create a Formula PR or run a permission
|
||||
canary. The workflow uses the existing repository-scoped
|
||||
`HOMEBREW_PR_TOKEN` release identity because GitHub does not allow its built-in
|
||||
Actions App to bypass this repository's rulesets. Its owner is the designated
|
||||
always-bypass actor for controlled Formula publication and break-glass recovery,
|
||||
including on `main-merge-writers`. The workflow creates the
|
||||
Actions App to bypass this repository's rulesets. That identity is the sole
|
||||
user bypass actor on the two default-branch rulesets. The workflow creates the
|
||||
nine Code Admission checks for the Formula-only commit only after proving its
|
||||
sole parent already has all nine successful checks and the committed Formula
|
||||
exactly matches this release's verified bytes. Formula commits retain
|
||||
`[skip ci]`, so the sealing step exposes only the reviewed commit identity to
|
||||
an independent confirmation job. That job creates the
|
||||
`Coverage Baseline Cache` acknowledgement and emits the reviewed
|
||||
`coverage-baseline-promote` repository dispatch. The default-branch
|
||||
`Coverage Baseline Promotion` workflow independently verifies the exact
|
||||
single-parent Formula commit, both parent and target admission contexts, and
|
||||
default-branch containment before checking out the target. It restores only
|
||||
the exact parent profile, recomputes the complete profile if that cache is
|
||||
absent, and saves the Formula SHA under the `main` cache scope. Because the
|
||||
cache save action treats upload errors as warnings, a second lookup must report
|
||||
`cache-hit=true` for the exact target key before the producer succeeds. The
|
||||
promotion completes the unique acknowledgement, and the confirmation job
|
||||
waits for that exact check-run ID. npm and mirror publication depend only on
|
||||
the immutable release job, so a transient
|
||||
cache-service failure cannot strand an otherwise valid release between
|
||||
channels; the final release-delivery gate still fails until the exact cache is
|
||||
confirmed. Once Formula sealing exposes the target SHA, the confirmation job
|
||||
also runs when a later immutable-package recheck fails, so a post-push failure
|
||||
cannot orphan the producer. Rerun the failed promotion/confirmation path after
|
||||
repairing the producer. Never add a prefix `restore-keys` fallback to this path.
|
||||
|
||||
`Coverage Baseline Repair` is the independent safety net for every merged PR.
|
||||
Its base-owned `pull_request_target: closed` job never checks out or executes PR
|
||||
content: it binds the closed event's PR number and stable head SHA to the
|
||||
current merged-PR facts (`merged_at`, `base.ref`, and `merge_commit_sha`) and
|
||||
proves that merge commit is contained in `main`. It deliberately does not
|
||||
compare REST `base.sha`, because that field follows the live base branch and
|
||||
can move after the merge. Only then does it emit a
|
||||
`coverage-baseline-repair` repository dispatch. Workflow-skip directives alone
|
||||
do not suppress `pull_request_target`, subject to GitHub's separate
|
||||
security-sensitive branch-name restriction described above. The low-trust
|
||||
trigger is forbidden from writing the default-branch cache directly. Before
|
||||
dispatching, it gives Actions event delivery one minute to expose a run from
|
||||
the exact protected `.github/workflows/ci.yml` workflow and exits if that normal producer already
|
||||
owns the SHA, avoiding a duplicate full-suite run. A successful CI producer
|
||||
must hard-verify its exact cache key. If that run instead completes with any
|
||||
non-success conclusion, a separate base-owned `workflow_run` dispatcher binds
|
||||
the exact workflow ID/path, run ID/attempt, conclusion, repository, branch, and
|
||||
head SHA before requesting repair. `workflow_run` also has read-only
|
||||
default-branch cache access, so both dispatchers use the reviewed
|
||||
`repository_dispatch` exception. The dispatched default-branch producer
|
||||
revalidates the corresponding merged-PR or failed-CI identity before checkout,
|
||||
restores only the exact target key, recomputes the complete profile on a miss,
|
||||
and verifies `cache-hit=true` after saving. An hourly schedule refreshes the
|
||||
event-time `main` SHA after direct break-glass pushes or cache eviction;
|
||||
`workflow_dispatch` provides the same current-main repair on demand. The
|
||||
dedicated App identity remains mandatory because events created by the built-in
|
||||
`GITHUB_TOKEN` can suppress both the main push and the closed-PR event.
|
||||
exactly matches this release's verified bytes.
|
||||
|
||||
Keep `HOMEBREW_PR_TOKEN` repository-scoped with `Contents: write` and
|
||||
`Pull requests: write` (the latter remains necessary for withdrawal rollback),
|
||||
|
||||
+16
-206
@@ -147,112 +147,13 @@ maintainer pool. A current-head approval or change request is preserved; after
|
||||
a new push, stale activity does not suppress a fresh request, and an
|
||||
outstanding change requester is preferred for continuity.
|
||||
|
||||
The branch rulesets keep one human approval and all nine strict required
|
||||
contexts, require someone other than the latest pusher to approve after the
|
||||
most recent head update, and restrict `main` updates to the dedicated Reviewer
|
||||
Router App in pull-request mode plus the designated Formula publishers and
|
||||
break-glass identities. Repository auto-merge is enabled for ready PRs, so the
|
||||
App-owned request records the automation intent while the App's synchronous
|
||||
merge path waits for that approval and the current revision's nine green
|
||||
checks. If `main` advances, strict checks rerun before merge. The
|
||||
reviewer routing job uses the built-in `GITHUB_TOKEN` to request reviewers with
|
||||
`Contents: read` and `Pull requests: write`. A separate base-owned cleanup job
|
||||
isolates the merge-authority permissions (`Contents: write` and `Pull
|
||||
requests: write`), revalidates the exact event base/head, and uses the built-in
|
||||
token only to disable an existing request owned by `github-actions[bot]` or one
|
||||
whose title or merge metadata requests that GitHub skip workflows; it never
|
||||
enables auto-merge. The job then mints a current-repository installation token
|
||||
for the dedicated Reviewer Router GitHub App, proves its emitted slug matches
|
||||
the reviewed `REVIEWER_ROUTER_APP_SLUG`, replaces every non-App request, and
|
||||
enables native auto-merge with fixed metadata. This
|
||||
identity boundary is required because GitHub suppresses
|
||||
most workflow events created by the built-in token; using it for auto-merge would
|
||||
silently skip the merge commit's protected-main CI and baseline-cache
|
||||
producer. Token minting or takeover fails closed without falling back to
|
||||
`GITHUB_TOKEN`: the unsafe request is cleared before credentials are read, and
|
||||
the required `Test` context live-verifies the exact `main-merge-writers` update
|
||||
rule. GitHub's read APIs may omit `parameters` for the strict
|
||||
`update_allows_fetch_and_merge: false` value, so the gate accepts only that
|
||||
exact omission or a one-field `parameters` object containing explicit boolean
|
||||
`false`; every other present shape or value fails closed. The gate then binds
|
||||
the same ruleset node through GraphQL and requires its non-null
|
||||
`updateAllowsFetchAndMerge` value to be exactly `false`. It also requires its
|
||||
own built-in token to report
|
||||
`current_user_can_bypass: never`. The minted App separately requires
|
||||
`pull_requests_only` on that writer rule and `never` on every other active main
|
||||
ruleset before it can enable, reconcile, or synchronously merge. The read-only
|
||||
`Test`
|
||||
token may receive a repository projection with both merge-default properties
|
||||
omitted; it accepts only that complete omission or exact `MERGE_MESSAGE` plus
|
||||
`PR_TITLE`/`BLANK`, while partial or malformed projections fail closed.
|
||||
The same unprivileged `pull_request` job may receive an empty repository-variable
|
||||
projection for an external fork. Only when the event head repository differs
|
||||
from the base repository does it substitute the exact reviewed public slug
|
||||
`dingtalk-dws-reviewer-router` for identity comparison. An empty variable on a
|
||||
same-repository PR and every malformed non-empty value still fail closed. This
|
||||
fallback neither mints a token nor grants merge authority; the base-owned
|
||||
Router continues to require its minted App slug to equal the repository
|
||||
variable before any mutation. The minted App's `Contents: write` token must
|
||||
observe the exact reviewed defaults
|
||||
before either mutation path proceeds. GitHub hides the complete
|
||||
`bypass_actors` list from low-privilege callers, so the rollout audit must still
|
||||
keep the writer list at exactly the Reviewer App, `haofeng0705` (ID
|
||||
`30925823`), and `PeterGuy326` (ID `47820304`). The required check finally
|
||||
accepts a null or exact App-owned
|
||||
request after a short takeover grace period. Null is safe from the suppressed
|
||||
event path because the built-in Actions identity cannot update `main`; other
|
||||
permitted identities produce either a main push or the trusted closed-PR
|
||||
repair. Drafts skip the identity step, while `ready_for_review`, `edited`,
|
||||
`auto_merge_enabled`, and `auto_merge_disabled` explicitly start fresh admission
|
||||
for readiness, title, and merge-request changes. Router does not react to
|
||||
`auto_merge_disabled`, so a
|
||||
human can deliberately leave the PR manual-only for break-glass handling.
|
||||
Reviewer routing remains available. The protected-main push that deploys the
|
||||
workflow automatically migrates every open, ready non-App request and repairs
|
||||
unsafe App metadata; it disables workflow-skipping requests for correction.
|
||||
Because GitHub's deferred native auto-merge path does not reliably apply an
|
||||
App's pull-request-only ruleset bypass, a zero-permission approval-signal
|
||||
workflow and completed `CI` / `Code Admission — AI Behavior` workflows wake the
|
||||
same trusted default-branch reconciliation through `workflow_run`. That event
|
||||
is only a wake-up signal: the privileged job does not consume its pull-request
|
||||
payload or artifacts and does not check out the triggering run's code. It
|
||||
re-enumerates open `main` PRs through the API and attempts only an exact
|
||||
App-owned request through the synchronous PR merge endpoint. Immediately before
|
||||
each attempt it revalidates the App's ruleset boundary and PR intent, supplies
|
||||
the current head SHA, and treats server-declared not-ready or
|
||||
concurrent-revision responses as retriable. The live preflight requires the
|
||||
exact repository-owned approval ruleset and exact nine-check strict quality
|
||||
ruleset, with every context bound to the GitHub Actions App
|
||||
(`integration_id=15368`) and the Reviewer Router App unable to bypass either;
|
||||
a missing, disabled, incorrectly sourced, or weakened gate fails closed before
|
||||
merge. GitHub—not the workflow—decides whether the
|
||||
merge is admissible. A staggered twice-hourly schedule provides eventual
|
||||
recovery, and a manual `workflow_dispatch` from `main` is the immediate
|
||||
idempotent retry path.
|
||||
Reconciliation never enables an originally null request. The reviewer router
|
||||
is orchestration, not a quality context, and
|
||||
must not be added to the ruleset.
|
||||
|
||||
Disabling the App-owned request before the reconcile job's final PR read keeps
|
||||
the PR manual-only. GitHub can atomically bind the subsequent merge to the head
|
||||
SHA, but it cannot bind that call to the auto-merge intent; a disable racing
|
||||
after the final read may therefore lose to the in-flight merge. Closing the PR
|
||||
or changing its head blocks the attempt only if GitHub observes that state
|
||||
before accepting the merge endpoint call; no client-side action can revoke a
|
||||
merge that the server has already accepted.
|
||||
|
||||
The merge endpoint has no expected-base precondition. Reconciliation checks
|
||||
that the base is this repository's `main` immediately before and after the call,
|
||||
but a retarget racing after the final read is not atomically preventable in the
|
||||
workflow. Operators must disable the App-owned intent and wait for all running
|
||||
`Reviewer routing` reconciliation jobs to finish before retargeting a PR; a
|
||||
stronger adversarial guarantee requires a GitHub-side branch/ruleset control.
|
||||
|
||||
GitHub may omit `pull_request_target` for security-sensitive head branch names,
|
||||
including names that look like commit SHAs. Those PRs cannot use Router App
|
||||
takeover or the closed-event repair: rename the branch for the supported path,
|
||||
or use the designated break-glass identity with a safe final message so main
|
||||
push CI remains the exact-SHA producer.
|
||||
The branch ruleset keeps one human approval and all nine strict required
|
||||
contexts, and requires someone other than the latest pusher to approve after
|
||||
the most recent head update. Repository auto-merge is enabled for ready PRs,
|
||||
so a PR merges after that approval and the current revision's nine checks are
|
||||
green. If `main` advances, strict checks rerun before merge. The reviewer
|
||||
router is orchestration, not a quality context, and must not be added to the
|
||||
ruleset.
|
||||
|
||||
## Running focused gates locally
|
||||
|
||||
@@ -283,11 +184,11 @@ candidate SHA。
|
||||
`check-interface-baseline.sh` 不再作为本地或 CI 的兼容性审批入口,也不能用于批准
|
||||
flag 迁移。
|
||||
|
||||
Schema compatibility 使用同一组 base、stable、candidate refs,以及 base-owned flag
|
||||
与 command migration ledgers。merge-base-owned checker 分别规范化 merge-base 与
|
||||
stable 的完整 Schema,并让 candidate 对两份历史 contract 独立执行检查;它只把已通过
|
||||
Interface lifecycle 的 exact rename、command move 或 flag extraction 规范化到当前历史
|
||||
副本,不会维护第二份 allowlist,也不会放宽其他 Schema 历史字段。
|
||||
Schema compatibility 使用同一组 base、stable、candidate refs 和同一份 base-owned flag
|
||||
migration ledger。merge-base-owned checker 分别规范化 merge-base 与 stable 的完整
|
||||
Schema,并让 candidate 对两份历史 contract 独立执行检查;它只把已通过 Interface
|
||||
lifecycle 的 exact rename 规范化到当前历史副本,不会维护第二份 allowlist,也不会
|
||||
放宽其他 Schema 历史字段。
|
||||
|
||||
For a release-seal branch that archives rendered fragments:
|
||||
|
||||
@@ -300,90 +201,8 @@ base_ref=$(git merge-base HEAD origin/main)
|
||||
standard PR, CI derives changed packages and their reverse-dependency test
|
||||
closure, then generates candidate and merge-base profiles with the same test
|
||||
scope and `coverpkg`. High-risk and protected-main runs use the complete
|
||||
profiles. The complete candidate profile is produced by disjoint per-shard
|
||||
helper jobs (`scripts/ci/test-packages.sh list-coverage`, kept serial with
|
||||
`-p 1` inside each shard; `verify` proves the shard union equals the
|
||||
full-suite scope exactly once) and concatenated in the aggregate job before
|
||||
enforcement. The complete merge-base profile is restored from an exact-key
|
||||
cache written by the last green `main` push of that same commit (key:
|
||||
merge-base SHA plus resolved Go version); any miss falls back to recomputing
|
||||
it in a merge-base worktree. The trusted `main` producer and PR consumer use
|
||||
the same dedicated cache profile path because GitHub includes that path in the
|
||||
cache version; the runtime-facing candidate and baseline filenames remain
|
||||
separate. Near-miss reuse is forbidden — the caches carry no prefix restore
|
||||
keys, because a neighbouring commit's profile would compare the candidate
|
||||
against the wrong baseline. PR concurrency is keyed by PR number, so a later
|
||||
revision cancels the stale run instead of letting obsolete test matrices
|
||||
compete with the replacement for hosted runners. If cancellation interrupts a
|
||||
cold-cache fallback, the latest run recomputes the same exact merge-base
|
||||
profile authoritatively. Main concurrency remains keyed by pushed SHA, so a
|
||||
newer main push cannot cancel a predecessor's producer.
|
||||
|
||||
Every supported main advancement path has an exact-SHA producer. The required
|
||||
`Test` context rejects GitHub workflow-skip directives in PR and auto-merge
|
||||
metadata, reruns when that metadata is enabled, disabled, or edited, and
|
||||
verifies the live App/writer-ruleset identity contract. Reviewer Router
|
||||
additionally binds auto-merge to the exact head OID and writes a fixed safe
|
||||
merge headline/body. The sole break-glass publisher must retain a safe final
|
||||
message; the release-controlled Formula-only path
|
||||
is the sole supported use of `[skip ci]`. A full source push
|
||||
saves the assembled profile after the aggregate gate passes. A trusted
|
||||
documentation or release-seal push independently verifies that the complete
|
||||
`before...after` diff contains only the reviewed metadata allowlist, restores
|
||||
only the exact `before` cache, recomputes the full profile if the chain is
|
||||
cold, and makes that helper a dependency of the required `Coverage` context.
|
||||
Release-generated Formula commits intentionally retain `[skip ci]`; after
|
||||
their nine synthetic contexts are sealed, an independent release-governance
|
||||
job creates an acknowledgement and emits a `coverage-baseline-promote`
|
||||
repository dispatch. The default-branch promotion
|
||||
workflow revalidates the exact single-parent Formula identity, successful
|
||||
parent and target contexts, and main containment before it promotes the exact
|
||||
parent cache or performs the same full fallback. Every target-main producer
|
||||
follows its save with a lookup-only restore and requires
|
||||
`cache-hit=true` for the exact key; this turns the cache action's otherwise
|
||||
warning-only upload failure or prefix match into a hard failure. Formula
|
||||
promotion additionally updates one release-created `Coverage Baseline Cache`
|
||||
check. A separate confirmation job waits for that exact check-run ID while npm
|
||||
and mirrors remain dependent only on the immutable publication job; cache
|
||||
failure therefore makes the final delivery gate red without creating a
|
||||
partially published release. Once Formula sealing exposes its SHA, a later
|
||||
publication verification failure cannot suppress that confirmation job.
|
||||
|
||||
A separate base-owned `pull_request_target: closed` safety net covers the final
|
||||
merged SHA even if a human or integration changes the merge message after PR
|
||||
checks finish. Skip directives alone do not suppress `pull_request_target`,
|
||||
subject to GitHub's separate security-sensitive branch-name restriction above.
|
||||
That job executes no PR code and only dispatches after binding the exact
|
||||
closed-event PR number and stable head SHA to merged-PR facts
|
||||
(`merged_at`, `base.ref`, and `merge_commit_sha`) and proving `main`
|
||||
containment. It does not compare the later REST `base.sha`, which follows the
|
||||
live base branch after merge. Because GitHub makes default-branch caches
|
||||
read-only to `pull_request_target`, the dispatcher first waits up to one minute
|
||||
for a run from the exact protected
|
||||
`.github/workflows/ci.yml` workflow and exits when that normal producer exists.
|
||||
A successful main CI hard-verifies the exact key itself. A completed
|
||||
non-success run starts a separate base-owned `workflow_run` dispatcher, which
|
||||
binds the exact CI workflow ID/path, run ID/attempt, conclusion, upstream
|
||||
repository, `main` branch, and head SHA. That trigger is also cache-read-only,
|
||||
so either trusted dispatcher uses `repository_dispatch`; its producer
|
||||
revalidates the merged-PR or failed-CI identity, checks out the contained SHA,
|
||||
and produces/verifies the exact full cache.
|
||||
An hourly schedule and a main-only manual dispatch repair the event-time main
|
||||
SHA after a direct break-glass push or cache eviction. The dispatch exception
|
||||
is intentional: unlike an ordinary event created by `GITHUB_TOKEN`, GitHub
|
||||
allows `repository_dispatch` to start another workflow. A legacy built-in-token
|
||||
merge can suppress the closed event too, which is why the required `Test`
|
||||
identity gate and dedicated Reviewer Router App are still mandatory.
|
||||
|
||||
A cold miss can still occur during a producer race or after cache eviction,
|
||||
but it remains fail-safe: the PR recomputes the authoritative baseline with a
|
||||
30-minute job budget and saves a PR-scoped copy for same-PR reruns. It is no
|
||||
longer possible for a supported main-advance path to omit its producer
|
||||
silently. That PR-scoped fallback save remains a best-effort acceleration and
|
||||
does not replace the normal push, metadata, Formula, and merged-PR repair
|
||||
producers. Supporting and (when
|
||||
platform-selected) native profiles are generated before the aggregate
|
||||
`Coverage` context evaluates them. The
|
||||
profiles. Supporting and (when platform-selected) native profiles are
|
||||
generated before the aggregate `Coverage` context evaluates them. The
|
||||
aggregate and native gates require 100% coverage for changed executable Go
|
||||
statements. Overall coverage remains an unrounded, zero-tolerance,
|
||||
scope-matched merge-base non-regression check. Candidate and baseline profiles
|
||||
@@ -414,9 +233,7 @@ tool、parameter、mapping、positional execution、constraint 与 safety 语义
|
||||
|
||||
The `main` quality ruleset must enable strict required-status-check policy
|
||||
(`strict_required_status_checks_policy=true`) so a PR is revalidated whenever
|
||||
`main` advances. Every entry must select the GitHub Actions App
|
||||
(`integration_id=15368`), not “any source”. It must require these exact
|
||||
context/source pairs and no legacy aliases:
|
||||
`main` advances. It must require these exact contexts and no legacy aliases:
|
||||
|
||||
- `Lint`
|
||||
- `Test`
|
||||
@@ -435,11 +252,4 @@ unproducible required context.
|
||||
|
||||
The branch ruleset also requires one approval after the latest push. Enable
|
||||
repository auto-merge and automatic head-branch deletion; keep the base-owned
|
||||
reviewer router outside the required-context list. Install its dedicated
|
||||
GitHub App only on this repository with `Contents: read and write` and `Pull
|
||||
requests: read and write`; do not grant Actions, Workflows, or Administration.
|
||||
Give it pull-request-only bypass on `main-merge-writers` and no bypass on any
|
||||
other ruleset. Store the App client ID and lowercase slug in repository
|
||||
variables `REVIEWER_ROUTER_APP_CLIENT_ID` and `REVIEWER_ROUTER_APP_SLUG`, and
|
||||
its private key in repository secret `REVIEWER_ROUTER_APP_PRIVATE_KEY`. Do not
|
||||
reuse release, Homebrew, or personal tokens for this boundary.
|
||||
reviewer router outside the required-context list.
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
# CLI Help / Schema 兼容迁移治理
|
||||
# CLI flag 兼容迁移治理
|
||||
|
||||
本文定义两种受控 flag 迁移:
|
||||
|
||||
1. `flag_rename`:保留旧 flag 的可执行兼容性,但把它从 Help 与 Agent Schema 中隐藏,并将新的规范 flag 设为唯一可见入口;rename 必须保持原 flag 的 requiredness,optional 只能迁到 optional,required 只能迁到 required。
|
||||
2. `requiredness_change`:同一个公开 flag 从 optional 精确提升为 required;flag 的名称、类型、作用域、可见性、shorthand、`no_opt` 与 alias 关系必须保持不变。
|
||||
|
||||
两种原语都只放行清单精确登记的变化,不是通用 breaking-change 豁免,也不得在同一 command/flag 上叠加以绕过 rename 的 requiredness 保持规则。
|
||||
|
||||
同一套 base-owned lifecycle 也治理两类跨命令迁移:旧命令保留执行能力但从 Help / Schema 导航隐藏,并迁到新的公开命令路径;或把旧命令中的一个可选 flag 拆成新的专用命令。跨命令迁移只允许清单精确声明的 `command_became_hidden` / `flag_became_hidden` 及其 Schema 投影,不是通用 command-path breaking-change 豁免。
|
||||
本文定义一种受控迁移:保留旧 flag 的可执行兼容性,但把它从 Help 与 Agent Schema 中隐藏,并将新的规范 flag 设为唯一可见入口。迁移必须保持原 flag 的 requiredness:optional 只能迁到 optional,required 只能迁到 required。它只解决这一种精确变更,不是通用 breaking-change 豁免。
|
||||
|
||||
同名 flag 的精确类型迁移属于另一类评审机制,只能进入
|
||||
`internal/interfacesnapshot/reviewed.go` 与 legacy smoke helper 的镜像表;flag rename
|
||||
@@ -38,7 +31,7 @@ Smoke fixture,不参与迁移审批。
|
||||
同时提供 `--base` 与 `--stable`;核心 lifecycle 也拒绝缺失 stable 的非空清单,避免
|
||||
调用方因漏传历史参考而提前清理 consumed receipt。
|
||||
|
||||
PR merge-base 同时拥有快照生成器、比较器和已审批清单。门禁用这套 base-owned helper 检查同一个已提交 candidate revision、merge-base 与 stable,candidate 不能通过修改自己的 Go 比较 helper 来放宽规则。candidate 中的清单只参与迁移状态流转,不能批准同一个 PR 引入的接口变化。首次引入 flag 机制时,merge-base 尚无迁移解析器;bootstrap 会用 merge-base 已有的 modern Interface Snapshot 做不带豁免的普通比较,并只接受 candidate 中逐字匹配的空 flag 清单。后续引入 command migration 扩展时,base 已拥有 flag comparator;bootstrap 仍只执行 base-owned 普通比较,不向旧 helper 传入新的 command ledger,因此允许随治理 PR 提交仍处于 before 的 pending 计划,也不会授予任何迁移豁免。bootstrap 无法让旧 helper 证明新治理实现本身正确,因此本治理 PR 的新 parser、lifecycle、launcher 与 hostile tests 仍是必须由真人评审的受保护策略变更;它们合入后才成为后续 PR 的 base-owned authority。
|
||||
PR merge-base 同时拥有快照生成器、比较器和已审批清单。门禁用这套 base-owned helper 检查同一个已提交 candidate revision、merge-base 与 stable,candidate 不能通过修改自己的 Go 比较 helper 来放宽规则。candidate 中的清单只参与迁移状态流转,不能批准同一个 PR 引入的接口变化。首次引入本机制时,merge-base 尚无迁移解析器;bootstrap 会用 merge-base 已有的 modern Interface Snapshot 做不带豁免的普通比较,并只接受 candidate 中逐字匹配的空清单,不会让 candidate 新增的 comparator 决定本 PR 是否兼容。bootstrap 无法让旧 helper 证明新治理实现本身正确,因此本治理 PR 的新 parser、lifecycle、launcher 与 hostile tests 仍是必须由真人评审的受保护策略变更;它们合入后才成为后续 PR 的 base-owned authority。
|
||||
|
||||
这条边界保护比较规则和审批数据,不是任意代码沙箱。GitHub workflow / launcher 的变更仍由仓库保护规则和真人评审负责;candidate Cobra 构建也会执行 candidate 代码,因此对同一 runner 上的主动恶意代码,需要独立进程或文件系统隔离,不能把本门禁描述成已经解决。
|
||||
|
||||
@@ -46,80 +39,22 @@ PR merge-base 同时拥有快照生成器、比较器和已审批清单。门禁
|
||||
|
||||
```text
|
||||
scripts/policy/interface-migrations/approved-flag-migrations-v1.json
|
||||
scripts/policy/interface-migrations/approved-command-migrations-v1.json
|
||||
```
|
||||
|
||||
清单使用严格 JSON 解析:版本、字段名大小写、JSON 值类型、命令路径和 flag 名都必须精确;拒绝重复键、未知键、scalar `null` 与尾随 JSON 值,`reason` 不能为空;禁止 `*`、`?`、前缀规则或其他 wildcard。历史未声明 `kind` 的记录按 `flag_rename` 解释;新增同名 requiredness 迁移必须显式写 `kind: requiredness_change` 和单一 `flag` before/after。清单中的 `pending` 记录只记录已评审计划,并授权其精确列出的后续产品迁移;候选与 merge-base 仍必须精确匹配 `before`,不能授权同一个提交中的接口变化,也不能作为其他命令或参数的通配豁免。
|
||||
|
||||
首次引入一个旧 merge-base 不认识的新 `kind` 时,机制 PR 不得同时写入该 kind 的 pending 记录,因为旧的 base-owned 严格解析器会拒绝未知字段。必须先合入 parser、lifecycle、CLI/Schema adapter 与 hostile tests;待这些实现成为新的 merge-base authority 后,再用独立治理审批 PR 新增 pending,最后才由产品 PR 消费。
|
||||
|
||||
## 跨命令迁移原语
|
||||
|
||||
`approved-command-migrations-v1.json` 只接受两种 `kind`:
|
||||
|
||||
| kind | CLI after 状态 | Schema 允许的精确投影 |
|
||||
|---|---|---|
|
||||
| `command_move` | legacy 命令仍 runnable、由 visible 变 hidden;replacement 由 absent 变 visible runnable | 同一 stable tool identity 的 `primary_cli_path` 改到 replacement;只允许清单列出的参数改名,参数类型、property、requiredness、default 等必须等价 |
|
||||
| `flag_extraction` | legacy 命令保持 visible runnable;指定 legacy flag 仍可执行但由 visible 变 hidden;replacement 由 absent 变 visible runnable | source tool 只删除指定参数;replacement tool 必须位于精确的新路径,并保持 source 的 interface 与 safety identity;清单必须完整列出每个 source 参数到 replacement 参数或常量 property 的承接关系 |
|
||||
|
||||
`command_move` 只能隐藏没有子命令的 legacy leaf,且 legacy 与 replacement
|
||||
不得互为祖先路径;整棵命令树的迁移需要单独设计逐叶治理,不能复用这一原语。
|
||||
稳定 Schema tool 可以继续接受普通的 optional 参数新增,但不得借路径迁移引入清单未登记的
|
||||
`required`、`cli_required` 或 `required_when` 参数;参数改名的目标也不得与历史
|
||||
Schema 中已有的其他参数重名,避免把两个历史参数静默合并。`flag_extraction` 只接受
|
||||
optional bool legacy flag,不能隐藏仍由 Cobra hard-required 的参数。它必须对 source tool
|
||||
的全部历史参数逐项声明:普通参数使用精确 `from` → `to`(同名也必须显式写出),且恰好
|
||||
一个与 legacy flag 同名的 `from` 使用 `replacement_constant`,不得同时声明 `to`;所有
|
||||
`from` 与 replacement 参数/property 目标必须唯一。legacy bool flag 的 `no_opt` 必须等于
|
||||
常量布尔值的字符串形式。v1 只治理 optional bool flag 的 `NoOpt=true` 激活分支,因此
|
||||
`replacement_constant.value` 与 legacy `no_opt` 都必须是 `true`;negative flag、默认即
|
||||
`true` 或固定 `false` 的语义不在本轮证明范围,必须另行设计,不能借本清单放行。
|
||||
|
||||
如果 `command_move` 的参数 `from` 在更早 stable 中仍使用另一历史名称,Schema adapter
|
||||
只能把同一 legacy command 上、已经由 base-owned lifecycle 返回且
|
||||
`state=consumed` 的 flag rename 回执作为前驱边。例如
|
||||
`group → conversation-id` 与 `conversation-id → open-topic-id` 可以组合,但不能把
|
||||
candidate 自增的 pending 记录、其他命令的同名参数、参数概念词典或 CLI alias 当作证据。
|
||||
首次消费 pending command 回执时,merge-base 的 normalized Schema 必须真实发布中间参数,
|
||||
并逐跳验证参数签名和 constraints;command 回执合入为 consumed 后,中间 Schema 已从 main
|
||||
消失,此时保留的两份 consumed 回执可继续对 stable 做受限重放,直到 stable 也达到 after
|
||||
并让回执转为惰性记录或由独立 PR 清理。两种阶段都拒绝残留 predecessor/intermediate、字段漂移、环、分叉、
|
||||
target 碰撞或 primary path/tool identity 不唯一;positionals 不在该组合授权面内。
|
||||
|
||||
`replacement_constant` 不是清单自报即可成立的例外。after 阶段的 Interface Snapshot
|
||||
必须从 replacement 命令的同一份框架运行时声明中捕获完全一致的 property/value,缺失、
|
||||
值不符或额外常量都会使 lifecycle 落入 partial。对于 #1054,`dws chat topic create`
|
||||
必须通过 `NewLeafCommand` 的 `ConstParams` 声明并实际注入
|
||||
`convThreadEnabled=true`;手写 `RunE` 固定值、Cobra annotation 或只改清单都不能提供这份
|
||||
同源证据,Snapshot 只读取 `corecmd` 包内私有注册表公开的只读副本。第一次向旧快照增加
|
||||
bool 常量证据属于 bootstrap;一旦任一历史快照已记录该
|
||||
证据,普通 Interface Compare 会持续要求 property/value 集合完全一致,因此 ledger 清理后
|
||||
删除、翻转或增加常量仍会阻塞。若 candidate 改动 command ledger,则
|
||||
`internal/corecmd/corecmd.go`、`internal/corecmd/interface_const_params.go` 与
|
||||
`internal/helpers/leaf.go` 三份执行/证据桥必须保持 base Git blob 不变;框架演进必须先用
|
||||
独立 PR 合入,不能和产品消费混在一起。
|
||||
|
||||
replacement 必须保留 source 已发布的 dry-run 能力:历史 `dry_run` 非空时不得删除或改值;
|
||||
历史未声明时允许 replacement 新增 dry-run。这与普通 Schema 兼容规则保持同一单调边界。
|
||||
|
||||
两种迁移都要求旧 argv 继续可执行。删除旧命令、删除旧 flag、把 legacy 改成 non-runnable、改变未登记的历史参数、改变 interface / safety,或只完成部分 before → after 转换都会 fail closed。命令别名会先规范到 reference 的 canonical path,但清单本身仍只能记录精确 canonical 命令,不能用 alias 或前缀扩大授权。
|
||||
|
||||
跨命令清单复用下文同一套 `pending → consumed → inert/cleanup` 生命周期。治理 PR 只能新增 `pending` 且产品 surface 必须仍是 before;后续产品 PR 才能一次性切到 after 并改为 `consumed`。candidate 新增的 pending 记录不能批准自己的改动。
|
||||
|
||||
当前首批 pending 记录覆盖 `chat topic` 收口:`chat group create --thread` 拆到 `chat topic create`,以及 `chat message list-topic-replies` / `forward-topic` 迁到对应的 `chat topic` 命令。前一条完整登记 `name` / `type` / `users` 的同名承接,以及 `thread` → `convThreadEnabled=true` 的常量承接。产品 PR 消费这些记录时只能把三条 `state` 改为 `consumed`,不得改写其 before、after、Schema mapping、constant 或 reason。
|
||||
清单使用严格 JSON 解析:版本、字段名大小写、JSON 值类型、命令路径和 flag 名都必须精确;拒绝重复键、未知键、scalar `null` 与尾随 JSON 值,`reason` 不能为空;禁止 `*`、`?`、前缀规则或其他 wildcard。当前清单登记了 IM ID rename 的 `pending` 记录;`pending` 只记录已评审计划,候选与 merge-base 仍必须精确匹配 `before`,因此本治理 PR **不授权 PR #904 或任何产品接口变化**。
|
||||
|
||||
## 两阶段迁移与回执清理
|
||||
|
||||
rename 以 `(kind, command, legacy flag, canonical flag)` 为唯一精确键;requiredness change 以 `(kind, command, flag)` 为唯一精确键。二者经历同一生命周期:
|
||||
每条迁移以 `(command, legacy flag, canonical flag)` 为唯一精确键,并经历以下生命周期:
|
||||
|
||||
| 阶段 | PR 可以做什么 | 必须满足的快照状态 |
|
||||
|---|---|---|
|
||||
| 1. 治理审批 | 新增 `state: pending` 的精确记录;不得在同一个 PR 修改产品 surface | candidate 和 merge-base 都与记录中的 `before` 完全一致;该记录不改变 stable 的判断 |
|
||||
| 2. 产品迁移 | merge-base 已拥有 `pending` 后,按记录一次性切到精确 `after`,并把记录改为 `state: consumed` | rename 的 legacy 仍存在但由 visible 变 hidden,且声明 `alias_of`,canonical requiredness 保持不变;requiredness change 只把同名 flag 从 optional 提升为 required |
|
||||
| 2. 产品迁移 | merge-base 已拥有 `pending` 后,按记录一次性切到精确 `after`,并把记录改为 `state: consumed` | legacy 仍存在但由 visible 变 hidden,且声明 `alias_of`;canonical 的 requiredness 与 legacy 迁移前完全一致 |
|
||||
| 3. 保留回执 | 产品 PR 合入后,如果 stable 仍是 `before`,继续保留 `consumed` | merge-base 或 stable 仍有任一份尚未达到 `after` |
|
||||
| 4. 惰性保留或清理 | 当 merge-base 和 stable 都已经是 `after`,该记录不再提供任何授权;后续 PR 可以原样保留或删除 | 两份参考快照均精确匹配 `after`;保留时仍必须是不可改写的 `consumed`,接口偏离 `after` 继续失败 |
|
||||
| 4. 单独清理 | 当 merge-base 和 stable 都已经是 `after`,在后续 PR 删除该记录 | 两份参考快照均精确匹配 `after`;继续保留过期回执会被门禁拒绝 |
|
||||
|
||||
因此,新增 `pending` 和修改产品 surface 不能发生在同一个 PR;candidate 自己新增的记录不能 self-approve。迁移也不能部分执行:legacy、canonical、`alias_of` 或状态只要有一项不匹配,门禁即失败。stable 发布只会让已经追平的 `consumed` 回执变成无授权效果的审计记录,不会在没有代码变更时让后续业务 PR 失去合规性;清理仍可作为独立的账本压缩动作,但不再是下一个 PR 的强制前置条件。
|
||||
因此,新增 `pending` 和修改产品 surface 不能发生在同一个 PR;candidate 自己新增的记录不能 self-approve。迁移也不能部分执行:legacy、canonical、`alias_of` 或状态只要有一项不匹配,门禁即失败。
|
||||
|
||||
下面只是清单结构示例,不代表已审批命令;实际字段必须从 Interface Snapshot 核对:
|
||||
|
||||
@@ -164,27 +99,6 @@ rename 以 `(kind, command, legacy flag, canonical flag)` 为唯一精确键;r
|
||||
|
||||
产品迁移 PR 必须保持同一条记录的命令、flag、before/after 和 reason 不变,只把 `pending` 改成 `consumed`。
|
||||
|
||||
同名 flag requiredness 迁移的清单结构如下;示例不代表已经审批:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"migrations": [
|
||||
{
|
||||
"kind": "requiredness_change",
|
||||
"command": "dws report entry submit",
|
||||
"flag": {
|
||||
"name": "to-user-ids",
|
||||
"before": {"present": true, "type": "string", "scope": "local"},
|
||||
"after": {"present": true, "type": "string", "required": true, "scope": "local"}
|
||||
},
|
||||
"state": "pending",
|
||||
"reason": "Reject report submissions that have no visible recipient."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## `alias_of` 是框架来源的受评审关系证据
|
||||
|
||||
`alias_of` 不是 Schema 同义词、参数概念词典或任意文字声明。它只能由 `FlagSpec.Aliases` 写入,并与内部 origin `corecmd.flag_spec_aliases.v1` 成对出现;每次 Interface Integrity 都会在已提交的 detached candidate 上执行源码门禁,禁止其他生产文件写入或复刻这些 evidence token。Interface Snapshot 会验证:
|
||||
@@ -205,11 +119,10 @@ rename 以 `(kind, command, legacy flag, canonical flag)` 为唯一精确键;r
|
||||
|
||||
## 豁免边界
|
||||
|
||||
一条 base-owned、状态正确且前后快照精确匹配的记录,只会从普通兼容报告中移除以下三类预期 finding:
|
||||
一条 base-owned、状态正确且前后快照精确匹配的记录,只会从普通兼容报告中移除以下两类预期 finding:
|
||||
|
||||
1. legacy flag 的 `flag_became_hidden`(visible → hidden);
|
||||
2. required legacy 被新增的 required canonical 替代时产生的 `required_flag_added`;如果 canonical 在 before 阶段只是 hidden 占位符,则允许它在转为公开拼写时继承 legacy 的 requiredness。已有的 visible canonical 不允许借 rename 改变 requiredness。
|
||||
3. `requiredness_change` 中同名 flag 从 optional 提升为 required 时产生的 `flag_became_required`。
|
||||
|
||||
以下变化仍按普通兼容规则阻塞,不能被迁移记录掩盖:
|
||||
|
||||
@@ -217,7 +130,6 @@ rename 以 `(kind, command, legacy flag, canonical flag)` 为唯一精确键;r
|
||||
- flag 类型或迁移记录中的 scope、shorthand、`no_opt` 漂移;
|
||||
- `alias_of` 缺失、指向变化或 alias chain;
|
||||
- 命令路径及任何无关的阻塞性接口变化;
|
||||
- requiredness change 同时发生的 rename、隐藏、类型、scope、shorthand、`no_opt` 或 alias 漂移;
|
||||
- 不精确、部分完成、超出记录范围的 surface 变化。
|
||||
|
||||
## Schema 投影边界
|
||||
@@ -245,12 +157,6 @@ adapter 先构造经过上述验证的历史 contract 副本,再调用原 Sche
|
||||
canonical-only `after` 状态时不需要再次投影;adapter 保持 baseline 不变,由原 checker
|
||||
验证 candidate 是否仍与该 canonical contract 兼容。
|
||||
|
||||
`requiredness_change` 的 Schema adapter 只把历史同名 parameter 的 `required` 与
|
||||
`cli_required` 提升到 candidate 的 `true` 值,并要求 candidate 两者都为 `true`。parameter
|
||||
不存在、tool/path 不匹配时不制造 Schema surface;type、property、interface type、default、
|
||||
format、enum、`required_when`、constraints、positionals 与 safety 等全部字段仍交给原 checker,
|
||||
任何不相干漂移继续阻塞。
|
||||
|
||||
## 本地验证
|
||||
|
||||
先确保 merge-base 和 stable tag 已在本地,然后运行与 CI 相同的权威门禁:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Every runtime command the `dws` CLI exposes when loaded with the **pre** environment configuration.
|
||||
|
||||
- **Products**: 13
|
||||
- **Total commands**: 163
|
||||
- **Total commands**: 160
|
||||
- **Generated from**: `internal/plugin` command descriptors — the same code path the CLI uses at runtime.
|
||||
|
||||
> Auto-generated. Update plugin descriptors in `internal/plugin/`, not this file.
|
||||
@@ -33,7 +33,7 @@ Every command inherits these flags (documented here once, not repeated per comma
|
||||
- [`dws aitable` — AI Tables](#dws-aitable) · 41 commands
|
||||
- [`dws attendance` — Attendance](#dws-attendance) · 4 commands
|
||||
- [`dws calendar` — Calendar](#dws-calendar) · 14 commands
|
||||
- [`dws chat` — Group Chat / IM](#dws-chat) · 26 commands
|
||||
- [`dws chat` — Group Chat / IM](#dws-chat) · 23 commands
|
||||
- [`dws contact` — Contact Directory](#dws-contact) · 6 commands
|
||||
- [`dws devdoc` — Open Platform Docs](#dws-devdoc) · 2 commands
|
||||
- [`dws ding` — DING Messages](#dws-ding) · 2 commands
|
||||
@@ -134,15 +134,12 @@ _Calendar events, participants, meeting rooms, and busy-status queries._
|
||||
|
||||
_Group chats, conversations, messages, and robot/webhook integrations._
|
||||
|
||||
**26 commands**
|
||||
**23 commands**
|
||||
|
||||
| Command | Description | When to use |
|
||||
|---|---|---|
|
||||
| `dws chat bot search` | Search robots (bots) created by the current user by keyword. | When the agent needs to resolve one of its own bots by name to a robot code before sending bot messages. |
|
||||
| `dws chat conversation-info` | Retrieve basic metadata for a conversation (single chat or group chat) by conversation ID. | When the agent needs context about a conversation (name, type, member count) before operating on it. |
|
||||
| `dws chat emotion favorite` | Add a media ID to the current user's personal favorite emotions. | When the agent needs to save an available mediaId as a reusable personal emotion, optionally preserving source message context. |
|
||||
| `dws chat emotion list` | List the current user's personal favorite emotions. | When the agent needs to inspect available personal emotions or resolve an emotionId/mediaId before sending. |
|
||||
| `dws chat emotion send` | Send a personal favorite emotion to a group or direct chat as the authenticated user. | When the agent needs to send a known personal emotion mediaId to exactly one group, userId, or openDingTalkId target. |
|
||||
| `dws chat group create` | Create a new internal group chat with a set of initial members. | When the agent needs to spin up a dedicated group for a new project, incident, or discussion thread. |
|
||||
| `dws chat group members` | List members of a group chat; can also be used against the current user to enumerate their groups' members. | When the agent needs the roster of a group before mentioning, removing, or auditing members. |
|
||||
| `dws chat group members add` | Add one or more users to an existing group chat. | When the agent expands a group to include additional participants. |
|
||||
|
||||
@@ -1,454 +0,0 @@
|
||||
# AI 表格数据源指令使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
dws 新增了 7 个 AI 表格数据源同步管理指令,用于将外部数据源(一期支持审批数据)接入 AI 表格,实现数据的自动同步。
|
||||
|
||||
所有指令均通过 `dws aitable +datasource-*` 前缀调用,操作对象是 AI 表格中的"数据源表"——一种由数据源同步创建的特殊数据表。
|
||||
|
||||
## 指令速览
|
||||
|
||||
| 指令 | 用途 | 读写 | 风险 |
|
||||
|------|------|------|------|
|
||||
| `+datasource-list-sources` | 列出数据源类型可用的来源信息(OA 返回 result/processCode、sourceType、sourceUrl) | 读 | low |
|
||||
| `+datasource-get-fields` | 获取数据源来源的可同步字段结构 | 读 | low |
|
||||
| `+datasource-create` | 创建数据源表并触发首次同步 | 写 | medium |
|
||||
| `+datasource-update` | 更新已有数据源表的同步配置 | 写 | medium |
|
||||
| `+datasource-sync` | 手动触发一次同步 | 写 | medium |
|
||||
| `+datasource-sync-status` | 查询同步任务状态 | 读 | low |
|
||||
| `+datasource-get-config` | 查看数据源表配置 | 读 | low |
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. **登录认证**:执行 `dws auth login` 确保已登录
|
||||
2. **获取 Base ID**:通过 `dws aitable +base-list` 或 `dws aitable +base-search --query "关键词"` 获取目标 AI 表格的 Base ID
|
||||
|
||||
---
|
||||
|
||||
## 1. 列出数据源可用来源
|
||||
|
||||
```
|
||||
dws aitable +datasource-list-sources [flags]
|
||||
```
|
||||
|
||||
列出指定数据源类型可用的来源信息。OA 审批类型返回当前 Base 可用的审批数据源条目(`sources` 数组,当前通常为单条),用于构造 `+datasource-create` / `+datasource-update` / `+datasource-get-fields` 的 `--source-config`。OA 场景下每条 source 的 `result` 字段是 JSON 字符串,需解析后得到 `approvals` 数组,再从中提取目标模板的 `processCode`、`name`、`iconUrl`、`url`。
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--base-id` | string | 是 | 目标 Base ID |
|
||||
| `--datasource-type` | string | 是 | 数据源类型,目前支持审批(OA) |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 列出审批数据源来源,获取 result(JSON,解析后得到 approvals[].processCode)
|
||||
dws aitable +datasource-list-sources \
|
||||
--base-id BASE123 \
|
||||
--datasource-type OA
|
||||
```
|
||||
|
||||
### 返回值
|
||||
|
||||
返回 `sources` 数组,每个条目包含:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `result` | OA 审批场景为 JSON 字符串,解析后得到 `approvals` 数组;每个 approval 含 `processCode`、`name`、`iconUrl`、`url` |
|
||||
| `sourceType` | 数据源类型编号(OA 对应内部枚举值 2) |
|
||||
| `sourceUrl` | 数据源访问链接,可选 |
|
||||
|
||||
`result` 本身不是 `processCode`,需要解析出 `approvals` 数组,再取目标模板的 `processCode`、`name`、`iconUrl`、`url` 原样填入 `--source-config`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 获取数据源可同步字段
|
||||
|
||||
```
|
||||
dws aitable +datasource-get-fields [flags]
|
||||
```
|
||||
|
||||
获取指定数据源来源(如某个审批模板)的可同步字段列表,包括字段 ID、字段名称、字段类型和是否主键等信息。用于创建数据源前选择需要同步的字段。
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--base-id` | string | 是 | 目标 Base ID |
|
||||
| `--datasource-type` | string | 是 | 数据源类型,目前支持审批(OA) |
|
||||
| `--source-config` | string | 是 | 源配置 JSON 字符串,结构同 `+datasource-create` 的 `--source-config` |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 获取某审批模板的可同步字段
|
||||
dws aitable +datasource-get-fields \
|
||||
--base-id BASE123 \
|
||||
--datasource-type OA \
|
||||
--source-config '{"processCode":"PROC-XXXX","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"https://example.com/icon.png","url":"https://example.com/oa"}'
|
||||
```
|
||||
|
||||
### 返回值
|
||||
|
||||
返回可同步字段列表,每个字段包含字段 ID、名称、类型和是否主键。字段 ID 可用于 `+datasource-create` / `+datasource-update` 的 `--field-ids` 参数。
|
||||
|
||||
---
|
||||
|
||||
## 3. 创建数据源表
|
||||
|
||||
```
|
||||
dws aitable +datasource-create [flags]
|
||||
```
|
||||
|
||||
为指定 AI 表格创建数据源同步配置,自动创建一张数据源表并触发首次全量同步。
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--base-id` | string | 是 | 目标 Base ID(通过 `+base-list` / `+base-search` 获取) |
|
||||
| `--datasource-type` | string | 是 | 数据源类型,目前支持审批(OA) |
|
||||
| `--source-config` | string | 是 | 源配置 JSON 字符串(格式见下方) |
|
||||
| `--auto` | bool | 否 | 是否开启自动同步,默认 false;无论是否传入,CLI 都会把该字段下发给下游 |
|
||||
| `--auto-sync-setting` | string | 否 | 自动同步频率配置 JSON 字符串,仅在 `--auto=true` 时生效,格式见下方 |
|
||||
| `--field-ids` | stringSlice | 否 | 需要同步的字段 ID 列表,不传时同步全部字段 |
|
||||
|
||||
### source-config 格式(审批类)
|
||||
|
||||
审批数据源的 `--source-config` 是一个 JSON 对象字符串,包含以下字段:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `processCode` | string | 是 | 审批模板编码,对应 `+datasource-list-sources` 返回的 `result` |
|
||||
| `name` | string | 是 | 数据源展示名称,须从 `+datasource-list-sources` 结果原样透传 |
|
||||
| `iconUrl` | string | 是 | OA 审批图标 URL,须从 `+datasource-list-sources` 结果原样透传 |
|
||||
| `url` | string | 是 | OA 审批跳转链接,须从 `+datasource-list-sources` 结果原样透传 |
|
||||
| `dataType` | string | 是 | 数据时间范围类型:`time_range` / `start_time` / `recent_time` |
|
||||
| `recentDays` | string | 当 dataType=recent_time 时必填 | 近 N 天:`7d` / `30d` / `1y` |
|
||||
| `startDate` | string | 当 dataType=time_range 或 start_time 时必填 | 起始日期,格式 `yyyy-MM-dd` |
|
||||
| `endDate` | string | 当 dataType=time_range 时必填 | 结束日期,格式 `yyyy-MM-dd` |
|
||||
| `keepRemovedFields` | bool | 否 | 是否保留已删除字段,默认 false |
|
||||
|
||||
> 注:`splitParentTableField`、`enableDataSyncOaDetailList` 等字段为下游内部字段,无需传入,下游自动处理。
|
||||
|
||||
按 `dataType` 选择对应的时间参数组合:
|
||||
|
||||
| dataType | 需要的时间字段 | 说明 |
|
||||
|----------|----------------|------|
|
||||
| `recent_time` | `recentDays` | 同步近 N 天数据(7d/30d/1y) |
|
||||
| `start_time` | `startDate` | 同步从某日期至今的数据 |
|
||||
| `time_range` | `startDate` + `endDate` | 同步指定日期范围内的数据 |
|
||||
|
||||
### auto-sync-setting 格式
|
||||
|
||||
`--auto-sync-setting` 仅在 `--auto=true` 时生效,用于指定自动同步频率。不传时使用下游默认策略。
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `syncType` | string | 是 | `hourly`(按小时间隔)/ `scheduled`(定时触发) |
|
||||
| `hourlyInterval` | int | hourly 时必填 | 正整数,小时间隔 |
|
||||
| `scheduleType` | string | scheduled 时必填 | `daily` / `weekly` / `monthly` |
|
||||
| `timeValue` | string | scheduled 时必填 | 触发时间,格式 `HH:mm` |
|
||||
| `selectedMonthDays` | int[] | monthly 时必填 | 每月几号触发,1-31 |
|
||||
| `selectedWeekdays` | int[] | weekly 时必填 | 每周哪几天触发,1=周一…7=周日 |
|
||||
| `skipNonWorkingDay` | bool | 否 | 是否跳过非工作日,默认 false |
|
||||
|
||||
示例:`{"syncType":"scheduled","scheduleType":"daily","timeValue":"09:00"}`
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 基本创建——同步近 30 天审批数据
|
||||
dws aitable +datasource-create \
|
||||
--base-id BASE123 \
|
||||
--datasource-type OA \
|
||||
--source-config '{"processCode":"PROC-XXXX","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"https://example.com/icon.png","url":"https://example.com/oa"}'
|
||||
|
||||
# 指定日期范围创建并开启自动同步
|
||||
dws aitable +datasource-create \
|
||||
--base-id BASE123 \
|
||||
--datasource-type OA \
|
||||
--source-config '{"processCode":"PROC-XXXX","name":"采购申请","dataType":"time_range","startDate":"2025-01-01","endDate":"2025-12-31","iconUrl":"https://example.com/icon.png","url":"https://example.com/oa"}' \
|
||||
--auto
|
||||
|
||||
# 指定同步字段(仅同步部分字段,field-ids 可通过 +datasource-get-fields 获取)
|
||||
dws aitable +datasource-create \
|
||||
--base-id BASE123 \
|
||||
--datasource-type OA \
|
||||
--source-config '{"processCode":"PROC-XXXX","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"https://example.com/icon.png","url":"https://example.com/oa"}' \
|
||||
--field-ids fldAAA,fldBBB,fldCCC
|
||||
```
|
||||
|
||||
### 返回值
|
||||
|
||||
创建成功后返回新建数据源表 ID 和同步任务 ID,后续操作需要用到这两个 ID。
|
||||
|
||||
---
|
||||
|
||||
## 4. 更新数据源配置
|
||||
|
||||
```
|
||||
dws aitable +datasource-update [flags]
|
||||
```
|
||||
|
||||
更新已有数据源表的同步配置,支持更新源配置、自动同步开关和同步字段选择。更新后会自动触发一次同步。
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--base-id` | string | 是 | 目标 Base ID |
|
||||
| `--table-id` | string | 是 | 已存在的数据源表 ID(由 `+datasource-create` 返回) |
|
||||
| `--source-config` | string | 否 | 新的源配置 JSON 字符串,不传时保持原有配置。结构同 `+datasource-create` |
|
||||
| `--auto` | bool | 否 | 是否开启自动同步;仅显式设置时下发给下游,省略时保持原有自动同步开关不变 |
|
||||
| `--auto-sync-setting` | string | 否 | 自动同步频率配置 JSON 字符串,仅在显式设置 `--auto=true` 时生效;省略时保持原频率配置 |
|
||||
| `--field-ids` | stringSlice | 否 | 需要同步的字段 ID 列表,不传时保持现有字段配置 |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 更换审批模板并调整时间范围
|
||||
dws aitable +datasource-update \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456 \
|
||||
--source-config '{"processCode":"PROC-YYYY","name":"出差申请","dataType":"recent_time","recentDays":"30d","iconUrl":"https://example.com/icon.png","url":"https://example.com/oa"}'
|
||||
|
||||
# 开启自动同步
|
||||
dws aitable +datasource-update \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456 \
|
||||
--auto
|
||||
|
||||
# 更新同步字段范围
|
||||
dws aitable +datasource-update \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456 \
|
||||
--field-ids fldAAA,fldDDD
|
||||
```
|
||||
|
||||
> 注意:`--table-id` 指向的是数据源表(由 `+datasource-create` 创建),不是普通数据表。
|
||||
|
||||
---
|
||||
|
||||
## 5. 触发手动同步
|
||||
|
||||
```
|
||||
dws aitable +datasource-sync [flags]
|
||||
```
|
||||
|
||||
对已有数据源表触发一次手动同步。单次最多 5 张表,每张表独立提交,部分失败不影响其他表。
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--base-id` | string | 是 | 目标 Base ID |
|
||||
| `--table-ids` | stringSlice | 是 | 待触发同步的数据源表 ID 列表(1-5 个) |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 同步单张表
|
||||
dws aitable +datasource-sync \
|
||||
--base-id BASE123 \
|
||||
--table-ids TBL1
|
||||
|
||||
# 批量同步多张表(逗号分隔,最多 5 个)
|
||||
dws aitable +datasource-sync \
|
||||
--base-id BASE123 \
|
||||
--table-ids TBL1,TBL2,TBL3
|
||||
```
|
||||
|
||||
### 返回值
|
||||
|
||||
返回每个表的同步任务 ID,可通过 `+datasource-sync-status` 查询最终结果。
|
||||
|
||||
---
|
||||
|
||||
## 6. 查询同步状态
|
||||
|
||||
```
|
||||
dws aitable +datasource-sync-status [flags]
|
||||
```
|
||||
|
||||
按任务 ID 查询数据源表的同步任务状态。与 `+datasource-sync` / `+datasource-create` / `+datasource-update` 配对使用——这些指令触发同步后返回任务 ID,本指令通过任务 ID 查询最终结果。
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--base-id` | string | 是 | 目标 Base ID |
|
||||
| `--table-id` | string | 是 | 数据源表 ID |
|
||||
| `--task-ids` | stringSlice | 是 | 待查询的同步任务 ID 列表(1-5 个) |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 按任务 ID 查询(批量,最多 5 个)
|
||||
dws aitable +datasource-sync-status \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456 \
|
||||
--task-ids TASK1,TASK2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 获取数据源配置
|
||||
|
||||
```
|
||||
dws aitable +datasource-get-config [flags]
|
||||
```
|
||||
|
||||
获取指定数据源表的同步配置信息,包括源配置、同步模式、自动同步开关和同步状态。
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--base-id` | string | 是 | 目标 Base ID |
|
||||
| `--table-id` | string | 是 | 数据源表 ID |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
dws aitable +datasource-get-config \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 典型工作流
|
||||
|
||||
### 场景一:从零接入审批数据
|
||||
|
||||
```bash
|
||||
# 0. 获取 Base ID
|
||||
dws aitable +base-search --query "我的项目表"
|
||||
|
||||
# 1. 列出可用审批数据源来源,解析 result JSON 获取 approvals[].processCode
|
||||
dws aitable +datasource-list-sources \
|
||||
--base-id BASE123 \
|
||||
--datasource-type OA
|
||||
# → 返回 sources[0].result 为 JSON 字符串,解析后取 approvals[0].processCode=PROC-XXXX
|
||||
|
||||
# 2. 查看可同步字段(可选,用于指定 field-ids)
|
||||
dws aitable +datasource-get-fields \
|
||||
--base-id BASE123 \
|
||||
--datasource-type OA \
|
||||
--source-config '{"processCode":"PROC-XXXX","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"https://example.com/icon.png","url":"https://example.com/oa"}'
|
||||
|
||||
# 3. 创建数据源表(创建后自动触发首次同步)
|
||||
dws aitable +datasource-create \
|
||||
--base-id BASE123 \
|
||||
--datasource-type OA \
|
||||
--source-config '{"processCode":"PROC-XXXX","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"https://example.com/icon.png","url":"https://example.com/oa"}'
|
||||
# → 返回 tableId=TBL456, taskId=TASK001
|
||||
|
||||
# 4. 查询首次同步是否完成
|
||||
dws aitable +datasource-sync-status \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456 \
|
||||
--task-ids TASK001
|
||||
|
||||
# 5. 确认配置
|
||||
dws aitable +datasource-get-config \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456
|
||||
```
|
||||
|
||||
### 场景二:更换审批模板后重新同步
|
||||
|
||||
```bash
|
||||
# 1. 更新源配置(更新后自动触发一次同步)
|
||||
dws aitable +datasource-update \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456 \
|
||||
--source-config '{"processCode":"PROC-NEW","name":"新审批模板","dataType":"recent_time","recentDays":"30d","iconUrl":"https://example.com/icon.png","url":"https://example.com/oa"}'
|
||||
|
||||
# 2. 查询同步状态(更新后会返回新的 taskId)
|
||||
dws aitable +datasource-sync-status \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456 \
|
||||
--task-ids TASK002
|
||||
```
|
||||
|
||||
### 场景三:手动触发日常同步
|
||||
|
||||
```bash
|
||||
# 仅触发同步,不修改配置
|
||||
dws aitable +datasource-sync \
|
||||
--base-id BASE123 \
|
||||
--table-ids TBL456
|
||||
|
||||
# 查询结果(sync 会返回 taskId)
|
||||
dws aitable +datasource-sync-status \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456 \
|
||||
--task-ids TASK001
|
||||
```
|
||||
|
||||
### 场景四:开启自动同步后确认
|
||||
|
||||
```bash
|
||||
# 1. 更新配置,开启自动同步
|
||||
dws aitable +datasource-update \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456 \
|
||||
--auto
|
||||
|
||||
# 2. 确认配置已更新
|
||||
dws aitable +datasource-get-config \
|
||||
--base-id BASE123 \
|
||||
--table-id TBL456
|
||||
# → 返回中应显示 auto=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 通用选项
|
||||
|
||||
以下全局选项可在所有指令中使用:
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `-f, --format` | 输出格式:json(默认)/ table / raw / pretty / ndjson / csv |
|
||||
| `--jq` | jq 表达式过滤输出(如 `.tableId` 或 `.status`) |
|
||||
| `--fields` | 筛选输出字段(逗号分隔) |
|
||||
| `--dry-run` | 预览操作内容,不实际执行 |
|
||||
| `--profile` | 指定组织或账号 |
|
||||
| `--timeout` | HTTP 请求超时时间(秒,默认 30) |
|
||||
| `--debug` | 显示调试日志 |
|
||||
| `-v, --verbose` | 显示详细日志 |
|
||||
|
||||
### 输出过滤示例
|
||||
|
||||
```bash
|
||||
# 只取 tableId
|
||||
dws aitable +datasource-create ... --jq '.tableId'
|
||||
|
||||
# 只取同步状态
|
||||
dws aitable +datasource-sync-status ... --jq '.status'
|
||||
|
||||
# table 格式查看
|
||||
dws aitable +datasource-get-config ... -f table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **推荐流程**:先 `+datasource-list-sources` 解析 `result` JSON 获取 `approvals[].processCode`,再 `+datasource-get-fields` 查看可同步字段,最后 `+datasource-create` 创建数据源表。
|
||||
|
||||
2. **数据源表 vs 普通数据表**:`+datasource-create` 创建的是"数据源表",它由数据源同步驱动数据写入。`+datasource-update` 和 `+datasource-sync` 仅适用于数据源表,不可对普通数据表使用。
|
||||
|
||||
3. **datasource-type 透传**:CLI 层不对 `--datasource-type` 做枚举校验,目前一期仅支持 `OA`(审批)。后续支持其他类型时由服务端控制,CLI 无需修改。
|
||||
|
||||
4. **source-config 格式**:`--source-config` 必须是合法 JSON 字符串。审批数据源需要原样透传 `processCode`(从 `+datasource-list-sources` 返回的 `result` JSON 中解析 `approvals[]` 提取)、`name`、`iconUrl`、`url`,设置 `dataType`(时间范围类型),并按 `dataType` 提供对应的时间参数(`recentDays` / `startDate` / `endDate`)。
|
||||
|
||||
5. **同步限制**:`+datasource-sync` 单次最多 5 张表;`+datasource-sync-status` 单次最多查询 5 个任务 ID。
|
||||
|
||||
6. **创建即同步**:`+datasource-create` 和 `+datasource-update` 在操作完成后会自动触发一次同步,无需额外调用 `+datasource-sync`。
|
||||
|
||||
7. **自动同步**:`--auto` 开启后,数据源表会按 `--auto-sync-setting` 指定的频率自动定期同步;未指定频率时使用服务端默认策略。关闭 `--auto` 后仅能通过 `+datasource-sync` 手动触发。
|
||||
@@ -92,7 +92,6 @@ command/Leaf 不再写 `dws.schema.risk`;SafetySpec 走类型化 Final 载荷
|
||||
| `Required` / `MarkRequired` | 非空校验 / cobra 硬必填 | 是(`required`) |
|
||||
| `RequiredHint`, `Aliases`, `EnvVar` | 校验提示、隐藏别名、环境回退 | 否(执行细节;别名不上主 parameter 表) |
|
||||
| `ArgDefault`, `Bind`, `OmitEmpty`, `Trim`, `Transform` | toolArgs 装配语义 | 否(载荷细节;`Bind` 可进 property 映射,但不另造 flag) |
|
||||
| `Input` | 额外取值来源:`@path` 读文件 / `-` 读 stdin,在 required/enum/约束/`Validate` 之前原地解析 | 否(今日:能力由作者写进 `Usage` / `SchemaDescription` 文案,是已声明事实而非推断;不另造 flag。目标形态收敛为类型化投影字段,见 RFC §5.3) |
|
||||
|
||||
#### 1.2.2 编排 / 执行字段(不算声明)
|
||||
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# International DingTalk (`.io`) Guide
|
||||
|
||||
This guide explains how to log in to the international DingTalk region and run DWS commands against `*.dingtalk.io` services.
|
||||
|
||||
## Region behavior
|
||||
|
||||
- `dws auth login --intl` creates or refreshes an international login using the `.io` login, OAuth, and MCP services.
|
||||
- Omitting `--intl` keeps the existing domestic `.com` behavior.
|
||||
- `--intl` is a login option, not a global option for business commands. After login, commands such as `contact`, `calendar`, and `doc` derive the region from the selected Token/profile.
|
||||
- Each new Token records its login region. Switching profiles therefore switches the official DingTalk gateway region automatically.
|
||||
- `--international` is a compatibility alias. Prefer `--intl` in new scripts.
|
||||
|
||||
For the complete Chinese guide, see [DWS 国际版(DingTalk `.io`)使用手册](./international-region-guide.zh-CN.md).
|
||||
|
||||
## Check availability
|
||||
|
||||
```bash
|
||||
dws auth login --help
|
||||
```
|
||||
|
||||
The help output must include `--intl` and `--international`.
|
||||
|
||||
When validating a source checkout, build it first and use `./dws` so an older binary on `PATH` is not invoked accidentally:
|
||||
|
||||
```bash
|
||||
make build
|
||||
./dws auth login --help
|
||||
```
|
||||
|
||||
## Log in
|
||||
|
||||
Browser login:
|
||||
|
||||
```bash
|
||||
dws auth login --intl
|
||||
```
|
||||
|
||||
Device flow for SSH, containers, and headless environments:
|
||||
|
||||
```bash
|
||||
dws auth login --intl --device
|
||||
```
|
||||
|
||||
User OAuth with custom application credentials:
|
||||
|
||||
```bash
|
||||
dws auth login --intl \
|
||||
--client-id <APP_KEY> \
|
||||
--client-secret <APP_SECRET>
|
||||
```
|
||||
|
||||
This mode still requires the user to complete OAuth authorization in a browser; it is not a userless `client_credentials` login. The application must be configured on the international developer platform with the required callback and permissions. Never commit an AppSecret to source control or include it in logs.
|
||||
|
||||
## Verify the login
|
||||
|
||||
```bash
|
||||
dws auth status --format json
|
||||
dws profile list --format json
|
||||
dws contact user get-self
|
||||
```
|
||||
|
||||
The last command is a read-only smoke check. If the organization has not enabled CLI access, an organization administrator must enable it or approve the access request on the international developer platform.
|
||||
|
||||
## Use domestic and international profiles together
|
||||
|
||||
```bash
|
||||
# Domestic (.com)
|
||||
dws auth login
|
||||
|
||||
# International (.io)
|
||||
dws auth login --intl
|
||||
|
||||
# Find the stable profile selectors
|
||||
dws profile list --format json
|
||||
```
|
||||
|
||||
Persistently switch profiles:
|
||||
|
||||
```bash
|
||||
dws profile switch <corpId>:<userId>
|
||||
```
|
||||
|
||||
Toggle back to the previous profile:
|
||||
|
||||
```bash
|
||||
dws profile switch -
|
||||
```
|
||||
|
||||
Select a profile for one command without changing the default:
|
||||
|
||||
```bash
|
||||
dws --profile <corpId>:<userId> contact user get-self
|
||||
```
|
||||
|
||||
Do not add `--intl` to business commands. DWS routes official endpoints from the selected profile's Token region.
|
||||
|
||||
## Isolated smoke testing
|
||||
|
||||
Use a separate configuration directory to avoid changing the normal `~/.dws` login state:
|
||||
|
||||
```bash
|
||||
DWS_CONFIG_DIR=/tmp/dws-intl-smoke ./dws auth login --intl
|
||||
DWS_CONFIG_DIR=/tmp/dws-intl-smoke ./dws auth status --format json
|
||||
DWS_CONFIG_DIR=/tmp/dws-intl-smoke ./dws contact user get-self
|
||||
```
|
||||
|
||||
Use the same `DWS_CONFIG_DIR` for every command. Use `./dws` for a source build and `dws` for an installed release.
|
||||
|
||||
## Pre-release overrides (maintainers only)
|
||||
|
||||
Normal international users need only `--intl`; they should not set `--pre-url` or `--mcp-url`.
|
||||
|
||||
Maintainers can test the pre-release login/MCP pair with:
|
||||
|
||||
```bash
|
||||
dws auth login --intl --pre-url https://pre-login.dingtalk.io
|
||||
```
|
||||
|
||||
A corresponding `pre-mcp.*` URL is also accepted, and DWS derives the paired `pre-login.*` / `pre-mcp.*` bases. `--mcp-url` explicitly overrides the MCP base URL for that login.
|
||||
|
||||
Pre-release services may require internal network access or allowlisted accounts. `--pre-url` is intended primarily for the MCP-managed credential flow. Do not combine it with direct custom `--client-id/--client-secret` mode unless the pre-release API contract explicitly supports that combination.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### The browser still opens a `.com` page
|
||||
|
||||
1. Run `dws auth login --help` and confirm `--intl` is present.
|
||||
2. For a source checkout, use `./dws` instead of an older installed binary.
|
||||
3. Confirm the executed command is `dws auth login --intl`.
|
||||
|
||||
### A business command appears to use the wrong region
|
||||
|
||||
Run `dws profile list --format json`, then switch with the exact `<corpId>:<userId>` selector or use the global `--profile` option. For a legacy Token created before region metadata existed, reauthorize it with `dws auth login --intl` for an international account or `dws auth login` for a domestic account.
|
||||
|
||||
### Login succeeds but the command reports missing permission
|
||||
|
||||
This normally means the organization has not enabled CLI access or the application lacks a required permission. It does not by itself indicate a region-routing failure.
|
||||
|
||||
### Should I edit `~/.dws/mcp_url` manually?
|
||||
|
||||
No. Normal users should establish the login with `dws auth login` or `dws auth login --intl`. DWS then routes official endpoints from the selected Token/profile. Manual configuration is reserved for maintainers who explicitly control the target environment.
|
||||
|
||||
## Command reference
|
||||
|
||||
| Scenario | Command |
|
||||
|---|---|
|
||||
| Domestic browser login | `dws auth login` |
|
||||
| International browser login | `dws auth login --intl` |
|
||||
| International device login | `dws auth login --intl --device` |
|
||||
| Check auth state | `dws auth status --format json` |
|
||||
| List profiles | `dws profile list --format json` |
|
||||
| Persistently switch profile | `dws profile switch <corpId>:<userId>` |
|
||||
| Toggle to previous profile | `dws profile switch -` |
|
||||
| Select a profile once | `dws --profile <corpId>:<userId> <command>` |
|
||||
@@ -1,185 +0,0 @@
|
||||
# DWS 国际版(DingTalk `.io`)使用手册
|
||||
|
||||
本手册适用于使用钉钉国际版账号登录并调用国际站服务的用户。
|
||||
|
||||
## 核心规则
|
||||
|
||||
- `dws auth login --intl` 创建或刷新国际版登录,使用 `*.dingtalk.io` 登录、鉴权和 MCP 服务。
|
||||
- 不传 `--intl` 时仍使用国内钉钉 `*.dingtalk.com`,原有链路保持不变。
|
||||
- `--intl` 只用于登录命令。登录完成后,`contact`、`calendar`、`doc` 等业务命令不需要再传该参数。
|
||||
- 每个 Token 会记录登录区域。执行业务命令时,DWS 根据当前或 `--profile` 指定的账号自动选择 `.com` 或 `.io` 网关。
|
||||
- `--international` 是 `--intl` 的兼容别名;新脚本推荐使用较短的 `--intl`。
|
||||
|
||||
## 确认当前版本支持国际版
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
dws auth login --help
|
||||
```
|
||||
|
||||
帮助中应包含:
|
||||
|
||||
```text
|
||||
--intl
|
||||
--international
|
||||
```
|
||||
|
||||
从源码分支验证时,先在仓库根目录构建,并始终使用本次构建的 `./dws`,避免误用系统中已安装的旧版本:
|
||||
|
||||
```bash
|
||||
make build
|
||||
./dws auth login --help
|
||||
```
|
||||
|
||||
## 国际版登录
|
||||
|
||||
### 浏览器登录
|
||||
|
||||
```bash
|
||||
dws auth login --intl
|
||||
```
|
||||
|
||||
DWS 会打开国际版登录页面。完成扫码或账号授权后,登录结果会保存为本机 profile。
|
||||
|
||||
### 设备码登录
|
||||
|
||||
适用于 SSH、容器或没有可用浏览器的环境:
|
||||
|
||||
```bash
|
||||
dws auth login --intl --device
|
||||
```
|
||||
|
||||
按照终端提示,在另一台可打开浏览器的设备上完成授权。
|
||||
|
||||
### 使用自有应用凭证完成用户 OAuth
|
||||
|
||||
```bash
|
||||
dws auth login --intl \
|
||||
--client-id <APP_KEY> \
|
||||
--client-secret <APP_SECRET>
|
||||
```
|
||||
|
||||
该模式仍然需要用户在浏览器中完成 OAuth 授权,不是无用户授权的 `client_credentials` 登录。应用必须在国际版开放平台正确配置回调地址和所需权限。不要在命令历史、日志或 PR 中提交真实的 AppSecret。
|
||||
|
||||
## 验证登录和业务调用
|
||||
|
||||
查看当前登录状态:
|
||||
|
||||
```bash
|
||||
dws auth status --format json
|
||||
```
|
||||
|
||||
列出本机全部账号并找到当前 profile:
|
||||
|
||||
```bash
|
||||
dws profile list --format json
|
||||
```
|
||||
|
||||
执行一个只读命令验证国际链路,例如:
|
||||
|
||||
```bash
|
||||
dws contact user get-self
|
||||
```
|
||||
|
||||
登录状态正常但业务命令提示组织未开通 CLI 时,需要由国际版组织管理员在国际版开发者平台开启 CLI 访问或完成授权审批。
|
||||
|
||||
## 国内版和国际版账号并存
|
||||
|
||||
可以在同一台机器上分别登录国内版和国际版账号:
|
||||
|
||||
```bash
|
||||
# 国内版(.com)
|
||||
dws auth login
|
||||
|
||||
# 国际版(.io)
|
||||
dws auth login --intl
|
||||
|
||||
# 查看稳定的 profile 选择器
|
||||
dws profile list --format json
|
||||
```
|
||||
|
||||
持久切换账号:
|
||||
|
||||
```bash
|
||||
dws profile switch <corpId>:<userId>
|
||||
```
|
||||
|
||||
切回上一个账号:
|
||||
|
||||
```bash
|
||||
dws profile switch -
|
||||
```
|
||||
|
||||
只为单次命令指定账号,不修改默认账号:
|
||||
|
||||
```bash
|
||||
dws --profile <corpId>:<userId> contact user get-self
|
||||
```
|
||||
|
||||
DWS 会按照选中 profile 的 Token 区域自动选择 `.com` 或 `.io`,不需要在业务命令上追加 `--intl`。
|
||||
|
||||
## 使用独立配置目录进行验证
|
||||
|
||||
如果不希望测试登录影响日常使用的 `~/.dws`,可以指定独立配置目录:
|
||||
|
||||
```bash
|
||||
DWS_CONFIG_DIR=/tmp/dws-intl-smoke ./dws auth login --intl
|
||||
DWS_CONFIG_DIR=/tmp/dws-intl-smoke ./dws auth status --format json
|
||||
DWS_CONFIG_DIR=/tmp/dws-intl-smoke ./dws contact user get-self
|
||||
```
|
||||
|
||||
请在三条命令中使用同一个 `DWS_CONFIG_DIR`。验证源码分支时使用 `./dws`;验证已安装版本时可改为 `dws`。
|
||||
|
||||
## 预发参数(仅维护者)
|
||||
|
||||
普通国际版用户只需要 `--intl`,不要配置 `--pre-url` 或 `--mcp-url`。
|
||||
|
||||
维护者验证预发登录/MCP 链路时可以使用:
|
||||
|
||||
```bash
|
||||
dws auth login --intl --pre-url https://pre-login.dingtalk.io
|
||||
```
|
||||
|
||||
也可以传入对应的 `pre-mcp.*` 地址;DWS 会推导配套的 `pre-login.*` / `pre-mcp.*` 地址。`--mcp-url` 用于显式覆盖本次登录的 MCP base URL。
|
||||
|
||||
预发环境可能只对内网或特定测试账号开放。`--pre-url` 主要服务于 MCP 托管凭证登录流程;除非预发 API 契约已经明确支持,否则不要把它与自有 `--client-id/--client-secret` 直连模式组合使用。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 仍然打开 `.com` 登录页面
|
||||
|
||||
1. 运行 `dws auth login --help`,确认当前二进制包含 `--intl`。
|
||||
2. 从源码验证时使用 `./dws`,不要误用 PATH 中的旧版本。
|
||||
3. 确认实际执行的是 `dws auth login --intl`,而不是普通 `dws auth login`。
|
||||
|
||||
### 业务命令似乎使用了错误区域
|
||||
|
||||
先检查当前账号:
|
||||
|
||||
```bash
|
||||
dws profile list --format json
|
||||
```
|
||||
|
||||
然后使用精确的 `<corpId>:<userId>` 切换或通过全局 `--profile` 单次指定。对于在区域字段引入前生成的历史 Token,建议使用正确的登录方式重新授权:国际账号执行 `dws auth login --intl`,国内账号执行 `dws auth login`。
|
||||
|
||||
### 登录成功但提示没有权限
|
||||
|
||||
这通常是组织 CLI 准入或应用授权问题,不代表区域路由失败。请确认目标组织已开启 CLI 访问,并且当前应用拥有命令所需权限。
|
||||
|
||||
### 是否需要手工修改 `~/.dws/mcp_url`
|
||||
|
||||
不需要。正常使用应通过 `dws auth login` 或 `dws auth login --intl` 建立登录态;业务命令会根据选中的 Token/profile 自动路由。手工修改配置只适用于明确了解目标环境的维护者调试场景。
|
||||
|
||||
## 命令速查
|
||||
|
||||
| 场景 | 命令 |
|
||||
|---|---|
|
||||
| 国内版浏览器登录 | `dws auth login` |
|
||||
| 国际版浏览器登录 | `dws auth login --intl` |
|
||||
| 国际版设备码登录 | `dws auth login --intl --device` |
|
||||
| 查看登录状态 | `dws auth status --format json` |
|
||||
| 查看所有账号 | `dws profile list --format json` |
|
||||
| 持久切换账号 | `dws profile switch <corpId>:<userId>` |
|
||||
| 切回上一个账号 | `dws profile switch -` |
|
||||
| 单次指定账号 | `dws --profile <corpId>:<userId> <command>` |
|
||||
+5
-10
@@ -23,7 +23,7 @@
|
||||
|
||||
`plan` 是纯只读操作,不创建 tag、预留版本号或生成包。CHANGELOG 合入期间若另一个发布先占用了该版本,`publish` 会重新分配并因 CHANGELOG 章节不匹配而拒绝,需要重新 plan。`publish` 会先再次确认 dispatch SHA 仍是当前 `main`、Code Admission 和平台治理均通过,再由唯一的 write job 使用 GitHub API 原子创建 annotated tag;同一次 run 随即进入既有的跨平台构建、GitHub/npm、可选 OSS/Gitee 发布和 Homebrew 直交付 DAG。内置 `GITHUB_TOKEN` 创建的 tag 不依赖第二条 workflow 被再次触发。
|
||||
|
||||
为缩短封板前后的关键路径,`publish` 的只读版本规划会与平台治理检查并行,seal 仍严格等待二者成功;plan 在 candidate annotated tag 上验证过的 contract 和 stable/beta baseline 会绑定进 seal,并由 seal 后的 tag authority 检查复用。Code Admission 状态与 immutable-releases 治理仍会在 seal 后再次读取,避免 preflight 与发布之间的状态变化被忽略。随后三类只读门禁(release automation、CLI 与 Schema 兼容性、multi-profile E2E)与 GoReleaser 构建并行;Node/archive 等仅供后处理使用的工具也延后到构建完成后安装。并行和已验证结果复用只改变调度,不降低发布门禁:任何一条验证失败都会阻止 GitHub Release、npm、镜像和 Homebrew 发布,delivery proof 也要求三条验证 job 全部成功。
|
||||
为缩短封板前后的关键路径,`publish` 的只读版本规划会与平台治理检查并行,seal 仍严格等待二者成功;plan 在 candidate annotated tag 上验证过的 contract 和 stable/beta baseline 会绑定进 seal,并由 seal 后的 tag authority 检查复用。Code Admission 状态与 immutable-releases 治理仍会在 seal 后再次读取,避免 preflight 与发布之间的状态变化被忽略。随后三类只读门禁(release automation、命令兼容性、multi-profile E2E)与 GoReleaser 构建并行;Node/archive 等仅供后处理使用的工具也延后到构建完成后安装。并行和已验证结果复用只改变调度,不降低发布门禁:任何一条验证失败都会阻止 GitHub Release、npm、镜像和 Homebrew 发布,delivery proof 也要求三条验证 job 全部成功。
|
||||
|
||||
OSS 镜像默认不参与发布 DAG,适用于尚未创建 Bucket 的仓库。云端封板会把当时的仓库变量 `ENABLE_OSS_MIRROR=true` 记录为不可变 tag 元数据 `OSS-Mirror: enabled`,否则记录为 `deferred`;后续发布和撤回只读取该 sealed policy,不读取变量的当前值。`enabled` 继续对缺失凭据、无效 Bucket、上传、pointer 和撤回失败保持 fail-closed;`deferred` 明确跳过不存在的渠道。为避免补发后撤回遗漏,deferred 版本暂不接受 `repair_oss_version`,启用 OSS 只影响后续新 tag,直到补齐可审计的不可变 repair 证明。
|
||||
|
||||
@@ -102,7 +102,7 @@ fragments,然后停止。审阅生成内容并通过唯一的 release-seal PR
|
||||
dws-release v1.2.3-beta.1
|
||||
```
|
||||
|
||||
预检包含测试、策略检查、旧正式版 CLI 与 Schema 双基线兼容检查、全平台打包、npm 安装验证,以及 macOS 环境下的 Homebrew 安装验证。它还会从默认分支触发一次无发布权限的 `Release governance preflight`,用正式流水线相同的身份检查该精确 commit 的九个 Code Admission context 和 immutable releases。通过后回到上述 Actions 页面选择 beta 和 `release_operation=publish`;云端会重新绑定当前 `main`,然后直接进入 beta 自动发布,不需要人工审批或输入确认短语。
|
||||
预检包含测试、策略检查、旧正式版命令树兼容检查、全平台打包、npm 安装验证,以及 macOS 环境下的 Homebrew 安装验证。它还会从默认分支触发一次无发布权限的 `Release governance preflight`,用正式流水线相同的身份检查该精确 commit 的九个 Code Admission context 和 immutable releases。通过后回到上述 Actions 页面选择 beta 和 `release_operation=publish`;云端会重新绑定当前 `main`,然后直接进入 beta 自动发布,不需要人工审批或输入确认短语。
|
||||
|
||||
## 正式发布
|
||||
|
||||
@@ -140,19 +140,14 @@ dws-release v1.2.3 --from-beta v1.2.3-beta.1
|
||||
`.changes/<unique-name>.md` 中增加一个独立 fragment;格式和允许的分类见
|
||||
[`.changes/README.md`](../.changes/README.md)。预发封板时
|
||||
`scripts/release/prepare-changelog.sh prerelease <version>` 会稳定排序并汇总所有未归档
|
||||
fragment,写入唯一版本章节后移动到 `.changes/released/<version>/`。如果 beta 发布后又有
|
||||
带 fragment 的 PR 合入,而维护者决定直接发布 stable,
|
||||
`scripts/release/prepare-changelog.sh stable <version> --from-beta <tag>` 会保留 beta 晋级摘要
|
||||
模板,并把这些 post-beta fragments 写到明确的 `Changes since <beta>` 边界之后,再移动到
|
||||
`.changes/released/<stable-version>/`。没有 active fragment 时,stable 仍只生成原有晋级摘要
|
||||
模板。因此并发 PR 不会争用 `CHANGELOG.md`;唯一的 release-seal PR 同时提交生成的章节与
|
||||
归档移动,供审计复核。
|
||||
fragment,写入唯一版本章节后移动到 `.changes/released/<version>/`。因此并发 PR 不会争用
|
||||
`CHANGELOG.md`;唯一的 release-seal PR 同时提交生成的章节与归档移动,供审计复核。
|
||||
|
||||
## CI/CD 保证
|
||||
|
||||
- 只接受 `vX.Y.Z-beta.N` 和 `vX.Y.Z`,且新版本必须高于上一正式版。这里的“上一正式版”必须同时具备公开非草稿 GitHub Release 和同 tag/commit 的成功 Release workflow;只有 tag、没有交付成功的孤儿版本会阻断后续发布,要求走机器核验恢复补齐。云端 tag 会固定 `Release-Run`、requester、commit 和版本分配指纹,交付验证按该精确 run/attempt 及完整 job graph 取证,不接受任意 `workflow_dispatch`。历史版本若曾通过专用 recovery workflow 完成交付,只能使用仓库内 `delivered-stable-recoveries.json` 中精确到 tag、commit、run、workflow SHA 与 attempt 的 reviewed 证据。
|
||||
- tag 必须由云端 seal job 创建为 annotated tag;封板提交必须已通过 PR 合入并包含在远端 `main` 历史中。流水线允许其后 `main` 继续前进,但始终要求封板提交位于 `main` 历史中。
|
||||
- 日常 CI 和发布前都会对比“最新已交付正式版”的完整 CLI 与 Schema 契约;若长时间预检期间该 baseline 发生变化,会针对新的 baseline 重新比较。
|
||||
- 日常 CI 和发布前都会对比“最新已交付正式版”的完整命令树;若长时间预检期间该 baseline 发生变化,会针对新的 baseline 重新比较。
|
||||
- GoReleaser 只构建;Darwin 重签、checksums 重算和 npm 安装验证通过后,才统一上传 GitHub Release 的最终产物。
|
||||
- 六个平台归档会逐个解包并核验二进制内嵌版本;公开资产集合、checksums 集合和 npm tarball integrity 都必须精确一致。npm tarball 固定由 npm `10.9.2` 打包,避免重跑时因 runner 自带 npm 漂移产生不同字节。
|
||||
- stable 发布到 npm `latest`;prerelease 发布到 npm `beta`。启用 `ENABLE_OSS_MIRROR=true` 后,stable 同步 OSS `latest.txt` 和共享安装脚本,prerelease 只同步 OSS `beta.txt`,不会覆盖稳定入口。
|
||||
|
||||
@@ -274,7 +274,6 @@ Definition(仅声明;不可编译)
|
||||
| 层 | 含义 | 今日落点 |
|
||||
|---|---|---|
|
||||
| **声明(declare)** | `corecmd.Spec` / `LeafSpec` / `ContractDecl` **数据字段**(声明证据;交付见下) | `Flags`/`Constraints`/`Risk`/`ConstParams`/`Contract`;类型真身在 `corecmd/contract`(DTO:`SafetySpec`/`ParamDecl`/`ProductDecl`/`ContractFinalPayload`;**无** Cobra store) |
|
||||
| **非叶声明(group declare)** | owning Cobra 命令上的完整 `corecmd.GroupPolicy`;不是 leaf `Spec` 字段 | `Mode` / `Positionals` / `Recovery` 经 `corecmd.ApplyGroupPolicy` 一次编译为 Cobra 行为与私有框架元数据 |
|
||||
| **框架转换** | 类型转换并注册(**禁止** JSON 注解桥) | `embedContractDecl` → `corecmd/contractfinal.RegisterRuntimeContractFinal`(annotate + store;全部调用方直调,`corecmd.New` 内部注册) |
|
||||
| **注解 seam** | Cobra `dws.schema.*` 写入 | `internal/corecmd/runtimeannotate.AnnotateRuntime*`(框架侧;`cli` 根经 `runtime_schema_seam.go` 包内别名访问;`cli/runtimeannotate` 垫片包已删,一律直引 corecmd) |
|
||||
| **Schema 透传** / 交付 | 组装读取注册表,原样投影为 `ToolSpec`;`RegisterSchemaSourceRoot` → `ResolveSchemaBuild`(`ResolveMeta` 自同一组装投影);go:embed 仅限 reviewed 输入(MCP meta / `param_concepts` 等;reviewed `schema_command_registry/` 已退役,identity 由 collector 收集),映射排除走 Go ledger(`schema_parameter_mapping_ledger.go`),不得 embed Catalog | `internal/cli` 根(交付边界);ContractFinal store 在 `corecmd/contractfinal`(`cli` 根经 `runtime_schema_seam.go` 包内别名访问;`cli/contractfinal` 垫片包已删) |
|
||||
@@ -293,7 +292,7 @@ Definition(仅声明;不可编译)
|
||||
|
||||
下列字段**是**框架声明面(经 `corecmd.New` 生效并嵌入 `dws.schema.*`):
|
||||
|
||||
- `Flags`(含 Name/Kind/Default/Required/MarkRequired/Usage 等注册面;`Input` 是取值来源声明,经 `corecmd.New` 生效但**不**嵌入 `dws.schema.*`,能力靠 `Usage` 文案声明,见 §5.3)
|
||||
- `Flags`(含 Name/Kind/Default/Required/MarkRequired/Usage 等注册面)
|
||||
- `Constraints`
|
||||
- **非空** `Risk`(空值 = 运行时当只读确认,且**不**嵌入 `dws.schema.risk`)
|
||||
- `ConstParams`(载荷声明;不上用户 flag 表)
|
||||
@@ -311,25 +310,7 @@ Definition(仅声明;不可编译)
|
||||
3. 写副作用:新 Leaf 声明完整 `SafetySpec`(框架 `ConfirmSafety` + Schema Final);未迁移旧路径显式标注 `runtime_gate`;二者皆无则不合格;
|
||||
4. Schema `ToolSpec` 全字段组均落在 §5.0.4 表中某一权威格,禁止无主字段。
|
||||
|
||||
#### 5.0.2a 非叶命令契约(`corecmd.GroupPolicy`)
|
||||
|
||||
`corecmd.Spec` / `LeafSpec` 继续只定义叶命令。每个拥有子命令的 owning Cobra 命令必须在构造处通过 `corecmd.ApplyGroupPolicy` 声明一份完整 `GroupPolicy`:
|
||||
|
||||
| 轴 | 允许值 | 语义 |
|
||||
|---|---|---|
|
||||
| `Mode` | `navigation_only` / `hybrid` | 仅导航并展示帮助,或同时保留本命令业务执行 |
|
||||
| `Positionals` | `reject` / `allow` | 未匹配 token 进入命令恢复,或由本命令业务位置参数消费 |
|
||||
| `Recovery` | `sibling` / `deep` / `disabled` | 只建议直接子命令、显式允许后代路径恢复,或完全关闭恢复 |
|
||||
|
||||
硬规则:
|
||||
|
||||
1. 三个字段必须同时声明;全零值只表示 leaf,不能应用到命令。`navigation_only` 必须 `Positionals=reject`;`Positionals=allow` 必须 `Recovery=disabled`,避免业务 argv 与命令恢复争抢同一 token。
|
||||
2. `ApplyGroupPolicy` 是唯一编译入口:navigation 安装统一 help/错误 handler;hybrid 保留 owning `RunE`,仅在声明拒绝 positionals 且开启恢复时包裹 unknown-command 分支。恢复统一投影为有界 `CommandResolution`(最多 3 个建议 + 当前 parent `--help`);只有 `Recovery=deep` 才可建议完整后代路径。
|
||||
3. `GroupPolicy` **不推导** `TraverseChildren`。该 Cobra 字段会改变父级 local flag 是否向子命令传播,必须由原 owning command 显式保留,不能因迁移到 typo guidance 而扩大参数表面。
|
||||
4. 最终装配树门禁检查「有 children 必须有 GroupPolicy、leaf 不得残留 GroupPolicy、navigation/hybrid handler 与声明结构一致」。门禁不执行任意 `Args` 函数;`ApplyGroupPolicy` 对 `cobra.NoArgs` / `cobra.ArbitraryArgs` 的编译由 corecmd 单测覆盖。
|
||||
5. 命令树合并时,两侧非空 group 都必须先声明 policy;冲突声明、group 与 runnable/parse-bearing leaf 合并、或带 children 的未声明节点均 fail closed。纯 metadata 空壳可采用 typed source policy,不能借此吞掉 flags、hooks 或执行体。
|
||||
|
||||
#### 5.0.2b 三档叶声明路径(Tier1 / Tier2 / Tier3)
|
||||
#### 5.0.2a 三档声明路径(Tier1 / Tier2 / Tier3)
|
||||
|
||||
当前生产允许的三档路径(同一 `ContractFinal` 语义;不是互相否定):
|
||||
|
||||
@@ -692,52 +673,6 @@ func (k Key[T]) Declare(opts ...FlagOption[T]) FlagSpec
|
||||
- 构造时拒绝 `InputSourceInvalid`。
|
||||
- 当前没有任何 Shortcut 或 Leaf 声明 `Input`,因此 M1 增加能力且零上线表面变化。让现有命令采用它属于 §9 下的用户可见变更。
|
||||
|
||||
`Input` 的框架能力今日已在 `corecmd` 落地(声明即执行的过渡形态,语义与上文目标一致),使用指南:
|
||||
|
||||
**今日声明形态**:`FlagSpec.Input []string`,源常量 `corecmd.InputFile`(`"file"`)/ `corecmd.InputStdin`(`"stdin"`)。`helpers.LeafFlag` 是 `corecmd.FlagSpec` 别名,直接可用;`shortcut.Flag.Input` 同形声明,经 `FromShortcut` 映射到 `FlagSpec`。
|
||||
|
||||
```go
|
||||
// LeafSpec / helpers
|
||||
Flags: []helpers.LeafFlag{
|
||||
{
|
||||
Name: "content",
|
||||
Usage: "文档内容(支持 @文件路径 或 - 读 stdin)",
|
||||
Bind: "content",
|
||||
Input: []string{corecmd.InputFile, corecmd.InputStdin},
|
||||
},
|
||||
}
|
||||
|
||||
// shortcut
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "markdown", Desc: "Markdown 内容(支持 @文件路径 或 -)",
|
||||
Input: []string{"file", "stdin"}},
|
||||
}
|
||||
```
|
||||
|
||||
**运行时语义**(`resolveInputFlags`,在 `runDeclaredPreflight` 内、required/enum/约束/Validate 之前执行,原地改写 cobra flag 值):
|
||||
|
||||
- `--flag @path`:文件内容替换取值;`--flag -`:stdin 内容替换取值。
|
||||
- `--flag @@value`:转义为字面 `@value`,不做来源解析。
|
||||
- 只解析显式 CLI token(主名或别名);EnvVar 回落与注册默认值透传不解析。
|
||||
- 内容前置剥离 UTF-8 BOM;`Trim` 等既有语义照常作用于解析后的值。
|
||||
- 读取失败、源不支持、`@` 后空路径都是类型化校验错误(退出码 3);同时声明两种源而文件读取失败时附 stdin 引导 hint。
|
||||
|
||||
**作者守则**:
|
||||
|
||||
- 声明即全部能力:required/enum/约束/Validate 校验的已是解析后的真实内容,`Execute`/`Invoke` 无需任何额外代码。
|
||||
- `Usage`/`Desc` 必须写明支持 `@路径`/`-`;框架不自动改写 help 文案,今日也不向 Schema 投影(新增投影字段须先过 homology 评审,避免 catalog drift)。
|
||||
- `user_required` 确认的写命令若声明 `InputStdin`:stdin 在校验阶段被消费,交互确认将 fail-closed 为 `confirmation_required`,此类调用必须显式 `--yes`(或 `--dry-run`)。
|
||||
- **声明前先确认取值空间不会被前缀吃掉**:声明 `InputFile` 后,任何以 `@` 开头的合法值都会被当成文件路径(本产品尤其常见的是 at 提及类取值,如 `--at-user @zhangsan` 会报读取文件失败),用户只能改用 `@@` 转义;声明 `InputStdin` 后字面值 `-` 不可达(与 curl 等约定一致)。若该 flag 的正常取值可能命中这两种形态,就不要声明对应来源。
|
||||
- 声明在构造期校验(fail-closed panic):仅限 `KindString`;源值必须是 `file`/`stdin` 且不重复。
|
||||
|
||||
**今日实现与目标形态的差异**(迁移到本节目标 `FlagSpec` 时收敛):
|
||||
|
||||
| 维度 | 今日 | 目标 |
|
||||
|---|---|---|
|
||||
| 源类型 | `[]string` 常量 | 类型化 `InputSource` |
|
||||
| 路径边界 | 直接本地文件 IO | 复用 §5.5.2 本地文件 effect 边界 |
|
||||
| Schema 投影 | 无(靠作者在 Usage 声明) | 声明即最终源,随 Catalog 透传 |
|
||||
|
||||
核心 FlagSpec 故意没有:
|
||||
|
||||
- `Bind`;
|
||||
|
||||
@@ -54,8 +54,8 @@ DWS 对任何外部实现的持续兼容义务。后续设计以 DWS 自身约
|
||||
|
||||
| 模式 | Agent 目录布局 | 选择方式 |
|
||||
|---|---|---|
|
||||
| multi(默认) | canonical `~/.agents/skills/dingtalk-*/`;非 universal Agent 使用链接或复制兼容层 | 默认;`dws skill setup --mode multi` |
|
||||
| mono(兼容) | canonical `~/.agents/skills/dws/`;非 universal Agent 使用链接或复制兼容层 | `dws skill setup --mode mono` 或安装器的 mono opt-in |
|
||||
| multi(默认) | `<agent-home>/dingtalk-*/` 与必选 `dingtalk-shared/` | 默认;`dws skill setup --mode multi` |
|
||||
| mono(兼容) | `<agent-home>/dws/` | `dws skill setup --mode mono` 或安装器的 mono opt-in |
|
||||
|
||||
模式切换通过重新执行 setup 完成。安装 multi 前备份并移除 mono 的 `dws/`;安装
|
||||
mono 前只备份并移除能够证明由 DWS 管理的 multi 目录。两个方向都不提供隐式、
|
||||
@@ -140,26 +140,10 @@ Agent 仍只需以 `SKILL.md` 发现和加载 Skill;统一元数据位于 Agen
|
||||
|
||||
## 8. Upgrade 与恢复语义
|
||||
|
||||
升级器始终先发布 `~/.agents/skills` canonical 集合。固定兼容注册表中被分类为
|
||||
universal 的 Agent 不再保留 Agent 私有副本;检测到的
|
||||
非 universal Agent(如 Claude、OpenClaw、Hermes、Windsurf)使用指向 canonical
|
||||
的目录链接:npm 与 PowerShell 安装器在 Windows 上创建 junction,`dws upgrade` /
|
||||
`dws skill setup` 创建符号链接(`os.Symlink`)。链接不可用时回退为内容完整的
|
||||
直接复制,包括未开启开发者模式、因而无法创建符号链接的 Windows。
|
||||
自定义 `CODEX_HOME`、`CLAUDE_CONFIG_DIR`、`HERMES_HOME`、`AUTOHAND_HOME`、
|
||||
`GROK_HOME`、`VIBE_HOME`、`XDG_CONFIG_HOME` 与 OpenClaw 历史目录 `.clawdbot`、
|
||||
`.moltbot` 必须按 Agent 实际优先级解析。
|
||||
升级器对每个 Agent 目标执行:
|
||||
|
||||
Agent 兼容矩阵以 `vercel-labs/skills` 的 `agents.ts` 与 `installer.ts`(基准提交
|
||||
`c6f69c6`)为契约:76 个 ID 必须完整登记,其中 19 个 universal、57 个
|
||||
non-universal。`eve`、`promptscript` 没有全局目录,因此全局安装时跳过;多个 Agent
|
||||
解析到同一个 XDG 目录时按最终绝对路径去重(Windows 大小写不敏感)。DWS 额外支持
|
||||
Qoderwork(按 non-universal Agent 建立兼容链接);旧版使用的 `.github/skills`、
|
||||
`.amp/skills`、`.cline/skills` 与
|
||||
`.windsurf/skills` 仅作为可恢复迁移清理目标,不计入上游 Agent 枚举。
|
||||
对 universal Agent,上游 installer 的 global 模式明确选择 canonical 并跳过
|
||||
Agent 私有 global 目录;注册表中的 `globalSkillsDir` 仍用于识别和退役历史 native
|
||||
路径,不作为 universal symlink 模式的发布目标。
|
||||
- 先探测具体 Agent home;只在没有任何具体 Agent 时使用 `~/.agents/skills` 通用 fallback;
|
||||
- 具体 Agent 安装成功后,将 `~/.agents/skills` 中旧的 DWS 受管副本可恢复地迁入备份,避免 Codex 等同时扫描两个根目录时重复发现同名 Skill;
|
||||
|
||||
1. 只读计算对面布局、过期受管 Skill 和同名官方 Skill;
|
||||
2. 在目标文件系统的 staging 中复制完整新集合;
|
||||
@@ -167,14 +151,6 @@ Agent 私有 global 目录;注册表中的 `globalSkillsDir` 仍用于识别
|
||||
4. 逐项发布 staging;任一发布失败时删除已发布的新目录,并逆序恢复该目标的全部旧目录;
|
||||
5. 仅在没有目标失败且至少一个目标成功时更新状态快照。
|
||||
|
||||
旧集合可能位于外部卷或自定义 Agent 根,而备份固定写入
|
||||
`~/.dws/skill-backups`。因此备份与反向恢复统一采用 rename-first:同卷直接原子
|
||||
rename;遇到跨文件系统错误时,在目标所在文件系统创建临时 staging,词法复制并
|
||||
保留目录/文件权限、普通文件、符号链接及 dangling symlink,校验路径类型、目录项、
|
||||
文件大小与 SHA256、链接目标后,再将 staging 原子 rename 为正式目标。正式目标
|
||||
再次校验成功后才删除源路径。复制、校验或发布失败时保留源并清理 staging;源删除
|
||||
失败时允许源与正式目标同时存在,但必须返回明确错误,不能报告成功。
|
||||
|
||||
Go upgrade 当前提供 **单 Agent 目标级事务恢复**:复制失败发生在旧目录移动前;
|
||||
备份中途失败会恢复此前已移动的目录;发布中途失败会恢复该目标的完整旧集合。不同
|
||||
Agent 目标仍彼此独立,一个目标失败不会回滚此前已经成功升级的其他目标,这与
|
||||
@@ -183,31 +159,11 @@ Agent 目标仍彼此独立,一个目标失败不会回滚此前已经成功
|
||||
## 9. 备份合同
|
||||
|
||||
- 路径:`~/.dws/skill-backups/<UTC 时间戳>/...`;
|
||||
- 主要操作:同一文件系统内使用 rename 移动;跨文件系统使用目标卷 staging 的
|
||||
copy → verify → publish → remove 回退;
|
||||
- 主要操作:同一文件系统内使用 rename 移动;
|
||||
- 失败语义:备份失败时原目录保持不变,目标安装失败;
|
||||
- 恢复语义:反向恢复使用相同回退;若删除备份源失败,原路径和备份可同时存在,
|
||||
但恢复必须失败并明确提示两份均被保留;
|
||||
- 可见性:计划和执行日志显示原路径与备份路径;
|
||||
- 保留策略:自动修剪,仅保留最近 5 批。
|
||||
|
||||
跨卷回退只有 staging → 正式目标的发布 rename 是原子的,整次迁移不是跨文件系统
|
||||
原子事务;该边界由“发布前不删源、发布后再次校验、删除失败保留两份”补偿。Shell
|
||||
入口继续使用系统 `mv` 的跨文件系统复制/删除能力;Go、npm 与 PowerShell 显式实现
|
||||
上述验证和失败合同。
|
||||
|
||||
原子 no-replace 发布(Linux `RENAME_NOREPLACE`、Darwin `RENAME_EXCL`)依赖底层文件
|
||||
系统支持:`rename(2)` 只列出 ext4、btrfs、tmpfs 与 cifs,因此 NFS、FUSE 与
|
||||
overlayfs 家目录会以 `EINVAL` 拒绝该 flag。这些文件系统不得让安装整体失败,而是降级
|
||||
为原子占位发布:目录目标用 `mkdir` 认领(已占用即 `EEXIST`,认领期间目标始终被本事务
|
||||
持有,源子项逐个移入认领目录,最终以 rename 覆盖仅属于本事务的空认领或直接移入);
|
||||
普通文件目标用硬链接占位(同样以 `EEXIST` 拒绝已占用路径)后删除源。任何一步失败都会
|
||||
回迁已移动的子项并只撤销本事务的占位,被并发创建的对象(文件、符号链接或目录)既不会
|
||||
被覆盖,也不会被链接进内部。逐子项移动路径不是全量原子可见(降级文件系统上的可接受
|
||||
边界),但不覆盖契约在所有平台保持不变。Windows `MoveFile` 本身即拒绝已存在的目标,
|
||||
无需降级。npm 与 Shell 安装面遵循同一占位模型:目录用 `mkdir`/子项移动,链接直接在
|
||||
目标路径创建(symlink(2) 原子拒绝已占用路径)。
|
||||
|
||||
备份是安装安全机制,不等于独立 rollback 产品。需要切回 mono 时重新运行
|
||||
`dws skill setup --mode mono`。
|
||||
|
||||
@@ -236,7 +192,6 @@ setup 在未显式指定 `--source` 时的本地回退缓存。
|
||||
| `scripts/install.ps1` | multi | 任一检测到的目标失败则脚本非零 |
|
||||
| `scripts/install-skills.sh` | multi | 任一检测到的目标失败则脚本非零 |
|
||||
| npm `install.js` | multi | 任一检测到的目标失败则 postinstall 失败 |
|
||||
| `scripts/install-event.sh` / `install-devapp.*` | 产品 multi 子集 | 同样使用 canonical 与 Agent 兼容层 |
|
||||
|
||||
Homebrew 不直接向 Agent home 铺设 Skill;安装 CLI 后由 setup 执行相同流程。
|
||||
|
||||
@@ -254,12 +209,6 @@ Homebrew 不直接向 Agent home 铺设 Skill;安装 CLI 后由 setup 执行
|
||||
- 复制失败不留下 Agent 可见的残缺官方目录;
|
||||
- 普通 upgrade 恢复被删除的预制 Skill,并安装新增官方 Skill;
|
||||
- Windows、macOS、Linux 的路径和覆盖率门禁;
|
||||
- symlinked parent、npm/PowerShell 的 Windows junction、`dws upgrade` /
|
||||
`dws skill setup` 的符号链接、链接失败复制回退与 broken link 修复;
|
||||
- Claude/Codex/Hermes 自定义根目录及 OpenClaw 历史目录优先级;
|
||||
- `CLAUDE_CONFIG_DIR`、`HERMES_HOME`、`XDG_CONFIG_HOME` 等自定义根跨文件系统时的
|
||||
正向备份、反向恢复、普通链接及 dangling symlink 词法保留;
|
||||
- copy、verify、publish、remove 各阶段故障,以及非跨设备权限错误不得进入复制回退;
|
||||
- npm、Shell、PowerShell 与包管理器安装冒烟。
|
||||
|
||||
## 13. 后续演进
|
||||
|
||||
+346
-1053
File diff suppressed because it is too large
Load Diff
@@ -102,7 +102,7 @@
|
||||
<tr><td><code>minutes +latest-minutes</code></td><td>列妙记→取最新一条详情</td><td class="c ok">编译/挂载</td></tr>
|
||||
<tr><td><code>minutes +action-items</code></td><td>列妙记→取最新→取其待办</td><td class="c ok">编译/挂载</td></tr>
|
||||
<tr><td><code>wiki +wiki-new-doc --space <名></code></td><td>按名搜知识空间→建文档(跨 doc server 路由)</td><td class="c ok">编译/挂载</td></tr>
|
||||
<tr><td><code>doc +doc-append --doc --content</code></td><td>文档末尾追加文本</td><td class="c ok">编译/挂载</td></tr>
|
||||
<tr><td><code>doc +doc-append --doc --text</code></td><td>文档末尾追加文本</td><td class="c ok">编译/挂载</td></tr>
|
||||
<tr><td><code>doc +share-doc --to <名> --url</code></td><td>解析人→把文档链接私信 TA(跨服务)</td><td class="c ok">编译/挂载</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -56,13 +56,13 @@
|
||||
|
||||
| shortcut | 多步/智能逻辑 | 验证 |
|
||||
|----------|--------------|------|
|
||||
| `chat +dm --to <姓名> --content` | 搜人→解析唯一 userId→发单聊;多人消歧 | ✅ dry-run 真机 |
|
||||
| `chat +dm --to <姓名> --text` | 搜人→解析唯一 userId→发单聊;多人消歧 | ✅ dry-run 真机 |
|
||||
| `contact +lookup --name <姓名>` | 搜人→解析 userId→取完整资料 | ✅ **真机端到端** |
|
||||
| `todo +assign --to <姓名> --task` | 解析人→建待办并把 TA 设为执行人 | ✅ dry-run 真机 |
|
||||
| `chat +send-to-group --group <群名> --content` | 按群名搜群(search_groups)→消歧→发消息 | ✅ 编译/挂载 |
|
||||
| `chat +send-to-group --group <群名> --text` | 按群名搜群(search_groups)→消歧→发消息 | ✅ 编译/挂载 |
|
||||
| `calendar +book --title --start --end [--with <姓名CSV>]` | 建日程→按名加参与者→**失败回滚删日程**(对标 lark `calendar +create`) | ✅ dry-run 真机 |
|
||||
| `calendar +free --who <姓名> --start --end` | 解析人→查其时段忙闲 | ✅ **真机端到端**(解析 202397→查忙闲) |
|
||||
| `chat +broadcast --to <姓名CSV> --content` | 多名逐一解析→群发单聊,失败汇总不中断 | ✅ 编译/挂载 |
|
||||
| `chat +broadcast --to <姓名CSV> --text` | 多名逐一解析→群发单聊,失败汇总不中断 | ✅ 编译/挂载 |
|
||||
| `minutes +latest-minutes` | 列妙记→取最新一条详情 | ✅ 编译/挂载 |
|
||||
| `chat +group-members --group <群名>` | 按群名搜群→列群成员 | ✅ 编译/挂载 |
|
||||
| `contact +org --name <姓名>` | 解析人→取详情拿 deptId→查部门详情 | ✅ **真机端到端**(3 步:董鑫阳→模型算法/16人) |
|
||||
@@ -76,7 +76,7 @@
|
||||
| `todo +todo-done --task <关键词>` | 列我的待办→按标题匹配→标记完成 | ✅ 编译/挂载 |
|
||||
| `calendar +reschedule --event <id>` | 查日程详情→改时间(查→改机械多步) | ✅ 编译/挂载 |
|
||||
| `wiki +wiki-new-doc --space <名>` | 按名搜知识空间→在其下建文档(跨 doc server 路由) | ✅ 编译/挂载 |
|
||||
| `doc +doc-append --doc --content` | 文档末尾追加文本(update_document append 模式) | ✅ 编译/挂载 |
|
||||
| `doc +doc-append --doc --text` | 文档末尾追加文本(update_document append 模式) | ✅ 编译/挂载 |
|
||||
| `minutes +action-items` | 列妙记→取最新→取其待办事项 | ✅ 编译/挂载 |
|
||||
| `minutes +detail --id <taskUuid>` | 一条命令聚合听记 basic/summary/keywords/transcript/todos,partial-failure 容错 | ✅ 全量测试 |
|
||||
| `minutes +replace-batch --id --pair "原文=>替换"…` | 多组批量替换文字,去重校验+逐组结果聚合 | ✅ 全量测试 |
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# OA Attachment Download URL Output Design
|
||||
|
||||
## Goal
|
||||
|
||||
Keep the existing command and MCP request unchanged while making the returned
|
||||
OSS signed URL directly copyable from JSON output:
|
||||
|
||||
```text
|
||||
dws oa approval attachment download-url
|
||||
```
|
||||
|
||||
## Scope
|
||||
|
||||
Only `oa approval attachment download-url` changes. The other OA attachment
|
||||
commands and the global JSON formatter retain their current behavior.
|
||||
|
||||
## Design
|
||||
|
||||
The command continues to invoke MCP server `oa`, tool
|
||||
`get_attachment_download_url`, with the same arguments. Its leaf declaration
|
||||
provides a command-specific `Call` callback that invokes the existing MCP
|
||||
dispatcher with HTML escaping disabled when the selected output format is
|
||||
JSON. This preserves literal `&` separators in `result.downloadUri` instead of
|
||||
rendering them as `\u0026`.
|
||||
|
||||
For `raw`, `table`, and other non-JSON formats, the callback uses the existing
|
||||
escaped dispatcher behavior so their current rendering remains unchanged.
|
||||
|
||||
The change does not alter the URL, decode or re-sign it, download the file, or
|
||||
change global JSON serialization.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Authentication, MCP transport, gateway, PAT, and business errors continue
|
||||
through the existing dispatcher and retain their current classification and
|
||||
output behavior.
|
||||
|
||||
## Verification
|
||||
|
||||
Add a `TestCrossPlatformCoverage*` regression test that executes the real Cobra
|
||||
leaf in explicit JSON mode with a fake MCP result containing a signed URL. It
|
||||
must verify:
|
||||
|
||||
- the request still targets `oa/get_attachment_download_url`;
|
||||
- the exact request arguments remain unchanged, including omission of the
|
||||
optional boolean when the flag was not supplied;
|
||||
- stdout contains literal `&OSSAccessKeyId=` and `&Signature=`;
|
||||
- stdout contains no `\u0026` escape.
|
||||
|
||||
The fake caller must report JSON format (or the command must be executed with
|
||||
`--format json`) so the test fails against the current escaped JSON path rather
|
||||
than accidentally exercising raw MCP text output.
|
||||
|
||||
Run the focused OA attachment tests, format modified Go files, and rebuild the
|
||||
CLI. No commit is created.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user