# Maintainer Automation Notes This document keeps agent- and maintainer-specific workflow notes out of the repository root while preserving repo-local guidance for automation. ## Read Order 1. `README.md` 2. `CONTRIBUTING.md` 3. `docs/architecture.md` 4. This document ## Project Snapshot - `dws` is a Go-based DingTalk Workspace CLI and MCP runtime bridge. - Product commands are loaded dynamically via `internal/plugin` from bundled descriptors. - Command handlers live in `internal/helpers`; runtime execution flows through `internal/executor` and `internal/transport`. ## Repository Map - `cmd`: public CLI entrypoint - `internal/app`: root command wiring, static utility commands, plugin loading - `internal/helpers`: product command handlers (dev, chat, calendar, contact, etc.) - `internal/plugin`: plugin-based dynamic command loader - `internal/cli`: catalog types and static endpoint loader - `internal/executor`: invocation dispatch and result handling - `internal/transport`: MCP HTTP client and request signing - `internal/auth`: login, token management, agent-code detection - `internal/audit`: user operation audit log - `internal/errors`: structured error model with categories and hints - `internal/keychain`: OS keychain integration for credential storage - `internal/security`: endpoint allowlist and domain trust - `internal/pat`: PAT (Personal Access Token) authorization flow - `docs/`: public architecture and reference docs - `scripts/`: build, test, lint, packaging, and policy checks - `test/`: CLI, integration, contract, unit, and skill E2E test suites ## Task Routing - Add or fix a command path: start from `internal/helpers` (handler implementations) or `internal/app` (command tree wiring) - Protocol or transport issues: inspect `internal/transport` - Auth or login issues: inspect `internal/auth`, `internal/pat`, `internal/keychain` - Error message or category issues: inspect `internal/errors` - Audit log issues: inspect `internal/audit` - Plugin loading or command surface: inspect `internal/plugin` - Failure or degraded mode: inspect `internal/errors` ## Policy Checks When command surface or plugin descriptors change, run: - `./scripts/policy/check-command-surface.sh --strict` - `./scripts/policy/check-open-source-assets.sh` ## Common Commands ```bash make build make test make lint ./scripts/dev/ci-local.sh git diff --check ``` ## Reviewer Router GitHub App Reviewer requests and merge authority intentionally use different identities. The base-owned `pull_request_target` workflow may use its built-in `GITHUB_TOKEN` to request reviewers, but it must mint a dedicated GitHub App installation token before enabling auto-merge. GitHub suppresses most workflow events created by the built-in token; using it for auto-merge prevents the merge commit's `push` workflows from running and leaves the exact-SHA Coverage baseline without a trusted main-scoped producer. Configure the dedicated App before merging a workflow revision that requires it: - install it only on `DingTalk-Real-AI/dingtalk-workspace-cli`; - grant only `Contents: read and write` and `Pull requests: read and write`; - set repository variable `REVIEWER_ROUTER_APP_CLIENT_ID` to its client ID; - set `REVIEWER_ROUTER_APP_SLUG` to its exact lowercase slug; - set repository secret `REVIEWER_ROUTER_APP_PRIVATE_KEY` to its private key; - create one active repository branch ruleset named `main-merge-writers`, targeting only `refs/heads/main`, with exactly one `Restrict updates` rule (`update_allows_fetch_and_merge: false`). GitHub may project that strict value through the read APIs as `{type: "update"}` with `parameters` omitted; consumers accept only that exact omission or a one-field `parameters` object containing explicit boolean `false`, and reject every other present shape or value. They then bind the same ruleset node through GraphQL and require its non-null `updateAllowsFetchAndMerge` value to be exactly `false`; - give that ruleset exactly three bypass actors: the Reviewer Router App as an `Integration` in `pull_request` mode, plus `haofeng0705` (ID `30925823`) and `PeterGuy326` (ID `47820304`) in `always` mode for Formula publication and break-glass recovery; - never give the App bypass on `main-protection`, `main-quality`, or any other ruleset, and never reuse `HOMEBREW_PR_TOKEN`, `RELEASE_GOVERNANCE_TOKEN`, or a personal token for Reviewer Router. The workflow limits each minted token to the current repository, requests the two permissions explicitly, and lets the token action revoke it at job end. It also requires the minted App slug to equal the reviewed repository variable; there is no `GITHUB_TOKEN` fallback. Before reading App credentials, the base-owned workflow revalidates the event's exact base/head and uses its built-in token only to disable an existing request owned by `github-actions[bot]` or one whose title or merge metadata requests that GitHub skip workflows. A mint or permission failure therefore leaves that PR manual-merge only. The built-in token's `Contents: write` permission is isolated to this trusted cleanup job and is never used to enable auto-merge; review routing keeps `Contents: read`. Existing requests owned by a human or another non-built-in identity are replaced with the exact dedicated-App request after token minting. Only an already App-owned request with the fixed headline/body is preserved. The required `Test` context reads the live repository settings and applied rulesets, verifies the exact writer-rule shape, and requires its own built-in Actions identity to report `current_user_can_bypass: never`. Before enabling or reconciling auto-merge, the minted App independently requires `pull_requests_only` on that writer rule and `never` on every other active main ruleset. These identity-relative checks remain available to low-privilege tokens; GitHub deliberately hides the full `bypass_actors` list from callers without ruleset-write access. Operators must therefore inspect that list during rollout and keep it at the exact three actors above. The required `Test` context then briefly waits for the concurrent router takeover and accepts only a null request or the configured App owner with exact fixed metadata. A null request is safe for this failure mode because the built-in Actions identity cannot pass the writer rule; other permitted identities emit either a protected-main push or the trusted closed-PR repair. Draft PRs skip this identity check; the explicit `ready_for_review` trigger reruns admission when they become merge-eligible, while `edited` and `auto_merge_enabled` rerun both workflows when the PR title or merge request changes. A human `auto_merge_disabled` event reruns CI without silently re-enabling the request, leaving it available only to the designated break-glass identity. The required `Test` context rejects GitHub workflow-skip directives in the PR title or an existing auto-merge request and verifies the repository's reviewed `MERGE_MESSAGE` title plus `PR_TITLE` or `BLANK` body defaults. GitHub does not expose those merge-related settings to the read-only admission token: the classifier accepts only both exact reviewed values or the complete omission of both properties, and rejects partial omission, `null`, or any other value. Before any enable, reconcile, or merge mutation, the dedicated App's current-repository token (which has `Contents: write`) must observe both exact reviewed values. The dedicated App binds each mutation to the exact head OID and supplies a fixed safe headline and body, so GitHub cannot copy an unsafe PR title into its merge commit. After enabling, the workflow requires the owner to equal the token action's exact `[bot]` output. If the event base/head changes during the mutation window, it removes only that App-owned request and fails the run. The App-owned native auto-merge request is the reviewed automation intent, not the sole executor: GitHub's deferred auto-merge path does not reliably apply a GitHub App's pull-request-only ruleset bypass. A zero-permission approval-signal workflow converts submitted or dismissed reviews into `workflow_run`; completed admission workflows use the same trusted default-branch trigger. The serialized reconcile job treats `workflow_run` only as a wake-up signal: it never reads the triggering run's pull-request payload or artifacts and never checks out code from that run. It enumerates open `main` PRs again through the API, then revalidates the safe App owner, metadata, and ruleset boundary immediately before calling the synchronous PR merge endpoint with the exact current head SHA. The preflight requires exactly one repository-owned `main-protection` ruleset with one latest-head approval and exactly one repository-owned `main-quality` ruleset with the reviewed nine strict checks. The App must report `never` on both and on every other non-writer ruleset. Every required context must be bound to the GitHub Actions App (`integration_id=15368`); a missing, different, or duplicate context/source entry fails closed together with deletion or weakening of either gate. HTTP 405 means the PR is not ready, while 409 means its revision changed; either remains open for the next event. Other failures make reconciliation red. A concurrent native merge is accepted only after the final PR state proves the exact head, App identity, and non-empty merge SHA. A staggered twice-hourly schedule provides eventual recovery if a webhook or workflow completion is delayed, and `workflow_dispatch` remains the on-demand repair path. The break-glass publisher must preserve a safe final commit message; `[skip ci]`, `[ci skip]`, `[no ci]`, `[skip actions]`, `[actions skip]`, and a `skip-checks: true` trailer are forbidden outside the release-controlled Formula-only path below. GitHub may suppress `pull_request_target` entirely for security-sensitive head branch names, including names that look like commit SHAs. Such a PR receives neither App takeover nor the closed-event repair. Rename the head branch for the normal path; if break-glass merge is unavoidable, preserve a safe final message so the protected-main push CI remains the authoritative producer. After installing the App, the protected-main push that deploys this workflow runs reconciliation automatically. Approval-signal and admission-workflow completions run the same serialized recovery path. The job enumerates open, ready `main` PRs with any non-App owner, unsafe App metadata, or workflow-skip metadata. It revalidates each base/head, converges a safe request to the exact dedicated-App owner and fixed message, and leaves a workflow-skipping request disabled for manual correction. It never enables auto-merge where the request was already null. Every exact safe App request is then attempted through the synchronous, SHA-bound merge endpoint; a server-declared not-ready result remains open for the next event. A mid-migration failure leaves the affected PR disabled for a fresh routing event or break-glass merge. One PR failure is recorded without preventing later legacy owners from being attempted; the batch ends red with a per-PR summary. Manually dispatch `Reviewer routing` from `main` until the failed count is zero. Disabling the App-owned auto-merge request before the reconcile job's final PR read leaves that PR manual-only. That final read is the cancellation linearization point: GitHub's merge API can condition atomically on the head SHA but not on the auto-merge request itself, so a disable racing after that read may lose to an already-issued merge request. To stop an in-flight attempt before the merge endpoint accepts it, close the PR or change its head; if the server observes that state first, it rejects the state/SHA-bound merge. No client-side action can revoke a merge that GitHub has already accepted. The endpoint has no equivalent expected-base parameter. The workflow therefore checks `base=main` and the repository before and after merge and fails any retargeted result, but a retarget racing after the final read cannot be made atomic client-side. Never retarget a PR while its App-owned intent is active: disable the request, wait until all running `Reviewer routing` reconciliation jobs finish, and only then change the base. Preventing a malicious same-instant retarget requires a GitHub-side branch/ruleset control rather than workflow code. A PR that introduces or rotates this identity still runs the old base-owned router. Install/configure the App and activate the exact writer ruleset first; this blocks its legacy `github-actions[bot]` request from writing `main`. After the governance PR's final push, disable that old request, confirm the live settings/ruleset contract and all required checks are green for the exact head, then have only `haofeng0705` or `PeterGuy326` merge that head with the repository-generated safe merge message. Verify the resulting merge SHA has a `CI` run with `event=push`, a successful `Coverage` context, and an exact-SHA baseline cache under `refs/heads/main`. Confirm automatic reconciliation reports zero failures and zero non-App owners. Finally use a normal canary PR to verify that the dedicated App is both `enabledBy` and `mergedBy`, and that the same post-merge chain repeats before declaring the rollout complete. ## Homebrew Formula Delivery Official releases use the designated `HOMEBREW_PR_TOKEN` identity to update exactly one tracked Formula after the immutable GitHub assets and their checksums have passed verification. The publisher validates the rendered Ruby, commits only the configured Formula path, never force-pushes `main`, and retries from a fresh clone up to three times when `main` advances concurrently. Normal stable and beta releases do not create a Formula PR or run a permission canary. The workflow uses the existing repository-scoped `HOMEBREW_PR_TOKEN` release identity because GitHub does not allow its built-in Actions App to bypass this repository's rulesets. Its owner is the designated always-bypass actor for controlled Formula publication and break-glass recovery, including on `main-merge-writers`. The workflow creates the nine Code Admission checks for the Formula-only commit only after proving its sole parent already has all nine successful checks and the committed Formula exactly matches this release's verified bytes. Formula commits retain `[skip ci]`, so the sealing step exposes only the reviewed commit identity to an independent confirmation job. That job creates the `Coverage Baseline Cache` acknowledgement and emits the reviewed `coverage-baseline-promote` repository dispatch. The default-branch `Coverage Baseline Promotion` workflow independently verifies the exact single-parent Formula commit, both parent and target admission contexts, and default-branch containment before checking out the target. It restores only the exact parent profile, recomputes the complete profile if that cache is absent, and saves the Formula SHA under the `main` cache scope. Because the cache save action treats upload errors as warnings, a second lookup must report `cache-hit=true` for the exact target key before the producer succeeds. The promotion completes the unique acknowledgement, and the confirmation job waits for that exact check-run ID. npm and mirror publication depend only on the immutable release job, so a transient cache-service failure cannot strand an otherwise valid release between channels; the final release-delivery gate still fails until the exact cache is confirmed. Once Formula sealing exposes the target SHA, the confirmation job also runs when a later immutable-package recheck fails, so a post-push failure cannot orphan the producer. Rerun the failed promotion/confirmation path after repairing the producer. Never add a prefix `restore-keys` fallback to this path. `Coverage Baseline Repair` is the independent safety net for every merged PR. Its base-owned `pull_request_target: closed` job never checks out or executes PR content: it binds the closed event's PR number and stable head SHA to the current merged-PR facts (`merged_at`, `base.ref`, and `merge_commit_sha`) and proves that merge commit is contained in `main`. It deliberately does not compare REST `base.sha`, because that field follows the live base branch and can move after the merge. Only then does it emit a `coverage-baseline-repair` repository dispatch. Workflow-skip directives alone do not suppress `pull_request_target`, subject to GitHub's separate security-sensitive branch-name restriction described above. The low-trust trigger is forbidden from writing the default-branch cache directly. Before dispatching, it gives Actions event delivery one minute to expose a run from the exact protected `.github/workflows/ci.yml` workflow and exits if that normal producer already owns the SHA, avoiding a duplicate full-suite run. A successful CI producer must hard-verify its exact cache key. If that run instead completes with any non-success conclusion, a separate base-owned `workflow_run` dispatcher binds the exact workflow ID/path, run ID/attempt, conclusion, repository, branch, and head SHA before requesting repair. `workflow_run` also has read-only default-branch cache access, so both dispatchers use the reviewed `repository_dispatch` exception. The dispatched default-branch producer revalidates the corresponding merged-PR or failed-CI identity before checkout, restores only the exact target key, recomputes the complete profile on a miss, and verifies `cache-hit=true` after saving. An hourly schedule refreshes the event-time `main` SHA after direct break-glass pushes or cache eviction; `workflow_dispatch` provides the same current-main repair on demand. The dedicated App identity remains mandatory because events created by the built-in `GITHUB_TOKEN` can suppress both the main push and the closed-PR event. Keep `HOMEBREW_PR_TOKEN` repository-scoped with `Contents: write` and `Pull requests: write` (the latter remains necessary for withdrawal rollback), keep its owner as the designated ruleset bypass actor, and do not reuse `RELEASE_GOVERNANCE_TOKEN`. The workflow and publisher provide the Formula-only path restriction; GitHub rulesets do not infer that restriction from the token. ## Release Governance and Recovery Store `RELEASE_GOVERNANCE_TOKEN` as a dedicated Actions secret with only repository `Administration: read`. The immutable-releases REST endpoint is an administration setting and cannot be read by the workflow's built-in `GITHUB_TOKEN`. Both the default-branch governance preflight and the tag contract use this same credential so a missing or expired identity is detected before an irreversible tag is created. Recovery is restricted to an existing annotated tag whose exact tag object, commit, sealed metadata, original failed run/attempt, requester identity and Release state all match; it then reuses the normal release jobs without a second-person environment approval. A same-run “Re-run failed jobs” is even lighter: the seal job may adopt an existing tag only when its complete authority matches that run and its original attempt is not newer than the current attempt. Do not put publication secrets in temporary branches or create ad-hoc recovery workflows. Cloud-sealed releases mirror to OSS only when the repository variable `ENABLE_OSS_MIRROR` is exactly `true`. Leave the variable unset while no Bucket is provisioned; GitHub, npm, and Homebrew delivery can then complete without running the OSS step. Once enabled, missing credentials, an invalid Bucket, or an upload failure remains fail-closed. The cloud tag immutably records the decision as `OSS-Mirror: enabled|deferred`; publication and withdrawal consume that sealed value instead of the variable's later state. Deferred releases cannot use `repair_oss_version`; enabling OSS applies to later release tags until an audited immutable repair marker is implemented. If an immutable GitHub Release and npm package were delivered but an enabled downstream China mirror failed, dispatch the normal `Release` workflow from the protected default branch with exactly one of `repair_gitee_version` or `repair_oss_version`. Channel repair accepts a fully successful exact release, or a failed exact-tag run only when its latest attempt completed the release contract, build, Apple signature, immutable GitHub publication, and npm delivery checks for the exact tagged commit. OSS repair additionally requires the tag's sealed policy to be `enabled`. It then downloads and re-verifies the immutable assets before invoking only the selected mirror. For a failed release, an OSS repair requires the OSS step itself to be the recorded failure. A Gitee repair accepts either a failed Gitee job or a Gitee job that was skipped behind that OSS failure; the latter is an explicit Gitee backfill and does not claim that OSS has been repaired. Gitee repair requires `GITEE_TOKEN`, `GITEE_USER`, and `GITEE_REPO`; OSS repair requires `OSS_ACCESS_KEY_ID`, `OSS_ACCESS_KEY_SECRET`, `OSS_ENDPOINT`, and `OSS_BUCKET` (with optional `OSS_PREFIX`) as Actions secrets. Missing credentials fail the selected repair closed. ## Handoff Checklist Before handoff, include: 1. Changed files and why 2. Verification commands run and outcomes 3. Known risks or follow-up work