> ## Documentation Index
> Fetch the complete documentation index at: https://docs.devin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 对每个 PR 强制执行迁移检查清单

export const UseCaseHero = ({title, description, prompt, category, features, devinUrl, agent, intent, playbookId, type}) => {
  const encodedPrompt = encodeURIComponent(prompt || '');
  const tag = 'docs-use-case-gallery';
  const utm = 'utm_source=docs&utm_medium=use-case-gallery&utm_campaign=hero-cta';
  const agentParams = (agent ? '&agent=' + agent : '') + (intent ? '&intent=' + intent : '') + (playbookId ? '&playbookId=' + playbookId : '');
  const devinHref = type === 'schedule' ? 'https://app.devin.ai/settings/schedules/create?' + utm + agentParams + (prompt ? '&prompt=' + encodedPrompt : '') : type === 'review' ? 'https://app.devin.ai/review?' + utm : agent === 'ada' ? 'https://app.devin.ai/search?' + utm + '&noSubmit=true' + (prompt ? '&prompt=' + encodedPrompt : '') : devinUrl ? devinUrl.includes('?') ? devinUrl + '&' + utm + agentParams : devinUrl + '?' + utm + agentParams : prompt ? 'https://app.devin.ai/?tags=' + tag + '&' + utm + agentParams + '&prompt=' + encodedPrompt : 'https://app.devin.ai/?' + utm + agentParams;
  const buttonLabel = type === 'schedule' ? 'Schedule in Devin ↗' : type === 'review' ? 'Set Up Devin Review ↗' : agent === 'advanced' ? 'Try in Devin ↗' : agent === 'dana' ? 'Try in Dana ↗' : agent === 'ada' ? 'Try in Ask Devin ↗' : 'Try in Devin ↗';
  const featureList = features ? features.split(',').map(f => f.trim()) : [];
  return <div className="uc-hero">
      <div className="uc-hero-inner">
        <div className="uc-hero-left">
          <h1 className="uc-hero-title">{title}</h1>
          <p className="uc-hero-desc">{description}</p>
          <div>
            <a href={devinHref} target="_blank" rel="noopener noreferrer" className="try-in-devin-btn">
              {buttonLabel}
            </a>
          </div>
        </div>
        <div className="uc-hero-meta">
          <div className="uc-meta-item">
            <span className="uc-meta-label">Author</span>
            <span className="uc-meta-value">Cognition</span>
          </div>
          <div className="uc-meta-item">
            <span className="uc-meta-label">Category</span>
            <span className="uc-meta-value">{category}</span>
          </div>
          {featureList.length > 0 && <div className="uc-meta-item">
              <span className="uc-meta-label">Features</span>
              <span className="uc-meta-value">{featureList.join(', ')}</span>
            </div>}
        </div>
      </div>
    </div>;
};

export const PromptBlock = ({children, type, agent, intent, playbookId}) => {
  var utm = 'utm_source=docs&utm_medium=use-case-gallery&utm_campaign=prompt-block';
  var tag = 'docs-use-case-gallery';
  var agentParams = (agent ? '&agent=' + agent : '') + (intent ? '&intent=' + intent : '') + (playbookId ? '&playbookId=' + playbookId : '');
  var label = type === 'schedule' ? 'Schedule in Devin' : type === 'playbook' ? 'Create Playbook' : type === 'knowledge' ? 'Add to Knowledge' : agent === 'advanced' ? 'Try in Devin' : agent === 'dana' ? 'Try in Dana' : agent === 'ada' ? 'Try in Ask Devin' : 'Try in Devin';
  var buildUrl = function (text) {
    var encoded = encodeURIComponent(text);
    if (type === 'schedule') return 'https://app.devin.ai/settings/schedules/create?' + utm + agentParams + '&prompt=' + encoded;
    if (type === 'playbook') return 'https://app.devin.ai/settings/playbooks/create?' + utm + '&body=' + encoded;
    if (type === 'knowledge') return 'https://app.devin.ai/knowledge?' + utm + '&body=' + encoded;
    if (agent === 'ada') return 'https://app.devin.ai/search?' + utm + '&noSubmit=true&prompt=' + encoded;
    return 'https://app.devin.ai/?tags=' + tag + '&' + utm + agentParams + '&prompt=' + encoded;
  };
  const ref = React.useRef(null);
  const [href, setHref] = React.useState('#');
  React.useEffect(() => {
    if (!ref.current) return;
    var codeEl = ref.current.querySelector('pre code');
    if (codeEl) {
      var text = codeEl.textContent.trim();
      if (text) setHref(buildUrl(text));
    }
    var header = ref.current.querySelector('[data-component-part="code-block-header"]');
    if (header && !header.querySelector('.prompt-block-devin-link')) {
      var link = document.createElement('a');
      link.href = href;
      link.target = '_blank';
      link.rel = 'noopener noreferrer';
      link.className = 'prompt-block-devin-link';
      link.style.cssText = 'display:inline-flex;align-items:center;gap:6px;text-decoration:none;color:#fff;font-size:11px;font-weight:500;padding:4px 10px;border-radius:6px;white-space:nowrap;background:#317CFF;transition:background 0.2s;margin-left:8px;';
      link.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> ' + label;
      link.onmouseenter = function () {
        link.style.background = '#2968D9';
      };
      link.onmouseleave = function () {
        link.style.background = '#317CFF';
      };
      header.appendChild(link);
    }
    var existingLink = ref.current.querySelector('.prompt-block-devin-link');
    if (existingLink && href !== '#') existingLink.href = href;
  });
  return <div className="prompt-block" ref={ref}>{children}</div>;
};

