diff --git a/.github/workflows/ci-failure-notify.yml b/.github/workflows/ci-failure-notify.yml new file mode 100644 index 0000000..4f40ba6 --- /dev/null +++ b/.github/workflows/ci-failure-notify.yml @@ -0,0 +1,90 @@ +name: CI Failure Slack Notify + +on: + workflow_run: + workflows: + - CI + - Playwright E2E Tests + - TruffleHog Secret Scan + - Lighthouse CI + types: [completed] + branches: [main] + +jobs: + notify-failure: + name: Notify Slack on failure + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + steps: + - name: Send Slack notification + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }} + WORKFLOW_NAME: ${{ github.event.workflow_run.name }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + ACTOR: ${{ github.event.workflow_run.actor.login }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + SERVER_URL: ${{ github.server_url }} + REPOSITORY: ${{ github.repository }} + run: | + SHORT_SHA="${COMMIT_SHA:0:7}" + COMMIT_URL="${SERVER_URL}/${REPOSITORY}/commit/${COMMIT_SHA}" + + PAYLOAD=$(jq -n \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg text "🚨 ${WORKFLOW_NAME} が main ブランチで失敗しました" \ + --arg workflow "$WORKFLOW_NAME" \ + --arg actor "$ACTOR" \ + --arg commit_url "$COMMIT_URL" \ + --arg short_sha "$SHORT_SHA" \ + --arg run_url "$RUN_URL" \ + '{ + channel: $channel, + text: $text, + blocks: [ + { + type: "header", + text: { + type: "plain_text", + text: "🚨 CI/CD 失敗通知(main ブランチ)", + emoji: true + } + }, + { + type: "section", + fields: [ + { type: "mrkdwn", text: ("*ワークフロー:*\n" + $workflow) }, + { type: "mrkdwn", text: "*ブランチ:*\n`main`" }, + { type: "mrkdwn", text: ("*実行者:*\n" + $actor) }, + { type: "mrkdwn", text: ("*コミット:*\n<" + $commit_url + "|" + $short_sha + ">") } + ] + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "📋 ログを確認", emoji: true }, + url: $run_url, + style: "danger" + } + ] + } + ] + }') + + RESPONSE=$(curl -sf -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") || true + + if [ -z "$RESPONSE" ]; then + echo "::error::Slack API request failed (network error or empty response)" + exit 1 + fi + + if ! echo "$RESPONSE" | jq -e '.ok == true' > /dev/null; then + ERROR=$(echo "$RESPONSE" | jq -r '.error // "unknown_error"') + echo "::error::Slack API error: ${ERROR}" + exit 1 + fi diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml new file mode 100644 index 0000000..13d1853 --- /dev/null +++ b/.github/workflows/dependabot-automerge.yml @@ -0,0 +1,50 @@ +name: Dependabot自動マージ + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot-automerge: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + + steps: + - name: Dependabotメタデータ取得 + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: 更新タイプをチェック + id: check-update-type + run: | + # グループ化PRの場合、updated-dependencies-jsonから各依存関係の更新タイプを確認 + DEPENDENCIES='${{ steps.metadata.outputs.updated-dependencies-json }}' + + # 依存関係が空の場合はスキップ + if [ -z "$DEPENDENCIES" ] || [ "$DEPENDENCIES" = "[]" ]; then + echo "can-automerge=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # すべての依存関係がpatchまたはminorかチェック + CAN_MERGE=$(echo "$DEPENDENCIES" | jq -r ' + if length == 0 then + false + else + all(.[]; .["update-type"] == "version-update:semver-patch" or .["update-type"] == "version-update:semver-minor") + end + ') + + echo "can-automerge=$CAN_MERGE" >> $GITHUB_OUTPUT + echo "依存関係の更新タイプ確認完了: can-automerge=$CAN_MERGE" + + - name: minor/patchを自動マージ + if: steps.check-update-type.outputs.can-automerge == 'true' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/devin-slack-trigger.yml b/.github/workflows/devin-slack-trigger.yml new file mode 100644 index 0000000..c5aa5e8 --- /dev/null +++ b/.github/workflows/devin-slack-trigger.yml @@ -0,0 +1,84 @@ +name: Devin Slack Trigger + +on: + issues: + types: [labeled] + +permissions: + issues: write + +jobs: + trigger-devin: + name: Trigger Devin via Slack + runs-on: ${{ vars.RUNNER_LINUX || 'ubuntu-latest' }} + # Slack への通知トリガのみの小さなジョブなので短めに打ち切る + timeout-minutes: 10 + if: github.event.label.name == 'devin' + concurrency: + group: devin-trigger-${{ github.event.issue.number }} + cancel-in-progress: false + + steps: + - name: Check for duplicate trigger + id: duplicate-check + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + } + ); + const alreadyTriggered = comments.some(c => + c.body?.includes('') + ); + if (alreadyTriggered) { + core.notice('Devin session already triggered for this issue. Skipping.'); + } + core.setOutput('skip', String(alreadyTriggered)); + + - name: Post message to Slack + if: steps.duplicate-check.outputs.skip == 'false' + id: slack-post + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }} + DEVIN_USER_ID: ${{ vars.SLACK_DEVIN_USER_ID }} + with: + script: | + const issue = context.payload.issue; + + const message = `!swe\n[${issue.title}]\n<@${process.env.DEVIN_USER_ID}> Please handle this issue. First, create a plan and share it before implementing. Think in English, respond in Japanese.\n${issue.html_url}`; + + const resp = await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.SLACK_BOT_TOKEN}`, + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ + channel: process.env.SLACK_CHANNEL_ID, + text: message, + }), + }); + + const data = await resp.json(); + if (!data.ok) { + core.setFailed(`Slack API error: ${data.error || 'unknown'}`); + } + + - name: Post marker comment + if: success() && steps.duplicate-check.outputs.skip == 'false' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: ':robot_face: Devin session triggered via Slack ', + }); diff --git a/.github/workflows/merge-quality-gate.yml b/.github/workflows/merge-quality-gate.yml new file mode 100644 index 0000000..94b8f2f --- /dev/null +++ b/.github/workflows/merge-quality-gate.yml @@ -0,0 +1,142 @@ +name: Merge Quality Gate + +on: + pull_request: + branches: [main] + types: [closed] + +jobs: + check-quality: + name: Post-merge quality check + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + permissions: + contents: read + checks: read + pull-requests: read + steps: + - name: Check for unresolved quality issues + id: check + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # aligned + with: + script: | + const prNumber = context.payload.pull_request.number; + + // PRコメントを取得してLighthouseスコアを確認 + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const issues = []; + + // Lighthouseスコア確認 + const lhComment = comments.find(c => c.body?.includes('')); + if (lhComment) { + // 🔴🟠のみマッチ(🟢=良好スコアは品質問題として検出しない) + const perfMatch = lhComment.body.match(/Performance\s*\|\s*(?:🔴|🟠)\s*(\d+)/); + if (perfMatch) { + const perfScore = parseInt(perfMatch[1]); + issues.push({ + type: 'lighthouse', + message: `Lighthouse Performance: ${perfScore}/100`, + score: perfScore, + }); + } + } + + // protect-main警告が残っていないか確認 + const protectComment = comments.find(c => c.body?.includes('')); + if (protectComment) { + issues.push({ + type: 'protect-main', + message: 'mainブランチ保護ポリシー警告が未解消', + }); + } + + // チェック結果を出力 + if (issues.length > 0) { + core.setOutput('has_issues', 'true'); + core.setOutput('issues', JSON.stringify(issues)); + core.info(`未解消の品質問題: ${issues.length}件`); + } else { + core.setOutput('has_issues', 'false'); + core.info('品質問題なし'); + } + + - name: Notify Slack if unresolved issues + if: steps.check.outputs.has_issues == 'true' + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }} + ISSUES_JSON: ${{ steps.check.outputs.issues }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + ACTOR: ${{ github.event.pull_request.merged_by.login }} + run: | + ISSUE_LIST=$(echo "$ISSUES_JSON" | jq -r '[.[] | "• " + .message] | join("\n")') + + PAYLOAD=$(jq -n \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg text "⚠️ 品質問題が未解消のままマージされました: #${PR_NUMBER}" \ + --arg pr_url "$PR_URL" \ + --arg pr_number "$PR_NUMBER" \ + --arg pr_title "$PR_TITLE" \ + --arg actor "$ACTOR" \ + --arg issues "$ISSUE_LIST" \ + '{ + channel: $channel, + text: $text, + blocks: [ + { + type: "header", + text: { + type: "plain_text", + text: "⚠️ 品質問題未解消マージ通知", + emoji: true + } + }, + { + type: "section", + fields: [ + { type: "mrkdwn", text: ("*PR:*\n<" + $pr_url + "|#" + $pr_number + " " + $pr_title + ">") }, + { type: "mrkdwn", text: ("*マージ実行者:*\n" + $actor) } + ] + }, + { + type: "section", + text: { + type: "mrkdwn", + text: ("*未解消の問題:*\n" + $issues) + } + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "📋 PRを確認", emoji: true }, + url: $pr_url + } + ] + } + ] + }') + + RESPONSE=$(curl -sf -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") || true + + if [ -z "$RESPONSE" ]; then + echo "::error::Slack API request failed (network error or empty response)" + exit 1 + fi + + if ! echo "$RESPONSE" | jq -e '.ok == true' > /dev/null; then + ERROR=$(echo "$RESPONSE" | jq -r '.error // "unknown_error"') + echo "::error::Slack API error: ${ERROR}" + exit 1 + fi diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml new file mode 100644 index 0000000..a305bac --- /dev/null +++ b/.github/workflows/trufflehog.yml @@ -0,0 +1,25 @@ +name: TruffleHog Secret Scan + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + security-events: write + +jobs: + trufflehog: + name: Secret Scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + fetch-depth: 0 + + - name: TruffleHog OSS + uses: trufflesecurity/trufflehog@702bbefa350895c57f0e303eeb2855a1c637908f # v3.92.2 + with: + extra_args: --only-verified