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

Saiful AlamSaiful Alam
9 min read

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:

  1. Trigger on Push to main: I wanted the release process to kick off automatically every time I pushed new commits to the main branch.

  2. 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.

  3. 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.

  4. Create a Production Archive: The system should generate a clean, production-only .zip archive, excluding all development files and dependencies.

  5. Generate a GitHub Release: A new draft release should be created on GitHub for each new version.

  6. Automated Changelog: The release notes should automatically include a list of all the commit messages since the last release.

  7. 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 the main 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. The key is generated based on the operating system and a hash of the composer.lock file. If the composer.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. The cache: '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 our yarn.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 and yarn 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:

  1. git describe --tags --abbrev=0: This command finds the most recent tag in the Git history. If no tags are found, it defaults to v0.0.0.

  2. We then parse this tag, split it into major, minor, and patch components.

  3. We increment the patch version by one.

  4. Finally, we construct the new version string (e.g., v1.2.4) and output it as version and version_number for use in later steps. We use id: version to be able to reference these outputs later with steps.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 cds into this temporary directory and runs composer 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.

0
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.