Skip to content
Merged
90 changes: 90 additions & 0 deletions .github/workflows/ci-failure-notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: CI Failure Slack Notify

on:
workflow_run:
workflows:
- CI
- Playwright E2E Tests
- TruffleHog Secret Scan
- Lighthouse CI
Comment on lines +5 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify workflow `name:` fields match the list referenced in ci-failure-notify.yml
fd -e yml -e yaml . .github/workflows --exec sh -c 'echo "== {} =="; rg -n "^name:" "{}"'

Repository: kouiso/sns-app

Length of output: 571


🏁 Script executed:

#!/bin/bash
set -e
printf '== ci-failure-notify.yml names and workflow trigger block ==\n'
rg -n "^(name:|  workflow |      )|on:" .github/workflows/ci-failure-notify.yml
printf '\n== all referenced workflow file candidates by basename/name words ==\n'
for candidate in CI "Playwright E2E Tests" "TruffleHog Secret Scan" "Lighthouse CI"; do
  printf '\n--- "%s" ---\n' "$candidate"
  fd -e yml -e yaml -i "$candidate" .github/workflows || true
done
printf '\n== workflow names in ci failure notify context with line numbers ==\n'
sed -n '1,40p' .github/workflows/ci-failure-notify.yml | nl -ba

Repository: kouiso/sns-app

Length of output: 3677


Align the failure notify workflow list with actual workflow name: declarations.

Only TruffleHog Secret Scan has a matching declaration in .github/workflows/trufflehog.yml. CI, Playwright E2E Tests, and Lighthouse CI have no name: matches, so failures from those workflows will not fire this notification. Add the missing workflow name: fields or update this workflows: list to match existing declarations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-failure-notify.yml around lines 5 - 9, Align the
workflows list in the failure notification configuration with actual workflow
name declarations: either add matching name fields to the CI, Playwright E2E
Tests, and Lighthouse CI workflow definitions, or replace those entries with
their existing declared names. Preserve the matching TruffleHog Secret Scan
entry so notifications trigger for every intended workflow.

Comment on lines +5 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Subscribe the failure notifier to the gates workflow

The existing CI workflow is named gates in .github/workflows/gates.yml, while a repo-wide workflow search finds no workflow named CI, Playwright E2E Tests, or Lighthouse CI. Because workflow_run.workflows matches workflow names, failures of the repository's actual main-branch quality gate will not trigger this Slack notifier; only the newly added TruffleHog workflow is currently covered.

Useful? React with 👍 / 👎.

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
Comment on lines +76 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Duplicated Slack POST error-handling gap: swallowed error bodies and no request timeout.

Both workflows use the identical curl -sf ... || true pattern followed by an empty-response check. curl -f discards the response body whenever the HTTP status itself signals failure, so on non-2xx responses (rate limiting, 5xx) the real Slack error is replaced by a generic "network error or empty response" message. Neither call sets --max-time, so a hung connection can block the job instead of failing fast.

  • .github/workflows/ci-failure-notify.yml#L76-L90: drop -f from the curl call at Line 76 and add --max-time 15 so the actual response body and Slack error are always captured and the request cannot hang indefinitely.
  • .github/workflows/merge-quality-gate.yml#L128-L142: apply the same fix to the curl call at Line 128.
📍 Affects 2 files
  • .github/workflows/ci-failure-notify.yml#L76-L90 (this comment)
  • .github/workflows/merge-quality-gate.yml#L128-L142
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-failure-notify.yml around lines 76 - 90, Update the
Slack POST curl invocations in .github/workflows/ci-failure-notify.yml lines
76-90 and .github/workflows/merge-quality-gate.yml lines 128-142 by removing -f
and adding --max-time 15, while preserving the existing response validation and
error handling so non-2xx response bodies reach the Slack API error extraction.

50 changes: 50 additions & 0 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
@@ -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]'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files mentioning dependabot =="
git ls-files .github/workflows | xargs -r rg -n "dependabot|automerge|github\.actor|pull_request\.user\.login|pull_request\.user" || true

