Dependency Upgrade Skill
Detect outdated dependencies, verify security and maturity, remove unnecessary packages, upgrade safely, and create a PR.
Overview
The malgo project has four categories of dependencies:
| Category | Files | How to check | How to update |
|---|---|---|---|
| Haskell packages | package.yaml, cabal.project.freeze |
cabal outdated --freeze-file |
Edit freeze / regenerate |
| GitHub Actions | .github/workflows/*.yml |
gh api to check latest releases |
Update SHA + version comment |
| mise toolchain | mise.toml |
mise outdated |
mise use <tool>@<version> |
| Nix flake inputs | flake.lock (if present at root) |
nix flake update --dry-run |
nix flake update |
Prerequisites
Before starting any dependency work, run these setup commands:
mise trust # Approve mise permissions (required in fresh environments / subagents)
cabal update # Refresh the Hackage index — this updates the local index-state
cabal update is essential because cabal outdated compares against the local index.
Without it, you're comparing against a stale snapshot and will miss available updates.
Step-by-step workflow
1. Audit unnecessary dependencies
Before upgrading anything, check whether each dependency in package.yaml is actually
used. Removing unneeded dependencies shrinks the attack surface and reduces build time.
How to detect unused Haskell dependencies
Check all dependency sections in package.yaml:
- Top-level
dependencies:(library deps) tests:sectiondependencies:(test-only deps)build-tools:(build tools likehappy,ormolu)
For each package, search the source code for imports from that package. Use these heuristics:
| Package | Expected import pattern |
|---|---|
aeson |
import Data.Aeson |
lens |
import Control.Lens |
megaparsec |
import Text.Megaparsec |
effectful |
import Effectful |
| (etc.) | Check Hackage for the package's module names |
grep -r "import.*ModuleName" src/ app/ test/
If zero hits are found for a dependency, it is a removal candidate. Some packages are
trickier — they may be used only as build-tool plugins (like effectful-plugin) or
provide orphan instances loaded implicitly. Flag these for the user to confirm.
For build-tools, check if the tool is actually invoked. For example, happy processes
.y files — if no .y files exist in the repo, it's likely unused. Similarly, check
test-only dependencies by searching only in test/.
Removal process
- Remove the package from
package.yamldependencies:(or move to test-only if only used in tests) - Run
hpack && cabal buildto verify compilation - If build fails, check which modules need the package and decide:
- If few usages exist, consider replacing with standard library equivalents
- If heavily used, keep the dependency
- Run
mise run testto verify no runtime breakage
Present removal candidates to the user with a brief rationale before removing.
2. Detect outdated dependencies
Run detection for each category. Present results as a consolidated table.
Haskell
cabal outdated --freeze-file
This compares cabal.project.freeze against the Hackage index. Also check if the
index-state in cabal.project.freeze is stale (more than ~2 weeks old).
If cabal outdated is not available or errors, fall back to manually comparing
key packages in package.yaml against Hackage.
GitHub Actions
For each uses: line in .github/workflows/*.yml that pins to a SHA:
- Extract the owner/repo and current SHA/tag
- Use
gh api repos/{owner}/{repo}/releases/latestto find the latest release - Use
gh api repos/{owner}/{repo}/git/ref/tags/{tag}to get the SHA for the new tag - Flag any actions where the pinned version is behind latest
mise
mise outdated
Pay attention to GHC version changes — major GHC upgrades (e.g., 9.12 → 9.14) are breaking and should be flagged separately from patch updates.
Nix flake
Only if a flake.nix exists at the project root:
nix flake update --dry-run
3. Security and maturity verification
This is the most critical step. Do not skip it. Every upgrade candidate must pass these checks before being applied.
3a. Security advisory check
For Haskell packages, check:
- Haskell Security Advisory Database: search https://github.com/haskell/security-advisories
for the package name using
gh apiorgh search issues - Hackage package page changelog and metadata for security-related notes
gh search issues --repo haskell/security-advisories "<package-name>" --json title,url,state
For GitHub Actions, check:
- GitHub Security Advisories for the action's repository
- Whether the repository has had any recent suspicious commits or ownership changes
gh api repos/{owner}/{repo}/security-advisories --jq '.[].summary'
For Nix flake inputs, check the upstream project's security advisories.
3b. Supply chain attack indicators
Before adopting any new version, look for these red flags:
For Haskell packages:
- Maintainer change: Compare the
maintainerfield between the current and new version on Hackage. A sudden change in maintainer is a yellow flag — investigate.curl -s https://hackage.haskell.org/package/<pkg>-<old-ver>/<pkg>.cabal | grep -i maintainer curl -s https://hackage.haskell.org/package/<pkg>-<new-ver>/<pkg>.cabal | grep -i maintainer - Unusual dependency additions: If the new version adds dependencies that seem
unrelated to the package's purpose (e.g., a parser library suddenly depending on
networkorprocess), flag it. - Upload timing: A release published in the last 48 hours with no prior beta/RC and no changelog entry is suspicious. Prefer versions that have been on Hackage for at least 1-2 weeks.
For GitHub Actions:
- Repository transfer: Check if the action's repository was recently transferred
to a different owner.
gh api repos/{owner}/{repo}and look atcreated_atvs the account's age. - Force-pushed tags: If a tag's SHA differs from what release notes reference,
the tag may have been moved. Always resolve the SHA yourself:
gh api repos/{owner}/{repo}/git/ref/tags/{tag} --jq '.object.sha' - Commit verification: Prefer actions whose release commits are signed.
gh api repos/{owner}/{repo}/git/commits/{sha} --jq '.verification.verified'
For all categories:
- If anything looks off, stop and report to the user rather than proceeding.
3c. Release maturity check
Avoid bleeding-edge releases. Apply these minimum age thresholds:
| Risk level | Minimum age since release |
|---|---|
| Patch (x.y.Z) | 3 days |
| Minor (x.Y.0) | 1 week |
| Major (X.0.0) | 2 weeks |
To check release age:
Hackage:
curl -s "https://hackage.haskell.org/package/<pkg>/preferred.json"
Or check the upload timestamp on the Hackage package page.
GitHub Actions:
gh api repos/{owner}/{repo}/releases/latest --jq '.published_at'
If a release is younger than the threshold, flag it and suggest waiting or pinning to the previous stable version.
4. Present findings and get approval
Show the user a comprehensive summary table:
Category | Package/Action | Current | Latest | Risk | Security | Age | Action
----------------|-------------------------|-----------|-----------|--------|----------|---------|--------
Haskell | lens | 5.3.4 | 5.3.5 | low | clean | 3 weeks | upgrade
Haskell | effectful | 2.6.1.0 | 2.7.0.0 | medium | clean | 2 weeks | upgrade
Haskell | some-pkg | 1.0.0 | 1.0.1 | low | ⚠ new maintainer | 2 days | HOLD
GitHub Actions | actions/checkout | v6.0.1 | v6.0.2 | low | clean | 1 month | upgrade
Haskell | unused-pkg | 0.5.0 | - | - | - | - | REMOVE
Risk levels:
- low: patch version bump
- medium: minor version bump
- high: major version bump
Suggest a default scope (typically all low+medium with clean security and sufficient age, holding anything flagged), but do not apply anything yet — proceed to Gate A.
4.5. Gate A — Confirmation before applying changes (HARD STOP)
This is a mandatory stop. Do not proceed past this gate without explicit user approval, even when running under Auto Mode. Auto Mode's "execute immediately" default does not override this gate — dependency upgrades are not "low-risk routine work" and require human review.
What to do at this gate:
- After presenting the table from §4, stop and wait for the user to say which candidates to apply. Acceptable approval signals are explicit phrases like "apply", "go ahead", "approve", "proceed", or an enumerated subset of candidates. Silence or ambiguity is not approval.
- If the user's intent is unclear (e.g., they say "looks good" without specifying
scope), use
AskUserQuestionto confirm the exact set to apply. - Confirm the scope before any file modification: which removals, which version bumps, which Action SHA updates, and whether to also push / open a PR later.
- Do not run
cabal freeze, editpackage.yaml, edit workflow files, or create a branch until approval is given.
Once approval is received, proceed to §5.
5. Apply upgrades
Haskell freeze file update
- Run
cabal update(this refreshes the local Hackage index to the latest state) - Delete the old
index-stateline fromcabal.project.freeze - Run
cabal freezeto regenerate the freeze file (it will use the latest index-state) - If the freeze fails due to version conflicts, try relaxing constraints in
cabal.projectand report to the user
Important: after freezing, run hpack && cabal build to verify the build works.
GitHub Actions
For each action to update:
- Look up the new release tag and its commit SHA via
gh api - Verify the SHA matches what the release references
- Replace the old SHA with the new one
- Update the version comment (e.g.,
# v6.0.1→# v6.0.2)
Format: uses: owner/repo@<full-sha> # v<tag>
mise
Update mise.toml directly. For tools pinned to "latest", no change is needed —
they auto-resolve. For tools with explicit versions (like GHC), update the version string.
After updating, run mise install to verify.
Nix flake
nix flake update
6. Verify
Run the project's standard verification:
mise run build && mise run test
If tests fail:
- Check if golden test outputs need resetting (
mise run resetthen re-run tests) - Check for API changes in upgraded packages
- Report failures to the user before proceeding
6.5. Gate B — Confirmation before pushing & creating the PR (HARD STOP)
This is a second mandatory stop. Do not push the branch or create a PR without explicit user approval, even when running under Auto Mode. Pushing and opening a PR are externally visible actions and must not be taken on assumption.
What to do at this gate:
- It is fine to create local commits on a feature branch (so the user can review
git log/git diff), but do not rungit pushorgh pr createyet. - Show the user:
- The branch name you intend to push
- The list of commits (
git log --oneline master..HEAD) - The drafted PR title and body (paste it inline so they can edit it)
- Wait for an explicit go-ahead like "push", "open the PR", "create PR", "looks good — ship it". A vague "thanks" is not approval.
- If the user wants edits to the PR description, branch name, or commits, apply them and re-confirm before proceeding.
- Only after approval, run
git push -u origin <branch>andgh pr create.
7. Create PR
Create a branch and PR using gh:
- Branch name:
chore/deps-upgrade-YYYY-MM-DD - Commit message format (Conventional Commits):
- Removals:
refactor(deps): remove unused <pkg> - Upgrades:
chore(deps): upgrade dependencies - Security fixes:
fix(deps): upgrade <pkg> to fix <advisory>
- Removals:
- PR body should list:
- Dependencies removed (with rationale)
- Dependencies upgraded (with version changes)
- Security notes (any advisories addressed, any flags encountered)
- Verification results
Use separate commits for removals vs upgrades. Follow the project's Conventional Commits format.
Important notes
- Always stop for explicit user approval at the two gates (Gate A in §4.5
before applying changes, Gate B in §6.5 before pushing & creating the PR).
These gates are mandatory even under Auto Mode — the "execute immediately"
default does not extend to dependency upgrades or PR creation. Treat silence,
vague acknowledgement ("ok", "thanks"), or implicit consent as not approved
and ask again with
AskUserQuestion. - Never upgrade GHC major version without explicit user approval — this can break the entire build and requires careful migration.
- The
allow-newerfield incabal.projectmay need adjustment when upgrading. Currently it hasallow-newer: text. Check if this is still needed after upgrading. - The existing CI workflow (
haskell-deps-update.yml) handles lower bound bumps automatically. This skill is for comprehensive upgrades beyond what that workflow covers. - Always verify SHA hashes for GitHub Actions — don't just trust tag names, as tags
can be moved. Use
gh apito resolve the actual commit SHA for a tag. - When in doubt, hold — it is always safer to skip a suspicious upgrade and report findings to the user than to apply it.
Source: takoeight0821/malgo — distributed by TomeVault.