diff --git a/.github/actions/auth-github/action.yml b/.github/actions/auth-github/action.yml
deleted file mode 100644
index 4fd8530..0000000
--- a/.github/actions/auth-github/action.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: Auth Github
-description: Authenticate with Github Container Registry
-author: "havard.bakke@pexip.com"
-
-inputs:
- github_token:
- required: true
- description: The Github token used to login to the Github Container Registry
-
-runs:
- using: "composite"
- steps:
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ inputs.github_token }}
diff --git a/.github/actions/docker-security-scan/action.yml b/.github/actions/docker-security-scan/action.yml
deleted file mode 100644
index d9c95ae..0000000
--- a/.github/actions/docker-security-scan/action.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-name: Security scan a docker image
-description: Security scans a docker image using Snyk
-author: "havard.bakke@pexip.com"
-
-inputs:
- image:
- required: true
- description: Name of docker image to scan
- dockerfile:
- required: true
- default: ./Dockerfile
- description: The docker file used when building the image
- snyk_platform:
- required: false
- default: linux/amd64
- description: Docker image platform to scan
- snyk_token:
- required: false
- description: A token used by Snyk to scan docker image for vulnerabilities
- snyk_threshold:
- required: false
- default: medium
- description: Snyk severity threshold
-runs:
- using: "composite"
- steps:
- - name: Setup gcloud SDK
- uses: google-github-actions/setup-gcloud@v2
-
- - name: Setup Snyk
- id: snyk-setup
- if: ${{ inputs.snyk_token != '' }}
- uses: snyk/actions/setup@master
-
- - name: Security scan docker image
- id: snyk
- if: ${{ inputs.snyk_token != '' }}
- shell: bash
- env:
- SNYK_TOKEN: ${{ inputs.SNYK_TOKEN }}
- run: |
- snyk container test ${{ inputs.image }} --platform=${{ inputs.snyk_platform }} --severity-threshold=${{ inputs.snyk_threshold }} --file=${{ inputs.dockerfile }}
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 912c2c9..abcd569 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,6 +11,8 @@ on:
required: false
default: false
+permissions:
+
jobs:
release:
runs-on: ubuntu-latest
@@ -21,8 +23,21 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- - uses: ./.github/actions/release
+
+ - name: Create release
+ id: create-release
+ uses: ./release-action
with:
version: ${{ inputs.version }}
pre_release: ${{ inputs.pre_release }}
github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Generate job summary
+ shell: bash
+ run: |
+ echo "## Release Created Successfully" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- **Version**: ${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Pre-release**: ${{ inputs.pre_release }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Release URL**: https://github.com/${{ github.repository }}/releases/tag/${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
diff --git a/README.md b/README.md
index 3fac47a..124700d 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,213 @@
-# shared-github-actions
-Github-actions worflows and actions accessible to all Pexip workflows
+# Pexip shared github-actions
-## Examples
+GitHub Actions workflows and actions accessible to all Pexip workflows. This repository provides reusable composite actions for common CI/CD tasks including Docker builds, security scanning, Terraform deployments, and release automation.
+
+## Table of Contents
+
+- [Available Actions](#available-actions)
+- [Quick Start](#quick-start)
+- [Prerequisites](#prerequisites)
+- [Examples](#examples)
+
+## Available Actions
+
+### Authentication
+
+- **[auth-gcp-action](auth-gcp-action)** - Authenticate with Google Cloud Platform using service account key or workload identity federation
+- **[auth-github-action](auth-github-action)** - Authenticate with GitHub Container Registry
+
+### Docker
+
+- **[docker-build-action](docker-build-action)** - Build and push Docker images with automatic tagging and metadata
+- **[docker-security-scan-action](docker-security-scan-action)** - Security scan Docker images using Snyk
+
+### Terraform
+
+- **[terraform-deploy-gcp-action](terraform-deploy-gcp-action)** - Deploy infrastructure to GCP using Terraform (init, validate, plan, apply)
+- **[terraform-deploy-openstack-action](terraform-deploy-openstack-action)** - Deploy infrastructure to OpenStack using Terraform
+
+### Release
+
+- **[release-action](release-action)** - Create GitHub releases with auto-generated notes and optional Jira integration
+
+### Security Tools
+
+- **[setup-zizmor-action](setup-zizmor-action)** - Install zizmor CLI tool for GitHub Actions security analysis
+
+## Quick Start
-The examples are located in the '/examples' folder.
+### Using Actions in Your Workflow
-## Automatically generated release notes
+Reference actions from this repository using the following pattern:
-The release workflow automatically generates release notes based on how pull requests are labeled.
-The example configuration expects the following labels to be used:
+```yaml
+uses: pexip/shared-github-actions/{action-name}@{ref}
+```
+
+### Example: Build and Push Docker Image
+
+```yaml
+steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - uses: pexip/shared-github-actions/auth-gcp-action@master
+ with:
+ repository: ${{ vars.DOCKER_REPO }}
+ service_account_key: ${{ secrets.DEPLOY_SERVICE_ACCOUNT_KEY }}
-* bug
-* change
-* feature
+ - uses: pexip/shared-github-actions/docker-build-action@master
+ with:
+ repository: ${{ vars.DOCKER_REPO }}
+ image_name: my-application
+ dockerfile: Dockerfile
+```
-This is controlled through the '.github/release.yml' configuration file
+### Example: Terraform Deployment
```yaml
-changelog:
- categories:
- - title: New
- labels:
- - '*'
- exclude:
- labels:
- - bug
- - change
- - title: Changes
- labels:
- - change
- - title: Bug Fixes
- labels:
- - bug
+steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - uses: pexip/shared-github-actions/auth-gcp-action@master
+ with:
+ repository: ${{ vars.DOCKER_REPO }}
+ service_account_key: ${{ secrets.DEPLOY_SERVICE_ACCOUNT_KEY }}
+
+ - uses: pexip/shared-github-actions/terraform-deploy-gcp-action@master
+ with:
+ directory: ./deploy
+ token: ${{ secrets.GITHUB_TOKEN }}
```
+### Example: Authenticate with Workload Identity Federation
+
+Workload Identity Federation allows GitHub Actions to authenticate to GCP without using service account keys.
+
+#### Prerequisites
+
+1. **Create a Workload Identity Pool:**
+ ```bash
+ gcloud iam workload-identity-pools create "github-pool" \
+ --project="${PROJECT_ID}" \
+ --location="global" \
+ --display-name="GitHub Actions Pool"
+ ```
+
+2. **Create a Workload Identity Provider:**
+ ```bash
+ gcloud iam workload-identity-pools providers create-oidc "github-provider" \
+ --project="${PROJECT_ID}" \
+ --location="global" \
+ --workload-identity-pool="github-pool" \
+ --display-name="GitHub provider" \
+ --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
+ --attribute-condition="assertion.repository_owner == 'pexip'" \
+ --issuer-uri="https://token.actions.githubusercontent.com"
+ ```
+
+3. **Create a Service Account:**
+ ```bash
+ gcloud iam service-accounts create "github-actions-sa" \
+ --project="${PROJECT_ID}" \
+ --display-name="GitHub Actions Service Account"
+ ```
+
+4. **Grant permissions to the Service Account:**
+ ```bash
+ # Example: Grant Artifact Registry
+ gcloud projects add-iam-policy-binding ${PROJECT_ID} \
+ --member="serviceAccount:github-actions-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
+ --role="roles/artifactregistry.writer"
+ ```
+
+5. **Allow the Workload Identity Pool to impersonate the Service Account:**
+ ```bash
+ gcloud iam service-accounts add-iam-policy-binding "github-actions-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
+ --project="${PROJECT_ID}" \
+ --role="roles/iam.workloadIdentityUser" \
+ --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.repository/pexip/REPOSITORY_NAME"
+ ```
+
+6. **Get the Workload Identity Provider resource name:**
+ ```bash
+ gcloud iam workload-identity-pools providers describe "github-provider" \
+ --project="${PROJECT_ID}" \
+ --location="global" \
+ --workload-identity-pool="github-pool" \
+ --format="value(name)"
+ ```
+ Save this value as `WORKLOAD_IDENTITY_PROVIDER` variable in your repository.
+
+#### Usage
+
+```yaml
+steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - uses: pexip/shared-github-actions/auth-gcp-action@master
+ with:
+ repository: ${{ vars.DOCKER_REPO }}
+ workload_identity_provider: ${{ vars.WORKLOAD_IDENTITY_PROVIDER }}
+ service_account: ${{ vars.SERVICE_ACCOUNT_EMAIL }}
+
+ - uses: pexip/shared-github-actions/docker-build-action@master
+ with:
+ repository: ${{ vars.DOCKER_REPO }}
+ image_name: my-application
+ dockerfile: Dockerfile
+```
+
+### Example: Create a Release
+
+```yaml
+steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - uses: pexip/shared-github-actions/release-action@master
+ with:
+ version: v1.0.0
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+```
+
+## Prerequisites
+
+### Required Secrets
+
+Configure these secrets in your repository settings:
+
+- **`DEPLOY_SERVICE_ACCOUNT_KEY`** - GCP service account JSON key for authentication and Docker registry access
+- **`SNYK_PEXIP_UNSORTED_ACCESS_TOKEN`** - Snyk API token for security scanning (if using docker-security-scan)
+- **`GITHUB_TOKEN`** - Automatically provided by GitHub Actions
+
+### Optional Secrets
+
+- **`jira_webhook`** - Jira automation webhook URL for release integration
+
+### Required Variables
+
+Configure these variables in your repository settings:
+
+- **`DOCKER_REPO`** - Docker repository URL (e.g., `europe-docker.pkg.dev/project-id/repo-name`)
+- **`DOCKER_IMAGE`** - Docker image name
+- **`DEPLOY_PROJECT_ID`** - GCP project ID for deployments
+
+### Optional Variables (for Workload Identity Federation)
+
+If using Workload Identity Federation instead of service account keys:
+
+- **`WORKLOAD_IDENTITY_PROVIDER`** - Workload identity provider resource name (e.g., `projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID`)
+- **`SERVICE_ACCOUNT_EMAIL`** - Service account email to impersonate (e.g., `my-service-account@project-id.iam.gserviceaccount.com`)
+
+## Examples
+
+Complete workflow examples are located in the [examples](examples) folder:
+
+- **[development.yml](examples/development.yml)** - Full development pipeline with Docker build, security scan, and Terraform deployment
+- **[production.yml](examples/production.yml)** - Production deployment workflow triggered on main branch pushes or version tags
+- **[release.yml](examples/release.yml)** - Release workflow with GitHub and Jira integration
+These examples demonstrate common patterns for integrating multiple actions into complete CI/CD pipelines.
diff --git a/_shared/README.md b/_shared/README.md
new file mode 100644
index 0000000..469e25c
--- /dev/null
+++ b/_shared/README.md
@@ -0,0 +1,48 @@
+# Shared Action Modules
+
+This directory contains shared JavaScript modules used by multiple composite actions to avoid code duplication.
+
+## terraform-pr-comment.js
+
+Shared module for posting Terraform plan results to pull request comments.
+
+### Features
+
+- **Single source of truth**: Eliminates code duplication across terraform-deploy-gcp and terraform-deploy-openstack
+- **Reliable plan reading**: Reads from terraform.plan.txt file instead of unreliable stdout
+- **Smart updates**: Only updates comments when content actually changes (MD5 hash comparison)
+- **Size management**: Automatically truncates large plans with warnings
+- **Security warnings**: Detects and warns about potential sensitive values
+- **Destroy warnings**: Highlights when resources will be destroyed
+- **Platform-specific branding**: Different emojis and labels for GCP vs OpenStack
+- **Error handling**: Graceful failures that don't break the workflow
+
+### Usage
+
+```javascript
+const prComment = require('${{ github.action_path }}/../_shared/terraform-pr-comment.js');
+
+await prComment.createOrUpdatePRComment({
+ github,
+ context,
+ core,
+ directory: './terraform',
+ platform: 'gcp', // or 'openstack'
+ outcomes: {
+ fmt: 'success',
+ init: 'success',
+ validate: 'success',
+ trivy: 'success',
+ plan: 'success'
+ },
+ validationOutput: 'Validation output here...'
+});
+```
+
+### Maintenance
+
+When updating this module, remember that changes will affect both:
+- `.github/actions/terraform-deploy-gcp/action.yml`
+- `.github/actions/terraform-deploy-openstack/action.yml`
+
+Test changes with both platforms before committing.
diff --git a/_shared/terraform-pr-comment.js b/_shared/terraform-pr-comment.js
new file mode 100644
index 0000000..0903c97
--- /dev/null
+++ b/_shared/terraform-pr-comment.js
@@ -0,0 +1,208 @@
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+module.exports = {
+ /**
+ * Create or update a PR comment with Terraform plan results
+ * @param {Object} params - Parameters object
+ * @param {Object} params.github - GitHub API client
+ * @param {Object} params.context - GitHub Actions context
+ * @param {Object} params.core - GitHub Actions core
+ * @param {string} params.directory - Terraform directory path
+ * @param {string} params.platform - Platform name (gcp or openstack)
+ * @param {Object} params.outcomes - Step outcomes object
+ * @param {string} params.validationOutput - Validation output text
+ */
+ async createOrUpdatePRComment({
+ github,
+ context,
+ core,
+ directory,
+ platform,
+ outcomes,
+ validationOutput
+ }) {
+ try {
+ // Read plan from file (more reliable than stdout)
+ const planPath = path.join(directory, 'terraform.plan.txt');
+ let planOutput = '';
+
+ try {
+ planOutput = fs.readFileSync(planPath, 'utf8');
+ } catch (err) {
+ planOutput = 'Plan file not found. Check Terraform Plan step for errors.';
+ }
+
+ // Extract plan summary (resources to add/change/destroy)
+ const summaryMatch = planOutput.match(/Plan: (\d+) to add, (\d+) to change, (\d+) to destroy/);
+ const planSummary = summaryMatch
+ ? {
+ add: parseInt(summaryMatch[1]),
+ change: parseInt(summaryMatch[2]),
+ destroy: parseInt(summaryMatch[3])
+ }
+ : null;
+
+ // Truncate if too large (GitHub limit is 65536 chars)
+ const MAX_LENGTH = 60000;
+ let truncated = false;
+ if (planOutput.length > MAX_LENGTH) {
+ planOutput = planOutput.substring(0, MAX_LENGTH);
+ truncated = true;
+ }
+
+ // Generate comment content
+ const output = this.generateComment({
+ platform,
+ planOutput,
+ planSummary,
+ truncated,
+ outcomes,
+ validationOutput,
+ commitSha: context.sha.substring(0, 7),
+ timestamp: new Date().toISOString()
+ });
+
+ // Update or create comment
+ await this.updateOrCreateComment({ github, context, output, platform });
+
+ } catch (error) {
+ // Don't fail the workflow if comment fails
+ console.error('Failed to post PR comment:', error);
+ core.warning(`Failed to post PR comment: ${error.message}`);
+ }
+ },
+
+ /**
+ * Generate the formatted comment body
+ * @param {Object} params - Parameters for comment generation
+ * @returns {string} Formatted markdown comment
+ */
+ generateComment({
+ platform,
+ planOutput,
+ planSummary,
+ truncated,
+ outcomes,
+ validationOutput,
+ commitSha,
+ timestamp
+ }) {
+ const identifier = ``;
+
+ // Platform-specific emoji/branding
+ const platformInfo = {
+ gcp: { emoji: 'āļø', name: 'Google Cloud Platform' },
+ openstack: { emoji: 'š', name: 'OpenStack' }
+ };
+
+ const { emoji, name } = platformInfo[platform] || { emoji: 'šļø', name: platform };
+
+ // Format plan summary
+ const summaryText = planSummary
+ ? `**${planSummary.add}** to add, **${planSummary.change}** to change, **${planSummary.destroy}** to destroy`
+ : 'No changes';
+
+ // If the plan will destroy resources, add a prominent warning to alert reviewers to potential impact
+ const destroyWarning = planSummary && planSummary.destroy > 0
+ ? `\n> ā ļø **Warning:** This plan will **destroy ${planSummary.destroy}** resource(s). Review carefully before applying.\n`
+ : '';
+
+ // Detect sensitive values
+ const hasSensitiveValues = planOutput.match(/(password|secret|token|key|private_key)/i);
+ const sensitiveWarning = hasSensitiveValues
+ ? '\n> š **Security Alert:** Plan may contain sensitive values. Review carefully before applying.\n'
+ : '';
+
+ return `${identifier}
+## ${emoji} Terraform Plan Results - ${name}
+
+**Commit:** \`${commitSha}\` | **Triggered:** ${timestamp}
+
+### Step Results
+
+| Step | Status |
+|------|--------|
+| š Format | \`${outcomes.fmt}\` |
+| āļø Initialization | \`${outcomes.init}\` |
+| š¤ Validation | \`${outcomes.validate}\` |
+| š Security Scan | \`${outcomes.trivy}\` |
+| š Plan | \`${outcomes.plan}\` |
+
+### Plan Summary
+
+${summaryText}
+
+${destroyWarning}${sensitiveWarning}
+
+Show Validation Output
+
+\`\`\`
+${validationOutput}
+\`\`\`
+
+
+
+Show Full Plan
+
+\`\`\`terraform
+${planOutput}
+\`\`\`
+
+${truncated ? '\nā ļø **Plan truncated due to size limits. View full plan in workflow logs.**\n' : ''}
+
+
+
+---
+*Terraform plan generated by [terraform-deploy-${platform}](https://github.com/pexip/shared-github-actions)*
+`;
+ },
+
+ /**
+ * Update existing comment or create new one
+ * @param {Object} params - Parameters object
+ */
+ async updateOrCreateComment({ github, context, output, platform }) {
+ const identifier = ``;
+
+ // Find existing comment by unique identifier
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+
+ const botComment = comments.find(comment =>
+ comment.body && comment.body.includes(identifier)
+ );
+
+ // Generate hash to check if update is needed
+ const contentHash = crypto.createHash('md5').update(output).digest('hex');
+
+ if (botComment) {
+ // Check if content actually changed
+ const existingHash = crypto.createHash('md5').update(botComment.body).digest('hex');
+
+ if (existingHash !== contentHash) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: output
+ });
+ console.log('Updated existing PR comment');
+ } else {
+ console.log('Comment unchanged, skipping update');
+ }
+ } else {
+ await github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: output
+ });
+ console.log('Created new PR comment');
+ }
+ }
+};
diff --git a/.github/actions/auth-gcp/action.yml b/auth-gcp-action/action.yml
similarity index 58%
rename from .github/actions/auth-gcp/action.yml
rename to auth-gcp-action/action.yml
index 1b483bb..b0f9f06 100644
--- a/.github/actions/auth-gcp/action.yml
+++ b/auth-gcp-action/action.yml
@@ -5,7 +5,7 @@ author: "havard.bakke@pexip.com"
inputs:
repository:
required: false
- description: The repository to deploy to
+ description: The GCP hosted repository to use
service_account_key:
required: false
description: The GCP service account JSON key used to authenticate towards Google
@@ -19,15 +19,27 @@ inputs:
runs:
using: "composite"
steps:
- - name: Authenticate towards Google
+ - name: Validate authentication inputs
+ shell: bash
+ run: |
+ if [ -z "${{ inputs.service_account_key }}" ] && [ -z "${{ inputs.workload_identity_provider }}" ]; then
+ echo "Error: Either service_account_key or workload_identity_provider must be provided"
+ exit 1
+ fi
+ if [ -n "${{ inputs.workload_identity_provider }}" ] && [ -z "${{ inputs.service_account }}" ]; then
+ echo "Error: service_account is required when using workload_identity_provider"
+ exit 1
+ fi
+
+ - name: Authenticate towards Google (service account key)
if: ${{ inputs.service_account_key != '' }}
- uses: google-github-actions/auth@v2
+ uses: google-github-actions/auth@v3
with:
credentials_json: ${{ inputs.service_account_key }}
- - name: Authenticate towards Google
+ - name: Authenticate towards Google (workload identity)
if: ${{ inputs.workload_identity_provider != '' }}
- uses: google-github-actions/auth@v2
+ uses: google-github-actions/auth@v3
with:
token_format: access_token
workload_identity_provider: ${{ inputs.workload_identity_provider }}
@@ -35,10 +47,9 @@ runs:
- name: Setup gcloud SDK
if: ${{ inputs.repository != '' }}
- uses: google-github-actions/setup-gcloud@v2
+ uses: google-github-actions/setup-gcloud@v3
- name: Configure Docker repository
if: ${{ inputs.repository != '' }}
- working-directory: ${{ inputs.directory }}
shell: bash
run: gcloud auth configure-docker ${{ inputs.repository }}
\ No newline at end of file
diff --git a/auth-github-action/action.yml b/auth-github-action/action.yml
new file mode 100644
index 0000000..82865d4
--- /dev/null
+++ b/auth-github-action/action.yml
@@ -0,0 +1,26 @@
+name: Authenticate to GitHub Container Registry
+description: Authenticate with GitHub Container Registry (ghcr.io) for Docker operations
+author: "havard.bakke@pexip.com"
+
+inputs:
+ github_token:
+ required: true
+ description: The GitHub token used to login to the GitHub Container Registry
+
+runs:
+ using: "composite"
+ steps:
+ - name: Validate inputs
+ shell: bash
+ run: |
+ if [ -z "${{ inputs.github_token }}" ]; then
+ echo "Error: github_token is required"
+ exit 1
+ fi
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ inputs.github_token }}
diff --git a/.github/actions/docker-build/action.yml b/docker-build-action/action.yml
similarity index 68%
rename from .github/actions/docker-build/action.yml
rename to docker-build-action/action.yml
index 1d097f9..5f81eb5 100644
--- a/.github/actions/docker-build/action.yml
+++ b/docker-build-action/action.yml
@@ -5,10 +5,10 @@ author: "havard.bakke@pexip.com"
inputs:
repository:
required: true
- description: The repository to deploy to
+ description: The Docker repository to push the image to
image_name:
required: true
- description: Name of docker image to build and publish
+ description: Name of Docker image to build and publish
context:
required: false
description: The context to use when building the image
@@ -16,17 +16,17 @@ inputs:
push_image:
required: false
description: Should the image be pushed to the repository
- default: true
+ default: "true"
tag:
required: false
- description: Optional tag to apply to the docker image
+ description: Optional tag to apply to the Docker image
dockerfile:
- required: true
+ required: false
default: ./Dockerfile
- description: The docker file to use when building the image
+ description: The Dockerfile to use when building the image
gh_token:
required: false
- description: A token used by the Dockerfile
+ description: A GitHub token used by the Dockerfile (passed as build secret)
outputs:
image:
@@ -39,8 +39,29 @@ outputs:
runs:
using: "composite"
steps:
+ - name: Validate inputs
+ shell: bash
+ run: |
+ if [ -z "${{ inputs.repository }}" ]; then
+ echo "Error: repository is required"
+ exit 1
+ fi
+ if [ -z "${{ inputs.image_name }}" ]; then
+ echo "Error: image_name is required"
+ exit 1
+ fi
+ if [ ! -f "${{ inputs.dockerfile }}" ]; then
+ echo "Error: Dockerfile not found at ${{ inputs.dockerfile }}"
+ exit 1
+ fi
+ if [ ! -d "${{ inputs.context }}" ]; then
+ echo "Error: Context directory not found at ${{ inputs.context }}"
+ exit 1
+ fi
+
- name: Setup gcloud SDK
- uses: google-github-actions/setup-gcloud@v2
+ if: contains(inputs.repository, 'pkg.dev') || contains(inputs.repository, 'gcr.io')
+ uses: google-github-actions/setup-gcloud@v3
- name: Docker metadata
id: metadata
@@ -78,7 +99,7 @@ runs:
push: ${{ inputs.push_image }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
- annotations: ${{ steps.meta.outputs.annotations }}
+ annotations: ${{ steps.metadata.outputs.annotations }}
- name: Generate job summary
shell: bash
diff --git a/docker-security-scan-action/action.yml b/docker-security-scan-action/action.yml
new file mode 100644
index 0000000..45ad10e
--- /dev/null
+++ b/docker-security-scan-action/action.yml
@@ -0,0 +1,54 @@
+name: Security scan a Docker image
+description: Security scans a Docker image using Snyk
+author: "havard.bakke@pexip.com"
+
+inputs:
+ image:
+ required: true
+ description: Name of Docker image to scan
+ dockerfile:
+ required: false
+ default: ./Dockerfile
+ description: The Dockerfile used when building the image
+ snyk_platform:
+ required: false
+ default: linux/amd64
+ description: Docker image platform to scan
+ snyk_token:
+ required: false
+ description: A token used by Snyk to scan Docker image for vulnerabilities
+ snyk_threshold:
+ required: false
+ default: medium
+ description: Snyk severity threshold (low, medium, high, critical)
+runs:
+ using: "composite"
+ steps:
+ - name: Validate inputs
+ shell: bash
+ run: |
+ if [ -z "${{ inputs.image }}" ]; then
+ echo "Error: image is required"
+ exit 1
+ fi
+ if [ -z "${{ inputs.snyk_token }}" ]; then
+ echo "Warning: snyk_token is not provided. Skipping security scan."
+ fi
+
+ - name: Setup gcloud SDK
+ if: contains(inputs.image, 'pkg.dev') || contains(inputs.image, 'gcr.io')
+ uses: google-github-actions/setup-gcloud@v3
+
+ - name: Setup Snyk
+ id: snyk-setup
+ if: ${{ inputs.snyk_token != '' }}
+ uses: snyk/actions/setup@master
+
+ - name: Security scan Docker image
+ id: snyk
+ if: ${{ inputs.snyk_token != '' }}
+ shell: bash
+ env:
+ SNYK_TOKEN: ${{ inputs.snyk_token }}
+ run: |
+ snyk container test ${{ inputs.image }} --platform=${{ inputs.snyk_platform }} --severity-threshold=${{ inputs.snyk_threshold }} --file=${{ inputs.dockerfile }}
\ No newline at end of file
diff --git a/examples/development.yml b/examples/development.yml
index 9a8580e..c5df8bb 100644
--- a/examples/development.yml
+++ b/examples/development.yml
@@ -21,18 +21,17 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- - uses: pexip/shared-github-actions/.github/actions/auth-gcp@master
+ - uses: pexip/shared-github-actions/auth-gcp-action@master
id: auth-gcp
with:
repository: ${{ vars.DOCKER_REPO }}
service_account_key: ${{ secrets.DEPLOY_SERVICE_ACCOUNT_KEY }}
- - uses: pexip/shared-github-actions/.github/actions/docker-build@master
+ - uses: pexip/shared-github-actions/docker-build-action@master
id: build
with:
repository: ${{ vars.DOCKER_REPO }}
image_name: ${{ vars.DOCKER_IMAGE }}
dockerfile: Dockerfile.debian
- service_account_key: ${{ secrets.DEPLOY_SERVICE_ACCOUNT_KEY }}
docker-security-scan:
needs: docker-build
@@ -41,17 +40,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- - uses: pexip/shared-github-actions/.github/actions/auth-gcp@master
+ - uses: pexip/shared-github-actions/auth-gcp-action@master
id: auth-gcp
with:
repository: ${{ vars.DOCKER_REPO }}
service_account_key: ${{ secrets.DEPLOY_SERVICE_ACCOUNT_KEY }}
- - uses: pexip/shared-github-actions/.github/actions/docker-security-scan@master
+ - uses: pexip/shared-github-actions/docker-security-scan-action@master
with:
image: ${{ needs.docker-build.outputs.image }}
dockerfile: ${{ needs.docker-build.outputs.dockerfile }}
snyk_token: ${{ secrets.SNYK_PEXIP_UNSORTED_ACCESS_TOKEN }}
- service_account_key: ${{ secrets.DEPLOY_SERVICE_ACCOUNT_KEY }}
terraform-deploy:
needs: docker-build
@@ -65,14 +63,12 @@ jobs:
run: |
echo "TF_VAR_project_id=${{ vars.DEPLOY_PROJECT_ID }}" >> $GITHUB_ENV
echo "TF_VAR_container=${{ needs.docker-build.outputs.tags }}" >> $GITHUB_ENV
- - uses: pexip/shared-github-actions/.github/actions/auth-gcp@master
+ - uses: pexip/shared-github-actions/auth-gcp-action@master
id: auth-gcp
with:
repository: ${{ vars.DOCKER_REPO }}
service_account_key: ${{ secrets.DEPLOY_SERVICE_ACCOUNT_KEY }}
- - uses: pexip/shared-github-actions/.github/actions/terraform-deploy-gcp@master
+ - uses: pexip/shared-github-actions/terraform-deploy-gcp-action@master
with:
directory: ./deploy
- repository: ${{ vars.DOCKER_REPO }}
- service_account_key: ${{ secrets.DEPLOY_SERVICE_ACCOUNT_KEY }}
token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/examples/production.yml b/examples/production.yml
index 130a772..004a98c 100644
--- a/examples/production.yml
+++ b/examples/production.yml
@@ -24,10 +24,11 @@ jobs:
shell: bash
run: |
echo "TF_VAR_project_id=${{ vars.DEPLOY_PROJECT_ID }}" >> $GITHUB_ENV
- echo "TF_VAR_container=${{ needs.docker-build.outputs.tags }}" >> $GITHUB_ENV
- - uses: pexip/shared-github-actions/.github/actions/terraform-deploy-gcp@master
+ - uses: pexip/shared-github-actions/auth-gcp-action@master
with:
- directory: ./deploy
repository: ${{ vars.DOCKER_REPO }}
service_account_key: ${{ secrets.DEPLOY_SERVICE_ACCOUNT_KEY }}
+ - uses: pexip/shared-github-actions/terraform-deploy-gcp-action@master
+ with:
+ directory: ./deploy
token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/examples/release.yml b/examples/release.yml
index 6137029..5a8b285 100644
--- a/examples/release.yml
+++ b/examples/release.yml
@@ -15,10 +15,11 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- - uses: pexip/shared-github-actions/.github/actions/release@master
+ - uses: pexip/shared-github-actions/release-action@master
with:
- version: ${{ inputs.VERSION }}
+ version: ${{ inputs.version }}
github_token: ${{ secrets.GITHUB_TOKEN }}
- jira_webhook: ${{ secrets.jira_webhook }}
- jira_artifact_name: freeswitch-cvp
- jira_project_key: VJ
+ # Optional: Jira integration (uncomment and configure if needed)
+ # jira_webhook: ${{ secrets.JIRA_WEBHOOK }}
+ # jira_artifact_name: my-component-name
+ # jira_project_key: PROJECT
diff --git a/.github/actions/release/action.yml b/release-action/action.yml
similarity index 53%
rename from .github/actions/release/action.yml
rename to release-action/action.yml
index dc94b86..ba346a2 100644
--- a/.github/actions/release/action.yml
+++ b/release-action/action.yml
@@ -1,35 +1,54 @@
name: Create a release
-description: Creates a release on Github and tags the code
+description: Creates a release on GitHub and tags the code
author: "havard.bakke@pexip.com"
inputs:
version:
required: true
- description: Release version
+ description: Release version (e.g., v1.0.0)
pre_release:
- type: boolean
- default: false
- description: Pre-release?
+ required: false
+ default: "false"
+ description: Mark as pre-release (true or false)
jira_webhook:
required: false
description: The Jira webhook URL
jira_artifact_name:
required: false
- description: The name of the component to release
+ description: The name of the component to release in Jira
jira_project_key:
required: false
- description: The jira project key (given issues in project named TEST-123, TEST is the key)
+ description: The Jira project key (given issues in project named TEST-123, TEST is the key)
github_token:
- required: false
- description: The secrets.GITHUB_TOKEN
+ required: true
+ description: The GitHub token (use secrets.GITHUB_TOKEN)
runs:
using: "composite"
steps:
+ - name: Validate inputs
+ shell: bash
+ run: |
+ if [ -z "${{ inputs.version }}" ]; then
+ echo "Error: version is required"
+ exit 1
+ fi
+ if [ -z "${{ inputs.github_token }}" ]; then
+ echo "Error: github_token is required"
+ exit 1
+ fi
+ # Validate Jira inputs as a group
+ if [ -n "${{ inputs.jira_project_key }}" ]; then
+ if [ -z "${{ inputs.jira_webhook }}" ] || [ -z "${{ inputs.jira_artifact_name }}" ]; then
+ echo "Error: When jira_project_key is provided, jira_webhook and jira_artifact_name are also required"
+ exit 1
+ fi
+ fi
+
- name: Create release
uses: actions/github-script@v7
with:
- github-token: ${{ inputs.GITHUB_TOKEN }}
+ github-token: ${{ inputs.github_token }}
script: |
try {
const version = '${{ inputs.version }}'
@@ -49,8 +68,8 @@ runs:
core.setFailed(error.message);
}
- name: Create Jira release
- if: inputs.jira_project_key != null
- uses: GeoWerkstatt/create-jira-release@v1
+ if: inputs.jira_project_key != ''
+ uses: GeoWerkstatt/create-jira-release@66a6a3a84ddaf7349c47ee0aa20f252b80420146 #v1.0.9
with:
jira-project-key: ${{ inputs.jira_project_key }}
jira-automation-webhook: ${{ inputs.jira_webhook }}
diff --git a/.github/actions/terraform-deploy-openstack/action.yml b/terraform-deploy-gcp-action/action.yml
similarity index 56%
rename from .github/actions/terraform-deploy-openstack/action.yml
rename to terraform-deploy-gcp-action/action.yml
index 49591cc..c54651b 100644
--- a/.github/actions/terraform-deploy-openstack/action.yml
+++ b/terraform-deploy-gcp-action/action.yml
@@ -1,27 +1,41 @@
name: Terraform deploy
-description: Deploys resources to OpenStack using terraform
+description: Deploys resources to GCP using Terraform
author: "havard.bakke@pexip.com"
inputs:
deploy_on_pull:
required: false
description: Should pull requests be deployed
- default: false
+ default: "false"
continue_on_security_warnings:
required: false
description: Should the pipeline block on security warnings
- default: false
+ default: "false"
directory:
- required: true
+ required: false
default: ./deploy
- description: The directory within the repo containing the terraform code
+ description: The directory within the repo containing the Terraform code
token:
required: false
- description: The secrets.GITHUB_TOKEN
+ description: The GitHub token (secrets.GITHUB_TOKEN) - required for PR comments
runs:
using: "composite"
steps:
+ - name: Validate inputs
+ shell: bash
+ run: |
+ if [ ! -d "${{ inputs.directory }}" ]; then
+ echo "Error: Terraform directory not found at ${{ inputs.directory }}"
+ exit 1
+ fi
+ if [ "${{ github.event_name }}" == "pull_request" ] && [ -z "${{ inputs.token }}" ]; then
+ echo "Warning: token is not provided. PR comments will be skipped."
+ fi
+
+ - name: Setup gcloud SDK
+ uses: google-github-actions/setup-gcloud@v3
+
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
@@ -47,7 +61,7 @@ runs:
- name: Run Trivy vulnerability scanner
id: trivy
- uses: aquasecurity/trivy-action@0.29.0
+ uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 #0.33.1
with:
scan-ref: ${{ inputs.directory }}
scan-type: 'fs'
@@ -71,61 +85,31 @@ runs:
- name: Pull Request Comment
uses: actions/github-script@v7
- if: github.event_name == 'pull_request'
- env:
- PLAN: "${{ steps.plan.outputs.stdout }}"
+ if: github.event_name == 'pull_request' && inputs.token != ''
with:
github-token: ${{ inputs.token }}
script: |
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- })
- const botComment = comments.find(comment => {
- return comment.user.type === 'Bot' && comment.body.includes('Terraform Format and Style')
- })
-
- const output = `#### Terraform Format and Style š \`${{ steps.fmt.outcome }}\`
- #### Terraform Initialization āļø \`${{ steps.init.outcome }}\`
- #### Terraform Validation š¤ \`${{ steps.validate.outcome }}\`
- Validation Output
-
- \`\`\`\n
- ${{ steps.validate.outputs.stdout }}
- \`\`\`
-
-
-
- #### Terraform Plan š \`${{ steps.plan.outcome }}\`
-
- Show Plan
-
- \`\`\`terraform\n
- ${process.env.PLAN}
- \`\`\`
-
- `;
-
- if (botComment) {
- github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: botComment.id,
- body: output
- })
- } else {
- github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: output
- })
- }
+ const prComment = require('${{ github.workspace }}/_shared/terraform-pr-comment.js');
+
+ await prComment.createOrUpdatePRComment({
+ github,
+ context,
+ core,
+ directory: '${{ inputs.directory }}',
+ platform: 'gcp',
+ outcomes: {
+ fmt: '${{ steps.fmt.outcome }}',
+ init: '${{ steps.init.outcome }}',
+ validate: '${{ steps.validate.outcome }}',
+ trivy: '${{ steps.trivy.outcome }}',
+ plan: '${{ steps.plan.outcome }}'
+ },
+ validationOutput: `${{ steps.validate.outputs.stdout }}`
+ });
- name: Terraform Status
id: status
- if: steps.plan.outcome == 'failure' || steps.validate.outcome == 'failure' || steps.init.outcome == 'failure' || steps.fmt.outcome == 'failure'
+ if: steps.plan.outcome == 'failure' || steps.validate.outcome == 'failure' || steps.init.outcome == 'failure' || steps.fmt.outcome == 'failure' || (steps.trivy.outcome == 'failure' && inputs.continue_on_security_warnings != 'true')
working-directory: ${{ inputs.directory }}
shell: bash
run: |
@@ -133,11 +117,12 @@ runs:
echo Terraform Validate: ${{ steps.validate.outcome }}
echo Terraform Init: ${{ steps.init.outcome }}
echo Terraform Fmt: ${{ steps.fmt.outcome }}
+ echo Trivy Scan: ${{ steps.trivy.outcome }}
exit 1
- name: Terraform Apply
id: apply
- if: ((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && github.event_name == 'push') || (inputs.deploy_on_pull == 'true' && github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch')
+ if: ((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && github.event_name == 'push') || (inputs.deploy_on_pull == 'true' && github.event_name == 'pull_request') || github.event_name == 'workflow_dispatch'
working-directory: ${{ inputs.directory }}
shell: bash
run: terraform apply -auto-approve terraform.plan
diff --git a/.github/actions/terraform-deploy-gcp/action.yml b/terraform-deploy-openstack-action/action.yml
similarity index 55%
rename from .github/actions/terraform-deploy-gcp/action.yml
rename to terraform-deploy-openstack-action/action.yml
index dc5cd5d..8b43e1c 100644
--- a/.github/actions/terraform-deploy-gcp/action.yml
+++ b/terraform-deploy-openstack-action/action.yml
@@ -1,29 +1,37 @@
name: Terraform deploy
-description: Deploys resources to GCP using terraform
+description: Deploys resources to OpenStack using Terraform
author: "havard.bakke@pexip.com"
inputs:
deploy_on_pull:
required: false
description: Should pull requests be deployed
- default: false
+ default: "false"
continue_on_security_warnings:
required: false
description: Should the pipeline block on security warnings
- default: false
+ default: "false"
directory:
- required: true
+ required: false
default: ./deploy
- description: The directory within the repo containing the terraform code
+ description: The directory within the repo containing the Terraform code
token:
required: false
- description: The secrets.GITHUB_TOKEN
+ description: The GitHub token (secrets.GITHUB_TOKEN) - required for PR comments
runs:
using: "composite"
steps:
- - name: Setup gcloud SDK
- uses: google-github-actions/setup-gcloud@v2
+ - name: Validate inputs
+ shell: bash
+ run: |
+ if [ ! -d "${{ inputs.directory }}" ]; then
+ echo "Error: Terraform directory not found at ${{ inputs.directory }}"
+ exit 1
+ fi
+ if [ "${{ github.event_name }}" == "pull_request" ] && [ -z "${{ inputs.token }}" ]; then
+ echo "Warning: token is not provided. PR comments will be skipped."
+ fi
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
@@ -50,7 +58,7 @@ runs:
- name: Run Trivy vulnerability scanner
id: trivy
- uses: aquasecurity/trivy-action@0.29.0
+ uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 #0.33.1
with:
scan-ref: ${{ inputs.directory }}
scan-type: 'fs'
@@ -74,61 +82,31 @@ runs:
- name: Pull Request Comment
uses: actions/github-script@v7
- if: github.event_name == 'pull_request'
- env:
- PLAN: "${{ steps.plan.outputs.stdout }}"
+ if: github.event_name == 'pull_request' && inputs.token != ''
with:
github-token: ${{ inputs.token }}
script: |
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- })
- const botComment = comments.find(comment => {
- return comment.user.type === 'Bot' && comment.body.includes('Terraform Format and Style')
- })
-
- const output = `#### Terraform Format and Style š \`${{ steps.fmt.outcome }}\`
- #### Terraform Initialization āļø \`${{ steps.init.outcome }}\`
- #### Terraform Validation š¤ \`${{ steps.validate.outcome }}\`
- Validation Output
-
- \`\`\`\n
- ${{ steps.validate.outputs.stdout }}
- \`\`\`
-
-
-
- #### Terraform Plan š \`${{ steps.plan.outcome }}\`
-
- Show Plan
-
- \`\`\`terraform\n
- ${process.env.PLAN}
- \`\`\`
-
- `;
-
- if (botComment) {
- github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: botComment.id,
- body: output
- })
- } else {
- github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: output
- })
- }
+ const prComment = require('${{ github.workspace }}/_shared/terraform-pr-comment.js');
+
+ await prComment.createOrUpdatePRComment({
+ github,
+ context,
+ core,
+ directory: '${{ inputs.directory }}',
+ platform: 'openstack',
+ outcomes: {
+ fmt: '${{ steps.fmt.outcome }}',
+ init: '${{ steps.init.outcome }}',
+ validate: '${{ steps.validate.outcome }}',
+ trivy: '${{ steps.trivy.outcome }}',
+ plan: '${{ steps.plan.outcome }}'
+ },
+ validationOutput: `${{ steps.validate.outputs.stdout }}`
+ });
- name: Terraform Status
id: status
- if: steps.plan.outcome == 'failure' || steps.validate.outcome == 'failure' || steps.init.outcome == 'failure' || steps.fmt.outcome == 'failure'
+ if: steps.plan.outcome == 'failure' || steps.validate.outcome == 'failure' || steps.init.outcome == 'failure' || steps.fmt.outcome == 'failure' || (steps.trivy.outcome == 'failure' && inputs.continue_on_security_warnings != 'true')
working-directory: ${{ inputs.directory }}
shell: bash
run: |
@@ -136,11 +114,12 @@ runs:
echo Terraform Validate: ${{ steps.validate.outcome }}
echo Terraform Init: ${{ steps.init.outcome }}
echo Terraform Fmt: ${{ steps.fmt.outcome }}
+ echo Trivy Scan: ${{ steps.trivy.outcome }}
exit 1
- name: Terraform Apply
id: apply
- if: ((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && github.event_name == 'push') || (inputs.deploy_on_pull == 'true' && github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch')
+ if: ((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && github.event_name == 'push') || (inputs.deploy_on_pull == 'true' && github.event_name == 'pull_request') || github.event_name == 'workflow_dispatch'
working-directory: ${{ inputs.directory }}
shell: bash
run: terraform apply -auto-approve terraform.plan