Claude Skill

sgc-electron-delta-updater

为 Electron 桌面应用设计、实现和验证增量自动更新,覆盖 macOS Sparkle、签名、appcast、发布产物与失败拦截。用户提到增量更新、Sparkle、检查更新、appcast、DMG 更新或 delta updater 时使用。

LLM Mart · 0 points · 10 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lovstudio-skills-skills_electron-delta-updater-0b16007.zip · 9 KB
Part of lovstudio/skills — 83 skills

Install

skills CLI npx skills add https://github.com/lovstudio/skills/tree/main/skills/electron-delta-updater
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
Git git clone https://github.com/lovstudio/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.

README

Electron 增量更新 · Electron Delta Updates

Version

为 Electron 应用建立并验证可恢复的增量自动更新流程。

本地安装

将目录链接到本地 Agent Skills 目录,链接名为 sgc-electron-delta-updater。安装后可直接说“给这个 Electron App 加增量自动更新”。

使用

  • “为 macOS Electron 应用配置 Sparkle delta-only 更新,并验证 appcast 与公证产物。”
  • “检查更新在代理网络下一直转圈,修复并验证后续重试。”

质量门

python3 scripts/validate_skill.py .

同时运行目标项目的更新器单测、打包、签名/公证和公开更新源验证。

依赖

  • Python 3.8+
  • PyYAML
  • 目标 Electron 项目的打包与签名工具

License

MIT

Skill manifest

Electron 增量更新 · Electron Delta Updates

为现有 Electron 应用交付可验证的增量更新链路;以用户实际安装包、公开更新源和安装结果为准,而不是以 CI 成功为准。

Triggers

Activate when

  • 用户说“给这个 Electron App 加增量自动更新”或“检查更新一直转圈”。
  • 用户提到 macOS Sparkle、appcast、DMG、blockmap、签名、公证或 delta 更新包。
  • User asks to “add a delta updater”, “ship Sparkle updates”, or “verify an Electron update release”.

Do not activate when

  • 用户只想修改版本号、更新日志或普通下载页;使用版本管理或发布工作流。
  • 用户要做网页热更新、PWA 缓存更新或移动端 OTA;使用相应平台的更新机制。

Workflow (MANDATORY)

You MUST follow these steps in order.

Step 0: Establish the update contract

  • Identify target platforms, package format, current updater library, update feed, signing owner, release channel, and rollback expectation.
  • Treat macOS delta-only behavior as an explicit product promise only when complete archives are intentionally excluded.
  • Separate four states: check, download/stage, install handoff, and post-restart verification.
  • Read references/electron-macos-delta.md for macOS Sparkle work and references/release-verification.md before modifying a release pipeline.

Step 1: Make checks bounded and recoverable

  • Use one in-flight check per update service.
  • Pass an AbortSignal through every network layer, including appcast fetches.
  • On timeout, abort the request, wait for stale updater state and connections to settle, then allow a later retry.
  • Configure the updater's own network session; do not assume it inherits a developer shell proxy.
  • Keep the UI state retryable after every timeout, download failure, or malformed feed.

Step 2: Enforce the artifact policy

  • Generate architecture-specific feed entries and match each delta to its source and target version.
  • Verify signature, length, architecture, and URL before staging an update.
  • In delta-only mode, stop on any complete-package fallback; surface a manual-install path instead of silently downloading a larger archive.
  • If a local feed proxy is required, bind it to loopback, permit only the verified delta URL, preserve HTTP range behavior, and keep the served artifact URL ending in .delta.

Step 3: Coordinate install and restart

  • Complete application cleanup before giving control to the native updater.
  • Keep the process alive until asynchronous install handoff is confirmed.
  • Use references/release-verification.md to verify the staged update and the installed version after relaunch.
  • For a normal user-invoked app restart without an update, use sgc-electron-app-relaunch instead.

