Compare commits

...
Author SHA1 Message Date
修雨 7fd40ea19a Merge branch 'main' into cleanup/remove-shortcut-eval-pii 2026-07-21 20:34:48 +08:00
修雨 2f0f32f56f Merge pull request #739 from DingTalk-Real-AI/fix/release-artifact-raw-version-check
fix(release): verify packaged artifact versions from raw binary bytes
2026-07-21 19:25:39 +08:00
修雨 068d9ff2f5 fix(release): verify packaged artifact versions from raw binary bytes 2026-07-21 19:25:14 +08:00
修雨 418928b9a5 Merge pull request #736 from DingTalk-Real-AI/chore/changelog-v1.0.53-stable
docs(changelog): finalize v1.0.53 stable section
2026-07-21 19:00:26 +08:00
修雨 b047b2c3c9 docs(changelog): fold post-beta.7 entries into v1.0.53 stable section 2026-07-21 18:59:56 +08:00
修雨 a516e5f54a Merge pull request #735 from sczheng189/codex/fix-stable-version-verification
fix(release): verify package versions from raw binaries
2026-07-21 18:55:54 +08:00
修雨 70e4e75c66 Merge branch 'main' into codex/fix-stable-version-verification 2026-07-21 18:55:31 +08:00
修雨 1116916b24 Merge pull request #734 from DingTalk-Real-AI/revert-732-fix/release-admission-commit-statuses
Revert "fix(release): check commit statuses in Code Admission gates"
2026-07-21 18:53:57 +08:00
修雨 c0c81b4d70 Merge pull request #733 from DingTalk-Real-AI/revert-730-codex/fix-release-version-verifier
Revert "fix(release): validate packaged version at runtime"
2026-07-21 18:53:53 +08:00
zhengyubai c14e24569c fix(release): verify package versions from raw binaries 2026-07-21 19:49:18 +09:00
SCzheng 8add2c00cf Revert "fix(release): check commit statuses in Code Admission gates (#732)"
This reverts commit 29dceec5ce.
2026-07-21 19:48:47 +09:00
DennisandClaude Opus 4.8 e15a2c4efb chore: remove shortcut eval artifacts containing real PII
These files were real-backend capture artifacts committed by mistake and
contain personal data — employee names/emails, mail subjects, conversation &
message IDs, contact userIds/org, and hardcoded real test-target IDs:

- docs/shortcut-real-read-results.json   (raw read responses)
- docs/shortcut-real-write-results.json  (raw write responses)
- docs/shortcut-comparison.html          (embeds the raw responses)
- scripts/run_shortcut_real_read_matrix.py (hardcoded real target IDs)

They are dev-only capture artifacts, not build/CI inputs — the checked-in
public_catalog_generated.go is committed and no workflow/Makefile references
them, so removal does not affect the build. The generator scripts under
scripts/ that read these JSONs are local dev tools; they should consume a
locally-provided, uncommitted capture instead.

Add .gitignore rules so these (and the untracked shortcut-gsb-eval.* variants)
can never be re-committed.

