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
修雨 eedc41ac54 Merge branch 'main' into revert-730-codex/fix-release-version-verifier 2026-07-21 18:52:05 +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
修雨 29dceec5ce fix(release): check commit statuses in Code Admission gates (#732)
The "AI Behavior" context is reported as a commit status (via
github.rest.repos.createCommitStatus) rather than a check run, but the
Code Admission gates only queried check runs via
github.rest.checks.listForRef. This caused every release to fail with
"missing: AI Behavior" since the context was never found.

Add a commit-status query after the check-run loop in both the preflight
and sealed-commit Code Admission gates. Statuses are merged only for
required contexts not already covered by a check run, preserving the
existing check-run precedence.
2026-07-21 18:48:03 +08:00
SCzheng b78a0dee47 Revert "fix(release): validate packaged version at runtime" 2026-07-21 19:46:32 +09:00
修雨 807191396e Merge pull request #730 from DingTalk-Real-AI/codex/fix-release-version-verifier
fix(release): validate packaged version at runtime
2026-07-21 18:12:40 +08: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
11 changed files with 49 additions and 32362 deletions
+6 -11
View File
@@ -1047,9 +1047,7 @@ jobs:
rm -f "$RUNNER_TEMP/dws-developer-id-password"
- name: Verify final release artifacts
run: |
DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-release-artifacts.sh" "$RELEASE_VERSION"
run: ./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION"
- name: Verify npm package before sealing GitHub Release
run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$RELEASE_VERSION"
@@ -1174,9 +1172,7 @@ jobs:
path: dist
- name: Verify finalized release artifacts before publication
run: |
DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-release-artifacts.sh" "$RELEASE_VERSION"
run: ./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION"
- name: Verify npm package before sealing GitHub Release
run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$RELEASE_VERSION"
@@ -1272,7 +1268,7 @@ jobs:
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/download-github-release-assets.sh" \
"$RELEASE_VERSION" "$published_dir"
DWS_PACKAGE_DIST_DIR="$published_dir" \
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-release-artifacts.sh" "$RELEASE_VERSION"
./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION"
for asset in "$published_dir"/dws-* "$published_dir"/checksums.txt; do
if [ "$IS_RECOVERY" = "true" ] && ! cmp -s "$asset" "dist/$(basename "$asset")"; then
echo "Public recovery asset differs from this run's sealed artifact: $(basename "$asset")" >&2
@@ -1378,7 +1374,7 @@ jobs:
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/download-github-release-assets.sh" \
"$RELEASE_VERSION" "$sealed_upload"
DWS_PACKAGE_DIST_DIR="$sealed_upload" \
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-release-artifacts.sh" "$RELEASE_VERSION"
./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION"
for remote_asset in "$sealed_upload"/dws-* "$sealed_upload"/checksums.txt; do
local_asset="dist/$(basename "$remote_asset")"
cmp -s "$local_asset" "$remote_asset" || {
@@ -1448,7 +1444,7 @@ jobs:
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/download-github-release-assets.sh" \
"$RELEASE_VERSION" "$immutable_dir"
DWS_PACKAGE_DIST_DIR="$immutable_dir" \
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-release-artifacts.sh" "$RELEASE_VERSION"
./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION"
for immutable_asset in "$immutable_dir"/dws-* "$immutable_dir"/checksums.txt; do
sealed_asset="dist/$(basename "$immutable_asset")"
cmp -s "$sealed_asset" "$immutable_asset" || {
@@ -1570,8 +1566,7 @@ jobs:
--pattern 'dws-*' \
--pattern 'checksums.txt' \
--clobber
DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \
"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-release-artifacts.sh" "$RELEASE_VERSION"
./scripts/release/verify-release-artifacts.sh "$RELEASE_VERSION"
DWS_PACKAGE_SOURCE_ROOT="$GITHUB_WORKSPACE" \
DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \
./scripts/release/stage-npm-package.sh "$RELEASE_VERSION"
+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"
}
+3 -41
View File
@@ -72,7 +72,7 @@ else
exit 1
fi
verify_binary_archive() {
verify_binary_version() {
asset="$1"
extract_dir="$tmp/extract-${asset}"
mkdir -p "$extract_dir"
@@ -95,51 +95,13 @@ verify_binary_archive() {
return 1
}
LC_ALL=C grep -aFq "v$SEMVER" "$binary" || {
printf '%s binary does not contain expected version marker v%s\n' \
"$asset" "$SEMVER" >&2
printf '%s binary does not embed expected version v%s\n' "$asset" "$SEMVER" >&2
return 1
}
# Execute one native artifact and validate the CLI's public version contract.
# `strings` output has no symbol boundaries, so requiring the injected version
# to appear on an exact line can reject a correct Go binary when adjacent
# printable bytes are coalesced into the same output line.
if [ "$asset" = dws-linux-amd64.tar.gz ]; then
command -v python3 >/dev/null 2>&1 || {
printf 'python3 is required to validate the release binary version\n' >&2
return 1
}
version_json="$extract_dir/version.json"
isolated_home="$extract_dir/home"
mkdir -p "$isolated_home"
if ! env -i HOME="$isolated_home" PATH=/usr/bin:/bin LANG=C.UTF-8 \
"$binary" version --format json >"$version_json"; then
printf '%s binary could not report its version\n' "$asset" >&2
return 1
fi
reported_version="$(python3 -c '
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
payload = json.load(handle)
version = payload.get("version")
if not isinstance(version, str) or not version:
raise SystemExit(1)
print(version)
' "$version_json")" || {
printf '%s binary returned an invalid version payload\n' "$asset" >&2
return 1
}
[ "$reported_version" = "v$SEMVER" ] || {
printf '%s binary reports version %s, expected v%s\n' \
"$asset" "$reported_version" "$SEMVER" >&2
return 1
}
fi
}
for asset in $EXPECTED_PLATFORM_ASSETS; do
verify_binary_archive "$asset"
verify_binary_version "$asset"
done
printf 'Release artifacts verified for v%s.\n' "$SEMVER"
-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 -17
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)
@@ -1948,23 +1971,6 @@ printf '%s\n' "$*" >> "$SLEEP_CALL_LOG"
}
}
func TestReleaseWorkflowUsesTrustedArtifactVerifierForRecovery(t *testing.T) {
t.Parallel()
workflow := readReleaseWorkflow(t)
trusted := `"$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-release-artifacts.sh"`
if got := strings.Count(workflow, trusted); got != 6 {
t.Fatalf("trusted artifact verifier call count = %d, want 6", got)
}
workspaceDist := `DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \` + "\n" +
` "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-release-artifacts.sh"`
if got := strings.Count(workflow, workspaceDist); got != 3 {
t.Fatalf("workspace dist-bound trusted verifier call count = %d, want 3", got)
}
if strings.Contains(workflow, "./scripts/release/verify-release-artifacts.sh") {
t.Fatal("release workflow still executes the sealed tag's artifact verifier")
}
}
func TestReleaseStaysDraftUntilFinalizedAssetDigestsMatch(t *testing.T) {
t.Parallel()
+2 -22
View File
@@ -30,18 +30,7 @@ func writeVersionedReleaseArchive(t *testing.T, dist, asset, version string) {
if strings.HasSuffix(asset, ".zip") {
binary = "dws.exe"
}
content := []byte("fake release binary\n" + version + "\n")
if asset == "dws-linux-amd64.tar.gz" {
content = []byte(fmt.Sprintf(`#!/bin/sh
set -eu
if [ "$#" -ne 3 ] || [ "$1" != version ] || [ "$2" != --format ] || [ "$3" != json ]; then
printf 'unexpected version command\n' >&2
exit 2
fi
printf '{"version":"%s"}\n'
`, version))
}
mustWriteFile(t, filepath.Join(stage, binary), content, 0o755)
mustWriteFile(t, filepath.Join(stage, binary), []byte("fake release binary\n"+version+"\n"), 0o755)
if strings.HasSuffix(asset, ".zip") {
mustRun(t, stage, "zip", "-q", filepath.Join(dist, asset), binary)
return
@@ -1959,18 +1948,9 @@ func TestReleaseArtifactVerificationRequiresEveryChecksum(t *testing.T) {
writeReleaseChecksums(t, dist, true)
cmd = exec.Command("sh", r.verify, "v1.2.3")
cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+dist)
if output, err := cmd.CombinedOutput(); err == nil || !strings.Contains(string(output), "dws-windows-arm64.zip binary does not contain expected version marker v1.2.3") {
if output, err := cmd.CombinedOutput(); err == nil || !strings.Contains(string(output), "dws-windows-arm64.zip binary") {
t.Fatalf("mixed-version archive was not rejected: err=%v\noutput:\n%s", err, output)
}
writeVersionedReleaseArchive(t, dist, "dws-windows-arm64.zip", "v1.2.3")
writeVersionedReleaseArchive(t, dist, "dws-linux-amd64.tar.gz", "v1.2.3-beta.1")
writeReleaseChecksums(t, dist, true)
cmd = exec.Command("sh", r.verify, "v1.2.3")
cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+dist)
if output, err := cmd.CombinedOutput(); err == nil || !strings.Contains(string(output), "dws-linux-amd64.tar.gz binary reports version v1.2.3-beta.1, expected v1.2.3") {
t.Fatalf("native prefixed version was not rejected: err=%v\noutput:\n%s", err, output)
}
}
func TestReleaseCommandValidatesThenPushesAnnotatedTag(t *testing.T) {