<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Laravel Git Blog - Workflows</title><description>Blog posts tagged Workflows: notes on Git, code review and shipping Laravel projects.</description><link>https://laravelgit.com/</link><language>en-gb</language><atom:link href="https://laravelgit.com/blog/tag/workflows.xml" rel="self" type="application/rss+xml"/><item><title>A First CI Pipeline for a Laravel Project</title><link>https://laravelgit.com/blog/a-first-ci-pipeline-for-laravel/</link><guid isPermaLink="true">https://laravelgit.com/blog/a-first-ci-pipeline-for-laravel/</guid><description>Install, lint, test, build - four jobs that run on every pull request and turn &apos;works on my machine&apos; into a check everyone can see.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;CI does not need to be elaborate to be valuable. For a small Laravel team, a first pipeline is four jobs that run on every pull request, and its real product is a green checkmark everyone trusts.&lt;/p&gt;
&lt;h2&gt;The four jobs&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Install&lt;/strong&gt;: &lt;code&gt;composer install&lt;/code&gt; and &lt;code&gt;npm ci&lt;/code&gt;. If dependencies cannot install cleanly from the lockfiles, nothing else matters - this is why the lockfiles are committed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lint&lt;/strong&gt;: &lt;code&gt;./vendor/bin/pint --test&lt;/code&gt; fails the build on style drift, so style stops being a code review topic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test&lt;/strong&gt;: &lt;code&gt;php artisan test&lt;/code&gt; against a throwaway database (SQLite in memory is enough for a first pipeline).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build&lt;/strong&gt;: &lt;code&gt;npm run build&lt;/code&gt; proves the frontend compiles - the failure you do not want to meet at deploy time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Any pipeline runner can express this: GitHub Actions, GitLab CI, or the CI your self-hosted setup provides. The shape is the same everywhere: checkout, cache the composer/npm directories, run the four jobs.&lt;/p&gt;
&lt;h2&gt;Make the check required&lt;/h2&gt;
&lt;p&gt;A pipeline nobody is forced to wait for becomes decoration within a month. Wire it into &lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;branch protection&lt;/a&gt;: pull requests cannot merge until CI is green. That single setting is the difference between &amp;quot;we have CI&amp;quot; and &amp;quot;CI has us&amp;quot; - and it is what lets &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small PRs&lt;/a&gt; move quickly, because reviewers see the robot&amp;#39;s verdict before they spend their own attention.&lt;/p&gt;
&lt;h2&gt;Keep it boring and fast&lt;/h2&gt;
&lt;p&gt;Two traps make teams abandon CI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Slow runs.&lt;/strong&gt; Cache dependencies, and resist running the full browser-test suite on every push - that belongs in a nightly job. A PR pipeline that answers in under five minutes gets respected; one that takes forty gets bypassed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flaky tests.&lt;/strong&gt; A test that fails randomly teaches everyone to click &amp;quot;re-run&amp;quot; without looking, which is worse than no test. Quarantine flakes the week they appear.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What not to automate yet&lt;/h2&gt;
&lt;p&gt;Deploys. The first pipeline stops at the green checkmark on the PR; deployment stays a deliberate human act until the test suite earns more trust. When that day comes, the pipeline&amp;#39;s final job is a tag, and &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;Deploy With Tags, Not Branches&lt;/a&gt; takes it from there.&lt;/p&gt;
&lt;p&gt;Four jobs, one required check, five minutes. That is the whole first pipeline - everything after is refinement.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>A Staging Environment Worth Having</title><link>https://laravelgit.com/blog/a-staging-environment-worth-having/</link><guid isPermaLink="true">https://laravelgit.com/blog/a-staging-environment-worth-having/</guid><description>Staging only catches what it shares with production. Same deploy path, same services, sanitized data - and a clear rule for what it is for.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most staging environments fail quietly: they run different code, on different data, deployed by a different process, so a green staging test proves almost nothing and everyone learns to ignore it. A staging environment earns its cost by being production&amp;#39;s rehearsal room, and that property comes from three decisions.&lt;/p&gt;
&lt;h2&gt;Same deploy path&lt;/h2&gt;
&lt;p&gt;Deploy to staging exactly the way you deploy to production - same script, same &lt;a href=&quot;/blog/zero-downtime-deploys-for-laravel/&quot;&gt;releases and symlink structure&lt;/a&gt;, same &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;tag-based flow&lt;/a&gt;, differing only in target host. This is the highest-value property: most deploy failures are process failures, and you want to meet them on staging. If production deploys a tag and staging deploys &amp;quot;whatever branch someone pushed,&amp;quot; staging is testing a process you do not use.&lt;/p&gt;
&lt;h2&gt;Same shape of services&lt;/h2&gt;
&lt;p&gt;Match the services that change behavior: same PHP version, same database engine and version, a real queue worker, a real cache. SQLite on staging against MySQL in production hides the query that only MySQL rejects. The hardware can be smaller - the interfaces cannot.&lt;/p&gt;
&lt;h2&gt;Sanitized, realistic data&lt;/h2&gt;
&lt;p&gt;Copy production data down and scrub it: anonymize emails, strip tokens and personal fields, keep the volumes and shapes. &lt;code&gt;User::factory()&lt;/code&gt; data has perfect distributions and no history; real data has a user from 2019 with a null middle name that breaks your new export. Laravel&amp;#39;s model factories can help fill gaps, but the scrubbed copy is what finds migration surprises.&lt;/p&gt;
&lt;h2&gt;What staging is for&lt;/h2&gt;
&lt;p&gt;Decide, write it down, and the environment stays healthy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rehearse deploys and migrations before production.&lt;/li&gt;
&lt;li&gt;Let someone click through a feature before release.&lt;/li&gt;
&lt;li&gt;Reproduce a production bug without touching production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What it is not: a long-lived fork of production with its own data edits, or a second production for selected users. Both drift, and drifted staging is worse than none - it provides false confidence.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;/blog/managing-env-files-across-environments/&quot;&gt;env file&lt;/a&gt; for staging points at its own services but mirrors production&amp;#39;s key list, and the quarterly audit keeps it that way. Done right, &amp;quot;it worked on staging&amp;quot; is a sentence worth saying in an incident review.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Branch Naming Conventions That Scale Past Three People</title><link>https://laravelgit.com/blog/branch-naming-conventions-for-small-teams/</link><guid isPermaLink="true">https://laravelgit.com/blog/branch-naming-conventions-for-small-teams/</guid><description>Branch names are a user interface for your team&apos;s work. The conventions that survive growth, the ones that collapse, and why boring always wins.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With two developers, branch names do not matter - you both know what everything is. Add a third, then a fifth, and suddenly &lt;code&gt;fix-stuff&lt;/code&gt; and &lt;code&gt;stefan-wip-2&lt;/code&gt; are a shared inbox full of unmarked letters. A naming convention is not bureaucracy; it is how a branch list becomes readable at a glance.&lt;/p&gt;
&lt;h2&gt;The shape that works&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;BILL-88-retry-failed-invoices
feature/team-invitations
hotfix/checkout-double-charge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three ingredients, in rough order of value:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A ticket reference when one exists.&lt;/strong&gt; &lt;code&gt;BILL-88-retry-failed-invoices&lt;/code&gt; links the branch to the tracker without opening anything. Tools pick this up automatically - GitHub and GitLab cross-reference, and the PR inherits the link. When there is no ticket, skip it; inventing ceremony for a typo fix is worse than omitting it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lowercase words joined by dashes.&lt;/strong&gt; No underscores, no camelCase, no spaces escaped into &lt;code&gt;%20&lt;/code&gt;. Branch names appear in URLs, CI logs and shell commands; the boring format survives all three.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A type prefix for the exceptions.&lt;/strong&gt; &lt;code&gt;hotfix/&lt;/code&gt; and &lt;code&gt;release/&lt;/code&gt; prefixes earn their keep because they signal urgency and different rules - a hotfix skips the queue (see &lt;a href=&quot;/blog/shipping-a-hotfix-without-the-drama/&quot;&gt;shipping a hotfix without the drama&lt;/a&gt;). Feature branches need no prefix; they are the default case.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The conventions that collapse&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Names as ownership.&lt;/strong&gt; &lt;code&gt;stefan-billing&lt;/code&gt; tells you who, not what - and the moment two people touch it, the name is a lie. Ownership lives in the PR assignee, not the branch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Version branches that never die.&lt;/strong&gt; &lt;code&gt;v2-redesign&lt;/code&gt; living for four months is not a branch, it is a fork with anxiety. If work takes that long, the answer is &lt;a href=&quot;/blog/feature-flags-and-trunk-based-development/&quot;&gt;feature flags&lt;/a&gt; and incremental merges, not a longer-lived branch name.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Over-specified formats.&lt;/strong&gt; &lt;code&gt;type/TEAM-1234/short-description/v1&lt;/code&gt; looks great in a wiki and collapses on contact with reality. Every required element is a place to be wrong.&lt;/p&gt;
&lt;h2&gt;Pair it with cleanup&lt;/h2&gt;
&lt;p&gt;Names decay faster than branches. A convention only stays legible if merged branches get deleted - most hosts offer this as a button or an automatic setting - and stale ones get pruned locally (&lt;code&gt;git fetch --prune&lt;/code&gt;, or the config version in &lt;a href=&quot;/blog/settings-worth-changing-in-your-git-config/&quot;&gt;five git config settings worth changing&lt;/a&gt;). A branch list of twelve current names beats a list of two hundred historical ones, whatever convention they follow.&lt;/p&gt;
&lt;p&gt;Pick the boring format, write it in CONTRIBUTING.md, and spend the saved energy on the work the branches exist for.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Branch Protection Rules That Stick</title><link>https://laravelgit.com/blog/branch-protection-rules-that-stick/</link><guid isPermaLink="true">https://laravelgit.com/blog/branch-protection-rules-that-stick/</guid><description>Three rules cover most of the risk on main: review required, tests green, no force-push. More than that tends to get routed around.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Branch protection fails in two ways: too little of it, and so much of it that the team routes around it. For a small team, three rules do most of the work.&lt;/p&gt;
&lt;h2&gt;1. Require a pull request and one review&lt;/h2&gt;
&lt;p&gt;Nobody pushes straight to main, including the senior who &amp;quot;just fixes a typo&amp;quot;. One reviewer is enough at this size — the point is a second pair of eyes, not a committee.&lt;/p&gt;
&lt;h2&gt;2. Require checks to pass&lt;/h2&gt;
&lt;p&gt;The test suite runs on the PR and must be green before merge. This is what keeps &amp;quot;main always runs&amp;quot; true in practice rather than in principle. If the suite is slow, fix the suite — do not downgrade the rule to &amp;quot;tests optional&amp;quot;.&lt;/p&gt;
&lt;h2&gt;3. No force-pushes to main&lt;/h2&gt;
&lt;p&gt;Shared history is append-only. Rewriting main breaks everyone&amp;#39;s clones and makes the audit trail meaningless. Rebasing stays on feature branches, where it belongs.&lt;/p&gt;
&lt;h2&gt;What to leave out until it hurts&lt;/h2&gt;
&lt;p&gt;Code-owner reviews, signed-commit requirements, linear-history-only, status checks from five tools — each adds friction that makes sense at a specific size or risk level. Add them when a real incident names the need, not because the settings page offers them.&lt;/p&gt;
&lt;p&gt;The test of a ruleset is whether the team still respects it in month six. Small and enforced beats thorough and bypassed.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Reviews</category></item><item><title>CODEOWNERS Files: Automatic Reviewers for the Risky Parts of Your Repo</title><link>https://laravelgit.com/blog/codeowners-files-for-laravel-teams/</link><guid isPermaLink="true">https://laravelgit.com/blog/codeowners-files-for-laravel-teams/</guid><description>A CODEOWNERS file routes reviews to the people who know each part of the codebase. How the syntax works, where Laravel teams get the most value, and the failure modes to avoid.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Some pull requests are fine with any reviewer. Some are not: a migration touching a billing table, a change to the deploy script, a tweak to authentication middleware. For those, you want a specific pair of eyes - and you want that to happen by default, not because someone remembered to ask.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;CODEOWNERS&lt;/code&gt; file encodes that routing. When a pull request touches a path with an owner, the platform automatically requests the owner&amp;#39;s review, and &lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;branch protection&lt;/a&gt; can require their approval before merge.&lt;/p&gt;
&lt;h2&gt;The syntax&lt;/h2&gt;
&lt;p&gt;The file lives at &lt;code&gt;.github/CODEOWNERS&lt;/code&gt; (GitHub) or &lt;code&gt;.gitlab/CODEOWNERS&lt;/code&gt; (GitLab), and each line is a pattern followed by one or more owners:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Default owner for everything
* @stefan

