Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/DEFAULT_GITHUB_AUTOMATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ This repository uses a default GitHub automation baseline for quality, safety, a
- Bot-authored PRs can be automatically approved when `AUTO_MERGE_BOT_APPROVE=true`
(repository variable) and `AUTO_MERGE_APPROVE_TOKEN` (PAT secret) are configured.
- Required labels (create manually or via the CLI):
```bash
gh label create auto-merge --color 0075ca --description "Squash-merge when all CI gates pass"
gh label create autofix --color e4e669 --description "Auto-merge for automated fix PRs"
```
```bash
gh label create auto-merge --color 0075ca --description "Squash-merge when all CI gates pass"
gh label create autofix --color e4e669 --description "Auto-merge for automated fix PRs"
```
- Human-authored PRs always require at least one human approval regardless of labels.

9. **Dependabot auto-merge**
Expand Down
196 changes: 98 additions & 98 deletions .github/actions/check-auto-merge-eligibility/action.yml
Original file line number Diff line number Diff line change
@@ -1,118 +1,118 @@
name: Check Auto-Merge Eligibility

Check warning on line 1 in .github/actions/check-auto-merge-eligibility/action.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

1:1 [document-start] missing document start "---"
description: >
Validates all eligibility criteria for automatic PR merging and outputs
whether the PR is eligible together with a human-readable reason.
Checks: not draft, targets main, not a fork, has auto-merge/autofix label,
not blocked/conflicted, no CHANGES_REQUESTED reviews, has an APPROVED review.
Validates all eligibility criteria for automatic PR merging and outputs
whether the PR is eligible together with a human-readable reason.
Checks: not draft, targets main, not a fork, has auto-merge/autofix label,
not blocked/conflicted, no CHANGES_REQUESTED reviews, has an APPROVED review.

inputs:
pr-number:
description: Pull request number to evaluate
required: true
github-token:
description: >
GitHub token with pull-requests:read and checks:read scope.
Defaults to the built-in GITHUB_TOKEN.
required: false
default: ${{ github.token }}
pr-number:
description: Pull request number to evaluate
required: true
github-token:
description: >
GitHub token with pull-requests:read and checks:read scope.
Defaults to the built-in GITHUB_TOKEN.
required: false
default: ${{ github.token }}

outputs:
eligible:
description: "'true' if the PR meets all auto-merge criteria, 'false' otherwise"
value: ${{ steps.check.outputs.eligible }}
reason:
description: >
Human-readable explanation of the eligibility decision.
When eligible=true this is a success message; otherwise it lists failures.
value: ${{ steps.check.outputs.reason }}
eligible:
description: "'true' if the PR meets all auto-merge criteria, 'false' otherwise"
value: ${{ steps.check.outputs.eligible }}
reason:
description: >
Human-readable explanation of the eligibility decision.
When eligible=true this is a success message; otherwise it lists failures.
value: ${{ steps.check.outputs.reason }}

runs:
using: composite
steps:
- name: Evaluate PR eligibility
id: check
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
PR_NUMBER: ${{ inputs.pr-number }}
run: |
set -euo pipefail
using: composite
steps:
- name: Evaluate PR eligibility
id: check
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
PR_NUMBER: ${{ inputs.pr-number }}
run: |
set -euo pipefail