echo
echo "== target workflow =="
if [ -f .github/workflows/dependabot-automerge.yml ]; then
  nl -ba .github/workflows/dependabot-automerge.yml | sed -n '1,120p'
else
  echo ".github/workflows/dependabot-automerge.yml not found"
fi

echo
echo "== relevant event context definitions in repository if any =="
rg -n "pull_request|pull_request_target|Dependabot|dependabot|automerge" .github README.md || true

Repository: kouiso/sns-app

Length of output: 976


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target workflow =="
awk '{printf "%6d\t%s\n", NR, $0}' .github/workflows/dependabot-automerge.yml | sed -n '1,140p'

echo
echo "== parsed workflow triggers and if conditions =="
python3 - <<'PY'
from pathlib import Path
text = Path('.github/workflows/dependabot-automerge.yml').read_text()
for i, line in enumerate(text.splitlines(), 1):
    for key in ('triggers:', 'github.actor', 'github.event.pull_request.user.login'):
        if key in line:
            print(f'{i}: {line}')
PY

echo
echo "== static check: pull_request.author_login used?"
python3 - <<'PY'
import re
text = open('.github/workflows/dependabot-automerge.yml', encoding='utf-8').read()
for expr in ['github.actor ==', 'pull_request.user.login ==', 'pull_request.author_login ==', 'github.event.pull_request.user.login ==']:
    print(expr, expr in text)
PY

Repository: kouiso/sns-app

Length of output: 2366


🌐 Web query:

GitHub Actions github.actor is the person or app event type pull_request user login Dependabot automerge example

💡 Result:

Using github.actor to verify if a pull request was opened by Dependabot is insecure and can be bypassed by malicious actors [1][2][3]. The github.actor context identifies the user who triggered the specific workflow run, which can be altered by events such as a user forcing a re-run or interacting with the pull request (e.g., adding a label or triggering a synchronize event) [4][1][5]. For secure automerge workflows, you must verify the immutable author of the pull request instead [2][5]. Use the following conditional check to ensure the pull request was truly created by Dependabot [4][2]: if: github.event.pull_request.user.login == 'dependabot[bot]' For even higher assurance, you can also verify the immutable numeric ID of the Dependabot account (49699333) [6][7]: if: github.event.pull_request.user.id == 49699333 && github.event.pull_request.user.login == 'dependabot[bot]' Example Automerge Workflow Implementation: name: Dependabot auto-merge on: pull_request permissions: contents: write pull-requests: write jobs: dependabot: runs-on: ubuntu-latest # Secure check: Verify the pull request author, not the triggering actor if: github.event.pull_request.user.login == 'dependabot[bot]' steps: - name: Fetch Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v3 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge if: steps.metadata.outputs.update-type == 'version-update:semver-patch' run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} Key Security Considerations: 1. Never rely on github.actor or github.triggering_actor as a security gate [1][2]. 2. Workflows triggered by Dependabot on pull_request events operate with read-only GITHUB_TOKEN permissions by default [8][9][10]. You must explicitly define the required permissions (e.g., contents: write, pull-requests: write) in your workflow file [9][10]. 3. For workflows requiring advanced security, consider using environment protection rules with required human reviewers instead of purely automated logic [1][6].

Citations:


Check the Dependabot pull request author, not the workflow trigger actor.

github.actor reflects the user or app that triggered the workflow run and can differ from the Dependabot pull request author. Skip this job unless the pull request was authored by Dependabot.

Proposed fix
-    if: github.actor == 'dependabot[bot]'
+    if: github.event.pull_request.user.login == 'dependabot[bot]'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if: github.actor == 'dependabot[bot]'
if: github.event.pull_request.user.login == 'dependabot[bot]'
🧰 Tools
🪛 zizmor (1.28.0)

[error] 12-12: spoofable bot actor check (bot-conditions): actor context may be spoofable

