Behind the Scenes

What We Do for 40 Minutes Every Monday at 9:20: After Renovate, composer audit and Trivy, the Question Only a Human Can Answer

2026.09.03 · 25 views
What We Do for 40 Minutes Every Monday at 9:20: After Renovate, composer audit and Trivy, the Question Only a Human Can Answer

Machines scan daily, humans triage weekly, and there are only three verdicts — fix today, fix this sprint, or write down why we won't.

Share:

Six hours to answer one question: does this actually affect us?

One Wednesday afternoon a PHP package was flagged with a high-severity vulnerability. The client contact asked on LINE, "are we affected?" Our answer was "let me check" — and the check took six hours. We knew the package was in composer.lock. We did not know which code path called it, whether we ever reached the vulnerable function, or how many of our other seven client projects had the same version installed. After that we put this on a fixed schedule: Mondays at 9:20am, 40 minutes. Not to be more secure — to make "are we affected?" a question with a 20-minute answer.

What we do

In one sentence: machines scan daily, humans triage weekly, and there are only three possible verdicts — fix today, fix this sprint, or document and don't fix.

The flow:

Renovate daily schedule → upgrade PRs → CI runs (Pest tests + PHPStan + Trivy scan) → green PRs auto-merge (patch versions only) → anything failing, plus all minor/major, joins the Monday list → 9:20am triage (15 minutes) → classified P0 / P1 / P2 → P0 handled same day, P1 enters the sprint, P2 written into SECURITY-DECISIONS.md

The value is not in the tools. It is in the last step: "not fixing" is a decision that requires a written reason. That is the largest difference between this and how most teams work.

Why we built it this way

We started with "fix every alert as it arrives" and abandoned it in two months. The reason is practical: a studio maintaining a dozen Laravel and Flutter projects that treats every dependency alert as an incident interrupts engineers a dozen times a week — and most of those interruptions end in "we only use that package in tests" or "we never call the affected function."

So we made three trade-offs:

  • Trade-off 1: fixed slot in exchange for complete context. Handling random interruptions produces worse decisions than reviewing all client projects together once a week. The cost is a worst-case six-day delay window — which is why P0 has a bypass.
  • Trade-off 2: auto-merge patch versions only. Minor and major always get human eyes. Auto-merging minors is usually fine in the Laravel ecosystem, but we run payment and scheduling logic where one unexpected behaviour change costs far more than the ten minutes saved.
  • Trade-off 3: accept "won't fix" with documentation rather than letting things pile up indefinitely. Without this rule the Monday list grows unreadable within three months.

The P0 bypass is simple: if a vulnerability has publicly available exploit code and our code path demonstrably reaches it, anyone can open a hotfix without waiting for Monday. We have used it twice in twelve months.

How it actually runs

Step 1: a renovate.json in every project root. The key setting separates security updates from routine ones:

{
  "extends": ["config:recommended"],
  "schedule": ["before 6am on monday"],
  "packageRules": [
    { "matchUpdateTypes": ["patch"], "automerge": true },
    { "matchUpdateTypes": ["minor", "major"], "automerge": false }
  ],
  "vulnerabilityAlerts": { "labels": ["security"], "schedule": ["at any time"] }
}

Step 2: three CI gates on every PR, ordered cheapest first:

- run: composer audit --locked --format=plain
- run: vendor/bin/phpstan analyse --memory-limit=1G
- run: vendor/bin/pest --parallel
- uses: aquasecurity/trivy-action@master
  with: { scan-type: 'fs', severity: 'HIGH,CRITICAL', exit-code: '1' }

composer audit reads the Packagist security advisories database. On the Flutter side we run dart pub outdated and cross-check the GitHub Advisory Database by hand — Dart's tooling is not mature enough yet, and we would rather say so plainly than pretend it is automated.

Step 3: Monday 9:20, one shared list. Four fixed columns: package / version gap / do we actually call it / classification. That third column is where all the value sits, and it is the only part requiring a human — usually rg against the function name answers it in two minutes.

Step 4: P2 items go into SECURITY-DECISIONS.md. Minimal format: date, package and version, decision, reasoning, review date. The review date is mandatory and usually set 90 days out.

