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