(bot-conditions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/dependabot-automerge.yml at line 12, Update the job
condition in the workflow to check the pull request author associated with the
event rather than github.actor, and only allow the job when that author is
Dependabot. Preserve the existing job behavior for Dependabot-authored pull
requests.

Source: Linters/SAST tools


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 }}'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow excerpt =="
if [ -f .github/workflows/dependabot-automerge.yml ]; then
  nl -ba .github/workflows/dependabot-automerge.yml | sed -n '1,140p'
else
  echo "missing .github/workflows/dependabot-automerge.yml"
fi

echo
echo "== permissions and dependency env/action context =="
rg -n "permissions:|contents:|pull-requests:|updated-dependencies-json|DEPENDENCIES|dependabot|dependabot-automerge|metadata" .github/workflows/dependabot-automerge.yml .github/workflows || true

echo
echo "== shell quoting/expansion probe for malicious output shape =="
python3 - <<'PY'
payload = "'; :; echo INJECTED >&2"
# Simulate GitHub expression replacement before bash sees the literal line.
line = f"          DEPENDENCIES='{payload}'"
print("generated line:")
print(line)
PY
bash -c "DEPENDENCIES='';:'; echo INJECTED >&2" || true

Repository: kouiso/sns-app

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow excerpt =="
if [ -f .github/workflows/dependabot-automerge.yml ]; then
  cat -n .github/workflows/dependabot-automerge.yml | sed -n '1,140p'
else
  echo "missing .github/workflows/dependabot-automerge.yml"
fi

echo
echo "== permissions and dependency env/action context =="
grep -RInE "permissions:|contents:|pull-requests:|updated-dependencies-json|DEPENDENCIES|dependabot|metadata" .github/workflows/dependabot-automerge.yml .github/workflows 2>/dev/null || true

echo
echo "== shell quoting/expansion probe for malicious output shape =="
payload="'; :; echo INJECTED >&2"
generated="DEPENDENCIES='${payload}'"
printf 'generated output line:\n%s\n' "$generated"
bash -c "DEPENDENCIES='';:'; echo INJECTED >&2" || true

echo
echo "== safe assignment with environment variable probe =="
payload="'; :; echo INJECTED >&2"
bash -c 'export DEPENDENCIES="'"$payload"'"' 2>&1 || true
bash -c 'echo "$DEPENDENCIES"' 2>&1 || true

Repository: kouiso/sns-app

Length of output: 4832


Do not expand metadata output into shell source.

This job runs with write access for contents and pull-requests, and ${{ steps.metadata.outputs.updated-dependencies-json }} is substituted before Bash parses the line. A malicious package/version in the metadata can escape DEPENDENCIES='...' and execute commands. Pass the value through env and read $DEPENDENCIES only.

Proposed fix
       - name: 更新タイプをチェック
         id: check-update-type
+        env:
+          DEPENDENCIES: ${{ steps.metadata.outputs.updated-dependencies-json }}
         run: |
           # グループ化PRの場合、updated-dependencies-jsonから各依存関係の更新タイプを確認
-          DEPENDENCIES='${{ steps.metadata.outputs.updated-dependencies-json }}'
🧰 Tools
🪛 zizmor (1.28.0)

[info] 25-25: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/dependabot-automerge.yml at line 25, Update the workflow
step containing the DEPENDENCIES assignment so the metadata output is passed via
the step’s env configuration and read only through the quoted shell variable
$DEPENDENCIES, avoiding direct GitHub expression interpolation into Bash source.

Source: Linters/SAST tools


# 依存関係が空の場合はスキップ
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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read the camelCase Dependabot update type

dependabot/fetch-metadata v2 represents entries in updated-dependencies-json with the camelCase property updateType, not update-type. Consequently this predicate reads null for every dependency and CAN_MERGE is always false, so even patch and minor Dependabot PRs never reach the auto-merge step.