What this costs us

  • Cost 1: 40 minutes of every Monday morning. Two people, roughly 70 person-hours a year. That is a real cost, not something done "while we're at it."
  • Cost 2: a worst-case six-day delay. A medium-severity issue disclosed on Tuesday waits until the following Monday. The P0 bypass covers the top of the range; for medium severity we are genuinely accepting risk.
  • Cost 3: Renovate is noisy. A dozen projects generate 20–40 PRs a week. Even with patch auto-merge, the remainder needs eyes. We retuned the schedule three times before it became tolerable.
  • Cost 4: SECURITY-DECISIONS.md needs maintenance. An unreviewed review date is the same as no review date. We put it into the quarterly tech-debt review or it rots.

Where this is the wrong approach

  • Single-product, single-team companies. With one codebase, handling alerts as they arrive is not expensive, and a weekly meeting becomes ceremony.
  • Heavily regulated industries (finance, medical devices). These usually carry mandated remediation windows — high severity within 72 hours, for example — which a weekly cadence violates outright. Go event-driven instead.
  • Projects with no automated tests. The whole design assumes a green CI can be trusted. Auto-merging patches without tests is a bet on the package author's semver discipline.

Why clients should care

  • Response time. When a framework vulnerability hits the news and you ask whether you are affected, our target is an answer in 20 minutes, because the inventory, versions and call relationships already exist. This does not make the system safer by itself. It means you do not spend a day in uncertainty.
  • Maintenance costs don't spike. Dependencies left untouched for two years turn the eventual forced upgrade into a 40–80 hour project. The marginal cost of weekly handling is far below paying it all at once.
  • You get a record of what we decided not to fix. That is useful in security audits, tenders and vendor transitions. Most teams record what they patched and never record why they didn't — and the second is what auditors ask about.

If you want to run this yourself

  • Suggestion 1: start with call-path judgment, not tool purchases. Practise on an existing project: pick any dependency alert, rg for the affected function name, and time how long it takes to answer "do we reach this?" That skill matters more than any scanner.
  • Suggestion 2: use three severity levels, not five. We tried P0 through P4; P3 and P4 never differed in practice and only added discussion time. Define the boundaries by behaviour — fix today / fix this sprint / document and don't fix — rather than by severity scores.
  • Suggestion 3: institutionalise "won't fix." Without a legitimate exit, engineers substitute "leave it for now", and "leave it for now" leaves no record.
  • Suggestion 4: start auto-merge at patch and watch for three months before widening. We still have not added minor, and that is not conservatism — it is because we priced the debugging cost of one unexpected behaviour change.
  • Suggestion 5: order CI gates cheapest first. composer audit takes 3 seconds; the test suite takes 4 minutes. Get the order wrong and you wait extra minutes every day.

Where we have used it

An e-commerce client (anonymised) had gone 19 months without a comprehensive dependency upgrade. The first inventory listed 63 pending items; we cleared it to 11 over two sprints, then held it between 5 and 15 with 40 minutes a week. The real payoff came in year two: on the day a widely-used PHP package disclosed a high-severity issue, we told them within 18 minutes that they had it installed but never called the affected function, so it could ship with the regular Monday batch — rather than asking them to take the site down.

A scheduling-system client demonstrated the limits. Their Flutter app depended on an abandoned third-party package, and four consecutive Monday meetings could only mark it P2. We eventually replaced it over two sprints. The process keeps a problem continuously visible; it does not make the decision for you.

FAQ

Why not just use Dependabot?

We use both. The GitHub Advisory Database produces high-quality alerts, so we keep Dependabot's security alerts. But Renovate offers far more control over scheduling, grouped upgrades and auto-merge rules, so upgrade PRs go through Renovate.

How is 40 minutes enough for a dozen projects?

Because 80% of items are already handled by CI before the meeting. The meeting genuinely discusses 5 to 12 items, and most are settled in 30 seconds with "we don't call it, P2." Time goes to the one or two that require reading code.

Do clients pay for those 40 minutes?

It is included in our monthly maintenance retainers at no separate charge. One-off projects that close on delivery do not include it — which is one of the reasons we recommend a retainer for clients running long-lived systems.

We have no automated tests. Which part can we adopt first?

Start with the weekly list and the classification; skip auto-merge. The list alone has value: it turns "are we affected?" into a question you can look up. Enable auto-merge once test coverage is in place.

Next step

If your project has gone more than a year without a comprehensive dependency review, we can run a dependency health check and deliver an inventory with call-path assessment and severity recommendations. And if you are an engineer who likes this way of working — write the process down, then actually follow it — we are always open to a conversation.

Share:
Behind the Scenes Back to Blog