From 5fde6222d40f85f9fcf25267af7951b1ae124a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=89=E6=BE=9C?= Date: Tue, 14 Jul 2026 18:34:40 +0800 Subject: [PATCH 01/14] fix: correct calendar rooms help metavar --- internal/app/root_help_test.go | 19 +++++++++++++++++++ internal/cli/schema_catalog.json | 8 ++++---- internal/helpers/calendar.go | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/internal/app/root_help_test.go b/internal/app/root_help_test.go index 02d8e487..d83f31be 100644 --- a/internal/app/root_help_test.go +++ b/internal/app/root_help_test.go @@ -47,6 +47,25 @@ func TestRootHelpHidesCompatibilityOnlyCommands(t *testing.T) { } } +func TestCalendarEventCreateHelpKeepsRoomsStringMetavar(t *testing.T) { + cmd := NewRootCommand() + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"calendar", "event", "create", "--help"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("calendar event create --help: %v\n%s", err, out.String()) + } + + help := out.String() + if !strings.Contains(help, "--rooms string") { + t.Fatalf("calendar event create help missing string metavar for --rooms:\n%s", help) + } + if strings.Contains(help, "--rooms room search") { + t.Fatalf("calendar event create help treated description text as --rooms metavar:\n%s", help) + } +} + func TestRootKeepsMainBranchChatCompatibilityCommands(t *testing.T) { root := NewRootCommand() listDirect := mustFindCommand(t, root, "chat", "message", "list-direct") diff --git a/internal/cli/schema_catalog.json b/internal/cli/schema_catalog.json index af0aba58..6c5894a9 100644 --- a/internal/cli/schema_catalog.json +++ b/internal/cli/schema_catalog.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:884195abd73e845d7839395ca832b15ad182853163f5631f8b3d198ec9db3457", + "source_hash": "sha256:0e4542da49f6dd68f25292d9c9bed90df9820b586409af5a1070d134e5db7bc5", "surface_hash": "sha256:e858be68701d256818eccd085a6f7f97655835b9c7cbbce6275ed930c2436dc3", "catalog": { "agent_metadata": { @@ -139979,7 +139979,7 @@ "type": "string" }, "rooms": { - "description": "会议室 roomId 列表,逗号分隔 (创建时直接预定,roomId 必须来自 `room search` 返回,若是循环会议,必须设置recurrence-end-date,避免长期预订)", + "description": "会议室 roomId 列表,逗号分隔 (创建时直接预定,roomId 必须来自“room search”返回,若是循环会议,必须设置recurrence-end-date,避免长期预订)", "field_provenance": { "description": { "candidates": [ @@ -139987,7 +139987,7 @@ "precedence": "cobra_contract", "selected": true, "source": "cobra_usage", - "value": "会议室 roomId 列表,逗号分隔 (创建时直接预定,roomId 必须来自 `room search` 返回,若是循环会议,必须设置recurrence-end-date,避免长期预订)" + "value": "会议室 roomId 列表,逗号分隔 (创建时直接预定,roomId 必须来自“room search”返回,若是循环会议,必须设置recurrence-end-date,避免长期预订)" }, { "precedence": "mcp_metadata", @@ -140005,7 +140005,7 @@ "precedence": "cobra_contract", "resolution": "highest_precedence", "source": "cobra_usage", - "value": "会议室 roomId 列表,逗号分隔 (创建时直接预定,roomId 必须来自 `room search` 返回,若是循环会议,必须设置recurrence-end-date,避免长期预订)" + "value": "会议室 roomId 列表,逗号分隔 (创建时直接预定,roomId 必须来自“room search”返回,若是循环会议,必须设置recurrence-end-date,避免长期预订)" }, "interface_type": { "candidates": [ diff --git a/internal/helpers/calendar.go b/internal/helpers/calendar.go index e9af5ff0..e0b3c975 100644 --- a/internal/helpers/calendar.go +++ b/internal/helpers/calendar.go @@ -1173,7 +1173,7 @@ func newCalendarCommand() *cobra.Command { eventCreateCmd.Flags().String("members", "", "") _ = eventCreateCmd.Flags().MarkHidden("members") eventCreateCmd.Flags().String("open-dingtalk-ids", "", "openDingTalkId 列表,逗号分隔 (与 --attendees 至少传一个)") - eventCreateCmd.Flags().String("rooms", "", "会议室 roomId 列表,逗号分隔 (创建时直接预定,roomId 必须来自 `room search` 返回,若是循环会议,必须设置recurrence-end-date,避免长期预订)") + eventCreateCmd.Flags().String("rooms", "", "会议室 roomId 列表,逗号分隔 (创建时直接预定,roomId 必须来自“room search”返回,若是循环会议,必须设置recurrence-end-date,避免长期预订)") eventCreateCmd.Flags().String("room-ids", "", "") _ = eventCreateCmd.Flags().MarkHidden("room-ids") eventCreateCmd.Flags().String("roomIds", "", "") From 22d19863fb09ffcf6abf1fb189d198a57e6bde2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:15:37 +0800 Subject: [PATCH 02/14] feat(release): add guarded prerelease and stable pipeline (#54) * feat(release): add guarded prerelease and stable pipeline * feat(release): add guided dws-release entry (cherry picked from commit f7fa7b78f325f3574f0487862fc3e65bba5cdc96) --- .github/actionlint.yaml | 6 + .github/workflows/ci.yml | 152 ++- .github/workflows/mirror-to-gitee.yml | 24 +- .github/workflows/publish-npm-release.yml | 71 -- .github/workflows/release.yml | 946 +++++++++++++++--- .github/workflows/sync-release-to-gitee.yml | 55 - .goreleaser.yaml | 9 +- CHANGELOG.md | 4 + Makefile | 49 +- build/homebrew.rb.tmpl | 42 +- docs/releasing.md | 113 +++ scripts/README.md | 12 +- scripts/release/build-and-publish-gitee.sh | 55 +- scripts/release/dws-release.sh | 322 ++++++ scripts/release/pack-npm-package.sh | 51 + scripts/release/post-goreleaser.sh | 59 +- scripts/release/prepare-changelog.sh | 97 ++ scripts/release/publish-gitee-local.sh | 71 +- scripts/release/release-contract.sh | 221 ++++ scripts/release/release-lib.sh | 167 ++++ scripts/release/release.sh | 353 +++++++ scripts/release/stage-npm-package.sh | 56 ++ scripts/release/sync-to-gitee.sh | 207 +++- scripts/release/sync-to-oss.sh | 160 ++- scripts/release/verify-delivered-stable.sh | 84 ++ .../release/verify-github-release-assets.sh | 35 + .../release/verify-github-tag-authority.sh | 37 + scripts/release/verify-package-managers.sh | 97 +- scripts/release/verify-release-artifacts.sh | 107 ++ test/scripts/package_script_test.go | 31 +- test/scripts/release_entry_test.go | 301 ++++++ test/scripts/release_script_test.go | 924 +++++++++++++++++ 32 files changed, 4327 insertions(+), 591 deletions(-) create mode 100644 .github/actionlint.yaml delete mode 100644 .github/workflows/publish-npm-release.yml delete mode 100644 .github/workflows/sync-release-to-gitee.yml create mode 100644 docs/releasing.md create mode 100755 scripts/release/dws-release.sh create mode 100755 scripts/release/pack-npm-package.sh create mode 100755 scripts/release/prepare-changelog.sh create mode 100755 scripts/release/release-contract.sh create mode 100755 scripts/release/release-lib.sh create mode 100755 scripts/release/release.sh create mode 100755 scripts/release/stage-npm-package.sh create mode 100755 scripts/release/verify-delivered-stable.sh create mode 100755 scripts/release/verify-github-release-assets.sh create mode 100755 scripts/release/verify-github-tag-authority.sh create mode 100755 scripts/release/verify-release-artifacts.sh create mode 100644 test/scripts/release_entry_test.go create mode 100644 test/scripts/release_script_test.go diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000..eea35124 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,6 @@ +paths: + .github/workflows/release.yml: + ignore: + # 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' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3a81156..f83fce6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,10 +63,17 @@ jobs: # package timeout aligned with the macOS race job so the Linux runner's # five-minute default does not expire while that gate is still making # progress. - run: go test -v -race -count=1 -timeout=10m ./cmd/... ./internal/... + run: | + go test -v -race -count=1 -timeout=10m \ + ./cmd/... \ + ./internal/... \ + ./pkg/... \ + ./test/unit/... + go test -v -count=1 -timeout=5m \ + ./test/cli/... - - name: Test release scripts - run: go test -v -count=1 -timeout=5m ./test/scripts + - name: Test engineering scripts + run: go test -v -count=1 -timeout=5m ./test/scripts/... test-darwin: name: Test (macOS auth/keychain) @@ -408,41 +415,25 @@ jobs: - name: Run edition contract tests run: go test -v -count=1 ./pkg/editiontest/... - ci-gate: - name: CI Gate - needs: - - lint - - test - - test-darwin - - test-windows - - coverage - - coverage-darwin - - coverage-windows - - policy - - interface-integrity - - cli-smoke - - mock-mcp-smoke - - edition-tests + code-check: + name: Code Check + needs: [lint, test, test-darwin, test-windows, coverage, policy, edition-tests] if: ${{ always() }} runs-on: ubuntu-latest timeout-minutes: 5 permissions: {} steps: - - name: Verify required checks + - name: Verify code checks env: LINT_RESULT: ${{ needs.lint.result }} TEST_RESULT: ${{ needs.test.result }} TEST_DARWIN_RESULT: ${{ needs.test-darwin.result }} TEST_WINDOWS_RESULT: ${{ needs.test-windows.result }} COVERAGE_RESULT: ${{ needs.coverage.result }} - COVERAGE_DARWIN_RESULT: ${{ needs.coverage-darwin.result }} - COVERAGE_WINDOWS_RESULT: ${{ needs.coverage-windows.result }} POLICY_RESULT: ${{ needs.policy.result }} - INTERFACE_INTEGRITY_RESULT: ${{ needs.interface-integrity.result }} - CLI_SMOKE_RESULT: ${{ needs.cli-smoke.result }} - MOCK_MCP_SMOKE_RESULT: ${{ needs.mock-mcp-smoke.result }} EDITION_TESTS_RESULT: ${{ needs.edition-tests.result }} run: | + set -eu failed=0 for check in \ "Lint:$LINT_RESULT" \ @@ -450,12 +441,7 @@ jobs: "Test (macOS auth/keychain):$TEST_DARWIN_RESULT" \ "Test (Windows):$TEST_WINDOWS_RESULT" \ "Coverage:$COVERAGE_RESULT" \ - "Coverage (macOS):$COVERAGE_DARWIN_RESULT" \ - "Coverage (Windows):$COVERAGE_WINDOWS_RESULT" \ "Policy Check:$POLICY_RESULT" \ - "Interface Integrity:$INTERFACE_INTEGRITY_RESULT" \ - "CLI Smoke:$CLI_SMOKE_RESULT" \ - "Mock MCP Smoke:$MOCK_MCP_SMOKE_RESULT" \ "Edition Contract Tests:$EDITION_TESTS_RESULT" do name="${check%%:*}" @@ -467,6 +453,114 @@ jobs: done test "$failed" -eq 0 + command-compatibility: + name: Command Compatibility + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: read + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch official release tags + run: | + git fetch --force --no-tags \ + https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ + '+refs/tags/v*:refs/tags/v*' + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Check command compatibility + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PUSH_BEFORE_SHA: ${{ github.event.before }} + run: | + set -eu + base_ref="$PUSH_BEFORE_SHA" + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + base_ref="$PR_BASE_SHA" + fi + stable_ref="$(git tag --merged HEAD --list 'v*' --sort=-version:refname | awk '/^v[0-9]+\.[0-9]+\.[0-9]+$/ { print; exit }')" + test -n "$base_ref" + test -n "$stable_ref" + stable_commit="$(git rev-parse "$stable_ref^{commit}")" + ./scripts/release/verify-delivered-stable.sh "$stable_ref" "$stable_commit" + ./scripts/policy/check-command-compatibility.sh \ + --base-ref "$base_ref" \ + --stable-ref "$stable_ref" + + cli-smoke: + name: CLI Smoke + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run CLI smoke tests + run: go test -count=1 -timeout=5m ./test/smoke/... + + mock-mcp-smoke: + name: Mock MCP Smoke + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run Mock MCP smoke tests + run: go test -count=1 -timeout=5m ./test/mock_mcp/... + + ci-gate: + name: CI Gate + needs: [code-check, command-compatibility, cli-smoke, mock-mcp-smoke] + if: ${{ always() }} + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + steps: + - name: Verify required checks + env: + CODE_CHECK_RESULT: ${{ needs.code-check.result }} + COMMAND_COMPATIBILITY_RESULT: ${{ needs.command-compatibility.result }} + CLI_SMOKE_RESULT: ${{ needs.cli-smoke.result }} + MOCK_MCP_SMOKE_RESULT: ${{ needs.mock-mcp-smoke.result }} + run: | + set -eu + failed=0 + for check in \ + "Code Check:$CODE_CHECK_RESULT" \ + "Command Compatibility:$COMMAND_COMPATIBILITY_RESULT" \ + "CLI Smoke:$CLI_SMOKE_RESULT" \ + "Mock MCP Smoke:$MOCK_MCP_SMOKE_RESULT" + do + name="${check%%:*}" + result="${check#*:}" + printf '%s: %s\n' "$name" "$result" + if [ "$result" != "success" ]; then + failed=1 + fi + done + test "$failed" -eq 0 + notify-downstream: name: Notify Wukong Overlay needs: [ci-gate] diff --git a/.github/workflows/mirror-to-gitee.yml b/.github/workflows/mirror-to-gitee.yml index 2ae83b28..c440312f 100644 --- a/.github/workflows/mirror-to-gitee.yml +++ b/.github/workflows/mirror-to-gitee.yml @@ -1,4 +1,5 @@ -# 把本仓库代码自动镜像到 Gitee,供国内用户访问(raw 脚本入口 + tags)。 +# 把本仓库 main 代码自动镜像到 Gitee,供国内用户访问 raw 脚本入口。 +# Release tag 与附件只由 release.yml 的受控 publication queue 发布。 # 用 HTTPS + 令牌直接 git push(无需 SSH key),复用已配置的 secret: # GITEE_TOKEN —— Gitee 私人令牌(勾 projects) # GITEE_USER —— 令牌所属 Gitee 用户名(用于 https 推送鉴权) @@ -10,8 +11,6 @@ on: push: branches: - main - tags: - - 'v*' schedule: - cron: '0 18 * * *' workflow_dispatch: @@ -23,13 +22,14 @@ concurrency: jobs: mirror: runs-on: ubuntu-latest + if: ${{ github.ref_name == github.event.repository.default_branch && github.repository_owner == 'DingTalk-Real-AI' }} # GitHub Actions 不允许在 job-level if 直接引用 secrets,故先用 env 暴露再在 step 守卫。 env: GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} GITEE_USER: ${{ secrets.GITEE_USER }} GITEE_REPO: ${{ secrets.GITEE_REPO }} steps: - - name: Checkout (full history + tags) + - name: Checkout main history if: env.GITEE_TOKEN != '' uses: actions/checkout@v4 with: @@ -41,14 +41,7 @@ jobs: set -eu REMOTE="https://${GITEE_USER}:${GITEE_TOKEN}@gitee.com/${GITEE_REPO}.git" - if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then - VERSION="$GITHUB_REF_NAME" ./scripts/release/sync-gitee-tag.sh - echo "✅ 已镜像 tag ${GITHUB_REF_NAME} 到 Gitee ${GITEE_REPO}" - exit 0 - fi - - # 取到 main 与所有 tag(落到 origin/* 与本地 tags,避免推当前分支引用冲突) - git fetch --force --tags origin 'refs/heads/main:refs/remotes/origin/main' + git fetch --force origin 'refs/heads/main:refs/remotes/origin/main' # Gitee 专属分支:在 origin/main 之上叠加一个 README 本地化 commit。 # GitHub 那份 README 不变;只有推往 Gitee 的副本被改写。 @@ -80,9 +73,6 @@ jobs: git add README.md README_zh.md 2>/dev/null || true git commit -m "docs(gitee): localize install commands + coverage badge for Gitee mirror" || true - # 镜像对齐(force:Gitee 始终跟随 GitHub + Gitee 专属 README 本地化) + # main 镜像对齐;release tag 由 release.yml 单独校验后创建,禁止在这里 force。 git push --force "$REMOTE" 'gitee-main:refs/heads/main' - # Release tags are immutable. Push only missing tags and fail closed - # on a conflicting existing ref instead of trying to move it. - timeout --signal=TERM 180s git push --tags "$REMOTE" - echo "✅ 已镜像 main(+Gitee README 本地化) + tags 到 Gitee ${GITEE_REPO}" + echo "✅ 已镜像 main(含 Gitee README 本地化)到 Gitee ${GITEE_REPO}" diff --git a/.github/workflows/publish-npm-release.yml b/.github/workflows/publish-npm-release.yml deleted file mode 100644 index 20cd62c8..00000000 --- a/.github/workflows/publish-npm-release.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Publish npm release - -on: - workflow_dispatch: - inputs: - version: - description: "Release tag to publish to npm (e.g. v1.0.48)" - required: true - type: string - -permissions: - contents: read - -jobs: - publish-npm: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Download GitHub release assets - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -eu - mkdir -p dist - gh release download "${{ inputs.version }}" \ - --repo "${{ github.repository }}" \ - --dir dist \ - --pattern 'dws-*' \ - --pattern 'checksums.txt' \ - --clobber - ls -la dist - - - name: Stage npm package - run: | - set -eu - version="${{ inputs.version }}" - semver="${version#v}" - pkg_root="dist/npm/dingtalk-workspace-cli" - rm -rf "$pkg_root" - mkdir -p "$pkg_root/assets" "$pkg_root/bin" - cp build/npm/install.js "$pkg_root/install.js" - cp build/npm/bin/dws.js "$pkg_root/bin/dws.js" - cp build/npm/README.md "$pkg_root/README.md" - sed "s|__VERSION__|${semver}|g" build/npm/package.json.tmpl > "$pkg_root/package.json" - cp dist/dws-* "$pkg_root/assets/" - cp dist/checksums.txt "$pkg_root/assets/" - test -f "$pkg_root/assets/dws-skills.zip" - cat "$pkg_root/package.json" - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - registry-url: "https://registry.npmjs.org" - - - name: Publish stable to npm - if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(inputs.version, '-') }} - working-directory: dist/npm/dingtalk-workspace-cli - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Publish prerelease to npm beta - if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(inputs.version, '-') }} - working-directory: dist/npm/dingtalk-workspace-cli - run: npm publish --access public --tag beta - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9471a744..dc9bde54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,43 +7,239 @@ on: workflow_dispatch: inputs: repair_npm_version: - description: "Only publish an existing release to npm, e.g. v1.0.48" + description: "Recover npm for a public immutable release without moving latest/beta" required: false type: string permissions: - contents: write + contents: read + +# Queue tag releases and npm repairs behind one publication lock. The local +# entrypoint also refuses to tag while a Release run is active. +concurrency: + group: dws-release-publication + queue: max jobs: - release: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }} + release-contract: + if: ${{ github.event_name == 'push' }} runs-on: ubuntu-latest - timeout-minutes: 30 - + timeout-minutes: 20 + permissions: + actions: read + checks: read + contents: read + outputs: + channel: ${{ steps.contract.outputs.channel }} + previous_stable: ${{ steps.contract.outputs.previous_stable }} + previous_stable_commit: ${{ steps.contract.outputs.previous_stable_commit }} + from_beta: ${{ steps.contract.outputs.from_beta }} + from_beta_commit: ${{ steps.contract.outputs.from_beta_commit }} steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Check Homebrew PR automation token - if: ${{ github.repository_owner == 'DingTalk-Real-AI' }} - # Organization policy intentionally prevents the broad, built-in - # GITHUB_TOKEN from creating PRs. Keep Formula automation on a - # repository-scoped token instead of weakening that policy. - env: - HOMEBREW_PR_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }} + - name: Fetch release branch and official tags run: | - if [ -z "${HOMEBREW_PR_TOKEN:-}" ]; then - echo "HOMEBREW_PR_TOKEN is required to open Formula PRs from official releases" >&2 - exit 1 - fi + git fetch --force origin \ + '+refs/heads/main:refs/remotes/origin/main' + git fetch --force --no-tags \ + https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ + '+refs/tags/v*:refs/tags/v*' + + - name: Validate release contract + id: contract + run: | + set -eu + . ./scripts/release/release-lib.sh + channel="$(release_channel_for_version "$GITHUB_REF_NAME")" + ./scripts/release/release-contract.sh \ + --channel "$channel" \ + --version "$GITHUB_REF_NAME" \ + --context ci \ + --remote origin \ + --branch main \ + --notes-output "$RUNNER_TEMP/release-notes.md" \ + --metadata-output "$GITHUB_OUTPUT" + + - name: Verify remote annotated tag authority + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./scripts/release/verify-github-tag-authority.sh "$GITHUB_REF_NAME" "$GITHUB_SHA" + + - name: Require successful CI Gate on the sealed commit + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const { data } = await github.rest.checks.listForRef({ + owner, + repo, + ref: context.sha, + check_name: "CI Gate", + filter: "latest", + per_page: 100, + }); + const passed = data.check_runs.some( + (run) => run.name === "CI Gate" && run.conclusion === "success", + ); + if (!passed) { + core.setFailed(`CI Gate has not succeeded for ${context.sha}`); + } + + - name: Require delivered previous stable baseline + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PREVIOUS_STABLE: ${{ steps.contract.outputs.previous_stable }} + PREVIOUS_STABLE_COMMIT: ${{ steps.contract.outputs.previous_stable_commit }} + run: ./scripts/release/verify-delivered-stable.sh "$PREVIOUS_STABLE" "$PREVIOUS_STABLE_COMMIT" + + - name: Require immutable releases governance + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const response = await github.request( + "GET /repos/{owner}/{repo}/immutable-releases", + { + owner, + repo, + headers: { "X-GitHub-Api-Version": "2026-03-10" }, + }, + ); + if (response.data.enabled !== true) { + core.setFailed("Immutable releases must be enabled before publishing"); + } + + - name: Require successful beta delivery before stable promotion + if: ${{ steps.contract.outputs.channel == 'stable' }} + uses: actions/github-script@v7 + env: + FROM_BETA: ${{ steps.contract.outputs.from_beta }} + FROM_BETA_COMMIT: ${{ steps.contract.outputs.from_beta_commit }} + with: + script: | + const { owner, repo } = context.repo; + const expectedAssets = new Set([ + "dws-darwin-amd64.tar.gz", + "dws-darwin-arm64.tar.gz", + "dws-linux-amd64.tar.gz", + "dws-linux-arm64.tar.gz", + "dws-windows-amd64.zip", + "dws-windows-arm64.zip", + "dws-skills.zip", + "checksums.txt", + ]); + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner, + repo, + tag: process.env.FROM_BETA, + }); + if (release.draft || !release.prerelease || !release.immutable) { + core.setFailed(`${process.env.FROM_BETA} is not a published immutable prerelease`); + return; + } + const assetNames = release.assets.map((asset) => asset.name); + const names = new Set(assetNames); + const missing = [...expectedAssets].filter((name) => !names.has(name)); + const extra = assetNames.filter((name) => !expectedAssets.has(name)); + if ( + missing.length > 0 || + extra.length > 0 || + assetNames.length !== expectedAssets.size + ) { + core.setFailed( + `beta release asset set differs: missing=${missing.join(",")} extra=${extra.join(",")}`, + ); + return; + } + const runs = await github.paginate(github.rest.actions.listWorkflowRuns, { + owner, + repo, + workflow_id: "release.yml", + branch: process.env.FROM_BETA, + event: "push", + status: "completed", + per_page: 100, + }); + const passed = runs.some( + (run) => + run.head_sha === process.env.FROM_BETA_COMMIT && + run.head_branch === process.env.FROM_BETA && + run.conclusion === "success", + ); + if (!passed) { + core.setFailed(`Release workflow did not succeed for ${process.env.FROM_BETA}`); + } - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod + - name: Test release automation + run: go test -v -count=1 -timeout=5m ./test/scripts/... -run '^TestRelease' + + - name: Verify module files are tidy + run: | + go mod tidy + git diff --exit-code -- go.mod go.sum + + - name: Verify released command tree + env: + PREVIOUS_STABLE: ${{ steps.contract.outputs.previous_stable }} + run: | + set -eu + test -n "$PREVIOUS_STABLE" + ./scripts/policy/check-command-compatibility.sh \ + --base-ref HEAD \ + --stable-ref "$PREVIOUS_STABLE" + + release: + name: Build signed release artifacts + if: ${{ github.event_name == 'push' }} + needs: [release-contract] + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch release branch and official tags + run: | + git fetch --force origin \ + '+refs/heads/main:refs/remotes/origin/main' + git fetch --force --no-tags \ + https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ + '+refs/tags/v*:refs/tags/v*' + + - name: Revalidate contract and render release notes + run: | + ./scripts/release/release-contract.sh \ + --channel "${{ needs.release-contract.outputs.channel }}" \ + --version "$GITHUB_REF_NAME" \ + --context ci \ + --remote origin \ + --branch main \ + --notes-output "$RUNNER_TEMP/release-notes.md" + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + - name: Install archive tooling run: sudo apt-get update && sudo apt-get install -y zip unzip @@ -94,11 +290,11 @@ jobs: echo "DWS_APPLE_CERTIFICATE_P12=$certificate_path" >> "$GITHUB_ENV" echo "DWS_APPLE_CERTIFICATE_PASSWORD_FILE=$password_path" >> "$GITHUB_ENV" - - name: Run GoReleaser + - name: Build release artifacts without publishing uses: goreleaser/goreleaser-action@v6 with: - version: "~> v2" - args: release --clean + version: "v2.16.0" + args: release --clean --skip=publish --release-notes=${{ runner.temp }}/release-notes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -106,6 +302,7 @@ jobs: run: ./scripts/release/post-goreleaser.sh env: DWS_PACKAGE_VERSION: ${{ github.ref_name }} + DWS_RELEASE_BASE_URL: https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }} DWS_REQUIRE_DEVELOPER_ID_SIGNING: ${{ github.repository_owner == 'DingTalk-Real-AI' }} - name: Remove Apple Developer ID certificate @@ -114,14 +311,16 @@ jobs: rm -f "$RUNNER_TEMP/dws-developer-id.p12" rm -f "$RUNNER_TEMP/dws-developer-id-password" - # GoReleaser uploads the original archives to a Draft before - # post-goreleaser.sh replaces the Darwin binaries. Re-upload every changed - # file, verify the Draft digests, and keep it private for Apple validation. - - name: Upload finalized signed assets to release + - name: Verify final release artifacts + run: ./scripts/release/verify-release-artifacts.sh "$GITHUB_REF_NAME" + + - name: Verify npm package before sealing GitHub Release + run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$GITHUB_REF_NAME" + + - name: Reverify remote tag before sealing GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DWS_PUBLISH_RELEASE: "false" - run: ./scripts/release/finalize-github-release.sh + run: ./scripts/release/verify-github-tag-authority.sh "$GITHUB_REF_NAME" "$GITHUB_SHA" - name: Preserve finalized distribution files uses: actions/upload-artifact@v4 @@ -132,24 +331,19 @@ jobs: retention-days: 1 verify-darwin-signatures: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }} + name: Verify Apple Developer ID signatures + if: ${{ github.event_name == 'push' }} needs: release runs-on: macos-latest timeout-minutes: 10 - + permissions: + contents: read steps: - - name: Download finalized Darwin assets from Draft release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - mkdir -p dist - gh release download "$GITHUB_REF_NAME" \ - --repo "$GITHUB_REPOSITORY" \ - --dir dist \ - --pattern 'dws-darwin-amd64.tar.gz' \ - --pattern 'dws-darwin-arm64.tar.gz' \ - --clobber + - name: Restore finalized distribution files + uses: actions/download-artifact@v4 + with: + name: finalized-release-dist + path: dist - name: Verify finalized Darwin signatures with Apple codesign run: | @@ -165,69 +359,36 @@ jobs: done publish-release: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }} - needs: - - release - - verify-darwin-signatures + name: Publish immutable GitHub Release + if: ${{ github.event_name == 'push' }} + needs: [release-contract, release, verify-darwin-signatures] runs-on: ubuntu-latest - # The optional Gitee fallback has its own bounded retry budget and may be - # enabled during a cross-border incident. Normal releases skip that step. - timeout-minutes: 120 - + timeout-minutes: 30 + permissions: + contents: write steps: - - name: Check out repository + - name: Check out sealed release source uses: actions/checkout@v4 - - - name: Restore finalized distribution files - uses: actions/download-artifact@v4 with: - name: finalized-release-dist - path: dist + fetch-depth: 0 - - name: Publish verified Draft release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false + - name: Fetch release branch and official tags + run: | + git fetch --force origin \ + '+refs/heads/main:refs/remotes/origin/main' + git fetch --force --no-tags \ + https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ + '+refs/tags/v*:refs/tags/v*' - - name: Open stable Homebrew formula PR - # Beta builds must never replace the stable Homebrew formula. Formula - # updates use the normal PR path instead of writing main from a tag job. - if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(github.ref_name, '-') }} - run: ./scripts/release/publish-homebrew-formula.sh - env: - DWS_TAP_REPO_URL: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git - DWS_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }} - DWS_TAP_PR_REPOSITORY: ${{ github.repository }} - DWS_TAP_PR_BRANCH: "automation/homebrew-${{ github.ref_name }}" - DWS_TAP_PR_TITLE: "chore: update Homebrew formula for ${{ github.ref_name }}" - DWS_TAP_COMMIT_MESSAGE: "chore: update formula for ${{ github.ref_name }}" - - - name: Open beta Homebrew formula PR - # Keep beta in a separately named, keg-only Formula so it cannot replace the - # stable dws link for ordinary Homebrew users. - if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(github.ref_name, '-') }} - run: ./scripts/release/publish-homebrew-formula.sh - env: - DWS_FORMULA_SOURCE: dist/homebrew/dingtalk-workspace-cli-beta.rb - DWS_TAP_FORMULA_PATH: Formula/dingtalk-workspace-cli-beta.rb - DWS_TAP_REPO_URL: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git - DWS_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }} - DWS_TAP_PR_REPOSITORY: ${{ github.repository }} - DWS_TAP_PR_BRANCH: "automation/homebrew-beta-${{ github.ref_name }}" - DWS_TAP_PR_TITLE: "chore: update Homebrew beta formula for ${{ github.ref_name }}" - DWS_TAP_COMMIT_MESSAGE: "chore: update beta formula for ${{ github.ref_name }}" - - - name: Sync release to China OSS mirror - # 自动同步到国内镜像,供 install.sh 的 DWS_RELEASE_BASE 开关消费。 - # 脚本自带门控:未配置 OSS_* secret 时优雅跳过,不影响海外发布。 - run: ./scripts/release/sync-to-oss.sh - env: - VERSION: ${{ github.ref_name }} - OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }} - OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} - OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }} - OSS_BUCKET: ${{ secrets.OSS_BUCKET }} - OSS_PREFIX: ${{ secrets.OSS_PREFIX }} + - name: Revalidate contract and render release notes + run: | + ./scripts/release/release-contract.sh \ + --channel "${{ needs.release-contract.outputs.channel }}" \ + --version "$GITHUB_REF_NAME" \ + --context ci \ + --remote origin \ + --branch main \ + --notes-output "$RUNNER_TEMP/release-notes.md" - name: Setup Node.js uses: actions/setup-node@v4 @@ -235,22 +396,343 @@ jobs: node-version: "20" registry-url: "https://registry.npmjs.org" - - name: Publish stable to npm - # 只有官方仓库发 npm;fork(dev 预览)没有 NPM_TOKEN,跳过以免红叉。 - # 必须在 Gitee mirror 前发布:Gitee 附件上传偶发长时间挂住,不能阻塞 npm/latest。 - if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(github.ref_name, '-') }} - working-directory: dist/npm/dingtalk-workspace-cli - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Install archive tooling + run: sudo apt-get update && sudo apt-get install -y zip unzip - - name: Publish prerelease to npm beta - # 预发布版本不能更新 npm latest,避免普通 npm 安装链路拿到 beta。 - if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(github.ref_name, '-') }} - working-directory: dist/npm/dingtalk-workspace-cli - run: npm publish --access public --tag beta + - name: Restore finalized distribution files + uses: actions/download-artifact@v4 + with: + name: finalized-release-dist + path: dist + + - name: Verify finalized release artifacts before publication + run: ./scripts/release/verify-release-artifacts.sh "$GITHUB_REF_NAME" + + - name: Verify npm package before sealing GitHub Release + run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$GITHUB_REF_NAME" + + - name: Reverify remote tag before sealing GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./scripts/release/verify-github-tag-authority.sh "$GITHUB_REF_NAME" "$GITHUB_SHA" + + - name: Publish or reuse immutable GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_CHANNEL: ${{ needs.release-contract.outputs.channel }} + run: | + set -eu + expected_prerelease=false + prerelease_flag="" + if [ "$RELEASE_CHANNEL" = "prerelease" ]; then + expected_prerelease=true + prerelease_flag="--prerelease" + fi + + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + is_draft="$(gh release view "$GITHUB_REF_NAME" --json isDraft --jq .isDraft)" + is_prerelease="$(gh release view "$GITHUB_REF_NAME" --json isPrerelease --jq .isPrerelease)" + if [ "$is_prerelease" != "$expected_prerelease" ]; then + echo "Existing release channel does not match $RELEASE_CHANNEL." >&2 + exit 1 + fi + if [ "$is_draft" = "false" ]; then + is_immutable="$( + gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/$GITHUB_REPOSITORY/releases/tags/$GITHUB_REF_NAME" \ + --jq .immutable + )" + if [ "$is_immutable" != "true" ]; then + echo "Published release is mutable; refusing to reuse its assets." >&2 + exit 1 + fi + remote_body="$(gh release view "$GITHUB_REF_NAME" --json body --jq .body)" + local_body="$(cat "$RUNNER_TEMP/release-notes.md")" + if [ "$remote_body" != "$local_body" ]; then + echo "Published release notes differ from the sealed CHANGELOG." >&2 + exit 1 + fi + published_dir="$RUNNER_TEMP/published-release" + mkdir -p "$published_dir" + gh release download "$GITHUB_REF_NAME" \ + --dir "$published_dir" \ + --pattern 'dws-*' \ + --pattern 'checksums.txt' \ + --clobber + DWS_PACKAGE_DIST_DIR="$published_dir" \ + ./scripts/release/verify-release-artifacts.sh "$GITHUB_REF_NAME" + for asset in "$published_dir"/dws-* "$published_dir"/checksums.txt; do + cp "$asset" dist/ + done + DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ + DWS_PACKAGE_SOURCE_ROOT="$GITHUB_WORKSPACE" \ + ./scripts/release/stage-npm-package.sh "$GITHUB_REF_NAME" + echo "Published GitHub Release is immutable and verified; reusing its exact assets." + exit 0 + fi + gh release edit "$GITHUB_REF_NAME" \ + --title "$GITHUB_REF_NAME" \ + --notes-file "$RUNNER_TEMP/release-notes.md" + else + # shellcheck disable=SC2086 + gh release create "$GITHUB_REF_NAME" \ + --verify-tag \ + --draft \ + --title "$GITHUB_REF_NAME" \ + --notes-file "$RUNNER_TEMP/release-notes.md" \ + $prerelease_flag + fi + gh release upload "$GITHUB_REF_NAME" \ + dist/dws-*.tar.gz \ + dist/dws-windows-*.zip \ + dist/dws-skills.zip \ + dist/checksums.txt \ + --clobber + ./scripts/release/verify-github-release-assets.sh "$GITHUB_REF_NAME" + sealed_upload="$RUNNER_TEMP/sealed-upload" + rm -rf "$sealed_upload" + mkdir -p "$sealed_upload" + gh release download "$GITHUB_REF_NAME" \ + --dir "$sealed_upload" \ + --pattern 'dws-*' \ + --pattern 'checksums.txt' \ + --clobber + DWS_PACKAGE_DIST_DIR="$sealed_upload" \ + ./scripts/release/verify-release-artifacts.sh "$GITHUB_REF_NAME" + for remote_asset in "$sealed_upload"/dws-* "$sealed_upload"/checksums.txt; do + local_asset="dist/$(basename "$remote_asset")" + cmp -s "$local_asset" "$remote_asset" || { + echo "Uploaded GitHub asset differs from local sealed bytes: $(basename "$remote_asset")" >&2 + exit 1 + } + done + gh release edit "$GITHUB_REF_NAME" --draft=false + + - name: Require immutable published GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -eu + immutable="$( + gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/$GITHUB_REPOSITORY/releases/tags/$GITHUB_REF_NAME" \ + --jq .immutable + )" + test "$immutable" = true || { + echo "GitHub Release is not immutable; refusing downstream publication." >&2 + exit 1 + } + ./scripts/release/verify-github-release-assets.sh "$GITHUB_REF_NAME" + ./scripts/release/verify-github-tag-authority.sh "$GITHUB_REF_NAME" "$GITHUB_SHA" + + - name: Reverify exact immutable npm package + run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$GITHUB_REF_NAME" + + publish-channels: + name: Publish npm and mirrors + if: ${{ github.event_name == 'push' }} + needs: [release-contract, publish-release] + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - name: Check out sealed release source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch release branch and official tags + run: | + git fetch --force origin \ + '+refs/heads/main:refs/remotes/origin/main' + git fetch --force --no-tags \ + https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ + '+refs/tags/v*:refs/tags/v*' + + - name: Revalidate sealed contract + run: | + ./scripts/release/release-contract.sh \ + --channel "${{ needs.release-contract.outputs.channel }}" \ + --version "$GITHUB_REF_NAME" \ + --context ci \ + --remote origin \ + --branch main + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + + - name: Install archive tooling + run: sudo apt-get update && sudo apt-get install -y zip unzip + + - name: Download and verify immutable GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -eu + release_state="$( + gh api -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/$GITHUB_REPOSITORY/releases/tags/$GITHUB_REF_NAME" \ + --jq '[.draft, .immutable] | @tsv' + )" + test "$release_state" = "$(printf 'false\ttrue')" || { + echo "GitHub Release must be public and immutable before channel publication." >&2 + exit 1 + } + ./scripts/release/verify-github-release-assets.sh "$GITHUB_REF_NAME" + ./scripts/release/verify-github-tag-authority.sh "$GITHUB_REF_NAME" "$GITHUB_SHA" + mkdir -p dist + gh release download "$GITHUB_REF_NAME" \ + --dir dist \ + --pattern 'dws-*' \ + --pattern 'checksums.txt' \ + --clobber + ./scripts/release/verify-release-artifacts.sh "$GITHUB_REF_NAME" + DWS_PACKAGE_SOURCE_ROOT="$GITHUB_WORKSPACE" \ + DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ + ./scripts/release/stage-npm-package.sh "$GITHUB_REF_NAME" + + - name: Verify immutable npm package without publication credentials + run: | + ./scripts/release/verify-package-managers.sh \ + --npm-only \ + --expected-version "$GITHUB_REF_NAME" + + - name: Inspect npm channel state + id: npm-state + if: ${{ github.repository_owner == 'DingTalk-Real-AI' }} + env: + RELEASE_CHANNEL: ${{ needs.release-contract.outputs.channel }} + run: | + set -eu + . ./scripts/release/release-lib.sh + semver="${GITHUB_REF_NAME#v}" + npm_tag=latest + if [ "$RELEASE_CHANNEL" = "prerelease" ]; then npm_tag=beta; fi + current="$(npm view dingtalk-workspace-cli "dist-tags.$npm_tag")" + local_integrity="$( + ./scripts/release/pack-npm-package.sh \ + "$GITHUB_WORKSPACE/dist/npm/dingtalk-workspace-cli" \ + "$RUNNER_TEMP/dingtalk-workspace-cli.tgz" + )" + version_error="$RUNNER_TEMP/npm-version-error.log" + if existing="$(npm view "dingtalk-workspace-cli@$semver" version 2>"$version_error")"; then + if [ "$existing" != "$semver" ]; then + echo "npm returned unexpected version $existing for $semver" >&2 + exit 1 + fi + elif grep -Eq 'E404|404 Not Found' "$version_error"; then + existing="" + else + cat "$version_error" >&2 + exit 1 + fi + if [ -n "$existing" ]; then + remote_integrity="$(npm view "dingtalk-workspace-cli@$semver" dist.integrity)" + if [ -z "$remote_integrity" ] || [ "$remote_integrity" != "$local_integrity" ]; then + echo "npm $semver integrity differs from the sealed package." >&2 + echo "local: $local_integrity" >&2 + echo "remote: $remote_integrity" >&2 + exit 1 + fi + advance_channel=false + if [ "$current" = "$semver" ]; then + : + elif [ -n "$current" ] && release_version_is_greater "v$current" "$GITHUB_REF_NAME"; then + echo "npm $npm_tag already points to newer v$current; leaving it unchanged." + elif [ -z "$current" ] || release_version_is_greater "$GITHUB_REF_NAME" "v$current"; then + advance_channel=true + else + echo "Cannot reconcile npm $npm_tag=$current with $GITHUB_REF_NAME" >&2 + exit 1 + fi + echo "published=true" >> "$GITHUB_OUTPUT" + echo "advance_channel=$advance_channel" >> "$GITHUB_OUTPUT" + echo "npm_tag=$npm_tag" >> "$GITHUB_OUTPUT" + echo "semver=$semver" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ -n "$current" ] && ! release_version_is_greater "$GITHUB_REF_NAME" "v$current"; then + echo "$GITHUB_REF_NAME would move npm $npm_tag backwards from v$current" >&2 + exit 1 + fi + echo "published=false" >> "$GITHUB_OUTPUT" + echo "advance_channel=false" >> "$GITHUB_OUTPUT" + echo "npm_tag=$npm_tag" >> "$GITHUB_OUTPUT" + echo "semver=$semver" >> "$GITHUB_OUTPUT" + + - name: Publish missing version to npm channel + if: ${{ github.repository_owner == 'DingTalk-Real-AI' && steps.npm-state.outputs.published != 'true' }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TAG: ${{ steps.npm-state.outputs.npm_tag }} + SEMVER: ${{ steps.npm-state.outputs.semver }} + run: | + set -eu + . ./scripts/release/release-lib.sh + current="$(npm view dingtalk-workspace-cli "dist-tags.$NPM_TAG")" + if [ -n "$current" ] && [ "$current" != "$SEMVER" ] && ! release_version_is_greater "v$SEMVER" "v$current"; then + echo "Refusing to move npm $NPM_TAG backwards from v$current to v$SEMVER" >&2 + exit 1 + fi + npm publish "$RUNNER_TEMP/dingtalk-workspace-cli.tgz" \ + --access public \ + --tag "$NPM_TAG" \ + --ignore-scripts \ + --registry=https://registry.npmjs.org + + - name: Advance npm channel for an existing exact version + if: ${{ github.repository_owner == 'DingTalk-Real-AI' && steps.npm-state.outputs.advance_channel == 'true' }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TAG: ${{ steps.npm-state.outputs.npm_tag }} + SEMVER: ${{ steps.npm-state.outputs.semver }} + run: | + set -eu + . ./scripts/release/release-lib.sh + current="$(npm view dingtalk-workspace-cli "dist-tags.$NPM_TAG")" + if [ "$current" = "$SEMVER" ]; then exit 0; fi + if [ -n "$current" ] && release_version_is_greater "v$current" "v$SEMVER"; then + echo "npm $NPM_TAG advanced to newer v$current; leaving it unchanged." + exit 0 + fi + if [ -n "$current" ] && ! release_version_is_greater "v$SEMVER" "v$current"; then + echo "Refusing unsafe npm $NPM_TAG transition from v$current to v$SEMVER" >&2 + exit 1 + fi + npm dist-tag add "dingtalk-workspace-cli@$SEMVER" "$NPM_TAG" \ + --registry=https://registry.npmjs.org + + - name: Verify npm channel delivery + if: ${{ github.repository_owner == 'DingTalk-Real-AI' }} + env: + NPM_TAG: ${{ steps.npm-state.outputs.npm_tag }} + SEMVER: ${{ steps.npm-state.outputs.semver }} + 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." + + - name: Sync release artifacts to China OSS mirror + run: ./scripts/release/sync-to-oss.sh + env: + VERSION: ${{ github.ref_name }} + DWS_RELEASE_CHANNEL: ${{ needs.release-contract.outputs.channel }} + OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }} + OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} + OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }} + OSS_BUCKET: ${{ secrets.OSS_BUCKET }} + OSS_PREFIX: ${{ secrets.OSS_PREFIX }} + DWS_REQUIRE_OSS: ${{ github.repository_owner == 'DingTalk-Real-AI' && '1' || '0' }} mirror-gitee-release: # Keep the optional cross-border fallback out of publish-release so all @@ -274,52 +756,160 @@ jobs: path: dist - name: Mirror release to Gitee (China) - timeout-minutes: 100 + if: ${{ vars.ENABLE_GITEE_UPLOAD_FALLBACK == 'true' }} + timeout-minutes: 20 run: ./scripts/release/sync-to-gitee.sh env: VERSION: ${{ github.ref_name }} GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} GITEE_USER: ${{ secrets.GITEE_USER }} GITEE_REPO: ${{ secrets.GITEE_REPO }} + DWS_REQUIRE_GITEE: "1" repair-npm: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.repair_npm_version != '' }} + if: ${{ github.event_name == 'workflow_dispatch' && inputs.repair_npm_version != '' && github.ref_name == github.event.repository.default_branch && github.repository_owner == 'DingTalk-Real-AI' }} runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 20 + permissions: + actions: read + contents: read steps: - - name: Check out repository + - name: Check out trusted release tooling uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + path: tooling - - name: Download GitHub release assets + - name: Validate repair version + id: version + working-directory: tooling env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ inputs.repair_npm_version }} run: | set -eu - mkdir -p dist - gh release download "${{ inputs.repair_npm_version }}" \ - --repo "${{ github.repository }}" \ - --dir dist \ - --pattern 'dws-*' \ - --pattern 'checksums.txt' \ - --clobber - ls -la dist + . ./scripts/release/release-lib.sh + if ! release_is_stable_version "$VERSION" && ! release_is_prerelease_version "$VERSION"; then + echo "Invalid release version: $VERSION" >&2 + exit 2 + fi + echo "semver=${VERSION#v}" >> "$GITHUB_OUTPUT" - - name: Stage npm package - run: | - set -eu - version="${{ inputs.repair_npm_version }}" - semver="${version#v}" - pkg_root="dist/npm/dingtalk-workspace-cli" - rm -rf "$pkg_root" - mkdir -p "$pkg_root/assets" "$pkg_root/bin" - cp build/npm/install.js "$pkg_root/install.js" - cp build/npm/bin/dws.js "$pkg_root/bin/dws.js" - cp build/npm/README.md "$pkg_root/README.md" - sed "s|__VERSION__|${semver}|g" build/npm/package.json.tmpl > "$pkg_root/package.json" - cp dist/dws-* "$pkg_root/assets/" - cp dist/checksums.txt "$pkg_root/assets/" - test -f "$pkg_root/assets/dws-skills.zip" - cat "$pkg_root/package.json" + - name: Verify immutable release authority + id: authority + uses: actions/github-script@v7 + env: + VERSION: ${{ inputs.repair_npm_version }} + with: + script: | + const { owner, repo } = context.repo; + const version = process.env.VERSION; + const ref = await github.rest.git.getRef({ + owner, + repo, + ref: `tags/${version}`, + }); + if (ref.data.object.type !== "tag") { + core.setFailed(`${version} is not an annotated tag`); + return; + } + const tag = await github.rest.git.getTag({ + owner, + repo, + tag_sha: ref.data.object.sha, + }); + if (tag.data.tag !== version || tag.data.object.type !== "commit") { + core.setFailed(`${version} does not resolve to one annotated commit tag`); + return; + } + const commitSha = tag.data.object.sha; + const defaultBranch = context.payload.repository.default_branch; + const branch = await github.rest.repos.getBranch({ + owner, + repo, + branch: defaultBranch, + }); + const comparison = await github.rest.repos.compareCommitsWithBasehead({ + owner, + repo, + basehead: `${commitSha}...${branch.data.commit.sha}`, + }); + if (!["ahead", "identical"].includes(comparison.data.status)) { + core.setFailed(`${version} is not contained in ${defaultBranch}`); + return; + } + const release = await github.rest.repos.getReleaseByTag({ + owner, + repo, + tag: version, + }); + if ( + release.data.tag_name !== version || + release.data.draft || + !release.data.immutable + ) { + core.setFailed(`${version} is not a public immutable GitHub Release`); + return; + } + const expectedAssets = new Set([ + "dws-darwin-amd64.tar.gz", + "dws-darwin-arm64.tar.gz", + "dws-linux-amd64.tar.gz", + "dws-linux-arm64.tar.gz", + "dws-windows-amd64.zip", + "dws-windows-arm64.zip", + "dws-skills.zip", + "checksums.txt", + ]); + const assetNames = release.data.assets.map((asset) => asset.name); + if ( + assetNames.length !== expectedAssets.size || + assetNames.some((name) => !expectedAssets.has(name)) || + new Set(assetNames).size !== expectedAssets.size + ) { + core.setFailed(`${version} does not contain the exact release asset set`); + return; + } + const runs = await github.paginate(github.rest.actions.listWorkflowRuns, { + owner, + repo, + workflow_id: "release.yml", + branch: version, + event: "push", + status: "completed", + per_page: 100, + }); + let passed = false; + for (const run of runs) { + if (run.head_sha !== commitSha || run.head_branch !== version) continue; + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: run.id, + filter: "all", + per_page: 100, + }); + if ( + jobs.some( + (job) => + job.name === "Publish immutable GitHub Release" && + job.conclusion === "success", + ) + ) { + passed = true; + break; + } + } + if (!passed) { + core.setFailed(`Immutable GitHub Release job did not succeed for ${version} at ${commitSha}`); + return; + } + core.setOutput("commit_sha", commitSha); + + - name: Check out target release source + uses: actions/checkout@v4 + with: + ref: ${{ steps.authority.outputs.commit_sha }} + path: release-source - name: Setup Node.js uses: actions/setup-node@v4 @@ -327,16 +917,74 @@ jobs: node-version: "20" registry-url: "https://registry.npmjs.org" - - name: Publish stable to npm - if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(inputs.repair_npm_version, '-') }} - working-directory: dist/npm/dingtalk-workspace-cli - run: npm publish --access public + - name: Download and verify GitHub release assets env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ inputs.repair_npm_version }} + run: | + set -eu + mkdir -p dist + gh release download "$VERSION" \ + --repo "$GITHUB_REPOSITORY" \ + --dir dist \ + --pattern 'dws-*' \ + --pattern 'checksums.txt' \ + --clobber + DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ + "$GITHUB_WORKSPACE/tooling/scripts/release/verify-release-artifacts.sh" "$VERSION" - - name: Publish prerelease to npm beta - if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(inputs.repair_npm_version, '-') }} - working-directory: dist/npm/dingtalk-workspace-cli - run: npm publish --access public --tag beta + - name: Stage and verify target npm package + env: + VERSION: ${{ inputs.repair_npm_version }} + run: | + set -eu + DWS_PACKAGE_SOURCE_ROOT="$GITHUB_WORKSPACE/release-source" \ + DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ + "$GITHUB_WORKSPACE/tooling/scripts/release/stage-npm-package.sh" "$VERSION" + DWS_PACKAGE_DIST_DIR="$GITHUB_WORKSPACE/dist" \ + "$GITHUB_WORKSPACE/tooling/scripts/release/verify-package-managers.sh" \ + --npm-only \ + --expected-version "$VERSION" \ + --skip-skill-targets + + - name: Inspect npm version + id: npm + env: + SEMVER: ${{ steps.version.outputs.semver }} + run: | + set -eu + local_integrity="$( + "$GITHUB_WORKSPACE/tooling/scripts/release/pack-npm-package.sh" \ + "$GITHUB_WORKSPACE/dist/npm/dingtalk-workspace-cli" \ + "$RUNNER_TEMP/dingtalk-workspace-cli-repair.tgz" + )" + version_error="$RUNNER_TEMP/npm-version-error.log" + if existing="$(npm view "dingtalk-workspace-cli@$SEMVER" version 2>"$version_error")"; then + if [ "$existing" != "$SEMVER" ]; then + echo "npm returned unexpected version $existing for $SEMVER" >&2 + exit 1 + fi + elif grep -Eq 'E404|404 Not Found' "$version_error"; then + existing="" + else + cat "$version_error" >&2 + exit 1 + fi + if [ "$existing" = "$SEMVER" ]; then + remote_integrity="$(npm view "dingtalk-workspace-cli@$SEMVER" dist.integrity)" + if [ -z "$remote_integrity" ] || [ "$remote_integrity" != "$local_integrity" ]; then + echo "npm $SEMVER integrity differs from the immutable release package." >&2 + echo "local: $local_integrity" >&2 + echo "remote: $remote_integrity" >&2 + exit 1 + fi + echo "published=true" >> "$GITHUB_OUTPUT" + else + echo "published=false" >> "$GITHUB_OUTPUT" + fi + + - name: Backfill exact version to npm + if: ${{ steps.npm.outputs.published != 'true' }} + run: npm publish "$RUNNER_TEMP/dingtalk-workspace-cli-repair.tgz" --access public --tag backfill --ignore-scripts --registry=https://registry.npmjs.org env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/sync-release-to-gitee.yml b/.github/workflows/sync-release-to-gitee.yml deleted file mode 100644 index 6922d30c..00000000 --- a/.github/workflows/sync-release-to-gitee.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Sync release to Gitee - -# Manually mirror a published GitHub release's assets to the matching Gitee -# release. Use this to repair a release whose Gitee mirror is incomplete (e.g. -# the Release job timed out mid-upload). It runs ONLY the idempotent Gitee sync -# step — it does not run GoReleaser and does not touch the GitHub release, so -# there is no release outage. The sync script skips assets already on Gitee, so -# this only uploads what is missing. - -on: - workflow_dispatch: - inputs: - version: - description: "Release tag to mirror to Gitee (e.g. v1.0.42)" - required: true - type: string - -permissions: - contents: read - -jobs: - sync-gitee: - runs-on: ubuntu-latest - # Each step has its own ceiling. Their 115-minute sum leaves five minutes - # for runner scheduling/teardown inside this 120-minute job deadline. - timeout-minutes: 120 - steps: - - name: Check out repository - uses: actions/checkout@v4 - timeout-minutes: 5 - - - name: Download GitHub release assets - timeout-minutes: 10 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -eu - mkdir -p dist - gh release download "${{ inputs.version }}" \ - --repo "${{ github.repository }}" \ - --dir dist \ - --pattern 'dws-*' \ - --pattern 'checksums.txt' \ - --clobber - ls -la dist - - - name: Mirror release to Gitee (China) - # Idempotent: uploads only assets not already present on the Gitee release. - timeout-minutes: 100 - run: ./scripts/release/sync-to-gitee.sh - env: - VERSION: ${{ inputs.version }} - GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} - GITEE_USER: ${{ secrets.GITEE_USER }} - GITEE_REPO: ${{ secrets.GITEE_REPO }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ce5ab1a4..ab1ceeed 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,19 +1,14 @@ # GoReleaser configuration for dws # Docs: https://goreleaser.com # -# To release: -# git tag -a v0.1.0 -m "Release v0.1.0" -# git push origin v0.1.0 +# To release, use scripts/release/release.sh. It seals main, validates the +# CHANGELOG and packages, then pushes the annotated tag for CI/CD to publish. # # To test locally (no publish): # goreleaser release --snapshot --clean version: 2 -before: - hooks: - - go mod tidy - builds: - main: ./cmd binary: dws diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad33e6e..8a7983b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,10 @@ This release seals the `v1.0.52` line with personal event subscriptions, a deter - **Complex Connect messages and attachments** (#606, #612) — rich-text messages retain all embedded pictures in order, queued turns keep every pending attachment, and unknown or future callback shapes reach each Agent backend with their message type and raw JSON instead of being discarded. Attachment recovery is locator-based, nested `chatRecord` pictures/audio/video/files can be recovered from message APIs after Stream ACK, and OpenCode uses a full-duration storyboard for large videos to avoid base64 OOMs while preserving the original download for the turn. - **macOS auth survives Keychain mode changes** (#597) — credential reads try existing compatible DEKs without creating key material, updates preserve the DEK that decrypted existing ciphertext, unreadable slots fail closed before token exchange, profile slots use the canonical auth backend, and `auth status` reports ciphertext/key mismatches instead of treating them as ordinary logout. Dedicated macOS race and Windows DPAPI coverage protect the cross-platform paths. +### Changed + +- **Guarded prerelease and stable automation** — adds the guided `dws-release` entry for one-command CHANGELOG preparation, validation-only and annotated-tag publication flows; promotes only an explicitly validated beta; verifies command-tree compatibility and all six packaged binaries; and serializes immutable GitHub Release, npm channel, OSS, and optional Gitee delivery with fail-closed recovery checks. + ## [1.0.51] - 2026-07-10 This release promotes the sealed `v1.0.51-beta.1` contents to stable. It syncs the hardcoded Wukong command surface, prevents `dev connect` conversations from blocking on messages received mid-turn, and makes local credential failures diagnosable without mutating key material. diff --git a/Makefile b/Makefile index 17e2608c..27a423aa 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ GO ?= go -DWS_POLICY_TMPDIR ?= $(CURDIR)/.worktrees/policy-tmp -POLICY_GOTMPDIR ?= $(DWS_POLICY_TMPDIR)/go -POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)" +REMOTE ?= +PUBLISH ?= 0 +YES ?= 0 -.PHONY: all help build rebuild test lint 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 cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema generate-schema-agent-metadata generate-schema-catalog package release publish-homebrew-formula setup-hooks +.PHONY: all help build rebuild test lint fmt policy edition-test test-schema-agent-examples generate-schema generate-schema-agent-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 @@ -28,8 +28,11 @@ help: @printf " make generate-schema - Regenerate embedded Agent metadata and the release Catalog\n" @printf " make generate-schema-agent-metadata - Regenerate versioned Agent metadata\n" @printf " make generate-schema-catalog - Regenerate the embedded release Catalog\n" - @printf " make package - Build all release artifacts locally (goreleaser snapshot)\n" - @printf " make release - Build and publish a release via goreleaser\n" + @printf " make package - Build all release artifacts locally\n" + @printf " make changelog-pre VERSION=vX.Y.Z-beta.N - Prepare prerelease notes\n" + @printf " make changelog-stable VERSION=vX.Y.Z FROM_BETA=vX.Y.Z-beta.N - Prepare stable notes\n" + @printf " make release-pre VERSION=vX.Y.Z-beta.N [PUBLISH=1] - Validate or publish prerelease\n" + @printf " make release-stable VERSION=vX.Y.Z FROM_BETA=vX.Y.Z-beta.N [PUBLISH=1] - Validate or publish stable\n" @printf " make publish-homebrew-formula - Push dist/homebrew/dingtalk-workspace-cli.rb to a tap repo\n" build: @@ -129,8 +132,8 @@ generate-schema-catalog: -output internal/cli/schema_catalog.json package: - @./scripts/dev/build-all.sh - @./scripts/release/post-goreleaser.sh + @version="$(if $(VERSION),$(VERSION),v0.0.0-SNAPSHOT)"; VERSION="$${version#v}" ./scripts/dev/build-all.sh + @version="$(if $(VERSION),$(VERSION),v0.0.0-SNAPSHOT)"; DWS_PACKAGE_VERSION="$$version" ./scripts/release/post-goreleaser.sh publish-homebrew-formula: @./scripts/release/publish-homebrew-formula.sh @@ -138,6 +141,32 @@ publish-homebrew-formula: setup-hooks: @git config core.hooksPath scripts/hooks 2>/dev/null || true +changelog-pre: + @test -n "$(VERSION)" || (printf 'VERSION is required, e.g. v1.2.3-beta.1\n' >&2; exit 2) + @./scripts/release/prepare-changelog.sh prerelease "$(VERSION)" + +changelog-stable: + @test -n "$(VERSION)" || (printf 'VERSION is required, e.g. v1.2.3\n' >&2; exit 2) + @test -n "$(FROM_BETA)" || (printf 'FROM_BETA is required, e.g. v1.2.3-beta.2\n' >&2; exit 2) + @./scripts/release/prepare-changelog.sh stable "$(VERSION)" --from-beta "$(FROM_BETA)" + +release-pre: + @test -n "$(VERSION)" || (printf 'VERSION is required, e.g. v1.2.3-beta.1\n' >&2; exit 2) + @test -n "$(REMOTE)" || (printf 'REMOTE is required, e.g. origin\n' >&2; exit 2) + @args=""; \ + if [ "$(PUBLISH)" = "1" ]; then args="$$args --publish"; fi; \ + if [ "$(YES)" = "1" ]; then args="$$args --yes"; fi; \ + ./scripts/release/release.sh prerelease "$(VERSION)" --remote "$(REMOTE)" $$args + +release-stable: + @test -n "$(VERSION)" || (printf 'VERSION is required, e.g. v1.2.3\n' >&2; exit 2) + @test -n "$(FROM_BETA)" || (printf 'FROM_BETA is required, e.g. v1.2.3-beta.2\n' >&2; exit 2) + @test -n "$(REMOTE)" || (printf 'REMOTE is required, e.g. origin\n' >&2; exit 2) + @args=""; \ + if [ "$(PUBLISH)" = "1" ]; then args="$$args --publish"; fi; \ + if [ "$(YES)" = "1" ]; then args="$$args --yes"; fi; \ + ./scripts/release/release.sh stable "$(VERSION)" --from-beta "$(FROM_BETA)" --remote "$(REMOTE)" $$args + release: - goreleaser release --clean - @./scripts/release/post-goreleaser.sh + @printf 'Use make release-pre or make release-stable; direct goreleaser publishing is disabled.\n' >&2 + @exit 2 diff --git a/build/homebrew.rb.tmpl b/build/homebrew.rb.tmpl index 0b10c88c..9575adcc 100644 --- a/build/homebrew.rb.tmpl +++ b/build/homebrew.rb.tmpl @@ -2,6 +2,7 @@ class __CLASS_NAME__ < Formula desc "Install locally built DingTalk workspace CLI artifacts for verification" homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli" url "__ARCHIVE_URL__" + version "__VERSION__" sha256 "__ARCHIVE_SHA256__" license "Apache-2.0" __KEG_ONLY_LINE__ @@ -30,11 +31,42 @@ __KEG_ONLY_LINE__ end end - def caveats - <<~EOS - Agent Skills are bundled in #{pkgshare}/skills/dws. - Run `dws skill setup` to install them into your Agent directories. - EOS + def post_install + require "fileutils" + + skill_root = pkgshare/"skills/dws" + entries = Dir["#{skill_root}/*"] + return if entries.empty? + + skill_home_override = "__SKILL_HOME_OVERRIDE__" + skill_home = skill_home_override.empty? ? Dir.home : skill_home_override + targets = [ + Pathname.new(File.join(skill_home, ".agents/skills/dws")), + Pathname.new(File.join(skill_home, ".claude/skills/dws")), + Pathname.new(File.join(skill_home, ".cursor/skills/dws")), + Pathname.new(File.join(skill_home, ".qoder/skills/dws")), + Pathname.new(File.join(skill_home, ".qoderwork/skills/dws")), + Pathname.new(File.join(skill_home, ".gemini/skills/dws")), + Pathname.new(File.join(skill_home, ".codex/skills/dws")), + Pathname.new(File.join(skill_home, ".github/skills/dws")), + Pathname.new(File.join(skill_home, ".windsurf/skills/dws")), + Pathname.new(File.join(skill_home, ".augment/skills/dws")), + Pathname.new(File.join(skill_home, ".cline/skills/dws")), + Pathname.new(File.join(skill_home, ".amp/skills/dws")), + Pathname.new(File.join(skill_home, ".kiro/skills/dws")), + Pathname.new(File.join(skill_home, ".trae/skills/dws")), + Pathname.new(File.join(skill_home, ".openclaw/skills/dws")), + Pathname.new(File.join(skill_home, ".hermes/skills/dws")), + ] + + targets.each_with_index do |dest, index| + parent_gate = dest.parent.parent + next if index > 0 && !parent_gate.directory? + + FileUtils.rm_rf(dest) + FileUtils.mkdir_p(dest) + FileUtils.cp_r(entries, dest) + end end test do diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 00000000..f37a65ed --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,113 @@ +# 发布手册(预发 / 正式) + +发布只走一条链路:本地脚本负责封板、验证并推送 annotated tag;GitHub Actions 负责构建和发布最终产物。不要直接运行 `goreleaser release`,也不要手工补打或移动 tag。 + +发布前必须完成平台治理:目标 GitHub 仓库已启用 immutable releases,`main` 要求 `CI Gate`,操作机已安装并登录 `gh`。本地脚本会在封 tag 前通过 API 检查 immutable releases、当前 SHA 的 `CI Gate` 和在途 Release;`v*` tag ruleset 仍需仓库管理员预先配置并由操作人确认。 + +## 日常只用一个入口 + +安装发布 Skill 后直接运行: + +```bash +dws-release +``` + +零参数会进入引导模式。仓库内的等价入口是 `./scripts/release/dws-release.sh`。第一次使用只需配置一次生产发布远端,命令会把远端名及其规范化仓库身份一起保存在当前 Git 仓库中: + +```bash +dws-release config --remote origin +``` + +之后命令按仓库状态自动走到正确步骤:缺少精确 CHANGELOG 章节时只生成模板并停止;补全、提交并合入 `main` 后,再运行同一条命令就会安全快进本地 `main` 并执行完整预检。若同名 remote 后续被改指向其他仓库会直接拒绝。只有显式增加 `--publish` 才会进入 tag 发布,且底层仍要求最终版本确认。 + +## 发布模型 + +```text +main 上的候选代码 + beta CHANGELOG + → vX.Y.Z-beta.N(预发验证) + → 只允许补正式 CHANGELOG,源码不得再变化 + → vX.Y.Z(正式发布) +``` + +正式版必须显式指定本次验证过的 beta。脚本会比较两者:除 `CHANGELOG.md` 外只要有任何文件变化,就拒绝正式发布。这样预发测过的代码、命令树和正式发布的代码是同一份。 + +## 预发发布 + +运行统一入口: + +```bash +dws-release v1.2.3-beta.1 +``` + +如果 CHANGELOG 尚不存在,该命令只生成模板并停止。补全内容、删除所有 `TODO`,提交后通过 PR 合入 `main`;然后重新运行完全相同的命令,它会执行完整预检: + +```bash +dws-release v1.2.3-beta.1 +``` + +预检包含测试、策略检查、旧正式版命令树兼容检查、全平台打包、npm 安装验证,以及 macOS 环境下的 Homebrew 安装验证。通过后发布: + +```bash +dws-release v1.2.3-beta.1 --publish +``` + +命令会在所有预检完成后要求再次输入完整版本号。统一入口不提供跳过确认的参数。 + +## 正式发布 + +beta 验证通过后,运行正式版入口: + +```bash +dws-release v1.2.3 --from-beta v1.2.3-beta.1 +``` + +首次运行只生成正式版 CHANGELOG 并停止。补全内容、删除 `TODO`,提交后通过 PR 合入 `main`;重新运行同一条命令做完整预检,确认后增加 `--publish`: + +```bash +dws-release v1.2.3 --from-beta v1.2.3-beta.1 +dws-release v1.2.3 --from-beta v1.2.3-beta.1 --publish +``` + +`FROM_BETA` 不会自动推断,并会写入 stable annotated tag 的 `From-Beta` 元数据,CI 会再次读取和验证。 + +## CHANGELOG 契约 + +每个 tag 必须有唯一、非空且不含 `TODO/TBD` 的精确章节: + +```markdown +## [1.2.3-beta.1] - 2026-07-11 + +### Changed + +- 本次 beta 验证的用户可见变化。 +``` + +正式版使用 `## [1.2.3] - YYYY-MM-DD`。该章节会直接成为 GitHub Release Notes。 + +## CI/CD 保证 + +- 只接受 `vX.Y.Z-beta.N` 和 `vX.Y.Z`,且新版本必须高于上一正式版。这里的“上一正式版”必须同时具备公开非草稿 GitHub Release 和同 tag/commit 的成功 Release workflow;只有 tag、没有交付成功的孤儿版本会阻断后续发布,要求先重跑补齐。 +- tag 必须是 annotated tag;本地脚本在推送前重新确认 HEAD 与远端 `main` 完全一致,CI 允许其后 `main` 前进,但要求封板提交仍位于 `main` 历史中。 +- 日常 CI 和发布前都会对比“最新已交付正式版”的完整命令树;若长时间预检期间该 baseline 发生变化,会针对新的 baseline 重新比较。 +- GoReleaser 只构建;Darwin 重签、checksums 重算和 npm 安装验证通过后,才统一上传 GitHub Release 的最终产物。 +- 六个平台归档会逐个解包并核验二进制内嵌版本;公开资产集合、checksums 集合和 npm tarball integrity 都必须精确一致。npm tarball 固定由 npm `10.9.2` 打包,避免重跑时因 runner 自带 npm 漂移产生不同字节。 +- stable 发布到 npm `latest`,更新 OSS `latest.txt` 和共享安装脚本;prerelease 发布到 npm `beta`,只更新 OSS `beta.txt`,不会覆盖稳定入口。 +- Release workflow 使用一个最多容纳 100 个 pending run 的串行 publication queue;本地入口仍要求上一条 Release 完成后才能封下一个 tag。 +- 本地 tag push 失败时会删除本次新建的本地 tag。tag 一旦成功推送,后续发布归 CI 所有,禁止改 tag 指向或复用版本号。 + +npm 补发只允许从默认分支触发 Release workflow 的 `repair_npm_version`。它只支持启用 immutable releases 后、由本流水线成功产出的公开 immutable release:目标必须是 `main` 历史中的 annotated tag,并且同 commit 的 `Build immutable GitHub Release` job 已成功。即使后续 npm 分发失败,这个独立的产物封存边界仍可作为补发依据。补发会用目标 commit 的 npm 模板重组包,逐平台核验资产和二进制版本,再发布到隔离的 `backfill` dist-tag,不会回滚 `latest` / `beta`。历史 mutable release 不进入自动补发路径,避免把可被替换的资产带入 npm。 + +OSS/Gitee 分发失败时直接重跑该 tag 的 `Publish npm and mirrors` failed job;各步会复用 immutable GitHub 资产并保持 channel 单调。独立 Gitee release workflow 和本地直发脚本已停用,避免绕开 publication queue 或用重新构建的不同字节覆盖镜像。 + +OSS 的 `latest.txt` / `beta.txt` 当前是镜像频道元数据;仓库内安装器仍从 GitHub/Gitee 解析版本,不能把 OSS pointer 当成已接入的安装通道。 + +Homebrew 当前只属于本机预检/手工公式通道:预检会在当前 macOS 架构真实安装,但 Release workflow 不发布 tap,CI 生成的单主机公式也不应当作 Darwin 双架构正式交付。正式自动交付范围是 GitHub Release、npm、OSS,以及显式开启时的 Gitee fallback;Homebrew 双架构 tap 发布需另立需求。 + +## 平台治理前置 + +仓库管理员还需要在 GitHub 平台配置两项不可由脚本替代的规则: + +- `main` 必须要求精确的 `CI Gate`;tag workflow 也会通过 Checks API 再确认该封板 SHA 已通过。 +- 必须启用 immutable releases;它只保护启用后发布的 release,因此应在第一次使用新流水线前配置。为 `v*` 增加 tag ruleset,限制创建权限,并在 release 发布前保护 tag 的短暂窗口。 + +immutable releases 或 `CI Gate` 缺失时,发布脚本会自动拒绝封 tag。tag ruleset 可能来自组织层,脚本不自动推断其最终作用范围;管理员确认不能省略,脚本约定也不能替代平台强制。 diff --git a/scripts/README.md b/scripts/README.md index 381a2dd5..ce386a83 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -6,15 +6,15 @@ These repo-local entrypoints are the supported shell entrypoints for building, t - `make test`: run `go test ./...` - `make lint`: run formatting checks and required `golangci-lint` - `make fmt`: format Go source files under `cmd/`, `internal/`, and `test/` -- `make policy`: reuse the current `dws` binary and run the complete policy suite (`make build` first) -- `make package`: build all release artifacts locally via `goreleaser --snapshot` -- `make release`: build and publish a release via `goreleaser` +- `make package`: build all release artifacts locally +- `make release-pre VERSION=vX.Y.Z-beta.N`: validate a prerelease (`PUBLISH=1` pushes its tag) +- `make release-stable VERSION=vX.Y.Z FROM_BETA=vX.Y.Z-beta.N`: validate a stable promotion (`PUBLISH=1` pushes its tag) Script groups: - Root installers: `./scripts/install.sh`, `./scripts/install.ps1`, `./scripts/install-skills.sh` - Product convenience installers: `./scripts/install-devapp.sh`, `./scripts/install-devapp.ps1`, `./scripts/install-event.sh` - Dev helpers: `./scripts/dev/build.sh`, `./scripts/dev/lint.sh`, `./scripts/dev/ci-local.sh`, `./scripts/dev/run-mock-e2e.sh`, `./scripts/dev/coverage.sh` -- Policy checks: `./scripts/policy/check-schema-command-registry.sh`, `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-command-surface.sh`, `./scripts/policy/check-schema-catalog.sh`, `./scripts/policy/check-schema-binary.sh`, `./scripts/policy/check-open-source-assets.sh` -- Policy runtime files: set `DWS_POLICY_TMPDIR` to override the default `.worktrees/policy-tmp` workspace -- Release helpers: `./scripts/release/post-goreleaser.sh`, `./scripts/release/verify-package-managers.sh`, `./scripts/release/publish-homebrew-formula.sh` +- Policy checks: `./scripts/policy/check-schema-command-registry.sh`, `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-command-surface.sh`, `./scripts/policy/check-command-compatibility.sh --base-ref --stable-ref `, `./scripts/policy/check-schema-catalog.sh`, `./scripts/policy/check-schema-binary.sh`, `./scripts/policy/check-open-source-assets.sh` +- Release entrypoint and contract: `./scripts/release/release.sh`, `./scripts/release/release-contract.sh`; operator guide: [`docs/releasing.md`](../docs/releasing.md) +- Release packaging helpers: `./scripts/release/post-goreleaser.sh`, `./scripts/release/stage-npm-package.sh`, `./scripts/release/pack-npm-package.sh`, `./scripts/release/verify-delivered-stable.sh`, `./scripts/release/verify-release-artifacts.sh`, `./scripts/release/verify-github-release-assets.sh`, `./scripts/release/verify-github-tag-authority.sh`, `./scripts/release/verify-package-managers.sh`, `./scripts/release/publish-homebrew-formula.sh` diff --git a/scripts/release/build-and-publish-gitee.sh b/scripts/release/build-and-publish-gitee.sh index fd8800e2..2f265a2a 100755 --- a/scripts/release/build-and-publish-gitee.sh +++ b/scripts/release/build-and-publish-gitee.sh @@ -1,52 +1,7 @@ -#!/usr/bin/env bash -# Build release artifacts locally in the current runner and publish them to Gitee. - +#!/bin/sh set -eu -ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" -SCRIPT_ROOT="$ROOT" -cd "$SCRIPT_ROOT" - -VERSION="${VERSION:-$(git describe --tags --exact-match 2>/dev/null || true)}" -[ -n "$VERSION" ] || { - echo "error: VERSION is required or HEAD must be an exact release tag" >&2 - exit 1 -} -case "$VERSION" in - v*) TAG="$VERSION"; SEMVER="${VERSION#v}" ;; - *) TAG="v$VERSION"; SEMVER="$VERSION" ;; -esac - -git fetch --tags origin "refs/tags/${TAG}:refs/tags/${TAG}" >/dev/null 2>&1 || true -target_commit="$(git rev-parse "${TAG}^{commit}" 2>/dev/null || true)" -[ -n "$target_commit" ] || { - echo "error: could not resolve tag ${TAG}" >&2 - exit 1 -} - -current_commit="$(git rev-parse HEAD)" -WORKDIR="$SCRIPT_ROOT" -cleanup_worktree() { - if [ "$WORKDIR" != "$SCRIPT_ROOT" ]; then - git -C "$SCRIPT_ROOT" worktree remove --force "$WORKDIR" >/dev/null 2>&1 || rm -rf "$WORKDIR" - fi -} -trap cleanup_worktree EXIT - -if [ "$current_commit" != "$target_commit" ]; then - WORKDIR="$(mktemp -d)" - rm -rf "$WORKDIR" - git worktree add --detach "$WORKDIR" "$TAG" - mkdir -p "$WORKDIR/scripts/release" - for release_script in publish-gitee-local.sh reconcile-gitee-assets.sh; do - cp "$SCRIPT_ROOT/scripts/release/$release_script" "$WORKDIR/scripts/release/$release_script" - chmod +x "$WORKDIR/scripts/release/$release_script" - done -fi - -cd "$WORKDIR" - -echo "==> Building ${TAG} locally for Gitee" -VERSION="$SEMVER" ./scripts/dev/build-all.sh -DWS_PACKAGE_VERSION="$TAG" ./scripts/release/post-goreleaser.sh -VERSION="$TAG" ./scripts/release/publish-gitee-local.sh +printf '%s\n' \ + 'Direct Gitee release builds are disabled.' \ + 'Use make release-pre/release-stable; release.yml mirrors the exact immutable GitHub assets.' >&2 +exit 2 diff --git a/scripts/release/dws-release.sh b/scripts/release/dws-release.sh new file mode 100755 index 00000000..d030d975 --- /dev/null +++ b/scripts/release/dws-release.sh @@ -0,0 +1,322 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/release-lib.sh" +ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)" + +VERSION="" +FROM_BETA="" +REMOTE="" +MODE="check" +MODE_SET=0 +WIZARD=0 + +usage() { + cat >&2 <<'EOF' +usage: dws-release [version] [options] + dws-release config [--remote ] + +With no arguments, starts an interactive release guide. For a version that has +no CHANGELOG section yet, prepares the template and stops. Otherwise, runs the +full guarded preflight; add --publish to publish after the preflight succeeds. + +Options: + --from-beta Required stable promotion baseline + --remote Override the configured release remote + --check Validate only (default) + --publish Run the preflight, then create and push the tag + -h, --help Show this help + +Examples: + dws-release config --remote origin + dws-release v1.2.3-beta.1 + dws-release v1.2.3-beta.1 --publish + dws-release v1.2.3 --from-beta v1.2.3-beta.1 + dws-release v1.2.3 --from-beta v1.2.3-beta.1 --publish +EOF +} + +die_usage() { + printf '%s\n' "$1" >&2 + usage + exit 2 +} + +is_interactive() { + [ -t 0 ] && [ -t 1 ] +} + +repository_identity() { + identity_url="$1" + case "$identity_url" in + https://github.com/*) identity_path="${identity_url#https://github.com/}" ;; + http://github.com/*) identity_path="${identity_url#http://github.com/}" ;; + git://github.com/*) identity_path="${identity_url#git://github.com/}" ;; + git@github.com:*) identity_path="${identity_url#git@github.com:}" ;; + ssh://git@github.com/*) identity_path="${identity_url#ssh://git@github.com/}" ;; + *) printf '%s\n' "$identity_url"; return 0 ;; + esac + identity_path="${identity_path%/}" + identity_path="${identity_path%.git}" + printf 'github.com/%s\n' "$identity_path" +} + +release_remote_identity() { + identity_remote="$1" + identity_fetch_url="$(git remote get-url "$identity_remote" 2>/dev/null)" || { + printf 'configured release remote does not exist: %s\n' "$identity_remote" >&2 + return 1 + } + identity_push_urls="$(git remote get-url --push --all "$identity_remote" 2>/dev/null)" || { + printf 'could not resolve push URL for release remote: %s\n' "$identity_remote" >&2 + return 1 + } + identity_push_count="$(printf '%s\n' "$identity_push_urls" | sed '/^$/d' | wc -l | tr -d '[:space:]')" + [ "$identity_push_count" -eq 1 ] || { + printf 'release remote must have exactly one push URL: %s\n' "$identity_remote" >&2 + return 1 + } + identity_push_url="$(printf '%s\n' "$identity_push_urls" | sed -n '1p')" + identity_fetch="$(repository_identity "$identity_fetch_url")" + identity_push="$(repository_identity "$identity_push_url")" + [ "$identity_fetch" = "$identity_push" ] || { + printf 'release remote fetch and push URLs target different repositories:\n fetch: %s\n push: %s\n' \ + "$identity_fetch_url" "$identity_push_url" >&2 + return 1 + } + printf '%s\n' "$identity_fetch" +} + +set_mode() { + requested_mode="$1" + if [ "$MODE_SET" -eq 1 ] && [ "$MODE" != "$requested_mode" ]; then + die_usage '--check and --publish cannot be used together' + fi + MODE="$requested_mode" + MODE_SET=1 +} + +require_remote() { + if [ -z "$REMOTE" ]; then + REMOTE="$(git config --local --get dws.releaseRemote 2>/dev/null || true)" + fi + if [ -z "$REMOTE" ] && is_interactive; then + printf 'Configured remotes:\n' >&2 + git remote -v >&2 + printf 'Release remote: ' >&2 + IFS= read -r REMOTE + fi + [ -n "$REMOTE" ] || { + printf '%s\n' 'release remote is not configured; run: dws-release config --remote ' >&2 + exit 1 + } + expected_repository="$(git config --local --get dws.releaseRepository 2>/dev/null || true)" + [ -n "$expected_repository" ] || { + printf '%s\n' 'release repository identity is not configured; rerun: dws-release config --remote ' >&2 + exit 1 + } + actual_repository="$(release_remote_identity "$REMOTE")" || exit 1 + [ "$actual_repository" = "$expected_repository" ] || { + printf 'release remote %s changed repository identity:\n configured: %s\n current: %s\n' \ + "$REMOTE" "$expected_repository" "$actual_repository" >&2 + printf '%s\n' 'Review the remote and run dws-release config again only if this change is intentional.' >&2 + exit 1 + } +} + +sync_main_if_safe() { + current_branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" + [ "$current_branch" = "main" ] || { + printf 'release validation must run from the main worktree (current: %s)\n' "${current_branch:-detached HEAD}" >&2 + exit 1 + } + [ -z "$(git status --porcelain --untracked-files=all)" ] || { + printf '%s\n' 'release main worktree must be clean before synchronization' >&2 + exit 1 + } + + printf '==> Synchronizing %s/main\n' "$REMOTE" + git fetch --force "$REMOTE" "+refs/heads/main:refs/remotes/$REMOTE/main" + remote_main="refs/remotes/$REMOTE/main" + head_commit="$(git rev-parse HEAD)" + remote_commit="$(git rev-parse "$remote_main^{commit}")" + if [ "$head_commit" = "$remote_commit" ]; then + return 0 + fi + if git merge-base --is-ancestor HEAD "$remote_main"; then + git merge --ff-only "$remote_main" + return 0 + fi + if git merge-base --is-ancestor "$remote_main" HEAD; then + printf 'local main is ahead of %s/main; publish only after the commits are reviewed and pushed\n' "$REMOTE" >&2 + else + printf 'local main has diverged from %s/main; resolve it explicitly before release\n' "$REMOTE" >&2 + fi + exit 1 +} + +configure_remote() { + shift + config_remote="" + while [ "$#" -gt 0 ]; do + case "$1" in + --remote) + [ "$#" -ge 2 ] || die_usage '--remote requires a value' + config_remote="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) die_usage "unknown config argument: $1" ;; + esac + done + + cd "$ROOT" + if [ -z "$config_remote" ]; then + configured="$(git config --local --get dws.releaseRemote 2>/dev/null || true)" + configured_repository="$(git config --local --get dws.releaseRepository 2>/dev/null || true)" + printf 'Configured release remote: %s\n' "${configured:-none}" + printf 'Configured repository: %s\n' "${configured_repository:-none}" + git remote -v + exit 0 + fi + configured_repository="$(release_remote_identity "$config_remote")" || exit 1 + git config --local dws.releaseRemote "$config_remote" + git config --local dws.releaseRepository "$configured_repository" + printf 'Configured release remote: %s\n' "$config_remote" + printf 'Configured repository: %s\n' "$configured_repository" + exit 0 +} + +if [ "${1:-}" = "config" ]; then + configure_remote "$@" +fi + +[ "$#" -gt 0 ] || WIZARD=1 +while [ "$#" -gt 0 ]; do + case "$1" in + --from-beta) + [ "$#" -ge 2 ] || die_usage '--from-beta requires a value' + FROM_BETA="$2" + shift 2 + ;; + --remote) + [ "$#" -ge 2 ] || die_usage '--remote requires a value' + REMOTE="$2" + shift 2 + ;; + --check) set_mode check; shift ;; + --publish) set_mode publish; shift ;; + -h|--help) usage; exit 0 ;; + -*) die_usage "unknown argument: $1" ;; + *) + [ -z "$VERSION" ] || die_usage "unexpected argument: $1" + VERSION="$1" + shift + ;; + esac +done + +if [ -z "$VERSION" ]; then + is_interactive || { usage; exit 2; } + printf 'Release version (vX.Y.Z-beta.N or vX.Y.Z): ' >&2 + IFS= read -r VERSION +fi + +CHANNEL="$(release_channel_for_version "$VERSION")" || exit 2 +if [ "$CHANNEL" = "stable" ]; then + if [ -z "$FROM_BETA" ] && is_interactive; then + printf 'Validated beta tag for %s: ' "$VERSION" >&2 + IFS= read -r FROM_BETA + fi + [ -n "$FROM_BETA" ] || die_usage 'stable release requires --from-beta ' + release_validate_version_channel prerelease "$FROM_BETA" || exit 2 + [ "$(release_core_tag "$FROM_BETA")" = "$VERSION" ] || { + printf 'stable version %s does not match beta baseline %s\n' "$VERSION" "$FROM_BETA" >&2 + exit 2 + } +else + [ -z "$FROM_BETA" ] || die_usage '--from-beta is only valid for stable releases' +fi + +if [ "$WIZARD" -eq 1 ]; then + printf 'Mode [check/publish] (default: check): ' >&2 + IFS= read -r selected_mode + case "$selected_mode" in + ''|check) MODE=check ;; + publish) MODE=publish ;; + *) die_usage "invalid mode: $selected_mode" ;; + esac +fi +[ -z "${DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE:-}" ] || { + printf '%s\n' 'DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE is test-only and cannot be set through dws-release' >&2 + exit 1 +} +[ -z "${DWS_RELEASE_OFFICIAL_TAGS_URL:-}" ] || { + printf '%s\n' 'DWS_RELEASE_OFFICIAL_TAGS_URL cannot override release authority through dws-release' >&2 + exit 1 +} +[ -z "${DWS_RELEASE_OFFICIAL_REPOSITORY:-}" ] || { + printf '%s\n' 'DWS_RELEASE_OFFICIAL_REPOSITORY cannot override release authority through dws-release' >&2 + exit 1 +} + +cd "$ROOT" +CHANGELOG="$ROOT/CHANGELOG.md" +[ -f "$CHANGELOG" ] || { printf 'CHANGELOG not found: %s\n' "$CHANGELOG" >&2; exit 1; } +MAIN_SYNCED=0 +current_branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" +if [ "$current_branch" = "main" ]; then + require_remote + sync_main_if_safe + MAIN_SYNCED=1 +fi +semver="$(release_semver "$VERSION")" +section_count="$(awk -v wanted="$semver" ' + BEGIN { prefix = "## [" wanted "] - "; count = 0 } + index($0, prefix) == 1 { count++ } + END { print count } +' "$CHANGELOG")" + +if [ "$section_count" -eq 0 ]; then + printf '==> Preparing the missing CHANGELOG section for %s\n' "$VERSION" + if [ "$CHANNEL" = "stable" ]; then + "$SCRIPT_DIR/prepare-changelog.sh" stable "$VERSION" --from-beta "$FROM_BETA" + else + "$SCRIPT_DIR/prepare-changelog.sh" prerelease "$VERSION" + fi + printf '\nCHANGELOG preparation is complete; publishing intentionally stopped.\n' + printf 'Replace TODO, review the diff, commit it, and merge it to main. Then run dws-release for %s again.\n' "$VERSION" + exit 0 +fi + +notes_tmp="$(mktemp "${TMPDIR:-/tmp}/dws-release-entry.XXXXXX")" +cleanup() { rm -f "$notes_tmp"; } +trap cleanup EXIT HUP INT TERM +release_extract_changelog "$CHANGELOG" "$semver" "$notes_tmp" + +if [ "$MAIN_SYNCED" -ne 1 ]; then + require_remote + sync_main_if_safe +fi + +printf '==> DWS release entry\n' +printf ' mode: %s\n' "$MODE" +printf ' channel: %s\n' "$CHANNEL" +printf ' version: %s\n' "$VERSION" +printf ' remote: %s\n' "$REMOTE" +[ -z "$FROM_BETA" ] || printf ' beta: %s\n' "$FROM_BETA" + +set -- "$CHANNEL" "$VERSION" --remote "$REMOTE" +if [ -n "$FROM_BETA" ]; then + set -- "$@" --from-beta "$FROM_BETA" +fi +if [ "$MODE" = "publish" ]; then + set -- "$@" --publish +fi +cleanup +trap - EXIT HUP INT TERM +exec "$SCRIPT_DIR/release.sh" "$@" diff --git a/scripts/release/pack-npm-package.sh b/scripts/release/pack-npm-package.sh new file mode 100755 index 00000000..d04bd9b2 --- /dev/null +++ b/scripts/release/pack-npm-package.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -eu + +PACKAGE_DIR="${1:-}" +OUTPUT="${2:-}" + +[ -n "$PACKAGE_DIR" ] && [ -n "$OUTPUT" ] || { + printf 'usage: pack-npm-package.sh \n' >&2 + exit 2 +} +[ -f "$PACKAGE_DIR/package.json" ] || { + printf 'npm package manifest not found: %s/package.json\n' "$PACKAGE_DIR" >&2 + exit 1 +} +command -v npx >/dev/null 2>&1 || { printf 'npx is required\n' >&2; exit 1; } +command -v node >/dev/null 2>&1 || { printf 'node is required\n' >&2; exit 1; } +NPM_PACK_VERSION="10.9.2" + +output_dir="$(CDPATH= cd -- "$(dirname -- "$OUTPUT")" && pwd)" +output_name="$(basename -- "$OUTPUT")" +rm -f "$output_dir/$output_name" + +pack_json="$( + npx --yes --package "npm@$NPM_PACK_VERSION" -- \ + npm pack "$PACKAGE_DIR" --pack-destination "$output_dir" --json --ignore-scripts +)" +pack_metadata="$(printf '%s' "$pack_json" | node -e ' +let input = ""; +process.stdin.on("data", (chunk) => { input += chunk; }); +process.stdin.on("end", () => { + const entries = JSON.parse(input); + if (!Array.isArray(entries) || entries.length !== 1) { + throw new Error("npm pack did not return exactly one package"); + } + const entry = entries[0]; + if (!entry.filename || !entry.integrity) { + throw new Error("npm pack output is missing filename or integrity"); + } + process.stdout.write(`${entry.filename}\n${entry.integrity}\n`); +}); +')" +packed_name="$(printf '%s\n' "$pack_metadata" | sed -n '1p')" +integrity="$(printf '%s\n' "$pack_metadata" | sed -n '2p')" +[ -f "$output_dir/$packed_name" ] || { + printf 'npm pack did not create expected tarball: %s\n' "$packed_name" >&2 + exit 1 +} +if [ "$packed_name" != "$output_name" ]; then + mv "$output_dir/$packed_name" "$output_dir/$output_name" +fi +printf '%s\n' "$integrity" diff --git a/scripts/release/post-goreleaser.sh b/scripts/release/post-goreleaser.sh index e0a0fcdd..d74f9143 100755 --- a/scripts/release/post-goreleaser.sh +++ b/scripts/release/post-goreleaser.sh @@ -90,36 +90,27 @@ resolve_release_base_url() { stage_npm_package() { version="$1" - pkg_root="$DIST_DIR/npm/dingtalk-workspace-cli" - - rm -rf "$pkg_root" - mkdir -p "$pkg_root/assets" "$pkg_root/bin" - - cp "$ROOT/build/npm/install.js" "$pkg_root/install.js" - cp "$ROOT/build/npm/bin/dws.js" "$pkg_root/bin/dws.js" - cp "$ROOT/build/npm/README.md" "$pkg_root/README.md" - sed "s|__VERSION__|$version|g" "$ROOT/build/npm/package.json.tmpl" > "$pkg_root/package.json" - - for artifact in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/dws-skills.zip; do - if [ -f "$artifact" ]; then - cp "$artifact" "$pkg_root/assets/" - fi - done + DWS_PACKAGE_SOURCE_ROOT="$ROOT" \ + DWS_PACKAGE_DIST_DIR="$DIST_DIR" \ + "$ROOT/scripts/release/stage-npm-package.sh" "$version" } # ---------- Homebrew formula staging ---------- render_homebrew_formula() { class_name="$1" - archive_url="$2" - skills_url="$3" - archive_sha="$4" - skills_sha="$5" - keg_only_line="$6" - output_path="$7" + formula_version="$2" + archive_url="$3" + skills_url="$4" + archive_sha="$5" + skills_sha="$6" + keg_only_line="$7" + output_path="$8" sed \ -e "s|__CLASS_NAME__|$class_name|g" \ + -e "s|__VERSION__|$formula_version|g" \ + -e "s|__SKILL_HOME_OVERRIDE__||g" \ -e "s|__ARCHIVE_URL__|$archive_url|g" \ -e "s|__ARCHIVE_SHA256__|$archive_sha|g" \ -e "s|__SKILLS_URL__|$skills_url|g" \ @@ -193,6 +184,7 @@ stage_homebrew_formula() { render_homebrew_formula \ "DingtalkWorkspaceCliLocal" \ + "$version" \ "file://$archive_path" \ "file://$DIST_DIR/dws-skills.zip" \ "$archive_sha" \ @@ -200,27 +192,12 @@ stage_homebrew_formula() { ' keg_only "Local verification formula to avoid linking conflicts"' \ "$formula_dir/dingtalk-workspace-cli-local.rb" - formula_class="DingtalkWorkspaceCli" - formula_path="$formula_dir/dingtalk-workspace-cli.rb" - keg_only_line="" - channel_caveat="" - case "$version" in - *-*) - formula_class="DingtalkWorkspaceCliBeta" - formula_path="$formula_dir/dingtalk-workspace-cli-beta.rb" - keg_only_line=' keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli"' - channel_caveat=' This beta is keg-only. Add #{opt_bin} to PATH to use its `dws` binary.' - ;; - esac - - render_homebrew_release_formula \ - "$formula_class" \ + render_homebrew_formula \ + "DingtalkWorkspaceCli" \ "$version" \ - "$release_url_base" \ - "$(sha256_file "$darwin_amd64")" \ - "$(sha256_file "$darwin_arm64")" \ - "$(sha256_file "$linux_amd64")" \ - "$(sha256_file "$linux_arm64")" \ + "$release_url_base/$archive_name" \ + "$release_url_base/$skills_name" \ + "$archive_sha" \ "$skills_sha" \ "$keg_only_line" \ "$channel_caveat" \ diff --git a/scripts/release/prepare-changelog.sh b/scripts/release/prepare-changelog.sh new file mode 100755 index 00000000..99f0fb91 --- /dev/null +++ b/scripts/release/prepare-changelog.sh @@ -0,0 +1,97 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/release-lib.sh" +ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)" + +CHANNEL="${1:-}" +VERSION="${2:-}" +FROM_BETA="" +FROM_REF="" +CHANGELOG="$ROOT/CHANGELOG.md" + +usage() { + cat >&2 <<'EOF' +usage: prepare-changelog.sh [options] + +Options: + --from-beta Required for stable release notes + --from-ref Commit-list baseline for prerelease notes + --changelog Override CHANGELOG.md path +EOF +} + +[ -n "$CHANNEL" ] && [ -n "$VERSION" ] || { usage; exit 2; } +shift 2 +while [ "$#" -gt 0 ]; do + case "$1" in + --from-beta) [ "$#" -ge 2 ] || { usage; exit 2; }; FROM_BETA="$2"; shift 2 ;; + --from-ref) [ "$#" -ge 2 ] || { usage; exit 2; }; FROM_REF="$2"; shift 2 ;; + --changelog) [ "$#" -ge 2 ] || { usage; exit 2; }; CHANGELOG="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) printf 'unknown argument: %s\n' "$1" >&2; usage; exit 2 ;; + esac +done + +release_validate_version_channel "$CHANNEL" "$VERSION" +cd "$ROOT" +[ -f "$CHANGELOG" ] || { printf 'CHANGELOG not found: %s\n' "$CHANGELOG" >&2; exit 1; } +[ -z "$(git status --porcelain --untracked-files=all)" ] || { + printf 'prepare changelog requires a clean worktree\n' >&2 + exit 1 +} + +semver="$(release_semver "$VERSION")" +if grep -Fq "## [$semver] - " "$CHANGELOG"; then + printf 'CHANGELOG section already exists for %s\n' "$semver" >&2 + exit 1 +fi +if [ "$CHANNEL" = "stable" ]; then + [ -n "$FROM_BETA" ] || { printf 'stable changelog requires --from-beta\n' >&2; exit 1; } + release_is_prerelease_version "$FROM_BETA" || { printf 'invalid beta baseline: %s\n' "$FROM_BETA" >&2; exit 1; } + [ "$(release_core_tag "$FROM_BETA")" = "$VERSION" ] || { + printf 'stable version %s does not match beta baseline %s\n' "$VERSION" "$FROM_BETA" >&2 + exit 1 + } + FROM_REF="$FROM_BETA" +fi + +release_date="${DWS_RELEASE_DATE:-$(TZ=Asia/Shanghai date +%F)}" +section="$(mktemp "${TMPDIR:-/tmp}/dws-changelog-section.XXXXXX")" +output="$(mktemp "${TMPDIR:-/tmp}/dws-changelog-output.XXXXXX")" +cleanup() { rm -f "$section" "$output"; } +trap cleanup EXIT HUP INT TERM + +{ + printf '## [%s] - %s\n\n' "$semver" "$release_date" + if [ "$CHANNEL" = "stable" ]; then + printf 'This release promotes the sealed `%s` contents to stable.\n\n' "$FROM_BETA" + else + printf '\n\n' + fi + printf '### Changed\n\n' + if [ "$CHANNEL" = "stable" ]; then + printf -- '- TODO: summarize the complete user-visible release promoted from `%s`.\n' "$FROM_BETA" + else + printf -- '- TODO: summarize this beta candidate and its validation scope.\n' + fi +} > "$section" + +inserted=0 +while IFS= read -r line || [ -n "$line" ]; do + printf '%s\n' "$line" >> "$output" + if [ "$line" = '## [Unreleased]' ]; then + printf '\n' >> "$output" + cat "$section" >> "$output" + inserted=1 + fi +done < "$CHANGELOG" +[ "$inserted" -eq 1 ] || { printf 'CHANGELOG is missing ## [Unreleased]\n' >&2; exit 1; } +cp "$output" "$CHANGELOG" + +printf 'Prepared CHANGELOG template for %s. Replace TODO, review, commit, and merge it before release.\n' "$VERSION" +if [ -n "$FROM_REF" ] && git rev-parse --verify --quiet "$FROM_REF^{commit}" >/dev/null; then + printf '\nCommits since %s:\n' "$FROM_REF" + git log --oneline "$FROM_REF..HEAD" +fi diff --git a/scripts/release/publish-gitee-local.sh b/scripts/release/publish-gitee-local.sh index 483c7df6..423f6802 100755 --- a/scripts/release/publish-gitee-local.sh +++ b/scripts/release/publish-gitee-local.sh @@ -1,68 +1,7 @@ -#!/usr/bin/env bash -# Publish locally-built release artifacts to the matching Gitee release. -# -# Intended to run inside Gitee Go after building artifacts in China. This avoids -# the unreliable GitHub Actions -> Gitee cross-border upload path. - +#!/bin/sh set -eu -SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -DIST_DIR="${DIST_DIR:-dist}" -GITEE_API="${GITEE_API:-https://gitee.com/api/v5}" -GITEE_REPO="${GITEE_REPO:-DingTalk-Real-AI/dingtalk-workspace-cli}" -GITEE_TOKEN="${GITEE_TOKEN:-${GITEE_ACCESS_TOKEN:-}}" -GITEE_CURL_CONNECT_TIMEOUT="${GITEE_CURL_CONNECT_TIMEOUT:-15}" -GITEE_CURL_MAX_TIME="${GITEE_CURL_MAX_TIME:-120}" - -err() { - printf 'error: %s\n' "$*" >&2 - exit 1 -} - -[ -n "$GITEE_TOKEN" ] || err "GITEE_TOKEN is required" -[ -d "$DIST_DIR" ] || err "dist dir not found: $DIST_DIR" - -VERSION="${VERSION:-$(git describe --tags --exact-match 2>/dev/null || true)}" -[ -n "$VERSION" ] || err "VERSION is required or HEAD must be an exact tag" -case "$VERSION" in - v*) ;; - *) VERSION="v$VERSION" ;; -esac - -OWNER="${GITEE_REPO%%/*}" -NAME="${GITEE_REPO##*/}" -base="${GITEE_API}/repos/${OWNER}/${NAME}" - -api_get() { - curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "$@" -} - -echo "📦 Publishing local artifacts for ${VERSION} to Gitee ${GITEE_REPO}" - -target_commit="$(git rev-parse "${VERSION}^{commit}" 2>/dev/null || git rev-parse HEAD)" - -rel_json="$(api_get "${base}/releases/tags/${VERSION}?access_token=${GITEE_TOKEN}" 2>/dev/null || true)" -release_id="$(printf '%s' "$rel_json" | grep -o '"id":[ ]*[0-9]*' | head -1 | grep -o '[0-9]*' || true)" - -if [ -z "$release_id" ]; then - echo " No Gitee release for ${VERSION} yet — creating it." - rel_json="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \ - -X POST "${base}/releases" \ - -F "access_token=${GITEE_TOKEN}" \ - -F "tag_name=${VERSION}" \ - -F "name=${VERSION}" \ - -F "body=Gitee-local build of ${VERSION} for China users." \ - -F "target_commitish=${target_commit}" 2>/dev/null || true)" - release_id="$(printf '%s' "$rel_json" | grep -o '"id":[ ]*[0-9]*' | head -1 | grep -o '[0-9]*' || true)" -fi -[ -n "$release_id" ] || err "could not get/create Gitee release for ${VERSION}. Response: ${rel_json}" -echo " Gitee release id = ${release_id}" - -DIST_DIR="$DIST_DIR" \ -GITEE_API="$GITEE_API" \ -GITEE_TOKEN="$GITEE_TOKEN" \ -GITEE_REPO="$GITEE_REPO" \ -GITEE_CURL_CONNECT_TIMEOUT="$GITEE_CURL_CONNECT_TIMEOUT" \ -GITEE_CURL_MAX_TIME="$GITEE_CURL_MAX_TIME" \ -GITEE_RELEASE_ID="$release_id" \ - "$SCRIPT_DIR/reconcile-gitee-assets.sh" +printf '%s\n' \ + 'Direct Gitee artifact publication is disabled.' \ + 'Use the guarded Release workflow so Gitee receives the exact immutable GitHub assets.' >&2 +exit 2 diff --git a/scripts/release/release-contract.sh b/scripts/release/release-contract.sh new file mode 100755 index 00000000..71bbb6b1 --- /dev/null +++ b/scripts/release/release-contract.sh @@ -0,0 +1,221 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/release-lib.sh" + +ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)" +CHANNEL="" +VERSION="" +CONTEXT="local" +REMOTE="origin" +BRANCH="main" +FROM_BETA="" +FROM_BETA_COMMIT="" +CHANGELOG="" +NOTES_OUTPUT="" +METADATA_OUTPUT="" + +usage() { + cat >&2 <<'EOF' +usage: release-contract.sh --channel --version [options] + +Options: + --context Local pre-push checks or tag-workflow checks + --remote Release remote (default: origin) + --branch Sealed release branch (default: main) + --from-beta Required stable promotion baseline + --repo-root Override repository root (primarily for tests) + --changelog Override CHANGELOG.md path + --notes-output Write the exact CHANGELOG section body + --metadata-output Append channel/version/baseline key-value output +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --channel) [ "$#" -ge 2 ] || { usage; exit 2; }; CHANNEL="$2"; shift 2 ;; + --version) [ "$#" -ge 2 ] || { usage; exit 2; }; VERSION="$2"; shift 2 ;; + --context) [ "$#" -ge 2 ] || { usage; exit 2; }; CONTEXT="$2"; shift 2 ;; + --remote) [ "$#" -ge 2 ] || { usage; exit 2; }; REMOTE="$2"; shift 2 ;; + --branch) [ "$#" -ge 2 ] || { usage; exit 2; }; BRANCH="$2"; shift 2 ;; + --from-beta) [ "$#" -ge 2 ] || { usage; exit 2; }; FROM_BETA="$2"; shift 2 ;; + --repo-root) [ "$#" -ge 2 ] || { usage; exit 2; }; ROOT="$2"; shift 2 ;; + --changelog) [ "$#" -ge 2 ] || { usage; exit 2; }; CHANGELOG="$2"; shift 2 ;; + --notes-output) [ "$#" -ge 2 ] || { usage; exit 2; }; NOTES_OUTPUT="$2"; shift 2 ;; + --metadata-output) [ "$#" -ge 2 ] || { usage; exit 2; }; METADATA_OUTPUT="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) printf 'unknown argument: %s\n' "$1" >&2; usage; exit 2 ;; + esac +done + +[ -n "$CHANNEL" ] && [ -n "$VERSION" ] || { usage; exit 2; } +case "$CONTEXT" in local|ci) ;; *) printf 'invalid context: %s\n' "$CONTEXT" >&2; exit 2 ;; esac +release_validate_version_channel "$CHANNEL" "$VERSION" + +cd "$ROOT" +git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { + printf 'not a Git worktree: %s\n' "$ROOT" >&2 + exit 1 +} +[ -n "$CHANGELOG" ] || CHANGELOG="$ROOT/CHANGELOG.md" +[ -f "$CHANGELOG" ] || { printf 'CHANGELOG not found: %s\n' "$CHANGELOG" >&2; exit 1; } + +head_commit="$(git rev-parse HEAD)" +remote_main="refs/remotes/$REMOTE/$BRANCH" +git remote get-url "$REMOTE" >/dev/null 2>&1 || { + printf 'release remote does not exist: %s\n' "$REMOTE" >&2 + exit 1 +} +git rev-parse --verify --quiet "$remote_main^{commit}" >/dev/null || { + printf 'release branch is not available locally: %s/%s\n' "$REMOTE" "$BRANCH" >&2 + exit 1 +} +remote_main_commit="$(git rev-parse "$remote_main^{commit}")" + +if [ "$CONTEXT" = "local" ]; then + [ -z "$(git status --porcelain --untracked-files=all)" ] || { + printf 'release worktree must be clean (staged, unstaged, and untracked files are blocked)\n' >&2 + exit 1 + } + current_branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" + [ "$current_branch" = "$BRANCH" ] || { + printf 'local release must run from branch %s (current: %s)\n' "$BRANCH" "${current_branch:-detached HEAD}" >&2 + exit 1 + } + [ "$head_commit" = "$remote_main_commit" ] || { + printf 'HEAD must exactly match %s/%s before release\n' "$REMOTE" "$BRANCH" >&2 + exit 1 + } + if git rev-parse --verify --quiet "refs/tags/$VERSION" >/dev/null; then + printf 'release tag already exists locally: %s\n' "$VERSION" >&2 + exit 1 + fi + remote_tag="$(git ls-remote --tags "$REMOTE" "refs/tags/$VERSION" "refs/tags/$VERSION^{}")" || { + printf 'could not query release tags from remote: %s\n' "$REMOTE" >&2 + exit 1 + } + [ -z "$remote_tag" ] || { + printf 'release tag already exists on %s: %s\n' "$REMOTE" "$VERSION" >&2 + exit 1 + } +else + git rev-parse --verify --quiet "refs/tags/$VERSION^{commit}" >/dev/null || { + printf 'CI release tag is not available: %s\n' "$VERSION" >&2 + exit 1 + } + [ "$(git rev-parse "refs/tags/$VERSION^{commit}")" = "$head_commit" ] || { + printf 'CI checkout HEAD does not match tag %s\n' "$VERSION" >&2 + exit 1 + } + [ "$(git cat-file -t "refs/tags/$VERSION")" = "tag" ] || { + printf 'release tag must be annotated: %s\n' "$VERSION" >&2 + exit 1 + } + git merge-base --is-ancestor HEAD "$remote_main" || { + printf 'release tag commit must be contained in %s/%s\n' "$REMOTE" "$BRANCH" >&2 + exit 1 + } +fi + +previous_stable="" +previous_stable_commit="" +for tag in $(git tag --list 'v*' --sort=-version:refname); do + [ "$tag" = "$VERSION" ] && continue + if release_is_stable_version "$tag"; then + previous_stable="$tag" + previous_stable_commit="$(git rev-parse "$tag^{commit}")" + break + fi +done +if [ -n "$previous_stable" ] && ! release_core_is_greater "$VERSION" "$previous_stable"; then + printf 'release version %s must be greater than latest stable %s\n' "$VERSION" "$previous_stable" >&2 + exit 1 +fi + +core_tag="$(release_core_tag "$VERSION")" +if [ "$CHANNEL" = "prerelease" ]; then + [ -z "$FROM_BETA" ] || { printf -- '--from-beta is only valid for stable releases\n' >&2; exit 1; } + if git rev-parse --verify --quiet "refs/tags/$core_tag" >/dev/null; then + printf 'cannot publish prerelease after stable tag exists: %s\n' "$core_tag" >&2 + exit 1 + fi + previous_beta="" + for tag in $(git tag --list "$core_tag-beta.*" --sort=-version:refname); do + [ "$tag" = "$VERSION" ] && continue + if release_is_prerelease_version "$tag"; then + previous_beta="$tag" + break + fi + done + beta_number="$(release_beta_number "$VERSION")" + if [ -z "$previous_beta" ]; then + [ "$beta_number" -eq 1 ] || { + printf 'the first prerelease for %s must be beta.1\n' "$core_tag" >&2 + exit 1 + } + else + previous_beta_number="$(release_beta_number "$previous_beta")" + expected_beta_number=$((previous_beta_number + 1)) + [ "$beta_number" -eq "$expected_beta_number" ] || { + printf 'prerelease after %s must be %s-beta.%s\n' "$previous_beta" "$core_tag" "$expected_beta_number" >&2 + exit 1 + } + fi +else + if [ -z "$FROM_BETA" ] && [ "$CONTEXT" = "ci" ]; then + FROM_BETA="$(git for-each-ref "refs/tags/$VERSION" --format='%(contents)' | awk '/^From-Beta:[[:space:]]*/ { sub(/^From-Beta:[[:space:]]*/, ""); print }')" + fi + [ -n "$FROM_BETA" ] || { + printf 'stable release requires an explicit --from-beta tag\n' >&2 + exit 1 + } + release_is_prerelease_version "$FROM_BETA" || { + printf 'invalid stable beta baseline: %s\n' "$FROM_BETA" >&2 + exit 1 + } + [ "$(release_core_tag "$FROM_BETA")" = "$VERSION" ] || { + printf 'stable version %s does not match beta baseline %s\n' "$VERSION" "$FROM_BETA" >&2 + exit 1 + } + git rev-parse --verify --quiet "refs/tags/$FROM_BETA^{commit}" >/dev/null || { + printf 'stable beta baseline is not available locally: %s\n' "$FROM_BETA" >&2 + exit 1 + } + FROM_BETA_COMMIT="$(git rev-parse "refs/tags/$FROM_BETA^{commit}")" + git merge-base --is-ancestor "$FROM_BETA^{commit}" HEAD || { + printf 'stable beta baseline is not an ancestor of HEAD: %s\n' "$FROM_BETA" >&2 + exit 1 + } + if ! git diff --quiet "$FROM_BETA^{commit}" HEAD -- . ':(exclude)CHANGELOG.md'; then + printf 'stable source drifted from %s; only CHANGELOG.md may differ\n' "$FROM_BETA" >&2 + git diff --name-only "$FROM_BETA^{commit}" HEAD -- . ':(exclude)CHANGELOG.md' >&2 + exit 1 + fi +fi + +semver="$(release_semver "$VERSION")" +if [ -n "$NOTES_OUTPUT" ]; then + release_extract_changelog "$CHANGELOG" "$semver" "$NOTES_OUTPUT" +else + notes_tmp="$(mktemp "${TMPDIR:-/tmp}/dws-release-contract.XXXXXX")" + trap 'rm -f "$notes_tmp"' EXIT HUP INT TERM + release_extract_changelog "$CHANGELOG" "$semver" "$notes_tmp" +fi + +if [ -n "$METADATA_OUTPUT" ]; then + mkdir -p "$(dirname "$METADATA_OUTPUT")" + { + printf 'channel=%s\n' "$CHANNEL" + printf 'version=%s\n' "$VERSION" + printf 'semver=%s\n' "$semver" + printf 'previous_stable=%s\n' "$previous_stable" + printf 'previous_stable_commit=%s\n' "$previous_stable_commit" + printf 'from_beta=%s\n' "$FROM_BETA" + printf 'from_beta_commit=%s\n' "$FROM_BETA_COMMIT" + } >> "$METADATA_OUTPUT" +fi + +printf 'Release contract passed: channel=%s version=%s commit=%s' "$CHANNEL" "$VERSION" "$head_commit" +[ -z "$FROM_BETA" ] || printf ' from-beta=%s' "$FROM_BETA" +printf '\n' diff --git a/scripts/release/release-lib.sh b/scripts/release/release-lib.sh new file mode 100755 index 00000000..c27f6733 --- /dev/null +++ b/scripts/release/release-lib.sh @@ -0,0 +1,167 @@ +#!/bin/sh + +# Shared release version and CHANGELOG helpers. This file is sourced by the +# local release command, the CI contract, and mirror publishing so every stage +# agrees on what "prerelease" and "stable" mean. + +release_stable_pattern='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' +release_prerelease_pattern='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-beta\.[1-9][0-9]*$' + +release_is_stable_version() { + printf '%s\n' "$1" | grep -Eq "$release_stable_pattern" +} + +release_is_prerelease_version() { + printf '%s\n' "$1" | grep -Eq "$release_prerelease_pattern" +} + +release_channel_for_version() { + version="$1" + if release_is_stable_version "$version"; then + printf '%s\n' stable + return 0 + fi + if release_is_prerelease_version "$version"; then + printf '%s\n' prerelease + return 0 + fi + printf 'invalid release version: %s (expected vX.Y.Z or vX.Y.Z-beta.N)\n' "$version" >&2 + return 1 +} + +release_validate_version_channel() { + expected="$1" + version="$2" + case "$expected" in + stable|prerelease) ;; + *) + printf 'invalid release channel: %s (expected prerelease or stable)\n' "$expected" >&2 + return 1 + ;; + esac + actual="$(release_channel_for_version "$version")" || return 1 + if [ "$actual" != "$expected" ]; then + printf 'release channel/version mismatch: channel=%s version=%s\n' "$expected" "$version" >&2 + return 1 + fi +} + +release_semver() { + printf '%s\n' "${1#v}" +} + +release_core_tag() { + version="$1" + printf '%s\n' "${version%%-beta.*}" +} + +release_beta_number() { + printf '%s\n' "${1##*.}" +} + +release_core_is_greater() { + candidate="$(release_core_tag "$1")" + baseline="$(release_core_tag "$2")" + candidate="${candidate#v}" + baseline="${baseline#v}" + awk -v candidate="$candidate" -v baseline="$baseline" 'BEGIN { + split(candidate, c, ".") + split(baseline, b, ".") + for (i = 1; i <= 3; i++) { + if ((c[i] + 0) > (b[i] + 0)) exit 0 + if ((c[i] + 0) < (b[i] + 0)) exit 1 + } + exit 1 + }' +} + +release_version_is_greater() { + _rvig_candidate="$1" + _rvig_baseline="$2" + _rvig_candidate_channel="$(release_channel_for_version "$_rvig_candidate")" || return 1 + _rvig_baseline_channel="$(release_channel_for_version "$_rvig_baseline")" || return 1 + + if release_core_is_greater "$_rvig_candidate" "$_rvig_baseline"; then + return 0 + fi + if release_core_is_greater "$_rvig_baseline" "$_rvig_candidate"; then + return 1 + fi + if [ "$_rvig_candidate_channel" = "stable" ] && [ "$_rvig_baseline_channel" = "prerelease" ]; then + return 0 + fi + if [ "$_rvig_candidate_channel" = "prerelease" ] && [ "$_rvig_baseline_channel" = "prerelease" ]; then + [ "$(release_beta_number "$_rvig_candidate")" -gt "$(release_beta_number "$_rvig_baseline")" ] + return + fi + return 1 +} + +# Extract one exact CHANGELOG section (without its H2 heading) and validate that +# it is dated, unique, non-placeholder content with at least one bullet. +release_extract_changelog() { + changelog="$1" + semver="$2" + output="$3" + tmp="$(mktemp "${TMPDIR:-/tmp}/dws-release-notes.XXXXXX")" + + set +e + awk -v wanted="$semver" ' + BEGIN { + prefix = "## [" wanted "] - " + found = 0 + active = 0 + invalid_date = 0 + meaningful = 0 + bullet = 0 + placeholder = 0 + } + /^## / { + active = 0 + if (index($0, prefix) == 1) { + found++ + active = 1 + date = substr($0, length(prefix) + 1) + if (date !~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) invalid_date = 1 + } + next + } + active { + print + if ($0 ~ /[^[:space:]]/) meaningful = 1 + if ($0 ~ /^- /) bullet = 1 + lowered = tolower($0) + if (lowered ~ /todo|tbd/) placeholder = 1 + } + END { + if (found != 1) exit 41 + if (invalid_date) exit 42 + if (!meaningful || !bullet) exit 43 + if (placeholder) exit 44 + } + ' "$changelog" > "$tmp" + status=$? + set -e + + case "$status" in + 0) ;; + 41) printf 'CHANGELOG must contain exactly one section: ## [%s] - YYYY-MM-DD\n' "$semver" >&2 ;; + 42) printf 'CHANGELOG section for %s has an invalid date\n' "$semver" >&2 ;; + 43) printf 'CHANGELOG section for %s must contain release notes and at least one bullet\n' "$semver" >&2 ;; + 44) printf 'CHANGELOG section for %s still contains TODO/TBD placeholders\n' "$semver" >&2 ;; + *) printf 'failed to parse CHANGELOG section for %s\n' "$semver" >&2 ;; + esac + if [ "$status" -ne 0 ]; then + rm -f "$tmp" + return "$status" + fi + + if [ "$output" = "-" ]; then + cat "$tmp" + else + mkdir -p "$(dirname "$output")" + mv "$tmp" "$output" + tmp="" + fi + [ -z "$tmp" ] || rm -f "$tmp" +} diff --git a/scripts/release/release.sh b/scripts/release/release.sh new file mode 100755 index 00000000..3285bda9 --- /dev/null +++ b/scripts/release/release.sh @@ -0,0 +1,353 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/release-lib.sh" +ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)" + +CHANNEL="${1:-}" +VERSION="${2:-}" +REMOTE="" +BRANCH="main" +FROM_BETA="" +PUBLISH=0 +YES=0 +OFFICIAL_TAGS_URL="${DWS_RELEASE_OFFICIAL_TAGS_URL:-https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git}" + +usage() { + cat >&2 <<'EOF' +usage: release.sh [options] + +Runs the full test, command-compatibility, package, and install preflight. +The default is validation only; add --publish to create and push the tag. + +Options: + --remote Required tag destination + --from-beta Required for stable releases + --publish Create and push the annotated release tag + --yes Skip the interactive version confirmation +EOF +} + +[ -n "$CHANNEL" ] && [ -n "$VERSION" ] || { usage; exit 2; } +shift 2 +while [ "$#" -gt 0 ]; do + case "$1" in + --remote) [ "$#" -ge 2 ] || { usage; exit 2; }; REMOTE="$2"; shift 2 ;; + --from-beta) [ "$#" -ge 2 ] || { usage; exit 2; }; FROM_BETA="$2"; shift 2 ;; + --publish) PUBLISH=1; shift ;; + --yes) YES=1; shift ;; + -h|--help) usage; exit 0 ;; + *) printf 'unknown argument: %s\n' "$1" >&2; usage; exit 2 ;; + esac +done + +release_validate_version_channel "$CHANNEL" "$VERSION" +[ -n "$REMOTE" ] || { printf '%s\n' '--remote is required' >&2; exit 2; } +cd "$ROOT" +fetch_url="$(git remote get-url "$REMOTE" 2>/dev/null)" || { printf 'unknown release remote: %s\n' "$REMOTE" >&2; exit 1; } +push_urls="$(git remote get-url --push --all "$REMOTE" 2>/dev/null)" || { + printf 'could not resolve push URL for release remote: %s\n' "$REMOTE" >&2 + exit 1 +} +[ "$(printf '%s\n' "$push_urls" | sed '/^$/d' | wc -l | tr -d '[:space:]')" -eq 1 ] || { + printf 'release remote must have exactly one push URL: %s\n' "$REMOTE" >&2 + exit 1 +} +push_url="$(printf '%s\n' "$push_urls" | sed -n '1p')" + +repository_identity() { + identity_url="$1" + case "$identity_url" in + https://github.com/*) identity_path="${identity_url#https://github.com/}" ;; + http://github.com/*) identity_path="${identity_url#http://github.com/}" ;; + git://github.com/*) identity_path="${identity_url#git://github.com/}" ;; + git@github.com:*) identity_path="${identity_url#git@github.com:}" ;; + ssh://git@github.com/*) identity_path="${identity_url#ssh://git@github.com/}" ;; + *) printf '%s\n' "$identity_url"; return 0 ;; + esac + identity_path="${identity_path%/}" + identity_path="${identity_path%.git}" + printf 'github.com/%s\n' "$identity_path" +} + +fetch_identity="$(repository_identity "$fetch_url")" +push_identity="$(repository_identity "$push_url")" +[ "$fetch_identity" = "$push_identity" ] || { + printf 'release remote fetch and push URLs target different repositories:\n fetch: %s\n push: %s\n' "$fetch_url" "$push_url" >&2 + exit 1 +} +printf 'Release target: %s\n fetch: %s\n push: %s\n' "$REMOTE" "$fetch_url" "$push_url" + +github_repository_from_url() { + github_identity="$(repository_identity "$1")" + case "$github_identity" in + github.com/*) printf '%s\n' "${github_identity#github.com/}" ;; + *) return 1 ;; + esac +} + +require_delivered_previous_stable() { + github_repository="$(github_repository_from_url "$push_url" 2>/dev/null || true)" + if [ -z "$github_repository" ]; then + [ "${DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE:-0}" = "1" ] || { + printf 'release validation requires a github.com remote to prove the stable baseline was delivered\n' >&2 + return 1 + } + return 0 + fi + [ -n "$previous_stable" ] || { + printf 'a delivered previous stable baseline is required\n' >&2 + return 1 + } + stable_commit="$(git rev-parse "$previous_stable^{commit}")" + DWS_RELEASE_OFFICIAL_REPOSITORY="${DWS_RELEASE_OFFICIAL_REPOSITORY:-DingTalk-Real-AI/dingtalk-workspace-cli}" \ + "$SCRIPT_DIR/verify-delivered-stable.sh" "$previous_stable" "$stable_commit" +} + +require_github_publication_authority() { + github_repository="$(github_repository_from_url "$push_url" 2>/dev/null || true)" + if [ -z "$github_repository" ]; then + [ "${DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE:-0}" = "1" ] || { + printf 'publishing requires a github.com remote so CI and delivery authority can be verified\n' >&2 + return 1 + } + printf 'warning: GitHub publication checks explicitly disabled for non-GitHub test remote\n' >&2 + return 0 + fi + command -v gh >/dev/null 2>&1 || { + printf 'gh is required to verify CI and release authority before publishing\n' >&2 + return 1 + } + + immutable_enabled="$( + gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "repos/$github_repository/immutable-releases" \ + --jq '.enabled' + )" || { + printf 'immutable releases are not enabled for %s; enable them before allocating a tag\n' "$github_repository" >&2 + return 1 + } + [ "$immutable_enabled" = "true" ] || { + printf 'immutable releases are not enabled for %s\n' "$github_repository" >&2 + return 1 + } + + active_runs="$( + gh api -H 'Accept: application/vnd.github+json' \ + "repos/$github_repository/actions/workflows/release.yml/runs?per_page=100" \ + --jq '.workflow_runs[] | select(.status != "completed") | [.id, .event, .status, .head_branch] | @tsv' + )" || { + printf 'could not query active Release workflow runs for %s\n' "$github_repository" >&2 + return 1 + } + [ -z "$active_runs" ] || { + printf 'another Release workflow is still active; wait for it before allocating a new tag:\n%s\n' "$active_runs" >&2 + return 1 + } + + sealed_commit="$(git rev-parse HEAD)" + passed_gate="$( + gh api -H 'Accept: application/vnd.github+json' \ + "repos/$github_repository/commits/$sealed_commit/check-runs?check_name=CI%20Gate&filter=latest&per_page=100" \ + --jq '[.check_runs[] | select(.name == "CI Gate" and .conclusion == "success")] | length' + )" || { + printf 'could not query CI Gate for %s\n' "$sealed_commit" >&2 + return 1 + } + [ "$passed_gate" -gt 0 ] || { + printf 'CI Gate has not succeeded for sealed commit %s in %s\n' "$sealed_commit" "$github_repository" >&2 + return 1 + } + + if [ "$CHANNEL" = "stable" ]; then + beta_state="$( + gh api -H 'Accept: application/vnd.github+json' \ + "repos/$github_repository/releases/tags/$FROM_BETA" \ + --jq '[.tag_name, .draft, .prerelease, .immutable] | @tsv' + )" || { + printf 'could not query beta release %s in %s\n' "$FROM_BETA" "$github_repository" >&2 + return 1 + } + [ "$beta_state" = "$(printf '%s\tfalse\ttrue\ttrue' "$FROM_BETA")" ] || { + printf '%s must be a public immutable prerelease before stable tag allocation (got: %s)\n' "$FROM_BETA" "$beta_state" >&2 + return 1 + } + beta_assets="$( + gh api -H 'Accept: application/vnd.github+json' \ + "repos/$github_repository/releases/tags/$FROM_BETA" \ + --jq '.assets[].name' + )" || return 1 + [ "$(printf '%s\n' "$beta_assets" | sed '/^$/d' | wc -l | tr -d '[:space:]')" -eq 8 ] || { + printf 'beta release %s must contain exactly eight supported assets\n' "$FROM_BETA" >&2 + return 1 + } + for beta_asset in \ + dws-darwin-amd64.tar.gz dws-darwin-arm64.tar.gz \ + dws-linux-amd64.tar.gz dws-linux-arm64.tar.gz \ + dws-windows-amd64.zip dws-windows-arm64.zip \ + dws-skills.zip checksums.txt; do + [ "$(printf '%s\n' "$beta_assets" | grep -Fxc "$beta_asset")" -eq 1 ] || { + printf 'beta release %s must contain %s exactly once\n' "$FROM_BETA" "$beta_asset" >&2 + return 1 + } + done + beta_commit="$(git rev-parse "$FROM_BETA^{commit}")" + beta_runs="$( + gh api -H 'Accept: application/vnd.github+json' \ + "repos/$github_repository/actions/workflows/release.yml/runs?branch=$FROM_BETA&event=push&status=completed&per_page=100" \ + --jq '.workflow_runs[] | [.head_sha, .head_branch, .conclusion] | @tsv' + )" || return 1 + printf '%s\n' "$beta_runs" | awk -F '\t' -v sha="$beta_commit" -v tag="$FROM_BETA" ' + $1 == sha && $2 == tag && $3 == "success" { found = 1 } + END { exit(found ? 0 : 1) } + ' || { + printf 'Release workflow did not succeed for %s at %s\n' "$FROM_BETA" "$beta_commit" >&2 + return 1 + } + fi +} + +fetch_release_tags() { + git fetch --force "$REMOTE" '+refs/tags/v*:refs/tags/v*' + git fetch --force --no-tags "$OFFICIAL_TAGS_URL" '+refs/tags/v*:refs/tags/v*' +} + +printf '==> Refreshing %s/%s and release tags\n' "$REMOTE" "$BRANCH" +git fetch --force "$REMOTE" "+refs/heads/$BRANCH:refs/remotes/$REMOTE/$BRANCH" +fetch_release_tags + +metadata="$(mktemp "${TMPDIR:-/tmp}/dws-release-metadata.XXXXXX")" +final_metadata="$(mktemp "${TMPDIR:-/tmp}/dws-release-final-metadata.XXXXXX")" +cleanup() { rm -f "$metadata" "$final_metadata"; } +trap cleanup EXIT HUP INT TERM + +run_contract() { + if [ -n "$FROM_BETA" ]; then + "$SCRIPT_DIR/release-contract.sh" \ + --channel "$CHANNEL" \ + --version "$VERSION" \ + --context local \ + --remote "$REMOTE" \ + --branch "$BRANCH" \ + --from-beta "$FROM_BETA" \ + "$@" + else + "$SCRIPT_DIR/release-contract.sh" \ + --channel "$CHANNEL" \ + --version "$VERSION" \ + --context local \ + --remote "$REMOTE" \ + --branch "$BRANCH" \ + "$@" + fi +} + +run_contract --metadata-output "$metadata" +previous_stable="$(sed -n 's/^previous_stable=//p' "$metadata" | tail -1)" + +printf '==> Verifying delivered stable baseline %s\n' "${previous_stable:-none}" +require_delivered_previous_stable + +if [ "$PUBLISH" -eq 1 ]; then + printf '==> Verifying GitHub publication authority before local gates\n' + require_github_publication_authority +fi + +printf '==> Running repository test and policy gates\n' +make test +make policy + +if [ -n "$previous_stable" ]; then + printf '==> Comparing command tree with %s\n' "$previous_stable" + "$ROOT/scripts/policy/check-command-compatibility.sh" \ + --base-ref "$REMOTE/$BRANCH" \ + --stable-ref "$previous_stable" +fi + +printf '==> Building local release artifacts for %s\n' "$VERSION" +make package VERSION="$VERSION" + +printf '==> Verifying release artifact set and checksums\n' +"$SCRIPT_DIR/verify-release-artifacts.sh" "$VERSION" + +printf '==> Verifying npm package installation\n' +"$SCRIPT_DIR/verify-package-managers.sh" --npm-only --expected-version "$VERSION" +if command -v brew >/dev/null 2>&1; then + printf '==> Verifying Homebrew package installation\n' + "$SCRIPT_DIR/verify-package-managers.sh" --brew-only --expected-version "$VERSION" +fi + +# Collect human confirmation before the final authority refresh. Nothing may +# block between that refresh and tag creation. +if [ "$PUBLISH" -eq 1 ] && [ "$YES" -ne 1 ]; then + if [ ! -t 0 ]; then + printf 'interactive confirmation is unavailable; pass --yes after reviewing the preflight\n' >&2 + exit 1 + fi + printf 'Type %s to create and push the release tag: ' "$VERSION" + IFS= read -r confirmation + [ "$confirmation" = "$VERSION" ] || { printf 'release cancelled\n' >&2; exit 1; } +fi + +# Re-check after every local gate so tag creation cannot race with a modified +# source tree. dist/ is ignored and therefore does not make the tree dirty. +printf '==> Refreshing %s/%s before sealing the tag\n' "$REMOTE" "$BRANCH" +git fetch --force "$REMOTE" "+refs/heads/$BRANCH:refs/remotes/$REMOTE/$BRANCH" +fetch_release_tags +run_contract --metadata-output "$final_metadata" +final_previous_stable="$(sed -n 's/^previous_stable=//p' "$final_metadata" | tail -1)" +if [ "$final_previous_stable" != "$previous_stable" ]; then + printf '==> Stable authority advanced from %s to %s; rechecking command compatibility\n' \ + "${previous_stable:-none}" "${final_previous_stable:-none}" + [ -n "$final_previous_stable" ] || { + printf 'latest stable authority unexpectedly became empty\n' >&2 + exit 1 + } + "$ROOT/scripts/policy/check-command-compatibility.sh" \ + --base-ref "$REMOTE/$BRANCH" \ + --stable-ref "$final_previous_stable" +fi + +if [ "$PUBLISH" -ne 1 ]; then + printf '\nPreflight passed. No tag was created.\n' + printf 'Publish with the same command plus --publish.\n' + exit 0 +fi + +printf '==> Reconfirming GitHub publication authority immediately before tag creation\n' +require_github_publication_authority + +printf '==> Creating annotated tag %s\n' "$VERSION" +if [ "$CHANNEL" = "stable" ]; then + git tag -a "$VERSION" -m "Release $VERSION" -m 'Channel: stable' -m "From-Beta: $FROM_BETA" +else + git tag -a "$VERSION" -m "Release $VERSION" -m 'Channel: prerelease' +fi + +if ! git push "$push_url" "refs/tags/$VERSION"; then + set +e + remote_refs="$(git ls-remote --tags "$push_url" "refs/tags/$VERSION" "refs/tags/$VERSION^{}")" + query_status=$? + set -e + if [ "$query_status" -ne 0 ]; then + printf 'tag push reported failure and remote state could not be verified; keeping local tag %s for investigation\n' "$VERSION" >&2 + exit 1 + fi + remote_object="$(printf '%s\n' "$remote_refs" | awk '$2 !~ /\^\{\}$/ { print $1; exit }')" + local_object="$(git rev-parse "refs/tags/$VERSION")" + if [ -z "$remote_object" ]; then + git tag -d "$VERSION" >/dev/null 2>&1 || true + printf 'tag push failed; remote has no tag and the new local tag was removed: %s\n' "$VERSION" >&2 + exit 1 + fi + if [ "$remote_object" != "$local_object" ]; then + printf 'tag push failed and remote %s points elsewhere; keeping local tag for investigation\n' "$VERSION" >&2 + exit 1 + fi + printf 'warning: push reported failure, but push target %s has the exact sealed tag; treating it as published\n' "$push_url" >&2 +fi + +printf 'Release tag pushed: %s -> %s. CI/CD now owns artifact publication.\n' "$VERSION" "$push_url" diff --git a/scripts/release/stage-npm-package.sh b/scripts/release/stage-npm-package.sh new file mode 100755 index 00000000..91dac23b --- /dev/null +++ b/scripts/release/stage-npm-package.sh @@ -0,0 +1,56 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +DEFAULT_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)" +SOURCE_ROOT="${DWS_PACKAGE_SOURCE_ROOT:-$DEFAULT_ROOT}" +DIST_DIR="${DWS_PACKAGE_DIST_DIR:-$DEFAULT_ROOT/dist}" +VERSION="${1:-${DWS_PACKAGE_VERSION:-}}" + +[ -n "$VERSION" ] || { printf 'package version is required\n' >&2; exit 2; } +SEMVER="${VERSION#v}" +printf '%s\n' "$SEMVER" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' || { + printf 'invalid npm package version: %s\n' "$VERSION" >&2 + exit 2 +} + +PKG_ROOT="$DIST_DIR/npm/dingtalk-workspace-cli" +rm -rf "$PKG_ROOT" +mkdir -p "$PKG_ROOT/assets" "$PKG_ROOT/bin" + +cp "$SOURCE_ROOT/build/npm/install.js" "$PKG_ROOT/install.js" +cp "$SOURCE_ROOT/build/npm/bin/dws.js" "$PKG_ROOT/bin/dws.js" +cp "$SOURCE_ROOT/build/npm/README.md" "$PKG_ROOT/README.md" +sed "s|__VERSION__|$SEMVER|g" "$SOURCE_ROOT/build/npm/package.json.tmpl" > "$PKG_ROOT/package.json" + +command -v node >/dev/null 2>&1 || { printf 'node is required to validate the npm manifest\n' >&2; exit 1; } +node - "$PKG_ROOT/package.json" "$SEMVER" <<'NODE' +const fs = require("fs"); +const [manifestPath, version] = process.argv.slice(2); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +const expectedScripts = { postinstall: "node install.js" }; +if (manifest.name !== "dingtalk-workspace-cli") { + throw new Error(`unexpected npm package name: ${manifest.name}`); +} +if (manifest.version !== version) { + throw new Error(`unexpected npm package version: ${manifest.version}`); +} +if (JSON.stringify(manifest.scripts) !== JSON.stringify(expectedScripts)) { + throw new Error(`unexpected npm lifecycle scripts: ${JSON.stringify(manifest.scripts)}`); +} +if (!manifest.bin || manifest.bin.dws !== "./bin/dws.js") { + throw new Error("unexpected npm binary entrypoint"); +} +NODE + +copied=0 +for artifact in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt; do + [ -f "$artifact" ] || continue + cp "$artifact" "$PKG_ROOT/assets/" + copied=$((copied + 1)) +done +[ "$copied" -gt 0 ] || { printf 'no release assets found in %s\n' "$DIST_DIR" >&2; exit 1; } +[ -f "$PKG_ROOT/assets/dws-skills.zip" ] || { printf 'dws-skills.zip is missing\n' >&2; exit 1; } +[ -f "$PKG_ROOT/assets/checksums.txt" ] || { printf 'checksums.txt is missing\n' >&2; exit 1; } + +printf 'Staged npm package %s at %s\n' "$SEMVER" "$PKG_ROOT" diff --git a/scripts/release/sync-to-gitee.sh b/scripts/release/sync-to-gitee.sh index 7d5d4897..6ce19325 100755 --- a/scripts/release/sync-to-gitee.sh +++ b/scripts/release/sync-to-gitee.sh @@ -24,7 +24,7 @@ # Gating: if GITEE_TOKEN / GITEE_USER / GITEE_REPO are unset, exit 0 with a # notice so the step can live in release.yml without breaking forks. -set -eu +set -euo pipefail SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" DIST_DIR="${DIST_DIR:-dist}" @@ -68,6 +68,10 @@ missing="" [ -z "${GITEE_USER:-}" ] && missing="$missing GITEE_USER" [ -z "${GITEE_REPO:-}" ] && missing="$missing GITEE_REPO" if [ -n "$missing" ]; then + if [ "${DWS_REQUIRE_GITEE:-0}" = "1" ]; then + echo "❌ Gitee mirror sync is enabled but credentials are missing:${missing}" >&2 + exit 1 + fi echo "ℹ️ Gitee mirror sync skipped — missing:${missing}" echo " Set these as repo secrets to auto-mirror releases to Gitee for China users." exit 0 @@ -96,6 +100,7 @@ bounded_max_time() { } VERSION="${VERSION:-$(git describe --tags --always 2>/dev/null || echo dev)}" +DWS_PACKAGE_DIST_DIR="$DIST_DIR" "$SCRIPT_DIR/verify-release-artifacts.sh" "$VERSION" OWNER="${GITEE_REPO%%/*}" NAME="${GITEE_REPO##*/}" base="${GITEE_API}/repos/${OWNER}/${NAME}" @@ -121,6 +126,29 @@ api_get() { --max-time "$max_time" "$@" } +gitee_commit="$(gitee_tag_commit || true)" +if [ -n "$gitee_commit" ] && [ "$gitee_commit" != "$target_commit" ]; then + echo "❌ Existing Gitee tag ${VERSION} points to ${gitee_commit}, expected ${target_commit}; refusing to move it." >&2 + exit 1 +fi +if [ -z "$gitee_commit" ]; then + echo " Creating Gitee tag ${VERSION} -> ${target_commit}" + git push "$git_remote" "refs/tags/${VERSION}:refs/tags/${VERSION}" >/dev/null +else + echo " Gitee tag ${VERSION} already points to ${target_commit}." +fi + +for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do + gitee_commit="$(gitee_tag_commit || true)" + [ "$gitee_commit" = "$target_commit" ] && break + sleep 5 +done +[ "$gitee_commit" = "$target_commit" ] || { + echo "❌ Gitee tag ${VERSION} is not aligned after push: got ${gitee_commit:-}, want ${target_commit}" >&2 + exit 1 +} +echo " Gitee tag ${VERSION} is aligned." + # ── Resolve or create the Gitee release for this tag ────────────────────────── rel_json="$(api_get "$GITEE_RELEASE_LOOKUP_MAX_TIME" \ "${base}/releases/tags/${VERSION}?access_token=${GITEE_TOKEN}" 2>/dev/null || true)" @@ -143,26 +171,165 @@ fi [ -n "$release_id" ] || { echo "❌ Could not get/create Gitee release for ${VERSION}. Response: ${rel_json}" >&2; exit 1; } echo " Gitee release id = ${release_id}" -# ── Reconcile the complete, byte-verified release asset set ────────────────── -remaining="$(deadline_remaining)" || err "overall Gitee sync deadline exhausted before asset reconciliation" -reconcile_timeout="$GITEE_RECONCILE_TIMEOUT_SECONDS" -max_child_timeout=$(( remaining - GITEE_CHILD_DEADLINE_RESERVE_SECONDS )) -[ "$max_child_timeout" -gt 0 ] || err "insufficient Gitee sync budget for asset reconciliation" -if [ "$reconcile_timeout" -gt "$max_child_timeout" ]; then - reconcile_timeout="$max_child_timeout" +# ── Mirror each artifact, verifying content so re-runs self-heal ───────────── +# Pull the current attachment list (name + attach id + download url) so we can, +# per file: +# • skip it when it is already on Gitee, unique, AND byte-identical, +# • REPLACE it when present but stale (different bytes) — e.g. the darwin +# binaries are re-signed and so differ between GitHub and a prior mirror +# run, which breaks install.sh's checksums.txt verification on macOS, +# • DEDUP it when the same name has >1 attachment — a prior run that failed to +# delete (see below) left the old copy *and* an extra upload; Gitee then +# serves the OLDER one by name, so the stale binary wins. We delete every +# copy and upload one fresh. +# • upload it when missing. +# This brings the Gitee release into byte-for-byte agreement with $DIST_DIR, +# which the caller fills from the GitHub release whose checksums.txt is what +# install.sh verifies against. +# +# We list attachments via the dedicated /attach_files endpoint, NOT the release +# detail (/releases/{id}) endpoint: the latter's "assets" array omits the attach +# id, so DELETE /attach_files/{id} was previously called with an empty id and +# silently no-op'd — leaving stale + duplicate darwin binaries on Gitee. +load_assets_map() { + curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \ + "${base}/releases/${release_id}/attach_files?access_token=${GITEE_TOKEN}" \ + | python3 -c 'import json,sys +try: + data=json.load(sys.stdin) + rows=data if isinstance(data,list) else data.get("attach_files",[]) + for a in rows: + n=a.get("name",""); i=a.get("id",""); u=a.get("browser_download_url","") + if n and i!="": + print("%s\t%s\t%s" % (n, i, u)) +except Exception: + sys.exit(1)' +} + +assets_map="$(load_assets_map)" || { + echo "❌ Could not list Gitee release attachments; refusing a blind upload." >&2 + exit 1 +} + +sha256_of() { # sha256 of a file ($1) or, with no arg, of stdin + if command -v sha256sum >/dev/null 2>&1; then sha256sum ${1:+"$1"} | awk '{print $1}'; + else shasum -a 256 ${1:+"$1"} | awk '{print $1}'; fi +} + +gitee_attach() { # upload file $1; success when the response carries a download url + file="$1" + fn="$(basename "$file")" + attempt=1 + while [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ]; do + response="$(curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_UPLOAD_MAX_TIME" \ + --retry 2 --retry-delay 5 --retry-all-errors \ + -X POST "${base}/releases/${release_id}/attach_files" \ + -F "access_token=${GITEE_TOKEN}" -F "file=@${file}" 2>&1 || true)" + if printf '%s' "$response" | grep -q '"browser_download_url"'; then + return 0 + fi + echo " ⚠ upload attempt ${attempt}/${GITEE_UPLOAD_RETRIES} failed for ${fn}: $(printf '%s' "$response" | head -c 240)" >&2 + attempt=$((attempt + 1)) + [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ] && sleep "$GITEE_UPLOAD_RETRY_DELAY" + done + return 1 +} + +gitee_delete() { # delete attachment by id $1 + curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \ + -X DELETE "${base}/releases/${release_id}/attach_files/${1}?access_token=${GITEE_TOKEN}" \ + >/dev/null +} + +uploaded=0 +replaced=0 +skipped=0 +failed=0 +for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt; do + [ -f "$f" ] || continue + fn="$(basename "$f")" + local_sha="$(sha256_of "$f")" + # Every attach id currently carrying this name (may be >1 from a botched run). + ids="$(printf '%s\n' "$assets_map" | awk -F'\t' -v n="$fn" '$1==n {print $2}')" + aurl="$(printf '%s\n' "$assets_map" | awk -F'\t' -v n="$fn" '$1==n {print $3; exit}')" + count="$(printf '%s' "$ids" | grep -c . || true)" + + if [ "$count" -eq 0 ]; then + echo " ⬆ ${fn} (new)" + if gitee_attach "$f"; then + uploaded=$((uploaded + 1)) + else + echo " ❌ upload failed for ${fn}" >&2 + failed=$((failed + 1)) + fi + continue + fi + + if [ "$count" -eq 1 ]; then + gitee_sha="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "$aurl" 2>/dev/null | sha256_of || true)" + if [ "$gitee_sha" = "$local_sha" ]; then + echo " ✓ ${fn} already correct on Gitee — skip" + skipped=$((skipped + 1)) + continue + fi + echo " ↻ ${fn} differs on Gitee (stale) — deleting + re-uploading" + else + echo " ↻ ${fn} has ${count} copies on Gitee (dup) — deleting all + re-uploading one" + fi + + # Delete every copy, then upload exactly one fresh, correct file. + delete_failed=0 + for aid in $ids; do + if ! gitee_delete "$aid"; then + echo " ❌ failed to delete stale Gitee attachment ${aid} for ${fn}" >&2 + delete_failed=1 + fi + done + if [ "$delete_failed" -ne 0 ]; then + failed=$((failed + 1)) + continue + fi + if gitee_attach "$f"; then + replaced=$((replaced + 1)) + else + echo " ❌ re-upload failed for ${fn}" >&2 + failed=$((failed + 1)) + fi +done + +if [ "$uploaded" -eq 0 ] && [ "$replaced" -eq 0 ] && [ "$skipped" -eq 0 ]; then + echo "❌ No artifacts found to mirror. Did the build (goreleaser) run / were assets downloaded into ${DIST_DIR}?" >&2 + exit 1 fi -DIST_DIR="$DIST_DIR" \ -GITEE_API="$GITEE_API" \ -GITEE_TOKEN="$GITEE_TOKEN" \ -GITEE_REPO="$GITEE_REPO" \ -GITEE_CURL_CONNECT_TIMEOUT="$GITEE_CURL_CONNECT_TIMEOUT" \ -GITEE_CURL_MAX_TIME="$GITEE_CURL_MAX_TIME" \ -GITEE_RELEASE_ID="$release_id" \ -GITEE_OVERALL_TIMEOUT_SECONDS="$reconcile_timeout" \ - "$SCRIPT_DIR/reconcile-gitee-assets.sh" - -deadline_remaining >/dev/null || err "overall Gitee sync deadline exhausted after asset reconciliation" - +# Gitee may accept an upload before the attachment list is consistent. Re-read +# the release and require every supported asset to be unique and byte-identical. +verified=0 +for verify_attempt in 1 2 3 4 5; do + final_map="$(load_assets_map || true)" + verify_failed=0 + for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt; do + fn="$(basename "$f")" + rows="$(printf '%s\n' "$final_map" | awk -F'\t' -v n="$fn" '$1==n')" + count="$(printf '%s\n' "$rows" | grep -c . || true)" + if [ "$count" -ne 1 ]; then + verify_failed=1 + continue + fi + aurl="$(printf '%s\n' "$rows" | awk -F'\t' 'NR==1 {print $3}')" + remote_sha="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "$aurl" 2>/dev/null | sha256_of || true)" + [ "$remote_sha" = "$(sha256_of "$f")" ] || verify_failed=1 + done + if [ "$verify_failed" -eq 0 ] && [ "$failed" -eq 0 ]; then + verified=1 + break + fi + [ "$verify_attempt" -lt 5 ] && sleep 5 +done +[ "$verified" -eq 1 ] || { + echo "❌ Gitee release ${VERSION} does not contain one byte-identical copy of every release asset." >&2 + exit 1 +} +echo "✅ Gitee release ${VERSION}: uploaded ${uploaded}, replaced ${replaced}, skipped ${skipped} (already correct)." echo " China install: DWS_GITEE_REPO=${GITEE_REPO} \\" echo " curl -fsSL https://gitee.com/${GITEE_REPO}/raw/main/scripts/install.sh | sh" diff --git a/scripts/release/sync-to-oss.sh b/scripts/release/sync-to-oss.sh index 54a7332d..0d972a49 100755 --- a/scripts/release/sync-to-oss.sh +++ b/scripts/release/sync-to-oss.sh @@ -2,19 +2,16 @@ # Copyright 2026 Alibaba Group # Licensed under the Apache License, Version 2.0 # -# Sync release artifacts to a China-accessible OSS mirror so that the install -# scripts' DWS_RELEASE_BASE switch can serve domestic users. +# Sync release artifacts to a China-accessible OSS mirror. Channel pointers are +# mirror metadata; repository installers currently resolve GitHub/Gitee and do +# not consume these OSS pointers directly. # -# Mirror layout produced (matches install.sh RELEASE_BASE="//"): +# Mirror layout produced: # oss://$OSS_BUCKET/$OSS_PREFIX/download//dws--.tar.gz|.zip # oss://$OSS_BUCKET/$OSS_PREFIX/download//checksums.txt # oss://$OSS_BUCKET/$OSS_PREFIX/download//dws-skills.zip -# oss://$OSS_BUCKET/$OSS_PREFIX/latest.txt (plain version string) -# -# So with the CDN/custom domain CNAME'd to the bucket, China users run: -# DWS_RELEASE_BASE=https:///$OSS_PREFIX/download \ -# DWS_VERSION= \ -# curl -fsSL https:///$OSS_PREFIX/install.sh | sh +# oss://$OSS_BUCKET/$OSS_PREFIX/latest.txt (stable version only) +# oss://$OSS_BUCKET/$OSS_PREFIX/beta.txt (prerelease version only) # # Required environment (CI secrets): # OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET, OSS_ENDPOINT, OSS_BUCKET @@ -28,6 +25,9 @@ set -eu +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/release-lib.sh" + DIST_DIR="${DIST_DIR:-dist}" OSS_PREFIX="${OSS_PREFIX:-dws}" @@ -38,6 +38,10 @@ for v in OSS_ACCESS_KEY_ID OSS_ACCESS_KEY_SECRET OSS_ENDPOINT OSS_BUCKET; do [ -z "$val" ] && missing="$missing $v" done if [ -n "$missing" ]; then + if [ "${DWS_REQUIRE_OSS:-0}" = "1" ]; then + echo "❌ OSS mirror sync is required but credentials are missing:${missing}" >&2 + exit 1 + fi echo "ℹ️ OSS mirror sync skipped — missing:${missing}" echo " Set these as repo secrets to auto-publish releases to the China mirror." exit 0 @@ -45,20 +49,60 @@ fi # ── Resolve version ────────────────────────────────────────────────────────── VERSION="${VERSION:-$(git describe --tags --always 2>/dev/null || echo dev)}" +CHANNEL="${DWS_RELEASE_CHANNEL:-$(release_channel_for_version "$VERSION")}" +release_validate_version_channel "$CHANNEL" "$VERSION" echo "📦 Syncing release ${VERSION} → oss://${OSS_BUCKET}/${OSS_PREFIX}/download/${VERSION}/" +# Never move a channel pointer to an incomplete or mixed-version directory, +# even when this helper is invoked outside the normal workflow. +DWS_PACKAGE_DIST_DIR="$DIST_DIR" "$SCRIPT_DIR/verify-release-artifacts.sh" "$VERSION" + # ── Ensure ossutil is available ─────────────────────────────────────────────── OSSUTIL="${OSSUTIL:-ossutil}" +OSSUTIL_INSTALL_DIR="" if ! command -v "$OSSUTIL" >/dev/null 2>&1; then - echo "⬇ ossutil not found; installing to ./.ossutil ..." - os="$(uname -s | tr '[:upper:]' '[:lower:]')" + echo "⬇ ossutil not found; installing a verified temporary copy ..." + case "$(uname -s)" in + Linux) os=linux ;; + Darwin) os=mac ;; + *) printf 'unsupported ossutil operating system: %s\n' "$(uname -s)" >&2; exit 1 ;; + esac arch="$(uname -m)" case "$arch" in x86_64|amd64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; esac - curl -fsSL "https://gosspublic.alicdn.com/ossutil/v2/2.0.0/ossutil-2.0.0-${os}-${arch}.zip" -o /tmp/ossutil.zip - unzip -qo /tmp/ossutil.zip -d /tmp/ossutil-extract - found="$(find /tmp/ossutil-extract -name ossutil -type f | head -1)" - mkdir -p ./.ossutil && cp "$found" ./.ossutil/ossutil && chmod +x ./.ossutil/ossutil - OSSUTIL="./.ossutil/ossutil" + case "${os}-${arch}" in + linux-amd64) expected_ossutil_sha256=3ae4d9fc85a7a6e9f5654d1599766f1a3a42a3692870887b5ae9338d582ef65a ;; + linux-arm64) expected_ossutil_sha256=f6c95ba0c2d2ef30290af686ce4d706c701f4734ce8090bee4288a77e3f1d764 ;; + mac-amd64) expected_ossutil_sha256=8437fdd3ef1a3eb12310f61fcf1c00a5bff5cdab47b4fea815527472e7cf896c ;; + mac-arm64) expected_ossutil_sha256=058fd048f321f8c80def8b748030531646eefe3a82837bf16b581ba7d9c84ac7 ;; + *) printf 'unsupported ossutil architecture: %s-%s\n' "$os" "$arch" >&2; exit 1 ;; + esac + ossutil_archive="$(mktemp "${TMPDIR:-/tmp}/ossutil-2.3.0.XXXXXX.zip")" + ossutil_extract="$(mktemp -d "${TMPDIR:-/tmp}/ossutil-2.3.0.XXXXXX")" + env -u OSS_ACCESS_KEY_ID -u OSS_ACCESS_KEY_SECRET \ + curl -fsSL \ + "https://gosspublic.alicdn.com/ossutil/v2/2.3.0/ossutil-2.3.0-${os}-${arch}.zip" \ + -o "$ossutil_archive" + if command -v sha256sum >/dev/null 2>&1; then + actual_ossutil_sha256="$(sha256sum "$ossutil_archive" | awk '{print $1}')" + else + actual_ossutil_sha256="$(shasum -a 256 "$ossutil_archive" | awk '{print $1}')" + fi + [ "$actual_ossutil_sha256" = "$expected_ossutil_sha256" ] || { + rm -rf "$ossutil_archive" "$ossutil_extract" + printf 'ossutil archive checksum mismatch for %s-%s\n' "$os" "$arch" >&2 + exit 1 + } + unzip -qo "$ossutil_archive" -d "$ossutil_extract" + found="$(find "$ossutil_extract" -name ossutil -type f | head -1)" + [ -n "$found" ] || { + rm -rf "$ossutil_archive" "$ossutil_extract" + printf 'verified ossutil archive does not contain the executable\n' >&2 + exit 1 + } + OSSUTIL_INSTALL_DIR="$(mktemp -d "${TMPDIR:-/tmp}/dws-ossutil.XXXXXX")" + cp "$found" "$OSSUTIL_INSTALL_DIR/ossutil" && chmod +x "$OSSUTIL_INSTALL_DIR/ossutil" + rm -rf "$ossutil_archive" "$ossutil_extract" + OSSUTIL="$OSSUTIL_INSTALL_DIR/ossutil" fi oss_cp() { @@ -70,8 +114,59 @@ oss_cp() { "$1" "oss://${OSS_BUCKET}/$2" } +oss_get() { + # oss_get + "$OSSUTIL" cp -f \ + --access-key-id "$OSS_ACCESS_KEY_ID" \ + --access-key-secret "$OSS_ACCESS_KEY_SECRET" \ + --endpoint "$OSS_ENDPOINT" \ + "oss://${OSS_BUCKET}/$1" "$2" +} + base="${OSS_PREFIX}/download/${VERSION}" +if [ "$CHANNEL" = "stable" ]; then + pointer_name="latest.txt" +else + pointer_name="beta.txt" +fi +current_pointer_file="$(mktemp "${TMPDIR:-/tmp}/dws-current-pointer.XXXXXX")" +pointer_file="$(mktemp "${TMPDIR:-/tmp}/dws-release-pointer.XXXXXX")" +pointer_status_file="$(mktemp "${TMPDIR:-/tmp}/dws-pointer-status.XXXXXX")" +trap 'rm -f "$current_pointer_file" "$pointer_file" "$pointer_status_file"; [ -z "$OSSUTIL_INSTALL_DIR" ] || rm -rf "$OSSUTIL_INSTALL_DIR"' EXIT HUP INT TERM +rm -f "$current_pointer_file" +current_version="" +update_pointer=1 +if oss_get "${OSS_PREFIX}/${pointer_name}" "$current_pointer_file" >"$pointer_status_file" 2>&1; then + [ -s "$current_pointer_file" ] || { + cat "$pointer_status_file" >&2 + echo "❌ OSS ${pointer_name} was read successfully but is empty; refusing to publish." >&2 + exit 1 + } + current_version="$(tr -d '[:space:]' < "$current_pointer_file")" + release_validate_version_channel "$CHANNEL" "$current_version" || { + printf '❌ OSS %s contains an invalid %s channel version: %s\n' "$pointer_name" "$CHANNEL" "$current_version" >&2 + exit 1 + } + if [ "$current_version" != "$VERSION" ]; then + if release_version_is_greater "$VERSION" "$current_version"; then + : + elif release_version_is_greater "$current_version" "$VERSION"; then + update_pointer=0 + echo "ℹ️ OSS ${pointer_name} already points to newer ${current_version}; assets will be repaired without moving it." + else + echo "❌ Cannot order OSS ${pointer_name}=${current_version} against ${VERSION}." >&2 + exit 1 + fi + fi +elif grep -Eq '(^|[^[:alnum:]])NoSuchKey([^[:alnum:]]|$)' "$pointer_status_file"; then + printf 'ℹ️ OSS %s does not exist yet; creating it.\n' "$pointer_name" +else + cat "$pointer_status_file" >&2 + printf '❌ Could not read OSS %s; refusing to move the channel pointer.\n' "$pointer_name" >&2 + exit 1 +fi + # ── Upload binaries + checksums + skills ────────────────────────────────────── uploaded=0 for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt; do @@ -85,16 +180,27 @@ if [ "$uploaded" -eq 0 ]; then exit 1 fi -# ── Publish the latest pointer (for DWS_VERSION=latest resolution on mirror) ── -echo "$VERSION" > /tmp/dws-latest.txt -oss_cp /tmp/dws-latest.txt "${OSS_PREFIX}/latest.txt" +# ── Publish a channel-specific pointer ─────────────────────────────────────── +# A beta must never move latest.txt: that pointer is consumed by ordinary +# installs and is the stable channel contract. +pointer_summary="${pointer_name}" +if [ "$update_pointer" -eq 1 ]; then + echo "$VERSION" > "$pointer_file" + oss_cp "$pointer_file" "${OSS_PREFIX}/${pointer_name}" +else + pointer_summary="${pointer_name} unchanged at ${current_version}" +fi -# ── Publish the install scripts themselves (entry layer) ────────────────────── -for s in install.sh install.ps1 install-skills.sh; do - [ -f "scripts/$s" ] && oss_cp "scripts/$s" "${OSS_PREFIX}/$s" -done +# Shared installer entrypoints are stable-only. A prerelease must not replace +# the stable entry layer, even though those scripts currently fetch from +# GitHub/Gitee rather than this asset mirror. +installer_summary="" +if [ "$CHANNEL" = "stable" ] && [ "$update_pointer" -eq 1 ]; then + for s in install.sh install.ps1 install-skills.sh; do + [ -f "scripts/$s" ] && oss_cp "scripts/$s" "${OSS_PREFIX}/$s" + done + installer_summary=" + stable install scripts" +fi -echo "✅ Synced ${uploaded} artifact(s) + install scripts + latest.txt to the OSS mirror." -echo " China install:" -echo " DWS_RELEASE_BASE=https:///${OSS_PREFIX}/download DWS_VERSION=${VERSION} \\" -echo " curl -fsSL https:///${OSS_PREFIX}/install.sh | sh" +echo "✅ Synced ${uploaded} artifact(s) + ${pointer_summary}${installer_summary} to the OSS mirror." +echo " Mirror path: ${OSS_PREFIX}/download/${VERSION}/" diff --git a/scripts/release/verify-delivered-stable.sh b/scripts/release/verify-delivered-stable.sh new file mode 100755 index 00000000..46b293a6 --- /dev/null +++ b/scripts/release/verify-delivered-stable.sh @@ -0,0 +1,84 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +. "$SCRIPT_DIR/release-lib.sh" + +TAG="${1:-}" +EXPECTED_COMMIT="${2:-}" +REPOSITORY="${DWS_RELEASE_OFFICIAL_REPOSITORY:-DingTalk-Real-AI/dingtalk-workspace-cli}" + +[ -n "$TAG" ] && [ -n "$EXPECTED_COMMIT" ] || { + printf 'usage: verify-delivered-stable.sh \n' >&2 + exit 2 +} +release_is_stable_version "$TAG" || { + printf 'invalid stable delivery baseline: %s\n' "$TAG" >&2 + exit 2 +} +command -v curl >/dev/null 2>&1 || { printf 'curl is required to verify stable delivery\n' >&2; exit 1; } +command -v python3 >/dev/null 2>&1 || { printf 'python3 is required to verify stable delivery\n' >&2; exit 1; } + +API_TOKEN="${DWS_RELEASE_GITHUB_TOKEN:-}" +if [ -z "$API_TOKEN" ] && [ "${GITHUB_ACTIONS:-false}" != "true" ] && command -v gh >/dev/null 2>&1; then + API_TOKEN="$(gh auth token 2>/dev/null || true)" +fi +if [ -z "$API_TOKEN" ]; then + API_TOKEN="${GITHUB_TOKEN:-}" +fi +github_get() { + if [ -n "$API_TOKEN" ]; then + curl -fsSL \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + -H "Authorization: Bearer $API_TOKEN" \ + "https://api.github.com/$1" 2>/dev/null && return 0 + fi + curl -fsSL \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + "https://api.github.com/$1" +} + +if git rev-parse --verify --quiet "refs/tags/$TAG^{commit}" >/dev/null; then + local_commit="$(git rev-parse "refs/tags/$TAG^{commit}")" + [ "$local_commit" = "$EXPECTED_COMMIT" ] || { + printf 'stable tag %s resolves to %s, expected %s\n' "$TAG" "$local_commit" "$EXPECTED_COMMIT" >&2 + exit 1 + } +fi + +stable_state="$( + github_get "repos/$REPOSITORY/releases/tags/$TAG" \ + | python3 -c 'import json,sys +r=json.load(sys.stdin) +print("%s\t%s\t%s" % (r.get("tag_name", ""), str(r.get("draft", "")).lower(), str(r.get("prerelease", "")).lower()))' +)" || { + printf 'could not query previous stable release %s in %s\n' "$TAG" "$REPOSITORY" >&2 + exit 1 +} +[ "$stable_state" = "$(printf '%s\tfalse\tfalse' "$TAG")" ] || { + printf '%s is not a public stable GitHub Release in %s (got: %s)\n' \ + "$TAG" "$REPOSITORY" "$stable_state" >&2 + exit 1 +} + +stable_runs="$( + github_get "repos/$REPOSITORY/actions/workflows/release.yml/runs?branch=$TAG&event=push&status=completed&per_page=100" \ + | python3 -c 'import json,sys +for run in json.load(sys.stdin).get("workflow_runs", []): + print("%s\t%s\t%s" % (run.get("head_sha", ""), run.get("head_branch", ""), run.get("conclusion", "")))' +)" || { + printf 'could not query Release workflow delivery for %s in %s\n' "$TAG" "$REPOSITORY" >&2 + exit 1 +} +printf '%s\n' "$stable_runs" | awk -F '\t' -v sha="$EXPECTED_COMMIT" -v tag="$TAG" ' + $1 == sha && $2 == tag && $3 == "success" { found = 1 } + END { exit(found ? 0 : 1) } +' || { + printf 'Release workflow did not complete successfully for stable baseline %s at %s\n' \ + "$TAG" "$EXPECTED_COMMIT" >&2 + exit 1 +} + +printf 'Delivered stable baseline verified: %s -> %s\n' "$TAG" "$EXPECTED_COMMIT" diff --git a/scripts/release/verify-github-release-assets.sh b/scripts/release/verify-github-release-assets.sh new file mode 100755 index 00000000..3d80771d --- /dev/null +++ b/scripts/release/verify-github-release-assets.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -eu + +TAG="${1:-}" +REPOSITORY="${GITHUB_REPOSITORY:-}" + +[ -n "$TAG" ] && [ -n "$REPOSITORY" ] || { + printf 'usage: GITHUB_REPOSITORY=owner/repo verify-github-release-assets.sh \n' >&2 + exit 2 +} +command -v gh >/dev/null 2>&1 || { printf 'gh is required\n' >&2; exit 1; } + +tmp="$(mktemp -d "${TMPDIR:-/tmp}/dws-github-assets.XXXXXX")" +trap 'rm -rf "$tmp"' EXIT HUP INT TERM +cat > "$tmp/expected" <<'EOF' +checksums.txt +dws-darwin-amd64.tar.gz +dws-darwin-arm64.tar.gz +dws-linux-amd64.tar.gz +dws-linux-arm64.tar.gz +dws-skills.zip +dws-windows-amd64.zip +dws-windows-arm64.zip +EOF +LC_ALL=C sort "$tmp/expected" -o "$tmp/expected" + +gh api -H 'Accept: application/vnd.github+json' \ + "repos/$REPOSITORY/releases/tags/$TAG" \ + --jq '.assets[].name' | LC_ALL=C sort > "$tmp/actual" +if ! diff -u "$tmp/expected" "$tmp/actual"; then + printf 'GitHub Release %s must contain exactly the supported assets\n' "$TAG" >&2 + exit 1 +fi + +printf 'GitHub Release asset set verified: %s\n' "$TAG" diff --git a/scripts/release/verify-github-tag-authority.sh b/scripts/release/verify-github-tag-authority.sh new file mode 100755 index 00000000..0a75b313 --- /dev/null +++ b/scripts/release/verify-github-tag-authority.sh @@ -0,0 +1,37 @@ +#!/bin/sh +set -eu + +TAG="${1:-}" +EXPECTED_COMMIT="${2:-}" +REPOSITORY="${GITHUB_REPOSITORY:-}" + +[ -n "$TAG" ] && [ -n "$EXPECTED_COMMIT" ] && [ -n "$REPOSITORY" ] || { + printf 'usage: GITHUB_REPOSITORY=owner/repo verify-github-tag-authority.sh \n' >&2 + exit 2 +} +command -v gh >/dev/null 2>&1 || { printf 'gh is required\n' >&2; exit 1; } + +local_object="$(git rev-parse "refs/tags/$TAG")" +remote_ref="$( + gh api -H 'Accept: application/vnd.github+json' \ + "repos/$REPOSITORY/git/ref/tags/$TAG" \ + --jq '[.object.type, .object.sha] | @tsv' +)" +[ "$remote_ref" = "$(printf 'tag\t%s' "$local_object")" ] || { + printf 'remote tag object for %s differs from the sealed annotated tag (local=%s remote=%s)\n' \ + "$TAG" "$local_object" "$remote_ref" >&2 + exit 1 +} + +remote_tag="$( + gh api -H 'Accept: application/vnd.github+json' \ + "repos/$REPOSITORY/git/tags/$local_object" \ + --jq '[.tag, .object.type, .object.sha] | @tsv' +)" +[ "$remote_tag" = "$(printf '%s\tcommit\t%s' "$TAG" "$EXPECTED_COMMIT")" ] || { + printf 'remote annotated tag %s does not peel to expected commit %s (got: %s)\n' \ + "$TAG" "$EXPECTED_COMMIT" "$remote_tag" >&2 + exit 1 +} + +printf 'GitHub tag authority verified: %s -> %s\n' "$TAG" "$EXPECTED_COMMIT" diff --git a/scripts/release/verify-package-managers.sh b/scripts/release/verify-package-managers.sh index 14399cc9..28ac1a2c 100755 --- a/scripts/release/verify-package-managers.sh +++ b/scripts/release/verify-package-managers.sh @@ -5,6 +5,10 @@ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" DIST_DIR="${DWS_PACKAGE_DIST_DIR:-$ROOT/dist}" FORMULA_PATH="$DIST_DIR/homebrew/dingtalk-workspace-cli-local.rb" NPM_STAGE_DIR="$DIST_DIR/npm/dingtalk-workspace-cli" +RUN_NPM=1 +RUN_BREW=1 +EXPECTED_VERSION="" +VERIFY_SKILL_TARGETS=1 say() { printf '%s\n' "$*" @@ -23,6 +27,34 @@ need_file() { [ -f "$1" ] || err "required file not found: $1" } +usage() { + printf '%s\n' "usage: $0 [--npm-only|--brew-only] [--expected-version ] [--skip-skill-targets]" >&2 +} + +mode_seen=0 +while [ "$#" -gt 0 ]; do + case "$1" in + --npm-only) + [ "$mode_seen" -eq 0 ] || { usage; exit 2; } + RUN_NPM=1; RUN_BREW=0; mode_seen=1; shift + ;; + --brew-only) + [ "$mode_seen" -eq 0 ] || { usage; exit 2; } + RUN_NPM=0; RUN_BREW=1; mode_seen=1; shift + ;; + --expected-version) + [ "$#" -ge 2 ] || { usage; exit 2; } + EXPECTED_VERSION="${2#v}" + shift 2 + ;; + --skip-skill-targets) VERIFY_SKILL_TARGETS=0; shift ;; + -h|--help) usage; exit 0 ;; + *) usage; exit 2 ;; + esac +done + +[ -z "$EXPECTED_VERSION" ] || need_cmd strings + TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-package-verify-XXXXXX")" HOME_AGENT_PARENTS=" .claude @@ -60,13 +92,11 @@ HOME_SKILL_TARGETS=" .hermes/skills/dws " cleanup() { - if command -v brew >/dev/null 2>&1; then - HOME="$TMP_ROOT/brew-home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ - brew uninstall --force dingtalk-workspace-cli-local >/dev/null 2>&1 || true - if [ -n "${BREW_TAP_NAME:-}" ]; then - HOME="$TMP_ROOT/brew-home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ - brew untap --force "$BREW_TAP_NAME" >/dev/null 2>&1 || true - fi + if [ "$RUN_BREW" -eq 1 ] && command -v brew >/dev/null 2>&1 && [ -n "${BREW_TAP_NAME:-}" ]; then + HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + brew uninstall --force "$BREW_TAP_NAME/dingtalk-workspace-cli-local" >/dev/null 2>&1 || true + HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + brew untap --force "$BREW_TAP_NAME" >/dev/null 2>&1 || true fi rm -rf "$TMP_ROOT" } @@ -111,8 +141,18 @@ verify_npm() { npm install -g "$tarball_path" >/dev/null [ -x "$npm_prefix/bin/dws" ] || err "npm install did not expose dws in $npm_prefix/bin" - "$npm_prefix/bin/dws" --help >/dev/null - verify_skill_targets "$npm_home" + HOME="$npm_home" "$npm_prefix/bin/dws" --help >/dev/null + 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" + EXPECTED_VERSION="$EXPECTED_VERSION" node -e ' + const pkg = require(process.argv[1]); + if (pkg.version !== process.env.EXPECTED_VERSION) process.exit(1); + ' "$NPM_STAGE_DIR/package.json" || err "npm package.json version mismatch" + fi + [ "$VERIFY_SKILL_TARGETS" -eq 0 ] || verify_skill_targets "$npm_home" HOME="$npm_home" npm_config_cache="$npm_cache" npm_config_prefix="$npm_prefix" \ npm uninstall -g dingtalk-workspace-cli >/dev/null @@ -123,6 +163,10 @@ verify_npm() { verify_brew() { need_cmd brew need_file "$FORMULA_PATH" + if [ -n "$EXPECTED_VERSION" ]; then + grep -Fq " version \"$EXPECTED_VERSION\"" "$FORMULA_PATH" || \ + err "Homebrew formula does not declare version $EXPECTED_VERSION" + fi brew_home="$TMP_ROOT/brew-home" mkdir -p "$brew_home" @@ -130,36 +174,39 @@ verify_brew() { BREW_TAP_NAME="local/dws-package-verify-$$" say "==> verifying Homebrew formula install" - HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ - brew uninstall --force dingtalk-workspace-cli-local >/dev/null 2>&1 || true - HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ - brew untap --force "$BREW_TAP_NAME" >/dev/null 2>&1 || true - - HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ brew tap-new --no-git "$BREW_TAP_NAME" >/dev/null tap_repo="$( - HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ brew --repository "$BREW_TAP_NAME" )" mkdir -p "$tap_repo/Formula" - cp "$FORMULA_PATH" "$tap_repo/Formula/dingtalk-workspace-cli-local.rb" + sed "s|skill_home_override = \"\"|skill_home_override = \"$brew_home\"|" \ + "$FORMULA_PATH" > "$tap_repo/Formula/dingtalk-workspace-cli-local.rb" - HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ - brew install -y "$BREW_TAP_NAME/dingtalk-workspace-cli-local" >/dev/null + HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + brew install "$BREW_TAP_NAME/dingtalk-workspace-cli-local" >/dev/null prefix="$( - HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ - brew --prefix dingtalk-workspace-cli-local + HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + brew --prefix "$BREW_TAP_NAME/dingtalk-workspace-cli-local" )" [ -x "$prefix/bin/dws" ] || err "brew install did not create $prefix/bin/dws" - "$prefix/bin/dws" --help >/dev/null - need_file "$prefix/share/dingtalk-workspace-cli-local/skills/dws/SKILL.md" + HOME="$brew_home" "$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" + installed_version="$(HOMEBREW_NO_AUTO_UPDATE=1 brew list --versions "$BREW_TAP_NAME/dingtalk-workspace-cli-local" | awk '{ print $2 }')" + [ "$installed_version" = "$EXPECTED_VERSION" ] || \ + err "Homebrew installed version $installed_version, expected $EXPECTED_VERSION" + fi + [ "$VERIFY_SKILL_TARGETS" -eq 0 ] || verify_skill_targets "$brew_home" } need_file "$DIST_DIR/dws-skills.zip" -verify_npm -verify_brew +[ "$RUN_NPM" -eq 0 ] || verify_npm +[ "$RUN_BREW" -eq 0 ] || verify_brew say "Package-manager verification complete." diff --git a/scripts/release/verify-release-artifacts.sh b/scripts/release/verify-release-artifacts.sh new file mode 100755 index 00000000..0ddc65df --- /dev/null +++ b/scripts/release/verify-release-artifacts.sh @@ -0,0 +1,107 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)" +DIST_DIR="${DWS_PACKAGE_DIST_DIR:-$ROOT/dist}" +VERSION="${1:-${DWS_PACKAGE_VERSION:-}}" + +[ -n "$VERSION" ] || { printf 'expected release version is required\n' >&2; exit 2; } +SEMVER="${VERSION#v}" +CHECKSUMS="$DIST_DIR/checksums.txt" +EXPECTED_PLATFORM_ASSETS=" +dws-darwin-amd64.tar.gz +dws-darwin-arm64.tar.gz +dws-linux-amd64.tar.gz +dws-linux-arm64.tar.gz +dws-windows-amd64.zip +dws-windows-arm64.zip +" +EXPECTED_ASSETS="$EXPECTED_PLATFORM_ASSETS +dws-skills.zip +" + +[ -f "$CHECKSUMS" ] || { printf 'missing checksums.txt in %s\n' "$DIST_DIR" >&2; exit 1; } + +tmp="$(mktemp -d "${TMPDIR:-/tmp}/dws-release-binary.XXXXXX")" +trap 'rm -rf "$tmp"' EXIT HUP INT TERM + +# The public asset namespace is an exact set. GoReleaser may also leave local +# metadata files (artifacts.json/config.yaml/metadata.json), but the upload and +# npm staging globs never publish them. +printf '%s\nchecksums.txt\n' "$EXPECTED_ASSETS" | sed '/^$/d' | LC_ALL=C sort > "$tmp/expected-root" +for path in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$CHECKSUMS"; do + [ -f "$path" ] || continue + basename "$path" +done | LC_ALL=C sort > "$tmp/actual-root" +if ! diff -u "$tmp/expected-root" "$tmp/actual-root"; then + printf 'public release assets must contain exactly the supported files\n' >&2 + exit 1 +fi + +checksum_format_ok=1 +awk ' + NF != 2 { bad = 1; next } + $1 !~ /^[0-9a-fA-F]{64}$/ { bad = 1; next } + { print $2 } + END { if (bad) exit 1 } +' "$CHECKSUMS" > "$tmp/checksum-assets" || checksum_format_ok=0 +[ "$checksum_format_ok" -eq 1 ] || { + printf 'checksums.txt must contain only SHA-256 and filename pairs\n' >&2 + exit 1 +} + +skills_checksum_count="$(awk '$2 == "dws-skills.zip" { count++ } END { print count + 0 }' "$CHECKSUMS")" +[ "$skills_checksum_count" -eq 1 ] || { + printf 'checksums.txt must contain dws-skills.zip exactly once (found %s)\n' "$skills_checksum_count" >&2 + exit 1 +} +printf '%s\n' "$EXPECTED_ASSETS" | sed '/^$/d' | LC_ALL=C sort > "$tmp/expected-checksums" +LC_ALL=C sort "$tmp/checksum-assets" > "$tmp/actual-checksums" +if ! diff -u "$tmp/expected-checksums" "$tmp/actual-checksums"; then + printf 'checksums.txt must describe exactly the supported release assets\n' >&2 + exit 1 +fi + +if command -v sha256sum >/dev/null 2>&1; then + (cd "$DIST_DIR" && sha256sum --check checksums.txt) +elif command -v shasum >/dev/null 2>&1; then + (cd "$DIST_DIR" && shasum -a 256 --check checksums.txt) +else + printf 'sha256sum or shasum is required\n' >&2 + exit 1 +fi + +verify_binary_version() { + asset="$1" + extract_dir="$tmp/extract-${asset}" + mkdir -p "$extract_dir" + case "$asset" in + *.tar.gz) + tar -xzf "$DIST_DIR/$asset" -C "$extract_dir" + binary="$extract_dir/dws" + ;; + *.zip) + unzip -q "$DIST_DIR/$asset" -d "$extract_dir" + binary="$extract_dir/dws.exe" + ;; + *) + printf 'unsupported release archive: %s\n' "$asset" >&2 + return 1 + ;; + esac + [ -f "$binary" ] || { + printf '%s does not contain the expected dws binary\n' "$asset" >&2 + return 1 + } + strings "$binary" | grep -Fqx "v$SEMVER" || { + printf '%s binary does not embed expected version v%s\n' "$asset" "$SEMVER" >&2 + return 1 + } +} + +for asset in $EXPECTED_PLATFORM_ASSETS; do + verify_binary_version "$asset" +done + +printf 'Release artifacts verified for v%s.\n' "$SEMVER" diff --git a/test/scripts/package_script_test.go b/test/scripts/package_script_test.go index b6620246..0dd71e37 100644 --- a/test/scripts/package_script_test.go +++ b/test/scripts/package_script_test.go @@ -105,6 +105,8 @@ func seedDistArchive(t *testing.T, path string) { func postGoreleaserEnv(t *testing.T, distDir, releaseBaseURL string) []string { t.Helper() + trimmedBaseURL := strings.TrimRight(releaseBaseURL, "/") + version := trimmedBaseURL[strings.LastIndex(trimmedBaseURL, "/")+1:] binDir := t.TempDir() fakeCodesign := filepath.Join(binDir, "codesign") @@ -114,7 +116,7 @@ func postGoreleaserEnv(t *testing.T, distDir, releaseBaseURL string) []string { return append(os.Environ(), "PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"), - "DWS_PACKAGE_VERSION=v0.0.0", + "DWS_PACKAGE_VERSION="+version, "DWS_PACKAGE_DIST_DIR="+distDir, "DWS_RELEASE_BASE_URL="+releaseBaseURL, ) @@ -186,6 +188,8 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) { formulaText := string(formulaData) for _, want := range []string{ "class DingtalkWorkspaceCliLocal < Formula", + "version \"1.2.3\"", + "skill_home_override = \"\"", "resource \"skills\" do", "Install locally built DingTalk workspace CLI artifacts for verification", } { @@ -202,14 +206,8 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) { releaseFormulaText := string(releaseFormulaData) for _, want := range []string{ "class DingtalkWorkspaceCli < Formula", - `desc "Automate DingTalk workspace tasks from the terminal"`, - `version "0.0.0"`, - "on_macos do", - "on_linux do", - "https://downloads.example.com/dws/releases/v1.2.3/dws-darwin-amd64.tar.gz", - "https://downloads.example.com/dws/releases/v1.2.3/dws-darwin-arm64.tar.gz", - "https://downloads.example.com/dws/releases/v1.2.3/dws-linux-amd64.tar.gz", - "https://downloads.example.com/dws/releases/v1.2.3/dws-linux-arm64.tar.gz", + "version \"1.2.3\"", + "https://downloads.example.com/dws/releases/v1.2.3/" + archiveName, "https://downloads.example.com/dws/releases/v1.2.3/dws-skills.zip", } { if !strings.Contains(releaseFormulaText, want) { @@ -259,13 +257,20 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) { } } - // Verify checksums.txt was updated to include skills zip + // Re-running post packaging must replace, not duplicate, the skills checksum. + cmd = exec.Command("sh", scriptPath) + cmd.Env = postGoreleaserEnv(t, distDir, "https://downloads.example.com/dws/releases/v1.2.3") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("second post-goreleaser.sh error = %v\noutput:\n%s", err, output) + } + + // Verify checksums.txt includes exactly one skills zip entry. checksumsData, err := os.ReadFile(filepath.Join(distDir, "checksums.txt")) if err != nil { t.Fatalf("ReadFile(checksums.txt) error = %v", err) } - if !strings.Contains(string(checksumsData), "dws-skills.zip") { - t.Fatalf("checksums.txt missing dws-skills.zip entry:\n%s", string(checksumsData)) + if count := strings.Count(string(checksumsData), "dws-skills.zip"); count != 1 { + t.Fatalf("checksums.txt dws-skills.zip count = %d, want 1:\n%s", count, checksumsData) } } @@ -449,7 +454,7 @@ func TestPostGoreleaserAllPlatformNpmAssets(t *testing.T) { } packageAssetsDir := filepath.Join(distDir, "npm", "dingtalk-workspace-cli", "assets") - for _, rel := range append(allArchives, "dws-skills.zip") { + for _, rel := range append(allArchives, "dws-skills.zip", "checksums.txt") { if _, err := os.Stat(filepath.Join(packageAssetsDir, rel)); err != nil { t.Fatalf("npm asset missing %q: %v", rel, err) } diff --git a/test/scripts/release_entry_test.go b/test/scripts/release_entry_test.go new file mode 100644 index 00000000..f81ce5ee --- /dev/null +++ b/test/scripts/release_entry_test.go @@ -0,0 +1,301 @@ +package scripts_test + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +type releaseEntryFixture struct { + repo *releaseTestRepo + entry string + log string +} + +func newReleaseEntryFixture(t *testing.T) *releaseEntryFixture { + t.Helper() + r := newReleaseTestRepo(t) + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + entry := filepath.Join(r.root, "scripts", "release", "dws-release.sh") + releaseCopyFile(t, r.lib, filepath.Join(r.root, "scripts", "release", "release-lib.sh"), 0o644) + releaseCopyFile(t, filepath.Join(sourceRoot, "scripts", "release", "dws-release.sh"), entry, 0o755) + fakeDelegate := func(name string) []byte { + return []byte("#!/bin/sh\nset -eu\nprintf '" + name + "' >> \"$DWS_RELEASE_CALL_LOG\"\nfor arg in \"$@\"; do printf '\\t%s' \"$arg\" >> \"$DWS_RELEASE_CALL_LOG\"; done\nprintf '\\n' >> \"$DWS_RELEASE_CALL_LOG\"\nexit \"${DWS_RELEASE_FAKE_EXIT:-0}\"\n") + } + mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), fakeDelegate("prepare"), 0o755) + mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "release.sh"), fakeDelegate("release"), 0o755) + r.commitAndPush(t, "install unified release entry fixture") + return &releaseEntryFixture{repo: r, entry: entry, log: filepath.Join(t.TempDir(), "calls.log")} +} + +func (f *releaseEntryFixture) run(t *testing.T, extraEnv []string, args ...string) (string, error) { + t.Helper() + cmd := exec.Command("sh", append([]string{f.entry}, args...)...) + cmd.Dir = f.repo.root + cmd.Env = append(os.Environ(), "DWS_RELEASE_CALL_LOG="+f.log) + cmd.Env = append(cmd.Env, extraEnv...) + output, err := cmd.CombinedOutput() + return string(output), err +} + +func (f *releaseEntryFixture) configureRemote(t *testing.T, remote string) { + t.Helper() + output, err := f.run(t, nil, "config", "--remote", remote) + if err != nil { + t.Fatalf("config remote error = %v\noutput:\n%s", err, output) + } +} + +func (f *releaseEntryFixture) callLog(t *testing.T) string { + t.Helper() + data, err := os.ReadFile(f.log) + if errors.Is(err, os.ErrNotExist) { + return "" + } + if err != nil { + t.Fatalf("ReadFile(call log) error = %v", err) + } + return string(data) +} + +func TestDWSReleaseEntryPreparesMissingChangelogAndStops(t *testing.T) { + tests := []struct { + name string + args []string + want string + }{ + { + name: "prerelease", + args: []string{"v1.0.1-beta.1", "--remote", "origin", "--publish"}, + want: "prepare\tprerelease\tv1.0.1-beta.1\n", + }, + { + name: "stable", + args: []string{"v1.0.1", "--from-beta", "v1.0.1-beta.1", "--remote", "origin"}, + want: "prepare\tstable\tv1.0.1\t--from-beta\tv1.0.1-beta.1\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f := newReleaseEntryFixture(t) + f.configureRemote(t, "origin") + output, err := f.run(t, nil, test.args...) + if err != nil { + t.Fatalf("dws-release prepare error = %v\noutput:\n%s", err, output) + } + if got := f.callLog(t); got != test.want { + t.Fatalf("delegate calls = %q, want %q", got, test.want) + } + if !strings.Contains(output, "publishing intentionally stopped") { + t.Fatalf("prepare output did not make the stop boundary clear:\n%s", output) + } + }) + } +} + +func TestDWSReleaseEntryRoutesOneGuardedCommand(t *testing.T) { + tests := []struct { + name string + section string + args []string + wantCall string + }{ + { + name: "check prerelease", + section: betaSection(), + args: []string{"v1.0.1-beta.1", "--remote", "origin"}, + wantCall: "release\tprerelease\tv1.0.1-beta.1\t--remote\torigin\n", + }, + { + name: "publish prerelease keeps confirmation", + section: betaSection(), + args: []string{"v1.0.1-beta.1", "--remote", "origin", "--publish"}, + wantCall: "release\tprerelease\tv1.0.1-beta.1\t--remote\torigin\t--publish\n", + }, + { + name: "check stable", + section: stableSection(), + args: []string{"v1.0.1", "--from-beta", "v1.0.1-beta.1", "--remote", "origin"}, + wantCall: "release\tstable\tv1.0.1\t--remote\torigin\t--from-beta\tv1.0.1-beta.1\n", + }, + { + name: "publish stable keeps confirmation", + section: stableSection(), + args: []string{"v1.0.1", "--from-beta", "v1.0.1-beta.1", "--remote", "origin", "--publish"}, + wantCall: "release\tstable\tv1.0.1\t--remote\torigin\t--from-beta\tv1.0.1-beta.1\t--publish\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f := newReleaseEntryFixture(t) + f.configureRemote(t, "origin") + mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(test.section)), 0o644) + f.repo.commitAndPush(t, "add candidate changelog") + output, err := f.run(t, nil, test.args...) + if err != nil { + t.Fatalf("dws-release route error = %v\noutput:\n%s", err, output) + } + if got := f.callLog(t); got != test.wantCall { + t.Fatalf("delegate calls = %q, want %q\noutput:\n%s", got, test.wantCall, output) + } + }) + } +} + +func TestDWSReleaseEntryRejectsInvalidInvocationBeforeDelegation(t *testing.T) { + tests := []struct { + name string + args []string + want string + }{ + {name: "invalid version", args: []string{"1.0.1"}, want: "invalid release version"}, + {name: "stable missing beta", args: []string{"v1.0.1"}, want: "requires --from-beta"}, + {name: "prerelease with beta", args: []string{"v1.0.1-beta.1", "--from-beta", "v1.0.1-beta.1"}, want: "only valid for stable"}, + {name: "conflicting mode", args: []string{"v1.0.1-beta.1", "--check", "--publish"}, want: "cannot be used together"}, + {name: "yes bypass", args: []string{"v1.0.1-beta.1", "--publish", "--yes"}, want: "unknown argument"}, + {name: "unknown flag", args: []string{"v1.0.1-beta.1", "--force"}, want: "unknown argument"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f := newReleaseEntryFixture(t) + output, err := f.run(t, nil, test.args...) + if err == nil || !strings.Contains(output, test.want) { + t.Fatalf("invalid invocation: err=%v, want=%q\noutput:\n%s", err, test.want, output) + } + if got := f.callLog(t); got != "" { + t.Fatalf("invalid invocation delegated work: %q", got) + } + }) + } +} + +func TestDWSReleaseEntryUsesConfiguredRemote(t *testing.T) { + f := newReleaseEntryFixture(t) + f.configureRemote(t, "origin") + mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + f.repo.commitAndPush(t, "add beta changelog") + output, err := f.run(t, nil, "v1.0.1-beta.1") + if err != nil { + t.Fatalf("configured remote route error = %v\noutput:\n%s", err, output) + } + want := "release\tprerelease\tv1.0.1-beta.1\t--remote\torigin\n" + if got := f.callLog(t); got != want { + t.Fatalf("delegate calls = %q, want %q", got, want) + } +} + +func TestDWSReleaseEntryFailsClosedWithoutRemote(t *testing.T) { + f := newReleaseEntryFixture(t) + mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + f.repo.commitAndPush(t, "add beta changelog") + output, err := f.run(t, nil, "v1.0.1-beta.1") + if err == nil || !strings.Contains(output, "release remote is not configured") { + t.Fatalf("missing remote did not fail closed: err=%v\noutput:\n%s", err, output) + } + if got := f.callLog(t); got != "" { + t.Fatalf("missing remote delegated work: %q", got) + } +} + +func TestDWSReleaseEntryPropagatesDelegateFailure(t *testing.T) { + f := newReleaseEntryFixture(t) + f.configureRemote(t, "origin") + mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + f.repo.commitAndPush(t, "add beta changelog") + output, err := f.run(t, []string{"DWS_RELEASE_FAKE_EXIT=23"}, "v1.0.1-beta.1", "--remote", "origin") + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) || exitErr.ExitCode() != 23 { + t.Fatalf("delegate error was not propagated: err=%v\noutput:\n%s", err, output) + } +} + +func TestDWSReleaseEntryFastForwardsCleanMain(t *testing.T) { + f := newReleaseEntryFixture(t) + f.configureRemote(t, "origin") + other := filepath.Join(t.TempDir(), "other") + mustRun(t, filepath.Dir(other), "git", "clone", "-b", "main", f.repo.remote, other) + mustRun(t, other, "git", "config", "user.name", "Release Test") + mustRun(t, other, "git", "config", "user.email", "release-test@example.com") + mustWriteFile(t, filepath.Join(other, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + mustRun(t, other, "git", "add", "CHANGELOG.md") + mustRun(t, other, "git", "commit", "-m", "add remote beta changelog") + mustRun(t, other, "git", "push", "origin", "main") + + output, err := f.run(t, nil, "v1.0.1-beta.1", "--remote", "origin") + if err != nil { + t.Fatalf("fast-forward route error = %v\noutput:\n%s", err, output) + } + localHead := strings.TrimSpace(mustOutput(t, f.repo.root, "git", "rev-parse", "HEAD")) + remoteHead := strings.TrimSpace(mustOutput(t, f.repo.root, "git", "rev-parse", "refs/remotes/origin/main")) + if localHead != remoteHead { + t.Fatalf("local main was not fast-forwarded: local=%s remote=%s", localHead, remoteHead) + } + if got := f.callLog(t); !strings.HasPrefix(got, "release\tprerelease\tv1.0.1-beta.1") { + t.Fatalf("fast-forward did not reach guarded delegate: %q", got) + } +} + +func TestDWSReleaseEntryRejectsRetargetedRemote(t *testing.T) { + f := newReleaseEntryFixture(t) + f.configureRemote(t, "origin") + mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + f.repo.commitAndPush(t, "add beta changelog") + otherRemote := filepath.Join(t.TempDir(), "other.git") + mustRun(t, f.repo.root, "git", "init", "--bare", otherRemote) + mustRun(t, f.repo.root, "git", "remote", "set-url", "origin", otherRemote) + + output, err := f.run(t, nil, "v1.0.1-beta.1") + if err == nil || !strings.Contains(output, "changed repository identity") { + t.Fatalf("retargeted remote was not rejected: err=%v\noutput:\n%s", err, output) + } + if got := f.callLog(t); got != "" { + t.Fatalf("retargeted remote delegated work: %q", got) + } +} + +func TestDWSReleaseEntryRejectsAuthorityOverrides(t *testing.T) { + tests := []struct { + name string + env string + want string + }{ + {name: "non github remote", env: "DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE=1", want: "test-only"}, + {name: "official tags URL", env: "DWS_RELEASE_OFFICIAL_TAGS_URL=https://example.com/tags.git", want: "cannot override"}, + {name: "official repository", env: "DWS_RELEASE_OFFICIAL_REPOSITORY=other/repo", want: "cannot override"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f := newReleaseEntryFixture(t) + output, err := f.run(t, []string{test.env}, "v1.0.1-beta.1") + if err == nil || !strings.Contains(output, test.want) { + t.Fatalf("authority override was not rejected: err=%v, want=%q\noutput:\n%s", err, test.want, output) + } + if got := f.callLog(t); got != "" { + t.Fatalf("authority override delegated work: %q", got) + } + }) + } +} + +func TestReleaseCommandRejectsNonInteractivePublishWithoutYes(t *testing.T) { + r := newReleaseTestRepo(t) + installReleaseCommandFixture(t, r) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + r.commitAndPush(t, "install release automation") + + output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"), + "prerelease", "v1.0.1-beta.1", "--remote", "origin", "--publish", + ) + if err == nil || !strings.Contains(output, "interactive confirmation is unavailable") { + t.Fatalf("non-interactive publish did not require confirmation: err=%v\noutput:\n%s", err, output) + } + if got := mustOutput(t, r.root, "git", "tag", "--list", "v1.0.1-beta.1"); got != "" { + t.Fatalf("rejected non-interactive publish created a tag: %s", got) + } +} diff --git a/test/scripts/release_script_test.go b/test/scripts/release_script_test.go new file mode 100644 index 00000000..50773aff --- /dev/null +++ b/test/scripts/release_script_test.go @@ -0,0 +1,924 @@ +package scripts_test + +import ( + "crypto/sha256" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "testing" +) + +var releasePlatformAssets = []string{ + "dws-darwin-amd64.tar.gz", + "dws-darwin-arm64.tar.gz", + "dws-linux-amd64.tar.gz", + "dws-linux-arm64.tar.gz", + "dws-windows-amd64.zip", + "dws-windows-arm64.zip", +} + +func writeVersionedReleaseArchive(t *testing.T, dist, asset, version string) { + t.Helper() + stage := t.TempDir() + binary := "dws" + if strings.HasSuffix(asset, ".zip") { + binary = "dws.exe" + } + 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 + } + mustRun(t, stage, "tar", "-czf", filepath.Join(dist, asset), binary) +} + +func writeReleaseChecksums(t *testing.T, dist string, includeSkills bool) { + t.Helper() + assets := append([]string{}, releasePlatformAssets...) + if includeSkills { + assets = append(assets, "dws-skills.zip") + } + sort.Strings(assets) + var lines []string + for _, asset := range assets { + data, err := os.ReadFile(filepath.Join(dist, asset)) + if err != nil { + t.Fatalf("ReadFile(%s) error = %v", asset, err) + } + lines = append(lines, fmt.Sprintf("%x %s", sha256.Sum256(data), asset)) + } + mustWriteFile(t, filepath.Join(dist, "checksums.txt"), []byte(strings.Join(lines, "\n")+"\n"), 0o644) +} + +func seedVersionedReleaseArtifacts(t *testing.T, dist, version string) { + t.Helper() + if err := os.MkdirAll(dist, 0o755); err != nil { + t.Fatalf("MkdirAll(%s) error = %v", dist, err) + } + for _, asset := range releasePlatformAssets { + writeVersionedReleaseArchive(t, dist, asset, version) + } + mustWriteFile(t, filepath.Join(dist, "dws-skills.zip"), []byte("fake skills\n"), 0o644) + writeReleaseChecksums(t, dist, true) +} + +type releaseTestRepo struct { + root string + remote string + contract string + prepare string + releaseCmd string + lib string + verify string +} + +func newReleaseTestRepo(t *testing.T) *releaseTestRepo { + t.Helper() + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + + base := t.TempDir() + root := filepath.Join(base, "work") + remote := filepath.Join(base, "remote.git") + mustRun(t, base, "git", "init", "--bare", remote) + mustRun(t, base, "git", "init", "-b", "main", root) + mustRun(t, root, "git", "config", "user.name", "Release Test") + mustRun(t, root, "git", "config", "user.email", "release-test@example.com") + + mustWriteFile(t, filepath.Join(root, "CHANGELOG.md"), []byte(releaseChangelog()), 0o644) + mustWriteFile(t, filepath.Join(root, "seed.txt"), []byte("initial\n"), 0o644) + mustRun(t, root, "git", "add", ".") + mustRun(t, root, "git", "commit", "-m", "initial stable") + mustRun(t, root, "git", "tag", "-a", "v1.0.0", "-m", "Release v1.0.0") + mustRun(t, root, "git", "remote", "add", "origin", remote) + mustRun(t, root, "git", "push", "-u", "origin", "main") + mustRun(t, root, "git", "push", "origin", "v1.0.0") + + return &releaseTestRepo{ + root: root, + remote: remote, + contract: filepath.Join(sourceRoot, "scripts", "release", "release-contract.sh"), + prepare: filepath.Join(sourceRoot, "scripts", "release", "prepare-changelog.sh"), + releaseCmd: filepath.Join(sourceRoot, "scripts", "release", "release.sh"), + lib: filepath.Join(sourceRoot, "scripts", "release", "release-lib.sh"), + verify: filepath.Join(sourceRoot, "scripts", "release", "verify-release-artifacts.sh"), + } +} + +func TestReleaseVersionOrdering(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + lib := filepath.Join(sourceRoot, "scripts", "release", "release-lib.sh") + tests := []struct { + candidate string + baseline string + greater bool + }{ + {candidate: "v1.0.2", baseline: "v1.0.1", greater: true}, + {candidate: "v1.1.0-beta.1", baseline: "v1.0.9-beta.9", greater: true}, + {candidate: "v1.0.1-beta.2", baseline: "v1.0.1-beta.1", greater: true}, + {candidate: "v1.0.1", baseline: "v1.0.1-beta.9", greater: true}, + {candidate: "v1.0.1-beta.1", baseline: "v1.0.1-beta.2", greater: false}, + {candidate: "v1.0.1-beta.1", baseline: "v1.0.1", greater: false}, + {candidate: "v1.0.1", baseline: "v1.0.1", greater: false}, + } + for _, test := range tests { + cmd := exec.Command("sh", "-c", `. "$1"; release_version_is_greater "$2" "$3"`, "sh", lib, test.candidate, test.baseline) + err := cmd.Run() + if (err == nil) != test.greater { + t.Fatalf("release_version_is_greater(%s, %s) error = %v, want greater=%v", test.candidate, test.baseline, err, test.greater) + } + } +} + +func TestReleaseNpmPackingIgnoresLifecycleScripts(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + packageDir := filepath.Join(t.TempDir(), "package") + marker := filepath.Join(t.TempDir(), "prepack-ran") + manifest := fmt.Sprintf(`{ + "name": "dingtalk-workspace-cli", + "version": "1.2.3", + "scripts": {"prepack": "touch %s"}, + "files": ["README.md"] +} +`, marker) + mustWriteFile(t, filepath.Join(packageDir, "package.json"), []byte(manifest), 0o644) + mustWriteFile(t, filepath.Join(packageDir, "README.md"), []byte("test package\n"), 0o644) + outputTarball := filepath.Join(t.TempDir(), "package.tgz") + cmd := exec.Command("sh", filepath.Join(sourceRoot, "scripts", "release", "pack-npm-package.sh"), packageDir, outputTarball) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("pack-npm-package error = %v\noutput:\n%s", err, output) + } + if !strings.HasPrefix(strings.TrimSpace(string(output)), "sha512-") { + t.Fatalf("pack output is not an integrity value: %s", output) + } + if _, err := os.Stat(outputTarball); err != nil { + t.Fatalf("packed tarball missing: %v", err) + } + if _, err := os.Stat(marker); !os.IsNotExist(err) { + t.Fatalf("npm prepack lifecycle unexpectedly ran: %v", err) + } +} + +func TestReleaseNpmStagingRejectsUnexpectedLifecycleScripts(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + root := t.TempDir() + source := filepath.Join(root, "source") + dist := filepath.Join(root, "dist") + mustWriteFile(t, filepath.Join(source, "build", "npm", "install.js"), []byte("\n"), 0o644) + mustWriteFile(t, filepath.Join(source, "build", "npm", "bin", "dws.js"), []byte("\n"), 0o755) + mustWriteFile(t, filepath.Join(source, "build", "npm", "README.md"), []byte("test\n"), 0o644) + mustWriteFile(t, filepath.Join(source, "build", "npm", "package.json.tmpl"), []byte(`{ + "name": "dingtalk-workspace-cli", + "version": "__VERSION__", + "bin": {"dws": "./bin/dws.js"}, + "scripts": {"postinstall": "node install.js", "prepublishOnly": "node steal.js"} +} +`), 0o644) + cmd := exec.Command("sh", filepath.Join(sourceRoot, "scripts", "release", "stage-npm-package.sh"), "v1.2.3") + cmd.Env = append(os.Environ(), "DWS_PACKAGE_SOURCE_ROOT="+source, "DWS_PACKAGE_DIST_DIR="+dist) + output, err := cmd.CombinedOutput() + if err == nil || !strings.Contains(string(output), "unexpected npm lifecycle scripts") { + t.Fatalf("unexpected lifecycle script was not rejected: err=%v\noutput:\n%s", err, output) + } +} + +func TestReleaseGitHubAssetSetIsExact(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + binDir := t.TempDir() + fakeGH := filepath.Join(binDir, "gh") + mustWriteFile(t, fakeGH, []byte("#!/bin/sh\nset -eu\nprintf '%s\\n' \"$GH_ASSETS\"\n"), 0o755) + exact := strings.Join([]string{ + "dws-windows-arm64.zip", + "dws-linux-amd64.tar.gz", + "checksums.txt", + "dws-skills.zip", + "dws-darwin-amd64.tar.gz", + "dws-windows-amd64.zip", + "dws-linux-arm64.tar.gz", + "dws-darwin-arm64.tar.gz", + }, "\n") + script := filepath.Join(sourceRoot, "scripts", "release", "verify-github-release-assets.sh") + run := func(assets string) (string, error) { + cmd := exec.Command("sh", script, "v1.2.3") + cmd.Env = []string{ + "PATH=" + binDir + string(os.PathListSeparator) + os.Getenv("PATH"), + "HOME=" + t.TempDir(), + "GITHUB_REPOSITORY=owner/repo", + "GH_ASSETS=" + assets, + } + output, err := cmd.CombinedOutput() + return string(output), err + } + if output, err := run(exact); err != nil { + t.Fatalf("exact GitHub assets rejected: %v\n%s", err, output) + } + if output, err := run(exact + "\nmalware.exe"); err == nil || !strings.Contains(output, "exactly the supported assets") { + t.Fatalf("extra GitHub asset was not rejected: err=%v\n%s", err, output) + } +} + +func TestReleaseDeliveredStableRequiresSuccessfulPublicDelivery(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + r := newReleaseTestRepo(t) + commit := strings.TrimSpace(mustOutput(t, r.root, "git", "rev-parse", "v1.0.0^{commit}")) + binDir := t.TempDir() + fakeCurl := filepath.Join(binDir, "curl") + mustWriteFile(t, fakeCurl, []byte(`#!/bin/sh +set -eu +for arg in "$@"; do last="$arg"; done +case "$last" in + */releases/tags/*) + printf '{"tag_name":"v1.0.0","draft":false,"prerelease":false}\n' + ;; + */actions/workflows/release.yml/runs*) + printf '{"workflow_runs":[{"head_sha":"%s","head_branch":"v1.0.0","conclusion":"%s"}]}\n' "$EXPECTED_COMMIT" "$RUN_CONCLUSION" + ;; + *) exit 1 ;; +esac +`), 0o755) + script := filepath.Join(sourceRoot, "scripts", "release", "verify-delivered-stable.sh") + run := func(conclusion string) (string, error) { + cmd := exec.Command("sh", script, "v1.0.0", commit) + cmd.Dir = r.root + cmd.Env = []string{ + "PATH=" + binDir + string(os.PathListSeparator) + os.Getenv("PATH"), + "HOME=" + t.TempDir(), + "DWS_RELEASE_OFFICIAL_REPOSITORY=owner/repo", + "EXPECTED_COMMIT=" + commit, + "RUN_CONCLUSION=" + conclusion, + } + output, err := cmd.CombinedOutput() + return string(output), err + } + if output, err := run("success"); err != nil { + t.Fatalf("delivered stable was rejected: %v\n%s", err, output) + } + if output, err := run("failure"); err == nil || !strings.Contains(output, "did not complete successfully") { + t.Fatalf("orphan stable tag was not rejected: err=%v\n%s", err, output) + } +} + +func releaseChangelog(sections ...string) string { + text := "# Changelog\n\n## [Unreleased]\n\n" + for _, section := range sections { + text += section + if !strings.HasSuffix(text, "\n\n") { + text += "\n" + } + } + text += "## [1.0.0] - 2026-07-01\n\n### Changed\n\n- Initial release.\n" + return text +} + +func betaSection() string { + return "## [1.0.1-beta.1] - 2026-07-11\n\n### Changed\n\n- Validate the sealed beta candidate.\n\n" +} + +func stableSection() string { + return "## [1.0.1] - 2026-07-11\n\nThis release promotes the sealed `v1.0.1-beta.1` contents to stable.\n\n### Changed\n\n- Publish the validated candidate to the stable channel.\n\n" +} + +func (r *releaseTestRepo) commitAndPush(t *testing.T, message string) { + t.Helper() + mustRun(t, r.root, "git", "add", ".") + mustRun(t, r.root, "git", "commit", "-m", message) + mustRun(t, r.root, "git", "push", "origin", "main") +} + +func (r *releaseTestRepo) seedBeta(t *testing.T) { + t.Helper() + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + mustWriteFile(t, filepath.Join(r.root, "feature.txt"), []byte("sealed candidate\n"), 0o644) + r.commitAndPush(t, "prepare beta") + mustRun(t, r.root, "git", "tag", "-a", "v1.0.1-beta.1", "-m", "Release v1.0.1-beta.1", "-m", "Channel: prerelease") + mustRun(t, r.root, "git", "push", "origin", "v1.0.1-beta.1") +} + +func runReleaseScript(t *testing.T, workdir, script string, args ...string) (string, error) { + t.Helper() + cmd := exec.Command("sh", append([]string{script}, args...)...) + cmd.Dir = workdir + cmd.Env = append(os.Environ(), "DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE=1") + if remote, err := exec.Command("git", "-C", workdir, "remote", "get-url", "origin").Output(); err == nil { + cmd.Env = append(cmd.Env, "DWS_RELEASE_OFFICIAL_TAGS_URL="+strings.TrimSpace(string(remote))) + } + output, err := cmd.CombinedOutput() + return string(output), err +} + +func TestReleaseContractAcceptsPrereleaseAndWritesNotes(t *testing.T) { + r := newReleaseTestRepo(t) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + r.commitAndPush(t, "prepare beta changelog") + + notes := filepath.Join(t.TempDir(), "notes.md") + output, err := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "prerelease", + "--version", "v1.0.1-beta.1", + "--context", "local", + "--remote", "origin", + "--notes-output", notes, + ) + if err != nil { + t.Fatalf("release contract error = %v\noutput:\n%s", err, output) + } + if !strings.Contains(output, "Release contract passed") { + t.Fatalf("contract output missing success:\n%s", output) + } + notesData, err := os.ReadFile(notes) + if err != nil { + t.Fatalf("ReadFile(notes) error = %v", err) + } + if !strings.Contains(string(notesData), "Validate the sealed beta candidate") { + t.Fatalf("notes did not come from exact changelog section:\n%s", notesData) + } +} + +func TestReleaseContractRejectsInvalidVersionChannelPairs(t *testing.T) { + r := newReleaseTestRepo(t) + tests := []struct { + channel string + version string + }{ + {channel: "prerelease", version: "v1.0.1"}, + {channel: "stable", version: "v1.0.1-beta.1"}, + {channel: "prerelease", version: "v1.0.1-rc.1"}, + {channel: "prerelease", version: "v1.0.1-preview"}, + {channel: "stable", version: "1.0.1"}, + {channel: "stable", version: "v01.0.1"}, + {channel: "prerelease", version: "v1.0.1-beta.0"}, + {channel: "prerelease", version: "v1.0.1-beta.2"}, + } + for _, test := range tests { + t.Run(test.channel+"_"+strings.ReplaceAll(test.version, ".", "_"), func(t *testing.T) { + output, err := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", test.channel, + "--version", test.version, + ) + if err == nil { + t.Fatalf("invalid pair unexpectedly passed:\n%s", output) + } + }) + } +} + +func TestReleaseContractRejectsBadChangelogSections(t *testing.T) { + tests := []struct { + name string + sections string + want string + }{ + {name: "missing", sections: "", want: "exactly one section"}, + {name: "empty", sections: "## [1.0.1-beta.1] - 2026-07-11\n\n", want: "must contain release notes"}, + {name: "placeholder", sections: "## [1.0.1-beta.1] - 2026-07-11\n\n### Changed\n\n- TODO: write this.\n\n", want: "TODO/TBD"}, + {name: "duplicate", sections: betaSection() + betaSection(), want: "exactly one section"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + r := newReleaseTestRepo(t) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(test.sections)), 0o644) + mustWriteFile(t, filepath.Join(r.root, "candidate.txt"), []byte(test.name+"\n"), 0o644) + r.commitAndPush(t, "write candidate changelog") + output, err := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "prerelease", + "--version", "v1.0.1-beta.1", + "--remote", "origin", + ) + if err == nil { + t.Fatalf("bad changelog unexpectedly passed:\n%s", output) + } + if !strings.Contains(output, test.want) { + t.Fatalf("output missing %q:\n%s", test.want, output) + } + }) + } +} + +func TestReleaseContractRejectsDirtyOrUnsyncedMain(t *testing.T) { + r := newReleaseTestRepo(t) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + r.commitAndPush(t, "prepare beta") + + mustWriteFile(t, filepath.Join(r.root, "dirty.txt"), []byte("dirty\n"), 0o644) + output, err := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "prerelease", + "--version", "v1.0.1-beta.1", + "--remote", "origin", + ) + if err == nil || !strings.Contains(output, "worktree must be clean") { + t.Fatalf("dirty worktree was not blocked: err=%v\noutput:\n%s", err, output) + } + + mustRun(t, r.root, "git", "add", "dirty.txt") + mustRun(t, r.root, "git", "commit", "-m", "local commit not pushed") + output, err = runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "prerelease", + "--version", "v1.0.1-beta.1", + "--remote", "origin", + ) + if err == nil || !strings.Contains(output, "must exactly match origin/main") { + t.Fatalf("unsynced main was not blocked: err=%v\noutput:\n%s", err, output) + } +} + +func TestReleaseContractStablePromotionAllowsOnlyChangelogDiff(t *testing.T) { + t.Run("sealed", func(t *testing.T) { + r := newReleaseTestRepo(t) + r.seedBeta(t) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(stableSection(), betaSection())), 0o644) + r.commitAndPush(t, "prepare stable changelog") + + output, err := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "stable", + "--version", "v1.0.1", + "--from-beta", "v1.0.1-beta.1", + "--remote", "origin", + ) + if err != nil { + t.Fatalf("sealed stable promotion error = %v\noutput:\n%s", err, output) + } + }) + + t.Run("source drift", func(t *testing.T) { + r := newReleaseTestRepo(t) + r.seedBeta(t) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(stableSection(), betaSection())), 0o644) + mustWriteFile(t, filepath.Join(r.root, "drift.txt"), []byte("untested change\n"), 0o644) + r.commitAndPush(t, "drift after beta") + + output, err := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "stable", + "--version", "v1.0.1", + "--from-beta", "v1.0.1-beta.1", + "--remote", "origin", + ) + if err == nil { + t.Fatalf("drifted stable promotion unexpectedly passed:\n%s", output) + } + if !strings.Contains(output, "only CHANGELOG.md may differ") || !strings.Contains(output, "drift.txt") { + t.Fatalf("drift output is not actionable:\n%s", output) + } + }) +} + +func TestReleaseContractCIReadsStableBaselineFromAnnotatedTag(t *testing.T) { + r := newReleaseTestRepo(t) + r.seedBeta(t) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(stableSection(), betaSection())), 0o644) + r.commitAndPush(t, "prepare stable changelog") + mustRun(t, r.root, "git", "tag", "-a", "v1.0.1", "-m", "Release v1.0.1", "-m", "Channel: stable", "-m", "From-Beta: v1.0.1-beta.1") + mustRun(t, r.root, "git", "push", "origin", "v1.0.1") + mustRun(t, r.root, "git", "checkout", "--detach", "v1.0.1") + + metadata := filepath.Join(t.TempDir(), "metadata") + output, err := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "stable", + "--version", "v1.0.1", + "--context", "ci", + "--remote", "origin", + "--metadata-output", metadata, + ) + if err != nil { + t.Fatalf("CI stable contract error = %v\noutput:\n%s", err, output) + } + metadataData, err := os.ReadFile(metadata) + if err != nil { + t.Fatalf("ReadFile(metadata) error = %v", err) + } + if !strings.Contains(string(metadataData), "from_beta=v1.0.1-beta.1") { + t.Fatalf("metadata missing annotated tag baseline:\n%s", metadataData) + } +} + +func TestReleaseContractCIRejectsLightweightTag(t *testing.T) { + r := newReleaseTestRepo(t) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + r.commitAndPush(t, "prepare beta") + mustRun(t, r.root, "git", "tag", "v1.0.1-beta.1") + mustRun(t, r.root, "git", "push", "origin", "v1.0.1-beta.1") + + output, err := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "prerelease", + "--version", "v1.0.1-beta.1", + "--context", "ci", + "--remote", "origin", + ) + if err == nil { + t.Fatalf("lightweight release tag unexpectedly passed:\n%s", output) + } + if !strings.Contains(output, "must be annotated") { + t.Fatalf("output missing annotated-tag guidance:\n%s", output) + } +} + +func TestReleaseContractCIAllowsMainToAdvanceAfterTagSeal(t *testing.T) { + r := newReleaseTestRepo(t) + r.seedBeta(t) + mustWriteFile(t, filepath.Join(r.root, "after-seal.txt"), []byte("main advanced\n"), 0o644) + r.commitAndPush(t, "advance main after beta seal") + mustRun(t, r.root, "git", "checkout", "--detach", "v1.0.1-beta.1") + + output, err := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "prerelease", + "--version", "v1.0.1-beta.1", + "--context", "ci", + "--remote", "origin", + ) + if err != nil { + t.Fatalf("sealed tag should remain valid after main advances: %v\noutput:\n%s", err, output) + } +} + +func TestReleasePrepareChangelogCreatesGuardedTemplate(t *testing.T) { + r := newReleaseTestRepo(t) + releaseCopyFile(t, r.lib, filepath.Join(r.root, "scripts", "release", "release-lib.sh"), 0o644) + releaseCopyFile(t, r.prepare, filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), 0o755) + r.commitAndPush(t, "install changelog preparation") + + cmd := exec.Command("sh", filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), "prerelease", "v1.0.1-beta.1") + cmd.Dir = r.root + cmd.Env = append(os.Environ(), "DWS_RELEASE_DATE=2026-07-11") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("prepare changelog error = %v\noutput:\n%s", err, output) + } + changelog, err := os.ReadFile(filepath.Join(r.root, "CHANGELOG.md")) + if err != nil { + t.Fatalf("ReadFile(CHANGELOG.md) error = %v", err) + } + if !strings.Contains(string(changelog), "## [1.0.1-beta.1] - 2026-07-11") || !strings.Contains(string(changelog), "TODO") { + t.Fatalf("prepared changelog missing guarded template:\n%s", changelog) + } + + r.commitAndPush(t, "commit unfinished release notes") + contractOutput, contractErr := runReleaseScript(t, r.root, r.contract, + "--repo-root", r.root, + "--channel", "prerelease", + "--version", "v1.0.1-beta.1", + "--remote", "origin", + ) + if contractErr == nil || !strings.Contains(contractOutput, "TODO/TBD") { + t.Fatalf("unfinished template was not blocked: err=%v\noutput:\n%s", contractErr, contractOutput) + } +} + +func TestReleaseMirrorUsesChannelSpecificPointer(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + script := filepath.Join(sourceRoot, "scripts", "release", "sync-to-oss.sh") + + for _, test := range []struct { + name string + version string + channel string + want string + doNotWant string + installer bool + }{ + {name: "prerelease", version: "v1.2.3-beta.1", channel: "prerelease", want: "/beta.txt", doNotWant: "/latest.txt", installer: false}, + {name: "stable", version: "v1.2.3", channel: "stable", want: "/latest.txt", doNotWant: "/beta.txt", installer: true}, + } { + t.Run(test.name, func(t *testing.T) { + root := t.TempDir() + dist := filepath.Join(root, "dist") + seedVersionedReleaseArtifacts(t, dist, test.version) + logPath := filepath.Join(root, "ossutil.log") + fakeOSSUtil := filepath.Join(root, "ossutil") + mustWriteFile(t, fakeOSSUtil, []byte("#!/bin/sh\nset -eu\nprevious=\npenultimate=\nfor arg in \"$@\"; do penultimate=\"$previous\"; previous=\"$arg\"; done\nlast=\"$previous\"\ncase \"$penultimate\" in oss://*) echo 'ErrorCode=NoSuchKey' >&2; exit 1 ;; esac\nprintf '%s\\n' \"$last\" >> \"$OSSUTIL_LOG\"\n"), 0o755) + + cmd := exec.Command("bash", script) + cmd.Dir = sourceRoot + cmd.Env = append(os.Environ(), + "DIST_DIR="+dist, + "VERSION="+test.version, + "DWS_RELEASE_CHANNEL="+test.channel, + "OSS_ACCESS_KEY_ID=test-key", + "OSS_ACCESS_KEY_SECRET=test-secret", + "OSS_ENDPOINT=https://oss.example.com", + "OSS_BUCKET=test-bucket", + "OSS_PREFIX=dws", + "OSSUTIL="+fakeOSSUtil, + "OSSUTIL_LOG="+logPath, + ) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("sync-to-oss error = %v\noutput:\n%s", err, output) + } + logData, err := os.ReadFile(logPath) + if err != nil { + t.Fatalf("ReadFile(ossutil log) error = %v", err) + } + if !strings.Contains(string(logData), test.want) { + t.Fatalf("mirror log missing %q:\n%s", test.want, logData) + } + if strings.Contains(string(logData), test.doNotWant) { + t.Fatalf("mirror log unexpectedly contains %q:\n%s", test.doNotWant, logData) + } + hasInstaller := strings.Contains(string(logData), "/dws/install.sh") + if hasInstaller != test.installer { + t.Fatalf("installer upload = %v, want %v:\n%s", hasInstaller, test.installer, logData) + } + }) + } +} + +func TestReleaseMirrorFailsClosedWhenPointerCannotBeRead(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + script := filepath.Join(sourceRoot, "scripts", "release", "sync-to-oss.sh") + + for _, test := range []struct { + name string + readAction string + want string + }{ + {name: "transport error", readAction: "echo 'connection timed out' >&2; exit 7", want: "Could not read OSS beta.txt"}, + {name: "empty pointer", readAction: ": > \"$last\"; exit 0", want: "read successfully but is empty"}, + } { + t.Run(test.name, func(t *testing.T) { + root := t.TempDir() + dist := filepath.Join(root, "dist") + seedVersionedReleaseArtifacts(t, dist, "v1.2.3-beta.1") + logPath := filepath.Join(root, "ossutil.log") + fakeOSSUtil := filepath.Join(root, "ossutil") + scriptText := "#!/bin/sh\nset -eu\nprevious=\npenultimate=\nfor arg in \"$@\"; do penultimate=\"$previous\"; previous=\"$arg\"; done\nlast=\"$previous\"\ncase \"$penultimate\" in oss://*) " + test.readAction + " ;; esac\nprintf '%s\\n' \"$last\" >> \"$OSSUTIL_LOG\"\n" + mustWriteFile(t, fakeOSSUtil, []byte(scriptText), 0o755) + + cmd := exec.Command("bash", script) + cmd.Dir = sourceRoot + cmd.Env = append(os.Environ(), + "DIST_DIR="+dist, + "VERSION=v1.2.3-beta.1", + "DWS_RELEASE_CHANNEL=prerelease", + "OSS_ACCESS_KEY_ID=test-key", + "OSS_ACCESS_KEY_SECRET=test-secret", + "OSS_ENDPOINT=https://oss.example.com", + "OSS_BUCKET=test-bucket", + "OSS_PREFIX=dws", + "OSSUTIL="+fakeOSSUtil, + "OSSUTIL_LOG="+logPath, + ) + output, err := cmd.CombinedOutput() + if err == nil || !strings.Contains(string(output), test.want) { + t.Fatalf("pointer failure was not closed: err=%v, want=%q\noutput:\n%s", err, test.want, output) + } + if logData, readErr := os.ReadFile(logPath); readErr == nil && strings.Contains(string(logData), "/beta.txt") { + t.Fatalf("failed pointer read still published beta pointer:\n%s", logData) + } + }) + } +} + +func TestReleaseMirrorRepairsHistoricalAssetsWithoutMovingNewerPointer(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + root := t.TempDir() + dist := filepath.Join(root, "dist") + seedVersionedReleaseArtifacts(t, dist, "v1.2.3-beta.1") + logPath := filepath.Join(root, "ossutil.log") + fakeOSSUtil := filepath.Join(root, "ossutil") + mustWriteFile(t, fakeOSSUtil, []byte("#!/bin/sh\nset -eu\nprevious=\npenultimate=\nfor arg in \"$@\"; do penultimate=\"$previous\"; previous=\"$arg\"; done\nlast=\"$previous\"\ncase \"$penultimate\" in oss://*) printf 'v1.2.4-beta.1\\n' > \"$last\"; exit 0 ;; esac\nprintf '%s\\n' \"$last\" >> \"$OSSUTIL_LOG\"\n"), 0o755) + cmd := exec.Command("bash", filepath.Join(sourceRoot, "scripts", "release", "sync-to-oss.sh")) + cmd.Dir = sourceRoot + cmd.Env = append(os.Environ(), + "DIST_DIR="+dist, + "VERSION=v1.2.3-beta.1", + "DWS_RELEASE_CHANNEL=prerelease", + "OSS_ACCESS_KEY_ID=test-key", + "OSS_ACCESS_KEY_SECRET=test-secret", + "OSS_ENDPOINT=https://oss.example.com", + "OSS_BUCKET=test-bucket", + "OSS_PREFIX=dws", + "OSSUTIL="+fakeOSSUtil, + "OSSUTIL_LOG="+logPath, + ) + output, err := cmd.CombinedOutput() + if err != nil || !strings.Contains(string(output), "assets will be repaired without moving it") { + t.Fatalf("historical OSS repair failed: err=%v\noutput:\n%s", err, output) + } + logData, err := os.ReadFile(logPath) + if err != nil { + t.Fatalf("ReadFile(ossutil log) error = %v", err) + } + if strings.Contains(string(logData), "/beta.txt") { + t.Fatalf("historical repair moved beta pointer:\n%s", logData) + } + if !strings.Contains(string(logData), "/download/v1.2.3-beta.1/") { + t.Fatalf("historical repair did not upload target assets:\n%s", logData) + } +} + +func TestReleaseRequiredMirrorsFailWithoutCredentials(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + for _, test := range []struct { + name string + script string + require string + want string + }{ + {name: "oss", script: "sync-to-oss.sh", require: "DWS_REQUIRE_OSS=1", want: "OSS mirror sync is required"}, + {name: "gitee", script: "sync-to-gitee.sh", require: "DWS_REQUIRE_GITEE=1", want: "Gitee mirror sync is enabled"}, + } { + t.Run(test.name, func(t *testing.T) { + cmd := exec.Command("bash", filepath.Join(sourceRoot, "scripts", "release", test.script)) + cmd.Dir = sourceRoot + cmd.Env = []string{"PATH=" + os.Getenv("PATH"), "HOME=" + t.TempDir(), test.require} + output, err := cmd.CombinedOutput() + if err == nil || !strings.Contains(string(output), test.want) { + t.Fatalf("required mirror did not fail closed: err=%v, want=%q\noutput:\n%s", err, test.want, output) + } + }) + } +} + +func TestReleaseArtifactVerificationRequiresEveryChecksum(t *testing.T) { + r := newReleaseTestRepo(t) + dist := t.TempDir() + seedVersionedReleaseArtifacts(t, dist, "v1.2.3") + 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 { + t.Fatalf("artifact verification error = %v\noutput:\n%s", err, output) + } + + writeReleaseChecksums(t, dist, false) + cmd = exec.Command("sh", r.verify, "v1.2.3") + cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+dist) + output, err := cmd.CombinedOutput() + if err == nil || !strings.Contains(string(output), "dws-skills.zip exactly once") { + t.Fatalf("missing checksum was not blocked: err=%v\noutput:\n%s", err, output) + } + + writeReleaseChecksums(t, dist, true) + mustWriteFile(t, filepath.Join(dist, "dws-linux-riscv64.tar.gz"), []byte("unexpected\n"), 0o644) + 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), "public release assets") { + t.Fatalf("extra public archive was not rejected: err=%v\noutput:\n%s", err, output) + } + if err := os.Remove(filepath.Join(dist, "dws-linux-riscv64.tar.gz")); err != nil { + t.Fatalf("Remove(extra archive) error = %v", err) + } + + writeVersionedReleaseArchive(t, dist, "dws-windows-arm64.zip", "v1.2.2") + 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") { + t.Fatalf("mixed-version archive was not rejected: err=%v\noutput:\n%s", err, output) + } +} + +func TestReleaseCommandValidatesThenPushesAnnotatedTag(t *testing.T) { + r := newReleaseTestRepo(t) + installReleaseCommandFixture(t, r) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + r.commitAndPush(t, "install release automation") + + output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"), + "prerelease", "v1.0.1-beta.1", "--remote", "origin", + ) + if err != nil { + t.Fatalf("release validation error = %v\noutput:\n%s", err, output) + } + if !strings.Contains(output, "No tag was created") { + t.Fatalf("validation output missing dry-run result:\n%s", output) + } + if mustOutput(t, r.root, "git", "tag", "--list", "v1.0.1-beta.1") != "" { + t.Fatal("validation-only release created a tag") + } + + output, err = runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"), + "prerelease", "v1.0.1-beta.1", "--remote", "origin", "--publish", "--yes", + ) + if err != nil { + t.Fatalf("release publish error = %v\noutput:\n%s", err, output) + } + if got := strings.TrimSpace(mustOutput(t, r.root, "git", "cat-file", "-t", "v1.0.1-beta.1")); got != "tag" { + t.Fatalf("release tag type = %q, want annotated tag", got) + } + if got := mustOutput(t, r.root, "git", "ls-remote", "--tags", "origin", "refs/tags/v1.0.1-beta.1"); !strings.Contains(got, "refs/tags/v1.0.1-beta.1") { + t.Fatalf("remote release tag missing:\n%s", got) + } +} + +func TestReleaseCommandCleansLocalTagWhenPushFails(t *testing.T) { + r := newReleaseTestRepo(t) + installReleaseCommandFixture(t, r) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + r.commitAndPush(t, "install release automation") + + hook := filepath.Join(r.remote, "hooks", "pre-receive") + mustWriteFile(t, hook, []byte("#!/bin/sh\nset -eu\nwhile read -r old new ref; do\n case \"$ref\" in refs/tags/*) exit 1 ;; esac\ndone\nexit 0\n"), 0o755) + + output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"), + "prerelease", "v1.0.1-beta.1", "--remote", "origin", "--publish", "--yes", + ) + if err == nil { + t.Fatalf("rejected tag push unexpectedly passed:\n%s", output) + } + if !strings.Contains(output, "new local tag was removed") { + t.Fatalf("push failure output missing cleanup result:\n%s", output) + } + if mustOutput(t, r.root, "git", "tag", "--list", "v1.0.1-beta.1") != "" { + t.Fatal("failed push left the local release tag behind") + } +} + +func TestReleaseCommandRejectsDifferentFetchAndPushRepositories(t *testing.T) { + r := newReleaseTestRepo(t) + installReleaseCommandFixture(t, r) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644) + r.commitAndPush(t, "install release automation") + otherRemote := filepath.Join(t.TempDir(), "other.git") + mustRun(t, r.root, "git", "init", "--bare", otherRemote) + mustRun(t, r.root, "git", "remote", "set-url", "--push", "origin", otherRemote) + + output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"), + "prerelease", "v1.0.1-beta.1", "--remote", "origin", + ) + if err == nil || !strings.Contains(output, "fetch and push URLs target different repositories") { + t.Fatalf("split release authority was not rejected: err=%v\noutput:\n%s", err, output) + } +} + +func TestReleaseCommandRechecksAdvancedStableAuthority(t *testing.T) { + r := newReleaseTestRepo(t) + installReleaseCommandFixture(t, r) + section := "## [1.0.2-beta.1] - 2026-07-11\n\n### Changed\n\n- Validate a candidate after stable authority advances.\n\n" + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(section)), 0o644) + mustWriteFile(t, filepath.Join(r.root, "scripts", "policy", "check-command-compatibility.sh"), []byte("#!/bin/sh\nset -eu\nprintf 'compatibility %s\\n' \"$*\"\n"), 0o755) + mustWriteFile(t, filepath.Join(r.root, "Makefile"), []byte("test:\n\t@:\npolicy:\n\t@:\npackage:\n\t@git tag -a v1.0.1 -m 'Release v1.0.1'\n\t@git push origin refs/tags/v1.0.1\n"), 0o644) + r.commitAndPush(t, "install advancing release fixture") + + output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"), + "prerelease", "v1.0.2-beta.1", "--remote", "origin", + ) + if err != nil { + t.Fatalf("release validation error = %v\noutput:\n%s", err, output) + } + if !strings.Contains(output, "Stable authority advanced from v1.0.0 to v1.0.1") || + !strings.Contains(output, "--stable-ref v1.0.1") { + t.Fatalf("advanced stable command tree was not rechecked:\n%s", output) + } +} + +func installReleaseCommandFixture(t *testing.T, r *releaseTestRepo) { + t.Helper() + for _, source := range []string{r.lib, r.contract, r.releaseCmd} { + releaseCopyFile(t, source, filepath.Join(r.root, "scripts", "release", filepath.Base(source)), 0o755) + } + mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "verify-package-managers.sh"), []byte("#!/bin/sh\nset -eu\nexit 0\n"), 0o755) + mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "verify-release-artifacts.sh"), []byte("#!/bin/sh\nset -eu\nexit 0\n"), 0o755) + mustWriteFile(t, filepath.Join(r.root, "scripts", "policy", "check-command-compatibility.sh"), []byte("#!/bin/sh\nset -eu\nexit 0\n"), 0o755) + mustWriteFile(t, filepath.Join(r.root, "Makefile"), []byte("test:\n\t@:\npolicy:\n\t@:\npackage:\n\t@:\n"), 0o644) +} + +func releaseCopyFile(t *testing.T, source, target string, mode os.FileMode) { + t.Helper() + data, err := os.ReadFile(source) + if err != nil { + t.Fatalf("ReadFile(%s) error = %v", source, err) + } + mustWriteFile(t, target, data, mode) +} From d288820b645928f34b1870f1ea6c93ccfbed0d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:27:00 +0800 Subject: [PATCH 03/14] fix(release): preserve unreleased changelog entries (#55) (cherry picked from commit e7989c1bb03ec461c638de89337d175f8ef115e4) --- scripts/release/prepare-changelog.sh | 25 +++++++++++++++---- test/scripts/release_script_test.go | 36 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/scripts/release/prepare-changelog.sh b/scripts/release/prepare-changelog.sh index 99f0fb91..0c718fa3 100755 --- a/scripts/release/prepare-changelog.sh +++ b/scripts/release/prepare-changelog.sh @@ -79,14 +79,29 @@ trap cleanup EXIT HUP INT TERM } > "$section" inserted=0 +in_unreleased=0 while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + '## [Unreleased]') + in_unreleased=1 + ;; + '## '*) + if [ "$in_unreleased" -eq 1 ]; then + cat "$section" >> "$output" + printf '\n' >> "$output" + inserted=1 + in_unreleased=0 + fi + ;; + esac printf '%s\n' "$line" >> "$output" - if [ "$line" = '## [Unreleased]' ]; then - printf '\n' >> "$output" - cat "$section" >> "$output" - inserted=1 - fi done < "$CHANGELOG" + +if [ "$in_unreleased" -eq 1 ]; then + printf '\n' >> "$output" + cat "$section" >> "$output" + inserted=1 +fi [ "$inserted" -eq 1 ] || { printf 'CHANGELOG is missing ## [Unreleased]\n' >&2; exit 1; } cp "$output" "$CHANGELOG" diff --git a/test/scripts/release_script_test.go b/test/scripts/release_script_test.go index 50773aff..ab424c80 100644 --- a/test/scripts/release_script_test.go +++ b/test/scripts/release_script_test.go @@ -593,6 +593,42 @@ func TestReleasePrepareChangelogCreatesGuardedTemplate(t *testing.T) { } } +func TestReleasePrepareChangelogKeepsUnreleasedContentAboveNewVersion(t *testing.T) { + r := newReleaseTestRepo(t) + releaseCopyFile(t, r.lib, filepath.Join(r.root, "scripts", "release", "release-lib.sh"), 0o644) + releaseCopyFile(t, r.prepare, filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), 0o755) + mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Changed\n\n- Keep this unreleased note.\n\n## [1.0.0] - 2026-07-01\n\n### Changed\n\n- Initial release.\n"), 0o644) + r.commitAndPush(t, "add unreleased changelog note") + + cmd := exec.Command("sh", filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), "prerelease", "v1.0.1-beta.1") + cmd.Dir = r.root + cmd.Env = append(os.Environ(), "DWS_RELEASE_DATE=2026-07-11") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("prepare changelog error = %v\noutput:\n%s", err, output) + } + data, err := os.ReadFile(filepath.Join(r.root, "CHANGELOG.md")) + if err != nil { + t.Fatalf("ReadFile(CHANGELOG.md) error = %v", err) + } + content := string(data) + positions := []int{ + strings.Index(content, "## [Unreleased]"), + strings.Index(content, "- Keep this unreleased note."), + strings.Index(content, "## [1.0.1-beta.1] - 2026-07-11"), + strings.Index(content, "## [1.0.0] - 2026-07-01"), + } + for index, position := range positions { + if position < 0 { + t.Fatalf("prepared changelog is missing expected marker %d:\n%s", index, content) + } + } + for index := 1; index < len(positions); index++ { + if positions[index-1] >= positions[index] { + t.Fatalf("prepared changelog order is invalid: %v\n%s", positions, content) + } + } +} + func TestReleaseMirrorUsesChannelSpecificPointer(t *testing.T) { sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) if err != nil { From 8f4ab176a816ff9d34fc7ae63c9c6dad10a0a8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:38:36 +0800 Subject: [PATCH 04/14] ci: add code admission gate (#53) * ci: add code admission gate * ci: fix fork release baseline (cherry picked from commit 7ff2f3a5f0435209908822e141a6355fc6fa4aa6) --- .github/workflows/ci.yml | 32 ++ CONTRIBUTING.md | 2 +- cmd/interface-snapshot/main.go | 195 ++++++++ cmd/interface-snapshot/main_test.go | 130 ++++++ internal/interfacesnapshot/compare.go | 441 ++++++++++++++++++ internal/interfacesnapshot/compare_test.go | 367 +++++++++++++++ internal/interfacesnapshot/snapshot.go | 270 +++++++++++ internal/interfacesnapshot/snapshot_test.go | 119 +++++ scripts/policy/check-command-compatibility.sh | 122 +++++ test/mock_mcp/mock_mcp_smoke_test.go | 219 +++++++++ test/smoke/cli_smoke_test.go | 212 +++++++++ 11 files changed, 2108 insertions(+), 1 deletion(-) create mode 100644 cmd/interface-snapshot/main.go create mode 100644 cmd/interface-snapshot/main_test.go create mode 100644 internal/interfacesnapshot/compare.go create mode 100644 internal/interfacesnapshot/compare_test.go create mode 100644 internal/interfacesnapshot/snapshot.go create mode 100644 internal/interfacesnapshot/snapshot_test.go create mode 100755 scripts/policy/check-command-compatibility.sh create mode 100644 test/mock_mcp/mock_mcp_smoke_test.go create mode 100644 test/smoke/cli_smoke_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f83fce6b..9a982cac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,38 @@ jobs: # version: v1.64.8 # args: ./... + code-check: + name: Code Check + needs: [lint, test, policy, edition-tests] + if: ${{ always() }} + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + steps: + - name: Verify code checks + env: + LINT_RESULT: ${{ needs.lint.result }} + TEST_RESULT: ${{ needs.test.result }} + POLICY_RESULT: ${{ needs.policy.result }} + EDITION_TESTS_RESULT: ${{ needs.edition-tests.result }} + run: | + set -eu + failed=0 + for check in \ + "Lint:$LINT_RESULT" \ + "Test:$TEST_RESULT" \ + "Policy Check:$POLICY_RESULT" \ + "Edition Contract Tests:$EDITION_TESTS_RESULT" + do + name="${check%%:*}" + result="${check#*:}" + printf '%s: %s\n' "$name" "$result" + if [ "$result" != "success" ]; then + failed=1 + fi + done + test "$failed" -eq 0 + test: name: Test runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7251571a..de7eff27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ git diff --check 1. Keep implementation and tests in sync. 2. Run `./scripts/dev/ci-local.sh`. -3. Run `./scripts/policy/check-command-surface.sh --strict` when command paths/flags change. +3. Run `./scripts/policy/check-command-surface.sh --strict` when command paths/flags change. CI also runs `./scripts/policy/check-command-compatibility.sh --base-ref --stable-ref ` against both the target branch and latest stable release. 4. Run `./scripts/policy/check-generated-drift.sh` when generated artifacts may change. 5. Run `./scripts/release/verify-package-managers.sh` when packaging or installer surfaces change (run `make package` first). 6. Update docs and `CHANGELOG.md` for behavior/interface changes. diff --git a/cmd/interface-snapshot/main.go b/cmd/interface-snapshot/main.go new file mode 100644 index 00000000..d921bff9 --- /dev/null +++ b/cmd/interface-snapshot/main.go @@ -0,0 +1,195 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// interface-snapshot is an internal CI helper. It is intentionally a separate +// binary so it can be copied into a temporary worktree and compiled against an +// older revision's real Cobra root. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/i18n" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot" +) + +func main() { + os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) +} + +func run(args []string, stdout, stderr io.Writer) int { + if len(args) == 0 { + printUsage(stderr) + return 2 + } + + switch args[0] { + case "generate": + if err := runGenerate(args[1:], stdout, stderr); err != nil { + fmt.Fprintln(stderr, err) + return 2 + } + return 0 + case "compare": + compatible, err := runCompare(args[1:], stdout, stderr) + if err != nil { + fmt.Fprintln(stderr, err) + return 2 + } + if !compatible { + return 1 + } + return 0 + default: + fmt.Fprintf(stderr, "unknown command %q\n", args[0]) + printUsage(stderr) + return 2 + } +} + +func runGenerate(args []string, stdout, stderr io.Writer) error { + flags := flag.NewFlagSet("generate", flag.ContinueOnError) + flags.SetOutput(stderr) + output := flags.String("output", "-", "snapshot output path, or - for stdout") + if err := flags.Parse(args); err != nil { + return err + } + if flags.NArg() != 0 { + return fmt.Errorf("generate accepts no positional arguments") + } + + home, err := os.MkdirTemp("", "dws-interface-snapshot-*") + if err != nil { + return fmt.Errorf("create isolated home: %w", err) + } + defer os.RemoveAll(home) + + environment := map[string]string{ + "DWS_CONFIG_DIR": home, + "DWS_LANG": "en", + "HOME": home, + "NO_COLOR": "1", + "USERPROFILE": home, + } + type previousEnv struct { + value string + set bool + } + previous := make(map[string]previousEnv, len(environment)) + for key, value := range environment { + oldValue, wasSet := os.LookupEnv(key) + previous[key] = previousEnv{value: oldValue, set: wasSet} + if err := os.Setenv(key, value); err != nil { + return fmt.Errorf("set %s: %w", key, err) + } + } + defer func() { + for key, old := range previous { + if old.set { + _ = os.Setenv(key, old.value) + } else { + _ = os.Unsetenv(key) + } + } + }() + previousLang := i18n.Lang() + defer i18n.SetLang(previousLang) + i18n.SetLang("en") + + snapshot := interfacesnapshot.Capture(app.NewRootCommand()) + if *output == "-" { + return interfacesnapshot.Write(stdout, snapshot) + } + + file, err := os.Create(filepath.Clean(*output)) + if err != nil { + return fmt.Errorf("create snapshot %q: %w", *output, err) + } + writeErr := interfacesnapshot.Write(file, snapshot) + closeErr := file.Close() + if writeErr != nil { + return fmt.Errorf("write snapshot %q: %w", *output, writeErr) + } + if closeErr != nil { + return fmt.Errorf("close snapshot %q: %w", *output, closeErr) + } + return nil +} + +func runCompare(args []string, stdout, stderr io.Writer) (bool, error) { + flags := flag.NewFlagSet("compare", flag.ContinueOnError) + flags.SetOutput(stderr) + currentPath := flags.String("current", "", "candidate snapshot path") + basePath := flags.String("base", "", "target main/development baseline snapshot path") + stablePath := flags.String("stable", "", "latest stable GA snapshot path") + if err := flags.Parse(args); err != nil { + return false, err + } + if flags.NArg() != 0 { + return false, fmt.Errorf("compare accepts no positional arguments") + } + if *currentPath == "" { + return false, fmt.Errorf("compare requires --current") + } + if *basePath == "" && *stablePath == "" { + return false, fmt.Errorf("compare requires --base, --stable, or both") + } + + current, err := readSnapshot(*currentPath) + if err != nil { + return false, fmt.Errorf("read current snapshot: %w", err) + } + references := make(map[string]interfacesnapshot.Snapshot, 2) + if *basePath != "" { + references["main"], err = readSnapshot(*basePath) + if err != nil { + return false, fmt.Errorf("read main/development baseline snapshot: %w", err) + } + } + if *stablePath != "" { + references["stable"], err = readSnapshot(*stablePath) + if err != nil { + return false, fmt.Errorf("read stable snapshot: %w", err) + } + } + + report := interfacesnapshot.CompareAll(current, references) + encoder := json.NewEncoder(stdout) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + if err := encoder.Encode(report); err != nil { + return false, fmt.Errorf("write comparison report: %w", err) + } + return report.Compatible, nil +} + +func readSnapshot(path string) (interfacesnapshot.Snapshot, error) { + file, err := os.Open(filepath.Clean(path)) + if err != nil { + return interfacesnapshot.Snapshot{}, err + } + defer file.Close() + return interfacesnapshot.Read(file) +} + +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]") +} diff --git a/cmd/interface-snapshot/main_test.go b/cmd/interface-snapshot/main_test.go new file mode 100644 index 00000000..8b791770 --- /dev/null +++ b/cmd/interface-snapshot/main_test.go @@ -0,0 +1,130 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot" +) + +func TestRunGenerateCapturesActualRootOffline(t *testing.T) { + var stdout, stderr bytes.Buffer + if exitCode := run([]string{"generate"}, &stdout, &stderr); exitCode != 0 { + t.Fatalf("run(generate) exit=%d stderr=%s", exitCode, stderr.String()) + } + snapshot, err := interfacesnapshot.Read(bytes.NewReader(stdout.Bytes())) + if err != nil { + t.Fatalf("decode generated snapshot: %v", err) + } + + commands := make(map[string]interfacesnapshot.Command, len(snapshot.Commands)) + for _, command := range snapshot.Commands { + commands[command.Path] = command + } + for _, path := range []string{"dws", "dws chat", "dws dev app create"} { + if _, ok := commands[path]; !ok { + t.Errorf("actual root snapshot is missing %q", path) + } + } + for _, path := range []string{"dws completion", "dws help"} { + if _, ok := commands[path]; ok { + t.Errorf("framework-noise path %q leaked into snapshot", path) + } + } + + create := commands["dws dev app create"] + if !hasFlag(create.LocalFlags, "name", "string") { + t.Errorf("dev app create local flags do not contain --name string: %#v", create.LocalFlags) + } + if !hasFlag(create.InheritedFlags, "profile", "string") { + t.Errorf("dev app create inherited flags do not contain --profile string: %#v", create.InheritedFlags) + } +} + +func TestRunCompareUsesBothSnapshotInputsAndExitCode(t *testing.T) { + current := commandSnapshot("dws") + mergeBase := commandSnapshot("dws") + stable := commandSnapshot("dws", "dws legacy") + + dir := t.TempDir() + currentPath := writeSnapshot(t, dir, "current.json", current) + mergeBasePath := writeSnapshot(t, dir, "base.json", mergeBase) + stablePath := writeSnapshot(t, dir, "stable.json", stable) + + var stdout, stderr bytes.Buffer + exitCode := run([]string{ + "compare", + "--current", currentPath, + "--base", mergeBasePath, + "--stable", stablePath, + }, &stdout, &stderr) + if exitCode != 1 { + t.Fatalf("run(compare) exit=%d, want 1; stdout=%s stderr=%s", exitCode, stdout.String(), stderr.String()) + } + if !bytes.Contains(stdout.Bytes(), []byte(`"reference": "main"`)) || + !bytes.Contains(stdout.Bytes(), []byte(`"reference": "stable"`)) || + !bytes.Contains(stdout.Bytes(), []byte(`"kind": "command_removed"`)) { + t.Fatalf("comparison report does not contain both references and the blocking change:\n%s", stdout.String()) + } +} + +func commandSnapshot(paths ...string) interfacesnapshot.Snapshot { + commands := make([]interfacesnapshot.Command, 0, len(paths)) + for _, path := range paths { + commands = append(commands, interfacesnapshot.Command{ + Path: path, + Aliases: []string{}, + LocalFlags: []interfacesnapshot.Flag{}, + InheritedFlags: []interfacesnapshot.Flag{}, + }) + } + return interfacesnapshot.Snapshot{ + SchemaVersion: interfacesnapshot.SchemaVersion, + Rules: interfacesnapshot.Rules{ + ExcludedCommandSubtrees: []string{}, + ExcludedFlags: []string{}, + }, + Commands: commands, + } +} + +func writeSnapshot(t *testing.T, dir, name string, snapshot interfacesnapshot.Snapshot) string { + t.Helper() + path := filepath.Join(dir, name) + file, err := os.Create(path) + if err != nil { + t.Fatalf("create %s: %v", path, err) + } + if err := interfacesnapshot.Write(file, snapshot); err != nil { + file.Close() + t.Fatalf("write %s: %v", path, err) + } + if err := file.Close(); err != nil { + t.Fatalf("close %s: %v", path, err) + } + return path +} + +func hasFlag(flags []interfacesnapshot.Flag, name, flagType string) bool { + for _, flag := range flags { + if flag.Name == name && flag.Type == flagType { + return true + } + } + return false +} diff --git a/internal/interfacesnapshot/compare.go b/internal/interfacesnapshot/compare.go new file mode 100644 index 00000000..79fcd2d3 --- /dev/null +++ b/internal/interfacesnapshot/compare.go @@ -0,0 +1,441 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package interfacesnapshot + +import ( + "fmt" + "reflect" + "sort" + "strings" +) + +// Report combines comparisons against independently supplied compatibility +// references, normally the PR merge-base and the latest stable GA release. +type Report struct { + Compatible bool `json:"compatible"` + Comparisons []Comparison `json:"comparisons"` +} + +// Comparison is the result for one reference snapshot. +type Comparison struct { + Reference string `json:"reference"` + Compatible bool `json:"compatible"` + Blocking []Change `json:"blocking"` + Additions []Change `json:"additions"` +} + +// Change describes one compatibility decision. Before and After are concise, +// human-readable values intended for CI annotations. +type Change struct { + Kind string `json:"kind"` + Path string `json:"path"` + Flag string `json:"flag,omitempty"` + Before string `json:"before,omitempty"` + After string `json:"after,omitempty"` +} + +// CompareAll compares current against every named reference in one pass. +// Additions are reported but remain compatible. +func CompareAll(current Snapshot, references map[string]Snapshot) Report { + labels := make([]string, 0, len(references)) + for label := range references { + labels = append(labels, label) + } + sort.Strings(labels) + + report := Report{Compatible: true, Comparisons: []Comparison{}} + for _, label := range labels { + comparison := Compare(current, references[label], label) + report.Comparisons = append(report.Comparisons, comparison) + if !comparison.Compatible { + report.Compatible = false + } + } + return report +} + +// Compare enforces the deliberately small admission policy: +// - every previously accepted command path must still resolve to a runnable, +// visible-compatible target (a rename may preserve the old path as an +// alias), +// - flags accepted at each command path may not disappear, change type, or +// become required; an existing path may not gain a new required flag, +// - new commands and flags are allowed. +// +// Comparing the effective local + inherited set is intentional. It catches a +// persistent flag whose scope is accidentally narrowed to its declaring +// command, while allowing a local flag to move to an ancestor without breaking +// the old invocation path. +func Compare(current, baseline Snapshot, reference string) Comparison { + result := Comparison{ + Reference: reference, + Compatible: true, + Blocking: []Change{}, + Additions: []Change{}, + } + + if !reflect.DeepEqual(current.Rules, baseline.Rules) { + result.Blocking = append(result.Blocking, Change{ + Kind: "snapshot_rules_changed", + Path: "dws", + Before: fmt.Sprintf("%v", baseline.Rules), + After: fmt.Sprintf("%v", current.Rules), + }) + } + result.Blocking = append(result.Blocking, aliasCollisionChanges(current)...) + + currentCommands := commandIndex(current) + baselineCommands := commandIndex(baseline) + currentAccepted := acceptedPathIndex(current) + baselineAccepted := acceptedPathIndex(baseline) + + removedPaths := make([]string, 0) + for path := range baselineAccepted { + if _, ok := currentAccepted[path]; !ok { + removedPaths = append(removedPaths, path) + } + } + for _, path := range minimalPaths(removedPaths) { + kind := "command_alias_removed" + if _, canonical := baselineCommands[path]; canonical { + kind = "command_removed" + } + result.Blocking = append(result.Blocking, Change{Kind: kind, Path: path}) + } + + mappedCurrent := make(map[string]bool) + for _, acceptedPath := range sortedAcceptedPaths(baselineAccepted) { + oldPath := baselineAccepted[acceptedPath] + oldCommand := baselineCommands[oldPath] + newPath, ok := currentAccepted[acceptedPath] + if !ok { + continue + } + newCommand, ok := currentCommands[newPath] + if !ok { + continue + } + mappedCurrent[newPath] = true + compareCommandContract(&result, acceptedPath, oldCommand, newCommand) + compareEffectiveFlags(&result, acceptedPath, oldCommand, newCommand) + } + + for _, path := range sortedCommandPaths(current.Commands) { + if mappedCurrent[path] { + continue + } + if _, existed := baselineCommands[path]; !existed { + result.Additions = append(result.Additions, Change{Kind: "command_added", Path: path}) + } + } + + sortChanges(result.Blocking) + sortChanges(result.Additions) + result.Blocking = dedupeChanges(result.Blocking) + result.Additions = dedupeChanges(result.Additions) + result.Compatible = len(result.Blocking) == 0 + return result +} + +func compareCommandContract(result *Comparison, acceptedPath string, oldCommand, newCommand Command) { + if oldCommand.Runnable && !newCommand.Runnable { + result.Blocking = append(result.Blocking, Change{ + Kind: "command_became_non_runnable", + Path: acceptedPath, + Before: "runnable", + After: "non-runnable", + }) + } + if !oldCommand.Hidden && newCommand.Hidden { + result.Blocking = append(result.Blocking, Change{ + Kind: "command_became_hidden", + Path: acceptedPath, + Before: "visible", + After: "hidden", + }) + } +} + +func compareEffectiveFlags(result *Comparison, acceptedPath string, oldCommand, newCommand Command) { + oldFlags := effectiveFlagIndex(oldCommand) + newFlags := effectiveFlagIndex(newCommand) + + for _, name := range sortedFlagNames(oldFlags) { + oldFlag := oldFlags[name] + newFlag, ok := newFlags[name] + if !ok { + result.Blocking = append(result.Blocking, Change{ + Kind: "flag_removed", + Path: acceptedPath, + Flag: name, + }) + continue + } + if oldFlag.Type != newFlag.Type { + result.Blocking = append(result.Blocking, Change{ + Kind: "flag_type_changed", + Path: acceptedPath, + Flag: name, + Before: oldFlag.Type, + After: newFlag.Type, + }) + } + if !oldFlag.Required && newFlag.Required { + result.Blocking = append(result.Blocking, Change{ + Kind: "flag_became_required", + Path: acceptedPath, + Flag: name, + Before: "optional", + After: "required", + }) + } + if oldFlag.Shorthand != "" && newFlag.Shorthand != oldFlag.Shorthand { + result.Blocking = append(result.Blocking, Change{ + Kind: "flag_shorthand_changed", + Path: acceptedPath, + Flag: name, + Before: oldFlag.Shorthand, + After: newFlag.Shorthand, + }) + } + if oldFlag.NoOpt != "" && newFlag.NoOpt != oldFlag.NoOpt { + result.Blocking = append(result.Blocking, Change{ + Kind: "flag_no_opt_changed", + Path: acceptedPath, + Flag: name, + Before: oldFlag.NoOpt, + After: newFlag.NoOpt, + }) + } + if !oldFlag.Hidden && newFlag.Hidden { + result.Blocking = append(result.Blocking, Change{ + Kind: "flag_became_hidden", + Path: acceptedPath, + Flag: name, + Before: "visible", + After: "hidden", + }) + } + } + + for _, name := range sortedFlagNames(newFlags) { + newFlag := newFlags[name] + if _, existed := oldFlags[name]; existed { + continue + } + if newFlag.Required { + result.Blocking = append(result.Blocking, Change{ + Kind: "required_flag_added", + Path: acceptedPath, + Flag: name, + Before: "absent", + After: "required", + }) + } + } + + // Report optional additions once where they are declared. Required + // additions were handled against the effective set above because they can + // break every descendant invocation. + newLocalFlags := flagIndex(newCommand.LocalFlags) + for _, name := range sortedFlagNames(newLocalFlags) { + newFlag := newLocalFlags[name] + if _, existed := oldFlags[name]; existed || newFlag.Required { + continue + } + result.Additions = append(result.Additions, Change{Kind: "flag_added", Path: newCommand.Path, Flag: name}) + } +} + +func commandIndex(snapshot Snapshot) map[string]Command { + out := make(map[string]Command, len(snapshot.Commands)) + for _, command := range snapshot.Commands { + out[command.Path] = command + } + return out +} + +func aliasCollisionChanges(snapshot Snapshot) []Change { + acceptedSiblings := make(map[string]string) + changes := []Change{} + for _, command := range snapshot.Commands { + parent, name := splitParent(command.Path) + for _, acceptedName := range append([]string{name}, command.Aliases...) { + acceptedName = strings.TrimSpace(acceptedName) + if acceptedName == "" { + continue + } + key := parent + "\x00" + acceptedName + if previous, exists := acceptedSiblings[key]; exists && previous != command.Path { + changes = append(changes, Change{ + Kind: "command_alias_collision", + Path: strings.TrimSpace(parent + " " + acceptedName), + Before: previous, + After: command.Path, + }) + continue + } + acceptedSiblings[key] = command.Path + } + } + return changes +} + +// acceptedPathIndex expands aliases at every path segment. For example, if +// "dws chat" has alias "im", then "dws im message send" remains accepted for +// every descendant even though descendants only store their canonical paths. +func acceptedPathIndex(snapshot Snapshot) map[string]string { + commands := append([]Command(nil), snapshot.Commands...) + sort.Slice(commands, func(i, j int) bool { + leftDepth, rightDepth := pathDepth(commands[i].Path), pathDepth(commands[j].Path) + if leftDepth != rightDepth { + return leftDepth < rightDepth + } + return commands[i].Path < commands[j].Path + }) + + variants := make(map[string][]string, len(commands)) + accepted := make(map[string]string) + // Canonical paths always win over an alias collision. + for _, command := range commands { + accepted[command.Path] = command.Path + } + + for _, command := range commands { + parent, name := splitParent(command.Path) + names := compactSorted(append([]string{name}, command.Aliases...)) + parentVariants := variants[parent] + if parent == "" { + parentVariants = []string{""} + } else if len(parentVariants) == 0 { + parentVariants = []string{parent} + } + + commandVariants := make([]string, 0, len(parentVariants)*len(names)) + for _, parentVariant := range parentVariants { + for _, candidateName := range names { + path := strings.TrimSpace(parentVariant + " " + candidateName) + commandVariants = append(commandVariants, path) + if _, canonical := accepted[path]; !canonical { + accepted[path] = command.Path + } + } + } + variants[command.Path] = compactSorted(commandVariants) + } + return accepted +} + +func flagIndex(flags []Flag) map[string]Flag { + out := make(map[string]Flag, len(flags)) + for _, flag := range flags { + out[flag.Name] = flag + } + return out +} + +func effectiveFlagIndex(command Command) map[string]Flag { + out := flagIndex(command.InheritedFlags) + for _, flag := range command.LocalFlags { + // A local flag is the callable definition when it shadows an inherited + // flag with the same name. + out[flag.Name] = flag + } + return out +} + +func sortedCommandPaths(commands []Command) []string { + paths := make([]string, 0, len(commands)) + for _, command := range commands { + paths = append(paths, command.Path) + } + sort.Strings(paths) + return paths +} + +func sortedAcceptedPaths(accepted map[string]string) []string { + paths := make([]string, 0, len(accepted)) + for path := range accepted { + paths = append(paths, path) + } + sort.Strings(paths) + return paths +} + +func sortedFlagNames(flags map[string]Flag) []string { + names := make([]string, 0, len(flags)) + for name := range flags { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func minimalPaths(paths []string) []string { + sort.Slice(paths, func(i, j int) bool { + leftDepth, rightDepth := pathDepth(paths[i]), pathDepth(paths[j]) + if leftDepth != rightDepth { + return leftDepth < rightDepth + } + return paths[i] < paths[j] + }) + minimal := make([]string, 0, len(paths)) + for _, path := range paths { + covered := false + for _, parent := range minimal { + if strings.HasPrefix(path, parent+" ") { + covered = true + break + } + } + if !covered { + minimal = append(minimal, path) + } + } + return minimal +} + +func splitParent(path string) (string, string) { + index := strings.LastIndexByte(path, ' ') + if index < 0 { + return "", path + } + return path[:index], path[index+1:] +} + +func pathDepth(path string) int { + return len(strings.Fields(path)) +} + +func sortChanges(changes []Change) { + sort.Slice(changes, func(i, j int) bool { + left := changes[i].Path + "\x00" + changes[i].Flag + "\x00" + changes[i].Kind + right := changes[j].Path + "\x00" + changes[j].Flag + "\x00" + changes[j].Kind + return left < right + }) +} + +func dedupeChanges(changes []Change) []Change { + if len(changes) < 2 { + return changes + } + out := changes[:1] + for _, change := range changes[1:] { + if change == out[len(out)-1] { + continue + } + out = append(out, change) + } + return out +} diff --git a/internal/interfacesnapshot/compare_test.go b/internal/interfacesnapshot/compare_test.go new file mode 100644 index 00000000..f3ef67c9 --- /dev/null +++ b/internal/interfacesnapshot/compare_test.go @@ -0,0 +1,367 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package interfacesnapshot + +import "testing" + +func TestCompareAdmissionPolicy(t *testing.T) { + base := testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "string", false)), + ) + + tests := []struct { + name string + current Snapshot + compatible bool + kind string + }{ + { + name: "command addition allowed", + current: testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "string", false)), + testCommand("dws status"), + ), + compatible: true, + }, + { + name: "flag addition allowed", + current: testSnapshot( + testCommand("dws"), + testCommand("dws search", + testFlag("limit", "int", false), + testFlag("query", "string", false), + ), + ), + compatible: true, + }, + { + name: "required flag addition blocked on existing command", + current: testSnapshot( + testCommand("dws"), + testCommand("dws search", + testFlag("query", "string", false), + testFlag("tenant", "string", true), + ), + ), + kind: "required_flag_added", + }, + { + name: "command deletion blocked", + current: testSnapshot( + testCommand("dws"), + ), + kind: "command_removed", + }, + { + name: "rename without alias blocked", + current: testSnapshot( + testCommand("dws"), + testCommand("dws find", testFlag("query", "string", false)), + ), + kind: "command_removed", + }, + { + name: "flag deletion blocked", + current: testSnapshot( + testCommand("dws"), + testCommand("dws search"), + ), + kind: "flag_removed", + }, + { + name: "optional becoming required blocked", + current: testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "string", true)), + ), + kind: "flag_became_required", + }, + { + name: "flag type change blocked", + current: testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "stringSlice", false)), + ), + kind: "flag_type_changed", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + comparison := Compare(test.current, base, "base") + if comparison.Compatible != test.compatible { + t.Fatalf("Compatible = %v, want %v; blocking=%#v", comparison.Compatible, test.compatible, comparison.Blocking) + } + if test.kind == "" { + return + } + if !hasChangeKind(comparison.Blocking, test.kind) { + t.Fatalf("blocking=%#v, want kind %q", comparison.Blocking, test.kind) + } + }) + } +} + +func TestCompareAllowsRenameWhenOldPathIsAlias(t *testing.T) { + base := testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "string", false)), + ) + current := testSnapshot( + testCommand("dws"), + testCommandWithAliases("dws find", []string{"search"}, testFlag("query", "string", false)), + ) + + comparison := Compare(current, base, "base") + if !comparison.Compatible { + t.Fatalf("rename with compatibility alias was blocked: %#v", comparison.Blocking) + } +} + +func TestCompareBlocksRemovedAlias(t *testing.T) { + base := testSnapshot( + testCommand("dws"), + testCommandWithAliases("dws search", []string{"find"}), + ) + current := testSnapshot( + testCommand("dws"), + testCommand("dws search"), + ) + + comparison := Compare(current, base, "base") + if comparison.Compatible || !hasChangeKind(comparison.Blocking, "command_alias_removed") { + t.Fatalf("removed alias was not blocked: %#v", comparison) + } +} + +func TestCompareBlocksAliasRetargetedToIncompatibleCommand(t *testing.T) { + base := testSnapshot( + testCommand("dws"), + testCommandWithAliases("dws search", []string{"find"}, testFlag("query", "string", false)), + ) + current := testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "string", false)), + testCommand("dws find"), + ) + + comparison := Compare(current, base, "base") + if comparison.Compatible || !hasFlagChange(comparison.Blocking, "flag_removed", "dws find", "query") { + t.Fatalf("alias retarget was not checked against its old contract: %#v", comparison) + } +} + +func TestCompareBlocksSnapshotRuleChanges(t *testing.T) { + base := testSnapshot(testCommand("dws")) + current := testSnapshot(testCommand("dws")) + current.Rules.ExcludedFlags = append(current.Rules.ExcludedFlags, "legacy") + + comparison := Compare(current, base, "base") + if comparison.Compatible || !hasChangeKind(comparison.Blocking, "snapshot_rules_changed") { + t.Fatalf("snapshot rule change was not blocked: %#v", comparison) + } +} + +func TestCompareBlocksCallableMetadataRegressions(t *testing.T) { + baseFlag := testFlag("format", "string", false) + baseFlag.Shorthand = "f" + baseFlag.NoOpt = "json" + base := testSnapshot( + testCommand("dws"), + testCommand("dws export", baseFlag), + ) + + tests := []struct { + name string + command Command + kind string + }{ + { + name: "command became non-runnable", + command: func() Command { + command := testCommand("dws export", baseFlag) + command.Runnable = false + return command + }(), + kind: "command_became_non_runnable", + }, + { + name: "command became hidden", + command: func() Command { + command := testCommand("dws export", baseFlag) + command.Hidden = true + return command + }(), + kind: "command_became_hidden", + }, + { + name: "flag shorthand removed", + command: func() Command { + flag := baseFlag + flag.Shorthand = "" + return testCommand("dws export", flag) + }(), + kind: "flag_shorthand_changed", + }, + { + name: "flag no-opt behavior removed", + command: func() Command { + flag := baseFlag + flag.NoOpt = "" + return testCommand("dws export", flag) + }(), + kind: "flag_no_opt_changed", + }, + { + name: "flag became hidden", + command: func() Command { + flag := baseFlag + flag.Hidden = true + return testCommand("dws export", flag) + }(), + kind: "flag_became_hidden", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + current := testSnapshot(testCommand("dws"), test.command) + comparison := Compare(current, base, "base") + if comparison.Compatible || !hasChangeKind(comparison.Blocking, test.kind) { + t.Fatalf("metadata regression %q was not blocked: %#v", test.kind, comparison) + } + }) + } +} + +func TestCompareUsesEffectiveFlagsAtEveryCommandPath(t *testing.T) { + base := testSnapshot( + testCommand("dws", testFlag("profile", "string", false)), + testCommandWithFlagScopes( + "dws search", + []Flag{testFlag("query", "string", false)}, + []Flag{testFlag("profile", "string", false)}, + ), + ) + + t.Run("persistent flag scope narrowing is blocked", func(t *testing.T) { + current := testSnapshot( + testCommand("dws", testFlag("profile", "string", false)), + testCommand("dws search", testFlag("query", "string", false)), + ) + comparison := Compare(current, base, "base") + if comparison.Compatible || !hasFlagChange(comparison.Blocking, "flag_removed", "dws search", "profile") { + t.Fatalf("lost inherited flag was not blocked: %#v", comparison) + } + }) + + t.Run("local flag moved to inherited remains compatible", func(t *testing.T) { + current := testSnapshot( + testCommand("dws", + testFlag("profile", "string", false), + testFlag("query", "string", false), + ), + testCommandWithFlagScopes( + "dws search", + nil, + []Flag{ + testFlag("profile", "string", false), + testFlag("query", "string", false), + }, + ), + ) + comparison := Compare(current, base, "base") + if !comparison.Compatible { + t.Fatalf("flag moved to an inherited scope was blocked: %#v", comparison.Blocking) + } + }) +} + +func TestCompareAllRequiresBothReferencesToPass(t *testing.T) { + current := testSnapshot(testCommand("dws"), testCommand("dws status")) + mergeBase := testSnapshot(testCommand("dws")) + stable := testSnapshot(testCommand("dws"), testCommand("dws legacy")) + + report := CompareAll(current, map[string]Snapshot{ + "stable": stable, + "merge-base": mergeBase, + }) + if report.Compatible { + t.Fatal("aggregate report passed even though stable comparison removed a command") + } + if len(report.Comparisons) != 2 || report.Comparisons[0].Reference != "merge-base" || report.Comparisons[1].Reference != "stable" { + t.Fatalf("comparisons are not deterministic: %#v", report.Comparisons) + } + if !report.Comparisons[0].Compatible || report.Comparisons[1].Compatible { + t.Fatalf("unexpected per-reference results: %#v", report.Comparisons) + } +} + +func testSnapshot(commands ...Command) Snapshot { + return Snapshot{ + SchemaVersion: SchemaVersion, + Rules: Rules{ + ExcludedCommandSubtrees: append([]string(nil), excludedCommandSubtrees...), + ExcludedFlags: []string{"help"}, + }, + Commands: commands, + } +} + +func testCommand(path string, flags ...Flag) Command { + return testCommandWithAliases(path, nil, flags...) +} + +func testCommandWithAliases(path string, aliases []string, flags ...Flag) Command { + return Command{ + Path: path, + Runnable: true, + Aliases: aliases, + LocalFlags: flags, + InheritedFlags: []Flag{}, + } +} + +func testCommandWithFlagScopes(path string, local, inherited []Flag) Command { + return Command{ + Path: path, + Aliases: []string{}, + LocalFlags: local, + InheritedFlags: inherited, + } +} + +func testFlag(name, flagType string, required bool) Flag { + return Flag{Name: name, Type: flagType, Required: required} +} + +func hasChangeKind(changes []Change, kind string) bool { + for _, change := range changes { + if change.Kind == kind { + return true + } + } + return false +} + +func hasFlagChange(changes []Change, kind, path, flag string) bool { + for _, change := range changes { + if change.Kind == kind && change.Path == path && change.Flag == flag { + return true + } + } + return false +} diff --git a/internal/interfacesnapshot/snapshot.go b/internal/interfacesnapshot/snapshot.go new file mode 100644 index 00000000..58b26c54 --- /dev/null +++ b/internal/interfacesnapshot/snapshot.go @@ -0,0 +1,270 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package interfacesnapshot captures and compares the public Cobra command +// surface without executing commands or contacting runtime services. +package interfacesnapshot + +import ( + "encoding/json" + "fmt" + "io" + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const SchemaVersion = 1 + +var ( + // Framework-owned command subtrees are deliberately excluded. They are + // generated by Cobra or shell-completion plumbing rather than DWS product + // code, so changes to them are not command-surface compatibility changes. + excludedCommandSubtrees = []string{ + "dws __complete", + "dws __completeNoDesc", + "dws completion", + "dws help", + } + + // Cobra installs --help lazily on every command. Keeping it would make the + // snapshot depend on whether help happened to be rendered before capture. + excludedFlags = map[string]bool{"help": true} +) + +// Rules records the noise filtering contract in every snapshot. A future rule +// change is therefore visible instead of silently changing comparison scope. +type Rules struct { + ExcludedCommandSubtrees []string `json:"excluded_command_subtrees"` + ExcludedFlags []string `json:"excluded_flags"` +} + +// Snapshot is a deterministic representation of a Cobra command tree. +type Snapshot struct { + SchemaVersion int `json:"schema_version"` + Rules Rules `json:"rules"` + Commands []Command `json:"commands"` +} + +// Command contains compatibility-relevant command metadata. Path always +// includes the root command name (for example, "dws chat message send"). +type Command struct { + Path string `json:"path"` + Runnable bool `json:"runnable"` + Hidden bool `json:"hidden"` + Deprecated string `json:"deprecated,omitempty"` + Aliases []string `json:"aliases"` + LocalFlags []Flag `json:"local_flags"` + InheritedFlags []Flag `json:"inherited_flags"` +} + +// Flag contains the stable pflag contract visible at a command node. +type Flag struct { + Name string `json:"name"` + Shorthand string `json:"shorthand,omitempty"` + Type string `json:"type"` + Default string `json:"default"` + NoOpt string `json:"no_opt,omitempty"` + Required bool `json:"required"` + Hidden bool `json:"hidden"` + Deprecated string `json:"deprecated,omitempty"` +} + +// Capture walks root without rendering help or executing any command. Both +// hidden compatibility commands and hidden flags are retained unless they are +// covered by the explicit framework-noise rules above. +func Capture(root *cobra.Command) Snapshot { + snapshot := Snapshot{ + SchemaVersion: SchemaVersion, + Rules: Rules{ + ExcludedCommandSubtrees: append([]string(nil), excludedCommandSubtrees...), + ExcludedFlags: []string{"help"}, + }, + Commands: []Command{}, + } + if root == nil { + return snapshot + } + + // --version is also installed lazily by Cobra, but unlike --help it is a + // real root CLI contract and should be captured. + root.InitDefaultVersionFlag() + + var walk func(*cobra.Command) + walk = func(cmd *cobra.Command) { + path := normalizePath(cmd.CommandPath()) + if commandExcluded(path) { + return + } + + aliases := append([]string{}, cmd.Aliases...) + for i := range aliases { + aliases[i] = strings.TrimSpace(aliases[i]) + } + aliases = compactSorted(aliases) + + snapshot.Commands = append(snapshot.Commands, Command{ + Path: path, + Runnable: cmd.Runnable(), + Hidden: cmd.Hidden, + Deprecated: strings.TrimSpace(cmd.Deprecated), + Aliases: aliases, + LocalFlags: captureFlags(cmd.LocalFlags()), + InheritedFlags: captureFlags(cmd.InheritedFlags()), + }) + + children := append([]*cobra.Command(nil), cmd.Commands()...) + sort.Slice(children, func(i, j int) bool { + return children[i].Name() < children[j].Name() + }) + for _, child := range children { + walk(child) + } + } + walk(root) + + sort.Slice(snapshot.Commands, func(i, j int) bool { + return snapshot.Commands[i].Path < snapshot.Commands[j].Path + }) + return snapshot +} + +// Write emits canonical, indented JSON with a trailing newline. +func Write(w io.Writer, snapshot Snapshot) error { + if err := snapshot.Validate(); err != nil { + return err + } + encoder := json.NewEncoder(w) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + return encoder.Encode(snapshot) +} + +// Read decodes and validates a snapshot. Unknown fields are rejected so a +// comparison never silently ignores a newer contract it does not understand. +func Read(r io.Reader) (Snapshot, error) { + var snapshot Snapshot + decoder := json.NewDecoder(r) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&snapshot); err != nil { + return Snapshot{}, err + } + if err := decoder.Decode(&struct{}{}); err != io.EOF { + if err == nil { + return Snapshot{}, fmt.Errorf("interface snapshot contains multiple JSON values") + } + return Snapshot{}, fmt.Errorf("read trailing interface snapshot data: %w", err) + } + if err := snapshot.Validate(); err != nil { + return Snapshot{}, err + } + return snapshot, nil +} + +// Validate checks the invariants needed by the comparison algorithm. +func (s Snapshot) Validate() error { + if s.SchemaVersion != SchemaVersion { + return fmt.Errorf("unsupported interface snapshot schema_version %d (want %d)", s.SchemaVersion, SchemaVersion) + } + seenCommands := make(map[string]bool, len(s.Commands)) + for _, command := range s.Commands { + if command.Path == "" { + return fmt.Errorf("interface snapshot contains an empty command path") + } + if seenCommands[command.Path] { + return fmt.Errorf("interface snapshot contains duplicate command path %q", command.Path) + } + seenCommands[command.Path] = true + if err := validateFlags(command.Path, "local", command.LocalFlags); err != nil { + return err + } + if err := validateFlags(command.Path, "inherited", command.InheritedFlags); err != nil { + return err + } + } + return nil +} + +func captureFlags(set *pflag.FlagSet) []Flag { + flags := []Flag{} + if set == nil { + return flags + } + set.VisitAll(func(flag *pflag.Flag) { + if flag == nil || excludedFlags[flag.Name] { + return + } + flags = append(flags, Flag{ + Name: flag.Name, + Shorthand: flag.Shorthand, + Type: flag.Value.Type(), + Default: flag.DefValue, + NoOpt: flag.NoOptDefVal, + Required: isRequired(flag), + Hidden: flag.Hidden, + Deprecated: strings.TrimSpace(flag.Deprecated), + }) + }) + sort.Slice(flags, func(i, j int) bool { return flags[i].Name < flags[j].Name }) + return flags +} + +func isRequired(flag *pflag.Flag) bool { + for _, value := range flag.Annotations[cobra.BashCompOneRequiredFlag] { + if value == "true" { + return true + } + } + return false +} + +func commandExcluded(path string) bool { + for _, prefix := range excludedCommandSubtrees { + if path == prefix || strings.HasPrefix(path, prefix+" ") { + return true + } + } + return false +} + +func normalizePath(path string) string { + return strings.Join(strings.Fields(path), " ") +} + +func compactSorted(values []string) []string { + sort.Strings(values) + out := values[:0] + for _, value := range values { + if value == "" || (len(out) > 0 && out[len(out)-1] == value) { + continue + } + out = append(out, value) + } + return out +} + +func validateFlags(path, scope string, flags []Flag) error { + seen := make(map[string]bool, len(flags)) + for _, flag := range flags { + if flag.Name == "" { + return fmt.Errorf("command %q contains an empty %s flag name", path, scope) + } + if seen[flag.Name] { + return fmt.Errorf("command %q contains duplicate %s flag %q", path, scope, flag.Name) + } + seen[flag.Name] = true + } + return nil +} diff --git a/internal/interfacesnapshot/snapshot_test.go b/internal/interfacesnapshot/snapshot_test.go new file mode 100644 index 00000000..fdcfa411 --- /dev/null +++ b/internal/interfacesnapshot/snapshot_test.go @@ -0,0 +1,119 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package interfacesnapshot + +import ( + "bytes" + "reflect" + "testing" + + "github.com/spf13/cobra" +) + +func TestCaptureUsesStableNoiseRulesAndFlagScopes(t *testing.T) { + root := &cobra.Command{Use: "dws", Version: "test"} + root.PersistentFlags().String("profile", "", "profile") + + service := &cobra.Command{ + Use: "service", + Aliases: []string{"svc", "api", "svc"}, + Hidden: true, + Deprecated: "use replacement", + } + leaf := &cobra.Command{Use: "run", Run: func(*cobra.Command, []string) {}} + leaf.Flags().String("name", "", "name") + leaf.Flags().Bool("force", false, "force") + if err := leaf.MarkFlagRequired("name"); err != nil { + t.Fatalf("MarkFlagRequired: %v", err) + } + leaf.InitDefaultHelpFlag() + service.AddCommand(leaf) + + help := &cobra.Command{Use: "help"} + completion := &cobra.Command{Use: "completion"} + completion.AddCommand(&cobra.Command{Use: "zsh"}) + root.AddCommand(service, help, completion) + + snapshot := Capture(root) + wantPaths := []string{"dws", "dws service", "dws service run"} + if got := sortedCommandPaths(snapshot.Commands); !reflect.DeepEqual(got, wantPaths) { + t.Fatalf("paths = %v, want %v", got, wantPaths) + } + + serviceSnapshot := commandIndex(snapshot)["dws service"] + if !serviceSnapshot.Hidden || serviceSnapshot.Deprecated != "use replacement" { + t.Fatalf("service metadata = %#v", serviceSnapshot) + } + if want := []string{"api", "svc"}; !reflect.DeepEqual(serviceSnapshot.Aliases, want) { + t.Fatalf("aliases = %v, want %v", serviceSnapshot.Aliases, want) + } + + leafSnapshot := commandIndex(snapshot)["dws service run"] + if !leafSnapshot.Runnable { + t.Fatal("runnable leaf was not recorded as runnable") + } + local := flagIndex(leafSnapshot.LocalFlags) + if len(local) != 2 { + t.Fatalf("local flags = %#v, want two business flags and no auto help flag", leafSnapshot.LocalFlags) + } + if local["name"].Type != "string" || !local["name"].Required { + t.Fatalf("name flag = %#v, want required string", local["name"]) + } + if local["force"].Type != "bool" || local["force"].Required { + t.Fatalf("force flag = %#v, want optional bool", local["force"]) + } + inherited := flagIndex(leafSnapshot.InheritedFlags) + if inherited["profile"].Type != "string" { + t.Fatalf("inherited flags = %#v, want root --profile", leafSnapshot.InheritedFlags) + } + if _, exists := inherited["help"]; exists { + t.Fatal("auto --help leaked into inherited flags") + } + + var first, second bytes.Buffer + if err := Write(&first, snapshot); err != nil { + t.Fatalf("first Write: %v", err) + } + if err := Write(&second, Capture(root)); err != nil { + t.Fatalf("second Write: %v", err) + } + if !bytes.Equal(first.Bytes(), second.Bytes()) { + t.Fatal("capturing the same Cobra root twice produced different JSON") + } +} + +func TestReadRejectsUnknownSnapshotFields(t *testing.T) { + input := bytes.NewBufferString(`{ + "schema_version": 1, + "rules": {"excluded_command_subtrees": [], "excluded_flags": []}, + "commands": [], + "future_field": true +}`) + if _, err := Read(input); err == nil { + t.Fatal("Read accepted an unknown field") + } +} + +func TestCompareBlocksCandidateSiblingAliasCollision(t *testing.T) { + base := testSnapshot(testCommand("dws")) + current := testSnapshot( + testCommand("dws"), + testCommandWithAliases("dws search", []string{"find"}), + testCommand("dws find"), + ) + comparison := Compare(current, base, "base") + if comparison.Compatible || !hasChangeKind(comparison.Blocking, "command_alias_collision") { + t.Fatalf("candidate sibling alias collision was not blocked: %#v", comparison) + } +} diff --git a/scripts/policy/check-command-compatibility.sh b/scripts/policy/check-command-compatibility.sh new file mode 100755 index 00000000..a15a69bb --- /dev/null +++ b/scripts/policy/check-command-compatibility.sh @@ -0,0 +1,122 @@ +#!/bin/sh +set -eu + +# Compare the current Cobra command surface with two local Git revisions: +# the PR merge-base/main reference and the latest stable GA tag. The caller must +# pass the highest non-prerelease SemVer tag (excluding beta/rc tags). Tag +# governance treats that tag as proof of a successfully published GA release; +# release automation must not leave a GA tag behind when publication fails. +# +# The script never fetches refs or snapshots from the network. With checkout +# fetch-depth=0, both references are materialized in temporary worktrees. A +# reference uses its own snapshot helper when available; older revisions are +# bootstrapped with the candidate helper. + +ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" +BASE_REF="" +STABLE_REF="" + +usage() { + printf '%s\n' "usage: $0 --base-ref --stable-ref " >&2 +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --base-ref) + [ "$#" -ge 2 ] || { usage; exit 2; } + BASE_REF="$2" + shift 2 + ;; + --stable-ref) + [ "$#" -ge 2 ] || { usage; exit 2; } + STABLE_REF="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'unknown argument: %s\n' "$1" >&2 + usage + exit 2 + ;; + esac +done + +[ -n "$BASE_REF" ] && [ -n "$STABLE_REF" ] || { usage; exit 2; } + +cd "$ROOT" +git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null || { + printf 'base ref is not available locally: %s\n' "$BASE_REF" >&2 + exit 2 +} +git rev-parse --verify --quiet "${STABLE_REF}^{commit}" >/dev/null || { + printf 'stable ref is not available locally: %s\n' "$STABLE_REF" >&2 + exit 2 +} + +TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-command-compat.XXXXXX")" +BASE_WORKTREE="$TMP_ROOT/base-worktree" +STABLE_WORKTREE="$TMP_ROOT/stable-worktree" + +cleanup() { + git -C "$ROOT" worktree remove --force "$BASE_WORKTREE" >/dev/null 2>&1 || true + git -C "$ROOT" worktree remove --force "$STABLE_WORKTREE" >/dev/null 2>&1 || true + rm -rf "$TMP_ROOT" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +ensure_snapshot_helper() { + worktree="$1" + + # Once a baseline contains the helper, use that revision's own capture + # rules. This makes a rule/filter change in the candidate visible as a + # snapshot_rules_changed failure instead of applying the new exclusion to + # both sides and accidentally hiding a removed command. Older revisions + # created before this helper existed are bootstrapped with the candidate + # implementation. + if [ -f "$worktree/cmd/interface-snapshot/main.go" ] && \ + [ -f "$worktree/internal/interfacesnapshot/snapshot.go" ] && \ + [ -f "$worktree/internal/interfacesnapshot/compare.go" ]; then + return + fi + if [ -e "$worktree/cmd/interface-snapshot" ] || \ + [ -e "$worktree/internal/interfacesnapshot" ]; then + printf 'baseline contains an incomplete interface snapshot helper: %s\n' "$worktree" >&2 + exit 2 + fi + + mkdir -p "$worktree/cmd/interface-snapshot" "$worktree/internal/interfacesnapshot" + cp -R "$ROOT/cmd/interface-snapshot/." "$worktree/cmd/interface-snapshot/" + cp -R "$ROOT/internal/interfacesnapshot/." "$worktree/internal/interfacesnapshot/" +} + +generate_ref_snapshot() { + ref="$1" + worktree="$2" + output="$3" + + git -C "$ROOT" worktree add --detach "$worktree" "$ref" >/dev/null + ensure_snapshot_helper "$worktree" + ( + cd "$worktree" + go run ./cmd/interface-snapshot generate --output "$output" + ) +} + +CANDIDATE="$TMP_ROOT/candidate.json" +BASELINE="$TMP_ROOT/merge-base.json" +STABLE="$TMP_ROOT/stable.json" + +go run ./cmd/interface-snapshot generate --output "$CANDIDATE" +generate_ref_snapshot "$BASE_REF" "$BASE_WORKTREE" "$BASELINE" +generate_ref_snapshot "$STABLE_REF" "$STABLE_WORKTREE" "$STABLE" + +go run ./cmd/interface-snapshot compare \ + --current "$CANDIDATE" \ + --base "$BASELINE" \ + --stable "$STABLE" diff --git a/test/mock_mcp/mock_mcp_smoke_test.go b/test/mock_mcp/mock_mcp_smoke_test.go new file mode 100644 index 00000000..a97cb649 --- /dev/null +++ b/test/mock_mcp/mock_mcp_smoke_test.go @@ -0,0 +1,219 @@ +package mock_mcp_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "reflect" + "sort" + "strings" + "sync" + "testing" + "time" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" +) + +const mockMCPSmokeHelperEnv = "DWS_MOCK_MCP_SMOKE_HELPER" + +type recordedToolCall struct { + path string + method string + authorization string + jsonrpc string + tool string + arguments map[string]any + err error +} + +// TestCLIHelperProcess runs the production CLI entrypoint with real os.Args. +// The parent test supplies only isolated temp directories, a loopback endpoint, +// and a synthetic token accepted by the local fake server. +func TestCLIHelperProcess(t *testing.T) { + if os.Getenv(mockMCPSmokeHelperEnv) != "1" { + return + } + + marker := -1 + for i, arg := range os.Args { + if arg == "--" { + marker = i + break + } + } + if marker < 0 { + fmt.Fprintln(os.Stderr, "Mock MCP smoke helper: missing -- argument marker") + os.Exit(2) + } + os.Args = append([]string{"dws"}, os.Args[marker+1:]...) + os.Exit(app.Execute()) +} + +func TestMockMCPSmoke_CLIRoutesSerializedArgumentsAndPrintsJSON(t *testing.T) { + var requestsMu sync.Mutex + var requests []recordedToolCall + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + call := recordedToolCall{ + path: r.URL.Path, + method: r.Method, + authorization: r.Header.Get("Authorization"), + } + + var envelope struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params struct { + Name string `json:"name"` + Arguments map[string]any `json:"arguments"` + } `json:"params"` + } + if err := json.NewDecoder(r.Body).Decode(&envelope); err != nil { + call.err = err + } else { + call.jsonrpc = envelope.JSONRPC + call.tool = envelope.Params.Name + call.arguments = envelope.Params.Arguments + if envelope.Method != "tools/call" { + call.err = fmt.Errorf("JSON-RPC method = %q, want tools/call", envelope.Method) + } + } + requestsMu.Lock() + requests = append(requests, call) + requestsMu.Unlock() + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "id": envelope.ID, + "result": map[string]any{ + "content": []map[string]any{{ + "type": "text", + "text": `{"success":true,"result":[{"userId":"mock-user-1","name":"Local Mock"}]}`, + }}, + }, + }) + })) + defer server.Close() + + env := isolatedCLIEnv(t, map[string]string{ + "DINGTALK_CONTACT_MCP_URL": server.URL + "/mcp/contact", + }) + args := []string{ + "--token", "ci-smoke-token", + "--format", "json", + "contact", "user", "get", + "--ids", "user-001,user-002", + } + stdout, stderr, err := runCLI(t, env, args...) + if err != nil { + t.Fatalf("dws %s failed: %v\nstdout:\n%s\nstderr:\n%s", strings.Join(args, " "), err, stdout, stderr) + } + + requestsMu.Lock() + recorded := append([]recordedToolCall(nil), requests...) + requestsMu.Unlock() + if len(recorded) != 1 { + t.Fatalf("local fake MCP server received %d requests, want exactly one tools/call: %#v", len(recorded), recorded) + } + call := recorded[0] + if call.err != nil { + t.Fatal(call.err) + } + if call.path != "/mcp/contact" { + t.Fatalf("request path = %q, want /mcp/contact", call.path) + } + if call.method != http.MethodPost { + t.Fatalf("HTTP method = %q, want POST", call.method) + } + if call.authorization != "Bearer ci-smoke-token" { + t.Fatalf("Authorization = %q, want synthetic smoke token", call.authorization) + } + if call.jsonrpc != "2.0" { + t.Fatalf("jsonrpc = %q, want 2.0", call.jsonrpc) + } + if call.tool != "get_user_info_by_user_ids" { + t.Fatalf("tool = %q, want get_user_info_by_user_ids", call.tool) + } + wantArgs := map[string]any{"user_id_list": []any{"user-001", "user-002"}} + if !reflect.DeepEqual(call.arguments, wantArgs) { + t.Fatalf("arguments = %#v, want %#v", call.arguments, wantArgs) + } + + var payload map[string]any + if err := json.Unmarshal([]byte(stdout), &payload); err != nil { + t.Fatalf("CLI returned non-JSON stdout: %v\nstdout:\n%s\nstderr:\n%s", err, stdout, stderr) + } + if payload["success"] != true { + t.Fatalf("CLI success = %#v, want true; payload=%#v", payload["success"], payload) + } + result, ok := payload["result"].([]any) + if !ok || len(result) != 1 { + t.Fatalf("CLI result = %#v, want one mock user", payload["result"]) + } + user, _ := result[0].(map[string]any) + if user["userId"] != "mock-user-1" { + t.Fatalf("CLI userId = %#v, want mock-user-1; payload=%#v", user["userId"], payload) + } +} + +func isolatedCLIEnv(t *testing.T, extra map[string]string) []string { + t.Helper() + + root := t.TempDir() + controlled := map[string]string{ + "HOME": root, + "USERPROFILE": root, + "DWS_CONFIG_DIR": filepath.Join(root, "config"), + "DWS_KEYCHAIN_DIR": filepath.Join(root, "keychain"), + "DWS_DISABLE_KEYCHAIN": "1", + "HTTP_PROXY": "http://127.0.0.1:1", + "HTTPS_PROXY": "http://127.0.0.1:1", + "http_proxy": "http://127.0.0.1:1", + "https_proxy": "http://127.0.0.1:1", + "NO_PROXY": "127.0.0.1,localhost,::1", + "no_proxy": "127.0.0.1,localhost,::1", + mockMCPSmokeHelperEnv: "1", + "DWS_ALLOW_HTTP_ENDPOINTS": "1", + "DWS_TRUSTED_DOMAINS": "127.0.0.1,localhost,::1", + } + for key, value := range extra { + controlled[key] = value + } + + env := make([]string, 0, len(controlled)+8) + for _, key := range []string{"PATH", "TMPDIR", "TEMP", "TMP", "LANG", "LC_ALL", "TZ", "SYSTEMROOT"} { + if value := os.Getenv(key); value != "" { + env = append(env, key+"="+value) + } + } + for key, value := range controlled { + env = append(env, key+"="+value) + } + sort.Strings(env) + return env +} + +func runCLI(t *testing.T, env []string, args ...string) (string, string, error) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + processArgs := append([]string{"-test.run=^TestCLIHelperProcess$", "--"}, args...) + cmd := exec.CommandContext(ctx, os.Args[0], processArgs...) + cmd.Env = env + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if ctx.Err() != nil { + t.Fatalf("dws %s timed out: %v\nstdout:\n%s\nstderr:\n%s", strings.Join(args, " "), ctx.Err(), stdout.String(), stderr.String()) + } + return stdout.String(), stderr.String(), err +} diff --git a/test/smoke/cli_smoke_test.go b/test/smoke/cli_smoke_test.go new file mode 100644 index 00000000..27f86e5c --- /dev/null +++ b/test/smoke/cli_smoke_test.go @@ -0,0 +1,212 @@ +package smoke_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" + "github.com/spf13/cobra" +) + +const cliSmokeHelperEnv = "DWS_CLI_SMOKE_HELPER" + +// TestCLIHelperProcess executes the real app entrypoint in a subprocess. Product +// routing inspects os.Args, so using Cobra.SetArgs would not exercise the same +// path as the dws binary. +func TestCLIHelperProcess(t *testing.T) { + if os.Getenv(cliSmokeHelperEnv) != "1" { + return + } + + marker := -1 + for i, arg := range os.Args { + if arg == "--" { + marker = i + break + } + } + if marker < 0 { + fmt.Fprintln(os.Stderr, "CLI smoke helper: missing -- argument marker") + os.Exit(2) + } + os.Args = append([]string{"dws"}, os.Args[marker+1:]...) + os.Exit(app.Execute()) +} + +func TestCLISmoke_AllPublicCommandsSupportHelp(t *testing.T) { + _ = isolatedCLIEnv(t) + + root := app.NewRootCommand() + root.InitDefaultHelpCmd() + paths := publicCommandPaths(root) + if len(paths) == 0 { + t.Fatal("public command traversal returned no commands") + } + + for _, path := range paths { + path := path + name := "root" + if len(path) > 0 { + name = strings.Join(path, "/") + } + t.Run(name, func(t *testing.T) { + args := append(append([]string(nil), path...), "--help") + // Help never executes a product handler, so SetArgs is sufficient here. + // Real product dispatch is covered below through app.Execute in a + // subprocess because that path intentionally inspects os.Args. + cmd := app.NewRootCommand() + var output bytes.Buffer + cmd.SetOut(&output) + cmd.SetErr(&output) + cmd.SetArgs(args) + err := cmd.Execute() + if err != nil { + t.Fatalf("dws %s failed: %v\noutput:\n%s", strings.Join(args, " "), err, output.String()) + } + if strings.TrimSpace(output.String()) == "" { + t.Fatalf("dws %s returned empty help", strings.Join(args, " ")) + } + }) + } + + t.Logf("validated --help for %d public Cobra command paths", len(paths)) +} + +func TestCLISmoke_RepresentativeStaticCommandsReturnMockJSON(t *testing.T) { + env := isolatedCLIEnv(t) + + tests := []struct { + name string + args []string + wantTool string + }{ + { + name: "contact search", + args: []string{"--mock", "--format", "json", "contact", "user", "search", "--query", "Ada"}, + wantTool: "search_contact_by_key_word", + }, + { + name: "calendar list", + args: []string{ + "--mock", "--format", "json", "calendar", "event", "list", + "--start", "2026-07-10T09:00:00+08:00", + "--end", "2026-07-10T10:00:00+08:00", + }, + wantTool: "list_calendar_events", + }, + { + name: "ding list", + args: []string{"--mock", "--format", "json", "ding", "message", "list", "--type", "UNREAD"}, + wantTool: "list_ding_messages", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + stdout, stderr, err := runCLI(t, env, tc.args...) + if err != nil { + t.Fatalf("dws %s failed: %v\nstdout:\n%s\nstderr:\n%s", strings.Join(tc.args, " "), err, stdout, stderr) + } + + var payload map[string]any + if err := json.Unmarshal([]byte(stdout), &payload); err != nil { + t.Fatalf("dws %s returned non-JSON stdout: %v\nstdout:\n%s\nstderr:\n%s", strings.Join(tc.args, " "), err, stdout, stderr) + } + if payload["_mock"] != true { + t.Fatalf("dws %s _mock = %#v, want true; payload=%#v", strings.Join(tc.args, " "), payload["_mock"], payload) + } + if payload["_tool"] != tc.wantTool { + t.Fatalf("dws %s _tool = %#v, want %q; payload=%#v", strings.Join(tc.args, " "), payload["_tool"], tc.wantTool, payload) + } + }) + } +} + +// publicCommandPaths follows only canonical, user-visible Cobra commands. +// Hidden commands and commands marked Deprecated by Cobra are intentionally +// excluded; aliases are not separate command nodes. Parents and leaves are both +// checked, while positional placeholders in Use are never synthesized. +func publicCommandPaths(root *cobra.Command) [][]string { + var paths [][]string + var walk func(*cobra.Command, []string) + walk = func(cmd *cobra.Command, path []string) { + if cmd != root && (cmd.Hidden || cmd.Deprecated != "") { + return + } + + paths = append(paths, append([]string(nil), path...)) + for _, child := range cmd.Commands() { + walk(child, append(path, child.Name())) + } + } + walk(root, nil) + + sort.Slice(paths, func(i, j int) bool { + return strings.Join(paths[i], " ") < strings.Join(paths[j], " ") + }) + return paths +} + +func isolatedCLIEnv(t *testing.T) []string { + t.Helper() + + root := t.TempDir() + controlled := map[string]string{ + "HOME": root, + "USERPROFILE": root, + "DWS_CONFIG_DIR": filepath.Join(root, "config"), + "DWS_KEYCHAIN_DIR": filepath.Join(root, "keychain"), + "DWS_DISABLE_KEYCHAIN": "1", + "HTTP_PROXY": "http://127.0.0.1:1", + "HTTPS_PROXY": "http://127.0.0.1:1", + "http_proxy": "http://127.0.0.1:1", + "https_proxy": "http://127.0.0.1:1", + "NO_PROXY": "127.0.0.1,localhost,::1", + "no_proxy": "127.0.0.1,localhost,::1", + cliSmokeHelperEnv: "1", + "DWS_ALLOW_HTTP_ENDPOINTS": "1", + "DWS_TRUSTED_DOMAINS": "127.0.0.1,localhost,::1", + } + for key, value := range controlled { + t.Setenv(key, value) + } + + env := make([]string, 0, len(controlled)+8) + for _, key := range []string{"PATH", "TMPDIR", "TEMP", "TMP", "LANG", "LC_ALL", "TZ", "SYSTEMROOT"} { + if value := os.Getenv(key); value != "" { + env = append(env, key+"="+value) + } + } + for key, value := range controlled { + env = append(env, key+"="+value) + } + sort.Strings(env) + return env +} + +func runCLI(t *testing.T, env []string, args ...string) (string, string, error) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + processArgs := append([]string{"-test.run=^TestCLIHelperProcess$", "--"}, args...) + cmd := exec.CommandContext(ctx, os.Args[0], processArgs...) + cmd.Env = env + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if ctx.Err() != nil { + t.Fatalf("dws %s timed out: %v\nstdout:\n%s\nstderr:\n%s", strings.Join(args, " "), ctx.Err(), stdout.String(), stderr.String()) + } + return stdout.String(), stderr.String(), err +} From fe5f484c29ad277c073ce0d242fab66e5a851592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Mon, 13 Jul 2026 15:47:23 +0800 Subject: [PATCH 05/14] fix(ci): integrate code admission dependencies --- .github/workflows/ci.yml | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a982cac..f83fce6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,38 +38,6 @@ jobs: # version: v1.64.8 # args: ./... - code-check: - name: Code Check - needs: [lint, test, policy, edition-tests] - if: ${{ always() }} - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: {} - steps: - - name: Verify code checks - env: - LINT_RESULT: ${{ needs.lint.result }} - TEST_RESULT: ${{ needs.test.result }} - POLICY_RESULT: ${{ needs.policy.result }} - EDITION_TESTS_RESULT: ${{ needs.edition-tests.result }} - run: | - set -eu - failed=0 - for check in \ - "Lint:$LINT_RESULT" \ - "Test:$TEST_RESULT" \ - "Policy Check:$POLICY_RESULT" \ - "Edition Contract Tests:$EDITION_TESTS_RESULT" - do - name="${check%%:*}" - result="${check#*:}" - printf '%s: %s\n' "$name" "$result" - if [ "$result" != "success" ]; then - failed=1 - fi - done - test "$failed" -eq 0 - test: name: Test runs-on: ubuntu-latest From 4d284c3740df933175ab521be9085eb80d865d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Wed, 15 Jul 2026 00:58:44 +0800 Subject: [PATCH 06/14] fix(release): rebase guardrails onto sealed main --- CHANGELOG.md | 10 +- test/scripts/package_script_test.go | 143 ++++++++++++++-------------- 2 files changed, 71 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7983b3..00ff178c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,9 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th ## [Unreleased] -### Added +### Changed -- **Declarative shortcut commands** (#592) — adds 366 `dws +` shortcuts across 16 services, including one-to-one MCP wrappers and multi-step smart workflows. Shortcuts publish stable Agent-visible contracts with named flags, validation and confirmation metadata, dry-run protection for writes, catalog/help routing, and optional local YAML extensions and usage recording. -- **Sheet imports and Aitable workflow writes** (#624) — adds `dws sheet import` / `sheet import create` for converting local xlsx/xls files into new online sheets, `sheet import get` for polling import tasks, and `dws aitable workflow create/update` for applying validated `workflow-dsl/v1` definitions, with matching reviewed Agent Schema and bundled Skill guidance. -- **Official multi-platform Homebrew channel** — stable `Formula/dingtalk-workspace-cli.rb` and keg-only `Formula/dingtalk-workspace-cli-beta.rb` live in this repository and select signed macOS Intel/Apple Silicon or Linux amd64/arm64 artifacts at install time. Stable and beta releases open isolated Formula update PRs after final artifact signing, so beta never replaces the stable Formula. Agent Skills stay under `pkgshare` without mutating the user's home directory, and both tracks are covered by the six-channel post-release verifier. +- **Guarded prerelease and stable automation** — adds the guided `dws-release` entry for one-command CHANGELOG preparation, validation-only and annotated-tag publication flows; promotes only an explicitly validated beta; verifies command-tree compatibility and all six packaged binaries; and serializes immutable GitHub Release, npm channel, OSS, and optional Gitee delivery with fail-closed recovery checks. ### Fixed @@ -42,10 +40,6 @@ This release seals the `v1.0.52` line with personal event subscriptions, a deter - **Complex Connect messages and attachments** (#606, #612) — rich-text messages retain all embedded pictures in order, queued turns keep every pending attachment, and unknown or future callback shapes reach each Agent backend with their message type and raw JSON instead of being discarded. Attachment recovery is locator-based, nested `chatRecord` pictures/audio/video/files can be recovered from message APIs after Stream ACK, and OpenCode uses a full-duration storyboard for large videos to avoid base64 OOMs while preserving the original download for the turn. - **macOS auth survives Keychain mode changes** (#597) — credential reads try existing compatible DEKs without creating key material, updates preserve the DEK that decrypted existing ciphertext, unreadable slots fail closed before token exchange, profile slots use the canonical auth backend, and `auth status` reports ciphertext/key mismatches instead of treating them as ordinary logout. Dedicated macOS race and Windows DPAPI coverage protect the cross-platform paths. -### Changed - -- **Guarded prerelease and stable automation** — adds the guided `dws-release` entry for one-command CHANGELOG preparation, validation-only and annotated-tag publication flows; promotes only an explicitly validated beta; verifies command-tree compatibility and all six packaged binaries; and serializes immutable GitHub Release, npm channel, OSS, and optional Gitee delivery with fail-closed recovery checks. - ## [1.0.51] - 2026-07-10 This release promotes the sealed `v1.0.51-beta.1` contents to stable. It syncs the hardcoded Wukong command surface, prevents `dev connect` conversations from blocking on messages received mid-turn, and makes local credential failures diagnosable without mutating key material. diff --git a/test/scripts/package_script_test.go b/test/scripts/package_script_test.go index 0dd71e37..75e33269 100644 --- a/test/scripts/package_script_test.go +++ b/test/scripts/package_script_test.go @@ -33,32 +33,6 @@ var expectedPackagedSkillTargets = []string{ ".hermes/skills/dws", } -// seedDistArtifacts creates minimal goreleaser output archives and a -// checksums.txt stub so post-goreleaser.sh can run without a real build. -// Every archive is valid so the packaging tests exercise extraction for all -// platforms; Darwin archives are additionally processed by the signing path. -func seedDistArtifacts(t *testing.T, distDir string, targets []string) { - t.Helper() - if err := os.MkdirAll(distDir, 0o755); err != nil { - t.Fatalf("MkdirAll(%s) error = %v", distDir, err) - } - - for _, target := range targets { - p := filepath.Join(distDir, target) - seedDistArchive(t, p) - } - - // Create empty checksums.txt (goreleaser creates this) - checksums := filepath.Join(distDir, "checksums.txt") - var lines []string - for _, target := range targets { - lines = append(lines, "deadbeef00000000000000000000000000000000000000000000000000000000 "+target) - } - if err := os.WriteFile(checksums, []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil { - t.Fatalf("WriteFile(%s) error = %v", checksums, err) - } -} - func seedDistArchive(t *testing.T, path string) { t.Helper() file, err := os.Create(path) @@ -103,10 +77,34 @@ func seedDistArchive(t *testing.T, path string) { } } -func postGoreleaserEnv(t *testing.T, distDir, releaseBaseURL string) []string { +// seedDistArtifacts creates minimal goreleaser output archives and a +// checksums.txt stub so post-goreleaser.sh can run without a real build. +// Every archive is valid so the packaging tests exercise extraction for all +// platforms; Darwin archives are additionally processed by the signing path. +func seedDistArtifacts(t *testing.T, distDir string, targets []string) { + t.Helper() + if err := os.MkdirAll(distDir, 0o755); err != nil { + t.Fatalf("MkdirAll(%s) error = %v", distDir, err) + } + + for _, target := range targets { + p := filepath.Join(distDir, target) + seedDistArchive(t, p) + } + + // Create empty checksums.txt (goreleaser creates this) + checksums := filepath.Join(distDir, "checksums.txt") + var lines []string + for _, target := range targets { + lines = append(lines, "deadbeef00000000000000000000000000000000000000000000000000000000 "+target) + } + if err := os.WriteFile(checksums, []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil { + t.Fatalf("WriteFile(%s) error = %v", checksums, err) + } +} + +func postGoreleaserEnv(t *testing.T, distDir, version, releaseBaseURL string) []string { t.Helper() - trimmedBaseURL := strings.TrimRight(releaseBaseURL, "/") - version := trimmedBaseURL[strings.LastIndex(trimmedBaseURL, "/")+1:] binDir := t.TempDir() fakeCodesign := filepath.Join(binDir, "codesign") @@ -161,7 +159,7 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) { seedDistArtifacts(t, distDir, targets) cmd := exec.Command("sh", scriptPath) - cmd.Env = postGoreleaserEnv(t, distDir, "https://downloads.example.com/dws/releases/v1.2.3") + cmd.Env = postGoreleaserEnv(t, distDir, "v1.2.3", "https://downloads.example.com/dws/releases/v1.2.3") output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("post-goreleaser.sh error = %v\noutput:\n%s", err, string(output)) @@ -259,7 +257,7 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) { // Re-running post packaging must replace, not duplicate, the skills checksum. cmd = exec.Command("sh", scriptPath) - cmd.Env = postGoreleaserEnv(t, distDir, "https://downloads.example.com/dws/releases/v1.2.3") + cmd.Env = postGoreleaserEnv(t, distDir, "v1.2.3", "https://downloads.example.com/dws/releases/v1.2.3") if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("second post-goreleaser.sh error = %v\noutput:\n%s", err, output) } @@ -440,7 +438,7 @@ func TestPostGoreleaserAllPlatformNpmAssets(t *testing.T) { seedDistArtifacts(t, distDir, allArchives) cmd := exec.Command("sh", scriptPath) - cmd.Env = postGoreleaserEnv(t, distDir, "https://downloads.example.com/dws/releases/v9.9.9") + cmd.Env = postGoreleaserEnv(t, distDir, "v9.9.9", "https://downloads.example.com/dws/releases/v9.9.9") output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("post-goreleaser.sh error = %v\noutput:\n%s", err, string(output)) @@ -533,7 +531,7 @@ func TestPostGoreleaserSkillsZipLayout(t *testing.T) { seedDistArtifacts(t, distDir, targets) cmd := exec.Command("sh", scriptPath) - cmd.Env = postGoreleaserEnv(t, distDir, "https://downloads.example.com/dws/releases/v0.0.0") + cmd.Env = postGoreleaserEnv(t, distDir, "v0.0.0-test", "https://downloads.example.com/dws/releases/v0.0.0") output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("post-goreleaser.sh error = %v\noutput:\n%s", err, string(output)) @@ -608,38 +606,40 @@ func TestReleaseWorkflowUploadsPostProcessedDarwinAssets(t *testing.T) { } workflow := string(data) + build := strings.Index(workflow, "Build release artifacts without publishing") postProcess := strings.Index(workflow, "./scripts/release/post-goreleaser.sh") - upload := strings.Index(workflow, "Upload finalized signed assets to release") - if postProcess == -1 || upload == -1 || upload < postProcess { - t.Fatalf("finalized asset upload must run after post-goreleaser.sh") + preserve := strings.Index(workflow, "Preserve finalized distribution files") + verifyJob := strings.Index(workflow, "verify-darwin-signatures:") + publishJob := strings.Index(workflow, "publish-release:") + if build == -1 || postProcess == -1 || preserve == -1 || verifyJob == -1 || publishJob == -1 || + !(build < postProcess && postProcess < preserve && preserve < verifyJob && verifyJob < publishJob) { + t.Fatalf("post-processed assets must be preserved, Apple-verified, and only then published") } - if !strings.Contains(workflow[upload:], "./scripts/release/finalize-github-release.sh") { - t.Fatal("release workflow must delegate atomic finalization to finalize-github-release.sh") - } - - finalizePath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "finalize-github-release.sh")) - if err != nil { - t.Fatalf("Abs(finalize-github-release.sh) error = %v", err) - } - finalizeData, err := os.ReadFile(finalizePath) - if err != nil { - t.Fatalf("ReadFile(%s) error = %v", finalizePath, err) - } - finalize := string(finalizeData) - + buildSection := workflow[build:verifyJob] for _, required := range []string{ - "dws-darwin-amd64.tar.gz", - "dws-darwin-arm64.tar.gz", + "--skip=publish", + "actions/upload-artifact@v4", + "finalized-release-dist", + } { + if !strings.Contains(buildSection, required) { + t.Errorf("signed build stage is missing %q", required) + } + } + + publishSection := workflow[publishJob:] + for _, required := range []string{ + "actions/download-artifact@v4", + "dist/dws-*.tar.gz", + "dist/dws-windows-*.zip", "checksums.txt", "dws-skills.zip", "gh release upload", - "gh release view", "--clobber", - "release asset digest mismatch", + "verify-release-artifacts.sh", } { - if !strings.Contains(finalize, required) { - t.Errorf("finalized asset upload is missing %q", required) + if !strings.Contains(publishSection, required) { + t.Errorf("immutable publication stage is missing %q", required) } } } @@ -658,7 +658,7 @@ func TestReleaseWorkflowConfiguresDeveloperIDSigning(t *testing.T) { workflow := string(data) prepare := strings.Index(workflow, "Prepare Apple Developer ID certificate") - goReleaser := strings.Index(workflow, "Run GoReleaser") + goReleaser := strings.Index(workflow, "Build release artifacts without publishing") postProcess := strings.Index(workflow, "./scripts/release/post-goreleaser.sh") cleanup := strings.Index(workflow, "Remove Apple Developer ID certificate") if prepare == -1 || goReleaser == -1 || postProcess == -1 || cleanup == -1 || @@ -749,36 +749,35 @@ func TestReleaseWorkflowUsesAppleCodesignBeforePublication(t *testing.T) { } workflow := string(data) - upload := strings.Index(workflow, "Upload finalized signed assets to release") + preserve := strings.Index(workflow, "Preserve finalized distribution files") verifyJob := strings.Index(workflow, "verify-darwin-signatures:") publishJob := strings.Index(workflow, "publish-release:") - if upload == -1 || verifyJob == -1 || publishJob == -1 || !(upload < verifyJob && verifyJob < publishJob) { - t.Fatal("finalized Draft assets must be uploaded, Apple-verified, and only then published") + if preserve == -1 || verifyJob == -1 || publishJob == -1 || !(preserve < verifyJob && verifyJob < publishJob) { + t.Fatal("finalized artifacts must be preserved, Apple-verified, and only then published") } codesign := strings.Index(workflow[verifyJob:publishJob], "codesign --verify --strict --verbose=4") - publish := strings.Index(workflow[publishJob:], `gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false`) + publish := strings.Index(workflow[publishJob:], `gh release edit "$GITHUB_REF_NAME" --draft=false`) if codesign == -1 || publish == -1 { t.Fatal("macOS codesign verification and explicit Draft publication are required") } - buildSection := workflow[upload:verifyJob] + buildSection := workflow[preserve:verifyJob] for _, required := range []string{ - `DWS_PUBLISH_RELEASE: "false"`, "actions/upload-artifact@v4", "finalized-release-dist", } { if !strings.Contains(buildSection, required) { - t.Errorf("Draft build stage is missing %q", required) + t.Errorf("signed build stage is missing %q", required) } } verifySection := workflow[verifyJob:publishJob] for _, required := range []string{ "runs-on: macos-latest", - "gh release download", - "dws-darwin-amd64.tar.gz", - "dws-darwin-arm64.tar.gz", + "actions/download-artifact@v4", + "finalized-release-dist", + `dws-darwin-${arch}.tar.gz`, "codesign --verify --strict --verbose=4", } { if !strings.Contains(verifySection, required) { @@ -790,13 +789,9 @@ func TestReleaseWorkflowUsesAppleCodesignBeforePublication(t *testing.T) { for _, required := range []string{ "verify-darwin-signatures", "actions/download-artifact@v4", - "Publish verified Draft release", - "Publish stable to npm", - "Publish prerelease to npm beta", - "Open stable Homebrew formula PR", - "Open beta Homebrew formula PR", - "DingTalk-Real-AI/dingtalk-workspace-cli.git", - "secrets.GITHUB_TOKEN", + "Publish or reuse immutable GitHub Release", + "gh release upload", + "Publish missing version to npm channel", } { if !strings.Contains(publishSection, required) { t.Errorf("post-verification publication stage is missing %q", required) From 1a51f3a8da856ec2f9c323b987850dc315b2051a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Wed, 15 Jul 2026 10:39:25 +0800 Subject: [PATCH 07/14] fix(release): pin goreleaser to pushed tag --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc9bde54..8eb05bdd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -297,6 +297,7 @@ jobs: args: release --clean --skip=publish --release-notes=${{ runner.temp }}/release-notes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_CURRENT_TAG: ${{ github.ref_name }} - name: Post-release packaging run: ./scripts/release/post-goreleaser.sh From 8d21510aec1b125d0035eb3f1ea4d443e3ecbbed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Thu, 16 Jul 2026 00:03:18 +0800 Subject: [PATCH 08/14] fix(ci): enforce clean release workflows --- .github/workflows/release.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8eb05bdd..5ba7466a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -650,20 +650,24 @@ jobs: echo "Cannot reconcile npm $npm_tag=$current with $GITHUB_REF_NAME" >&2 exit 1 fi - echo "published=true" >> "$GITHUB_OUTPUT" - echo "advance_channel=$advance_channel" >> "$GITHUB_OUTPUT" - echo "npm_tag=$npm_tag" >> "$GITHUB_OUTPUT" - echo "semver=$semver" >> "$GITHUB_OUTPUT" + { + echo "published=true" + echo "advance_channel=$advance_channel" + echo "npm_tag=$npm_tag" + echo "semver=$semver" + } >> "$GITHUB_OUTPUT" exit 0 fi if [ -n "$current" ] && ! release_version_is_greater "$GITHUB_REF_NAME" "v$current"; then echo "$GITHUB_REF_NAME would move npm $npm_tag backwards from v$current" >&2 exit 1 fi - echo "published=false" >> "$GITHUB_OUTPUT" - echo "advance_channel=false" >> "$GITHUB_OUTPUT" - echo "npm_tag=$npm_tag" >> "$GITHUB_OUTPUT" - echo "semver=$semver" >> "$GITHUB_OUTPUT" + { + echo "published=false" + echo "advance_channel=false" + echo "npm_tag=$npm_tag" + echo "semver=$semver" + } >> "$GITHUB_OUTPUT" - name: Publish missing version to npm channel if: ${{ github.repository_owner == 'DingTalk-Real-AI' && steps.npm-state.outputs.published != 'true' }} From 51f531c3fdb8c72815ed1e746dacef27773c5cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Thu, 16 Jul 2026 09:48:09 +0800 Subject: [PATCH 09/14] fix(release): isolate helper variables --- scripts/release/release-lib.sh | 80 ++++++++++++++--------------- test/scripts/release_script_test.go | 52 +++++++++++++++++++ 2 files changed, 92 insertions(+), 40 deletions(-) diff --git a/scripts/release/release-lib.sh b/scripts/release/release-lib.sh index c27f6733..6738deca 100755 --- a/scripts/release/release-lib.sh +++ b/scripts/release/release-lib.sh @@ -16,32 +16,32 @@ release_is_prerelease_version() { } release_channel_for_version() { - version="$1" - if release_is_stable_version "$version"; then + _rcfv_version="$1" + if release_is_stable_version "$_rcfv_version"; then printf '%s\n' stable return 0 fi - if release_is_prerelease_version "$version"; then + if release_is_prerelease_version "$_rcfv_version"; then printf '%s\n' prerelease return 0 fi - printf 'invalid release version: %s (expected vX.Y.Z or vX.Y.Z-beta.N)\n' "$version" >&2 + printf 'invalid release version: %s (expected vX.Y.Z or vX.Y.Z-beta.N)\n' "$_rcfv_version" >&2 return 1 } release_validate_version_channel() { - expected="$1" - version="$2" - case "$expected" in + _rvvc_expected="$1" + _rvvc_version="$2" + case "$_rvvc_expected" in stable|prerelease) ;; *) - printf 'invalid release channel: %s (expected prerelease or stable)\n' "$expected" >&2 + printf 'invalid release channel: %s (expected prerelease or stable)\n' "$_rvvc_expected" >&2 return 1 ;; esac - actual="$(release_channel_for_version "$version")" || return 1 - if [ "$actual" != "$expected" ]; then - printf 'release channel/version mismatch: channel=%s version=%s\n' "$expected" "$version" >&2 + _rvvc_actual="$(release_channel_for_version "$_rvvc_version")" || return 1 + if [ "$_rvvc_actual" != "$_rvvc_expected" ]; then + printf 'release channel/version mismatch: channel=%s version=%s\n' "$_rvvc_expected" "$_rvvc_version" >&2 return 1 fi } @@ -51,8 +51,8 @@ release_semver() { } release_core_tag() { - version="$1" - printf '%s\n' "${version%%-beta.*}" + _rct_version="$1" + printf '%s\n' "${_rct_version%%-beta.*}" } release_beta_number() { @@ -60,11 +60,11 @@ release_beta_number() { } release_core_is_greater() { - candidate="$(release_core_tag "$1")" - baseline="$(release_core_tag "$2")" - candidate="${candidate#v}" - baseline="${baseline#v}" - awk -v candidate="$candidate" -v baseline="$baseline" 'BEGIN { + _rcig_candidate="$(release_core_tag "$1")" + _rcig_baseline="$(release_core_tag "$2")" + _rcig_candidate="${_rcig_candidate#v}" + _rcig_baseline="${_rcig_baseline#v}" + awk -v candidate="$_rcig_candidate" -v baseline="$_rcig_baseline" 'BEGIN { split(candidate, c, ".") split(baseline, b, ".") for (i = 1; i <= 3; i++) { @@ -100,13 +100,13 @@ release_version_is_greater() { # Extract one exact CHANGELOG section (without its H2 heading) and validate that # it is dated, unique, non-placeholder content with at least one bullet. release_extract_changelog() { - changelog="$1" - semver="$2" - output="$3" - tmp="$(mktemp "${TMPDIR:-/tmp}/dws-release-notes.XXXXXX")" + _rec_changelog="$1" + _rec_semver="$2" + _rec_output="$3" + _rec_tmp="$(mktemp "${TMPDIR:-/tmp}/dws-release-notes.XXXXXX")" set +e - awk -v wanted="$semver" ' + awk -v wanted="$_rec_semver" ' BEGIN { prefix = "## [" wanted "] - " found = 0 @@ -139,29 +139,29 @@ release_extract_changelog() { if (!meaningful || !bullet) exit 43 if (placeholder) exit 44 } - ' "$changelog" > "$tmp" - status=$? + ' "$_rec_changelog" > "$_rec_tmp" + _rec_status=$? set -e - case "$status" in + case "$_rec_status" in 0) ;; - 41) printf 'CHANGELOG must contain exactly one section: ## [%s] - YYYY-MM-DD\n' "$semver" >&2 ;; - 42) printf 'CHANGELOG section for %s has an invalid date\n' "$semver" >&2 ;; - 43) printf 'CHANGELOG section for %s must contain release notes and at least one bullet\n' "$semver" >&2 ;; - 44) printf 'CHANGELOG section for %s still contains TODO/TBD placeholders\n' "$semver" >&2 ;; - *) printf 'failed to parse CHANGELOG section for %s\n' "$semver" >&2 ;; + 41) printf 'CHANGELOG must contain exactly one section: ## [%s] - YYYY-MM-DD\n' "$_rec_semver" >&2 ;; + 42) printf 'CHANGELOG section for %s has an invalid date\n' "$_rec_semver" >&2 ;; + 43) printf 'CHANGELOG section for %s must contain release notes and at least one bullet\n' "$_rec_semver" >&2 ;; + 44) printf 'CHANGELOG section for %s still contains TODO/TBD placeholders\n' "$_rec_semver" >&2 ;; + *) printf 'failed to parse CHANGELOG section for %s\n' "$_rec_semver" >&2 ;; esac - if [ "$status" -ne 0 ]; then - rm -f "$tmp" - return "$status" + if [ "$_rec_status" -ne 0 ]; then + rm -f "$_rec_tmp" + return "$_rec_status" fi - if [ "$output" = "-" ]; then - cat "$tmp" + if [ "$_rec_output" = "-" ]; then + cat "$_rec_tmp" else - mkdir -p "$(dirname "$output")" - mv "$tmp" "$output" - tmp="" + mkdir -p "$(dirname "$_rec_output")" + mv "$_rec_tmp" "$_rec_output" + _rec_tmp="" fi - [ -z "$tmp" ] || rm -f "$tmp" + [ -z "$_rec_tmp" ] || rm -f "$_rec_tmp" } diff --git a/test/scripts/release_script_test.go b/test/scripts/release_script_test.go index ab424c80..ed3bfe3d 100644 --- a/test/scripts/release_script_test.go +++ b/test/scripts/release_script_test.go @@ -138,6 +138,58 @@ func TestReleaseVersionOrdering(t *testing.T) { } } +func TestReleaseLibHelpersPreserveCallerVariables(t *testing.T) { + sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("Abs(repo root) error = %v", err) + } + lib := filepath.Join(sourceRoot, "scripts", "release", "release-lib.sh") + changelogPath := filepath.Join(t.TempDir(), "CHANGELOG.md") + mustWriteFile(t, changelogPath, []byte("## [1.2.3] - 2026-07-16\n\n- Test release.\n"), 0o644) + + script := ` +. "$1" +version=caller-version +expected=caller-expected +actual=caller-actual +candidate=caller-candidate +baseline=caller-baseline +changelog=caller-changelog +semver=caller-semver +output=caller-output +tmp=caller-tmp +status=caller-status + +release_channel_for_version v1.2.3 >/dev/null +release_validate_version_channel stable v1.2.3 +release_core_tag v1.2.3-beta.1 >/dev/null +release_core_is_greater v1.2.4 v1.2.3 +release_extract_changelog "$2" 1.2.3 - >/dev/null + +printf '%s\n' "$version" "$expected" "$actual" "$candidate" "$baseline" \ + "$changelog" "$semver" "$output" "$tmp" "$status" +` + output, err := exec.Command("sh", "-c", script, "sh", lib, changelogPath).CombinedOutput() + if err != nil { + t.Fatalf("release helper variable isolation error = %v\noutput:\n%s", err, output) + } + want := strings.Join([]string{ + "caller-version", + "caller-expected", + "caller-actual", + "caller-candidate", + "caller-baseline", + "caller-changelog", + "caller-semver", + "caller-output", + "caller-tmp", + "caller-status", + }, "\n") + "\n" + if string(output) != want { + t.Fatalf("release helpers changed caller variables:\ngot:\n%s\nwant:\n%s", output, want) + } +} + func TestReleaseNpmPackingIgnoresLifecycleScripts(t *testing.T) { sourceRoot, err := filepath.Abs(filepath.Join("..", "..")) if err != nil { From b5abe6d3284ee93da428c542719eba13f9c549ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Thu, 16 Jul 2026 10:15:26 +0800 Subject: [PATCH 10/14] fix(release): preserve latest main integration --- .github/workflows/ci.yml | 187 ++++++------------- .github/workflows/release.yml | 48 ++++- CHANGELOG.md | 8 +- Makefile | 5 +- build/homebrew.rb.tmpl | 42 +---- scripts/README.md | 2 + scripts/release/post-goreleaser.sh | 41 +++-- scripts/release/sync-to-gitee.sh | 205 +++------------------ scripts/release/verify-package-managers.sh | 37 ++-- test/scripts/gitee_sync_test.go | 46 ++--- test/scripts/package_script_test.go | 33 ++-- 11 files changed, 214 insertions(+), 440 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f83fce6b..529b3434 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,22 @@ jobs: # version: v1.64.8 # args: ./... + actionlint: + name: Actionlint + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Check GitHub Actions workflows + run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 + test: name: Test runs-on: ubuntu-latest @@ -63,17 +79,10 @@ jobs: # package timeout aligned with the macOS race job so the Linux runner's # five-minute default does not expire while that gate is still making # progress. - run: | - go test -v -race -count=1 -timeout=10m \ - ./cmd/... \ - ./internal/... \ - ./pkg/... \ - ./test/unit/... - go test -v -count=1 -timeout=5m \ - ./test/cli/... + run: go test -v -race -count=1 -timeout=10m ./cmd/... ./internal/... - - name: Test engineering scripts - run: go test -v -count=1 -timeout=5m ./test/scripts/... + - name: Test release scripts + run: go test -v -count=1 -timeout=5m ./test/scripts test-darwin: name: Test (macOS auth/keychain) @@ -415,123 +424,22 @@ jobs: - name: Run edition contract tests run: go test -v -count=1 ./pkg/editiontest/... - code-check: - name: Code Check - needs: [lint, test, test-darwin, test-windows, coverage, policy, edition-tests] - if: ${{ always() }} - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: {} - steps: - - name: Verify code checks - env: - LINT_RESULT: ${{ needs.lint.result }} - TEST_RESULT: ${{ needs.test.result }} - TEST_DARWIN_RESULT: ${{ needs.test-darwin.result }} - TEST_WINDOWS_RESULT: ${{ needs.test-windows.result }} - COVERAGE_RESULT: ${{ needs.coverage.result }} - POLICY_RESULT: ${{ needs.policy.result }} - EDITION_TESTS_RESULT: ${{ needs.edition-tests.result }} - run: | - set -eu - failed=0 - for check in \ - "Lint:$LINT_RESULT" \ - "Test:$TEST_RESULT" \ - "Test (macOS auth/keychain):$TEST_DARWIN_RESULT" \ - "Test (Windows):$TEST_WINDOWS_RESULT" \ - "Coverage:$COVERAGE_RESULT" \ - "Policy Check:$POLICY_RESULT" \ - "Edition Contract Tests:$EDITION_TESTS_RESULT" - do - name="${check%%:*}" - result="${check#*:}" - printf '%s: %s\n' "$name" "$result" - if [ "$result" != "success" ]; then - failed=1 - fi - done - test "$failed" -eq 0 - - command-compatibility: - name: Command Compatibility - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - actions: read - contents: read - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Fetch official release tags - run: | - git fetch --force --no-tags \ - https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ - '+refs/tags/v*:refs/tags/v*' - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Check command compatibility - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} - PUSH_BEFORE_SHA: ${{ github.event.before }} - run: | - set -eu - base_ref="$PUSH_BEFORE_SHA" - if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - base_ref="$PR_BASE_SHA" - fi - stable_ref="$(git tag --merged HEAD --list 'v*' --sort=-version:refname | awk '/^v[0-9]+\.[0-9]+\.[0-9]+$/ { print; exit }')" - test -n "$base_ref" - test -n "$stable_ref" - stable_commit="$(git rev-parse "$stable_ref^{commit}")" - ./scripts/release/verify-delivered-stable.sh "$stable_ref" "$stable_commit" - ./scripts/policy/check-command-compatibility.sh \ - --base-ref "$base_ref" \ - --stable-ref "$stable_ref" - - cli-smoke: - name: CLI Smoke - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Run CLI smoke tests - run: go test -count=1 -timeout=5m ./test/smoke/... - - mock-mcp-smoke: - name: Mock MCP Smoke - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Run Mock MCP smoke tests - run: go test -count=1 -timeout=5m ./test/mock_mcp/... - ci-gate: name: CI Gate - needs: [code-check, command-compatibility, cli-smoke, mock-mcp-smoke] + needs: + - lint + - actionlint + - test + - test-darwin + - test-windows + - coverage + - coverage-darwin + - coverage-windows + - policy + - interface-integrity + - cli-smoke + - mock-mcp-smoke + - edition-tests if: ${{ always() }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -539,18 +447,35 @@ jobs: steps: - name: Verify required checks env: - CODE_CHECK_RESULT: ${{ needs.code-check.result }} - COMMAND_COMPATIBILITY_RESULT: ${{ needs.command-compatibility.result }} + LINT_RESULT: ${{ needs.lint.result }} + ACTIONLINT_RESULT: ${{ needs.actionlint.result }} + TEST_RESULT: ${{ needs.test.result }} + TEST_DARWIN_RESULT: ${{ needs.test-darwin.result }} + TEST_WINDOWS_RESULT: ${{ needs.test-windows.result }} + COVERAGE_RESULT: ${{ needs.coverage.result }} + COVERAGE_DARWIN_RESULT: ${{ needs.coverage-darwin.result }} + COVERAGE_WINDOWS_RESULT: ${{ needs.coverage-windows.result }} + POLICY_RESULT: ${{ needs.policy.result }} + INTERFACE_INTEGRITY_RESULT: ${{ needs.interface-integrity.result }} CLI_SMOKE_RESULT: ${{ needs.cli-smoke.result }} MOCK_MCP_SMOKE_RESULT: ${{ needs.mock-mcp-smoke.result }} + EDITION_TESTS_RESULT: ${{ needs.edition-tests.result }} run: | - set -eu failed=0 for check in \ - "Code Check:$CODE_CHECK_RESULT" \ - "Command Compatibility:$COMMAND_COMPATIBILITY_RESULT" \ + "Lint:$LINT_RESULT" \ + "Actionlint:$ACTIONLINT_RESULT" \ + "Test:$TEST_RESULT" \ + "Test (macOS auth/keychain):$TEST_DARWIN_RESULT" \ + "Test (Windows):$TEST_WINDOWS_RESULT" \ + "Coverage:$COVERAGE_RESULT" \ + "Coverage (macOS):$COVERAGE_DARWIN_RESULT" \ + "Coverage (Windows):$COVERAGE_WINDOWS_RESULT" \ + "Policy Check:$POLICY_RESULT" \ + "Interface Integrity:$INTERFACE_INTEGRITY_RESULT" \ "CLI Smoke:$CLI_SMOKE_RESULT" \ - "Mock MCP Smoke:$MOCK_MCP_SMOKE_RESULT" + "Mock MCP Smoke:$MOCK_MCP_SMOKE_RESULT" \ + "Edition Contract Tests:$EDITION_TESTS_RESULT" do name="${check%%:*}" result="${check#*:}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ba7466a..d059e927 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,6 +49,16 @@ jobs: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git \ '+refs/tags/v*:refs/tags/v*' + - name: Check Homebrew PR automation token + if: ${{ github.repository_owner == 'DingTalk-Real-AI' }} + env: + HOMEBREW_PR_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }} + run: | + if [ -z "${HOMEBREW_PR_TOKEN:-}" ]; then + echo "HOMEBREW_PR_TOKEN is required to open Formula PRs from official releases" >&2 + exit 1 + fi + - name: Validate release contract id: contract run: | @@ -526,6 +536,30 @@ jobs: ./scripts/release/verify-github-release-assets.sh "$GITHUB_REF_NAME" ./scripts/release/verify-github-tag-authority.sh "$GITHUB_REF_NAME" "$GITHUB_SHA" + - name: Open stable Homebrew formula PR + if: ${{ github.repository_owner == 'DingTalk-Real-AI' && needs.release-contract.outputs.channel == 'stable' }} + run: ./scripts/release/publish-homebrew-formula.sh + env: + DWS_TAP_REPO_URL: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git + DWS_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }} + DWS_TAP_PR_REPOSITORY: ${{ github.repository }} + DWS_TAP_PR_BRANCH: "automation/homebrew-${{ github.ref_name }}" + DWS_TAP_PR_TITLE: "chore: update Homebrew formula for ${{ github.ref_name }}" + DWS_TAP_COMMIT_MESSAGE: "chore: update formula for ${{ github.ref_name }}" + + - name: Open beta Homebrew formula PR + if: ${{ github.repository_owner == 'DingTalk-Real-AI' && needs.release-contract.outputs.channel == 'prerelease' }} + run: ./scripts/release/publish-homebrew-formula.sh + env: + DWS_FORMULA_SOURCE: dist/homebrew/dingtalk-workspace-cli-beta.rb + DWS_TAP_FORMULA_PATH: Formula/dingtalk-workspace-cli-beta.rb + DWS_TAP_REPO_URL: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git + DWS_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }} + DWS_TAP_PR_REPOSITORY: ${{ github.repository }} + DWS_TAP_PR_BRANCH: "automation/homebrew-beta-${{ github.ref_name }}" + DWS_TAP_PR_TITLE: "chore: update Homebrew beta formula for ${{ github.ref_name }}" + DWS_TAP_COMMIT_MESSAGE: "chore: update beta formula for ${{ github.ref_name }}" + - name: Reverify exact immutable npm package run: ./scripts/release/verify-package-managers.sh --npm-only --expected-version "$GITHUB_REF_NAME" @@ -740,16 +774,15 @@ jobs: DWS_REQUIRE_OSS: ${{ github.repository_owner == 'DingTalk-Real-AI' && '1' || '0' }} mirror-gitee-release: - # Keep the optional cross-border fallback out of publish-release so all - # pre-sync work has an independently provable budget. + name: Mirror immutable release to Gitee if: ${{ vars.ENABLE_GITEE_UPLOAD_FALLBACK == 'true' }} - needs: - - release - - publish-release + needs: [release, publish-channels] runs-on: ubuntu-latest timeout-minutes: 120 + permissions: + contents: read steps: - - name: Check out repository + - name: Check out sealed release source uses: actions/checkout@v4 timeout-minutes: 5 @@ -761,8 +794,7 @@ jobs: path: dist - name: Mirror release to Gitee (China) - if: ${{ vars.ENABLE_GITEE_UPLOAD_FALLBACK == 'true' }} - timeout-minutes: 20 + timeout-minutes: 100 run: ./scripts/release/sync-to-gitee.sh env: VERSION: ${{ github.ref_name }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 00ff178c..1249931e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,15 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th ## [Unreleased] +### Added + +- **Declarative shortcut commands** (#592) — adds 366 `dws +` shortcuts across 16 services, including one-to-one MCP wrappers and multi-step smart workflows. Shortcuts publish stable Agent-visible contracts with named flags, validation and confirmation metadata, dry-run protection for writes, catalog/help routing, and optional local YAML extensions and usage recording. +- **Sheet imports and Aitable workflow writes** (#624) — adds `dws sheet import` / `sheet import create` for converting local xlsx/xls files into new online sheets, `sheet import get` for polling import tasks, and `dws aitable workflow create/update` for applying validated `workflow-dsl/v1` definitions, with matching reviewed Agent Schema and bundled Skill guidance. +- **Official multi-platform Homebrew channel** — stable `Formula/dingtalk-workspace-cli.rb` and keg-only `Formula/dingtalk-workspace-cli-beta.rb` live in this repository and select signed macOS Intel/Apple Silicon or Linux amd64/arm64 artifacts at install time. Stable and beta releases open isolated Formula update PRs after final artifact signing, so beta never replaces the stable Formula. Agent Skills stay under `pkgshare` without mutating the user's home directory, and both tracks are covered by the six-channel post-release verifier. + ### Changed -- **Guarded prerelease and stable automation** — adds the guided `dws-release` entry for one-command CHANGELOG preparation, validation-only and annotated-tag publication flows; promotes only an explicitly validated beta; verifies command-tree compatibility and all six packaged binaries; and serializes immutable GitHub Release, npm channel, OSS, and optional Gitee delivery with fail-closed recovery checks. +- **Guarded prerelease and stable automation** — adds the guided `dws-release` entry for one-command CHANGELOG preparation, validation-only and annotated-tag publication flows; promotes only an explicitly validated beta; verifies command-tree compatibility and all six packaged binaries; and serializes immutable GitHub Release, npm channel, OSS, Homebrew, and optional Gitee delivery with fail-closed recovery checks. ### Fixed diff --git a/Makefile b/Makefile index 27a423aa..db5b9810 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,11 @@ GO ?= go REMOTE ?= PUBLISH ?= 0 YES ?= 0 +DWS_POLICY_TMPDIR ?= $(CURDIR)/.worktrees/policy-tmp +POLICY_GOTMPDIR ?= $(DWS_POLICY_TMPDIR)/go +POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)" -.PHONY: all help build rebuild test lint fmt policy edition-test test-schema-agent-examples generate-schema generate-schema-agent-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 lint 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 cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema generate-schema-agent-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 diff --git a/build/homebrew.rb.tmpl b/build/homebrew.rb.tmpl index 9575adcc..0b10c88c 100644 --- a/build/homebrew.rb.tmpl +++ b/build/homebrew.rb.tmpl @@ -2,7 +2,6 @@ class __CLASS_NAME__ < Formula desc "Install locally built DingTalk workspace CLI artifacts for verification" homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli" url "__ARCHIVE_URL__" - version "__VERSION__" sha256 "__ARCHIVE_SHA256__" license "Apache-2.0" __KEG_ONLY_LINE__ @@ -31,42 +30,11 @@ __KEG_ONLY_LINE__ end end - def post_install - require "fileutils" - - skill_root = pkgshare/"skills/dws" - entries = Dir["#{skill_root}/*"] - return if entries.empty? - - skill_home_override = "__SKILL_HOME_OVERRIDE__" - skill_home = skill_home_override.empty? ? Dir.home : skill_home_override - targets = [ - Pathname.new(File.join(skill_home, ".agents/skills/dws")), - Pathname.new(File.join(skill_home, ".claude/skills/dws")), - Pathname.new(File.join(skill_home, ".cursor/skills/dws")), - Pathname.new(File.join(skill_home, ".qoder/skills/dws")), - Pathname.new(File.join(skill_home, ".qoderwork/skills/dws")), - Pathname.new(File.join(skill_home, ".gemini/skills/dws")), - Pathname.new(File.join(skill_home, ".codex/skills/dws")), - Pathname.new(File.join(skill_home, ".github/skills/dws")), - Pathname.new(File.join(skill_home, ".windsurf/skills/dws")), - Pathname.new(File.join(skill_home, ".augment/skills/dws")), - Pathname.new(File.join(skill_home, ".cline/skills/dws")), - Pathname.new(File.join(skill_home, ".amp/skills/dws")), - Pathname.new(File.join(skill_home, ".kiro/skills/dws")), - Pathname.new(File.join(skill_home, ".trae/skills/dws")), - Pathname.new(File.join(skill_home, ".openclaw/skills/dws")), - Pathname.new(File.join(skill_home, ".hermes/skills/dws")), - ] - - targets.each_with_index do |dest, index| - parent_gate = dest.parent.parent - next if index > 0 && !parent_gate.directory? - - FileUtils.rm_rf(dest) - FileUtils.mkdir_p(dest) - FileUtils.cp_r(entries, dest) - end + def caveats + <<~EOS + Agent Skills are bundled in #{pkgshare}/skills/dws. + Run `dws skill setup` to install them into your Agent directories. + EOS end test do diff --git a/scripts/README.md b/scripts/README.md index ce386a83..179e022b 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -6,6 +6,7 @@ These repo-local entrypoints are the supported shell entrypoints for building, t - `make test`: run `go test ./...` - `make lint`: run formatting checks and required `golangci-lint` - `make fmt`: format Go source files under `cmd/`, `internal/`, and `test/` +- `make policy`: reuse the current `dws` binary and run the complete policy suite (`make build` first) - `make package`: build all release artifacts locally - `make release-pre VERSION=vX.Y.Z-beta.N`: validate a prerelease (`PUBLISH=1` pushes its tag) - `make release-stable VERSION=vX.Y.Z FROM_BETA=vX.Y.Z-beta.N`: validate a stable promotion (`PUBLISH=1` pushes its tag) @@ -16,5 +17,6 @@ Script groups: - Product convenience installers: `./scripts/install-devapp.sh`, `./scripts/install-devapp.ps1`, `./scripts/install-event.sh` - Dev helpers: `./scripts/dev/build.sh`, `./scripts/dev/lint.sh`, `./scripts/dev/ci-local.sh`, `./scripts/dev/run-mock-e2e.sh`, `./scripts/dev/coverage.sh` - Policy checks: `./scripts/policy/check-schema-command-registry.sh`, `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-command-surface.sh`, `./scripts/policy/check-command-compatibility.sh --base-ref --stable-ref `, `./scripts/policy/check-schema-catalog.sh`, `./scripts/policy/check-schema-binary.sh`, `./scripts/policy/check-open-source-assets.sh` +- Policy runtime files: set `DWS_POLICY_TMPDIR` to override the default `.worktrees/policy-tmp` workspace - Release entrypoint and contract: `./scripts/release/release.sh`, `./scripts/release/release-contract.sh`; operator guide: [`docs/releasing.md`](../docs/releasing.md) - Release packaging helpers: `./scripts/release/post-goreleaser.sh`, `./scripts/release/stage-npm-package.sh`, `./scripts/release/pack-npm-package.sh`, `./scripts/release/verify-delivered-stable.sh`, `./scripts/release/verify-release-artifacts.sh`, `./scripts/release/verify-github-release-assets.sh`, `./scripts/release/verify-github-tag-authority.sh`, `./scripts/release/verify-package-managers.sh`, `./scripts/release/publish-homebrew-formula.sh` diff --git a/scripts/release/post-goreleaser.sh b/scripts/release/post-goreleaser.sh index d74f9143..af2d8440 100755 --- a/scripts/release/post-goreleaser.sh +++ b/scripts/release/post-goreleaser.sh @@ -99,18 +99,15 @@ stage_npm_package() { render_homebrew_formula() { class_name="$1" - formula_version="$2" - archive_url="$3" - skills_url="$4" - archive_sha="$5" - skills_sha="$6" - keg_only_line="$7" - output_path="$8" + archive_url="$2" + skills_url="$3" + archive_sha="$4" + skills_sha="$5" + keg_only_line="$6" + output_path="$7" sed \ -e "s|__CLASS_NAME__|$class_name|g" \ - -e "s|__VERSION__|$formula_version|g" \ - -e "s|__SKILL_HOME_OVERRIDE__||g" \ -e "s|__ARCHIVE_URL__|$archive_url|g" \ -e "s|__ARCHIVE_SHA256__|$archive_sha|g" \ -e "s|__SKILLS_URL__|$skills_url|g" \ @@ -184,7 +181,6 @@ stage_homebrew_formula() { render_homebrew_formula \ "DingtalkWorkspaceCliLocal" \ - "$version" \ "file://$archive_path" \ "file://$DIST_DIR/dws-skills.zip" \ "$archive_sha" \ @@ -192,12 +188,27 @@ stage_homebrew_formula() { ' keg_only "Local verification formula to avoid linking conflicts"' \ "$formula_dir/dingtalk-workspace-cli-local.rb" - render_homebrew_formula \ - "DingtalkWorkspaceCli" \ + formula_class="DingtalkWorkspaceCli" + formula_path="$formula_dir/dingtalk-workspace-cli.rb" + keg_only_line="" + channel_caveat="" + case "$version" in + *-*) + formula_class="DingtalkWorkspaceCliBeta" + formula_path="$formula_dir/dingtalk-workspace-cli-beta.rb" + keg_only_line=' keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli"' + channel_caveat=' This beta is keg-only. Add #{opt_bin} to PATH to use its `dws` binary.' + ;; + esac + + render_homebrew_release_formula \ + "$formula_class" \ "$version" \ - "$release_url_base/$archive_name" \ - "$release_url_base/$skills_name" \ - "$archive_sha" \ + "$release_url_base" \ + "$(sha256_file "$darwin_amd64")" \ + "$(sha256_file "$darwin_arm64")" \ + "$(sha256_file "$linux_amd64")" \ + "$(sha256_file "$linux_arm64")" \ "$skills_sha" \ "$keg_only_line" \ "$channel_caveat" \ diff --git a/scripts/release/sync-to-gitee.sh b/scripts/release/sync-to-gitee.sh index 6ce19325..f2382825 100755 --- a/scripts/release/sync-to-gitee.sh +++ b/scripts/release/sync-to-gitee.sh @@ -24,7 +24,7 @@ # Gating: if GITEE_TOKEN / GITEE_USER / GITEE_REPO are unset, exit 0 with a # notice so the step can live in release.yml without breaking forks. -set -euo pipefail +set -eu SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" DIST_DIR="${DIST_DIR:-dist}" @@ -69,7 +69,7 @@ missing="" [ -z "${GITEE_REPO:-}" ] && missing="$missing GITEE_REPO" if [ -n "$missing" ]; then if [ "${DWS_REQUIRE_GITEE:-0}" = "1" ]; then - echo "❌ Gitee mirror sync is enabled but credentials are missing:${missing}" >&2 + echo "error: Gitee mirror sync is enabled but credentials are missing:${missing}" >&2 exit 1 fi echo "ℹ️ Gitee mirror sync skipped — missing:${missing}" @@ -100,7 +100,6 @@ bounded_max_time() { } VERSION="${VERSION:-$(git describe --tags --always 2>/dev/null || echo dev)}" -DWS_PACKAGE_DIST_DIR="$DIST_DIR" "$SCRIPT_DIR/verify-release-artifacts.sh" "$VERSION" OWNER="${GITEE_REPO%%/*}" NAME="${GITEE_REPO##*/}" base="${GITEE_API}/repos/${OWNER}/${NAME}" @@ -126,29 +125,6 @@ api_get() { --max-time "$max_time" "$@" } -gitee_commit="$(gitee_tag_commit || true)" -if [ -n "$gitee_commit" ] && [ "$gitee_commit" != "$target_commit" ]; then - echo "❌ Existing Gitee tag ${VERSION} points to ${gitee_commit}, expected ${target_commit}; refusing to move it." >&2 - exit 1 -fi -if [ -z "$gitee_commit" ]; then - echo " Creating Gitee tag ${VERSION} -> ${target_commit}" - git push "$git_remote" "refs/tags/${VERSION}:refs/tags/${VERSION}" >/dev/null -else - echo " Gitee tag ${VERSION} already points to ${target_commit}." -fi - -for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do - gitee_commit="$(gitee_tag_commit || true)" - [ "$gitee_commit" = "$target_commit" ] && break - sleep 5 -done -[ "$gitee_commit" = "$target_commit" ] || { - echo "❌ Gitee tag ${VERSION} is not aligned after push: got ${gitee_commit:-}, want ${target_commit}" >&2 - exit 1 -} -echo " Gitee tag ${VERSION} is aligned." - # ── Resolve or create the Gitee release for this tag ────────────────────────── rel_json="$(api_get "$GITEE_RELEASE_LOOKUP_MAX_TIME" \ "${base}/releases/tags/${VERSION}?access_token=${GITEE_TOKEN}" 2>/dev/null || true)" @@ -171,165 +147,26 @@ fi [ -n "$release_id" ] || { echo "❌ Could not get/create Gitee release for ${VERSION}. Response: ${rel_json}" >&2; exit 1; } echo " Gitee release id = ${release_id}" -# ── Mirror each artifact, verifying content so re-runs self-heal ───────────── -# Pull the current attachment list (name + attach id + download url) so we can, -# per file: -# • skip it when it is already on Gitee, unique, AND byte-identical, -# • REPLACE it when present but stale (different bytes) — e.g. the darwin -# binaries are re-signed and so differ between GitHub and a prior mirror -# run, which breaks install.sh's checksums.txt verification on macOS, -# • DEDUP it when the same name has >1 attachment — a prior run that failed to -# delete (see below) left the old copy *and* an extra upload; Gitee then -# serves the OLDER one by name, so the stale binary wins. We delete every -# copy and upload one fresh. -# • upload it when missing. -# This brings the Gitee release into byte-for-byte agreement with $DIST_DIR, -# which the caller fills from the GitHub release whose checksums.txt is what -# install.sh verifies against. -# -# We list attachments via the dedicated /attach_files endpoint, NOT the release -# detail (/releases/{id}) endpoint: the latter's "assets" array omits the attach -# id, so DELETE /attach_files/{id} was previously called with an empty id and -# silently no-op'd — leaving stale + duplicate darwin binaries on Gitee. -load_assets_map() { - curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \ - "${base}/releases/${release_id}/attach_files?access_token=${GITEE_TOKEN}" \ - | python3 -c 'import json,sys -try: - data=json.load(sys.stdin) - rows=data if isinstance(data,list) else data.get("attach_files",[]) - for a in rows: - n=a.get("name",""); i=a.get("id",""); u=a.get("browser_download_url","") - if n and i!="": - print("%s\t%s\t%s" % (n, i, u)) -except Exception: - sys.exit(1)' -} - -assets_map="$(load_assets_map)" || { - echo "❌ Could not list Gitee release attachments; refusing a blind upload." >&2 - exit 1 -} - -sha256_of() { # sha256 of a file ($1) or, with no arg, of stdin - if command -v sha256sum >/dev/null 2>&1; then sha256sum ${1:+"$1"} | awk '{print $1}'; - else shasum -a 256 ${1:+"$1"} | awk '{print $1}'; fi -} - -gitee_attach() { # upload file $1; success when the response carries a download url - file="$1" - fn="$(basename "$file")" - attempt=1 - while [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ]; do - response="$(curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_UPLOAD_MAX_TIME" \ - --retry 2 --retry-delay 5 --retry-all-errors \ - -X POST "${base}/releases/${release_id}/attach_files" \ - -F "access_token=${GITEE_TOKEN}" -F "file=@${file}" 2>&1 || true)" - if printf '%s' "$response" | grep -q '"browser_download_url"'; then - return 0 - fi - echo " ⚠ upload attempt ${attempt}/${GITEE_UPLOAD_RETRIES} failed for ${fn}: $(printf '%s' "$response" | head -c 240)" >&2 - attempt=$((attempt + 1)) - [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ] && sleep "$GITEE_UPLOAD_RETRY_DELAY" - done - return 1 -} - -gitee_delete() { # delete attachment by id $1 - curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" \ - -X DELETE "${base}/releases/${release_id}/attach_files/${1}?access_token=${GITEE_TOKEN}" \ - >/dev/null -} - -uploaded=0 -replaced=0 -skipped=0 -failed=0 -for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt; do - [ -f "$f" ] || continue - fn="$(basename "$f")" - local_sha="$(sha256_of "$f")" - # Every attach id currently carrying this name (may be >1 from a botched run). - ids="$(printf '%s\n' "$assets_map" | awk -F'\t' -v n="$fn" '$1==n {print $2}')" - aurl="$(printf '%s\n' "$assets_map" | awk -F'\t' -v n="$fn" '$1==n {print $3; exit}')" - count="$(printf '%s' "$ids" | grep -c . || true)" - - if [ "$count" -eq 0 ]; then - echo " ⬆ ${fn} (new)" - if gitee_attach "$f"; then - uploaded=$((uploaded + 1)) - else - echo " ❌ upload failed for ${fn}" >&2 - failed=$((failed + 1)) - fi - continue - fi - - if [ "$count" -eq 1 ]; then - gitee_sha="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "$aurl" 2>/dev/null | sha256_of || true)" - if [ "$gitee_sha" = "$local_sha" ]; then - echo " ✓ ${fn} already correct on Gitee — skip" - skipped=$((skipped + 1)) - continue - fi - echo " ↻ ${fn} differs on Gitee (stale) — deleting + re-uploading" - else - echo " ↻ ${fn} has ${count} copies on Gitee (dup) — deleting all + re-uploading one" - fi - - # Delete every copy, then upload exactly one fresh, correct file. - delete_failed=0 - for aid in $ids; do - if ! gitee_delete "$aid"; then - echo " ❌ failed to delete stale Gitee attachment ${aid} for ${fn}" >&2 - delete_failed=1 - fi - done - if [ "$delete_failed" -ne 0 ]; then - failed=$((failed + 1)) - continue - fi - if gitee_attach "$f"; then - replaced=$((replaced + 1)) - else - echo " ❌ re-upload failed for ${fn}" >&2 - failed=$((failed + 1)) - fi -done - -if [ "$uploaded" -eq 0 ] && [ "$replaced" -eq 0 ] && [ "$skipped" -eq 0 ]; then - echo "❌ No artifacts found to mirror. Did the build (goreleaser) run / were assets downloaded into ${DIST_DIR}?" >&2 - exit 1 +# ── Reconcile the complete, byte-verified release asset set ────────────────── +remaining="$(deadline_remaining)" || err "overall Gitee sync deadline exhausted before asset reconciliation" +reconcile_timeout="$GITEE_RECONCILE_TIMEOUT_SECONDS" +max_child_timeout=$(( remaining - GITEE_CHILD_DEADLINE_RESERVE_SECONDS )) +[ "$max_child_timeout" -gt 0 ] || err "insufficient Gitee sync budget for asset reconciliation" +if [ "$reconcile_timeout" -gt "$max_child_timeout" ]; then + reconcile_timeout="$max_child_timeout" fi -# Gitee may accept an upload before the attachment list is consistent. Re-read -# the release and require every supported asset to be unique and byte-identical. -verified=0 -for verify_attempt in 1 2 3 4 5; do - final_map="$(load_assets_map || true)" - verify_failed=0 - for f in "$DIST_DIR"/dws-*.tar.gz "$DIST_DIR"/dws-*.zip "$DIST_DIR"/checksums.txt; do - fn="$(basename "$f")" - rows="$(printf '%s\n' "$final_map" | awk -F'\t' -v n="$fn" '$1==n')" - count="$(printf '%s\n' "$rows" | grep -c . || true)" - if [ "$count" -ne 1 ]; then - verify_failed=1 - continue - fi - aurl="$(printf '%s\n' "$rows" | awk -F'\t' 'NR==1 {print $3}')" - remote_sha="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" --max-time "$GITEE_CURL_MAX_TIME" "$aurl" 2>/dev/null | sha256_of || true)" - [ "$remote_sha" = "$(sha256_of "$f")" ] || verify_failed=1 - done - if [ "$verify_failed" -eq 0 ] && [ "$failed" -eq 0 ]; then - verified=1 - break - fi - [ "$verify_attempt" -lt 5 ] && sleep 5 -done -[ "$verified" -eq 1 ] || { - echo "❌ Gitee release ${VERSION} does not contain one byte-identical copy of every release asset." >&2 - exit 1 -} -echo "✅ Gitee release ${VERSION}: uploaded ${uploaded}, replaced ${replaced}, skipped ${skipped} (already correct)." +DIST_DIR="$DIST_DIR" \ +GITEE_API="$GITEE_API" \ +GITEE_TOKEN="$GITEE_TOKEN" \ +GITEE_REPO="$GITEE_REPO" \ +GITEE_CURL_CONNECT_TIMEOUT="$GITEE_CURL_CONNECT_TIMEOUT" \ +GITEE_CURL_MAX_TIME="$GITEE_CURL_MAX_TIME" \ +GITEE_RELEASE_ID="$release_id" \ +GITEE_OVERALL_TIMEOUT_SECONDS="$reconcile_timeout" \ + "$SCRIPT_DIR/reconcile-gitee-assets.sh" + +deadline_remaining >/dev/null || err "overall Gitee sync deadline exhausted after asset reconciliation" + echo " China install: DWS_GITEE_REPO=${GITEE_REPO} \\" echo " curl -fsSL https://gitee.com/${GITEE_REPO}/raw/main/scripts/install.sh | sh" diff --git a/scripts/release/verify-package-managers.sh b/scripts/release/verify-package-managers.sh index 28ac1a2c..886afbbc 100755 --- a/scripts/release/verify-package-managers.sh +++ b/scripts/release/verify-package-managers.sh @@ -93,9 +93,9 @@ HOME_SKILL_TARGETS=" " cleanup() { if [ "$RUN_BREW" -eq 1 ] && command -v brew >/dev/null 2>&1 && [ -n "${BREW_TAP_NAME:-}" ]; then - HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ - brew uninstall --force "$BREW_TAP_NAME/dingtalk-workspace-cli-local" >/dev/null 2>&1 || true - HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + HOME="$TMP_ROOT/brew-home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + brew uninstall --force dingtalk-workspace-cli-local >/dev/null 2>&1 || true + HOME="$TMP_ROOT/brew-home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ brew untap --force "$BREW_TAP_NAME" >/dev/null 2>&1 || true fi rm -rf "$TMP_ROOT" @@ -163,45 +163,40 @@ verify_npm() { verify_brew() { need_cmd brew need_file "$FORMULA_PATH" - if [ -n "$EXPECTED_VERSION" ]; then - grep -Fq " version \"$EXPECTED_VERSION\"" "$FORMULA_PATH" || \ - err "Homebrew formula does not declare version $EXPECTED_VERSION" - fi brew_home="$TMP_ROOT/brew-home" mkdir -p "$brew_home" - seed_agent_homes "$brew_home" BREW_TAP_NAME="local/dws-package-verify-$$" say "==> verifying Homebrew formula install" - HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + brew uninstall --force dingtalk-workspace-cli-local >/dev/null 2>&1 || true + HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + brew untap --force "$BREW_TAP_NAME" >/dev/null 2>&1 || true + HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ brew tap-new --no-git "$BREW_TAP_NAME" >/dev/null tap_repo="$( - HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ brew --repository "$BREW_TAP_NAME" )" mkdir -p "$tap_repo/Formula" - sed "s|skill_home_override = \"\"|skill_home_override = \"$brew_home\"|" \ - "$FORMULA_PATH" > "$tap_repo/Formula/dingtalk-workspace-cli-local.rb" + cp "$FORMULA_PATH" "$tap_repo/Formula/dingtalk-workspace-cli-local.rb" - HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ - brew install "$BREW_TAP_NAME/dingtalk-workspace-cli-local" >/dev/null + HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + brew install -y "$BREW_TAP_NAME/dingtalk-workspace-cli-local" >/dev/null prefix="$( - HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ - brew --prefix "$BREW_TAP_NAME/dingtalk-workspace-cli-local" + HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \ + brew --prefix dingtalk-workspace-cli-local )" [ -x "$prefix/bin/dws" ] || err "brew install did not create $prefix/bin/dws" - HOME="$brew_home" "$prefix/bin/dws" --help >/dev/null + "$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" - installed_version="$(HOMEBREW_NO_AUTO_UPDATE=1 brew list --versions "$BREW_TAP_NAME/dingtalk-workspace-cli-local" | awk '{ print $2 }')" - [ "$installed_version" = "$EXPECTED_VERSION" ] || \ - err "Homebrew installed version $installed_version, expected $EXPECTED_VERSION" fi - [ "$VERIFY_SKILL_TARGETS" -eq 0 ] || verify_skill_targets "$brew_home" + need_file "$prefix/share/dingtalk-workspace-cli-local/skills/dws/SKILL.md" } need_file "$DIST_DIR/dws-skills.zip" diff --git a/test/scripts/gitee_sync_test.go b/test/scripts/gitee_sync_test.go index 72dbf5fe..9a295054 100644 --- a/test/scripts/gitee_sync_test.go +++ b/test/scripts/gitee_sync_test.go @@ -374,15 +374,15 @@ func TestReconcileGiteeAssetsHonorsTheOverallDeadline(t *testing.T) { } } -func TestGiteeWorkflowsUseImmutableTagsAndBoundedRetryBudget(t *testing.T) { +func TestGiteeReleaseWorkflowUsesImmutableTagsAndBoundedRetryBudget(t *testing.T) { mirrorPath := mustAbs(t, filepath.Join("..", "..", ".github", "workflows", "mirror-to-gitee.yml")) mirrorData, err := os.ReadFile(mirrorPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", mirrorPath, err) } mirror := string(mirrorData) - if !strings.Contains(mirror, `VERSION="$GITHUB_REF_NAME" ./scripts/release/sync-gitee-tag.sh`) { - t.Fatal("tag workflow must delegate immutable tag reconciliation to sync-gitee-tag.sh") + if strings.Contains(mirror, "tags:") || strings.Contains(mirror, "sync-gitee-tag.sh") { + t.Fatal("code mirror must not publish release tags outside the guarded release workflow") } for _, forbidden := range []string{ `git push --force "$REMOTE" "refs/tags/`, @@ -394,9 +394,8 @@ func TestGiteeWorkflowsUseImmutableTagsAndBoundedRetryBudget(t *testing.T) { } manualPath := mustAbs(t, filepath.Join("..", "..", ".github", "workflows", "sync-release-to-gitee.yml")) - manualData, err := os.ReadFile(manualPath) - if err != nil { - t.Fatalf("ReadFile(%s) error = %v", manualPath, err) + if _, err := os.Stat(manualPath); !os.IsNotExist(err) { + t.Fatalf("manual Gitee release workflow must be removed, stat error = %v", err) } syncPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "sync-to-gitee.sh")) @@ -443,27 +442,6 @@ func TestGiteeWorkflowsUseImmutableTagsAndBoundedRetryBudget(t *testing.T) { } const workflowReserveMinutes = 5 - assertWorkflowBudget( - t, - string(manualData), - "sync-gitee:", - []string{ - "name: Check out repository", - "name: Download GitHub release assets", - "name: Mirror release to Gitee (China)", - }, - workflowReserveMinutes, - ) - manualSyncStepSeconds := workflowTimeoutMinutesAfter( - t, string(manualData), "name: Mirror release to Gitee (China)", - ) * 60 - if syncSeconds+workflowReserveMinutes*60 > manualSyncStepSeconds { - t.Fatalf( - "sync deadline %ds plus reserve exceeds manual sync step %ds", - syncSeconds, manualSyncStepSeconds, - ) - } - releasePath := mustAbs(t, filepath.Join("..", "..", ".github", "workflows", "release.yml")) releaseData, err := os.ReadFile(releasePath) if err != nil { @@ -474,7 +452,7 @@ func TestGiteeWorkflowsUseImmutableTagsAndBoundedRetryBudget(t *testing.T) { string(releaseData), "mirror-gitee-release:", []string{ - "name: Check out repository", + "name: Check out sealed release source", "name: Restore finalized distribution files", "name: Mirror release to Gitee (China)", }, @@ -495,8 +473,16 @@ func TestGiteeWorkflowsUseImmutableTagsAndBoundedRetryBudget(t *testing.T) { if err != nil { t.Fatalf("ReadFile(%s) error = %v", localBuildPath, err) } - if !strings.Contains(string(localBuildData), "publish-gitee-local.sh reconcile-gitee-assets.sh") { - t.Fatal("detached-tag Gitee builds must copy the shared asset reconciler with the publisher") + if !strings.Contains(string(localBuildData), "Direct Gitee release builds are disabled") { + t.Fatal("local Gitee builds must stay disabled so only immutable GitHub assets are mirrored") + } + localPublishPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "publish-gitee-local.sh")) + localPublishData, err := os.ReadFile(localPublishPath) + if err != nil { + t.Fatalf("ReadFile(%s) error = %v", localPublishPath, err) + } + if !strings.Contains(string(localPublishData), "Direct Gitee artifact publication is disabled") { + t.Fatal("direct local Gitee publication must stay disabled") } } diff --git a/test/scripts/package_script_test.go b/test/scripts/package_script_test.go index 75e33269..0d488084 100644 --- a/test/scripts/package_script_test.go +++ b/test/scripts/package_script_test.go @@ -186,10 +186,9 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) { formulaText := string(formulaData) for _, want := range []string{ "class DingtalkWorkspaceCliLocal < Formula", - "version \"1.2.3\"", - "skill_home_override = \"\"", "resource \"skills\" do", "Install locally built DingTalk workspace CLI artifacts for verification", + "Agent Skills are bundled in #{pkgshare}/skills/dws", } { if !strings.Contains(formulaText, want) { t.Fatalf("formula missing %q:\n%s", want, formulaText) @@ -204,8 +203,14 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) { releaseFormulaText := string(releaseFormulaData) for _, want := range []string{ "class DingtalkWorkspaceCli < Formula", - "version \"1.2.3\"", - "https://downloads.example.com/dws/releases/v1.2.3/" + archiveName, + `desc "Automate DingTalk workspace tasks from the terminal"`, + `version "1.2.3"`, + "on_macos do", + "on_linux do", + "https://downloads.example.com/dws/releases/v1.2.3/dws-darwin-amd64.tar.gz", + "https://downloads.example.com/dws/releases/v1.2.3/dws-darwin-arm64.tar.gz", + "https://downloads.example.com/dws/releases/v1.2.3/dws-linux-amd64.tar.gz", + "https://downloads.example.com/dws/releases/v1.2.3/dws-linux-arm64.tar.gz", "https://downloads.example.com/dws/releases/v1.2.3/dws-skills.zip", } { if !strings.Contains(releaseFormulaText, want) { @@ -375,7 +380,7 @@ func TestPostGoreleaserBuildsVersionedBetaFormula(t *testing.T) { "dws-linux-amd64.tar.gz", "dws-linux-arm64.tar.gz", }) - env := postGoreleaserEnv(t, distDir, "https://downloads.example.com/dws/releases/v1.2.3-beta.4") + env := postGoreleaserEnv(t, distDir, "v1.2.3-beta.4", "https://downloads.example.com/dws/releases/v1.2.3-beta.4") for i, value := range env { if strings.HasPrefix(value, "DWS_PACKAGE_VERSION=") { env[i] = "DWS_PACKAGE_VERSION=v1.2.3-beta.4" @@ -792,6 +797,10 @@ func TestReleaseWorkflowUsesAppleCodesignBeforePublication(t *testing.T) { "Publish or reuse immutable GitHub Release", "gh release upload", "Publish missing version to npm channel", + "Open stable Homebrew formula PR", + "Open beta Homebrew formula PR", + "DingTalk-Real-AI/dingtalk-workspace-cli.git", + "secrets.HOMEBREW_PR_TOKEN", } { if !strings.Contains(publishSection, required) { t.Errorf("post-verification publication stage is missing %q", required) @@ -828,14 +837,14 @@ func TestReleaseWorkflowOpensHomebrewPROnlyForOfficialStableTags(t *testing.T) { if start == -1 { t.Fatal("release workflow is missing the stable Homebrew PR step") } - end := strings.Index(workflow[start:], "- name: Mirror release to Gitee") + end := strings.Index(workflow[start:], "- name: Open beta Homebrew formula PR") if end == -1 { - t.Fatal("release workflow is missing the post-Homebrew Gitee step") + t.Fatal("release workflow is missing the beta Homebrew PR step after the stable step") } section := workflow[start : start+end] for _, required := range []string{ "github.repository_owner == 'DingTalk-Real-AI'", - "!contains(github.ref_name, '-')", + "needs.release-contract.outputs.channel == 'stable'", "./scripts/release/publish-homebrew-formula.sh", "secrets.HOMEBREW_PR_TOKEN", "DWS_TAP_PR_REPOSITORY", @@ -848,7 +857,7 @@ func TestReleaseWorkflowOpensHomebrewPROnlyForOfficialStableTags(t *testing.T) { if strings.Contains(section, "secrets.GITHUB_TOKEN") { t.Error("Homebrew Formula PRs must use the dedicated token so their CI is triggered") } - stableNPM := strings.Index(workflow, "- name: Publish stable to npm") + stableNPM := strings.Index(workflow, "- name: Publish missing version to npm channel") if stableNPM == -1 || start > stableNPM { t.Fatal("Homebrew PR creation must run before npm so a failure is safely rerunnable") } @@ -871,14 +880,14 @@ func TestReleaseWorkflowOpensVersionedHomebrewPRForBetaTags(t *testing.T) { if start == -1 { t.Fatal("release workflow is missing the beta Homebrew PR step") } - end := strings.Index(workflow[start:], "- name: Sync release to China OSS mirror") + end := strings.Index(workflow[start:], "- name: Reverify exact immutable npm package") if end == -1 { - t.Fatal("release workflow is missing the post-Homebrew OSS step") + t.Fatal("release workflow is missing the post-Homebrew npm verification step") } section := workflow[start : start+end] for _, required := range []string{ "github.repository_owner == 'DingTalk-Real-AI'", - "contains(github.ref_name, '-')", + "needs.release-contract.outputs.channel == 'prerelease'", "dist/homebrew/dingtalk-workspace-cli-beta.rb", "Formula/dingtalk-workspace-cli-beta.rb", "secrets.HOMEBREW_PR_TOKEN", From ede677e413baa7e26e0b4035882b9d177279c237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Thu, 16 Jul 2026 10:26:07 +0800 Subject: [PATCH 11/14] fix(ci): align interface coverage and baseline --- cmd/interface-snapshot/main_test.go | 4 +- internal/interfacesnapshot/compare_test.go | 16 +- internal/interfacesnapshot/snapshot_test.go | 6 +- test/fixtures/cli-interface-baseline.txt | 1116 ++++++++++++++++++- 4 files changed, 1112 insertions(+), 30 deletions(-) diff --git a/cmd/interface-snapshot/main_test.go b/cmd/interface-snapshot/main_test.go index 8b791770..1f6de835 100644 --- a/cmd/interface-snapshot/main_test.go +++ b/cmd/interface-snapshot/main_test.go @@ -22,7 +22,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot" ) -func TestRunGenerateCapturesActualRootOffline(t *testing.T) { +func TestCrossPlatformCoverageRunGenerateCapturesActualRootOffline(t *testing.T) { var stdout, stderr bytes.Buffer if exitCode := run([]string{"generate"}, &stdout, &stderr); exitCode != 0 { t.Fatalf("run(generate) exit=%d stderr=%s", exitCode, stderr.String()) @@ -56,7 +56,7 @@ func TestRunGenerateCapturesActualRootOffline(t *testing.T) { } } -func TestRunCompareUsesBothSnapshotInputsAndExitCode(t *testing.T) { +func TestCrossPlatformCoverageRunCompareUsesBothSnapshotInputsAndExitCode(t *testing.T) { current := commandSnapshot("dws") mergeBase := commandSnapshot("dws") stable := commandSnapshot("dws", "dws legacy") diff --git a/internal/interfacesnapshot/compare_test.go b/internal/interfacesnapshot/compare_test.go index f3ef67c9..5e5b0ba0 100644 --- a/internal/interfacesnapshot/compare_test.go +++ b/internal/interfacesnapshot/compare_test.go @@ -15,7 +15,7 @@ package interfacesnapshot import "testing" -func TestCompareAdmissionPolicy(t *testing.T) { +func TestCrossPlatformCoverageCompareAdmissionPolicy(t *testing.T) { base := testSnapshot( testCommand("dws"), testCommand("dws search", testFlag("query", "string", false)), @@ -115,7 +115,7 @@ func TestCompareAdmissionPolicy(t *testing.T) { } } -func TestCompareAllowsRenameWhenOldPathIsAlias(t *testing.T) { +func TestCrossPlatformCoverageCompareAllowsRenameWhenOldPathIsAlias(t *testing.T) { base := testSnapshot( testCommand("dws"), testCommand("dws search", testFlag("query", "string", false)), @@ -131,7 +131,7 @@ func TestCompareAllowsRenameWhenOldPathIsAlias(t *testing.T) { } } -func TestCompareBlocksRemovedAlias(t *testing.T) { +func TestCrossPlatformCoverageCompareBlocksRemovedAlias(t *testing.T) { base := testSnapshot( testCommand("dws"), testCommandWithAliases("dws search", []string{"find"}), @@ -147,7 +147,7 @@ func TestCompareBlocksRemovedAlias(t *testing.T) { } } -func TestCompareBlocksAliasRetargetedToIncompatibleCommand(t *testing.T) { +func TestCrossPlatformCoverageCompareBlocksAliasRetargetedToIncompatibleCommand(t *testing.T) { base := testSnapshot( testCommand("dws"), testCommandWithAliases("dws search", []string{"find"}, testFlag("query", "string", false)), @@ -164,7 +164,7 @@ func TestCompareBlocksAliasRetargetedToIncompatibleCommand(t *testing.T) { } } -func TestCompareBlocksSnapshotRuleChanges(t *testing.T) { +func TestCrossPlatformCoverageCompareBlocksSnapshotRuleChanges(t *testing.T) { base := testSnapshot(testCommand("dws")) current := testSnapshot(testCommand("dws")) current.Rules.ExcludedFlags = append(current.Rules.ExcludedFlags, "legacy") @@ -175,7 +175,7 @@ func TestCompareBlocksSnapshotRuleChanges(t *testing.T) { } } -func TestCompareBlocksCallableMetadataRegressions(t *testing.T) { +func TestCrossPlatformCoverageCompareBlocksCallableMetadataRegressions(t *testing.T) { baseFlag := testFlag("format", "string", false) baseFlag.Shorthand = "f" baseFlag.NoOpt = "json" @@ -247,7 +247,7 @@ func TestCompareBlocksCallableMetadataRegressions(t *testing.T) { } } -func TestCompareUsesEffectiveFlagsAtEveryCommandPath(t *testing.T) { +func TestCrossPlatformCoverageCompareUsesEffectiveFlagsAtEveryCommandPath(t *testing.T) { base := testSnapshot( testCommand("dws", testFlag("profile", "string", false)), testCommandWithFlagScopes( @@ -290,7 +290,7 @@ func TestCompareUsesEffectiveFlagsAtEveryCommandPath(t *testing.T) { }) } -func TestCompareAllRequiresBothReferencesToPass(t *testing.T) { +func TestCrossPlatformCoverageCompareAllRequiresBothReferencesToPass(t *testing.T) { current := testSnapshot(testCommand("dws"), testCommand("dws status")) mergeBase := testSnapshot(testCommand("dws")) stable := testSnapshot(testCommand("dws"), testCommand("dws legacy")) diff --git a/internal/interfacesnapshot/snapshot_test.go b/internal/interfacesnapshot/snapshot_test.go index fdcfa411..1d08dccc 100644 --- a/internal/interfacesnapshot/snapshot_test.go +++ b/internal/interfacesnapshot/snapshot_test.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" ) -func TestCaptureUsesStableNoiseRulesAndFlagScopes(t *testing.T) { +func TestCrossPlatformCoverageCaptureUsesStableNoiseRulesAndFlagScopes(t *testing.T) { root := &cobra.Command{Use: "dws", Version: "test"} root.PersistentFlags().String("profile", "", "profile") @@ -93,7 +93,7 @@ func TestCaptureUsesStableNoiseRulesAndFlagScopes(t *testing.T) { } } -func TestReadRejectsUnknownSnapshotFields(t *testing.T) { +func TestCrossPlatformCoverageReadRejectsUnknownSnapshotFields(t *testing.T) { input := bytes.NewBufferString(`{ "schema_version": 1, "rules": {"excluded_command_subtrees": [], "excluded_flags": []}, @@ -105,7 +105,7 @@ func TestReadRejectsUnknownSnapshotFields(t *testing.T) { } } -func TestCompareBlocksCandidateSiblingAliasCollision(t *testing.T) { +func TestCrossPlatformCoverageCompareBlocksCandidateSiblingAliasCollision(t *testing.T) { base := testSnapshot(testCommand("dws")) current := testSnapshot( testCommand("dws"), diff --git a/test/fixtures/cli-interface-baseline.txt b/test/fixtures/cli-interface-baseline.txt index f2926ffa..169f440b 100644 --- a/test/fixtures/cli-interface-baseline.txt +++ b/test/fixtures/cli-interface-baseline.txt @@ -1,7 +1,7 @@ [root] runnable: true hidden: false - commands: agoal, aisearch, aitable, api, attendance, audit, auth, calendar, chat, completion, config, contact, dev, devdoc, ding, doc, doctor, drive, event, help, live, mail, minutes, oa, pat, plugin, profile, recovery, report, schema, sheet, skill, todo, upgrade, version, wiki + commands: agoal, aisearch, aitable, api, attendance, audit, auth, calendar, chat, completion, config, contact, dev, devapp, devdoc, ding, doc, doctor, drive, event, help, live, mail, minutes, oa, pat, plugin, profile, recovery, report, schema, sheet, skill, todo, upgrade, version, wiki flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=persistent, --client-secret:string|required=false|hidden=false|no-opt=""|scope=persistent, --debug:bool|required=false|hidden=false|no-opt="true"|scope=persistent, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=persistent, --fields:string|required=false|hidden=false|no-opt=""|scope=persistent, -f/--format:string|required=false|hidden=false|no-opt=""|scope=persistent, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=persistent, --mock:bool|required=false|hidden=false|no-opt="true"|scope=persistent, -o/--output:string|required=false|hidden=true|no-opt=""|scope=persistent, --profile:string|required=false|hidden=false|no-opt=""|scope=persistent, --timeout:int|required=false|hidden=false|no-opt=""|scope=persistent, --token:string|required=false|hidden=true|no-opt=""|scope=persistent, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=persistent, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=persistent [agoal] @@ -152,9 +152,154 @@ [aitable] runnable: true hidden: false - commands: advperm, attachment, base, chart, create, dashboard, export, field, form, import, info, list, record, search, section, table, template, view, workflow + commands: +base-get, +base-list, +base-search, +chart-get, +chart-widgets-example, +dashboard-config-example, +dashboard-get, +field-get, +find-record, +form-field-list, +form-list, +form-share-get, +list-tables, +record-history-list, +record-query, +record-query-empty, +record-share-links, +record-share-url, +resolve-base, +resolve-table, +role-list, +section-list-empty, +section-list-nodes, +table-get, +template-search, +view-get, +view-get-frozen-cols, +view-get-lock, +view-get-row-height, advperm, attachment, base, chart, create, dashboard, export, field, form, import, info, list, record, search, section, table, template, view, workflow flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[aitable.+base-get] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+base-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+base-search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+chart-get] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --chart-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --dashboard-id:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+chart-widgets-example] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+dashboard-config-example] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+dashboard-get] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --dashboard-id:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+field-get] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --field-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+find-record] + runnable: true + hidden: false + flags: --base:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --table:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+form-field-list] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --view-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+form-list] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+form-share-get] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --view-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+list-tables] + runnable: true + hidden: false + flags: --base:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+record-history-list] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --offset:int|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --record-id:string|required=false|hidden=false|no-opt=""|scope=local, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+record-query] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --field-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --filters:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --record-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --sort:string|required=false|hidden=false|no-opt=""|scope=local, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+record-query-empty] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+record-share-links] + runnable: true + hidden: false + flags: --base:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --record-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --table:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --view-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+record-share-url] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --record-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --view-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+resolve-base] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+resolve-table] + runnable: true + hidden: false + flags: --base:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+role-list] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+section-list-empty] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+section-list-nodes] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+table-get] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+template-search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+view-get] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --view-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+view-get-frozen-cols] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --view-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+view-get-lock] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --view-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[aitable.+view-get-row-height] + runnable: true + hidden: false + flags: --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --table-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --view-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [aitable.advperm] runnable: true hidden: false @@ -839,9 +984,14 @@ [aitable.workflow] runnable: true hidden: false - commands: disable, enable, get, list + commands: create, disable, enable, get, list, update flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[aitable.workflow.create] + runnable: true + hidden: false + flags: --base:string|required=false|hidden=true|no-opt=""|scope=local, --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dsl:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --locale:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [aitable.workflow.disable] runnable: true hidden: false @@ -862,6 +1012,11 @@ hidden: false flags: --base:string|required=false|hidden=true|no-opt=""|scope=local, --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --offset:int|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[aitable.workflow.update] + runnable: true + hidden: false + flags: --base:string|required=false|hidden=true|no-opt=""|scope=local, --base-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dsl:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --locale:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --workflow-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [api] runnable: true hidden: false @@ -870,7 +1025,102 @@ [attendance] runnable: true hidden: false - commands: adjustment, approve, boss-check, check, checkin, class, globalsetting, group, overtime, record, report, rules, schedule, selfsetting, shift, summary, vacation + commands: +check-record, +check-result, +get-adjustment-rule, +get-approve-template, +get-checkin-record, +get-leave-records, +get-overtime-rule, +get-schedule, +get-self-setting, +get-summary, +list-approve, +list-leave-types, +my-attendance, +query-report-data, +search-adjustment-rule, +search-class, +search-group, +search-overtime-rule, +this-month, adjustment, approve, boss-check, check, checkin, class, globalsetting, group, overtime, record, report, rules, schedule, selfsetting, shift, summary, vacation + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+check-record] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+check-result] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --offset:int|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+get-adjustment-rule] + runnable: true + hidden: false + flags: --adjustment-id:int|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+get-approve-template] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --type:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+get-checkin-record] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --operator-corp-id:string|required=false|hidden=false|no-opt=""|scope=local, --operator-staff-id:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --staff-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+get-leave-records] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --leave-code:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+get-overtime-rule] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --overtime-id:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+get-schedule] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+get-self-setting] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --setting-scene:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+get-summary] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --date:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --stats-type:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+list-approve] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --types:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+list-leave-types] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+my-attendance] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+query-report-data] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --columns:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+search-adjustment-rule] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+search-class] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --filter-type:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+search-group] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --query-ble:bool|required=false|hidden=false|no-opt="true"|scope=local, --query-position:bool|required=false|hidden=false|no-opt="true"|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --type:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+search-overtime-rule] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[attendance.+this-month] + runnable: true + hidden: false flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited [attendance.adjustment] @@ -1212,7 +1462,107 @@ [calendar] runnable: true hidden: false - commands: acl, attachment, attendee, book, busy, event, room + commands: +agenda, +attendee-list, +book, +book-list, +book-search, +cancel-event, +conflicts, +free, +free-slots, +freebusy, +invite, +my-free, +next-event, +reschedule, +room-groups, +room-search, +suggest-time, +today, +tomorrow, +week, acl, attachment, attendee, book, busy, event, room + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+agenda] + runnable: true + hidden: false + flags: --calendar-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+attendee-list] + runnable: true + hidden: false + flags: --calendar-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --event:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+book] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --title:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --with:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+book-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+book-search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+cancel-event] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --event:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+conflicts] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --in-days:int|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+free] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --who:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+free-slots] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --from:int|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --in-days:int|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:int|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+freebusy] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --rooms:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+invite] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --event:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --with:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+my-free] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+next-event] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+reschedule] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --event:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+room-groups] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+room-search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --room-name:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+suggest-time] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --duration:string|required=false|hidden=false|no-opt=""|scope=local, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --with:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+today] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+tomorrow] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[calendar.+week] + runnable: true + hidden: false flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited [calendar.acl] @@ -1376,10 +1726,220 @@ [chat] runnable: true hidden: false - commands: bot, category, chmod, clear-all-red-point, clear-messages, clear-red-point, conversation-info, data-auth, group, group-mute, group-mute-member, group-role, hide, list-all-conversations, list-top-conversations, mark-read, mark-unread, media, message, mute, mute-at-all, mute-red-envelope, search, search-common, set-top, text + commands: +at-me, +bot-find, +bot-search, +broadcast, +category-create, +category-delete, +category-list, +category-rename, +chat-bots, +chat-dismiss, +chat-invite-url, +chat-list-all, +chat-list-join-requests, +chat-list-mine, +chat-mute, +chat-role-add, +chat-role-list, +chat-role-query-user, +chat-role-set-user, +chat-role-update, +chat-search, +chat-set-admin, +chat-set-history, +chat-update-alias, +chat-update-nick, +conversation-clear-all-red-point, +conversation-info, +conversation-list, +conversation-list-top, +dm, +group-members, +messages-list-direct, +messages-list-pin, +messages-list-unread-conversations, +messages-mget, +messages-query-send-status, +messages-read-status, +messages-send-by-webhook, +messages-update-card, +my-groups, +send-to-group, +unread-chats, bot, category, chmod, clear-all-red-point, clear-messages, clear-red-point, conversation-info, data-auth, group, group-mute, group-mute-member, group-role, hide, list-all-conversations, list-top-conversations, mark-read, mark-unread, media, message, mute, mute-at-all, mute-red-envelope, search, search-common, set-top, text aliases: im flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[chat.+at-me] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --days:int|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+bot-find] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+bot-search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:int|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+broadcast] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+category-create] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --title:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+category-delete] + runnable: true + hidden: false + flags: --category-id:int|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+category-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+category-rename] + runnable: true + hidden: false + flags: --category-id:int|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --title:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-bots] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-dismiss] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-invite-url] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --expires-seconds:int|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-list-all] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-list-join-requests] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-list-mine] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --exclude-muted:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --role:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-mute] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --off:bool|required=false|hidden=false|no-opt="true"|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-role-add] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-role-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-role-query-user] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-role-set-user] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --role-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-role-update] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --role-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --exclude-muted:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-set-admin] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --off:bool|required=false|hidden=false|no-opt="true"|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-set-history] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --option:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-update-alias] + runnable: true + hidden: false + flags: --alias-title:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+chat-update-nick] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --nick:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+conversation-clear-all-red-point] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+conversation-info] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --open-dingtalk-id:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+conversation-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:int|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --exclude-muted:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+conversation-list-top] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:int|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --exclude-muted:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+dm] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+group-members] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+messages-list-direct] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --forward:bool|required=false|hidden=false|no-opt="true"|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --open-dingtalk-id:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --time:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+messages-list-pin] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --open-conversation-id:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:int|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+messages-list-unread-conversations] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --count:int|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --exclude-muted:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+messages-mget] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --msg-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+messages-query-send-status] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --open-task-id:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+messages-read-status] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --conversation-id:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --message-id:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+messages-send-by-webhook] + runnable: true + hidden: false + flags: --at-all:bool|required=false|hidden=false|no-opt="true"|scope=local, --at-mobiles:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --at-users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --title:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+messages-update-card] + runnable: true + hidden: false + flags: --biz-id:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --content:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --flow-status:int|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+my-groups] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --type:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+send-to-group] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[chat.+unread-chats] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --count:int|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --exclude-muted:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [chat.bot] runnable: true hidden: false @@ -1979,9 +2539,79 @@ [contact] runnable: true hidden: false - commands: dept, label, relation, user + commands: +by-mobile, +dept-members, +list-dept-members, +list-followings, +list-role-members, +list-roles, +list-sub-depts, +lookup, +me, +org, +resolve-dept, +search-mobile, +search-user, +team, dept, label, relation, user flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[contact.+by-mobile] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mobile:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+dept-members] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dept:string|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+list-dept-members] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --depts:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+list-followings] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+list-role-members] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+list-roles] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+list-sub-depts] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dept:int|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+lookup] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+me] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+org] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+resolve-dept] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+search-mobile] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mobile:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+search-user] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[contact.+team] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [contact.dept] runnable: true hidden: false @@ -2353,6 +2983,107 @@ hidden: false flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --keyword:string|required=false|hidden=true|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --size:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[devapp] + runnable: false + hidden: false + commands: +create, +delete, +disable, +enable, +event-list, +get, +list, +member-add, +member-list, +member-remove, +permission-list, +robot-get, +update, +version-check-approval, +version-get, +version-list, +version-status, +webapp-config, +webapp-get + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+create] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --desc:string|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --icon-media-id:string|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+delete] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+disable] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+enable] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+event-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --keyword:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page-size:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+get] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+list] + runnable: true + hidden: false + flags: --app-group-id:int|required=false|hidden=false|no-opt=""|scope=local, --app-key:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --creator:string|required=false|hidden=false|no-opt=""|scope=local, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --develop-type:int|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --filter-cool-app:int|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page-size:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --robot-name:string|required=false|hidden=false|no-opt=""|scope=local, --sort-order:string|required=false|hidden=false|no-opt=""|scope=local, --sort-type:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+member-add] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --member-type:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, --user-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+member-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+member-remove] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --member-type:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, --user-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+permission-list] + runnable: true + hidden: false + flags: --api-status:string|required=false|hidden=false|no-opt=""|scope=local, --auth-status:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --keyword:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page-size:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --scope-type:string|required=false|hidden=false|no-opt=""|scope=local, --scope-value:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+robot-get] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+update] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --desc:string|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --icon-media-id:string|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+version-check-approval] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --version-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+version-get] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --version-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+version-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page-size:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+version-status] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --version-id:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+webapp-config] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --h5-page-type:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --homepage-url:string|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --omp-url:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --pc-homepage-url:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[devapp.+webapp-get] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --unified-app-id:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [devdoc] runnable: true hidden: false @@ -2373,9 +3104,29 @@ [ding] runnable: true hidden: false - commands: message + commands: +list, +recall-personal, +receiver-status, +send-personal, message flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[ding.+list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:int|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --type:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[ding.+recall-personal] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[ding.+receiver-status] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --ding-id:string|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[ding.+send-personal] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --content:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --type:string|required=false|hidden=false|no-opt=""|scope=local, --users:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --uuid:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [ding.message] runnable: true hidden: false @@ -2420,9 +3171,94 @@ [doc] runnable: true hidden: false - commands: block, comment, create, export, file, import, info, media, read, template, update, version + commands: +comment-create, +comment-list, +comment-reply, +copy, +doc-append, +export-get, +export-submit, +find-doc, +list, +move, +search, +share-doc, +template-list, +template-search, +version-list, +version-revert, +version-save, block, comment, create, export, file, import, info, media, read, template, update, version flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[doc.+comment-create] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --content:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mention:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+comment-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --resolve-status:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --type:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+comment-reply] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --comment-key:string|required=false|hidden=false|no-opt=""|scope=local, --content:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --emoji:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mention:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+copy] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --folder:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --workspace:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+doc-append] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --doc:string|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --text:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+export-get] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --job-id:string|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+export-submit] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --export-format:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+find-doc] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --folder:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --workspace:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+move] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --folder:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --workspace:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --created-from:int|required=false|hidden=false|no-opt=""|scope=local, --created-to:int|required=false|hidden=false|no-opt=""|scope=local, --creator-uids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --editor-uids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --extensions:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mentioned-uids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --visited-from:int|required=false|hidden=false|no-opt=""|scope=local, --visited-to:int|required=false|hidden=false|no-opt=""|scope=local, --workspace-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+share-doc] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --note:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --url:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+template-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --source:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+template-search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --source:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+version-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+version-revert] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --version:int|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[doc.+version-save] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [doc.block] runnable: true hidden: false @@ -2605,9 +3441,44 @@ [drive] runnable: true hidden: false - commands: commit, copy, delete, download, info, list, list-spaces, mkdir, move, permission, publish, recent, recycle, rename, search, shortcut, stats, upload, upload-info + commands: +copy, +find-file, +info, +move, +recent, +search, +search-docs, commit, copy, delete, download, info, list, list-spaces, mkdir, move, permission, publish, recent, recycle, rename, search, shortcut, stats, upload, upload-info flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[drive.+copy] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --folder:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --workspace:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[drive.+find-file] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[drive.+info] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --space-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[drive.+move] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --folder:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --workspace:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[drive.+recent] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --creator-type:int|required=false|hidden=false|no-opt=""|scope=local, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --operate-type:int|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[drive.+search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --created-from:int|required=false|hidden=false|no-opt=""|scope=local, --created-to:int|required=false|hidden=false|no-opt=""|scope=local, --creator-uids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --extensions:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --file-types:stringSlice|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --modified-from:int|required=false|hidden=false|no-opt=""|scope=local, --modified-to:int|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --target:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[drive.+search-docs] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [drive.commit] runnable: true hidden: false @@ -2813,9 +3684,59 @@ [mail] runnable: true hidden: false - commands: allow-list, attachment, auto-reply, block-list, contact, draft, folder, mailbox, message, rule, sent-message, tag, template, thread, user + commands: +contact-list, +find-mail-user, +folder-list, +recent-mail, +search-mail, +tag-list, +template-list, +thread-list, +unread-mail, +user-search, allow-list, attachment, auto-reply, block-list, contact, draft, folder, mailbox, message, rule, sent-message, tag, template, thread, user flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[mail.+contact-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[mail.+find-mail-user] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[mail.+folder-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --folder:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[mail.+recent-mail] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --folder:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[mail.+search-mail] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --size:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[mail.+tag-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[mail.+template-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[mail.+thread-list] + runnable: true + hidden: false + flags: --ascending:bool|required=false|hidden=false|no-opt="true"|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --folder:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[mail.+unread-mail] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[mail.+user-search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --email:string|required=false|hidden=false|no-opt=""|scope=local, --employee-no:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --keyword:string|required=false|hidden=false|no-opt=""|scope=local, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [mail.allow-list] runnable: true hidden: false @@ -3199,9 +4120,39 @@ [minutes] runnable: true hidden: false - commands: get, hot-word, list, mind-graph, permission, record, replace-text, speaker, tag, update, upload + commands: +detail, +list-all, +list-mine, +list-shared, +record-start, +replace-batch, get, hot-word, list, mind-graph, permission, record, replace-text, speaker, tag, update, upload flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[minutes.+detail] + runnable: true + hidden: false + flags: --artifacts:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --direction:string|required=false|hidden=false|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[minutes.+list-all] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[minutes.+list-mine] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[minutes.+list-shared] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[minutes.+record-start] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --session-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[minutes.+replace-batch] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=false|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --pair:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [minutes.get] runnable: true hidden: false @@ -3426,9 +4377,44 @@ [oa] runnable: true hidden: false - commands: approval + commands: +list-cc, +list-executed, +list-forms, +list-pending, +list-submitted, +my-initiated, +search-forms, approval flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[oa.+list-cc] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[oa.+list-executed] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[oa.+list-forms] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:int|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[oa.+list-pending] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:int|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --start:int|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[oa.+list-submitted] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[oa.+my-initiated] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:int|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[oa.+search-forms] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [oa.approval] runnable: true hidden: false @@ -3679,10 +4665,20 @@ [report] runnable: true hidden: false - commands: create, created, detail, entry, inbox, list, outbox, sent, stats, template + commands: +inbox-list, +outbox-list, create, created, detail, entry, inbox, list, outbox, sent, stats, template aliases: log flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[report.+inbox-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:int|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --sender-user-ids:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --size:int|required=false|hidden=false|no-opt=""|scope=local, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[report.+outbox-list] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:int|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --modified-end:string|required=false|hidden=false|no-opt=""|scope=local, --modified-start:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:int|required=false|hidden=false|no-opt=""|scope=local, --start:string|required=false|hidden=false|no-opt=""|scope=local, --template-name:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [report.create] runnable: true hidden: false @@ -3785,9 +4781,19 @@ [sheet] runnable: true hidden: false - commands: add-dimension, append, batch-update, chart, cond-format, copy, create, create-float-image, csv-get, csv-put, delete-dimension, delete-dropdown, delete-float-image, delete-sheet, export, filter, filter-view, find, get-dropdown, get-float-image, group-dimension, hide-gridline, info, insert-dimension, list, list-float-images, media-upload, merge-cells, move-dimension, new, pivot-table, range, replace, set-dropdown, show-gridline, table-get, table-put, template, ungroup-dimension, unmerge-cells, update, update-dimension, update-float-image, write-image + commands: +list-sheets, +read, add-dimension, append, batch-update, chart, cond-format, copy, create, create-float-image, csv-get, csv-put, delete-dimension, delete-dropdown, delete-float-image, delete-sheet, export, filter, filter-view, find, get-dropdown, get-float-image, group-dimension, hide-gridline, import, info, insert-dimension, list, list-float-images, media-upload, merge-cells, move-dimension, new, pivot-table, range, replace, set-dropdown, show-gridline, table-get, table-put, template, ungroup-dimension, unmerge-cells, update, update-dimension, update-float-image, write-image flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[sheet.+list-sheets] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[sheet.+read] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --range:string|required=false|hidden=false|no-opt=""|scope=local, --sheet-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --value-render-option:string|required=false|hidden=false|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [sheet.add-dimension] runnable: true hidden: false @@ -4017,6 +5023,22 @@ hidden: false flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --doc-id:string|required=false|hidden=true|no-opt=""|scope=local, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --file-id:string|required=false|hidden=true|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --node:string|required=false|hidden=false|no-opt=""|scope=local, --node-id:string|required=false|hidden=true|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --sheet-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[sheet.import] + runnable: true + hidden: false + commands: create, get + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --file:string|required=false|hidden=false|no-opt=""|scope=local, --file-path:string|required=false|hidden=true|no-opt=""|scope=local, --folder:string|required=false|hidden=true|no-opt=""|scope=local, --folder-token:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -n/--name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --parent-folder:string|required=false|hidden=true|no-opt=""|scope=local, --parent-folder-id:string|required=false|hidden=true|no-opt=""|scope=local, --parent-id:string|required=false|hidden=true|no-opt=""|scope=local, --parent-node-id:string|required=false|hidden=true|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --workspace:string|required=false|hidden=false|no-opt=""|scope=local, --workspace-id:string|required=false|hidden=true|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[sheet.import.create] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, --file:string|required=false|hidden=false|no-opt=""|scope=local, --folder:string|required=false|hidden=true|no-opt=""|scope=local, --folder-token:string|required=false|hidden=false|no-opt=""|scope=local, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -n/--name:string|required=false|hidden=false|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --workspace:string|required=false|hidden=false|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[sheet.import.get] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --task-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [sheet.info] runnable: true hidden: false @@ -4247,9 +5269,64 @@ [todo] runnable: true hidden: false - commands: comment, task + commands: +assign, +assign-multi, +created-todos, +get, +get-my-tasks, +list-attachment, +list-comment, +list-sub, +overdue, +remind, +todo-done, comment, task flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[todo.+assign] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --due:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --task:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:string|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+assign-multi] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --task:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --to:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+created-todos] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+get] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --task-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+get-my-tasks] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:string|required=false|hidden=false|no-opt=""|scope=local, --plan-finish-end:int|required=false|hidden=false|no-opt=""|scope=local, --plan-finish-start:int|required=false|hidden=false|no-opt=""|scope=local, --priority:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --role-types:stringSlice|required=false|hidden=false|no-opt=""|scope=local, --size:string|required=false|hidden=false|no-opt=""|scope=local, --status:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+list-attachment] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --task-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+list-comment] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page:string|required=false|hidden=false|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:string|required=false|hidden=false|no-opt=""|scope=local, --task-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+list-sub] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --task-id:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+overdue] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+remind] + runnable: true + hidden: false + flags: --at:string|required=false|hidden=false|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --task:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + +[todo.+todo-done] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --task:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [todo.comment] runnable: true hidden: false @@ -4375,9 +5452,14 @@ [wiki] runnable: true hidden: false - commands: member, node, space + commands: +space-search, member, node, space flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited +[wiki.+space-search] + runnable: true + hidden: false + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + [wiki.member] runnable: true hidden: false From ff4961ebbe83dc7318344af8ab558e781ce89870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Thu, 16 Jul 2026 10:56:04 +0800 Subject: [PATCH 12/14] fix(release): harden mirror credential transport --- scripts/release/reconcile-gitee-assets.sh | 8 +- scripts/release/sync-gitee-tag.sh | 20 +++- scripts/release/sync-to-gitee.sh | 19 +++- scripts/release/sync-to-oss.sh | 6 +- test/scripts/gitee_sync_test.go | 131 +++++++++++++++++++++- test/scripts/release_script_test.go | 13 ++- 6 files changed, 183 insertions(+), 14 deletions(-) diff --git a/scripts/release/reconcile-gitee-assets.sh b/scripts/release/reconcile-gitee-assets.sh index c56db597..5bf443b6 100755 --- a/scripts/release/reconcile-gitee-assets.sh +++ b/scripts/release/reconcile-gitee-assets.sh @@ -141,7 +141,8 @@ api_get() { list_assets() { api_get "$GITEE_LIST_MAX_TIME" \ - "${base}/releases/${release_id}/attach_files?access_token=${GITEE_TOKEN}" \ + -H "Authorization: token ${GITEE_TOKEN}" \ + "${base}/releases/${release_id}/attach_files" \ | python3 -c 'import json,sys data=json.load(sys.stdin) rows=data if isinstance(data,list) else data.get("attach_files",[]) @@ -214,7 +215,8 @@ delete_asset() { max_time="$(bounded_max_time "$GITEE_MUTATION_MAX_TIME")" || return 1 curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" \ --max-time "$max_time" \ - -X DELETE "${base}/releases/${release_id}/attach_files/${asset_id}?access_token=${GITEE_TOKEN}" \ + -H "Authorization: token ${GITEE_TOKEN}" \ + -X DELETE "${base}/releases/${release_id}/attach_files/${asset_id}" \ >/dev/null } @@ -241,7 +243,7 @@ gitee_attach() { if response="$(curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" \ --max-time "$max_time" \ -X POST "${base}/releases/${release_id}/attach_files" \ - -F "access_token=${GITEE_TOKEN}" -F "file=@${file}" 2>&1)"; then + -H "Authorization: token ${GITEE_TOKEN}" -F "file=@${file}" 2>&1)"; then status=0 else status=$? diff --git a/scripts/release/sync-gitee-tag.sh b/scripts/release/sync-gitee-tag.sh index 02f9e2d4..d4c07d17 100755 --- a/scripts/release/sync-gitee-tag.sh +++ b/scripts/release/sync-gitee-tag.sh @@ -49,11 +49,13 @@ case "$VERSION" in *) err "VERSION must be a v-prefixed release tag: ${VERSION}" ;; esac +gitee_askpass_required=0 if [ -z "${GITEE_GIT_REMOTE:-}" ]; then [ -n "${GITEE_USER:-}" ] || err "GITEE_USER is required" [ -n "${GITEE_TOKEN:-}" ] || err "GITEE_TOKEN is required" [ -n "$GITEE_REPO" ] || err "GITEE_REPO is required" - GITEE_GIT_REMOTE="https://${GITEE_USER}:${GITEE_TOKEN}@gitee.com/${GITEE_REPO}.git" + GITEE_GIT_REMOTE="https://gitee.com/${GITEE_REPO}.git" + gitee_askpass_required=1 fi if [ -z "${GITEE_PUBLIC_GIT_REMOTE:-}" ]; then [ -n "$GITEE_REPO" ] || err "GITEE_REPO is required" @@ -154,6 +156,22 @@ fi tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT +if [ "$gitee_askpass_required" -eq 1 ]; then + askpass="$tmp_dir/git-askpass.sh" + cat >"$askpass" <<'EOF' +#!/bin/sh +case "$1" in + *Username*) printf '%s\n' "$DWS_GITEE_USER" ;; + *) printf '%s\n' "$DWS_GITEE_TOKEN" ;; +esac +EOF + chmod 700 "$askpass" + export DWS_GITEE_USER="$GITEE_USER" + export DWS_GITEE_TOKEN="$GITEE_TOKEN" + export GIT_ASKPASS="$askpass" + export GIT_TERMINAL_PROMPT=0 +fi + remote_tag_commit() { local refs_file="${tmp_dir}/remote-refs" if ! run_bounded git ls-remote "$GITEE_PUBLIC_GIT_REMOTE" \ diff --git a/scripts/release/sync-to-gitee.sh b/scripts/release/sync-to-gitee.sh index f2382825..87c2965d 100755 --- a/scripts/release/sync-to-gitee.sh +++ b/scripts/release/sync-to-gitee.sh @@ -125,10 +125,21 @@ api_get() { --max-time "$max_time" "$@" } +release_id_from_json() { + python3 -c 'import json, sys +data = json.load(sys.stdin) +value = data.get("id") if isinstance(data, dict) else None +if isinstance(value, int) and not isinstance(value, bool) and value > 0: + print(value) +elif isinstance(value, str) and value.isdigit() and int(value) > 0: + print(value)' 2>/dev/null +} + # ── Resolve or create the Gitee release for this tag ────────────────────────── rel_json="$(api_get "$GITEE_RELEASE_LOOKUP_MAX_TIME" \ - "${base}/releases/tags/${VERSION}?access_token=${GITEE_TOKEN}" 2>/dev/null || true)" -release_id="$(printf '%s' "$rel_json" | grep -o '"id":[ ]*[0-9]*' | head -1 | grep -o '[0-9]*' || true)" + -H "Authorization: token ${GITEE_TOKEN}" \ + "${base}/releases/tags/${VERSION}" 2>/dev/null || true)" +release_id="$(printf '%s' "$rel_json" | release_id_from_json || true)" if [ -z "$release_id" ]; then deadline_remaining >/dev/null || err "overall Gitee sync deadline exhausted during release lookup" @@ -137,12 +148,12 @@ if [ -z "$release_id" ]; then || err "overall Gitee sync deadline exhausted before release creation" rel_json="$(curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" \ --max-time "$create_max_time" -X POST "${base}/releases" \ - -F "access_token=${GITEE_TOKEN}" \ + -H "Authorization: token ${GITEE_TOKEN}" \ -F "tag_name=${VERSION}" \ -F "name=${VERSION}" \ -F "body=Mirror of GitHub release ${VERSION} for China users." \ -F "target_commitish=${target_commit}" 2>/dev/null || true)" - release_id="$(printf '%s' "$rel_json" | grep -o '"id":[ ]*[0-9]*' | head -1 | grep -o '[0-9]*' || true)" + release_id="$(printf '%s' "$rel_json" | release_id_from_json || true)" fi [ -n "$release_id" ] || { echo "❌ Could not get/create Gitee release for ${VERSION}. Response: ${rel_json}" >&2; exit 1; } echo " Gitee release id = ${release_id}" diff --git a/scripts/release/sync-to-oss.sh b/scripts/release/sync-to-oss.sh index 0d972a49..ce12085c 100755 --- a/scripts/release/sync-to-oss.sh +++ b/scripts/release/sync-to-oss.sh @@ -107,9 +107,9 @@ fi oss_cp() { # oss_cp + # ossutil reads OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET from the + # environment. Never copy either secret into the process argv. "$OSSUTIL" cp -f \ - --access-key-id "$OSS_ACCESS_KEY_ID" \ - --access-key-secret "$OSS_ACCESS_KEY_SECRET" \ --endpoint "$OSS_ENDPOINT" \ "$1" "oss://${OSS_BUCKET}/$2" } @@ -117,8 +117,6 @@ oss_cp() { oss_get() { # oss_get "$OSSUTIL" cp -f \ - --access-key-id "$OSS_ACCESS_KEY_ID" \ - --access-key-secret "$OSS_ACCESS_KEY_SECRET" \ --endpoint "$OSS_ENDPOINT" \ "oss://${OSS_BUCKET}/$1" "$2" } diff --git a/test/scripts/gitee_sync_test.go b/test/scripts/gitee_sync_test.go index 9a295054..9a0b35f5 100644 --- a/test/scripts/gitee_sync_test.go +++ b/test/scripts/gitee_sync_test.go @@ -149,6 +149,79 @@ exec %q "$@" } } +func TestSyncGiteeTagUsesAskpassWithoutEmbeddingCredentials(t *testing.T) { + scriptPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "sync-gitee-tag.sh")) + root := t.TempDir() + workDir := filepath.Join(root, "work") + seedTaggedRepository(t, workDir, "v1.2.3") + expectedCommit := strings.TrimSpace(mustOutput(t, workDir, "git", "rev-parse", "v1.2.3^{commit}")) + + realGit, err := exec.LookPath("git") + if err != nil { + t.Fatalf("LookPath(git) error = %v", err) + } + wrapperDir := t.TempDir() + logPath := filepath.Join(root, "git.log") + pushMarker := filepath.Join(root, "pushed") + wrapper := fmt.Sprintf(`#!/bin/sh +set -eu +case "$1" in + ls-remote) + printf '%%s\n' "$*" >>"$GIT_TEST_LOG" + case " $* " in *"$DWS_GITEE_TOKEN"*) exit 91 ;; esac + [ "${GIT_TERMINAL_PROMPT:-}" = "0" ] + [ -x "${GIT_ASKPASS:-}" ] + [ "$("$GIT_ASKPASS" "Username for https://gitee.com")" = "$DWS_GITEE_USER" ] + [ "$("$GIT_ASKPASS" "Password for https://gitee.com")" = "$DWS_GITEE_TOKEN" ] + if [ -f "$GIT_PUSH_MARKER" ]; then + printf '%%s\trefs/tags/v1.2.3^{}\n' "$EXPECTED_COMMIT" + fi + ;; + push) + printf '%%s\n' "$*" >>"$GIT_TEST_LOG" + case " $* " in *"$DWS_GITEE_TOKEN"*) exit 92 ;; esac + [ "$2" = "https://gitee.com/owner/repo.git" ] + : >"$GIT_PUSH_MARKER" + ;; + *) exec %q "$@" ;; +esac +`, realGit) + mustWriteFile(t, filepath.Join(wrapperDir, "git"), []byte(wrapper), 0o755) + + cmd := exec.Command("bash", scriptPath) + cmd.Dir = workDir + cmd.Env = append(os.Environ(), + "PATH="+wrapperDir+string(os.PathListSeparator)+os.Getenv("PATH"), + "VERSION=v1.2.3", + "GITEE_USER=test-user", + "GITEE_TOKEN=sensitive-token", + "GITEE_REPO=owner/repo", + "GITEE_SOURCE_REMOTE=", + "GITEE_GIT_REMOTE=", + "GITEE_PUBLIC_GIT_REMOTE=", + "GITEE_TAG_VERIFY_ATTEMPTS=1", + "GITEE_TAG_VERIFY_DELAY=0", + "GITEE_GIT_TIMEOUT_SECONDS=10", + "GIT_TEST_LOG="+logPath, + "GIT_PUSH_MARKER="+pushMarker, + "EXPECTED_COMMIT="+expectedCommit, + ) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("sync-gitee-tag.sh error = %v\noutput:\n%s", err, output) + } + logData, err := os.ReadFile(logPath) + if err != nil { + t.Fatalf("ReadFile(git log) error = %v", err) + } + if strings.Contains(string(logData), "sensitive-token") { + t.Fatalf("Gitee token leaked into git argv:\n%s", logData) + } + if !strings.Contains(string(logData), "push https://gitee.com/owner/repo.git") { + t.Fatalf("tag push did not use the credential-free remote:\n%s", logData) + } +} + func TestSyncGiteeTagAndReleasePathShareOneOverallDeadline(t *testing.T) { scriptPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "sync-to-gitee.sh")) root := t.TempDir() @@ -213,6 +286,16 @@ func TestSyncToGiteeRunsTagReleaseCreationAndAssetReconciliationWithinOneBudget( releaseLookups := 0 releaseCreates := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/repos/") { + if got := r.Header.Get("Authorization"); got != "token test-token" { + t.Errorf("Authorization header = %q, want token authentication", got) + } + if got := r.URL.Query().Get("access_token"); got != "" { + t.Errorf("Gitee token leaked in query string: %q", got) + } + } else if got := r.Header.Get("Authorization"); got != "" { + t.Errorf("token forwarded to asset download URL: %q", got) + } switch { case r.Method == http.MethodGet && r.URL.Path == "/repos/owner/repo/releases/tags/v1.2.3": releaseMu.Lock() @@ -220,11 +303,17 @@ func TestSyncToGiteeRunsTagReleaseCreationAndAssetReconciliationWithinOneBudget( releaseMu.Unlock() http.NotFound(w, r) case r.Method == http.MethodPost && r.URL.Path == "/repos/owner/repo/releases": + if err := r.ParseMultipartForm(1 << 20); err != nil { + t.Errorf("ParseMultipartForm(release create) error = %v", err) + } + if got := r.FormValue("access_token"); got != "" { + t.Errorf("Gitee token leaked in release form: %q", got) + } releaseMu.Lock() releaseCreates++ releaseMu.Unlock() w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]any{"id": 1}) + _, _ = io.WriteString(w, `{"metadata":{"id":999},"id":1}`) default: fake.ServeHTTP(w, r) } @@ -416,6 +505,14 @@ func TestGiteeReleaseWorkflowUsesImmutableTagsAndBoundedRetryBudget(t *testing.T t.Fatalf("ReadFile(%s) error = %v", reconcilerPath, err) } reconciler := string(reconcilerData) + for _, forbidden := range []string{"?access_token=", `-F "access_token=`} { + if strings.Contains(syncScript, forbidden) || strings.Contains(reconciler, forbidden) { + t.Fatalf("Gitee API scripts still expose tokens via %q", forbidden) + } + } + if !strings.Contains(syncScript, "Authorization: token") || !strings.Contains(reconciler, "Authorization: token") { + t.Fatal("Gitee API scripts must authenticate with an Authorization header") + } perAssetSeconds := shellDefaultInt(t, reconciler, "GITEE_ASSET_TIMEOUT_SECONDS") overallSeconds := shellDefaultInt(t, reconciler, "GITEE_OVERALL_TIMEOUT_SECONDS") finalListSeconds := shellDefaultInt(t, reconciler, "GITEE_LIST_MAX_TIME") @@ -484,6 +581,21 @@ func TestGiteeReleaseWorkflowUsesImmutableTagsAndBoundedRetryBudget(t *testing.T if !strings.Contains(string(localPublishData), "Direct Gitee artifact publication is disabled") { t.Fatal("direct local Gitee publication must stay disabled") } + + tagSyncPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "sync-gitee-tag.sh")) + tagSyncData, err := os.ReadFile(tagSyncPath) + if err != nil { + t.Fatalf("ReadFile(%s) error = %v", tagSyncPath, err) + } + tagSync := string(tagSyncData) + if strings.Contains(tagSync, `https://${GITEE_USER}:${GITEE_TOKEN}@`) { + t.Fatal("Gitee git remote must not embed credentials") + } + for _, required := range []string{"GIT_ASKPASS", "GIT_TERMINAL_PROMPT", "DWS_GITEE_TOKEN"} { + if !strings.Contains(tagSync, required) { + t.Fatalf("Gitee tag sync missing askpass safeguard %q", required) + } + } } func mustAbs(t *testing.T, path string) string { @@ -657,6 +769,19 @@ func newFakeGiteeRelease(dropFirstResponse, failUploads bool) *fakeGiteeRelease func (f *fakeGiteeRelease) ServeHTTP(w http.ResponseWriter, r *http.Request) { const attachPath = "/repos/owner/repo/releases/1/attach_files" + if strings.HasPrefix(r.URL.Path, "/repos/") { + if got := r.Header.Get("Authorization"); got != "token test-token" { + http.Error(w, "missing token authorization header", http.StatusUnauthorized) + return + } + if r.URL.Query().Get("access_token") != "" { + http.Error(w, "token leaked in query string", http.StatusBadRequest) + return + } + } else if r.Header.Get("Authorization") != "" { + http.Error(w, "token forwarded to asset download URL", http.StatusBadRequest) + return + } switch { case r.Method == http.MethodGet && r.URL.Path == attachPath: f.list(w, r) @@ -692,6 +817,10 @@ func (f *fakeGiteeRelease) upload(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } + if r.FormValue("access_token") != "" { + http.Error(w, "token leaked in upload form", http.StatusBadRequest) + return + } file, header, err := r.FormFile("file") if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) diff --git a/test/scripts/release_script_test.go b/test/scripts/release_script_test.go index ed3bfe3d..37e0959c 100644 --- a/test/scripts/release_script_test.go +++ b/test/scripts/release_script_test.go @@ -704,8 +704,9 @@ func TestReleaseMirrorUsesChannelSpecificPointer(t *testing.T) { dist := filepath.Join(root, "dist") seedVersionedReleaseArtifacts(t, dist, test.version) logPath := filepath.Join(root, "ossutil.log") + argsLogPath := filepath.Join(root, "ossutil-args.log") fakeOSSUtil := filepath.Join(root, "ossutil") - mustWriteFile(t, fakeOSSUtil, []byte("#!/bin/sh\nset -eu\nprevious=\npenultimate=\nfor arg in \"$@\"; do penultimate=\"$previous\"; previous=\"$arg\"; done\nlast=\"$previous\"\ncase \"$penultimate\" in oss://*) echo 'ErrorCode=NoSuchKey' >&2; exit 1 ;; esac\nprintf '%s\\n' \"$last\" >> \"$OSSUTIL_LOG\"\n"), 0o755) + mustWriteFile(t, fakeOSSUtil, []byte("#!/bin/sh\nset -eu\nprintf '%s\\n' \"$@\" >> \"$OSSUTIL_ARGS_LOG\"\nprevious=\npenultimate=\nfor arg in \"$@\"; do penultimate=\"$previous\"; previous=\"$arg\"; done\nlast=\"$previous\"\ncase \"$penultimate\" in oss://*) echo 'ErrorCode=NoSuchKey' >&2; exit 1 ;; esac\nprintf '%s\\n' \"$last\" >> \"$OSSUTIL_LOG\"\n"), 0o755) cmd := exec.Command("bash", script) cmd.Dir = sourceRoot @@ -720,6 +721,7 @@ func TestReleaseMirrorUsesChannelSpecificPointer(t *testing.T) { "OSS_PREFIX=dws", "OSSUTIL="+fakeOSSUtil, "OSSUTIL_LOG="+logPath, + "OSSUTIL_ARGS_LOG="+argsLogPath, ) output, err := cmd.CombinedOutput() if err != nil { @@ -739,6 +741,15 @@ func TestReleaseMirrorUsesChannelSpecificPointer(t *testing.T) { if hasInstaller != test.installer { t.Fatalf("installer upload = %v, want %v:\n%s", hasInstaller, test.installer, logData) } + argsData, err := os.ReadFile(argsLogPath) + if err != nil { + t.Fatalf("ReadFile(ossutil args log) error = %v", err) + } + for _, forbidden := range []string{"test-key", "test-secret", "--access-key-id", "--access-key-secret"} { + if strings.Contains(string(argsData), forbidden) { + t.Fatalf("ossutil argv exposed %q:\n%s", forbidden, argsData) + } + } }) } } From c2e5fec967424ed715b0063ea3a8474d5c8bce46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Thu, 16 Jul 2026 11:59:31 +0800 Subject: [PATCH 13/14] test(calendar): cover rooms help in helpers package --- internal/helpers/calendar_help_test.go | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 internal/helpers/calendar_help_test.go diff --git a/internal/helpers/calendar_help_test.go b/internal/helpers/calendar_help_test.go new file mode 100644 index 00000000..1e1fa893 --- /dev/null +++ b/internal/helpers/calendar_help_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "strings" + "testing" +) + +func TestCalendarEventCreateHelpKeepsRoomsStringMetavar(t *testing.T) { + cmd := newCalendarCommand() + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"event", "create", "--help"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("calendar event create --help: %v\n%s", err, out.String()) + } + + help := out.String() + if !strings.Contains(help, "--rooms string") { + t.Fatalf("calendar event create help missing string metavar for --rooms:\n%s", help) + } + if strings.Contains(help, "--rooms room search") { + t.Fatalf("calendar event create help treated description text as --rooms metavar:\n%s", help) + } +} From 2b715e35add5b5ba962769f331e6449a0280146b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= Date: Thu, 16 Jul 2026 12:04:31 +0800 Subject: [PATCH 14/14] test(calendar): include help check in platform coverage --- internal/helpers/calendar_help_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/helpers/calendar_help_test.go b/internal/helpers/calendar_help_test.go index 1e1fa893..07a1a045 100644 --- a/internal/helpers/calendar_help_test.go +++ b/internal/helpers/calendar_help_test.go @@ -19,7 +19,7 @@ import ( "testing" ) -func TestCalendarEventCreateHelpKeepsRoomsStringMetavar(t *testing.T) { +func TestCrossPlatformCoverageCalendarEventCreateHelpKeepsRoomsStringMetavar(t *testing.T) { cmd := newCalendarCommand() var out bytes.Buffer cmd.SetOut(&out)