# CC Soccer D11 — Update Workflow

How we update Drupal core, contrib modules, and PHP dependencies as a team without stepping on each other or breaking prod.

**Last updated:** April 25, 2026

---

## TL;DR

| Situation | Command |
|---|---|
| You pulled changes from `main` | `ddev composer install && ddev drush updb -y && ddev drush cim -y && ddev drush cr` |
| You want to bump Drupal core | `ddev composer update "drupal/core-*" --with-all-dependencies` (then updb / cex / test / commit) |
| You want to bump one contrib module | `ddev composer update drupal/MODULENAME --with-dependencies` (then updb / cex / test / commit) |
| Production deploy | `ccsDeploy && ccsUpdb && ccsCim && ccsCr` |

**Rule of thumb:** `composer install` is everyday. `composer update` is a coordinated event.

---

## Mental Model

There are two halves to a Drupal update:

1. **Code** — Drupal core + contrib modules + PHP libraries. Composer-managed. Propagates via `composer.lock` in git. Anyone who runs `composer install` after pulling gets identical code.
2. **Database schema and config** — `drush updb` and `drush cim`. Run *per environment*, against each environment's own DB. Doesn't propagate via git directly. The *code* that knows how to perform the update propagates; each environment then runs the update locally against its own DB.

Every developer's local DDEV DB, the TEST server DB, and the production server DB each need their own `updb` + `cim` after pulling new code.

---

## composer.json vs composer.lock

- `composer.json` declares **acceptable version ranges** (e.g., `^11.2` = 11.2.x through <12.0.0).
- `composer.lock` records the **exact resolved versions** for everyone.
- Both files are committed to git.

`composer install` reads the lockfile and installs exactly those versions — deterministic, no surprises.

`composer update` ignores the lockfile, picks the newest acceptable versions per `composer.json`, and rewrites the lockfile.

**This is why `composer install` is the consumer command and `composer update` is the producer command.**

---

## The Standard Update Flow

One person — the "updater" — runs the full cycle on a feature branch.

### 1. Coordinate first

Before you run `composer update`, ping the other developer: "Taking the core update today" or "Bumping commerce module." This alone prevents 95% of merge conflicts.

### 2. Branch and update

```bash
git checkout main
git pull
git checkout -b chore/drupal-core-update-N.N.N

# For Drupal core security updates:
ddev composer update "drupal/core-*" --with-all-dependencies

# For a single contrib module:
ddev composer update drupal/MODULENAME --with-dependencies

# For everything that has updates available (riskier — review the diff):
ddev composer update --with-all-dependencies
```

### 3. Apply the updates locally

```bash
ddev drush updb -y     # run database update hooks
ddev drush cex -y      # export any config the update introduced
ddev drush cr          # clear cache
```

### 4. Test the site

Click through the parts of the site touched by the update. For core security releases, at minimum test:

- Login — passkey path AND password fallback
- Registration flow
- Schedule view (logged in and logged out)
- Admin pages — Content, Commerce, People

### 5. Commit and push

```bash
git add composer.json composer.lock config/sync/
git commit -m "Update Drupal core to X.Y.Z (security release)"
git push -u origin chore/drupal-core-update-N.N.N
```

Open a PR. Merge to `main`.

---

## The Consumer Flow (everyone else, every time you pull)

```bash
git pull
ddev composer install        # syncs vendor/ to the new lockfile
ddev drush updb -y           # run any new update hooks against your local DB
ddev drush cim -y            # import any new config from config/sync/
ddev drush cr                # clear cache
```

This is the same chain prod runs (via the `ccsDeploy && ccsUpdb && ccsCim && ccsCr` alias).

If nothing changed, all four commands are no-ops — safe to run after every pull.

---

## Conflict Scenarios

### Both developers ran `composer update` independently

**Symptom:** merge conflict in `composer.lock` when the second person tries to push.

`composer.lock` is thousands of lines of nested hashes — never hand-merge it. Pick one side as the winner and re-run install:

