Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b898f5c987 | ||
|
|
20750df20b | ||
|
|
749149b94a | ||
|
|
e5c8ff9acd | ||
|
|
706535b41e |
@@ -1694,13 +1694,28 @@ jobs:
|
||||
run: |
|
||||
set -eu
|
||||
. ./scripts/release/release-lib.sh
|
||||
delivered="$(npm view dingtalk-workspace-cli "dist-tags.$NPM_TAG")"
|
||||
if [ "$delivered" = "$SEMVER" ]; then exit 0; fi
|
||||
release_version_is_greater "v$delivered" "v$SEMVER" || {
|
||||
echo "npm $NPM_TAG=$delivered does not deliver or supersede v$SEMVER" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "npm $NPM_TAG already supersedes this historical rerun at v$delivered."
|
||||
attempt=1
|
||||
max_attempts=12
|
||||
while :; do
|
||||
delivered="$(npm view dingtalk-workspace-cli "dist-tags.$NPM_TAG" \
|
||||
--registry=https://registry.npmjs.org --prefer-online)"
|
||||
if [ "$delivered" = "$SEMVER" ]; then exit 0; fi
|
||||
if release_version_is_greater "v$delivered" "v$SEMVER"; then
|
||||
echo "npm $NPM_TAG already supersedes this historical rerun at v$delivered."
|
||||
exit 0
|
||||
fi
|
||||
release_version_is_greater "v$SEMVER" "v$delivered" || {
|
||||
echo "npm $NPM_TAG=$delivered cannot be reconciled with v$SEMVER" >&2
|
||||
exit 1
|
||||
}
|
||||
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||
echo "npm $NPM_TAG still reports older v$delivered after $attempt attempts; expected v$SEMVER" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "npm $NPM_TAG still reports older v$delivered; waiting for registry propagation ($attempt/$max_attempts)."
|
||||
sleep 5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
- name: Sync release artifacts to China OSS mirror
|
||||
if: ${{ needs.release-contract.outputs.oss_mirror == 'enabled' }}
|
||||
|
||||
@@ -6,6 +6,36 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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.
|
||||
|
||||
### Added
|
||||
|
||||
- **Enterprise and office command coverage** — adds enterprise creation, employee invitation, and account provisioning commands; 366 declarative service shortcuts; Sheet import commands; and Aitable workflow create/update support with reviewed Schema contracts.
|
||||
- **Multiple accounts in one DingTalk organization** — profiles can distinguish accounts by organization and user, select them explicitly, and log out one account or an entire organization without overwriting another account's credentials.
|
||||
- **Expanded personal IM event subscriptions** (#651) — adds read-receipt, recall, and reaction events for one-to-one and group chats, plus specified-sender subscriptions by staff ID or OpenDingTalk ID.
|
||||
- **Official multi-platform Homebrew channel** — ships separate stable and keg-only beta Formulae for macOS and Linux across amd64 and arm64, with isolated update PRs.
|
||||
|
||||
### Changed
|
||||
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
|
||||
## [1.0.53-beta.7] - 2026-07-21
|
||||
|
||||
This beta validates bounded npm channel verification after registry publication.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **npm dist-tag eventual consistency** — Release delivery now tolerates a briefly stale `latest` or `beta` read after publishing by retrying only when npm reports a valid older version. Registry errors, invalid or incomparable tags, and channels that never converge still fail closed without moving any tag during verification.
|
||||
|
||||
## [1.0.53-beta.6] - 2026-07-21
|
||||
|
||||
This beta validates guarded local release compatibility and tag-bound OSS deferral so an unprovisioned mirror cannot block the primary release channels.
|
||||
|
||||
@@ -637,6 +637,33 @@ func releaseWorkflowSection(t *testing.T, workflow, startMarker, endMarker strin
|
||||
return workflow[start : start+len(startMarker)+end]
|
||||
}
|
||||
|
||||
func releaseWorkflowRunScript(t *testing.T, workflow, stepName, nextStepName string) string {
|
||||
t.Helper()
|
||||
section := releaseWorkflowSection(
|
||||
t,
|
||||
workflow,
|
||||
" - name: "+stepName+"\n",
|
||||
"\n - name: "+nextStepName+"\n",
|
||||
)
|
||||
const runMarker = " run: |\n"
|
||||
start := strings.Index(section, runMarker)
|
||||
if start == -1 {
|
||||
t.Fatalf("release workflow step %q is missing a run block", stepName)
|
||||
}
|
||||
|
||||
lines := strings.Split(section[start+len(runMarker):], "\n")
|
||||
for i, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(line, " ") {
|
||||
t.Fatalf("release workflow step %q has an unexpected run indentation: %q", stepName, line)
|
||||
}
|
||||
lines[i] = strings.TrimPrefix(line, " ")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowUsesDedicatedGovernanceIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
workflow := readReleaseWorkflow(t)
|
||||
@@ -1794,6 +1821,133 @@ func TestReleaseWorkflowOpensVersionedHomebrewPRForBetaTags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowWaitsForNPMDistTagPropagation(t *testing.T) {
|
||||
workflow := readReleaseWorkflow(t)
|
||||
script := releaseWorkflowRunScript(
|
||||
t,
|
||||
workflow,
|
||||
"Verify npm channel delivery",
|
||||
"Sync release artifacts to China OSS mirror",
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sequence string
|
||||
wantSuccess bool
|
||||
wantCalls int
|
||||
wantSleeps int
|
||||
wantOutput string
|
||||
}{
|
||||
{
|
||||
name: "stale beta converges to target",
|
||||
sequence: "1.0.53-beta.6\n1.0.53-beta.6\n1.0.53-beta.7\n",
|
||||
wantSuccess: true,
|
||||
wantCalls: 3,
|
||||
wantSleeps: 2,
|
||||
},
|
||||
{
|
||||
name: "stale beta never converges",
|
||||
sequence: "1.0.53-beta.6\n",
|
||||
wantCalls: 12,
|
||||
wantSleeps: 11,
|
||||
wantOutput: "still reports older v1.0.53-beta.6 after 12 attempts",
|
||||
},
|
||||
{
|
||||
name: "permanent registry error fails immediately",
|
||||
sequence: "__NPM_ERROR__\n",
|
||||
wantCalls: 1,
|
||||
wantSleeps: 0,
|
||||
wantOutput: "permanent npm registry error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fakeBin := filepath.Join(root, "bin")
|
||||
if err := os.MkdirAll(fakeBin, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(%s) error = %v", fakeBin, err)
|
||||
}
|
||||
sequencePath := filepath.Join(root, "sequence")
|
||||
statePath := filepath.Join(root, "state")
|
||||
npmLogPath := filepath.Join(root, "npm.log")
|
||||
sleepLogPath := filepath.Join(root, "sleep.log")
|
||||
mustWriteFile(t, sequencePath, []byte(test.sequence), 0o644)
|
||||
mustWriteFile(t, filepath.Join(fakeBin, "npm"), []byte(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%s\n' "$*" >> "$NPM_CALL_LOG"
|
||||
test "$*" = "view dingtalk-workspace-cli dist-tags.beta --registry=https://registry.npmjs.org --prefer-online" || {
|
||||
echo "unexpected npm mutation: $*" >&2
|
||||
exit 97
|
||||
}
|
||||
call=0
|
||||
if test -f "$NPM_STATE"; then call="$(cat "$NPM_STATE")"; fi
|
||||
call=$((call + 1))
|
||||
printf '%s\n' "$call" > "$NPM_STATE"
|
||||
value="$(sed -n "${call}p" "$NPM_SEQUENCE")"
|
||||
if test -z "$value"; then value="$(tail -n 1 "$NPM_SEQUENCE")"; fi
|
||||
if test "$value" = "__NPM_ERROR__"; then
|
||||
echo "permanent npm registry error" >&2
|
||||
exit 42
|
||||
fi
|
||||
printf '%s\n' "$value"
|
||||
`), 0o755)
|
||||
mustWriteFile(t, filepath.Join(fakeBin, "sleep"), []byte(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%s\n' "$*" >> "$SLEEP_CALL_LOG"
|
||||
`), 0o755)
|
||||
|
||||
repoRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatalf("Abs(repository root) error = %v", err)
|
||||
}
|
||||
cmd := exec.Command("sh", "-c", script)
|
||||
cmd.Dir = repoRoot
|
||||
cmd.Env = append(os.Environ(),
|
||||
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
|
||||
"NPM_TAG=beta",
|
||||
"SEMVER=1.0.53-beta.7",
|
||||
"NPM_SEQUENCE="+sequencePath,
|
||||
"NPM_STATE="+statePath,
|
||||
"NPM_CALL_LOG="+npmLogPath,
|
||||
"SLEEP_CALL_LOG="+sleepLogPath,
|
||||
)
|
||||
output, runErr := cmd.CombinedOutput()
|
||||
if test.wantSuccess && runErr != nil {
|
||||
t.Fatalf("npm delivery verification error = %v\noutput:\n%s", runErr, output)
|
||||
}
|
||||
if !test.wantSuccess && runErr == nil {
|
||||
t.Fatalf("npm delivery verification unexpectedly succeeded\noutput:\n%s", output)
|
||||
}
|
||||
if test.wantOutput != "" && !strings.Contains(string(output), test.wantOutput) {
|
||||
t.Errorf("npm delivery verification output is missing %q:\n%s", test.wantOutput, output)
|
||||
}
|
||||
|
||||
npmLog, err := os.ReadFile(npmLogPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(%s) error = %v", npmLogPath, err)
|
||||
}
|
||||
npmCalls := strings.Split(strings.TrimSpace(string(npmLog)), "\n")
|
||||
if got := len(npmCalls); got != test.wantCalls {
|
||||
t.Errorf("npm view call count = %d, want %d; log:\n%s", got, test.wantCalls, npmLog)
|
||||
}
|
||||
if strings.Contains(string(npmLog), "dist-tag add") || strings.Contains(string(npmLog), "publish") {
|
||||
t.Errorf("delivery verification must remain read-only; log:\n%s", npmLog)
|
||||
}
|
||||
|
||||
sleepCalls := 0
|
||||
if sleepLog, err := os.ReadFile(sleepLogPath); err == nil {
|
||||
sleepCalls = len(strings.Fields(string(sleepLog)))
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Fatalf("ReadFile(%s) error = %v", sleepLogPath, err)
|
||||
}
|
||||
if sleepCalls != test.wantSleeps {
|
||||
t.Errorf("sleep call count = %d, want %d", sleepCalls, test.wantSleeps)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseStaysDraftUntilFinalizedAssetDigestsMatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user