351 lines
21 KiB
Markdown
351 lines
21 KiB
Markdown
# 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 `<app-slug>[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
|