Automating Laravel & Vue.js (Inertia) Releases with GitHub Actions


As developers, we love to build things. We pour our hearts and minds into creating applications that solve real-world problems. But when it comes to shipping our creations, the process can often be a tedious, repetitive, and error-prone chore. Manual releases are a time sink, and let's be honest, they're not the most exciting part of our job.
What if I told you there's a better way? A way to make your release process seamless, consistent, and fully automated. In this article, I'll walk you through how I automated the entire release workflow for my project, "Bill Organizer," a Laravel and Vue.js application. We'll take a deep dive into the GitHub Actions workflow I created, breaking it down step-by-step, and exploring the "how," the "what," and the "why" behind it.
By the end of this article, you'll have a clear understanding of how to build your own automated release pipeline, saving you time, reducing the risk of human error, and allowing you to focus on what you do best: writing code.
The Problem with Manual Releases
Before we jump into the solution, let's talk about the pain points of manual releases. If you've ever been responsible for deploying an application, some of these might sound familiar:
Time-Consuming: The process of pulling the latest code, installing dependencies, building assets, running tests, versioning, creating a changelog, and finally deploying can take a significant amount of time.
Error-Prone: With so many steps involved, it's easy to make a mistake. Forgetting to install a dependency, building with the wrong environment variables, or making a typo in a version number can all lead to a broken release.
Inconsistent: When different team members handle releases, they might do things slightly differently. This can lead to inconsistencies in your build artifacts and release notes.
Lack of Visibility: It can be difficult to track what's in each release, especially if you're not diligent about keeping a changelog.
Automating your release process helps to solve all of these problems. It ensures that every release is built and deployed in exactly the same way, every single time.
The Goal: A Fully Automated Release Pipeline
For my "Bill Organizer" project, I had a clear set of goals for my automated release system:
Trigger on Push to
main
: I wanted the release process to kick off automatically every time I pushed new commits to themain
branch.Semantic Versioning: I wanted to automatically generate a new semantic version number (e.g.,
v1.2.3
) for each release, incrementing the patch version each time.Build for Production: The workflow should install all the necessary dependencies (both PHP and Node.js), build the frontend assets, and create a production-ready application.
Create a Production Archive: The system should generate a clean, production-only
.zip
archive, excluding all development files and dependencies.Generate a GitHub Release: A new draft release should be created on GitHub for each new version.
Automated Changelog: The release notes should automatically include a list of all the commit messages since the last release.
Upload Artifacts: The
.zip
archive should be uploaded as an artifact to the GitHub release.
With these goals in mind, I turned to GitHub Actions, a powerful and flexible CI/CD platform that's built right into GitHub.
The Workflow: A Step-by-Step Breakdown
Let's dive into the release.yml
workflow file and break down each section.
name: Release
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
Triggering the Workflow
The on
section defines when the workflow will run. In this case, it's triggered in two ways:
push: branches: [main]
: This is the primary trigger. Every time new commits are pushed to themain
branch, this workflow will be executed. This is the heart of our continuous delivery pipeline.workflow_dispatch:
: This allows me to trigger the workflow manually from the GitHub UI. This is useful for re-running a release or for testing purposes.
Permissions
The permissions
block is crucial for security. It defines the permissions that the GITHUB_TOKEN
will have for this workflow. By default, the token is read-only. We need to grant it some write permissions:
contents: write
: This is required to create a GitHub release and to push tags.packages: write
: This is necessary if you're publishing packages to the GitHub Package Registry. While not strictly used for creating a release, it's good practice to have if you plan to extend your workflow in the future.
The release
Job
Now we get to the meat of the workflow: the release
job. This job runs on ubuntu-latest
, which is a virtual machine hosted by GitHub.
1. Checking Out the Code
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
The first step is to check out the repository's code. We use the actions/checkout@v4
action for this. The with: fetch-depth: 0
parameter is very important here. By default, checkout
only fetches the latest commit. fetch-depth: 0
tells the action to fetch the entire Git history, including all tags. We need this for our semantic versioning step later on.
2. Setting Up the Environment
Next, we need to set up the build environment with all the necessary tools and dependencies.
- name: Setup PHP 8.3
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
extensions: bcmath, ctype, fileinfo, json, mbstring, openssl, pdo, tokenizer, xml, zip
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'yarn'
Setup PHP: We use the
shivammathur/setup-php@v2
action to install PHP 8.3, Composer v2, and all the required PHP extensions for our Laravel application.Cache Composer Dependencies: Caching is a key optimization technique in CI/CD pipelines. We first get the Composer cache directory, then use the
actions/cache@v4
action to cache our Composer dependencies. Thekey
is generated based on the operating system and a hash of thecomposer.lock
file. If thecomposer.lock
file hasn't changed, the cache will be restored, saving us from having to download all the dependencies again.Setup Node.js: Similarly, we use the
actions/setup-node@v4
action to install Node.js 22. Thecache: 'yarn'
option automatically handles caching for our Yarn dependencies.
3. Building the Application
With our environment set up, it's time to build the application.
- name: Install Composer dependencies
run: composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist
- name: Install Yarn dependencies
run: yarn install --frozen-lockfile
- name: Copy environment file
run: cp .env.example .env
- name: Generate application key
run: php artisan key:generate
- name: Publish Ziggy configuration
run: php artisan ziggy:generate
- name: Type check TypeScript files
run: yarn lint
- name: Build frontend assets
run: yarn build
This is a series of standard build steps for a Laravel and Vue.js application:
We install Composer dependencies with
--no-dev
to exclude development packages, and--optimize-autoloader
for better performance.We install Yarn dependencies using
--frozen-lockfile
to ensure we're using the exact versions specified in ouryarn.lock
file.We then run our Laravel-specific build commands, such as generating an application key and publishing the Ziggy configuration.
Finally, we run
yarn lint
to check for any TypeScript errors andyarn build
to compile our frontend assets for production.
4. Semantic Versioning
This is where the magic happens. This step automatically determines the next version number.
- name: Generate semantic version
id: version
run: |
# Get the latest tag
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Latest tag: $LATEST_TAG"
# Remove 'v' prefix if it exists
LATEST_VERSION=${LATEST_TAG#v}
# Split version into parts
IFS='.' read -ra VERSION_PARTS <<< "$LATEST_VERSION"
MAJOR=${VERSION_PARTS[0]:-0}
MINOR=${VERSION_PARTS[1]:-0}
PATCH=${VERSION_PARTS[2]:-0}
# Increment patch version
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
echo "New version: $NEW_VERSION"
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "version_number=${MAJOR}.${MINOR}.${NEW_PATCH}" >> $GITHUB_OUTPUT
Here's how it works:
git describe --tags --abbrev=0
: This command finds the most recent tag in the Git history. If no tags are found, it defaults tov0.0.0
.We then parse this tag, split it into major, minor, and patch components.
We increment the patch version by one.
Finally, we construct the new version string (e.g.,
v1.2.4
) and output it asversion
andversion_number
for use in later steps. We useid: version
to be able to reference these outputs later withsteps.version.outputs.version
.
5. Creating the Production Archive
Now that our application is built and we have a new version number, we need to package it up into a clean, production-ready archive.
- name: Create production archive
run: |
# ... (rsync, composer install, cleanup, and zip commands) ...
This step is quite long, but it's doing some very important things:
It creates a temporary directory.
It uses
rsync
to copy all the application files, but it excludes all development-related files and directories like.git
,node_modules
,vendor
,tests
, etc. This ensures our archive is as small and clean as possible.It then
cd
s into this temporary directory and runscomposer install --no-dev
again. This is crucial because it installs only the production dependencies inside our clean archive directory.It performs some final cleanup, creates necessary directories, sets the correct permissions, and then creates a
.zip
archive named with the new version number (e.g.,bill-organizer-1.2.4.zip
).
6. Creating the GitHub Release
This is the final and most rewarding part of the workflow.
- name: Delete existing draft release with same version (if any)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# ... (gh release list and delete commands) ...
- name: Create Draft Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# ... (gh release create command) ...
We use the official GitHub CLI (gh
) to interact with the GitHub API.
Delete Existing Draft: First, we have a neat little step that checks if there's already a draft release with the same version tag. If there is, it deletes it. This is useful if you have to re-run the workflow, as it prevents you from having multiple draft releases for the same version.
Create Draft Release: This is the main event.
We use
git log
to get all the commit messages since the last tag. This will be our automated changelog.We then use
gh release create
to create a new draft release.We pass the new version tag, the path to our
.zip
archive, and a title for the release.The
--notes
flag is where we construct our detailed release notes. We include the new version number, the auto-generated changelog, installation instructions, and build information.
7. Uploading Build Artifacts
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: bill-organizer-${{ steps.version.outputs.version_number }}
path: bill-organizer-${{ steps.version.outputs.version_number }}.zip
retention-days: 30
Finally, we use the actions/upload-artifact@v4
action to upload our .zip
archive as a build artifact. This is useful for debugging purposes and for keeping a record of all the builds.
The Result: A Seamless Release Experience
And that's it! With this workflow in place, my release process is now completely automated. Every time I push to main
, a new, versioned, and production-ready release is created, complete with a changelog and a downloadable artifact.
This has been a game-changer for my development workflow. I no longer have to worry about the tedious and error-prone process of manual releases. I can simply push my code and be confident that a new release will be built and ready to go, allowing me to focus on what I love: building great software.
If you're still doing manual releases, I highly encourage you to explore GitHub Actions and build your own automated release pipeline. It's an investment that will pay for itself many times over in saved time, reduced stress, and more consistent, reliable releases.
You can download the full
release.yml
file from here.
Subscribe to my newsletter
Read articles from Saiful Alam directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Saiful Alam
Saiful Alam
An Expert software engineer in Laravel and React. Creates robust backends and seamless user interfaces. Committed to clean code and efficient project delivery, In-demand for delivering excellent user experiences.