Set up a nightly scan with a workflow that has a schedule trigger, a manual trigger, a concurrency group and one scan step whose exit code decides the result. Use an off-peak minute, remember the job runs on the default branch in UTC, and send failures somewhere a team will see them.
The workflow
name: nightly-scan
on:
schedule:
- cron: "23 3 * * *"
workflow_dispatch:
concurrency:
group: nightly-scan-${{ github.workflow }}
cancel-in-progress: false
jobs:
scan:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Run dependency scan
run: ./scripts/scan.sh
The scan script is yours: it calls whichever scanner you use and exits non zero when findings cross your threshold.
Why each part is there
| Line | Reason |
|---|---|
cron: "23 3 * * *" | Runs at 03:23 UTC. GitHub says the start of each hour is high load and scheduled runs can be delayed, so avoid minute zero. |
workflow_dispatch | Lets you test the job without waiting for night. |
concurrency | A group allows one running job at a time, so a slow run cannot overlap the next. |
cancel-in-progress: false | Lets the running scan finish. A newer waiting run replaces any older pending one. |
timeout-minutes | Stops a hung scan from burning hours. |
These behaviours are described in GitHub's documentation on schedule events and concurrency.
Steps
- Put the file in the default branch. Scheduled workflows only run there, using the latest commit.
- Pick a minute that is not 0, 15, 30 or 45.
- Decide the time zone. UTC is the default, and GitHub allows an optional IANA time zone setting.
- Run it once by hand with the manual trigger and read the output.
- Decide what fails the job. Start with critical findings that have a fix available, then tighten later.
- Route failures to a channel, and add a check that the job actually ran each night. See how to detect a silently failing scheduled scan.
What nightly does not cover
A nightly job on the default branch checks one branch at one time of day. It does not check release branches, forks or code that merged after the run. Add a pull request scan for the first, and extra branch checkouts inside the job for the second. If you maintain old versions, see scanning release branches on a schedule.
Quick check
Why is my workflow not running at local midnight?
The default time zone is UTC. Convert your target time, or set the time zone option.
Can I use daily as a cron shortcut?
No. GitHub documents that the at-sign shortcuts are not supported. Write five field cron syntax.
Sources
Last reviewed: 19 October 2026.