Note: this only removes them going forward. They remain in git history on
origin/main (commit 8687d68); scrubbing history requires a separate,
owner-approved filter-repo/force-push.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:09:47 +08:00
10 changed files with 39 additions and 32310 deletions
-38
View File
@@ -358,25 +358,6 @@ jobs:
latestByName.set(run.name, run);
}
}
// Also check commit statuses — some contexts (e.g. "AI Behavior")
// are reported as commit statuses rather than check runs.
const statuses = await github.paginate(
github.rest.repos.listCommitStatusesForRef,
{ owner, repo, ref: sha, per_page: 100 },
);
const seenStatusContexts = new Set();
for (const status of statuses) {
if (!requiredContexts.includes(status.context) || seenStatusContexts.has(status.context)) {
continue;
}
seenStatusContexts.add(status.context);
if (!latestByName.has(status.context)) {
latestByName.set(status.context, {
conclusion: status.state,
status: "completed",
});
}
}
const missing = requiredContexts.filter((name) => !latestByName.has(name));
const nonSuccess = requiredContexts.flatMap((name) => {
const run = latestByName.get(name);
@@ -772,25 +753,6 @@ jobs:
latestByName.set(run.name, run);
}
}
// Also check commit statuses — some contexts (e.g. "AI Behavior")
// are reported as commit statuses rather than check runs.
const statuses = await github.paginate(
github.rest.repos.listCommitStatusesForRef,
{ owner, repo, ref: sha, per_page: 100 },
);
const seenStatusContexts = new Set();
for (const status of statuses) {
if (!requiredContexts.includes(status.context) || seenStatusContexts.has(status.context)) {
continue;
}
seenStatusContexts.add(status.context);
if (!latestByName.has(status.context)) {
latestByName.set(status.context, {
conclusion: status.state,
status: "completed",
});
}
}
const missing = requiredContexts.filter((name) => !latestByName.has(name));
const nonSuccess = requiredContexts.flatMap((name) => {
const run = latestByName.get(name);
+8
View File
@@ -54,3 +54,11 @@ test/dev_functional/results.jsonl
/coverage-policy.txt
/coverage.html
dwsbin
# Local shortcut eval / real-backend capture artifacts — may contain real PII
# (employee names/emails, userIds, conversation & message IDs). Never commit.
/docs/shortcut-real-read-results.json
/docs/shortcut-real-write-results.json
/docs/shortcut-comparison.html
/docs/shortcut-gsb-eval.*
/scripts/run_shortcut_real_read_matrix.py
+3 -5
View File
@@ -6,13 +6,9 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
## [Unreleased]
### Changed
- **Relaxed stable promotion contract** — a stable release still requires a delivered, non-withdrawn beta baseline in its commit history, but no longer requires a byte-identical tree with that beta; reviewed commits merged to `main` after the beta can now ship in the stable release. Local releases now accept any sealed commit contained in `main` history and push only the release tag, so `main` is never frozen during the beta-to-stable window.
## [1.0.53] - 2026-07-21
This release promotes the sealed `v1.0.53-beta.7` contents to stable. It adds enterprise onboarding, declarative shortcuts, Sheet/Aitable writes, multi-account profiles, and broader personal IM events, while hardening authentication and the guarded release path.
This release promotes the validated `v1.0.53-beta.7` baseline to stable. It adds enterprise onboarding, declarative shortcuts, Sheet/Aitable writes, multi-account profiles, and broader personal IM events, while hardening authentication and the guarded release path.
### Added
@@ -25,12 +21,14 @@ This release promotes the sealed `v1.0.53-beta.7` contents to stable. It adds en
- **Personal event output contract** (#651) — `event consume` now emits event-specific top-level structured fields; scripts that consumed the former transport envelope must use the flat fields or select `-f raw`, while `--debug-raw-events` retains the diagnostic envelope.
- **Guarded release lifecycle** — beta/stable publication now uses explicit promotion, immutable delivery proofs, protected recovery, and tag-bound optional OSS policy; an unprovisioned OSS mirror is sealed as `deferred` so GitHub, npm, and Homebrew are not blocked.
- **Relaxed stable promotion contract** (#729) — a stable release still requires a delivered, non-withdrawn beta baseline in its commit history, but no longer requires a byte-identical tree with that beta; reviewed commits merged to `main` after the beta can now ship in the stable release. Local releases now accept any sealed commit contained in `main` history and push only the release tag, so `main` is never frozen during the beta-to-stable window.
### Fixed
- **Authentication and credential reliability** — organization-policy denials stop before mutation or polling, long-running clients reload and refresh access tokens consistently, concurrent credential writes are atomic, and Windows portable-auth commands fail before reading or writing unsupported credential bundles.
- **Command validation and compatibility** — invalid Sheet/task targets fail locally, IM shortcuts preserve AI-tag and alias compatibility, and Aitable import uploads require and forward a positive file size.
- **Release publication reliability** — GitHub draft publication is bound to one verified release ID and exact assets, preflight uses isolated installer worktrees, guarded local tags remain compatible, cloud planning fingerprints the actual allocated release refs, and npm channel verification waits for bounded registry propagation without moving tags.
- **Package-manager version verification** (#735) — npm-vendored, Homebrew-installed, and packaged release binaries are now verified by searching their raw bytes for the injected version marker, so a correctly versioned stable binary is no longer rejected when the short version marker coalesces with adjacent printable linker metadata; incorrect or missing markers still fail closed.
## [1.0.53-beta.7] - 2026-07-21
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+4 -6
View File
@@ -53,8 +53,6 @@ while [ "$#" -gt 0 ]; do
esac
done
[ -z "$EXPECTED_VERSION" ] || need_cmd strings
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-package-verify-XXXXXX")"
HOME_AGENT_PARENTS="
.claude
@@ -145,8 +143,8 @@ verify_npm() {
if [ -n "$EXPECTED_VERSION" ]; then
vendor_bin="$npm_prefix/lib/node_modules/dingtalk-workspace-cli/vendor/dws"
need_file "$vendor_bin"
strings "$vendor_bin" | grep -Fqx "v$EXPECTED_VERSION" || \
err "npm-installed binary does not embed expected version v$EXPECTED_VERSION"
LC_ALL=C grep -aFq "v$EXPECTED_VERSION" "$vendor_bin" || \
err "npm-installed binary does not contain expected version marker v$EXPECTED_VERSION"
EXPECTED_VERSION="$EXPECTED_VERSION" node -e '
const pkg = require(process.argv[1]);
if (pkg.version !== process.env.EXPECTED_VERSION) process.exit(1);
@@ -193,8 +191,8 @@ verify_brew() {
[ -x "$prefix/bin/dws" ] || err "brew install did not create $prefix/bin/dws"
"$prefix/bin/dws" --help >/dev/null
if [ -n "$EXPECTED_VERSION" ]; then
strings "$prefix/bin/dws" | grep -Fqx "v$EXPECTED_VERSION" || \
err "Homebrew-installed binary does not embed expected version v$EXPECTED_VERSION"
LC_ALL=C grep -aFq "v$EXPECTED_VERSION" "$prefix/bin/dws" || \
err "Homebrew-installed binary does not contain expected version marker v$EXPECTED_VERSION"
fi
need_file "$prefix/share/dingtalk-workspace-cli-local/skills/dws/SKILL.md"
}
+1 -1
View File
@@ -94,7 +94,7 @@ verify_binary_version() {
printf '%s does not contain the expected dws binary\n' "$asset" >&2
return 1
}
strings "$binary" | grep -Fqx "v$SEMVER" || {
LC_ALL=C grep -aFq "v$SEMVER" "$binary" || {
printf '%s binary does not embed expected version v%s\n' "$asset" "$SEMVER" >&2
return 1
}
-479
View File
@@ -1,479 +0,0 @@
#!/usr/bin/env python3
"""Run every read shortcut against the real DWS backend.
This launches the built CLI once per read shortcut. It does not use --mock and
does not use --dry-run. Inputs are synthetic but realistic where a shortcut
expects a name, date, or query; resource identifiers remain test placeholders
when no resource has been created for that command.
"""
from __future__ import annotations
import datetime as dt
import argparse
import json
import os
import shlex
import subprocess
import time
from pathlib import Path
from shortcut_real_result import (
classify_failure,
sanitize_result,
summarize_failure_categories,
classify_real_status,
summarize_results,
)
ROOT = Path(__file__).resolve().parents[1]
MATRIX_PATH = Path("/private/tmp/dws-shortcut-matrix.json")
OUT_PATH = ROOT / "docs" / "shortcut-real-read-results.json"
BIN = os.environ.get("DWS_REAL_TEST_BIN", "/private/tmp/dws-real-test")
DEVAPP_FIXTURE_ID = "678f27ec-4339-49d8-9c49-371b284bf552"
DEVAPP_FIXTURE_VERSION_ID = "4743accb-45e2-4bc9-8c96-74fc62ace2e8"
CHAT_FIXTURE_OPEN_CONVERSATION_ID = "cid3Jijzhe2aqs9ysOXjhi05g=="
CHAT_FIXTURE_GROUP_NAME = "浅曦-kida,Dennis,秋画"
CHAT_FIXTURE_DM_OPEN_CONVERSATION_ID = "cidie1367hAfBxqipzE59k5sknHLrHmvYkw98NADhfnjPI="
CHAT_FIXTURE_OPEN_MESSAGE_ID = "msgEuOor1PmFBNlx9M06N9z1Q=="
CHAT_FIXTURE_OPEN_TASK_ID = "y/wM6Lo+9GbIqtILYPv1BZDcMW+2FgnqskgcpdOiMdM="
CALENDAR_FIXTURE_EVENT_ID = "THN4YUtOTlplYU9sZzd2czE4YURLQT09_1784078100000"
DOC_FIXTURE_NODE_ID = "P0MALyR8knpgo9GycY7ZlMxlJ3bzYmDO"
DOC_FIXTURE_FOLDER_ID = "Amq4vjg89ZOAdqyaSMKpApXdW3kdP0wQ"
DOC_FIXTURE_EXPORT_JOB_ID = "29346731713"
SHEET_FIXTURE_NODE_ID = "mweZ92PV6O36dZbnsMZx70ylJxEKBD6p"
DING_FIXTURE_OPEN_DING_ID = "5D73E3AC29C780072D1CD56C6874ACC2"
CONTACT_FIXTURE_MOBILE = "13161187007"
AITABLE_FIXTURE_BASE_ID = "gpG2NdyVXQyZ0OmoSbd1vbA6JMwvDqPk"
AITABLE_FIXTURE_TABLE_ID = "hERWDMS"
AITABLE_FIXTURE_VIEW_ID = "qvGDAH2"
AITABLE_FIXTURE_FORM_VIEW_ID = "lmeV1cb"
AITABLE_FIXTURE_FIELD_ID = "01ZM8y7"
AITABLE_FIXTURE_RECORD_ID = "1015oH3OXy"
AITABLE_FIXTURE_DASHBOARD_ID = "KY9tlWg5NEHgfs8WT6IO2"
AITABLE_FIXTURE_CHART_ID = "widget-dlxFo0tNSImp4ITpDn5fQ"
HELD_CASES = {
("devapp", "+credentials-get"):
"该命令会读取真实应用凭证/密钥;不能用真实 app 自动执行。当前仅用占位 ID 验证负向路径,真实成功需人工在安全环境单独确认。",
}
def ensure_matrix() -> dict:
env = os.environ.copy()
env.setdefault("GOCACHE", "/private/tmp/dws_gocache")
raw = subprocess.check_output(
["go", "run", "./scripts/gen_shortcut_test_matrix.go"],
cwd=ROOT,
env=env,
text=True,
)
MATRIX_PATH.write_text(raw, encoding="utf-8")
return json.loads(raw)
def replace_flag_values(args: list[str], service: str, command: str) -> list[str]:
today = dt.date.today()
start = (dt.datetime.now() + dt.timedelta(days=1)).replace(hour=10, minute=0, second=0, microsecond=0).isoformat() + "+08:00"
end = (dt.datetime.now() + dt.timedelta(days=1)).replace(hour=11, minute=0, second=0, microsecond=0).isoformat() + "+08:00"
no_id = "DWSREALREADNOSUCHID0000000000000"
no_conv = "cidDWSREALREADNOSUCHCONV"
day = str(today)
yesterday = str(today - dt.timedelta(days=1))
datetime_start = dt.datetime.now().replace(hour=10, minute=0, second=0, microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
datetime_end = (dt.datetime.now() + dt.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
replacements = {
"name": "DWS shortcut 真实测试",
"query": "测试",
"keyword": "测试",
"q": "测试",
"text": "测试",
"title": "测试",
"phone": "13000000000",
"mobile": "13000000000",
"user": "冬翔",
"users": "冬翔",
"to": "冬翔",
"with": "冬翔",
"who": "冬翔",
"dept": "模型算法",
"dept-id": "842379556",
"department-id": "842379556",
"start": start,
"end": end,
"from": str(today - dt.timedelta(days=7)),
"to-date": str(today),
"date": str(today),
"time": datetime_start,
"days": "7",
"types": "leave",
"columns": "1001",
"limit": "10",
"page": "1",
"page-size": "10",
"cursor": "0",
"calendar-id": "primary",
"event": no_id,
"type": "ALL",
"types": "leave",
"columns": "1001",
"role-types": "executor",
"status": "false",
"artifacts": "basic",
"direction": "older",
"file-types": "alidoc",
"order-by": "name",
"order": "asc",
"space-id": "1",
"space": "测试",
"base": no_id,
"base-id": no_id,
"table": no_id,
"table-id": no_id,
"view-id": no_id,
"record-id": no_id,
"record-ids": no_id,
"field-id": no_id,
"dashboard-id": no_id,
"chart-id": no_id,
"workflow-id": no_id,
"node": no_id,
"doc": no_id,
"folder": no_id,
"workspace": no_id,
"group": no_conv,
"conversation-id": no_conv,
"open-conversation-id": no_conv,
"message-id": no_id,
"msg-id": no_id,
"id": no_id,
"session-id": no_id,
"process-instance-id": no_id,
"task-id": no_id,
"template-id": no_id,
"mail-id": no_id,
"filters": "{}",
"sort": "[]",
}
if service == "calendar" and command == "+free-slots":
replacements["from"] = "9"
replacements["to"] = "18"
if service == "calendar":
replacements["event"] = CALENDAR_FIXTURE_EVENT_ID
replacements["cursor"] = ""
if command == "+freebusy":
replacements["users"] = "103262"
if service == "doc" and command == "+comment-list":
replacements["type"] = "global"
if service == "doc":
replacements["node"] = DOC_FIXTURE_NODE_ID
replacements["doc"] = DOC_FIXTURE_NODE_ID
replacements["folder"] = DOC_FIXTURE_FOLDER_ID
replacements["job-id"] = DOC_FIXTURE_EXPORT_JOB_ID
if service == "drive":
replacements["node"] = DOC_FIXTURE_NODE_ID
replacements["folder"] = DOC_FIXTURE_FOLDER_ID
if service == "todo":
replacements["task-id"] = "55119034912"
replacements["size"] = "10"
if service == "aitable":
replacements["name"] = "Real共创版设备去向登记"
replacements["base"] = AITABLE_FIXTURE_BASE_ID
replacements["base-id"] = AITABLE_FIXTURE_BASE_ID
replacements["table"] = AITABLE_FIXTURE_TABLE_ID
replacements["table-id"] = AITABLE_FIXTURE_TABLE_ID
replacements["view-id"] = AITABLE_FIXTURE_VIEW_ID
replacements["view-ids"] = AITABLE_FIXTURE_VIEW_ID
replacements["field-id"] = AITABLE_FIXTURE_FIELD_ID
replacements["field-ids"] = AITABLE_FIXTURE_FIELD_ID
replacements["record-id"] = AITABLE_FIXTURE_RECORD_ID
replacements["record-ids"] = AITABLE_FIXTURE_RECORD_ID
replacements["dashboard-id"] = AITABLE_FIXTURE_DASHBOARD_ID
replacements["chart-id"] = AITABLE_FIXTURE_CHART_ID
if command.startswith("+form-"):
replacements["view-id"] = AITABLE_FIXTURE_FORM_VIEW_ID
if command == "+resolve-table":
replacements["name"] = "Mac Mini"
if service == "ding" and command == "+list":
replacements["type"] = "ALL"
if service == "ding" and command == "+receiver-status":
replacements["ding-id"] = DING_FIXTURE_OPEN_DING_ID
if service == "contact" and command == "+list-sub-depts":
replacements["dept"] = "842379556"
if service == "contact":
replacements["name"] = "冬翔"
if command == "+by-mobile":
replacements["mobile"] = CONTACT_FIXTURE_MOBILE
if command == "+resolve-dept":
replacements["name"] = "模型算法"
if service == "oa":
now_ms = int(dt.datetime.now().timestamp() * 1000)
replacements["start"] = str(now_ms - 7 * 24 * 60 * 60 * 1000)
replacements["end"] = str(now_ms)
replacements["page"] = "1"
replacements["limit"] = "10"
if service == "report":
replacements["start"] = (dt.datetime.now() - dt.timedelta(days=7)).replace(microsecond=0).isoformat() + "+08:00"
replacements["end"] = dt.datetime.now().replace(microsecond=0).isoformat() + "+08:00"
replacements["modified-start"] = replacements["start"]
replacements["modified-end"] = replacements["end"]
if service == "attendance":
replacements.update({
"user": "202397",
"users": "202397",
"staff-ids": "202397",
"operator-staff-id": "202397",
"leave-code": "731ed089-62ff-4734-a6c7-3c8fcc8294fc",
"leave-names": "年假",
"start": yesterday,
"end": day,
"from": yesterday,
"to-date": day,
"date": day,
})
if command in {"+get-checkin-record", "+query-report-data", "+query-report-leave"}:
replacements["start"] = datetime_start
replacements["end"] = datetime_end
if command == "+get-approve-template":
replacements["type"] = "leave"
if command == "+search-group":
replacements["type"] = "FIXED"
if service == "devapp":
replacements["unified-app-id"] = DEVAPP_FIXTURE_ID
replacements["version-id"] = DEVAPP_FIXTURE_VERSION_ID
replacements["cursor"] = ""
if service == "mail":
replacements["email"] = "xinyang.dxy@alibaba-inc.com"
replacements["folder"] = "2"
replacements["query"] = "subject:测试"
replacements["keyword"] = "董鑫阳"
replacements["employee-no"] = "202397"
replacements["size"] = "10"
replacements["cursor"] = ""
if command == "+find-mail-user":
replacements["query"] = "董鑫阳"
if service == "chat":
replacements["group"] = CHAT_FIXTURE_OPEN_CONVERSATION_ID
replacements["conversation-id"] = CHAT_FIXTURE_OPEN_CONVERSATION_ID
replacements["open-conversation-id"] = CHAT_FIXTURE_OPEN_CONVERSATION_ID
replacements["msg-ids"] = CHAT_FIXTURE_OPEN_MESSAGE_ID
replacements["message-id"] = CHAT_FIXTURE_OPEN_MESSAGE_ID
replacements["msg-id"] = CHAT_FIXTURE_OPEN_MESSAGE_ID
replacements["open-task-id"] = CHAT_FIXTURE_OPEN_TASK_ID
if command == "+group-members":
replacements["group"] = CHAT_FIXTURE_GROUP_NAME
if command == "+messages-read-status":
replacements["conversation-id"] = CHAT_FIXTURE_DM_OPEN_CONVERSATION_ID
replacements["users"] = "冬翔"
if command == "+messages-resource-url":
replacements["type"] = "mediaId"
if command == "+bot-find":
replacements["cursor"] = ""
if service == "sheet" and command == "+list-sheets":
replacements["node"] = SHEET_FIXTURE_NODE_ID
out = list(args)
if "--format" not in out:
out.extend(["--format", "json"])
i = 0
while i < len(out) - 1:
if out[i].startswith("--"):
key = out[i][2:]
if key in replacements and not out[i + 1].startswith("--"):
out[i + 1] = replacements[key]
i += 2
continue
i += 1
return out
def shell_join(cmd: list[str]) -> str:
return " ".join(shlex.quote(x) for x in cmd)
def drop_flag(args: list[str], flag: str) -> list[str]:
out: list[str] = []
i = 0
while i < len(args):
if args[i] == flag:
i += 2 if i + 1 < len(args) and not args[i + 1].startswith("--") else 1
continue
out.append(args[i])
i += 1
return out
def adjust_command_args(args: list[str], service: str, command: str) -> list[str]:
if service == "chat" and command == "+chat-messages":
return drop_flag(args, "--user")
if service == "chat" and command == "+messages-list-direct":
out = drop_flag(args, "--open-dingtalk-id")
for i in range(len(out) - 1):
if out[i] == "--user":
out[i + 1] = "103262"
return out
if service == "calendar" and command == "+freebusy":
return drop_flag(args, "--rooms")
if service == "devapp" and command == "+permission-list":
out = drop_flag(args, "--scope-value")
out = drop_flag(out, "--scope-type")
out = drop_flag(out, "--api-status")
for i in range(len(out) - 1):
if out[i] == "--auth-status":
out[i + 1] = "ALL"
return out
if service == "doc" and command == "+search":
out = drop_flag(args, "--extensions")
out = drop_flag(out, "--created-from")
out = drop_flag(out, "--created-to")
out = drop_flag(out, "--visited-from")
out = drop_flag(out, "--visited-to")
out = drop_flag(out, "--creator-uids")
out = drop_flag(out, "--editor-uids")
out = drop_flag(out, "--mentioned-uids")
out = drop_flag(out, "--workspace-ids")
return out
if service == "doc" and command == "+comment-list":
out = drop_flag(args, "--cursor")
out = drop_flag(out, "--resolve-status")
return out
if service == "doc" and command == "+list":
out = drop_flag(args, "--workspace")
out = drop_flag(out, "--cursor")
return out
if service == "report" and command == "+outbox-list":
return drop_flag(args, "--template-name")
return args
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--service", action="append", help="Only run shortcuts from this service; may repeat")
parser.add_argument("--command", action="append", help="Only run this shortcut command; may repeat")
parser.add_argument("--failed-only", action="store_true", help="Only rerun shortcuts currently marked non-success in the output report")
ns = parser.parse_args()
services = set(ns.service or [])
commands = set(ns.command or [])
matrix = ensure_matrix()
rows = [r for r in matrix["results"] if r.get("risk") == "read"]
if services:
rows = [r for r in rows if r["service"] in services]
if commands:
rows = [r for r in rows if r["command"] in commands]
if ns.failed_only and OUT_PATH.exists():
existing = json.loads(OUT_PATH.read_text(encoding="utf-8"))
failed = {
(r.get("service"), r.get("command"))
for r in existing.get("results", [])
if r.get("status") != "real-ok"
}
rows = [r for r in rows if (r["service"], r["command"]) in failed]
results = []
summary = {"total": len(rows), "ok": 0, "error": 0, "timeout": 0, "held": 0}
for idx, r in enumerate(rows, 1):
key = (r["service"], r["command"])
if key in HELD_CASES:
summary["held"] += 1
item = {
"service": r["service"],
"command": r["command"],
"risk": r["risk"],
"method": "held; sensitive credential read",
"status": "held",
"input": "",
"args": [],
"stdout": "",
"stderr": HELD_CASES[key],
"exit_code": None,
"duration_ms": 0,
}
item = sanitize_result(item)
category, fixability, note = classify_failure(item)
item["failure_category"] = category
item["fixability"] = fixability
item["diagnosis"] = note
results.append(item)
continue
args = adjust_command_args(replace_flag_values(r["args"], r["service"], r["command"]), r["service"], r["command"])
cmd = [BIN] + args
started = time.time()
status = "real-error"
stdout = ""
stderr = ""
exit_code = None
try:
p = subprocess.run(cmd, text=True, capture_output=True, timeout=30)
stdout = p.stdout.strip()
stderr = p.stderr.strip()
exit_code = p.returncode
status = classify_real_status(exit_code, stdout)
if status == "real-ok":
status = "real-ok"
summary["ok"] += 1
else:
summary["error"] += 1
except subprocess.TimeoutExpired as e:
status = "timeout"
summary["timeout"] += 1
if isinstance(e.stdout, str):
stdout = e.stdout.strip()
if isinstance(e.stderr, str):
stderr = e.stderr.strip()
duration_ms = int((time.time() - started) * 1000)
item = {
"service": r["service"],
"command": r["command"],
"risk": r["risk"],
"method": "real-backend-read; no --mock; no --dry-run",
"status": status,
"input": shell_join(cmd),
"args": args,
"stdout": stdout,
"stderr": stderr,
"exit_code": exit_code,
"duration_ms": duration_ms,
}
item = sanitize_result(item)
category, fixability, note = classify_failure(item)
if category != "passed":
item["failure_category"] = category
item["fixability"] = fixability
item["diagnosis"] = note
results.append(item)
if idx % 25 == 0 or idx == len(rows):
print(
f"progress {idx}/{len(rows)} ok={summary['ok']} "
f"error={summary['error']} timeout={summary['timeout']} held={summary['held']}",
flush=True,
)
if not services and not commands and not ns.failed_only:
OUT_PATH.write_text(json.dumps({
"generated_at": dt.datetime.now().isoformat(),
"summary": summary,
"failure_categories": summarize_failure_categories(results),
"results": results,
}, ensure_ascii=False, indent=2), encoding="utf-8")
if OUT_PATH.exists() and (services or commands or ns.failed_only):
existing = json.loads(OUT_PATH.read_text(encoding="utf-8"))
replace_keys = {(r["service"], r["command"]) for r in results}
merged = [
r for r in existing.get("results", [])
if (r.get("service"), r.get("command")) not in replace_keys
]
merged.extend(results)
else:
merged = results
summary = summarize_results(merged, include_held=True)
OUT_PATH.write_text(json.dumps({
"generated_at": dt.datetime.now().isoformat(),
"summary": summary,
"failure_categories": summarize_failure_categories(merged),
"results": merged,
}, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"saved {OUT_PATH} batch={summarize_results(results, include_held=True)} merged={summary}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+23
View File
@@ -46,6 +46,29 @@ var expectedReleaseAdmissionContexts = []string{
"Mock MCP",
}
func TestPackageManagerVersionVerificationReadsRawBinary(t *testing.T) {
t.Parallel()
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "verify-package-managers.sh"))
if err != nil {
t.Fatalf("Abs(verify-package-managers.sh) error = %v", err)
}
data, err := os.ReadFile(scriptPath)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", scriptPath, err)
}
script := string(data)
for _, binary := range []string{`"$vendor_bin"`, `"$prefix/bin/dws"`} {
want := `LC_ALL=C grep -aFq "v$EXPECTED_VERSION" ` + binary
if !strings.Contains(script, want) {
t.Errorf("package-manager verifier is missing raw binary marker check %q", want)
}
}
if strings.Contains(script, `strings "$vendor_bin"`) || strings.Contains(script, `strings "$prefix/bin/dws"`) {
t.Fatal("package-manager verifier still requires the version marker to occupy a strings(1) line")
}
}
func seedDistArchive(t *testing.T, path string) {
t.Helper()
file, err := os.Create(path)