Build and Push
The Build and Push Job (build-and-push) serves as Job 1 in the automated CI/CD pipeline (.github/workflows/deploy.yml). Its core function is Continuous Integration (CI)—offloading resource-heavy compilation, dependency installation, and container packaging from your local machine and homelab server onto GitHub's cloud infrastructure.
-
Execution Environment & Job Permissions
-
Cloud Runner (runs-on: ubuntu-latest): The job executes on an ephemeral, cloud-hosted Virtual Machine managed by GitHub. Running the build phase in the cloud saves CPU cycles, RAM, and disk wear on local homelab hardware.
- Job Permissions:
- This enforces the principle of least privilege:
contents: readgrants the runner read-only access to download the repository code, whilepackages: writegrants write access to publish built images to GitHub Container Registry (GHCR). - Environment Variables & Expression Syntax (${{ ... }}):
REGISTRY: ghcr.iodefines the target container registry domain.IMAGE_NAME: ${{ github.repository }}dynamically evaluates tousername/repo-name(e.g.,ritikkumar27/astro_portfolio).- The double curly braces
${{ ... }}represent GitHub Actions expression syntax, instructing the runner to evaluate variables, secrets, and context objects at runtime rather than treating them as plain text.
- Step-by-Step Workflow Execution
Step 1: Checkout Repository (actions/checkout@v4)
- Blank Slate Initialization: The
ubuntu-latestrunner provisions as a completely empty VM. - Code Download:
actions/checkout@v4pulls a full copy of your source code (includingDockerfile,.dockerignore,package.json, and thesrc/folder) into the runner's workspace. - Version Locking (@v4): Pinning the action to version
v4prevents breaking workflow changes if GitHub releases future major updates.
Step 2: Log in to GitHub Container Registry (docker/login-action@v3)
- Authentication: Authenticates the cloud runner against
ghcr.iousingdocker/login-action@v3. - Dynamic Credentials:
username: ${{ github.actor }}fetches the handle of the user who triggered the push.password: ${{ secrets.GITHUB_TOKEN }}uses an automated, short-lived token generated natively by GitHub for that specific pipeline run. This avoids hardcoding permanent Personal Access Tokens (PATs) or plain-text passwords into repository files.
Step 3: Set up Docker Buildx (docker/setup-buildx-action@v3)
- BuildKit Engine: Initializes Docker Buildx, unlocking advanced container building capabilities, parallel layer execution, and multi-stage build optimizations.
Step 4: Multi-Stage Docker Compilation inside the Runner
The runner executes the multi-stage Dockerfile:
- Dependency Layer Caching: Copies
package.jsonandpackage-lock.json(orpnpm-lock.yaml) first and runsnpm ci(orpnpm install --frozen-lockfile). This creates an isolated cache layer fornode_modules. - Source Copy & Build: Executes
COPY . .followed byRUN npm run build(orpnpm run build), invoking Astro to process templates, Tailwind CSS, DaisyUI, and content collections into static HTML/CSS/JS inside/app/dist. - Build-Time Error Protections:
- Missing Collections Crash: If a content collection directory (like
store) defined inconfig.tsor routes is missing or empty, the@astrojs/sitemapintegration crashes during static generation (Cannot read properties of undefined (reading 'reduce')). To ensure Job 1 passes,sitemap()is either removed fromastro.config.mjsor empty collection directories are created with valid files. - RSS Method Capitalization: Astro API routes like
src/pages/rss.xml.jsrequire uppercase export handlers (export async function GET) to avoid router build warnings.
- Missing Collections Crash: If a content collection directory (like
- Stage 2 Handoff: Extracts
/app/distinto a lightweightcaddy:alpine(ornginx:alpine) image, discarding Node.js and all development dependencies entirely.
Step 5: Dual-Tagging & Image Push (docker/build-push-action@v5)
- Dual-Tagging Strategy: The job tags the compiled container image with two distinct references:
:latest: Used by the downstream homelab deployment script to pull the newest release.:${{ github.sha }}: Pins the image to the exact 40-character Git commit hash, creating an immutable audit trail for version tracking and rollback capability.
- Registry Upload: Pushes both tagged layers up to
ghcr.io/ritikkumar27/astro_portfolio.
- Package Linking & GHCR Permissions Architecture
Automatic Repository Linking vs. Manual Pushes
- Automatic Linking: When Job 1 pushes an image using
${{ secrets.GITHUB_TOKEN }}, GitHub recognizes that the token originated from that specific code repository. It automatically links the container package directly to the repository dashboard sidebar. - Manual Push Disconnect: Manual pushes from a laptop terminal using a Personal Access Token (PAT) land under the personal account's global "Packages" tab as unlinked packages. To link manual pushes without GitHub Actions, developers must manually click "Connect repository" in the UI or add
LABEL org.opencontainers.image.source="https://github.com/username/repo"to theDockerfile.
Resolving the 403 Forbidden Push Error
If an image was initially built and pushed manually from a laptop, GitHub assigns package ownership strictly to the individual user profile. If a subsequent GitHub Actions pipeline attempts to overwrite that image, the build will fail at the final step with a 403 Forbidden error.
To allow the automated pipeline to overwrite or update the package:
- Navigate to GitHub Profile → Packages → astro_portfolio → Package Settings.
- Under Manage Actions Access, click Add Repository and select the repository.
- Change the access role from
Readto Write or Admin.
- Handoff to Job 2 (
deploy)
The build-and-push job acts as a strict dependency gatekeeper for the deployment phase:
The self-hosted homelab runner will never attempt to pull an image or restart container services unless Job 1 completes with a zero exit code (0), guaranteeing that broken builds never hit production.