# ---------------------------------------------------------------------------
# Fetch PR metadata via the GitHub CLI.
# ---------------------------------------------------------------------------
pr_json="$(gh pr view "$PR_NUMBER" \
--json number,isDraft,baseRefName,headRepository,labels,mergeStateStatus,reviewDecision,state \
2>&1)" || {
# Sanitize raw gh output: strip ANSI escape codes, keep first line,
# truncate using bash substring (character-aware, avoids splitting
# multi-byte UTF-8 sequences that cut -c can mishandle on some systems).
first_line="$(printf '%s' "${pr_json}" | head -n1 | sed 's/\x1b\[[0-9;]*m//g')"
sanitized="${first_line:0:120}"
echo "eligible=false" >> "$GITHUB_OUTPUT"
printf 'reason=Failed to fetch PR data: %s\n' "${sanitized}" >> "$GITHUB_OUTPUT"
exit 0
}
# ---------------------------------------------------------------------------
# Fetch PR metadata via the GitHub CLI.
# ---------------------------------------------------------------------------
pr_json="$(gh pr view "$PR_NUMBER" \
--json number,isDraft,baseRefName,headRepository,labels,mergeStateStatus,reviewDecision,state \
2>&1)" || {
# Sanitize raw gh output: strip ANSI escape codes, keep first line,
# truncate using bash substring (character-aware, avoids splitting
# multi-byte UTF-8 sequences that cut -c can mishandle on some systems).
first_line="$(printf '%s' "${pr_json}" | head -n1 | sed 's/\x1b\[[0-9;]*m//g')"
sanitized="${first_line:0:120}"
echo "eligible=false" >> "$GITHUB_OUTPUT"
printf 'reason=Failed to fetch PR data: %s\n' "${sanitized}" >> "$GITHUB_OUTPUT"
exit 0
}

# ---------------------------------------------------------------------------
# Extract fields via jq.
# ---------------------------------------------------------------------------
is_draft="$(jq -r '.isDraft' <<< "$pr_json")"
base_ref="$(jq -r '.baseRefName' <<< "$pr_json")"
head_repo="$(jq -r '.headRepository.nameWithOwner // empty' <<< "$pr_json")"
pr_state="$(jq -r '.state' <<< "$pr_json")"
merge_state="$(jq -r '.mergeStateStatus // empty' <<< "$pr_json")"
review_decision="$(jq -r '.reviewDecision // empty' <<< "$pr_json")"
has_label="$(jq -r '
[.labels[].name] | any(. == "auto-merge" or . == "autofix")
' <<< "$pr_json")"
# ---------------------------------------------------------------------------
# Extract fields via jq.
# ---------------------------------------------------------------------------
is_draft="$(jq -r '.isDraft' <<< "$pr_json")"
base_ref="$(jq -r '.baseRefName' <<< "$pr_json")"
head_repo="$(jq -r '.headRepository.nameWithOwner // empty' <<< "$pr_json")"
pr_state="$(jq -r '.state' <<< "$pr_json")"
merge_state="$(jq -r '.mergeStateStatus // empty' <<< "$pr_json")"
review_decision="$(jq -r '.reviewDecision // empty' <<< "$pr_json")"
has_label="$(jq -r '
[.labels[].name] | any(. == "auto-merge" or . == "autofix")
' <<< "$pr_json")"

current_repo="${GITHUB_REPOSITORY:-}"
current_repo="${GITHUB_REPOSITORY:-}"

# ---------------------------------------------------------------------------
# Accumulate failures.
# ---------------------------------------------------------------------------
FAILURES=()
# ---------------------------------------------------------------------------
# Accumulate failures.
# ---------------------------------------------------------------------------
FAILURES=()

[[ "$pr_state" != "OPEN" ]] && \
FAILURES+=("PR state is '${pr_state}' (requires OPEN)")
[[ "$pr_state" != "OPEN" ]] && \
FAILURES+=("PR state is '${pr_state}' (requires OPEN)")

[[ "$is_draft" == "true" ]] && \
FAILURES+=("PR is a draft")
[[ "$is_draft" == "true" ]] && \
FAILURES+=("PR is a draft")

[[ "$base_ref" != "main" ]] && \
FAILURES+=("base branch is '${base_ref}' (requires 'main')")
[[ "$base_ref" != "main" ]] && \
FAILURES+=("base branch is '${base_ref}' (requires 'main')")

