Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7bdbc08
Update Node.js version and enhance workflow configuration (#1)
blytkerchan Mar 16, 2026
c544b75
Add @vercel/ncc for build process and update package scripts
blytkerchan Mar 16, 2026
138b1c7
fix: check out code before use
blytkerchan Mar 18, 2026
43aac1e
Add Dependabot auto-merge workflow
Copilot Apr 25, 2026
4e77739
Fix npm audit workflow failures
Copilot Apr 26, 2026
b1ce912
Update Node24-compatible workflow actions
blytkerchan Apr 26, 2026
bce4ea0
Update migrated action README references
blytkerchan Apr 28, 2026
38ff9a8
Add unit tests for action logic
blytkerchan Apr 28, 2026
b42d2ea
Fix PowerShell unit test glob
blytkerchan Apr 28, 2026
3acebef
Run unit tests with bash on Windows
blytkerchan Apr 28, 2026
adcf358
List unit test files explicitly
blytkerchan Apr 28, 2026
9903b5d
docs: align README action name
blytkerchan Apr 28, 2026
5b37bbd
ci: fix windows-2025 runner jobs failing on VS 2022 lookup
Copilot Jun 18, 2026
f6566b7
chore: install release-please (#4)
blytkerchan Jul 9, 2026
e4e0d65
Bump js-yaml from 3.13.1 to 3.15.0 (#5)
dependabot[bot] Jul 9, 2026
3e7627e
Bump flatted from 3.2.2 to 3.4.2 (#7)
dependabot[bot] Jul 9, 2026
6eb3836
Use DEPENDABOT_AUTOMERGE_TOKEN in dependabot automerge workflow
blytkerchan Jul 10, 2026
e2115e4
Bump minimatch from 3.1.2 to 3.1.5 (#6)
dependabot[bot] Jul 10, 2026
cb94b1b
Fix automerge: use pull_request_target instead of pull_request
blytkerchan Jul 10, 2026
22bcab4
Migrate ESLint config to flat config and bump to v10 (#9)
blytkerchan Jul 29, 2026
da94470
fix: stop dependabot automerge from merging past failing sibling chec…
blytkerchan Aug 2, 2026
49e26c7
docs: note branch-protection/matrix coupling in main.yml (#12)
blytkerchan Aug 2, 2026
4c2ebad
fix: adopt canonical hardened automerge template from guidance (#14)
blytkerchan Aug 2, 2026
d382dc9
chore: resync automerge workflow with guidance canonical template (#15)
blytkerchan Aug 2, 2026
6adb8f7
chore: resync automerge workflow with canonical template (#16)
blytkerchan Aug 3, 2026
b40c1fb
fix: resync automerge workflow with canonical template (guidance#15) …
blytkerchan Aug 5, 2026
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
17 changes: 0 additions & 17 deletions .eslintrc.json

This file was deleted.

27 changes: 27 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
target-branch: dev
schedule:
interval: weekly
day: monday
labels:
- dependencies
- npm
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]

- package-ecosystem: github-actions
directory: /
target-branch: dev
schedule:
interval: weekly
day: monday
labels:
- dependencies
- github-actions
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
243 changes: 243 additions & 0 deletions .github/workflows/ci_dependabot_automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
---
name: ci_dependabot_automerge

# This file is maintained in vln-devsecops/guidance, which keeps two
# byte-identical copies of it - one under .github/workflows/ and one under
# scaffold/repository-compliance/.github/workflows/ - and fails CI if they
# drift. Change it there, not in place, and read
# runbooks/dependabot-automerge.md first: every piece of this workflow is
# load-bearing and most of it is not obvious.

on:
pull_request_target:
types: [opened, synchronize, reopened]

# Read-only. Every write this workflow performs - approving and merging -
# uses DEPENDABOT_AUTOMERGE_TOKEN, not GITHUB_TOKEN, so GITHUB_TOKEN needs no
# write scope. That matters more than usual here: the job runs under
# pull_request_target, so a compromised step or action would otherwise hold a
# repo-write token.
permissions:
contents: read
pull-requests: read
checks: read

jobs:
automerge:
runs-on: ubuntu-latest
# The PR's author, not github.actor. github.actor is whoever triggered
# the event, so on `reopened` - or when anyone else pushes to the branch -
# it is that person, this condition goes false, and the job silently does
# nothing on a real Dependabot PR. Nothing reports that; it just never
# merges.
#
# This deliberately does NOT decide whether a human has touched the
# branch. That is a separate question with a separate answer - see the
# commit-authorship check in the merge step - because the two were
# previously conflated here, and the accidental protection it gave was
# paid for by silent no-ops.
if: github.event.pull_request.user.login == 'dependabot[bot]'
env:
# Auto-merge is opt-in per repository, and provisioning
# DEPENDABOT_AUTOMERGE_TOKEN is what opts in. Without it every step
# below skips and the PR is left for a human.
#
# There is deliberately no fallback to GITHUB_TOKEN. GITHUB_TOKEN can
# only approve when the org/repo setting "Allow GitHub Actions to create
# and approve pull requests" is enabled, so falling back would make
# whether a repo auto-merges depend on an org-level setting nobody sets
# per repo - and would silently start auto-merging in repos where that
# was never the intent. Dormant is the safe failure here: a human
# merging is a fine outcome, an unintended auto-merge is not.
#
# secrets is not an allowed context in `if:`, so it is surfaced through
# job-level env, where it is allowed.
HAS_AUTOMERGE_PAT: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN != '' }}
steps:
- name: Report that auto-merge is not enabled
if: env.HAS_AUTOMERGE_PAT != 'true'
env:
# Folded scalar rather than shell line continuations: a backslash
# continuation inside the quoted string embeds the indentation of
# each continued line into the notice text.
MSG: >-
DEPENDABOT_AUTOMERGE_TOKEN is not set for this repository, so
Dependabot auto-merge is disabled and this PR will not be merged
automatically. See runbooks/dependabot-automerge.md in
vln-devsecops/guidance to provision it.
run: echo "::notice::$MSG"

- name: Fetch Dependabot metadata
id: meta
if: env.HAS_AUTOMERGE_PAT == 'true'
uses: dependabot/fetch-metadata@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

# `gh pr merge --auto` only actually waits for anything if the default
# branch has a required status check configured via branch protection or
# a ruleset. Most repos in this portfolio have none - private repos on
# the Free plan get a 403 configuring one at all - and with nothing
# required, `--auto` merges the moment it is invoked, regardless of
# sibling jobs still running or already failed. That is what let
# node-dashboard#37 merge past a failing `plan` job. This step gates on
# the commit's own check runs instead, so it fails closed on every plan
# tier and does not depend on branch-protection config staying correct.
- name: Wait for other checks on this commit
if: >
env.HAS_AUTOMERGE_PAT == 'true' &&
(steps.meta.outputs.update-type == 'version-update:semver-minor' ||
steps.meta.outputs.update-type == 'version-update:semver-patch')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
SHA: ${{ github.event.pull_request.head.sha }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail

# How long to keep waiting while *no* sibling check has registered
# yet. A short fixed sleep is not enough: a queued Windows or
# self-hosted runner can take minutes to post its first check run,
# and "no checks yet" is indistinguishable from "this repo has no
# other CI". This window separates the two.
settle_seconds=180
# Overall bound once checks have appeared.
max_seconds=1800
interval=15

started=$SECONDS
while :; do
elapsed=$((SECONDS - started))

# --paginate emits one JSON document per page; --slurp collects
# them into a single array, so a commit with enough check runs to
# paginate does not yield concatenated (invalid) JSON.
#
# Exclude every job of *this* workflow run rather than matching on
# a job name: this job's own check run is posted against the same
# head SHA, so without the exclusion it waits on itself until the
# timeout and blocks the merge forever. A name match only works
# while the job happens to be called "automerge"; details_url is
# .../actions/runs/<run_id>/job/<job_id>, which pins the run
# exactly and needs no extra token scope.
runs_json=$(gh api "repos/$REPO/commits/$SHA/check-runs" --paginate --slurp \
| jq --arg run "$RUN_ID" '[.[] | .check_runs[]
| select(((.details_url // "")
| contains("/actions/runs/" + $run + "/")) | not)]')

total=$(jq 'length' <<<"$runs_json")

if [ "$total" -eq 0 ]; then
if [ "$elapsed" -lt "$settle_seconds" ]; then
echo "No sibling check runs on $SHA yet (${elapsed}s elapsed), waiting..."
sleep "$interval"
continue
fi
# Fail closed. A repo with no check at all on a Dependabot PR
# has nothing for this gate to gate on, and merging anyway
# would be the original unguarded behaviour wearing a gate's
# name - the PR would look checked because this job went green.
#
# Auto-merge is only supposed to be enabled once a repo has at
# least one PR-triggered check (see
# runbooks/dependabot-automerge.md), so reaching this point
# means the repo is misconfigured. Say so, loudly, rather than
# merging and looking correct.
echo "No sibling check runs appeared on $SHA within" \
"${settle_seconds}s. Refusing to auto-merge: there is" \
"nothing gating this PR. Add a PR-triggered check, or" \
"remove this workflow."
exit 1
fi

incomplete=$(jq '[.[] | select(.status != "completed")] | length' <<<"$runs_json")
if [ "$incomplete" -gt 0 ]; then
if [ "$elapsed" -ge "$max_seconds" ]; then
echo "Timed out after ${elapsed}s with $incomplete check(s) still running:"
jq -r '.[] | select(.status != "completed") | "- \(.name): \(.status)"' <<<"$runs_json"
exit 1
fi
echo "$incomplete of $total check(s) still running (${elapsed}s elapsed), waiting..."
sleep "$interval"
continue
fi

# Anything that is not success/skipped/neutral blocks the merge,
# including cancelled, timed_out, action_required and stale.
failed=$(jq '[.[] | select(.conclusion != "success"
and .conclusion != "skipped"
and .conclusion != "neutral")]' <<<"$runs_json")
if [ "$(jq 'length' <<<"$failed")" -gt 0 ]; then
echo "Refusing to auto-merge; check(s) did not succeed:"
jq -r '.[] | "- \(.name): \(.conclusion)"' <<<"$failed"
exit 1
fi

echo "All $total other check run(s) on $SHA succeeded."
exit 0
done

# Approves with the fine-grained PAT only - see HAS_AUTOMERGE_PAT above
# for why there is no GITHUB_TOKEN fallback.
#
# The trigger is pull_request_target, not pull_request, because a
# Dependabot PR running under pull_request cannot read Actions secrets -
# the PAT would silently evaluate to empty there, and this workflow
# would then skip in every repo rather than only the unprovisioned ones.
#
# SECURITY: this job must never check out or execute PR head code.
# pull_request_target runs with repository secrets against the base
# branch; adding actions/checkout of the PR head here would expose those
# secrets to code from the PR branch.
- name: Approve and enable auto-merge for minor/patch updates
if: >
env.HAS_AUTOMERGE_PAT == 'true' &&
(steps.meta.outputs.update-type == 'version-update:semver-minor' ||
steps.meta.outputs.update-type == 'version-update:semver-patch')
run: |
set -euo pipefail

# The gate above validated $SHA, the head at the time this run
# started. If Dependabot has pushed since (a rebase, a follow-up
# bump), the PR now points at a commit this run never checked -
# and `gh pr merge --auto` applies to the PR, not to a SHA, so it
# would enable auto-merge for that unchecked head. Without a
# required status check there is nothing downstream to catch it.
#
# Bail out and let the run triggered by that newer commit do the
# gating. Exit 0, not 1: being superseded is normal, not a failure.
current=$(gh pr view "$PR_URL" --json headRefOid --jq .headRefOid)
if [ "$current" != "$SHA" ]; then
echo "PR head moved $SHA -> $current since this run started;" \
"leaving it to the run for $current."
exit 0
fi

# Auto-merge covers what Dependabot wrote, and nothing else. If
# anyone has pushed onto the branch, the PR now carries changes no
# one reviewed and that the "it's only a version bump" reasoning
# does not apply to, so hand it back to a human.
#
# Commits with no resolvable author count as foreign: an unmatched
# commit email is exactly what an unexpected push looks like, and
# guessing in favour of merging is the wrong way to be wrong.
foreign=$(gh pr view "$PR_URL" --json commits --jq '
[ .commits[]
| (.authors // [])
| if length == 0 then ["unknown"] else map(.login // "unknown") end
| .[]
] | unique | map(select(. != "dependabot[bot]")) | join(", ")')
if [ -n "$foreign" ]; then
echo "::notice::PR carries commits authored by: $foreign." \
"Auto-merge only covers Dependabot's own commits, so this" \
"PR is left for human review."
exit 0
fi

gh pr review --approve "$PR_URL"
gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
SHA: ${{ github.event.pull_request.head.sha }}
GH_TOKEN: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }}
Loading