Useful? React with 👍 / 👎.

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 }}
84 changes: 84 additions & 0 deletions .github/workflows/devin-slack-trigger.yml
Original file line number Diff line number Diff line change
@@ -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('<!-- devin-slack-triggered -->')
);
Comment on lines +35 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files matching workflow:"
fd -a 'devin-slack-trigger\.yml$' . || true

file="$(fd 'devin-slack-trigger\.yml$' . | head -n1 || true)"
if [ -n "${file:-}" ]; then
  echo "=== $file ($(wc -l < "$file") lines) ==="
  cat -n "$file"
fi

echo "=== Search for marker and GitHub App token usage ==="
rg -n --hidden --glob '!**/.git/**' 'devin-slack-triggered|github-app-token|get-token|issues:labeled|comment' .github . 2>/dev/null | head -n 200

Repository: kouiso/sns-app

Length of output: 1311


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".github/workflows/devin-slack-trigger.yml"
echo "=== workflow context ==="
cat -n "$file" | sed -n '1,130p'

echo "=== read-only behavioral probe mirroring the duplicate check ==="
python3 - <<'PY'
comments = [
    {"user": {"login": "regular-commenter"}, "body": "<!-- devin-slack-triggered -->"},
]
alreadyTriggered = any(
    c.get("body") and "<!-- devin-slack-triggered -->" in c["body"]
    for c in comments
)
print("current accepts_commenter_marker =", alreadyTriggered)

comments = [
    {"user": {"login": "github-actions[bot]"}, "body": "<!-- devin-slack-triggered -->"},
    {"user": {"login": "regular-commenter"}, "body": "<!-- devin-slack-triggered -->"},
]
filtered = any(
    c.get("user", {}).get("login") == "github-actions[bot]"
    and c.get("body") and "<!-- devin-slack-triggered -->" in c["body"]
    for c in comments
)
print("current_with_commenter_and_bot = True unless comment order changes, but first caller only",
      filtered)
PY

Repository: kouiso/sns-app

Length of output: 4082


Require the duplicate marker to be posted by github-actions[bot].

Users who can comment can add <!-- devin-slack-triggered --> with their own account. That bypasses the Slack request when the label is applied later.

Proposed fix
-            const alreadyTriggered = comments.some(c =>
-              c.body?.includes('<!-- devin-slack-triggered -->')
+            const marker = '<!-- devin-slack-triggered -->';
+            const alreadyTriggered = comments.some(c =>
+              c.user?.login === 'github-actions[bot]' &&
+              c.body?.includes(marker)
             );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const alreadyTriggered = comments.some(c =>
c.body?.includes('<!-- devin-slack-triggered -->')
);
const marker = '<!-- devin-slack-triggered -->';
const alreadyTriggered = comments.some(c =>
c.user?.login === 'github-actions[bot]' &&
c.body?.includes(marker)
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/devin-slack-trigger.yml around lines 35 - 37, Update the
alreadyTriggered check in the workflow’s comment scan to require both the Devin
marker in c.body and an author login of github-actions[bot]. Ignore matching
markers posted by other users while preserving the existing duplicate-detection
behavior for workflow-generated comments.

Comment on lines +35 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Authenticate the deduplication marker

In this public repository, any issue commenter can include the hidden marker in a comment before a maintainer applies the devin label. Because this check accepts the marker from every author, that user-controlled comment makes the workflow skip Slack permanently; restrict the match to a trusted bot/app identity as well as the marker text.

Useful? React with 👍 / 👎.

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({
Comment on lines +74 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the Slack trigger idempotent across partial failures

If Slack accepts the message but the runner is interrupted or createComment fails afterward, no marker is recorded; rerunning the failed workflow therefore posts another !swe message and can start a second paid Devin session for the same issue. Record a durable reservation before the external side effect or use a deterministic idempotency mechanism that survives retries.

Useful? React with 👍 / 👎.

owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body: ':robot_face: Devin session triggered via Slack <!-- devin-slack-triggered -->',
});
142 changes: 142 additions & 0 deletions .github/workflows/merge-quality-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: Merge Quality Gate

on:
pull_request:
branches: [main]
types: [closed]
Comment on lines +3 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Slack credentials for merged fork PRs

When a PR originates from a fork, GitHub withholds repository secrets from workflows triggered by pull_request, including the closed activity used here. If such a PR is merged with a detected quality issue, SLACK_BOT_TOKEN and SLACK_CHANNEL_ID are empty and the notification fails instead of reaching Slack; use a suitably guarded privileged post-merge trigger such as pull_request_target or workflow_run.

Useful? React with 👍 / 👎.


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@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
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,
});
Comment on lines +26 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add pagination to listComments.