if [[ -n "$current_repo" && -n "$head_repo" && "$head_repo" != "$current_repo" ]]; then
FAILURES+=("PR is from a fork (${head_repo}); fork PRs are not eligible")
fi
if [[ -n "$current_repo" && -n "$head_repo" && "$head_repo" != "$current_repo" ]]; then
FAILURES+=("PR is from a fork (${head_repo}); fork PRs are not eligible")
fi

[[ "$has_label" != "true" ]] && \
FAILURES+=("missing 'auto-merge' or 'autofix' label")
[[ "$has_label" != "true" ]] && \
FAILURES+=("missing 'auto-merge' or 'autofix' label")

case "$merge_state" in
DIRTY) FAILURES+=("merge conflicts detected (mergeStateStatus=DIRTY)") ;;
BEHIND) FAILURES+=("branch is behind base branch (mergeStateStatus=BEHIND)") ;;
BLOCKED) FAILURES+=("merge is blocked by required status checks (mergeStateStatus=BLOCKED)") ;;
esac
case "$merge_state" in
DIRTY) FAILURES+=("merge conflicts detected (mergeStateStatus=DIRTY)") ;;
BEHIND) FAILURES+=("branch is behind base branch (mergeStateStatus=BEHIND)") ;;
BLOCKED) FAILURES+=("merge is blocked by required status checks (mergeStateStatus=BLOCKED)") ;;
esac

if [[ "$review_decision" == "CHANGES_REQUESTED" ]]; then
FAILURES+=("has CHANGES_REQUESTED review")
elif [[ "$review_decision" != "APPROVED" ]]; then
FAILURES+=("awaiting required approval (reviewDecision='${review_decision}')")
fi
if [[ "$review_decision" == "CHANGES_REQUESTED" ]]; then
FAILURES+=("has CHANGES_REQUESTED review")
elif [[ "$review_decision" != "APPROVED" ]]; then
FAILURES+=("awaiting required approval (reviewDecision='${review_decision}')")
fi