```bash
git fetch origin
git rebase origin/main
# conflict in composer.lock

git checkout --theirs composer.lock      # accept main's lockfile
ddev composer install                    # sync vendor/ to it

# if YOUR update added a package that main's update didn't, re-add it now:
ddev composer require drupal/something

git add composer.json composer.lock
git rebase --continue
```

### Conflict in `config/sync/*.yml` from updb

**Symptom:** same field/view/config exported with different shape on both branches.

These are human-readable YAML — usually mergeable by hand. If unsure, accept whichever branch's version came from running `drush updb && drush cex` against the most recent code, then re-run on your side and re-export to verify.

### "After I pulled, the site is broken"

99% of the time this is one of these, in order:

1. Forgot `ddev composer install` — `vendor/` is stale relative to the lockfile.
2. Forgot `ddev drush updb -y` — DB schema is behind the code.
3. Forgot `ddev drush cim -y` — config in DB is behind `config/sync/`.
4. Forgot `ddev drush cr` — caches are pointing at old class definitions.

Run all four in order. Almost always fixes it.

---

## What NOT to Do

- **Don't apply updates through Drupal's in-browser Update Manager UI** (`/admin/reports/updates` "update" button). Those changes get blown away on the next `composer install`. The page is fine for *seeing* what's available; never use it for *applying*.
- **Don't run `composer update` on a routine pull.** That's how you accidentally bump versions you didn't intend to. Use `composer install`.
- **Don't commit just `composer.json` without `composer.lock`** (or vice versa). They always go together.
- **Don't run `composer update` on prod.** Prod only runs `git pull && composer install --no-dev && drush updb -y && drush cim -y && drush cr`. The lockfile carries the new versions there.
- **Don't update both core and a major contrib module in the same commit** if you can avoid it. If something breaks, you want to know which bump caused it.

---

## DDEV Tool Upgrades (separate from Drupal updates)

DDEV occasionally prompts for its own upgrade ("Upgraded DDEV vX.Y.Z is available"). That's the tool, not Drupal — unrelated to composer or any of the above.

```bash
brew upgrade ddev          # if installed via Homebrew
ddev poweroff              # stops everything cleanly
ddev start                 # comes back up on the new version
```

Patch-level DDEV upgrades almost never affect the project. Major-version bumps occasionally need `ddev debug rebuild`.

---

## Production Deploy Reference

Prod is a server (InMotion), not DDEV. The deploy alias chain is:

```bash
ccsDeploy   # cd to site dir + git pull
ccsUpdb     # drush updb -y
ccsCim      # drush cim -y
ccsCr       # drush cr
```

Full deploy:

```bash
ccsDeploy && ccsUpdb && ccsCim && ccsCr
```

If `composer.lock` changed in the pull, also run composer install on the server:

```bash
cd ~/sitedir
composer install --no-dev --optimize-autoloader
```

(See `DEPLOYMENT_GUIDE.md` for the full server-side procedure including backups.)

---

## Quick Cheatsheet

| Day in the life | Command |
|---|---|
| Started my day, want latest | `git pull && ddev composer install && ddev drush updb -y && ddev drush cim -y && ddev drush cr` |
| About to update core (after coordinating) | `git checkout -b chore/...` then `ddev composer update "drupal/core-*" --with-all-dependencies` |
| About to update one contrib module | `git checkout -b chore/...` then `ddev composer update drupal/X --with-dependencies` |
| Finished update, ready to commit | `ddev drush updb -y && ddev drush cex -y && ddev drush cr`, then commit `composer.json` + `composer.lock` + `config/sync/` |
| Site looks broken after a pull | The four-command consumer flow above — always |

---

## Optional Safety Net: Snapshot Before Updating

DDEV can take a DB snapshot you can roll back to if `drush updb` chokes on something:

```bash
ddev snapshot --name=pre-core-update
# ...do the update, test...

# if something goes wrong:
ddev snapshot restore pre-core-update
```

Cheap insurance — under a second to take, lets you experiment freely.

---

## See Also

- `DEPENDENCIES.md` — what PHP libraries are installed and why
- `DEPLOYMENT_GUIDE.md` — full prod deploy procedure
- `INSTALLATION_GUIDE.md` — fresh local setup
- `SESSION_HANDOFF.md` — current state of the project