github.rest.issues.listComments returns only the first page of comments (default 30 per page) with no pagination applied. On a pull request with more than 30 comments, an older Lighthouse or protect-main comment can fall outside the fetched page, and comments.find(...) silently reports no quality issues even though one exists.

🛠️ Proposed fix
-            const { data: comments } = await github.rest.issues.listComments({
-              owner: context.repo.owner,
-              repo: context.repo.repo,
-              issue_number: prNumber,
-            });
+            const comments = await github.paginate(github.rest.issues.listComments, {
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              issue_number: prNumber,
+              per_page: 100,
+            });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/merge-quality-gate.yml around lines 26 - 30, Update the
listComments call in the workflow’s comment-checking logic to fetch all comment
pages, using the GitHub client’s pagination support before running
comments.find(...). Preserve the existing repository, pull-request number, and
comment filtering behavior while ensuring older comments are included.

Comment on lines +26 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Paginate PR comments before checking quality markers

When a merged PR has more than the REST endpoint's first page of comments, this request examines only that page, so a later <!-- lighthouse-ci --> or <!-- protect-main-bot --> comment is silently missed and the workflow reports has_issues=false. Use github.paginate, as the new Devin workflow already does for issue comments, before searching for either marker.

Useful? React with 👍 / 👎.


const issues = [];