<UseCaseHero title="在每个 PR 中强制执行迁移检查清单" description="构建一个仓库技能，让 Devin 在每次 PR（拉取请求）涉及数据库迁移时，捕获破坏性操作、验证回滚安全性，并校验架构更改。" prompt="在 .agents/skills/migration-checklist/migration-checklist.md 下创建一个 skill 文件，让 Devin 在 PR 中审查每一次数据库迁移，检查是否存在破坏性操作、外键缺少索引、回滚安全性以及架构漂移——并在所有检查通过之前阻止该 PR。" category="核心 Devin" features="Skills" />

<div className="uc-detail-wrapper">
  <Tip>不想手动配置？将本页链接粘贴到 Devin 会话中，让它为你完成所有设置。</Tip>

  <Steps>
    <Step title="创建迁移核对清单技能">
      仓库技能是一个 markdown 文件，你需要将其提交到任意仓库中的 `.agents/skills/<your-skill>/` 目录下。Devin 能看到所有已连接仓库中的所有技能——你可以手动触发它们，或者当 Devin 检测到相关情况时自动触发。这个技能会精确告知 Devin 在打开或更新 PR 之前应该如何审查数据库迁移，从而捕捉到常规代码审查通常会遗漏的错误。

      在你的代码仓库中提交 `.agents/skills/migration-checklist/migration-checklist.md` 文件：

      ```markdown theme={null}
      # 迁移安全检查清单

      ## 描述
      在开启或更新拉取请求（PR）之前，检查当前 PR 中的每个数据库迁移是否存在安全问题。

      ## 使用时机
      当 PR 差异中包含 `db/migrate/`（Rails）、`migrations/`（Django）或 `prisma/migrations/`（Prisma）中新增或修改的文件时，调用此技能。

      ## 检查清单

      ### 1. 检测破坏性操作
      扫描每个迁移文件，查找以下内容：
      - `DROP TABLE` 或 `drop_table`
      - `DROP COLUMN`、`remove_column` 或列删除操作
      - `TRUNCATE`
      - 导致精度损失的数据类型变更（例如 `text` → `varchar(50)`）

      如发现上述任何情况，在 PR 中添加评论，标记该操作并确认迁移中是否包含数据备份步骤。

      ### 2. 验证外键索引
      对于每个 `add_reference`、`add_foreign_key` 或以 `_id` 结尾的新列，确认存在对应的索引。如果不存在，请在提交前将其添加到迁移中。

      ### 3. 检查回滚安全性
      - 在测试数据库上运行 `bin/rails db:migrate:rollback STEP=<n>`（其中 n 为新迁移的数量）。
      - 如果回滚失败，添加 `down` 方法或可逆块后重试。
      - 在 PR 描述中报告所有不可逆的迁移。

      ### 4. 验证 schema 文件是否为最新版本
      运行迁移后，将 `db/schema.rb`（或 `structure.sql`）与 PR 中的版本进行差异比较。如果存在差异，请重新生成 schema 文件并将其包含在提交中。

      ### 5. 运行模型测试
      执行 `bin/rails test test/models/`，捕获因 schema 变更导致验证或关联失效的问题。所有测试必须通过后方可开启 PR。
      ```

      一旦这个文件被提交，Devin 就会将其视为一项可用技能。每当当前会话涉及此代码库中的迁移文件时，Devin 都可以自动触发该检查清单，或者你也可以在任何时候手动执行它。
    </Step>

    <Step title="在实际迁移中查看该技能的触发方式">
      当 Devin 处理会添加或修改迁移文件的任务时，它会读取 diff，匹配迁移检查清单技能，并在打开 PR 之前按照检查清单执行操作。实际执行过程如下：

      1. **扫描 diff** — Devin 看到 `db/migrate/` 中有新文件，并激活迁移检查清单技能
      2. **标记破坏性操作** — 该迁移移除了 `legacy_email` 列。Devin 添加了一条 PR 评论：
         > `remove_column :users, :legacy_email` 是一个破坏性操作。
         > 已验证：迁移包含数据备份步骤，会在删除前将值复制到
         > `user_archives` 中。
      3. **补充缺失索引** — 该迁移向 `invoices` 表添加了 `account_id` 列，但没有索引。Devin 将 `add_index :invoices, :account_id` 追加到迁移文件中
      4. **执行回滚** — Devin 在测试数据库上执行 `bin/rails db:migrate:rollback STEP=1`。回滚执行成功
      5. **重新生成 schema** — Devin 运行 `bin/rails db:schema:dump`，检测到 `db/schema.rb` 中的 diff，并在提交中包含更新后的文件
      6. **运行模型测试** — 所有模型测试均通过。Devin 打开 PR，并附上每项检查结果的摘要

      PR 描述中包含一个检查清单，展示哪些检查通过了、哪些是 Devin 修复的，这样审阅者就可以将精力集中在业务逻辑上，而不是迁移实现细节上。
    </Step>

    <Step title="将该技能适配到你的 ORM 和技术栈">
      上面的检查清单主要针对 Rails，但同样的结构适用于任何 ORM。请让 Devin 按你的技术栈重写这个技能：

      <PromptBlock>
        ```txt Adapt for Django theme={null}
        为我们的 Django 项目重写迁移检查清单技能：
        - 扫描 `apps/*/migrations/` 中的文件，查找具有破坏性的操作，
          如 RemoveField、DeleteModel，以及会导致数据丢失的 AlterField
        - 运行 `python manage.py migrate` 之后，确认
          `python manage.py showmigrations` 中不存在未应用的迁移
        - 运行 `python manage.py test --tag=models` 来验证模式变更
        - 检查各个迁移是否提供了 `reverse_code` 或 `RunSQL` 的反向操作，
          以确保可安全回滚
        ```
      </PromptBlock>

      <PromptBlock>
        ```txt Adapt for Prisma theme={null}
        为我们的 Prisma 项目重写迁移检查清单技能：
        - 扫描 `prisma/migrations/` 中的文件，查找 DROP TABLE、DROP COLUMN，
          以及会缩小字段宽度的 ALTER COLUMN
        - 对测试数据库运行 `npx prisma migrate deploy`，
          并确认其执行成功
        - 运行 `npx prisma migrate diff`，验证 schema.prisma 与
          已应用的迁移状态一致
        - 执行 `npm test -- --grep "model"`，以捕获损坏的关联关系
        ```
      </PromptBlock>
    </Step>

    <Step title="逐步扩展检查清单">
      每一次迁移事故都能暴露出清单未覆盖的空白。每发生一次事故，就补充一条规则——只需要向 skill 文件提交一行更改。

      下面是团队在真实事故后常见的补充内容：

      ```markdown theme={null}
      ### 6. Enforce migration naming conventions
      Reject migrations that don't follow the pattern
      `YYYYMMDDHHMMSS_verb_noun.rb` (e.g. `add_index_to_invoices`).
      Rename the file if needed.

      ### 7. Check for long-running locks
      Flag any `add_column` on tables with >1M rows that doesn't use
      `disable_ddl_transaction!` (Postgres) or `ALGORITHM=INPLACE`
      (MySQL). Large tables need non-blocking migrations.

      ### 8. Require a migration test
      Ensure a corresponding test file exists at
      `test/migrations/YYYYMMDDHHMMSS_migration_name_test.rb`.
      If missing, generate a skeleton test that runs the migration
      up and down.
      ```

      由于 skill 文件位于你的代码仓库中，这些规则会走代码评审流程——整个团队共同决定要检查的内容，并且它始终与迁移工具保持同步。
    </Step>
  </Steps>
</div>