Step 4: Validate the deliverable

  • Add tests for single-flight checks, timeout abort, stale-check recovery, rejected full fallback, feed proxy ranges, and asynchronous install handoff.
  • Run the project's format, lint, typecheck, test, package, signing, and notarization checks that apply.
  • Verify the public release page plus direct appcast and delta asset responses; compare the final artifact's version, digest, size, signature, and notarization state.
  • Report the updater backend, supported platforms, checked URLs, installed-version proof, and any manual-install condition.

Dependencies

  • An Electron project with a defined packaging and signing owner.
  • Sparkle tooling only for macOS Sparkle paths.

通用反馈闭环

用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:

  1. 先判断意见是 task-specific(仅本次)还是 reusable(可跨任务复用)。
  2. task-specific 只修改当前任务,不改 Skill。
  3. reusable 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
  4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
  5. reusable 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
Files (skills)
  • references
    • electron-macos-delta.md 1.1 KB
      # macOS Sparkle 增量更新契约
      
      ## 适用条件
      
      仅在应用以签名的 `.app` / DMG 发布、拥有稳定的 Sparkle 密钥和可公开读取的架构专属 appcast 时采用此路径。不要把开发态、未签名包或 ZIP 下载页当作自动更新测试替代品。
      
      ## 必须保持的边界
      
      - 检查:请求必须有截止时间,并把取消信号传进底层 fetch。
      - 下载:增量包的版本、架构、长度和签名必须与 appcast 一致。
      - 安装:下载完成不等于已安装;必须等待原生助手确认接手安装。
      - 回退:delta-only 产品将完整包回退视为失败,并告诉用户从发布页手动安装。
      
      ## 本地 feed 代理
      
      当原生更新器需要封闭的更新视图时,代理只应:
      
      1. 监听 `127.0.0.1` 的临时端口。
      2. 重写 appcast 中已校验的一个 delta enclosure。
      3. 把其他 enclosure 改成不可下载的地址。
      4. 保留 `.delta` 扩展名、`Range`、`If-Range`、`Content-Range` 和 backpressure。
      5. 在下载或安装结束后关闭服务器。
      
      不要把任意上游 URL 变成代理能力,也不要因为代理诊断变慢而阻塞真正的更新请求。
      
    • release-verification.md 798 B
      # 更新发布验证
      
      ## 发布前
      
      - 由最终已签名、已公证的包生成或刷新更新元数据;不要使用公证前的字节。
      - 确认稳定版、预览版和各架构 feed 没有互相引用。
      - 给更新检查、下载、失败、安装交接和启动后版本写最小自动化覆盖。
      
      ## 发布后
      
      1. 打开公开 release,确认版本、架构和下载包齐全。
      2. 直接请求各 appcast 与 delta 资源,记录成功响应与最终 URL。
      3. 对下载包执行项目要求的签名、公证、版本、大小和摘要检查。
      4. 从旧版本发起一次真实检查、下载、安装交接和重启。
      5. 重新读取已安装应用的版本,确认它与 feed 目标版本一致。
      
      绿色 CI、推送的 tag、或下载完成提示都不足以替代第五步。
      
  • scripts
    • validate_skill.py 11.4 KB
      #!/usr/bin/env python3
      """Validate a portable local LovStudio Skill source directory."""
      
      from __future__ import annotations
      
      import argparse
      import re
      import sys
      from pathlib import Path
      from typing import Any, Iterable
      
      try:
          import yaml
      except ImportError:
          print(
              "ERROR: PyYAML is required. Install it with: python3 -m pip install PyYAML",
              file=sys.stderr,
          )
          raise SystemExit(2)
      
      
      FRONTMATTER_KEYS = {"name", "description", "license", "allowed-tools", "metadata"}
      TEXT_SUFFIXES = {".md", ".json", ".yaml", ".yml", ".txt", ".svg", ".py"}
      JUNK_NAMES = {"__pycache__", ".DS_Store"}
      JUNK_SUFFIXES = {".pyc", ".pyo"}
      SKIP_DIRS = {".git", "dist", ".venv", "venv", "node_modules"}
      SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$")
      NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
      MARKDOWN_LINK_RE = re.compile(r"!?\[[^\]]*]\(([^)]+)\)")
      SKILL_PATH_RE = re.compile(r"\$(SKILL_DIR|KIT_DIR)/([A-Za-z0-9_./-]+)")
      
      
      class ValidationFailure(Exception):
          """Raised when source metadata cannot be parsed."""
      
      
      def read_text(path: Path) -> str:
          return path.read_text(encoding="utf-8")
      
      
      def compact_text(value: Any) -> str:
          return re.sub(r"\s+", " ", value).strip() if isinstance(value, str) else ""
      
      
      def split_frontmatter(path: Path) -> tuple[dict[str, Any], str]:
          text = read_text(path)
          if not text.startswith("---\n"):
              raise ValidationFailure(f"{path}: missing YAML frontmatter")
          marker = text.find("\n---\n", 4)
          if marker < 0:
              raise ValidationFailure(f"{path}: frontmatter is not closed")
          try:
              data = yaml.safe_load(text[4:marker])
          except yaml.YAMLError as exc:
              raise ValidationFailure(
                  f"{path}: standard YAML parser rejected frontmatter: {exc}"
              ) from exc
          if not isinstance(data, dict):
              raise ValidationFailure(f"{path}: frontmatter must be a mapping")
          return data, text[marker + 5 :]
      
      
      def iter_files(root: Path) -> Iterable[Path]:
          for path in root.rglob("*"):
              if any(part in SKIP_DIRS for part in path.relative_to(root).parts):
                  continue
              if path.is_file():
                  yield path
      
      
      def is_relative_to(path: Path, parent: Path) -> bool:
          try:
              path.relative_to(parent)
              return True
          except ValueError:
              return False
      
      
      def validate_skill_file(path: Path, errors: list[str]) -> dict[str, Any] | None:
          try:
              data, body = split_frontmatter(path)
          except ValidationFailure as exc:
              errors.append(str(exc))
              return None
      
          unexpected = sorted(set(data) - FRONTMATTER_KEYS)
          if unexpected:
              errors.append(f"{path}: unsupported frontmatter keys: {', '.join(unexpected)}")
      
          name = compact_text(data.get("name"))
          if not NAME_RE.fullmatch(name) or len(name) > 64:
              errors.append(f"{path}: name must be kebab-case and at most 64 characters")
      
          description = compact_text(data.get("description"))
          if not 50 <= len(description) <= 200:
              errors.append(
                  f"{path}: description must contain 50-200 characters "
                  f"(found {len(description)})"
              )
      
          metadata = data.get("metadata")
          if not isinstance(metadata, dict):
              errors.append(f"{path}: metadata must be a mapping")
          else:
              if not compact_text(metadata.get("author")):
                  errors.append(f"{path}: metadata.author is required")
              if not SEMVER_RE.fullmatch(compact_text(metadata.get("version"))):
                  errors.append(f"{path}: metadata.version must use SemVer")
              tags = metadata.get("tags")
              if not isinstance(tags, list) or not tags or not all(
                  isinstance(tag, str) and tag.strip() for tag in tags
              ):
                  errors.append(f"{path}: metadata.tags must be a non-empty list")
              dependencies = metadata.get("dependencies", [])
              if not isinstance(dependencies, list):
                  errors.append(f"{path}: metadata.dependencies must be a list")
      
          trigger_block = re.search(
              r"(?ms)^##\s+Triggers\s*$([\s\S]*?)(?=^##\s+|\Z)", body
          )
          if not trigger_block:
              errors.append(f"{path}: add an explicit '## Triggers' section")
          else:
              block = trigger_block.group(1)
              if len(re.findall(r"(?m)^\s*-\s+\S", block)) < 3:
                  errors.append(f"{path}: add two activation examples and one non-trigger")
              if not re.search(r"[\u3400-\u9fff]", block):
                  errors.append(f"{path}: add a concrete Chinese trigger phrase")
              if not re.search(r"(?i)\b(?:the|a|an|create|build|help|publish|review|use)\b", block):
                  errors.append(f"{path}: add a concrete English trigger phrase")
          if not re.search(
              r"(?mi)^###\s+(?:Do not activate when|Non-triggers?|不应触发|不要触发)\s*$",
              body,
          ):
              errors.append(f"{path}: add explicit non-trigger conditions")
          if len(read_text(path).splitlines()) >= 500:
              errors.append(f"{path}: keep SKILL.md below 500 lines")
          if not body.strip():
              errors.append(f"{path}: body is empty")
          return data
      
      
      def load_yaml(path: Path, errors: list[str]) -> dict[str, Any] | None:
          try:
              data = yaml.safe_load(read_text(path))
          except yaml.YAMLError as exc:
              errors.append(f"{path}: standard YAML parser rejected file: {exc}")
              return None
          if not isinstance(data, dict):
              errors.append(f"{path}: expected a YAML mapping")
              return None
          return data
      
      
      def validate_kit(root: Path, skill_names: set[str], errors: list[str]) -> None:
          manifest = root / "kit.yaml"
          if not manifest.exists():
              return
          data = load_yaml(manifest, errors)
          if data is None:
              return
          modules = data.get("modules")
          if not isinstance(modules, list) or not modules:
              errors.append(f"{manifest}: modules must be a non-empty list")
              return
          module_ids: set[str] = set()
          for index, module in enumerate(modules):
              label = f"{manifest}: modules[{index}]"
              if not isinstance(module, dict):
                  errors.append(f"{label}: expected a mapping")
                  continue
              module_id = compact_text(module.get("id"))
              skill_name = compact_text(module.get("skill"))
              relative = compact_text(module.get("path"))
              if not module_id or module_id in module_ids:
                  errors.append(f"{label}: id is required and must be unique")
              module_ids.add(module_id)
              module_path = (root / relative).resolve()
              if (
                  not relative
                  or not is_relative_to(module_path, root.resolve())
                  or not (module_path / "SKILL.md").is_file()
              ):
                  errors.append(f"{label}: missing module at '{relative}/SKILL.md'")
              if skill_name not in skill_names:
                  errors.append(f"{label}: unresolved child skill '{skill_name}'")
          pipelines = data.get("pipelines")
          if not isinstance(pipelines, dict) or not pipelines:
              errors.append(f"{manifest}: pipelines must be a non-empty mapping")
              return
          for pipeline, sequence in pipelines.items():
              if not isinstance(sequence, list) or not sequence:
                  errors.append(f"{manifest}: pipeline '{pipeline}' must be a non-empty list")
                  continue
              missing = [str(item) for item in sequence if item not in module_ids]
              if missing:
                  errors.append(
                      f"{manifest}: pipeline '{pipeline}' has unknown modules: "
                      + ", ".join(missing)
                  )
      
      
      def validate_local_references(root: Path, errors: list[str]) -> None:
          for path in iter_files(root):
              if path.suffix.lower() != ".md":
                  continue
              text = read_text(path)
              for raw in MARKDOWN_LINK_RE.findall(text):
                  target = raw.strip().split(maxsplit=1)[0].strip("<>").split("#", 1)[0]
                  if (
                      not target
                      or re.match(r"^[a-z][a-z0-9+.-]*:", target, re.I)
                      or any(token in target for token in ("TODO", "{", "}"))
                  ):
                      continue
                  if not (path.parent / target).resolve().exists():
                      errors.append(f"{path}: broken local link '{target}'")
              skill_root = path.parent if path.name == "SKILL.md" else root
              for variable, target in SKILL_PATH_RE.findall(text):
                  if "TODO" in target:
                      continue
                  base = skill_root if variable == "SKILL_DIR" else root
                  resolved = (base / target.rstrip(".,;:)")).resolve()
                  if not is_relative_to(resolved, root.resolve()) or not resolved.exists():
                      errors.append(f"{path}: missing required resource '${variable}/{target}'")
      
      
      def validate_hygiene(root: Path, errors: list[str]) -> None:
          private_path = re.compile(r"(?:/Users/[^/\s]+/|[A-Za-z]:\\\\Users\\\\[^\\\s]+\\\\)")
          for path in root.rglob("*"):
              if any(part in SKIP_DIRS for part in path.relative_to(root).parts):
                  continue
              if path.name in JUNK_NAMES or path.suffix.lower() in JUNK_SUFFIXES:
                  errors.append(f"{path}: generated/cache artifact must not ship")
          for path in iter_files(root):
              if path.suffix.lower() not in TEXT_SUFFIXES or path.name == "validate_skill.py":
                  continue
              text = read_text(path)
              if private_path.search(text):
                  errors.append(f"{path}: contains a private absolute user path")
              if path.name != "init_skill.py" and re.search(r"\bTODO\s*[::]", text):
                  errors.append(f"{path}: unresolved TODO placeholder")
          for relative in ("workbuddy", "scripts/build_workbuddy.py"):
              if (root / relative).exists():
                  errors.append(
                      f"{root / relative}: platform distribution artifacts belong to skill-publish"
                  )
      
      
      def validate_source(root: Path, errors: list[str]) -> None:
          root_skill = root / "SKILL.md"
          skill_files = [root_skill, *sorted((root / "skills").glob("*/SKILL.md"))]
          if not root_skill.is_file():
              errors.append(f"{root_skill}: file is required")
              return
          parsed: list[tuple[Path, dict[str, Any]]] = []
          for path in skill_files:
              data = validate_skill_file(path, errors)
              if data:
                  parsed.append((path, data))
          names = {compact_text(data.get("name")) for _, data in parsed}
          if len(names) != len(parsed):
              errors.append(f"{root}: every embedded Skill must have a unique name")
          validate_kit(root, names, errors)
      
          readme = root / "README.md"
          if not readme.is_file():
              errors.append(f"{readme}: file is required")
          elif parsed:
              metadata = parsed[0][1].get("metadata")
              version = compact_text(metadata.get("version")) if isinstance(metadata, dict) else ""
              if version and f"version-{version}-" not in read_text(readme):
                  errors.append(f"{readme}: version badge must match {version}")
      
          validate_hygiene(root, errors)
          validate_local_references(root, errors)
      
      
      def main() -> int:
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument("path", type=Path, help="Local Skill source directory")
          args = parser.parse_args()
          root = args.path.expanduser().resolve()
          if not root.is_dir():
              print(f"ERROR: directory does not exist: {root}", file=sys.stderr)
              return 2
          errors: list[str] = []
          validate_source(root, errors)
          if errors:
              print(f"FAILED: {len(errors)} issue(s)")
              for error in errors:
                  print(f"- {error}")
              return 1
          print(f"PASSED: source validation ({root})")
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
  • .gitignore 84 B · in bundle
  • CHANGELOG.md 192 B
    # Changelog
    
    ## [0.2.0] - 2026-08-24
    
    ### Added
    
    - add the shared feedback-classification and approval-invalidation gate used by every LovStudio Skill
    
    ## 0.1.0
    
    - Initial local Skill source.
    
  • LICENSE 1 KB · in bundle
  • README.md 833 B
    # Electron 增量更新 · Electron Delta Updates
    
    ![Version](https://img.shields.io/badge/version-0.2.0-CC785C)
    
    为 Electron 应用建立并验证可恢复的增量自动更新流程。
    
    ## 本地安装
    
    将目录链接到本地 Agent Skills 目录,链接名为 `sgc-electron-delta-updater`。安装后可直接说“给这个 Electron App 加增量自动更新”。
    
    ## 使用
    
    - “为 macOS Electron 应用配置 Sparkle delta-only 更新,并验证 appcast 与公证产物。”
    - “检查更新在代理网络下一直转圈,修复并验证后续重试。”
    
    ## 质量门
    
    ```bash
    python3 scripts/validate_skill.py .
    ```
    
    同时运行目标项目的更新器单测、打包、签名/公证和公开更新源验证。
    
    ## 依赖
    
    - Python 3.8+
    - PyYAML
    - 目标 Electron 项目的打包与签名工具
    
    ## License
    
    MIT
    
  • SKILL.md 4.8 KB
    ---
    name: sgc-electron-delta-updater
    description: >
      为 Electron 桌面应用设计、实现和验证增量自动更新,覆盖 macOS Sparkle、签名、appcast、发布产物与失败拦截。用户提到增量更新、Sparkle、检查更新、appcast、DMG 更新或 delta updater 时使用。
    license: MIT
    metadata:
      author: lovstudio
      version: "0.2.0"
      tags:
        - electron
        - auto-update
        - sparkle
        - delta
      compatibility: "Electron projects; macOS delta mode requires Sparkle-compatible packaging and signing."
      dependencies: []
    ---
    
    # Electron 增量更新 · Electron Delta Updates
    
    为现有 Electron 应用交付可验证的增量更新链路;以用户实际安装包、公开更新源和安装结果为准,而不是以 CI 成功为准。
    
    ## Triggers
    
    ### Activate when
    
    - 用户说“给这个 Electron App 加增量自动更新”或“检查更新一直转圈”。
    - 用户提到 macOS Sparkle、appcast、DMG、blockmap、签名、公证或 delta 更新包。
    - User asks to “add a delta updater”, “ship Sparkle updates”, or “verify an Electron update release”.
    
    ### Do not activate when
    
    - 用户只想修改版本号、更新日志或普通下载页;使用版本管理或发布工作流。
    - 用户要做网页热更新、PWA 缓存更新或移动端 OTA;使用相应平台的更新机制。
    
    ## Workflow (MANDATORY)
    
    **You MUST follow these steps in order.**
    
    ### Step 0: Establish the update contract
    
    - Identify target platforms, package format, current updater library, update feed, signing owner, release channel, and rollback expectation.
    - Treat macOS delta-only behavior as an explicit product promise only when complete archives are intentionally excluded.
    - Separate four states: check, download/stage, install handoff, and post-restart verification.
    - Read [references/electron-macos-delta.md](references/electron-macos-delta.md) for macOS Sparkle work and [references/release-verification.md](references/release-verification.md) before modifying a release pipeline.
    
    ### Step 1: Make checks bounded and recoverable
    
    - Use one in-flight check per update service.
    - Pass an `AbortSignal` through every network layer, including appcast fetches.
    - On timeout, abort the request, wait for stale updater state and connections to settle, then allow a later retry.
    - Configure the updater's own network session; do not assume it inherits a developer shell proxy.
    - Keep the UI state retryable after every timeout, download failure, or malformed feed.
    
    ### Step 2: Enforce the artifact policy
    
    - Generate architecture-specific feed entries and match each delta to its source and target version.
    - Verify signature, length, architecture, and URL before staging an update.
    - In delta-only mode, stop on any complete-package fallback; surface a manual-install path instead of silently downloading a larger archive.
    - If a local feed proxy is required, bind it to loopback, permit only the verified delta URL, preserve HTTP range behavior, and keep the served artifact URL ending in `.delta`.
    
    ### Step 3: Coordinate install and restart
    
    - Complete application cleanup before giving control to the native updater.
    - Keep the process alive until asynchronous install handoff is confirmed.
    - Use [references/release-verification.md](references/release-verification.md) to verify the staged update and the installed version after relaunch.
    - For a normal user-invoked app restart without an update, use `sgc-electron-app-relaunch` instead.
    
    ### Step 4: Validate the deliverable
    
    - Add tests for single-flight checks, timeout abort, stale-check recovery, rejected full fallback, feed proxy ranges, and asynchronous install handoff.
    - Run the project's format, lint, typecheck, test, package, signing, and notarization checks that apply.
    - Verify the public release page plus direct appcast and delta asset responses; compare the final artifact's version, digest, size, signature, and notarization state.
    - Report the updater backend, supported platforms, checked URLs, installed-version proof, and any manual-install condition.
    
    ## Dependencies
    
    - An Electron project with a defined packaging and signing owner.
    - Sparkle tooling only for macOS Sparkle paths.
    
    ## 通用反馈闭环
    
    用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:
    
    1. 先判断意见是 `task-specific`(仅本次)还是 `reusable`(可跨任务复用)。
    2. `task-specific` 只修改当前任务,不改 Skill。
    3. `reusable` 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
    4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
    5. `reusable` 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related