// Lighthouseスコア確認
const lhComment = comments.find(c => c.body?.includes('<!-- lighthouse-ci -->'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the quality-comment producers before consuming their markers

In the reviewed commit, a repo-wide search of .github finds <!-- lighthouse-ci --> and <!-- protect-main-bot --> only in this consumer; there is no Lighthouse or protect-main workflow that posts either comment, and the commit explicitly omits those source workflows. As a result, ordinary merged PRs always reach has_issues=false, so the newly added quality notification cannot detect the conditions it claims to report.

Useful? React with 👍 / 👎.

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('<!-- protect-main-bot -->'));
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
25 changes: 25 additions & 0 deletions .github/workflows/trufflehog.yml
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +8 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow excerpt =="
sed -n '1,80p' .github/workflows/trufflehog.yml 2>/dev/null || true

echo
echo "== workflow file metadata =="
git ls-files .github/workflows/trufflehog.yml

echo
echo "== search for trufflehog workflow references elsewhere =="
rg -n "trufflehog|security-events|persist-credentials|actions/checkout" .github/workflows || true

echo
echo "== inspect target action references from workflow (not executing repo file) =="
python3 - <<'PY'
from pathlib import Path
p=Path('.github/workflows/trufflehog.yml')
if p.exists():
    text=p.read_text()
    print("contains actions/checkout:", "actions/checkout" in text)
    print("contains persist-credentials:", "persist-credentials" in text)
    print("contains security-events: write:", "security-events: write" in text)
PY

Repository: kouiso/sns-app

Length of output: 1429


🌐 Web query:

trufflesecurity trufflehog v7.0.1 GitHub action.yml scan local git persist-credentials

💡 Result:

The TruffleHog GitHub Action, which can be configured via action.yml [1][2], is designed to scan local Git repositories within a CI environment [3]. When using this action, it is standard practice to use the actions/checkout step to prepare the repository for scanning [4][3][5]. Regarding your specific configuration requirements: 1. persist-credentials: When using the actions/checkout action in a GitHub workflow, it is common to set persist-credentials: false for security purposes [6][7][8][9]. This prevents the checkout action from persisting the default GitHub token in the local git configuration, which is a security best practice for CI/CD pipelines [8][9]. 2. Local Git Scanning: The TruffleHog GitHub Action performs scanning by running the TruffleHog CLI within a Docker container [10][2]. It mounts the current directory (the local git repository) into the container [1][2]. For the action to scan correctly, especially when checking ranges (such as in pull requests), you must ensure that the repository has been checked out with sufficient history [4][3]. Setting fetch-depth: 0 in the actions/checkout step is typically required to provide full git history for the scanner to compare branches or commits [4][3][9]. 3. Configuration Example: A typical implementation in a workflow file looks like this: steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - name: TruffleHog OSS uses: trufflesecurity/trufflehog@main with: extra_args: --results=verified,unknown In this setup, the TruffleHog action automatically handles the necessary git file:/// protocol to scan the local repository contents [1][2]. The use of persist-credentials: false is independent of the TruffleHog action itself; it ensures that your workflow does not inadvertently leave credentials behind in the environment where TruffleHog is scanning [6][8][9].

Citations:


🌐 Web query:

github actions trufflehog scanner no github upload security events local git scan

💡 Result:

To perform a local git scan without triggering GitHub Security events (such as automatic code scanning alerts or pull request annotations), you should avoid using the trufflesecurity/trufflehog GitHub Action wrapper. The official TruffleHog GitHub Action is designed specifically to integrate with GitHub's CI features by automatically formatting output for GitHub Actions, which generates code annotations and interacts with GitHub security interfaces [1][2][3]. There is no native configuration parameter within the Action itself to fully disable this behavior [2][3]. For a local scan that avoids these GitHub-specific integrations, the recommended approach is to run the TruffleHog CLI directly via a standard Docker container execution in your workflow [3][4]. Recommended approach: Instead of using the 'uses: trufflesecurity/trufflehog' syntax, invoke the official Docker image directly in your workflow step [1][3]: - name: Run TruffleHog Scan run: | docker run --rm -v "${{ github.workspace }}:/workdir" trufflesecurity/trufflehog:latest \ git file:///workdir --results=verified,unknown --json > scan-results.json This approach provides several advantages for your requirements: 1. No GitHub Security Integration: By running the CLI directly, you bypass the wrapper's logic that forces GitHub-specific output formats [1][2]. 2. Custom Handling: You can pipe the output to a file (as shown above), store it as a build artifact, or process it using your own custom scripts without the Action interfering with your PR or security tab [3][4]. 3. Local Git Scanning: When scanning a local repository within a container, ensure you volume mount the directory correctly (e.g., to /workdir) and scan that target to avoid git configuration errors [5][1]. If you prefer to continue using the Action but want to minimize its impact, some users have attempted to override default behavior by passing alternative output flags (e.g., --json) in extra_args, though this may not fully suppress all side effects related to the Action's integration [2][6]. Direct CLI execution remains the most reliable method for your use case [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== fetch pinned checkout metadata =="
if command -v gh >/dev/null 2>&1; then
  gh api repos/actions/checkout/git/refs/remotes/origin/v6 \
    --jq '{ref: .ref, object: {type: .object.type, sha: .object.sha}}' || true
  gh api repos/actions/checkout/git/refs/tags/v6 \
    --jq '{ref: .ref, object: {type: .object.type, sha: .object.sha}}' || true
else
  python3 - <<'PY'
import urllib.request
urls = [
  "https://raw.githubusercontent.com/actions/checkout/v6/action.yml",
  "https://api.github.com/repos/actions/checkout/git/refs/tags/v6",
]
for u in urls:
    try:
        print("\n-- " + u)
        data = urllib.request.urlopen(u, timeout=15).read().decode()
        lines = data.splitlines()
        for line in lines[:120]:
            print(line)
    except Exception as e:
        print("ERROR", e)
PY
fi

echo
echo "== fetch pinned TruffleHog action metadata =="
if command -v gh >/dev/null 2>&1; then
  gh api repos/trufflesecurity/trufflehog/git/refs/tags/v3.92.2 \
    --jq '{ref: .ref, object: {type: .object.type, sha: .object.sha}}'
  sha="$(gh api repos/trufflesecurity/trufflehog/git/refs/tags/v3.92.2 --jq '.object.type')"
  if [ "$sha" = "tag" ]; then
    target="$(gh api repos/trufflesecurity/trufflehog/git/tags/v3.92.2 --jq '.object.sha')"
    gh api "repos/trufflesecurity/trufflehog/git/tags/$target" \
      --jq '.message, .target.sha'
  else
    sha="$(gh api repos/trufflesecurity/trufflehog/git/refs/tags/v3.92.2 --jq '.object.sha')"
    gh api "repos/trufflesecurity/trufflehog/git/commits/$sha" \
      --jq '.sha, .message'
  fi
else
  python3 - <<'PY'
import json, urllib.request
ref=json.load(urllib.request.urlopen("https://api.github.com/repos/trufflesecurity трufflehog/git/refs/tags/v3.92.2", timeout=15))
print(json.dumps(ref, indent=2)[:1200])
try:
  obj=json.load(urllib.request.urlopen("https://api.github.com/repos/trufflesecurity/trufflehog/git/commits/702bbefa350895c57f0e303eeb2855a1c637908f", timeout=15))
  print(obj["sha"], obj["message"])
except Exception as e:
  print("ERROR", e)
PY
fi

Repository: kouiso/sns-app

Length of output: 953


🌐 Web query:

actions/checkout 1203 persist-credentials default true workflow persist credentials

💡 Result:

In the actions/checkout action, the persist-credentials input controls whether the authentication token or SSH key used for the checkout is configured in the local git configuration [1][2][3]. By default, this setting is true [1][4][3]. While there have been discussions and open pull requests proposing to change the default value of persist-credentials to false to enhance security and reduce the risk of exposing credentials [5], as of August 2, 2026, it remains true by default in the official repository [1][3]. When persist-credentials is set to true (the default), the credentials are made available to your workflow, enabling subsequent git commands to run with authentication [1][2]. The action removes these credentials during the post-job cleanup [1][2]. If you wish to disable this behavior, you must explicitly set persist-credentials: false in your workflow file [1][2].

Citations:


🌐 Web query:

github actions permissions security-events write actions uploading analysis results required

💡 Result:

To upload analysis results (such as SARIF files) to GitHub Code Scanning using GitHub Actions, your workflow job must have the following permissions configured [1][2][3]: 1. security-events: write: This permission is mandatory for all workflows to authorize the upload of security analysis results to the repository [1][2][3]. 2. actions: read: While often omitted in simple examples, this permission is explicitly required for workflows running in private repositories [1][2][4]. It allows the action to access necessary metadata about the workflow run required during the upload process [4][5]. 3. contents: read: This is generally required to allow the workflow to check out the repository code [1][2], which is a standard step in most analysis workflows. A recommended minimal permissions block for a job performing this task is [1][4]: permissions: security-events: write actions: read contents: read If you encounter "Bad credentials" or "Resource not accessible" errors, especially in private repositories, ensure that the actions: read permission is explicitly included in the job-level configuration, as it is often the missing requirement for successful API authentication during the upload [4]. Additionally, the action must use the built-in GITHUB_TOKEN to interact with the Code Scanning API; using a Personal Access Token (PAT) may result in permission errors [6][4].

Citations:


🌐 Web query:

"TruffleHog" "GitHub Code Scanning" "security-events: write" "upload" GitHub Action

💡 Result:

To integrate TruffleHog findings into GitHub Code Scanning (GHAS), you must configure your GitHub Actions workflow with the security-events: write permission and include a step to upload the results in SARIF format [1][2]. TruffleHog itself does not natively perform the SARIF upload; you typically run TruffleHog to generate a JSON report and then use the github/codeql-action/upload-sarif action (or a similar tool) to push that data to GitHub [1][3][2]. Key Implementation Steps: 1. Permissions: You must grant the workflow the security-events: write permission to allow the upload of SARIF results to the GitHub Security tab [1][4]. 2. Execute TruffleHog: Run the TruffleHog action (or CLI via container) to perform the scan [3][5][6]. Use continue-on-error: true for the TruffleHog step if you want the workflow to continue to the SARIF upload step even when secrets are detected, before failing the build with a final check [1][3][2]. 3. Generate SARIF: Configure TruffleHog to output its findings in a format that can be converted or directly uploaded as SARIF [1][3]. 4. Upload SARIF: Use the github/codeql-action/upload-sarif action with an if: always() condition to ensure the report is uploaded even if TruffleHog exits with a non-zero status code due to secret detection [1][2]. Example Configuration Pattern: permissions: contents: read security-events: write jobs: trufflehog: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run TruffleHog id: trufflehog uses: trufflesecurity/trufflehog@main continue-on-error: true - name: Upload SARIF if: always uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trufflehog-results.sarif - name: Fail on findings if: steps.trufflehog.outcome == 'failure' run: exit 1 Note: GitHub Advanced Security (GHAS) must be enabled on your repository to utilize Code Scanning alerts [1]. Private repositories require a valid GHAS license [1].

Citations:


🌐 Web query:

TruffleHog GitHub Action GitHub Code Scanning upload SARIF

💡 Result:

As of August 2026, the official TruffleHog GitHub Action does not natively support generating SARIF output or uploading results directly to GitHub Code Scanning [1][2][3]. While users have requested SARIF output support to facilitate integration with GitHub's security features [1][2], the current official action is designed primarily to run scans and block workflows (or report results) based on discovered credentials, typically outputting to logs or standard JSON via container-based workarounds [4][2][5][6]. If you wish to integrate TruffleHog results into GitHub Code Scanning, you must implement a custom workaround [2][6]: 1. Run TruffleHog: Execute the TruffleHog CLI (often via a Docker container) in your workflow, ensuring you output the results to a file (e.g., using --json) [4][6]. 2. Convert to SARIF: Use a third-party tool or a custom script to convert the TruffleHog JSON output into the SARIF 2.1.0 format required by GitHub [1][6]. 3. Upload to Code Scanning: Once you have a valid SARIF file, use the official github/codeql-action/upload-sarif action to upload the results to your repository's security tab [7]. Example workflow snippet for the upload step: - name: Upload SARIF file uses: github/codeql-action/upload-sarif@v4 with: sarif_file: path/to/your-converted-results.sarif [7] Note that users have noted difficulties in using automated upload features for secret scanning because secrets exist in git history, which can lead to issues with GitHub's automatic vulnerability resolution logic [1]. Always ensure your workflow includes the necessary permissions (security-events: write) to perform the upload [7].

Citations:


Remove security-events: write and avoid persisting checkout credentials.

This workflow only scans existing code, with no code-to-secret history scan or SARIF upload step. Drop security-events: write and set persist-credentials: false because full-history checkout does not need the token to persist across steps.

Proposed fix
 permissions:
   contents: read
-  security-events: write

 ...
         with:
           fetch-depth: 0
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
permissions:
contents: read
jobs:
trufflehog:
name: Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
persist-credentials: false
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 17-20: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/trufflehog.yml around lines 8 - 20, Update the trufflehog
workflow permissions to remove security-events: write, retaining only the read
permission required for contents. In the Checkout step using actions/checkout,
set persist-credentials to false while preserving the existing full-history
fetch-depth configuration.

Source: Linters/SAST tools


- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@702bbefa350895c57f0e303eeb2855a1c637908f # v3.92.2
with:
extra_args: --only-verified
Loading