# ---------------------------------------------------------------------------
# Emit outputs.
# ---------------------------------------------------------------------------
if (( ${#FAILURES[@]} == 0 )); then
echo "eligible=true" >> "$GITHUB_OUTPUT"
echo "reason=All eligibility checks passed." >> "$GITHUB_OUTPUT"
echo "✅ PR #${PR_NUMBER} is eligible for auto-merge."
else
echo "eligible=false" >> "$GITHUB_OUTPUT"
joined="$(printf '%s; ' "${FAILURES[@]}")"
joined="${joined%; }" # trim trailing '; '
printf 'reason=%s\n' "${joined}" >> "$GITHUB_OUTPUT"
echo "❌ PR #${PR_NUMBER} is NOT eligible: ${joined}"
fi
# ---------------------------------------------------------------------------
# Emit outputs.
# ---------------------------------------------------------------------------
if (( ${#FAILURES[@]} == 0 )); then
echo "eligible=true" >> "$GITHUB_OUTPUT"
echo "reason=All eligibility checks passed." >> "$GITHUB_OUTPUT"
echo "✅ PR #${PR_NUMBER} is eligible for auto-merge."
else
echo "eligible=false" >> "$GITHUB_OUTPUT"
joined="$(printf '%s; ' "${FAILURES[@]}")"
joined="${joined%; }" # trim trailing '; '
printf 'reason=%s\n' "${joined}" >> "$GITHUB_OUTPUT"
echo "❌ PR #${PR_NUMBER} is NOT eligible: ${joined}"
fi
17 changes: 13 additions & 4 deletions .github/workflows/actionlint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Workflow Lint

Check warning on line 1 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

1:1 [document-start] missing document start "---"

Check warning on line 1 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

1:1 [document-start] missing document start "---"

on:

Check warning on line 3 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

3:1 [truthy] truthy value should be one of [false, true]

Check warning on line 3 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

3:1 [truthy] truthy value should be one of [false, true]
pull_request:
paths:
- ".github/workflows/**"
Expand All @@ -12,7 +12,7 @@
- ".github/actions/**"
workflow_dispatch:
schedule:
- cron: "23 4 * * 1" # Weekly drift check (Mon 04:23 UTC)

Check warning on line 15 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

15:26 [comments] too few spaces before comment

Check warning on line 15 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

15:26 [comments] too few spaces before comment

# Cancel superseded runs on the same ref/PR, but never cancel runs on main
# (so the branch's required-status history stays intact).
Expand All @@ -30,10 +30,10 @@

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
HARDEN_RUNNER_SHA: 0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.2

Check warning on line 33 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

33:63 [comments] too few spaces before comment

Check warning on line 33 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

33:63 [comments] too few spaces before comment
CHECKOUT_SHA: 11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Check warning on line 34 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

34:58 [comments] too few spaces before comment

Check warning on line 34 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

34:58 [comments] too few spaces before comment
ACTION_ACTIONLINT_REF: 6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1.72.0

Check warning on line 35 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

35:67 [comments] too few spaces before comment

Check warning on line 35 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

35:67 [comments] too few spaces before comment
SHELLCHECK_ACTION_SHA: 00cae500b08a931fb5698e11e79bfbd38e612a38 # ludeeus/action-shellcheck@2.0.0

Check warning on line 36 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

36:67 [comments] too few spaces before comment

Check warning on line 36 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

36:67 [comments] too few spaces before comment
SHELLCHECK_VERSION: v0.10.0

jobs:
Expand All @@ -49,27 +49,36 @@

steps:
- name: Harden runner
uses: step-security/harden-runner@${{ env.HARDEN_RUNNER_SHA }}
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.2

Check warning on line 52 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

52:84 [comments] too few spaces before comment

Check warning on line 52 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

52:84 [comments] too few spaces before comment
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@${{ env.CHECKOUT_SHA }}
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Check warning on line 57 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

57:73 [comments] too few spaces before comment

Check warning on line 57 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

57:73 [comments] too few spaces before comment
with:
persist-credentials: false
fetch-depth: 1

- name: Setup ShellCheck (pinned)
uses: ludeeus/action-shellcheck@${{ env.SHELLCHECK_ACTION_SHA }}
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0

Check warning on line 63 in .github/workflows/actionlint.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

63:82 [comments] too few spaces before comment
with:
version: ${{ env.SHELLCHECK_VERSION }}
# Exclude vendored third-party scripts (dotnet-install.sh is the
# Microsoft .NET installer) and generated agent-template directories.
ignore_paths: dotnet-install.sh my-agent-0b2avt
# Only fail CI on error-severity findings; warnings in our own
# scripts are reported but do not block the build.
severity: error
Comment on lines +66 to +71

- name: Run actionlint
uses: reviewdog/action-actionlint@${{ env.ACTION_ACTIONLINT_REF }}
uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1.72.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
fail_on_error: true
fail_level: error
reporter: github-check
level: error
filter_mode: nofilter
# Suppress the "models" permission warning: it is a real GitHub Models API
# scope but is not yet in actionlint's known-permissions list.
actionlint_flags: '-ignore "unknown permission scope \"models\""'
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v3
uses: actions/setup-python@v5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by github-actions-mutable-action-tag.

You can view more details about this finding in the Semgrep AppSec Platform.

with:
python-version: "3.10"
- name: Install dependencies
Expand Down
4 changes: 1 addition & 3 deletions tests/test_auto_fix_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,7 @@ def test_auto_fix_has_cleanup_step() -> None:
"are accidentally included in the autofix branch"
)

cleanup_step = next(
step for step in steps if step["name"] == "Revert workflow file changes and remove stray files"
)
cleanup_step = next(step for step in steps if step["name"] == "Revert workflow file changes and remove stray files")
assert "detect_output.txt" in cleanup_step["run"], "Must remove stray detect_output.txt"
assert ".github/workflows" in cleanup_step["run"], "Must revert workflow file changes"
assert "git restore" in cleanup_step["run"], "Must use git restore to revert workflow changes"
Expand Down
Loading
Loading