# Database changes need the data team
database/migrations/ @stefan @amira

# Deploy and CI config
deploy.sh @ops-team
.github/workflows/ @ops-team

# One specific file
config/services.php @stefan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Patterns follow gitignore-style matching. Later lines override earlier ones for the paths they match, so put the general rules first and the specific ones last.&lt;/p&gt;
&lt;h2&gt;Where Laravel teams get the most value&lt;/h2&gt;
&lt;p&gt;You do not need to own every path. Two or three rules cover most of the risk:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;database/migrations/&lt;/code&gt;&lt;/strong&gt; - migrations are hard to roll back and easy to get subtly wrong. Our post on &lt;a href=&quot;/blog/migrations-in-code-review/&quot;&gt;reviewing migrations&lt;/a&gt; is the checklist your owner will run through.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy and CI configuration&lt;/strong&gt; - &lt;code&gt;deploy.sh&lt;/code&gt;, workflow files, &lt;code&gt;docker-compose.yml&lt;/code&gt;. A broken deploy script costs the whole team&amp;#39;s afternoon.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth and payment code&lt;/strong&gt; - the directories where a mistake is a security incident or a billing bug, not just a UI glitch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything else can stay with the team-wide default: anyone reviews, someone approves.&lt;/p&gt;
&lt;h2&gt;The failure modes&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Owner becomes a bottleneck.&lt;/strong&gt; If every migration needs one person&amp;#39;s approval and they are on holiday, work stops. Name at least two owners for critical paths, and keep the required-approvals count at one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File goes stale.&lt;/strong&gt; People change roles, teams change shape. A CODEOWNERS file pointing at someone who left is worse than none - it creates the illusion of review. Re-read it when the team changes, the same way you re-read branch protection rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ownership as territory.&lt;/strong&gt; The file routes review; it does not grant veto over ideas. If &amp;quot;owner approval required&amp;quot; starts meaning &amp;quot;nobody else may touch this,&amp;quot; that is a team problem the file merely made visible.&lt;/p&gt;
&lt;p&gt;Start small: one file, three rules, two owners per critical path. The first time an automatic review request catches a risky migration before it merges, the file has paid for itself.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Conventional Commits for a Small Team</title><link>https://laravelgit.com/blog/conventional-commits-for-a-small-team/</link><guid isPermaLink="true">https://laravelgit.com/blog/conventional-commits-for-a-small-team/</guid><description>A prefix on every commit message - feat, fix, chore - makes the log scannable and the changelog half-written. Keep the rules short or the team routes around them.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Six months of commit messages that say &amp;quot;fix&amp;quot;, &amp;quot;wip&amp;quot; and &amp;quot;changes&amp;quot; tell the next developer nothing - and on a small team the next developer is usually you. Conventional Commits add one prefix to every message and turn the log into something you can actually read.&lt;/p&gt;
&lt;h2&gt;The whole format&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type(scope): short summary
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The types that earn their keep on a Laravel app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat&lt;/code&gt; - new behaviour (&lt;code&gt;feat(billing): add invoice PDF download&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix&lt;/code&gt; - a bug (&lt;code&gt;fix(queue): stop retrying permanently failed jobs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refactor&lt;/code&gt; - same behaviour, different code&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt; - tests only&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chore&lt;/code&gt; - dependencies, config, tooling&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docs&lt;/code&gt; - the README and friends&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The scope is optional but cheap: one word naming the part of the app (&lt;code&gt;billing&lt;/code&gt;, &lt;code&gt;queue&lt;/code&gt;, &lt;code&gt;auth&lt;/code&gt;). The summary stays imperative and under about 70 characters.&lt;/p&gt;
&lt;h2&gt;What it buys&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git log --oneline&lt;/code&gt; becomes a changelog draft: everything user-visible is a &lt;code&gt;feat&lt;/code&gt; or &lt;code&gt;fix&lt;/code&gt;, everything else filters out. That is exactly the input the tags-to-changelog habit needs, and it makes releases a paste-and-edit job instead of an archaeology dig. Review gets faster too - a &lt;code&gt;refactor&lt;/code&gt; prefix sets expectations before the diff loads, and nobody hunts for behaviour changes in a commit that announced there are none.&lt;/p&gt;
&lt;p&gt;Breaking changes get a &lt;code&gt;!&lt;/code&gt; (&lt;code&gt;feat(api)!: drop v1 endpoints&lt;/code&gt;) or a &lt;code&gt;BREAKING CHANGE:&lt;/code&gt; footer, so they are impossible to miss at release time.&lt;/p&gt;
&lt;h2&gt;Adopt it without a fight&lt;/h2&gt;
&lt;p&gt;The failure mode is process, not spelling: a team that gets a 12-type spec and a CI gate on day one starts writing &lt;code&gt;chore: stuff&lt;/code&gt; to make the check pass. Start smaller:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Five or six types, written in the README or pull request template.&lt;/li&gt;
&lt;li&gt;Scope optional, body only when the why is not obvious from the diff.&lt;/li&gt;
&lt;li&gt;No CI enforcement at first. Squash-merge pull requests and write the conventional message on the squash commit - main stays clean even when the branch&amp;#39;s own commits are messy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If it still feels like overhead after a month, drop it. A convention the team half-follows is worse than none, because it teaches people the log cannot be trusted. The bar is not a perfect history; it is a &lt;code&gt;git log&lt;/code&gt; you can skim before a release and believe.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Releases</category></item><item><title>Deploy With Tags, Not Branches</title><link>https://laravelgit.com/blog/deploy-with-tags-not-branches/</link><guid isPermaLink="true">https://laravelgit.com/blog/deploy-with-tags-not-branches/</guid><description>Environment branches drift, get force-pushed, and lie about what is live. A deploy that checks out a tag is boring, reproducible, and honest.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A common small-team setup has a &lt;code&gt;production&lt;/code&gt; branch: merge into it, push, and the server pulls it. It works until the day someone commits directly to it, force-pushes it, or merges the wrong thing and the branch history no longer matches what anyone intended. The branch stops being a record and becomes a rumor.&lt;/p&gt;
&lt;p&gt;A tag does not have these problems.&lt;/p&gt;
&lt;h2&gt;What deploying a tag looks like&lt;/h2&gt;
&lt;p&gt;The release flow is: finish work on main, tag the commit, deploy the tag.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git tag -a v1.14.0 -m &amp;quot;v1.14.0&amp;quot;
git push origin v1.14.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the server, instead of &lt;code&gt;git pull&lt;/code&gt;, the deploy script fetches and checks out the exact tag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git fetch --tags
git checkout v1.14.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a detached HEAD on purpose - the server is not a place where commits happen, so there is nothing to lose. The result is that &amp;quot;what is running in production&amp;quot; has one unambiguous answer, and it is the same answer &lt;code&gt;git tag&lt;/code&gt; gives everyone on the team.&lt;/p&gt;
&lt;h2&gt;Why this beats an environment branch&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tags do not drift. A branch can pick up a stray commit; a tag points at one commit forever.&lt;/li&gt;
&lt;li&gt;Rollback is a checkout. Deploy the previous tag and you are back, with no revert commit needed - &lt;a href=&quot;/blog/when-to-reset-restore-or-revert/&quot;&gt;When to Reset, Restore, or Revert&lt;/a&gt; stays a developer-machine topic, not a 2 AM production topic.&lt;/li&gt;
&lt;li&gt;The release log writes itself. Tag messages and annotated-tag history double as the skeleton of a changelog, which is the whole trick in &lt;a href=&quot;/blog/keep-a-changelog-from-git-tags/&quot;&gt;Keep a Changelog Straight From Git Tags&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href=&quot;/blog/tagging-releases-properly/&quot;&gt;Tagging Releases Properly&lt;/a&gt; post covers annotation and naming; this one is about making the tag the unit of deployment.&lt;/p&gt;
&lt;h2&gt;Keeping staging honest&lt;/h2&gt;
&lt;p&gt;If you want a staging environment, deploy the same way: tag a release candidate (&lt;code&gt;v1.14.0-rc.1&lt;/code&gt;), deploy that tag to staging, and promote by re-tagging or tagging the final release on the same commit. &amp;quot;Staging runs something production has never seen&amp;quot; becomes structurally impossible.&lt;/p&gt;
&lt;p&gt;For the full pipeline around this - build steps, env handling, the push-to-server mechanics - &lt;a href=&quot;/blog/deploy-laravel-with-git-push/&quot;&gt;Deploy Laravel With Git Push&lt;/a&gt; walks through it. The short version of both posts: branches are for work in progress, tags are for facts. Deploy facts.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Does Your Team Need a Merge Queue? Probably Not Yet</title><link>https://laravelgit.com/blog/does-your-team-need-a-merge-queue/</link><guid isPermaLink="true">https://laravelgit.com/blog/does-your-team-need-a-merge-queue/</guid><description>Merge queues solve a real problem - broken main from semantically conflicting PRs - but the cure is heavier than the disease until a specific threshold. How to tell when you have crossed it.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A merge queue is CI&amp;#39;s answer to a sneaky failure: two pull requests, each green against main, that break when combined. One renames a function; the other adds a new call to the old name. Both pass, both merge, main is red. The queue fixes this by testing each PR against main plus everything already queued, so &amp;quot;green&amp;quot; means &amp;quot;green against what main will actually be.&amp;quot;&lt;/p&gt;
&lt;p&gt;GitHub, GitLab and Bors all offer one. The question is not whether they work - they do - but whether your team has the problem they solve.&lt;/p&gt;
&lt;h2&gt;The threshold&lt;/h2&gt;
&lt;p&gt;The broken-main-from-colliding-PRs problem needs two ingredients: enough parallel merges that collisions are likely, and a test suite slow enough that &amp;quot;just rebase and rerun CI&amp;quot; is painful. A team merging three pull requests a day with a five-minute suite almost never sees it. A team merging thirty a day with a forty-minute suite sees it weekly.&lt;/p&gt;
&lt;p&gt;Some honest signals you have crossed the line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Main breaks from semantic conflicts more than once a month, and each break costs real debugging time.&lt;/li&gt;
&lt;li&gt;Reviewers ask authors to &amp;quot;rebase and make sure it is still green&amp;quot; so often that the rebase-rerun loop is the bottleneck.&lt;/li&gt;
&lt;li&gt;Releases get delayed because main went red at the wrong moment.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What to do before you get there&lt;/h2&gt;
&lt;p&gt;Most small teams get the same protection cheaper:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Keep CI fast.&lt;/strong&gt; A five-minute suite makes &amp;quot;rebase and rerun&amp;quot; a shrug, not an afternoon. This is the same investment as always - &lt;a href=&quot;/blog/shallow-and-partial-clones-for-faster-ci/&quot;&gt;shallow clones&lt;/a&gt; and dependency caching - and it pays whether or not you ever need a queue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Require branches to be up to date before merging&lt;/strong&gt; on the riskiest repo. GitHub&amp;#39;s branch protection has this checkbox; it forces the rebase-rerun loop explicitly, which is exactly a manual merge queue. Fine at low volume, painful at high volume - which is your signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Merge promptly after approval.&lt;/strong&gt; Most collisions happen because approved PRs sit for days while main moves on. A queue will not fix a review-and-forget habit; &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small pull requests&lt;/a&gt; will.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When the manual loop becomes the thing slowing everyone down, the queue stops being infrastructure cosplay and starts paying rent. Until then, the honest answer is: not yet - and here is how you will know.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Use Draft Pull Requests to Get Feedback Early</title><link>https://laravelgit.com/blog/draft-pull-requests-for-early-feedback/</link><guid isPermaLink="true">https://laravelgit.com/blog/draft-pull-requests-for-early-feedback/</guid><description>Open the PR before the work is done. A draft invites direction-level feedback while changes are still cheap, and it shows the team what is in flight.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The traditional pull request arrives finished: code done, tests written, author braced for judgment. By that point the expensive decisions are locked in, and &amp;quot;have you considered a different approach?&amp;quot; means redoing two days of work. A draft PR flips the order - feedback lands while changing course is still cheap.&lt;/p&gt;
&lt;h2&gt;What a draft is for&lt;/h2&gt;
&lt;p&gt;Open the PR as a draft the moment the shape of the change exists: the migration, a sketched controller, maybe a failing test that names the behavior. The draft status says plainly: do not review the details yet, do look at the direction.&lt;/p&gt;
&lt;p&gt;That early look catches the reviews that matter most:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;This duplicates what the billing package already does&amp;quot; - before you polish your version.&lt;/li&gt;
&lt;li&gt;&amp;quot;The column should live on the invoices table, not the users table&amp;quot; - before the migration ships.&lt;/li&gt;
&lt;li&gt;&amp;quot;We tried this approach in March, here is why it hurt&amp;quot; - before you rediscover March.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The working agreement&lt;/h2&gt;
&lt;p&gt;Drafts only work if the team reads them in the right spirit. Two conventions keep it healthy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The author says what they want. &amp;quot;Does this structure make sense?&amp;quot; gets a useful answer; silence about intent gets either no review or a full one you did not want yet. The &lt;a href=&quot;/blog/writing-a-good-pull-request-description/&quot;&gt;PR description&lt;/a&gt; matters more, not less, in a draft.&lt;/li&gt;
&lt;li&gt;Nobody treats a draft as a promise. Scope can shrink, approach can change, the branch can be rebased - drafts are &lt;a href=&quot;/blog/why-you-should-force-push-with-lease/&quot;&gt;force-push-with-lease&lt;/a&gt; territory by default.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Marking the PR ready for review is then a real signal: the direction is settled, now judge the details.&lt;/p&gt;
&lt;h2&gt;The quiet side benefit&lt;/h2&gt;
&lt;p&gt;Drafts double as a status board. A team that opens drafts early can see what is in flight without asking - which prevents the awkward moment where two people discover at merge time that they both spent the week on the same invoice controller. It also feeds the habit that keeps everything else moving: work arrives in &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small, reviewable pieces&lt;/a&gt;, because a draft that has been open for three weeks is visibly not that.&lt;/p&gt;
&lt;p&gt;Open it early, say what you want, mark it ready when it is. The PR stops being a verdict and becomes a conversation - which was the point of having a team.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Feature Flags: How to Merge Daily Without Shipping Half-Built Work</title><link>https://laravelgit.com/blog/feature-flags-and-trunk-based-development/</link><guid isPermaLink="true">https://laravelgit.com/blog/feature-flags-and-trunk-based-development/</guid><description>Long-lived feature branches exist because unfinished code cannot go to production. Feature flags remove that premise - and pair naturally with trunk-based development.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The argument for a long-lived feature branch goes: the work takes three weeks, it cannot ship half-done, so it lives on a branch until it is ready. The cost shows up at the end - a three-week merge, conflicts with everything that moved underneath, and a review nobody can hold in their head.&lt;/p&gt;
&lt;p&gt;Feature flags attack the premise instead of the branch. If unfinished code can sit in production safely, invisible behind a flag, there is no reason to keep it out of main.&lt;/p&gt;
&lt;h2&gt;What a feature flag is&lt;/h2&gt;
&lt;p&gt;A flag is a runtime check around new behaviour:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (feature(&amp;#39;new-billing-dashboard&amp;#39;)) {
    return view(&amp;#39;billing.dashboard-new&amp;#39;);
}
return view(&amp;#39;billing.dashboard&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laravel teams have good options for the &lt;code&gt;feature()&lt;/code&gt; part: Laravel Pennant (first-party, ships with recent Laravel versions), a config value, or an external service. The mechanism matters less than the habit: new behaviour ships dark, off by default, and turns on when it is ready - per user, per team, or for everyone.&lt;/p&gt;
&lt;h2&gt;Why it pairs with trunk-based development&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/blog/trunk-based-development-for-small-laravel-teams/&quot;&gt;Trunk-based development&lt;/a&gt; asks for small, frequent merges into main. The objection is always &amp;quot;but my feature is not done.&amp;quot; Flags answer it: merge the first slice behind a flag on Monday, the second on Wednesday, the third next week. Each merge is small, reviewed quickly, and deployed continuously - while users see nothing until you flip the flag.&lt;/p&gt;
&lt;p&gt;The bonus nobody expects: rolling out to 5% of users, then 50%, then everyone is free. And if something breaks, &amp;quot;roll back&amp;quot; is turning the flag off - no revert deploy, no drama, exactly the cheap-exit thinking in &lt;a href=&quot;/blog/rollback-or-roll-forward/&quot;&gt;rollback or roll forward&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The disciplines that keep flags healthy&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Name flags for the feature, not the date.&lt;/strong&gt; &lt;code&gt;new-billing-dashboard&lt;/code&gt; is clear in six months; &lt;code&gt;experiment-2026-10&lt;/code&gt; is not.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flag the smallest unit of behaviour.&lt;/strong&gt; One flag around one feature. A flag that controls three unrelated things cannot be turned off cleanly when one of them misbehaves.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Delete flags when the rollout finishes.&lt;/strong&gt; A flag that stays at 100% forever is an &lt;code&gt;if&lt;/code&gt; statement with extra steps. Make &amp;quot;remove the flag&amp;quot; the last task on the feature ticket, and grep for stale flags every quarter. This is the unglamorous part, and the part that decides whether flags stay a tool or become sediment.&lt;/p&gt;
&lt;p&gt;Start with one flag on your next multi-day feature. The first time you merge on day one instead of week three - and turn the thing off from your phone when it misbehaves - the branch-everything habit loses its grip.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Git Bisect: Find the Commit That Broke It</title><link>https://laravelgit.com/blog/git-bisect-find-the-commit-that-broke-it/</link><guid isPermaLink="true">https://laravelgit.com/blog/git-bisect-find-the-commit-that-broke-it/</guid><description>The tests passed last week and fail now. Bisect turns &apos;somewhere in these 40 commits&apos; into six test runs.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Something that worked last release is broken now, and forty commits sit between then and now. Reading every diff is the slow way. &lt;code&gt;git bisect&lt;/code&gt; does a binary search over the range, so six checks find the culprit instead of forty.&lt;/p&gt;
&lt;h2&gt;Mark the ends&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bisect start
git bisect bad            # the commit you are on is broken
git bisect good v1.4.0    # the last tag you know was fine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git checks out a commit in the middle. Run whatever reproduces the bug — the feature test, the artisan command, the page that 500s — then tell Git the answer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bisect good   # or: git bisect bad
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Repeat. Each answer halves the range, and after a few rounds Git names the first bad commit with its full diff and message.&lt;/p&gt;
&lt;h2&gt;Let the test suite drive&lt;/h2&gt;
&lt;p&gt;If a single command answers &amp;quot;is this commit broken&amp;quot;, hand the whole search to it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bisect run php artisan test --filter=CheckoutTest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bisect now walks the range alone, marks each commit from the exit code, and stops on the first failure. This is the version worth using: it is fast, and it removes the temptation to eyeball a step and guess.&lt;/p&gt;
&lt;h2&gt;Two practical bumps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A commit that cannot run at all — a dependency changed and the test cannot boot — stalls the search. &lt;code&gt;git bisect skip&lt;/code&gt; tells Git to step over it, and &lt;code&gt;git bisect run&lt;/code&gt; can skip automatically when the command exits with 125.&lt;/li&gt;
&lt;li&gt;Vendored directories move between commits. Between steps you may need &lt;code&gt;composer install&lt;/code&gt;; inside &lt;code&gt;bisect run&lt;/code&gt;, a tiny shell script that runs it before the test keeps every commit bootable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When it names a commit&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bisect reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That returns you to where you started. The bisect log disappears, so note the hash first. The usual next step is not the fix — it is a failing regression test on current main that proves the bug, so the fix that follows can never quietly regress.&lt;/p&gt;
&lt;p&gt;Bisect rewards the boring habits: small commits that each boot, and tags you can trust as &amp;quot;known good&amp;quot;. Those habits are cheap right up until the day they are the fastest way back to a working app.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Managing .env Files Across Environments</title><link>https://laravelgit.com/blog/managing-env-files-across-environments/</link><guid isPermaLink="true">https://laravelgit.com/blog/managing-env-files-across-environments/</guid><description>Local, staging, production - each needs its own .env, none of them belongs in Git. The setup that keeps secrets out and onboarding fast.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every Laravel app has the same three-way split: your laptop, a staging server, production. Each needs its own &lt;code&gt;.env&lt;/code&gt;, and the &lt;code&gt;.env&lt;/code&gt; itself must never be committed - &lt;a href=&quot;/blog/what-not-to-commit-in-a-laravel-project/&quot;&gt;What Not to Commit&lt;/a&gt; covers why. What is worth deciding deliberately is how the values get to each environment and how the team learns about new keys.&lt;/p&gt;
&lt;h2&gt;.env.example is the contract&lt;/h2&gt;
&lt;p&gt;Commit &lt;code&gt;.env.example&lt;/code&gt; with every key and a safe placeholder value, and treat it as part of every pull request that adds configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MAIL_MAILER=
QUEUE_CONNECTION=
INVOICE_SIGNING_KEY=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new hire runs &lt;code&gt;cp .env.example .env&lt;/code&gt;, fills in the blanks, and the app boots. A reviewer sees the new key in the PR diff without seeing its value. When someone adds a key to their local &lt;code&gt;.env&lt;/code&gt; without updating the example, the next person&amp;#39;s setup breaks quietly - so make &amp;quot;PR adds a key to .env.example&amp;quot; as automatic as &amp;quot;PR includes a migration&amp;quot;.&lt;/p&gt;
&lt;h2&gt;Getting values to servers&lt;/h2&gt;
&lt;p&gt;Servers have no checkout step where you can edit a file by hand, so pick a delivery mechanism:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The deploy tool&amp;#39;s secrets store (most managed deploy platforms have one).&lt;/li&gt;
&lt;li&gt;A file written by your deploy script from your password manager or vault - never from the repo.&lt;/li&gt;
&lt;li&gt;On a self-hosted box you administer: a plain &lt;code&gt;.env&lt;/code&gt; in the shared directory of your &lt;a href=&quot;/blog/zero-downtime-deploys-for-laravel/&quot;&gt;releases structure&lt;/a&gt;, permissions &lt;code&gt;600&lt;/code&gt;, owned by the deploy user.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All three share one property: the value&amp;#39;s home is outside Git, and changing it does not require a commit.&lt;/p&gt;
&lt;h2&gt;After changing values&lt;/h2&gt;
&lt;p&gt;Laravel caches config aggressively in production, so a changed &lt;code&gt;.env&lt;/code&gt; does nothing until you rebuild the cache:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan config:cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fold that into the deploy script and &amp;quot;I changed it but nothing happened&amp;quot; stops being a genre of bug report. Also restart the queue workers - they hold the old config in memory, same lesson as the deploy post&amp;#39;s &lt;code&gt;queue:restart&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The audit habit&lt;/h2&gt;
&lt;p&gt;Once a quarter, diff each environment&amp;#39;s keys against &lt;code&gt;.env.example&lt;/code&gt;: keys present on servers but missing from the example are undocumented config, and keys in the example that no environment sets are dead weight. Two minutes with &lt;code&gt;comm&lt;/code&gt; on sorted key lists keeps the contract honest.&lt;/p&gt;
&lt;p&gt;Secrets stay out of history - and if one ever lands there, &lt;a href=&quot;/blog/a-secret-got-committed-now-what/&quot;&gt;A Secret Got Committed. Now What?&lt;/a&gt; is the cleanup drill.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Merging vs Rebasing on a Small Team</title><link>https://laravelgit.com/blog/merging-vs-rebasing-for-small-teams/</link><guid isPermaLink="true">https://laravelgit.com/blog/merging-vs-rebasing-for-small-teams/</guid><description>Rebase to tidy your own branch, merge to combine into main. One sentence of policy saves a lot of history arguments.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Few git arguments cost more than they settle. The working rule for a small team fits in one sentence: rebase your own branch, merge into main.&lt;/p&gt;
&lt;h2&gt;Rebase is for your branch&lt;/h2&gt;
&lt;p&gt;While your branch is still yours, keep it current and readable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git fetch origin
git rebase origin/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your commits replay on top of main, conflicts show up in small pieces while you still remember the change, and the branch reads like a story a reviewer can follow.&lt;/p&gt;
&lt;h2&gt;Merge is for main&lt;/h2&gt;
&lt;p&gt;When the branch is done, merge it. The merge commit marks where the work landed and keeps the group&amp;#39;s shared history untouched:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout main
git merge --no-ff pr-42-branch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--no-ff&lt;/code&gt; keeps the feature visible as one unit, which makes the changelog draft and any later revert much easier.&lt;/p&gt;
&lt;h2&gt;What not to do&lt;/h2&gt;
&lt;p&gt;Never rebase a branch someone else has pulled. Rewriting shared history is how teammates lose work and trust the repo a little less. If it is pushed and shared, it merges.&lt;/p&gt;
&lt;p&gt;That is the whole policy. Write it in the README and spend the argument on the code instead.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Reviews</category></item><item><title>Monorepo or Many Repos for a Small Laravel Team</title><link>https://laravelgit.com/blog/monorepo-or-many-repos/</link><guid isPermaLink="true">https://laravelgit.com/blog/monorepo-or-many-repos/</guid><description>One repo for the app and its packages, or a repo per package? For small teams the boring default wins: start with one repo and split only when it hurts.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Laravel projects grow extra code over time: a shared package, an internal admin tool, maybe a second app that reuses the first one&amp;#39;s domain logic. At some point you have to decide whether everything lives in one repository or gets split into several.&lt;/p&gt;
&lt;h2&gt;Start with one repo&lt;/h2&gt;
&lt;p&gt;For a small team, one repository is the right default for a simple reason: every change is atomic. When the app and its shared package live together, a rename lands in a single commit, CI runs once, and there is no version matrix to keep in your head. Composer makes this comfortable with path repositories:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;repositories&amp;quot;: [
  { &amp;quot;type&amp;quot;: &amp;quot;path&amp;quot;, &amp;quot;url&amp;quot;: &amp;quot;packages/billing&amp;quot; }
],
&amp;quot;require&amp;quot;: {
  &amp;quot;acme/billing&amp;quot;: &amp;quot;*&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The package sits in &lt;code&gt;packages/billing&lt;/code&gt; inside the same repo, and Composer symlinks it in. You get the directory structure of a split without any of the coordination cost.&lt;/p&gt;
&lt;h2&gt;When splitting starts to make sense&lt;/h2&gt;
&lt;p&gt;Split a repo only when one of these becomes true:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A package is genuinely reused across projects with different release cadences, and tagging it independently matters.&lt;/li&gt;
&lt;li&gt;Different people should have different access rights - a contractor on the marketing site should not see billing code.&lt;/li&gt;
&lt;li&gt;CI time is dominated by unrelated code, and path-scoped pipelines cannot fix it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;quot;One day this might be an open-source package&amp;quot; is not on the list. That day can be handled when it arrives.&lt;/p&gt;
&lt;h2&gt;Splitting later, keeping history&lt;/h2&gt;
&lt;p&gt;When the day comes, you do not have to lose history. &lt;code&gt;git subtree split&lt;/code&gt; extracts a directory into its own branch with every commit that touched it, ready to push into a fresh repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git subtree split --prefix=packages/billing -b billing-history
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a much better story than the usual alternative of planning repository topology up front for a future that may never happen. It is also worth contrasting with &lt;a href=&quot;/blog/submodules-when-a-laravel-team-needs-one/&quot;&gt;submodules&lt;/a&gt;, which solve a different problem - vendoring an outside dependency with its own history - and bring real day-to-day friction.&lt;/p&gt;
&lt;p&gt;The short version: one repo until something concrete hurts, path repositories for internal packages, subtree split when a package earns its independence. Repository topology is a scaling decision, and small teams scale by not making it early.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Onboarding a New Developer to Your Git Workflow</title><link>https://laravelgit.com/blog/onboarding-a-developer-to-your-git-workflow/</link><guid isPermaLink="true">https://laravelgit.com/blog/onboarding-a-developer-to-your-git-workflow/</guid><description>A new hire&apos;s first week sets their Git habits for years. Write down the five decisions, automate the enforceable ones, and pair on the first PR.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every team has a Git workflow, but most teams keep it in their heads. A new developer then learns it by breaking it: committing to the wrong branch, force-pushing the wrong thing, opening a 2,000-line PR on day three. A one-page written workflow and a little automation fix most of this before it happens.&lt;/p&gt;
&lt;h2&gt;Write down the five decisions&lt;/h2&gt;
&lt;p&gt;However your team works, these five choices define it. Put them in the README or CONTRIBUTING.md:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Where work happens: feature branches off &lt;code&gt;main&lt;/code&gt;, or straight to main for tiny teams.&lt;/li&gt;
&lt;li&gt;How branches are named: &lt;code&gt;feature/short-name&lt;/code&gt;, &lt;code&gt;fix/thing&lt;/code&gt;, whatever - consistency matters more than the scheme.&lt;/li&gt;
&lt;li&gt;What a pull request needs: description, linked ticket, one approval, green CI.&lt;/li&gt;
&lt;li&gt;How merges land: merge commit, squash, or rebase - &lt;a href=&quot;/blog/merging-vs-rebasing-for-small-teams/&quot;&gt;Merging vs Rebasing for Small Teams&lt;/a&gt; if this is still unsettled.&lt;/li&gt;
&lt;li&gt;How releases happen: tags, changelogs, deploy steps. &lt;a href=&quot;/blog/tagging-releases-properly/&quot;&gt;Tagging Releases Properly&lt;/a&gt; covers the shape.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A new hire who can read those five lines commits correctly on day one.&lt;/p&gt;
&lt;h2&gt;Automate what is enforceable&lt;/h2&gt;
&lt;p&gt;Rules that live only in the document decay. The ones that survive are enforced by the repo itself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;Branch protection&lt;/a&gt; on main, so &amp;quot;don&amp;#39;t push to main&amp;quot; is a fact, not a preference.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/git-hooks-for-laravel-projects/&quot;&gt;Git hooks&lt;/a&gt; for the local checks - Pint, tests, no debugging leftovers.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/conventional-commits-for-a-small-team/&quot;&gt;Conventional commits&lt;/a&gt; if the team wants parseable history, with a commit-msg hook to back it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automation also shortens the document: you stop writing rules down once the tooling states them for you.&lt;/p&gt;
&lt;h2&gt;Pair on the first pull request&lt;/h2&gt;
&lt;p&gt;The highest-leverage onboarding hour is walking a new developer through their first PR the team&amp;#39;s way: small branch, tidy commits, a description a stranger could review from, responding to comments without defensiveness. Point them at the team&amp;#39;s own posts on &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small pull requests&lt;/a&gt; and &lt;a href=&quot;/blog/writing-a-good-pull-request-description/&quot;&gt;PR descriptions&lt;/a&gt; if you have them - or write yours down once and link it forever.&lt;/p&gt;
&lt;p&gt;The measure of a good onboarding is how boring the second week is: the new hire ships a small PR, gets one round of comments, merges, and nobody had to correct a Git habit. That outcome is designed, not lucky.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Pull Request Templates That People Actually Fill In</title><link>https://laravelgit.com/blog/pull-request-templates-that-get-filled-in/</link><guid isPermaLink="true">https://laravelgit.com/blog/pull-request-templates-that-get-filled-in/</guid><description>A PR template should make good descriptions the path of least resistance. What to put in one, what to leave out, and why shorter always wins.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A pull request with an empty description is a small act of unkindness to its reviewer. They have to reconstruct the intent from the diff, guess at the testing, and ask questions the author could have answered in thirty seconds. Teams fix this with a template - then often ruin it by making the template a questionnaire nobody reads.&lt;/p&gt;
&lt;h2&gt;What a template is for&lt;/h2&gt;
&lt;p&gt;Drop a file at &lt;code&gt;.github/pull_request_template.md&lt;/code&gt; (or &lt;code&gt;.gitlab/merge_request_templates/Default.md&lt;/code&gt;) and every new pull request starts with its contents pre-filled. The author&amp;#39;s job shifts from &amp;quot;write something from nothing&amp;quot; to &amp;quot;fill in the blanks&amp;quot; - which is a much lower bar, and that is the whole point.&lt;/p&gt;
&lt;h2&gt;The sections that earn their place&lt;/h2&gt;
&lt;p&gt;Three prompts cover almost everything a reviewer needs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## What
One or two sentences: what does this change do?

## Why
What problem does it solve? Link the ticket if there is one.

## How tested
What did you run, click, or deploy to staging to convince yourself?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;quot;What&amp;quot; and &amp;quot;why&amp;quot; are the difference between a review and an interrogation. &amp;quot;How tested&amp;quot; is the one authors skip when there is no prompt, and the one that catches &amp;quot;I did not actually run it&amp;quot; before the reviewer does.&lt;/p&gt;
&lt;p&gt;For risky changes, one optional line helps: &lt;code&gt;**Risk:** what breaks if this is wrong?&lt;/code&gt; Leave it in the template as a comment or optional heading, and the answer doubles as your rollback plan - which pairs well with the habits in &lt;a href=&quot;/blog/rollback-or-roll-forward/&quot;&gt;rollback or roll forward&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What to leave out&lt;/h2&gt;
&lt;p&gt;Every field you add is a tax on every pull request. Skip:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Long checklists of things CI already checks.&lt;/strong&gt; &amp;quot;Tests pass&amp;quot; is a status badge, not a checkbox.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legalistic boilerplate.&lt;/strong&gt; Nobody has ever been protected by &amp;quot;I confirm I have read the contributing guide.&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sections that only apply to one pull request in fifty.&lt;/strong&gt; Database migration notes, screenshots, API changes - authors will add these when relevant. A heading they must delete is friction, not help.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a section is empty on most merged pull requests, cut it.&lt;/p&gt;
&lt;h2&gt;Keep it alive&lt;/h2&gt;
&lt;p&gt;A template is a living document with exactly one success metric: do merged pull requests have useful descriptions? Skim the last ten. If &amp;quot;How tested&amp;quot; is always &amp;quot;yes&amp;quot;, sharpen the prompt (&amp;quot;what command did you run?&amp;quot;). If a section is always deleted, remove it.&lt;/p&gt;
&lt;p&gt;The template is also a forcing function for smaller work: a pull request too big to describe in two sentences is a pull request that should be split, which is exactly the argument in &lt;a href=&quot;/blog/small-pull-requests-get-reviewed-faster/&quot;&gt;small pull requests get reviewed faster&lt;/a&gt;. And when the description is written, the advice in &lt;a href=&quot;/blog/writing-a-good-pull-request-description/&quot;&gt;writing a good pull request description&lt;/a&gt; makes it land.&lt;/p&gt;
&lt;p&gt;Start with three headings and resist every addition. The best template is the one nobody notices.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Rollback or Roll Forward? Decide Before You Need To</title><link>https://laravelgit.com/blog/rollback-or-roll-forward/</link><guid isPermaLink="true">https://laravelgit.com/blog/rollback-or-roll-forward/</guid><description>When a deploy breaks production, you either go back or fix ahead. The right answer depends on your migrations, your data, and how fast each path is.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Something is wrong in production. The temptation is to start fixing immediately - but the first decision is which direction to move: back to the last good release, or forward to a fix. Teams that decide the criteria in advance make this call in seconds; teams that improvise it make it under pressure, badly.&lt;/p&gt;
&lt;h2&gt;Roll back when you can&lt;/h2&gt;
&lt;p&gt;Going back is the better default whenever it is actually available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The bug is in the new code, not in data the new code wrote.&lt;/li&gt;
&lt;li&gt;No migration in this release changed the schema in a way old code cannot live with.&lt;/li&gt;
&lt;li&gt;Your deploys are &lt;a href=&quot;/blog/zero-downtime-deploys-for-laravel/&quot;&gt;structured for it&lt;/a&gt; - a symlink swap back, or deploying the previous &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;tag&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A rollback is one known-good state replacing one known-bad state. There is nothing to debug; you are simply not on fire anymore.&lt;/p&gt;
&lt;h2&gt;Roll forward when you must&lt;/h2&gt;
&lt;p&gt;Sometimes back is closed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A destructive migration already ran - dropped or renamed columns the old code expects.&lt;/li&gt;
&lt;li&gt;Users have created data under the new behavior, and reverting would strand or corrupt it.&lt;/li&gt;
&lt;li&gt;The &amp;quot;old&amp;quot; state has its own known bug that this release was fixing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then the fix is a new commit on main, reviewed as fast as a review can responsibly go, deployed as a new release. Note what rolling forward is not: committing directly to the server&amp;#39;s working tree. Even mid-incident, the fix goes through Git - a branch, a very short PR, a deploy - because the history of what happened during the incident is exactly what you will want afterwards. &lt;a href=&quot;/blog/when-to-reset-restore-or-revert/&quot;&gt;When to Reset, Restore, or Revert&lt;/a&gt; covers the mechanics; the revert of the bad commit belongs in history either way.&lt;/p&gt;
&lt;h2&gt;The deciding question&lt;/h2&gt;
&lt;p&gt;Ask one thing: can the old code run against the current database? If yes, roll back - it is faster and safer than any fix you will write under stress. If no, you are rolling forward, and the time to accept that is immediately, not after forty minutes of trying to make rollback possible.&lt;/p&gt;
&lt;p&gt;The two habits that keep the rollback door open are boring ones: backward-compatible migrations, and releases structured so going back is one command. Both are decided on quiet days, which is the only time incident decisions ever get made well.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Shipping a Hotfix Without the Drama</title><link>https://laravelgit.com/blog/shipping-a-hotfix-without-the-drama/</link><guid isPermaLink="true">https://laravelgit.com/blog/shipping-a-hotfix-without-the-drama/</guid><description>Branch from the live tag, fix one thing, tag the release, merge back. A hotfix is a small normal deploy, not a breach of process.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Production is broken and main has moved on - it contains three half-finished features that cannot ship today. The hotfix question is how to patch what is live without dragging main along. The answer is short: branch from the tag that is running, fix there, tag, deploy, merge back.&lt;/p&gt;
&lt;h2&gt;The walkthrough&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git switch -c hotfix/invoice-total-rounding v1.14.0
# fix the one thing, with a test
git tag -a v1.14.1 -m &amp;quot;v1.14.1&amp;quot;
git push origin hotfix/invoice-total-rounding v1.14.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deploy &lt;code&gt;v1.14.1&lt;/code&gt; exactly the way you deploy everything else - &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;Deploy With Tags, Not Branches&lt;/a&gt; applies even more under pressure, because &amp;quot;what is running&amp;quot; must have one unambiguous answer during an incident.&lt;/p&gt;
&lt;p&gt;Then bring the fix home so the next regular release does not reintroduce the bug:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git switch main
git merge hotfix/invoice-total-rounding
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Discipline under pressure&lt;/h2&gt;
&lt;p&gt;The rules that keep a hotfix small:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One fix.&lt;/strong&gt; A hotfix branch is not a vehicle for the cleanup you noticed on the way in. Everything else goes into a normal PR tomorrow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The test comes too.&lt;/strong&gt; If the bug was reachable once, it is reachable again; the test is what makes &amp;quot;fixed&amp;quot; verifiable rather than hoped. This is the moment the &lt;a href=&quot;/blog/a-first-ci-pipeline-for-laravel/&quot;&gt;first CI pipeline&lt;/a&gt; earns its keep - the hotfix PR gets the same green check as everything else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No shortcuts around review.&lt;/strong&gt; One approval, even a two-minute one, catches the mistake made at speed. &lt;a href=&quot;/blog/branch-protection-rules-that-stick/&quot;&gt;Branch protection&lt;/a&gt; should have no &amp;quot;except hotfixes&amp;quot; hole; it should have a fast path through the normal gate.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;If the fix is bigger than a hotfix&lt;/h2&gt;
&lt;p&gt;When the honest assessment is &amp;quot;this needs a day of work,&amp;quot; stop calling it a hotfix and decide &lt;a href=&quot;/blog/rollback-or-roll-forward/&quot;&gt;rollback or roll-forward&lt;/a&gt; instead. A rollback buys the day cleanly. The worst outcome is the middle path: an ever-growing hotfix branch that is secretly a release, deployed with none of the care a release gets.&lt;/p&gt;
&lt;p&gt;Branch from the tag, fix one thing, tag, merge back. Written down and rehearsed once, the next production bug is a procedure, not an event.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Squash Merge or Merge Commit: Choosing at the PR Button</title><link>https://laravelgit.com/blog/squash-merge-or-merge-commit/</link><guid isPermaLink="true">https://laravelgit.com/blog/squash-merge-or-merge-commit/</guid><description>The merge button offers three histories for the same code. Pick by what the branch&apos;s commits are worth, not by habit.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The pull request is approved and the button offers a choice: merge commit, squash, or rebase. The code landing on main is identical either way; what changes is the story main tells afterwards.&lt;/p&gt;
&lt;h2&gt;Merge commit: keep the branch as it was&lt;/h2&gt;
&lt;p&gt;A merge commit preserves every commit on the branch plus a marker joining it to main. Choose it when the branch&amp;#39;s commits are already clean - tidied with an interactive rebase, each one a real step. Main keeps the full context, and &lt;code&gt;git log --graph&lt;/code&gt; shows the branch as a readable bubble.&lt;/p&gt;
&lt;p&gt;The cost: if the branch was full of &amp;quot;wip&amp;quot; and &amp;quot;fix&amp;quot;, all of that lands on main forever.&lt;/p&gt;
&lt;h2&gt;Squash: the branch becomes one commit&lt;/h2&gt;
&lt;p&gt;Squashing collapses the whole PR into a single commit on main. Choose it for the common case: a feature branch with exploratory, fix-as-you-go commits whose value was the journey, not the record. Main gains one commit per piece of work, which is exactly the granularity a changelog and a &lt;code&gt;git log --oneline&lt;/code&gt; scan want.&lt;/p&gt;
&lt;p&gt;Two things make squashing work well. Write the squash commit message properly - it is the only message main will keep, so give it the conventional prefix and a real summary. And delete the branch after merging: pushing more commits to a branch that was already squash-merged produces a follow-up PR full of commits main already has, because the squashed commit shares no history with them.&lt;/p&gt;
&lt;h2&gt;Rebase merge: linear, commit by commit&lt;/h2&gt;
&lt;p&gt;Rebasing at merge replays the branch&amp;#39;s commits onto main one by one, no merge bubble. Choose it for small PRs that are already clean and where the team wants a strictly linear main. It asks the most of the branch - every commit lands verbatim, &amp;quot;fix typo&amp;quot; included - so it only suits branches that were tidied first.&lt;/p&gt;
&lt;h2&gt;A rule a small team can keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Branch is messy (the usual case): &lt;strong&gt;squash&lt;/strong&gt;, write a real message, delete the branch.&lt;/li&gt;
&lt;li&gt;Branch is clean and the steps matter: &lt;strong&gt;merge commit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Branch is clean and tiny: &lt;strong&gt;rebase&lt;/strong&gt; is fine, but squashing it is fine too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Agree on it once, write it in the README, and the choice stops costing a thought per PR.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>The Reflog: Undoing Almost Anything in Git</title><link>https://laravelgit.com/blog/the-reflog-undoing-almost-anything/</link><guid isPermaLink="true">https://laravelgit.com/blog/the-reflog-undoing-almost-anything/</guid><description>Reset too far, deleted the wrong branch, botched a rebase? The reflog remembers where HEAD has been, and that is usually enough.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Git rarely loses commits; it loses your pointer to them. The reflog is the record of everywhere HEAD has been, and it turns most &amp;quot;I destroyed everything&amp;quot; moments into a two-minute fix.&lt;/p&gt;
&lt;h2&gt;Find where you were&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reflog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every line is a position HEAD held, newest first: checkouts, commits, resets, rebases. Find the entry just before the mistake — its message usually names the operation that went wrong.&lt;/p&gt;
&lt;h2&gt;Go back there&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --hard HEAD@{2}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, safer, put a branch on the good position first and look at it before resetting anything:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch rescue HEAD@{2}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The classic recoveries&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git reset --hard&lt;/code&gt; one commit too far: the &amp;quot;lost&amp;quot; commit is in the reflog, reset back to it.&lt;/li&gt;
&lt;li&gt;Deleted branch you still needed: &lt;code&gt;git reflog&lt;/code&gt;, find the last commit the branch pointed at, &lt;code&gt;git branch the-name &amp;lt;hash&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rebase gone wrong: the reflog holds the branch&amp;#39;s pre-rebase position as &lt;code&gt;HEAD@{n}&lt;/code&gt; from before the rebase started.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The limits&lt;/h2&gt;
&lt;p&gt;The reflog is local and expires (90 days by default for reachable entries). It saves you from your own commands, not from a disk dying — that is what the remote is for. And uncommitted work that was never staged is genuinely gone; the reflog only tracks commits.&lt;/p&gt;
&lt;p&gt;When in doubt, &lt;code&gt;git branch rescue&lt;/code&gt; first. A branch you delete after inspecting costs nothing.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Trunk-Based Development on a Small Laravel Team</title><link>https://laravelgit.com/blog/trunk-based-development-for-small-laravel-teams/</link><guid isPermaLink="true">https://laravelgit.com/blog/trunk-based-development-for-small-laravel-teams/</guid><description>Short-lived branches into main beat long feature branches: smaller merges, a main that always runs, and deploys that stay boring.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Long-lived feature branches feel safe and bill you later: the two-week branch that takes a day to merge, and the merge nobody wants to re-run. Trunk-based development is the alternative that works well for small teams.&lt;/p&gt;
&lt;h2&gt;The rules, sized for a small team&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Everything targets &lt;code&gt;main&lt;/code&gt;. Branches live hours to a couple of days, never weeks.&lt;/li&gt;
&lt;li&gt;Main always runs: tests pass, migrations apply, the app boots.&lt;/li&gt;
&lt;li&gt;Review is quick and small, because the changes are small.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Half-done work still merges&lt;/h2&gt;
&lt;p&gt;The objection is always &amp;quot;the feature is not finished&amp;quot;. Merge it anyway, behind a flag. In Laravel that is a config entry or a &lt;code&gt;Feature::active()&lt;/code&gt; check, not a branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;if (config(&amp;#39;features.new_billing&amp;#39;)) {
    // new flow
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Delete the flag when the feature ships. A flag you forget is cheaper than a branch you rebase for a month.&lt;/p&gt;
&lt;h2&gt;What it buys&lt;/h2&gt;
&lt;p&gt;Conflicts stay small because diffs stay small. Main is deployable any day, so the git-push deploy from a tag or from main stops being an event. And code review gets read, because there is finally little enough of it.&lt;/p&gt;
&lt;p&gt;Start with the rule that matters most: nothing lives on a branch longer than two days.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Reviews</category></item><item><title>What Not to Commit in a Laravel Project</title><link>https://laravelgit.com/blog/what-not-to-commit-in-a-laravel-project/</link><guid isPermaLink="true">https://laravelgit.com/blog/what-not-to-commit-in-a-laravel-project/</guid><description>vendor, .env, caches and build output: the short list of what stays out of the repo, and the .gitignore that keeps it out.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A Laravel repo should hold the recipe, not the kitchen. The generated and environment-specific parts belong elsewhere.&lt;/p&gt;
&lt;h2&gt;The .gitignore that covers it&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-gitignore&quot;&gt;/vendor
/node_modules
.env
.env.backup
.phpunit.result.cache
/public/build
/public/hot
/storage/*.key
/storage/logs/*.log
Homestead.json
Homestead.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laravel&amp;#39;s default &lt;code&gt;.gitignore&lt;/code&gt; ships most of this. The ones people add by hand are the environment files and the editor/OS noise (&lt;code&gt;.idea&lt;/code&gt;, &lt;code&gt;.DS_Store&lt;/code&gt; — those belong in your global gitignore, not the project&amp;#39;s).&lt;/p&gt;
&lt;h2&gt;Why each one stays out&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vendor/&lt;/code&gt; and &lt;code&gt;node_modules/&lt;/code&gt; are rebuilt by &lt;code&gt;composer install&lt;/code&gt; and &lt;code&gt;npm ci&lt;/code&gt; from the lock files — which you do commit.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; is per-machine by definition and carries secrets. Commit &lt;code&gt;.env.example&lt;/code&gt; with the keys and empty values instead.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public/build&lt;/code&gt; is Vite output; it gets rebuilt on every deploy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;storage/*.key&lt;/code&gt; and logs are runtime state, different on every server.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Already committed something?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git rm --cached -r vendor&lt;/code&gt; stops tracking it without deleting your local copy, then commit the removal. If the file carried a secret, that is not enough — the history still has it, and the rotate-first routine applies.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>What to Do in a Detached HEAD</title><link>https://laravelgit.com/blog/what-to-do-in-a-detached-head/</link><guid isPermaLink="true">https://laravelgit.com/blog/what-to-do-in-a-detached-head/</guid><description>Detached HEAD sounds like an injury. It is just HEAD pointing at a commit instead of a branch - safe to look around, one command to keep any work.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;quot;You are in &amp;#39;detached HEAD&amp;#39; state&amp;quot; reads like a warning that something broke. Nothing broke. Git is saying your HEAD - the pointer that normally sits on a branch - is pointing directly at a commit. Everything works; the only real question is what happens to commits you make here.&lt;/p&gt;
&lt;h2&gt;How you got here&lt;/h2&gt;
&lt;p&gt;The usual doors in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout 3357a04      # inspect an old commit
git checkout v1.4.0       # look at exactly what a tag shipped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bisect and rebase also park you here on purpose. Checking out a tag to reproduce a bug against the exact deployed code is a completely ordinary reason to be detached.&lt;/p&gt;
&lt;h2&gt;Looking is free, committing is fine, leaving is the catch&lt;/h2&gt;
&lt;p&gt;Read files, run the test suite, boot the app - the working tree is real. You can even commit. The catch: no branch points at those new commits, so when you switch away they become unreferenced, and the only thing that will ever find them again is the reflog.&lt;/p&gt;
&lt;p&gt;So the rule is simple. The moment you make a commit you want to keep, give it a branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git switch -c fix/reproduce-invoice-bug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole fix. HEAD now points at a normal branch holding your work, and the detached state is over.&lt;/p&gt;
&lt;h2&gt;If you already switched away&lt;/h2&gt;
&lt;p&gt;The commits are not gone; they are unreferenced. Find the hash you were on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reflog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recent checkouts are right at the top. Put a branch on the hash:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git branch rescue 3357a04
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reflog keeps these entries for 90 days by default, so &amp;quot;I detached, committed, and switched away an hour ago&amp;quot; is a two-command recovery, not a lost afternoon.&lt;/p&gt;
&lt;h2&gt;The habit&lt;/h2&gt;
&lt;p&gt;Treat detached HEAD as a reading room, not a workspace. Look all you want; the instant you write something worth keeping, &lt;code&gt;git switch -c&lt;/code&gt; before you do anything else - especially before switching branches, which is the moment the work becomes findable only through the reflog.&lt;/p&gt;
&lt;p&gt;If you find yourself detaching often just to run someone else&amp;#39;s PR without disturbing your working tree, that is the job worktrees do without the detachment at all.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>When to Reset, Restore or Revert</title><link>https://laravelgit.com/blog/when-to-reset-restore-or-revert/</link><guid isPermaLink="true">https://laravelgit.com/blog/when-to-reset-restore-or-revert/</guid><description>Three commands that all undo something in Git. The right one depends on what state you are undoing and whether anyone else has seen it.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Git has three undo commands and they are not interchangeable. &lt;code&gt;restore&lt;/code&gt; touches uncommitted work, &lt;code&gt;reset&lt;/code&gt; moves your own branch, and &lt;code&gt;revert&lt;/code&gt; undoes a commit that other people may already have. Picking by what you are undoing - and who has seen it - keeps undo boring.&lt;/p&gt;
&lt;h2&gt;Restore: uncommitted work&lt;/h2&gt;
&lt;p&gt;You edited a file and the change is wrong. It is not staged, not committed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git restore app/Models/Invoice.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Staged it by accident but still want to edit? Unstage without losing the edit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git restore --staged app/Models/Invoice.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restore only reaches work that exists in some committed form. An edit to a file that was never committed anywhere is gone for good once restored away.&lt;/p&gt;
&lt;h2&gt;Reset: commits nobody else has&lt;/h2&gt;
&lt;p&gt;You committed on your branch, have not pushed, and want those commits back as working changes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --soft HEAD~1    # undo the commit, keep everything staged
git reset HEAD~1           # undo the commit, keep the changes unstaged
git reset --hard HEAD~1    # undo the commit and throw the changes away
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reset rewrites the branch, so it belongs to history only you have seen. If a commit is already on the shared remote, reset is how you end up force-pushing over a teammate&amp;#39;s work.&lt;/p&gt;
&lt;h2&gt;Revert: commits everyone has&lt;/h2&gt;
&lt;p&gt;A commit is on main, the deploy from it broke invoices, and the team has pulled it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git revert 3e39e4f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Revert adds a new commit that applies the inverse diff. History keeps growing forward, nobody re-pulls a rewritten branch, and &lt;code&gt;git revert&lt;/code&gt; of the revert brings the change back when the fix is ready. On a Laravel app this pairs cleanly with rollback: revert the commit, run &lt;code&gt;php artisan migrate:rollback&lt;/code&gt; if it carried a migration, deploy again.&lt;/p&gt;
&lt;h2&gt;The rule of thumb&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Not committed: &lt;strong&gt;restore&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Committed, not pushed: &lt;strong&gt;reset&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pushed: &lt;strong&gt;revert&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the mistake is bigger than one commit - a wrong rebase, a reset too far - the reflog remembers where the branch was before any of it. And when you are about to undo something you are not sure about, &lt;code&gt;git branch rescue&lt;/code&gt; first: a branch you delete after inspecting costs nothing.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Why Git Blame Is Not About Blame</title><link>https://laravelgit.com/blog/why-git-blame-is-not-about-blame/</link><guid isPermaLink="true">https://laravelgit.com/blog/why-git-blame-is-not-about-blame/</guid><description>Blame answers &apos;why does this line exist&apos;, not &apos;who do I talk to about breaking it&apos;. Used well it is the fastest route from a strange line to the decision behind it.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The name is the worst thing about &lt;code&gt;git blame&lt;/code&gt;. The command does not exist to find someone to fault; it exists to answer the question every developer asks twenty times a week: why is this line here?&lt;/p&gt;
&lt;h2&gt;From line to decision&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git blame app/Models/Invoice.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every line comes back annotated with the commit that last touched it, the author and the date. The commit hash is the valuable part - follow it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git show 8c6e7ec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you have the full diff, the commit message, and usually the pull request it came from. That chain - line, commit, message, PR - is the difference between staring at a strange condition and understanding the edge case it guards.&lt;/p&gt;
&lt;p&gt;Two options earn their keep:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git blame -L 40,60 app/Models/Invoice.php   # just these lines
git blame -w config/queue.php               # ignore whitespace-only changes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-w&lt;/code&gt; matters more than it looks: without it, the last person to reindent a block &amp;quot;owns&amp;quot; every line in it.&lt;/p&gt;
&lt;h2&gt;Hide the reformat commit&lt;/h2&gt;
&lt;p&gt;Every repo has one: the day the whole codebase was reformatted, and blame started pointing every line at it. Git has a built-in answer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config blame.ignoreRevsFile .git-blame-ignore-revs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Put the reformat commit&amp;#39;s hash in &lt;code&gt;.git-blame-ignore-revs&lt;/code&gt;, commit the file, and blame skips those revisions on everyone&amp;#39;s machine. GitHub honours the same file in its blame view, so the fix works locally and in the browser.&lt;/p&gt;
&lt;h2&gt;When blame is the wrong tool&lt;/h2&gt;
&lt;p&gt;Blame shows the last commit to touch a line. To find when something appeared or disappeared entirely - a setting, a constant, a call - the pickaxe is better:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log -S &amp;#39;QUEUE_CONNECTION&amp;#39; --oneline -- config/queue.php .env.example
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The culture part&lt;/h2&gt;
&lt;p&gt;Used as a name-and-shame tool, blame makes people defensive and teaches them to avoid touching old code. Used as a documentation tool, it does the opposite: the author of a line is the person most likely to remember the constraints behind it, and &amp;quot;what were we solving here?&amp;quot; gets a useful answer where &amp;quot;who broke this?&amp;quot; gets silence.&lt;/p&gt;
&lt;p&gt;The habit to build is simple. When a line surprises you, blame it, read the commit, and ask about the decision - not the person.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Why You Should Force Push With --force-with-lease</title><link>https://laravelgit.com/blog/why-you-should-force-push-with-lease/</link><guid isPermaLink="true">https://laravelgit.com/blog/why-you-should-force-push-with-lease/</guid><description>Rewriting your own branch is normal after a rebase. --force-with-lease makes the push refuse to run if someone else touched the branch since your fetch.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you rebase, squash or amend, you will eventually need to force-push. The question is never whether force-pushing is evil - it is whether the push can silently delete someone else&amp;#39;s work. Plain &lt;code&gt;--force&lt;/code&gt; can. &lt;code&gt;--force-with-lease&lt;/code&gt; cannot.&lt;/p&gt;
&lt;h2&gt;The legitimate case&lt;/h2&gt;
&lt;p&gt;Your PR branch, reviewed once, now rebased onto current main with the fixups squashed in. The remote still holds the pre-rebase version, and the only way to update it is a force-push. That is fine: the branch is yours, and nobody should be building on it.&lt;/p&gt;
&lt;h2&gt;What --force actually says&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git push --force
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;quot;Make the remote branch point here, whatever it points at now.&amp;quot; If a teammate pushed a commit to your branch since your last fetch - a reviewer fixing a typo through the web UI is the classic way - that commit is gone from the branch, and nobody was asked.&lt;/p&gt;
&lt;h2&gt;What --force-with-lease says&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git push --force-with-lease
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;quot;Make the remote branch point here, but only if it still points where my last fetch left it.&amp;quot; The lease is your local record of the remote ref. If the remote moved - anyone pushed anything - the push is refused and nothing is overwritten. You fetch, look at what arrived, rebase onto it, and push again.&lt;/p&gt;
&lt;p&gt;For another layer, &lt;code&gt;--force-if-includes&lt;/code&gt; additionally checks that the remote&amp;#39;s current commits are already part of what you are pushing, which catches some stale-reflog corner cases the lease alone misses.&lt;/p&gt;
&lt;h2&gt;The two rules that make it boring&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Never force-push a branch other people build on. Main, release branches, long-lived feature branches: these get merges and reverts, not rewrites. Branch protection should simply forbid it there.&lt;/li&gt;
&lt;li&gt;On your own short-lived branches, always &lt;code&gt;--force-with-lease&lt;/code&gt;, never bare &lt;code&gt;--force&lt;/code&gt;. Put it in muscle memory or in an alias, because the one time you reach for plain &lt;code&gt;--force&lt;/code&gt; is the one time the remote had a surprise waiting.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the worst already happened and a force-push ate a commit, it is recoverable: the commit still exists on the machine that made it, and that machine&amp;#39;s reflog knows the hash. A &lt;code&gt;git push &amp;lt;hash&amp;gt;:refs/heads/branch-name&lt;/code&gt; from there puts it back.&lt;/p&gt;
</content:encoded><category>Tooling</category><category>Workflows</category></item><item><title>Writing a CONTRIBUTING.md People Actually Read</title><link>https://laravelgit.com/blog/writing-a-contributing-guide/</link><guid isPermaLink="true">https://laravelgit.com/blog/writing-a-contributing-guide/</guid><description>A contributing guide is not a legal document - it is the answer to &apos;how do I get this running and get a PR in.&apos; The sections that earn their place, in the order a newcomer needs them.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most CONTRIBUTING.md files fail the same way: they are written for the maintainer&amp;#39;s peace of mind, not the reader&amp;#39;s goal. A new contributor - or a teammate joining the project, which is the same reader - wants three answers: how do I run this, how do I change it, and how do I get the change accepted. Write those, in that order, and stop.&lt;/p&gt;
&lt;h2&gt;1. Setup that actually works&lt;/h2&gt;
&lt;p&gt;The first section is the one that gets read: clone, install, run. For a Laravel app that is the composer install, &lt;code&gt;.env&lt;/code&gt; setup (which our &lt;a href=&quot;/blog/managing-env-files-across-environments/&quot;&gt;env files guide&lt;/a&gt; covers properly), migrations, and the one command that proves it works - &lt;code&gt;php artisan serve&lt;/code&gt;, &lt;code&gt;npm run dev&lt;/code&gt;, the test suite.&lt;/p&gt;
&lt;p&gt;The test is brutal and simple: hand the repo to someone who has never seen it and watch. Every place they stop is a line your guide is missing. Do this once a year, because setup rots.&lt;/p&gt;
&lt;h2&gt;2. The workflow, concretely&lt;/h2&gt;
&lt;p&gt;Branch naming if you have a convention, what a pull request should look like (link your &lt;a href=&quot;/blog/pull-request-templates-that-get-filled-in/&quot;&gt;PR template&lt;/a&gt; if you have one), who reviews, and how long a review usually takes. That last one matters more than anything else in the file: &amp;quot;expect a first review within two working days&amp;quot; turns waiting from neglect into process.&lt;/p&gt;
&lt;p&gt;Keep it to what a contributor does, not how the team philosophizes. Three paragraphs is plenty.&lt;/p&gt;
&lt;h2&gt;3. The bar for merging&lt;/h2&gt;
&lt;p&gt;What CI must pass, whether commits get squashed (see &lt;a href=&quot;/blog/squash-merge-or-merge-commit/&quot;&gt;squash merge or merge commit&lt;/a&gt;), any style tooling that will reject the PR - Pint, ESLint, whatever runs. Contributors forgive strict rules; they do not forgive discovering them in a red CI run after pushing.&lt;/p&gt;
&lt;h2&gt;What to leave out&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code of conduct boilerplate in the same file.&lt;/strong&gt; Worth having, worth its own file - GitHub surfaces CODE_OF_CONDUCT.md separately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architecture essays.&lt;/strong&gt; Link the README or docs instead; a guide that takes twenty minutes to read gets zero minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rules nobody enforces.&lt;/strong&gt; Each one teaches the reader that the file is decorative, and then they skip the real rules too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A contributing guide is a promise: follow this and your work lands. Every line either keeps that promise shorter or makes it longer. Keep the short ones.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Writing a Postmortem That Actually Changes Something</title><link>https://laravelgit.com/blog/writing-a-postmortem-that-changes-things/</link><guid isPermaLink="true">https://laravelgit.com/blog/writing-a-postmortem-that-changes-things/</guid><description>Most postmortems are written, nodded at, and never read again. The difference between theater and change is three sections written honestly: timeline, contributing factors, and actions with owners.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The deploy broke billing for forty minutes. Everyone is relieved it is over, everyone is tired, and now someone has to write the postmortem. The temptation is a document that says &amp;quot;a mistake happened, we fixed it, we will be more careful.&amp;quot; That document is worse than none: it performs learning without doing any.&lt;/p&gt;
&lt;p&gt;A postmortem that changes things has three sections, and each has a rule.&lt;/p&gt;
&lt;h2&gt;Timeline: facts, in order, with timestamps&lt;/h2&gt;
&lt;p&gt;When did monitoring fire, who saw it, what did they try, when did recovery start. Written from logs and chat history, not memory - memory edits itself within hours. The timeline is where you discover the real story: the alert that fired twenty minutes before anyone looked, the rollback that took fifteen minutes because nobody remembered the command (which is why &lt;a href=&quot;/blog/rollback-or-roll-forward/&quot;&gt;rollback or roll forward&lt;/a&gt; exists as a pre-made decision, not an improvised one).&lt;/p&gt;
&lt;p&gt;Rule: no judgments in the timeline. &amp;quot;Deployed v2.3.1 at 14:02&amp;quot; is a fact. &amp;quot;Deployed carelessly&amp;quot; is a verdict, and verdicts here make people defensive before you reach the part that matters.&lt;/p&gt;
&lt;h2&gt;Contributing factors: plural, and never a person&amp;#39;s name&lt;/h2&gt;
&lt;p&gt;&amp;quot;Stefan merged the bad migration&amp;quot; is not a cause; it is a scapegoat. The contributing factors are the things that made the mistake easy and its blast radius large: migrations deploy without a dry run, the billing smoke test only runs hourly, one person holds the rollback knowledge. Any of these could bite a different person next week, which is exactly why they are the fixable layer.&lt;/p&gt;
&lt;p&gt;Rule: if a factor cannot be assigned to a system, a process or a missing safeguard, keep digging.&lt;/p&gt;
&lt;h2&gt;Actions: owned, dated, small&lt;/h2&gt;
&lt;p&gt;Every action has one name and one date. &amp;quot;Improve testing&amp;quot; is a wish; &amp;quot;Amira adds a billing smoke test to the deploy pipeline by Nov 7&amp;quot; is an action. Three real actions beat ten aspirational ones - and they go in the tracker, not just the document, because a postmortem action without a ticket is a hope.&lt;/p&gt;
&lt;p&gt;Rule: review the actions at the next incident review or team meeting. An action that slips twice gets escalated or honestly dropped; silently forgotten is the one outcome that teaches the team postmortems are theater.&lt;/p&gt;
&lt;p&gt;Write it within two days, share it where everyone can read it, and thank the people named in the timeline - they are the ones who told you the truth.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>Zero-Downtime Deploys for Laravel, Explained Simply</title><link>https://laravelgit.com/blog/zero-downtime-deploys-for-laravel/</link><guid isPermaLink="true">https://laravelgit.com/blog/zero-downtime-deploys-for-laravel/</guid><description>Releases in directories, a symlink swap, and migrations that run before code. The boring structure behind deploys nobody notices.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The naive Laravel deploy is &lt;code&gt;git pull&lt;/code&gt; on the live directory, and it has a window where the code is half-old, half-new - composer autoloads one version, the templates reference another, and a visitor in that second gets a broken page. Zero-downtime deploys remove the window with one idea: never change the directory the web server is looking at.&lt;/p&gt;
&lt;h2&gt;Releases and one symlink&lt;/h2&gt;
&lt;p&gt;The structure, used by every deploy tool worth using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/var/www/app/
  releases/
    20261020-141122/
    20261021-093015/
  current -&amp;gt; releases/20261021-093015
  shared/
    .env
    storage/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each deploy is a full checkout into a new timestamped directory. Composer install, &lt;code&gt;artisan config:cache&lt;/code&gt;, asset builds - all happen there, while &lt;code&gt;current&lt;/code&gt; still points at the previous release and the site keeps serving it. The switch is a single atomic operation: repoint the &lt;code&gt;current&lt;/code&gt; symlink. If anything is wrong, point it back - rollback takes one second because the old release directory is still intact.&lt;/p&gt;
&lt;p&gt;Two things must live outside releases: &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;storage/&lt;/code&gt;. Symlink them in from &lt;code&gt;shared/&lt;/code&gt; so every release uses the same environment and the same uploaded files.&lt;/p&gt;
&lt;h2&gt;Migrations are the hard part&lt;/h2&gt;
&lt;p&gt;Code swaps are easy; databases cannot be symlinked back. The rule that keeps deploys reversible: every migration must work with both the old and the new code. Adding a column is fine. Renaming one is not - do it as three deploys (add new, backfill and dual-write, drop old). Run &lt;code&gt;php artisan migrate --force&lt;/code&gt; before the symlink swap, so the schema is ready for the release that is about to go live.&lt;/p&gt;
&lt;h2&gt;The queue and scheduler&lt;/h2&gt;
&lt;p&gt;Laravel queues hold serialized jobs, so after the swap, restart the workers (&lt;code&gt;artisan queue:restart&lt;/code&gt;) or they keep running the old code from memory. The scheduler needs no restart - cron resolves through &lt;code&gt;current&lt;/code&gt; on each run.&lt;/p&gt;
&lt;p&gt;That is the whole shape: build beside, switch atomically, migrate compatibly, restart workers. Whether you script it yourself or adopt a tool, &lt;a href=&quot;/blog/deploy-laravel-with-git-push/&quot;&gt;Deploy Laravel With Git Push&lt;/a&gt; covers the pipeline around it, and &lt;a href=&quot;/blog/deploy-with-tags-not-branches/&quot;&gt;Deploy With Tags, Not Branches&lt;/a&gt; decides what each release directory should contain. A deploy your users never notice is not luxury - it is just structure.&lt;/p&gt;
</content:encoded><category>Workflows</category></item><item><title>A Secret Got Committed. Now What?</title><link>https://laravelgit.com/blog/a-secret-got-committed-now-what/</link><guid isPermaLink="true">https://laravelgit.com/blog/a-secret-got-committed-now-what/</guid><description>Rotate first, scrub second, prevent third. The order matters more than the tooling.</description><pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It happens: &lt;code&gt;.env&lt;/code&gt;, an API key, a database password ends up in a commit. The fix is the same every time, and the order matters.&lt;/p&gt;
&lt;h2&gt;1. Rotate the secret first&lt;/h2&gt;
&lt;p&gt;Deleting the file from git does nothing while the key still works. Assume anything committed is public, even in a private repo: clones, forks and CI caches all copy it. Revoke or rotate the credential at the provider before touching the repository.&lt;/p&gt;
&lt;h2&gt;2. Scrub the history&lt;/h2&gt;
&lt;p&gt;Once the old key is dead, remove it from history so the repo stops carrying it. &lt;code&gt;git filter-repo&lt;/code&gt; is the tool that works:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install git-filter-repo
git filter-repo --path .env --invert-paths
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This rewrites every commit, so force-push and have the team re-clone. A commit hash they still hold can resurrect the file.&lt;/p&gt;
&lt;h2&gt;3. Stop it happening again&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.gitignore&lt;/code&gt; the file and commit a &lt;code&gt;.env.example&lt;/code&gt; with the keys but no values.&lt;/li&gt;
&lt;li&gt;Laravel reads real values from the environment, so nothing secret needs to be in the repo at all.&lt;/li&gt;
&lt;li&gt;Turn on your host&amp;#39;s push protection or run a pre-commit scan so a key gets caught before it lands.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The short version&lt;/h2&gt;
&lt;p&gt;A scrubbed repo with a live key is still a breach. A rotated key with an ugly history is just housekeeping. Rotate first.&lt;/p&gt;
</content:encoded><category>Security</category><category>Workflows</category></item><item><title>Git Hooks That Earn Their Place on a Laravel Project</title><link>https://laravelgit.com/blog/git-hooks-for-laravel-projects/</link><guid isPermaLink="true">https://laravelgit.com/blog/git-hooks-for-laravel-projects/</guid><description>Three hooks that catch real problems: Pint before commit, tests before push, and a sane commit message check.</description><pubDate>Sat, 17 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hooks live in &lt;code&gt;.git/hooks&lt;/code&gt;, which Git does not version. Point &lt;code&gt;core.hooksPath&lt;/code&gt; at a folder in the repo so the whole team runs the same ones:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config core.hooksPath bin/git-hooks
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pre-commit: fix style before it lands&lt;/h2&gt;
&lt;p&gt;Run Laravel Pint against the staged PHP files only, so the check stays fast:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/sh
files=$(git diff --cached --name-only --diff-filter=ACM -- &amp;#39;*.php&amp;#39;)
[ -z &amp;quot;$files&amp;quot; ] &amp;amp;&amp;amp; exit 0
echo &amp;quot;$files&amp;quot; | xargs ./vendor/bin/pint
echo &amp;quot;$files&amp;quot; | xargs git add
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pre-push: run the tests that matter&lt;/h2&gt;
&lt;p&gt;A full suite on every commit is too slow to keep. Run it on push instead, where a failure is still cheap:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/sh
php artisan test --stop-on-failure
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;commit-msg: keep the log readable&lt;/h2&gt;
&lt;p&gt;Reject empty subjects and very short ones. Everything stricter tends to get bypassed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/sh
subject=$(head -1 &amp;quot;$1&amp;quot;)
if [ ${#subject} -lt 10 ]; then
  echo &amp;quot;Commit subject too short - say what changed and why.&amp;quot; &amp;gt;&amp;amp;2
  exit 1
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Keep them honest&lt;/h2&gt;
&lt;p&gt;A hook the team bypasses with &lt;code&gt;--no-verify&lt;/code&gt; every day is worse than none. Start with these three, keep them under a few seconds, and only add more when a real mistake slips through twice.&lt;/p&gt;
</content:encoded><category>Workflows</category><category>Tooling</category></item><item><title>Code Review Habits That Keep Main Clean</title><link>https://laravelgit.com/blog/code-review-habits/</link><guid isPermaLink="true">https://laravelgit.com/blog/code-review-habits/</guid><description>Small changes, clear descriptions and fast feedback.</description><pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Review works best when it is cheap.&lt;/p&gt;
&lt;h2&gt;Keep changes small&lt;/h2&gt;
&lt;p&gt;Small changes get read. Large ones get skimmed.&lt;/p&gt;
&lt;h2&gt;Say what and why&lt;/h2&gt;
&lt;p&gt;A short description of the change and the reason saves the reviewer a guess.&lt;/p&gt;
&lt;h2&gt;Review quickly&lt;/h2&gt;
&lt;p&gt;A review that waits a week blocks the next change too.&lt;/p&gt;
</content:encoded><category>Reviews</category><category>Workflows</category></item><item><title>Shipping a Laravel App From Git</title><link>https://laravelgit.com/blog/shipping-laravel-from-git/</link><guid isPermaLink="true">https://laravelgit.com/blog/shipping-laravel-from-git/</guid><description>Keep deploys boring: build from a tagged commit, migrate, cache.</description><pubDate>Thu, 15 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A Laravel deploy is mostly the same few steps each time.&lt;/p&gt;
&lt;h2&gt;Build from a known commit&lt;/h2&gt;
&lt;p&gt;Deploy a tag or a commit hash, not whatever is on a branch right now.&lt;/p&gt;
&lt;h2&gt;Run migrations deliberately&lt;/h2&gt;
&lt;p&gt;Run them as a step you can see, and know how to roll back.&lt;/p&gt;
&lt;h2&gt;Warm the caches&lt;/h2&gt;
&lt;p&gt;Config, route and view caches are part of the release, not an afterthought.&lt;/p&gt;
</content:encoded><category>Releases</category><category>Workflows</category></item></channel></rss>