diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0dcd888..a0ff9b2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -47,10 +47,7 @@ } } }, - "image": "mcr.microsoft.com/devcontainers/base:debian", - "mounts": [ - "source=${localWorkspaceFolder}/snippets,target=${containerWorkspaceFolder}/.vscode,type=bind,consistency=cached" - ], + "image": "mcr.microsoft.com/devcontainers/java:21", "postCreateCommand": "chmod +x ${containerWorkspaceFolder}/.devcontainer/postCreateScript.sh && ${containerWorkspaceFolder}/.devcontainer/postCreateScript.sh", "runArgs": [ "--rm", diff --git a/.devcontainer/postCreateScript.sh b/.devcontainer/postCreateScript.sh index bbfc37d..a541f24 100755 --- a/.devcontainer/postCreateScript.sh +++ b/.devcontainer/postCreateScript.sh @@ -3,7 +3,6 @@ ########################################################################################## # File: postCreateScript.sh # Author: Vatsal Gupta -# Date: 09-Jul-2024 # Description: # This script runs automatically after the DevContainer environment has been created. @@ -30,15 +29,18 @@ ########################################################################################## # Constants ########################################################################################## -readonly SNIPPETS_DIR="snippets" -readonly VSCODE_DIR=".vscode" -readonly SNIPPET_FILES_PATTERN="*.code-snippets" - readonly ALIAS_SRC_URL="https://raw.githubusercontent.com/gvatsal60/Linux-Aliases/HEAD/install.sh" ########################################################################################## # Functions ########################################################################################## +curl_https() { + if ! command -v curl >/dev/null 2>&1; then + echo "Error: curl is not installed. Aborting." >&2 + return 1 + fi + curl -fsSL --proto '=https' "$@" +} ########################################################################################## # Main Script @@ -46,28 +48,4 @@ readonly ALIAS_SRC_URL="https://raw.githubusercontent.com/gvatsal60/Linux-Aliase # Install Linux aliases from external script using curl and execute immediately # Note: Make sure to review scripts fetched from external sources for security reasons -if command -v curl >/dev/null 2>&1; then - curl -fsSL "${ALIAS_SRC_URL}" | sh -else - echo "Error: curl is not installed. Unable to use Linux aliases" - exit 1 -fi - -# As bind mounts not supported in GitHub Codespaces -if [ -n "${CODESPACE_NAME}" ]; then - # Generate symlinks for snippet files - # Create the .vscode directory if it doesn't already exist. - mkdir -p "${VSCODE_DIR}" - # Unlink all symbolic links in the '.vscode' directory - find "${VSCODE_DIR}" -type l -name "${SNIPPET_FILES_PATTERN}" | while read -r file; do - unlink "${file}" - done - # Check if the 'snippets' directory exists - if [ -d "${SNIPPETS_DIR}" ]; then - # Find all .code-snippet files in the 'snippets' directory and create symbolic links - find "${SNIPPETS_DIR}" -type f -name "${SNIPPET_FILES_PATTERN}" | while read -r file; do - # Create a symbolic link in '.vscode' with the same base name - ln -s "$(realpath "${file}")" "${VSCODE_DIR}/$(basename "${file}")" - done - fi -fi +curl_https "${ALIAS_SRC_URL}" | sh diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml deleted file mode 100644 index 6ad3b4e..0000000 --- a/.github/FUNDING.yaml +++ /dev/null @@ -1,12 +0,0 @@ -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -polar: # Replace with a single Polar username -buy_me_a_coffee: gvatsal60 -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 3c5f948..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - -- OS: [e.g. iOS](https) -- Browser [e.g. chrome, safari](https) -- Version [e.g. 22](https) - -**Smartphone (please complete the following information):** - -- Device: [e.g. iPhone6](https) -- OS: [e.g. iOS8.1](https) -- Browser [e.g. stock browser, safari](https) -- Version [e.g. 22](https) - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md deleted file mode 100644 index b894315..0000000 --- a/.github/ISSUE_TEMPLATE/custom.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Custom issue template -about: Describe this issue template's purpose here. -title: '' -labels: '' -assignees: '' - ---- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index d3e0535..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...](https) - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 3c60bd6..ae9b2d1 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -12,3 +12,7 @@ updates: directory: "/" schedule: interval: "weekly" + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/bot-pr.yaml b/.github/workflows/bot-pr.yaml new file mode 100644 index 0000000..a3b0c43 --- /dev/null +++ b/.github/workflows/bot-pr.yaml @@ -0,0 +1,9 @@ +name: Trigger Bot Merge +on: + pull_request: +permissions: + pull-requests: write + contents: write +jobs: + call-auto-merge: + uses: gvatsal60/.github/.github/workflows/bot-pr-auto-approve-merge.yaml@08d0834f1b687dbab5353c7fb066d6b938699609 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..24ef468 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,79 @@ +name: CI +on: + push: + branches: [master] + pull_request: +permissions: + contents: read +jobs: + test: + name: Unit Tests (JDK ${{ matrix.java-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java-version: ['11', '17', '21'] + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java-version }} + distribution: 'temurin' + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ matrix.java-version }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle-${{ matrix.java-version }}- + ${{ runner.os }}-gradle- + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Run tests + run: ./gradlew test + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-jdk${{ matrix.java-version }} + path: build/reports/tests/ + - name: Upload coverage report + if: matrix.java-version == '17' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: build/reports/jacoco/ + - name: Coverage summary + if: matrix.java-version == '17' + run: | + if [ -f build/reports/jacoco/test/jacocoTestReport.csv ]; then + echo "## Coverage Summary" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + head -1 build/reports/jacoco/test/jacocoTestReport.csv >> "$GITHUB_STEP_SUMMARY" + tail -n +2 build/reports/jacoco/test/jacocoTestReport.csv | awk -F',' '{printf "%-40s %s/%s (%.1f%%)\n", $3, $8+$9, $4+$5+$6+$7+$8+$9, ($8+$9)/($4+$5+$6+$7+$8+$9)*100}' >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + fi + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Cache pre-commit hooks + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: ${{ runner.os }}-pre-commit- + - name: Install pre-commit + run: pip install pre-commit + - name: Run pre-commit hooks + run: pre-commit run --all-files diff --git a/.github/workflows/dependabot-pr.yaml b/.github/workflows/dependabot-pr.yaml deleted file mode 100644 index ce3f12f..0000000 --- a/.github/workflows/dependabot-pr.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: Dependabot Auto Approve & Merge -on: pull_request -permissions: - pull-requests: write - contents: write -jobs: - dependabot: - runs-on: ubuntu-latest - if: github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.user.type == 'Bot' - steps: - - name: Approve & Auto-Merge PR - run: | - gh pr review --approve "${PR_URL}" - gh pr merge --auto --squash "${PR_URL}" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/pre-commit-pr.yaml b/.github/workflows/pre-commit-pr.yaml deleted file mode 100644 index 0850d36..0000000 --- a/.github/workflows/pre-commit-pr.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: Pre-commit Auto Approve & Merge -on: pull_request -permissions: - pull-requests: write - contents: write -jobs: - pre-commit: - runs-on: ubuntu-latest - if: github.event.pull_request.user.login == 'pre-commit-ci[bot]' && github.event.pull_request.user.type == 'Bot' - steps: - - name: Approve & Auto-Merge PR - run: | - gh pr review --approve "${PR_URL}" - gh pr merge --auto --squash "${PR_URL}" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..c31fe10 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,90 @@ +name: Release +on: + push: + tags: + - 'v*' +permissions: + contents: write +jobs: + test: + name: Validate Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-gradle- + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Run tests + run: ./gradlew test + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: build/reports/tests/ + release: + name: Create Release + needs: test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Validate library.json version matches tag + run: | + LIB_VERSION=$(grep -o '"version": *"[^"]*"' resources/org/devops/library.json | grep -o '[0-9][^"]*') + TAG_VERSION="${{ steps.version.outputs.VERSION }}" + if [ "$LIB_VERSION" != "$TAG_VERSION" ]; then + echo "::warning::library.json version ($LIB_VERSION) does not match tag version ($TAG_VERSION)" + fi + - name: Generate changelog + id: changelog + run: | + # Get the previous tag + PREV_TAG=$(git tag --sort=-v:refname | grep '^v' | head -2 | tail -1) + if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$GITHUB_REF_NAME" ]; then + echo "CHANGELOG<> "$GITHUB_OUTPUT" + git log --pretty=format:"- %s (%h)" HEAD >> "$GITHUB_OUTPUT" + echo "" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + else + echo "CHANGELOG<> "$GITHUB_OUTPUT" + git log --pretty=format:"- %s (%h)" "${PREV_TAG}..HEAD" >> "$GITHUB_OUTPUT" + echo "" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + fi + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + body: | + ## Changes + + ${{ steps.changelog.outputs.CHANGELOG }} + + ## Usage + + ```groovy + @Library('devops-shared@${{ github.ref_name }}') _ + ``` + generate_release_notes: true + draft: false + prerelease: ${{ contains(steps.version.outputs.VERSION, '-') }} diff --git a/.gitignore b/.gitignore index c9c9b82..677b011 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ __pycache__/ # Java *.class +.gradle # Ruby .bundle @@ -113,3 +114,12 @@ lib/ dist/ bower_components/ *.min.js + +# Jenkins # +############ +jenkins-cli.jar +*.hpi +*.jpi +work/ +@tmp/ +*.groovy.xml diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index ed2ce28..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,13 +0,0 @@ -# Changelog - -All notable changes to [Project Name](https) will be documented in this file. - -## [Unreleased](#unreleased) - -## [1.0.0](#100---yyyy-mm-dd) - YYYY-MM-DD - -- Initial release - -## Older Releases - -[Older release notes can go here](#older-releases) diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 5695d26..0000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @gvatsal60 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 6adb73c..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,27 +0,0 @@ -# Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, -we as contributors and maintainers pledge to make participation -in our project and our community a harassment-free experience for everyone, -regardless of age, body size, disability, ethnicity, gender identity -and expression, level of experience, nationality, personal appearance, -race, religion, or sexual identity and orientation. - -## Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [contact email](github-actions[bot]@users.noreply.github.com). -All complaints will be reviewed and investigated promptly and fairly. - -## Attribution - -This [Code of Conduct]() -is adapted from the Contributor Covenant, version 2.0. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index e1965d2..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,28 +0,0 @@ -# Contributing to [Project Name](#contributing-to-project-name) - -Thank you for your interest in contributing to [Project Name](https)! -Please follow these guidelines to ensure a smooth process. - -## How to Contribute - -1. Fork the repository and create your branch from `master`. -2. Open an issue or feature request on GitHub or pick an existing one. -3. Work on your changes. -4. Ensure your code follows our coding standards and conventions. -5. Test your changes thoroughly. -6. Submit a pull request referencing the original issue. - -## Code Style - -[Describe your coding style guidelines here, if applicable.](#code-style) -We follow the [coding style guide](link_to_style_guide) for this project. - -## Reporting Bugs - -Please report bugs using the GitHub issue tracker. -Include steps to reproduce the bug and any relevant error messages. - -## Contact - -If you have questions or need further assistance, -feel free to contact us at [contact email](github-actions[bot]@users.noreply.github.com). diff --git a/Jenkinsfile.example b/Jenkinsfile.example new file mode 100644 index 0000000..4820b8b --- /dev/null +++ b/Jenkinsfile.example @@ -0,0 +1,114 @@ +// ============================================================================= +// Example Jenkinsfile consuming the shared library. +// +// Assumes the library is registered in Jenkins under the name "devops-shared". +// Adjust the @Library name to match your Jenkins configuration. +// ============================================================================= + +@Library('devops-shared') _ + + +def config = [ + buildTool : 'maven', + gitCredentialsId : 'git-cred', + slackChannel : '#builds', + emailRecipients : 'team@example.com', + retryAttempts : 2, + retryDelaySeconds : 10, + dockerImage : 'myapp', + dockerRegistry : 'registry.example.com', + dockerRegistryCredentialsId : 'docker-cred', +] + +pipeline { + agent any + + options { + timeout(time: 30, unit: 'MINUTES') + buildDiscarder(logRotator(numToKeepStr: '20')) + } + + stages { + stage('Checkout') { + steps { + checkoutCode( + url : 'https://github.com/org/repo.git', + credentialsId : config.gitCredentialsId, + branch : 'main', + changelog : true, + ) + } + } + + stage('Build') { + steps { + buildApp( + tool : config.buildTool, + goals : 'clean package', + extraArgs: ['-DskipTests'], + ) + } + } + + stage('Test') { + steps { + runTests( + tool : config.buildTool, + goals: 'test', + ) + } + } + + stage('Deploy (staging)') { + steps { + script { + waitForInput( + message : 'Approve staging deployment?', + submitter : 'dev-team', + timeoutMinutes: 120, + ) + deploy( + command : './scripts/deploy.sh staging', + version : "1.0.${env.BUILD_NUMBER}", + url : 'https://github.com/org/repo.git', + credentialsId : config.gitCredentialsId, + slackChannel : config.slackChannel, + emailRecipients: config.emailRecipients, + ) + } + } + } + + stage('Docker Build & Push') { + steps { + script { + dockerBuild( + image : "${config.dockerImage}", + tags : ["${env.BUILD_NUMBER}", 'latest'], + registry : config.dockerRegistry, + registryCredentialsId : config.dockerRegistryCredentialsId, + buildArgs : [BUILD_NUMBER: env.BUILD_NUMBER], + ) + dockerPush( + image : "${config.dockerImage}", + tag : env.BUILD_NUMBER, + registry : config.dockerRegistry, + registryCredentialsId : config.dockerRegistryCredentialsId, + ) + } + } + } + } + + post { + success { + notify status: 'SUCCESS', message: "Build ${env.JOB_NAME} #${env.BUILD_NUMBER} succeeded" + } + failure { + notify status: 'FAILURE', message: "Build ${env.JOB_NAME} #${env.BUILD_NUMBER} failed" + } + cleanup { + echo 'Pipeline finished; cleaning up workspace' + } + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f33fb59 --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +.PHONY: help build test clean lint check coverage jacoco ci format install-hooks verify + +GRADLE := ./gradlew --console=plain +PRE_COMMIT := pre-commit + +help: + @echo "Available targets:" + @echo " build - Compile the project" + @echo " test - Run Spock/Spock tests" + @echo " check - Run lint + test" + @echo " lint - Run pre-commit hooks" + @echo " format - Auto-fix lint issues" + @echo " coverage - Run tests with JaCoCo coverage report" + @echo " jacoco - Alias for coverage" + @echo " clean - Remove build artifacts" + @echo " install-hooks - Install pre-commit hooks" + @echo " verify - Run tests with Gradle verification" + @echo " ci - Run full CI pipeline (lint + build + test + coverage)" + @echo " all - Alias for ci" + +build: + $(GRADLE) build -x test + +test: + $(GRADLE) test + +lint: + $(PRE_COMMIT) run --all-files + +format: + $(PRE_COMMIT) run --all-files + +check: lint test + +coverage: + $(GRADLE) test jacocoTestReport + +jacoco: coverage + +clean: + $(GRADLE) clean + rm -rf build/ .gradle/ + +install-hooks: + $(PRE_COMMIT) install + +verify: + $(GRADLE) check + +ci: lint build test coverage + +all: ci diff --git a/README.md b/README.md index 3fe1dd7..9fed9e3 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,422 @@ -# 🚀 Project Template - -[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://img.shields.io/github/license/gvatsal60/project-template) -[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/gvatsal60/project-template/master.svg)](https://results.pre-commit.ci/latest/github/gvatsal60/project-template/HEAD) -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/7ae60c7a6c824bfea751067f08618c0d)](https://app.codacy.com/gh/gvatsal60/project-template/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) -[![CodeFactor](https://www.codefactor.io/repository/github/gvatsal60/project-template/badge)](https://www.codefactor.io/repository/github/gvatsal60/project-template) -![GitHub pull-requests](https://img.shields.io/github/issues-pr/gvatsal60/project-template) -![GitHub Issues](https://img.shields.io/github/issues/gvatsal60/project-template) -![GitHub forks](https://img.shields.io/github/forks/gvatsal60/project-template) -![GitHub stars](https://img.shields.io/github/stars/gvatsal60/project-template) - -Welcome to the `Project Template` repository! This serves as your starting point for new projects, -packed with the essential tools to ensure smooth development, code quality, and consistent documentation. -Inside, you'll find: - -1. 📄 README.md -2. 📝 LICENSE -3. 🚫 .gitignore -4. 🧑‍🤝‍🧑 CODE_OF_CONDUCT.md -5. ✍️ CONTRIBUTING.md -6. 💰 FUNDING.yml -7. 📜 CHANGELOG.md - -## 🛠️ Installation - -To set up your project using this template, just follow these simple steps: - -1. **Clone the repository:** - - ```sh - git clone https://github.com/gvatsal60/project-template.git - cd project-template +# 🚀 Jenkins Shared Library (`devops-shared`) + +A generic, reusable [Jenkins Shared Library](https://www.jenkins.io/doc/book/pipeline/shared-pipelines/) that +provides common pipeline abstractions so you don't have to copy-paste boilerplate +across every job. + +Built on the [project-template](https://github.com/gvatsal60/project-template) foundation and +licensed under **Apache 2.0**. + +--- + +## Table of Contents + +- [Features](#features) +- [Library Layout](#library-layout) +- [Quick Start](#quick-start) +- [Available Steps](#available-steps) + - [checkoutCode](#checkoutcode) + - [buildApp](#buildapp) + - [runTests](#runtests) + - [deploy](#deploy) + - [waitForInput](#waitforinput) + - [notify](#notify) + - [dockerBuild](#dockerbuild) + - [dockerPush](#dockerpush) + - [dockerLogin](#dockerlogin) + - [dockerLogout](#dockerlogout) + - [dockerTag](#dockertag) + - [dockerRun](#dockerrun) + - [dockerCompose](#dockercompose) + - [dockerClean](#dockerclean) + - [dockerInspect](#dockerinspect) + - [dockerLoginEcr](#dockerloginecr) + - [dockerLoginGcr](#dockerlogingcr) +- [Background Classes](#background-classes) +- [Configuration](#configuration) +- [Jenkinsfile Example](#jenkinsfile-example) +- [Development](#development) +- [License](#license) + +--- + +## Features + +- **Build-tool agnostic** — supports **Maven** and **Gradle** through a single + `buildApp` / `runTests` step. +- **Retry & timing** — all steps inherit retry and elapsed-time instrumentation + from `Utils.retry` / `Utils.time`. +- **Unified notifications** — Slack + email via the `notify` step. +- **Git helpers** — checkout with changelog, git tagging on release. +- **Docker support** — build, push, login, tag, run, compose, clean, inspect, and registry-specific logins (ECR, GCR). +- **Centralised config** — `Config` provides sane defaults with per-call overrides. +- **Reusable & typed** — strongly-typed Groovy classes under `src/org/devops/`. + +--- + +## Library Layout + +``` +jenkins-shared-library/ +├── src/ # Compiled Groovy/Java classes (importable) +│ └── org/devops/ +│ ├── Utils.groovy # logging, retry, timing +│ ├── Config.groovy # config defaults + overrides +│ ├── Notification.groovy # Slack & email +│ ├── GitHelper.groovy # checkout, tagging, changelogs +│ ├── BuildHelper.groovy # build + test orchestration +│ └── Docker.groovy # Docker build, push, login, run, compose, clean +├── vars/ # Global steps callable from any Jenkinsfile +│ ├── checkoutCode.groovy +│ ├── buildApp.groovy +│ ├── runTests.groovy +│ ├── deploy.groovy +│ ├── waitForInput.groovy +│ ├── notify.groovy +│ ├── dockerBuild.groovy +│ ├── dockerPush.groovy +│ ├── dockerLogin.groovy +│ ├── dockerLogout.groovy +│ ├── dockerTag.groovy +│ ├── dockerRun.groovy +│ ├── dockerCompose.groovy +│ ├── dockerClean.groovy +│ ├── dockerInspect.groovy +│ ├── dockerLoginEcr.groovy +│ └── dockerLoginGcr.groovy +├── resources/ # Non-Groovy resources +│ └── org/devops/library.json +├── Jenkinsfile.example # Example consumer pipeline +├── README.md +└── LICENSE +``` + +--- + +## Quick Start + +1. **Register the library in Jenkins:** + + *Manage Jenkins → Configure System → Global Pipeline Libraries* + + | Field | Value | + |------------------|--------------------------------| + | Name | `devops-shared` | + | Default version | `main` (or your release tag) | + | Source | Git / GitHub / Bitbucket URL | + | Load implicitly | *check* (optional) | + +2. **Use it in a Jenkinsfile:** + + ```groovy + @Library('devops-shared') _ + + pipeline { + agent any + stages { + stage('Checkout') { + steps { checkoutCode(url: 'https://github.com/org/repo.git') } + } + stage('Build') { steps { buildApp() } } + stage('Test') { steps { runTests() } } + } + } ``` -2. **Customize According to Your Project:** - * 🎯 Replace `[Project Name]` - * 📧 Update `[contact email]` - * 💡 Follow `[coding style guide]` +See [`Jenkinsfile.example`](Jenkinsfile.example) for a full, annotated pipeline. + +--- + +## Available Steps + +### `checkoutCode` + +Checkout source code with retry and optional changelog. + +```groovy +checkoutCode( + url : 'https://github.com/org/repo.git', + credentialsId : 'git-cred', // optional + branch : 'develop', // default 'main' + changelog : true, // default true + retryAttempts : 3, // default 1 + retryDelaySeconds : 10, // default 0 +) +``` + +### `buildApp` + +Run a build with the configured tool. + +```groovy +buildApp( + tool : 'maven', // 'maven' (default) | 'gradle' + goals : 'clean package', // maven default | 'clean build' for gradle + extraArgs: ['-DskipTests'], + retryAttempts: 2, + failOnError: true, +) +``` + +### `runTests` + +Run the test suite, then publish JUnit + coverage reports. + +```groovy +runTests( + tool : 'maven', // default + goals: 'test', + extraTestArgs: '-Dparallel=methods', +) +``` + +### `deploy` + +Execute a deploy command, tag git, and notify on completion. + +```groovy +deploy( + command : './scripts/deploy.sh staging', + version : '1.0.42', // optional — triggers git tag + url : 'https://github.com/org/repo.git', // required for tagging + credentialsId : 'git-cred', // for pushing tags + tag : true, // default true when version set + slackChannel : '#builds', + emailRecipients: 'team@example.com', +) +``` + +### `waitForInput` + +Wait for manual approval inside a timeout. + +```groovy +waitForInput( + message : 'Approve production deploy?', + ok : 'Approve', // default 'Approve' + submitter : 'dev-team', // Jenkins user/group + timeoutMinutes: 1440, // default 1440 (24h) +) +``` + +### `notify` + +Send a status notification (Slack + email). Controlled by `Config` defaults: +`notifyOnFailure` (default `true`), `notifyOnSuccess` (default `false`). + +```groovy +notify(status: 'SUCCESS', message: 'Deploy finished') + +// shorthand +notify currentBuild.result ?: 'SUCCESS', 'Build done' +``` + +### `dockerBuild` + +Build a Docker image with optional tags, build args, and registry login. + +```groovy +dockerBuild( + image : 'myapp', + tags : ['1.0.0', 'latest'], + registry : 'registry.example.com', + registryCredentialsId : 'docker-cred', + buildArgs : [JAVA_VERSION: '17'], + noCache : true, + retryAttempts : 2, +) +``` + +### `dockerPush` + +Push a Docker image tag to a registry. + +```groovy +dockerPush( + image : 'myapp', + tag : '1.0.0', + registry : 'registry.example.com', + registryCredentialsId : 'docker-cred', +) +``` + +### `dockerLogin` + +Login to a Docker registry using Jenkins credentials or plain username/password. + +```groovy +dockerLogin( + registry : 'registry.example.com', + credentialsId : 'docker-cred', +) + +// or with explicit credentials +dockerLogin( + registry : 'registry.example.com', + username : 'deployer', + password : 's3cret', +) +``` + +### `dockerLogout` + +Logout from a Docker registry. + +```groovy +dockerLogout(registry: 'registry.example.com') +dockerLogout // defaults to Docker Hub +``` + +### `dockerTag` + +Tag a Docker image with a new tag. + +```groovy +dockerTag(image: 'myapp', tag: '1.0.0') +dockerTag(image: 'myapp', tag: '1.0.0', registry: 'registry.example.com') +``` + +### `dockerRun` + +Run a Docker container with optional ports, volumes, env vars, and detach mode. + +```groovy +dockerRun( + image : 'myapp', + command : 'npm start', + detached : true, + ports : ['8080:8080'], + volumes : ['/data:/app/data'], + env : [NODE_ENV: 'production'], + rm : true, + name : 'myapp-container', +) +``` + +### `dockerCompose` + +Execute a docker-compose command. + +```groovy +dockerCompose(command: 'up', args: '-d') +dockerCompose(command: 'down', composeFile: 'docker-compose.prod.yml') +dockerCompose(command: 'build', env: [BUILD_NUMBER: env.BUILD_NUMBER]) +``` + +### `dockerClean` + +Clean up Docker resources (stopped containers, dangling images, unused volumes, networks, build cache). + +```groovy +dockerClean // remove stopped containers, dangling images, unused volumes and networks +dockerClean(buildCache: true, pruneAll: true) // aggressive cleanup +dockerClean(containers: false, images: true) // selective cleanup +``` + +### `dockerInspect` + +Inspect a Docker image or container and return the output. + +```groovy +def info = dockerInspect(target: 'myapp:latest') +def id = dockerInspect(target: 'myapp:latest', format: '{{.Id}}') +``` + +### `dockerLoginEcr` + +Login to AWS Elastic Container Registry (ECR). + +```groovy +dockerLoginEcr(region: 'us-east-1', accountId: '123456789012') +dockerLoginEcr( + region : 'us-east-1', + registry : '123456789012.dkr.ecr.us-east-1.amazonaws.com', + credentialsId : 'aws-creds', +) +``` + +### `dockerLoginGcr` + +Login to Google Container Registry (GCR). + +```groovy +dockerLoginGcr(credentialsId: 'gcr-service-account') +dockerLoginGcr(project: 'my-gcp-project') +dockerLoginGcr(registry: 'us.gcr.io', credentialsId: 'gcr-service-account') +``` + +--- + +## Background Classes + +| Class | Responsibility | +|-----------------|-------------------------------------------------------------| +| `Utils` | `info`/`warn`/`error` logging, `time`, `retry`, `get` | +| `Config` | Default map with override support; typed accessors | +| `GitHelper` | Checkout, changelog generation, git tagging | +| `BuildHelper` | Tool dispatch (Maven/Gradle), test result & coverage publish | +| `Notification` | Slack + email backends with graceful degradation | +| `Docker` | Build, push, login, tag, run, compose, clean, inspect, ECR/GCR login | + +All classes implement `Serializable` and accept the owning `Script` context so +they work correctly across pipeline restarts. + +--- + +## Configuration + +Every step merges its call-time arguments on top of `Config.DEFAULTS`. +The full set of overridable keys: + +| Key | Default | Description | +|---------------------|-------------|------------------------------------------| +| `branch` | `main` | Default git branch | +| `buildTool` | `maven` | `maven` or `gradle` | +| `failOnError` | `true` | Fail build on compile/test error | +| `failOnWarning` | `false` | Fail build on warnings | +| `notifyOnFailure` | `true` | Notify on `FAILURE` | +| `notifyOnSuccess` | `false` | Notify on `SUCCESS` | +| `slackChannel` | `#builds` | Default Slack channel | +| `emailRecipients` | `''` | Default email recipients | +| `gitCredentialsId` | `''` | Default git credentials ID | +| `timeoutMinutes` | `30` | Default pipeline timeout | +| `retryAttempts` | `1` | Default retry count for network ops | +| `retryDelaySeconds` | `0` | Default delay between retries | +| `cleanWorkspace` | `false` | Clean workspace before checkout | + +--- + +## Jenkinsfile Example + +A complete annotated example lives in [`Jenkinsfile.example`](Jenkinsfile.example). +It demonstrates checkout → build → test → manual approval → deploy with +notifications on every outcome. + +--- + +## Development + +This repository uses [pre-commit](https://pre-commit.com/) to enforce linting +across many languages. To set up locally: + +```sh +pip install pre-commit +pre-commit install +pre-commit run --all-files +``` + +The dev container (see `.devcontainer/`) provisions a ready-to-code environment. + +### Project tooling -## 💬 Contributing +| Tool | Purpose | +|-------------|----------------------------------| +| Groovy | Jenkins pipeline & library code | +| pre-commit | Multi-language linting hooks | +| markdownlint| README / doc style enforcement | -We love contributions! If you'd like to help improve this project, please review our -[Contribution Guidelines](https://github.com/gvatsal60/project-template/blob/HEAD/CONTRIBUTING.md) -before submitting pull requests. +--- -## 🛡️ License +## License -This project is licensed under the Apache License 2.0 License - -see the [LICENSE](https://github.com/gvatsal60/project-template/blob/HEAD/LICENSE) -file for details. +Apache License 2.0 — see [LICENSE](LICENSE). diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f4880cf --- /dev/null +++ b/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'groovy' + id 'jacoco' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation localGroovy() + testImplementation 'org.spockframework:spock-core:2.3-M4' + testImplementation 'org.codehaus.groovy:groovy-all:4.0.15' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.0' +} + +test { + useJUnitPlatform() + testLogging { + events 'passed', 'failed', 'skipped' + showStandardStreams = true + } + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + html.required = true + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f261cef --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..028fee4 --- /dev/null +++ b/gradlew @@ -0,0 +1,170 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which $JAVACMD >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum File descriptor limit, if available. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if $cygwin || $msys ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the Pattern for arguments to convert Unix paths to Windows paths + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygwin patterns + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /dev/null + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-" ` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a check + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS '"-Dorg.gradle.appname=$APP_BASE_NAME"' -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, user's volume is not always in current path. +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/resources/org/devops/library.json b/resources/org/devops/library.json new file mode 100644 index 0000000..c34335f --- /dev/null +++ b/resources/org/devops/library.json @@ -0,0 +1,5 @@ +{ + "description": "Generic reusable Jenkins Shared Library providing config management, build, test, checkout, deploy, notification, and Docker (build, push, login, tag, run, compose, clean, inspect, ECR/GCR login) steps.", + "name": "devops-shared-library", + "version": "1.0.0" +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e0363f5 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'devops-shared' diff --git a/snippets/bat.code-snippets b/snippets/bat.code-snippets deleted file mode 100644 index f224de4..0000000 --- a/snippets/bat.code-snippets +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @file bat.code-snippets - * @brief Contains custom code snippets for Windows Bat - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Windows Bat File Template": { - "scope": "bat", - "prefix": "template", - "body": [ - "@echo off", - "rem ##########################################################################################", - "rem File: ${TM_FILENAME}", - "rem Author: ${USER}", - "rem Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "rem Description: Brief description of the script purpose.", - "rem ##########################################################################################", - "", - "rem ##########################################################################################", - "rem License", - "rem ##########################################################################################", - "rem This script is licensed under the MIT License.", - "rem License information should be updated as necessary.", - "rem ##########################################################################################", - "", - "rem ##########################################################################################", - "rem Constants", - "rem ##########################################################################################", - "", - "rem ##########################################################################################", - "rem Functions", - "rem ##########################################################################################", - "", - "rem ##########################################################################################", - "rem Main Script", - "rem ##########################################################################################", - "", - "rem Main script logic goes here", - "" - ], - "description": "Windows bat file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/c_cpp.code-snippets b/snippets/c_cpp.code-snippets deleted file mode 100644 index cd9ac1b..0000000 --- a/snippets/c_cpp.code-snippets +++ /dev/null @@ -1,139 +0,0 @@ -/** - * @file c_cpp.code-snippets - * @brief Contains custom code snippets for C and C++ - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * Header File Template - **************************************************************************************/ - "C/C++ Header File Template": { - "scope": "c, cpp", - "prefix": "htemplate", - "body": [ - "/***************************************************************************************", - " * File: ${TM_FILENAME}", - " * Author: ${USER}", - " * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - " * Description: Brief description of the file's purpose.", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * License", - " **************************************************************************************/", - "/*", - " * This file is licensed under the MIT License.", - " * License information should be updated as necessary.", - " */", - "", - "/***************************************************************************************", - " * Includes", - " **************************************************************************************/", - "#ifndef _${TM_FILENAME_BASE/(.*)/${1:/upcase}/}_H_", - "#define _${TM_FILENAME_BASE/(.*)/${1:/upcase}/}_H_", - "", - "#include <> ${LINE_COMMENT} TODO", - "", - "/***************************************************************************************", - " * Constants", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Structure Declarations", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Class Declarations", - " **************************************************************************************/", - "", - "", - "#endif" - ], - "description": "C/C++ header file template", - "isFileTemplate": true - }, - /*************************************************************************************** - * Source File Template - **************************************************************************************/ - "C/C++ Source File Template": { - "scope": "c, cpp", - "prefix": "stemplate", - "body": [ - "/***************************************************************************************", - " * File: ${TM_FILENAME}", - " * Author: ${USER}", - " * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - " * Description: Brief description of the file's purpose.", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Includes", - " **************************************************************************************/", - "", - "#include \"${TM_FILENAME_BASE}\"", - "", - "/***************************************************************************************", - " * Global Functions Definitions", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Class Methods Definitions", - " **************************************************************************************/", - "" - ], - "description": "C/C++ source file template", - "isFileTemplate": true - }, - /*************************************************************************************** - * Debugging - **************************************************************************************/ - "Print Pointer Address": { - "scope": "c, cpp", - "prefix": "plog", - "body": [ - "printf(\"%s:%d $1: %p\\n\", \"${TM_FILENAME}\", __LINE__, $1); ${LINE_COMMENT} REMOVE" - ], - "description": "Prints pointer address to console" - }, - "Print Hex Value": { - "scope": "c, cpp", - "prefix": "hlog", - "body": [ - "printf(\"%s:%d $1: 0x%02x\\n\", \"${TM_FILENAME}\", __LINE__, $1); ${LINE_COMMENT} REMOVE" - ], - "description": "Print hex value to console" - }, - "Print String Value": { - "scope": "c, cpp", - "prefix": "slog", - "body": [ - "printf(\"%s:%d $1: %s\\n\", \"${TM_FILENAME}\", __LINE__, $1); ${LINE_COMMENT} REMOVE" - ], - "description": "Print string value to console" - }, - "Print Function Name": { - "scope": "c, cpp", - "prefix": "log0", - "body": [ - "printf(\"%s:%d %s $1\\n\", \"${TM_FILENAME}\", __LINE__, __FUNCTION__); ${LINE_COMMENT} REMOVE" - ], - "description": "Print filename with linenumber to console" - }, - "Print Integer Values": { - "scope": "c, cpp", - "prefix": "log1", - "body": [ - "printf(\"%s:%d $1: %d\\n\", \"${TM_FILENAME}\", __LINE__, $1); ${LINE_COMMENT} REMOVE" - ], - "description": "Print values to console" - } -} diff --git a/snippets/csharp.code-snippets b/snippets/csharp.code-snippets deleted file mode 100644 index 355c0b0..0000000 --- a/snippets/csharp.code-snippets +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @file csharp.code-snippets - * @brief Contains custom code snippets for C# - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * Header File Template - **************************************************************************************/ - "C# File Template": { - "scope": "csharp", - "prefix": "template", - "body": [ - "/***************************************************************************************", - " * File: ${TM_FILENAME}", - " * Author: ${USER}", - " * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - " * Description: Brief description of the file's purpose.", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * License", - " **************************************************************************************/", - "/*", - " * This file is licensed under the MIT License.", - " * License information should be updated as necessary.", - " */", - "", - "/***************************************************************************************", - " * Includes", - " **************************************************************************************/", - "using System;", - "", - "namespace ${TM_FILENAME_BASE/(.*)/${1:/upcase}/}", - "{", - "/***************************************************************************************", - " * Constants", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Structure Declarations", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Class Declarations", - " **************************************************************************************/", - "", - "}" - ], - "description": "CSharp file template", - "isFileTemplate": true - }, - /*************************************************************************************** - * Debugging - **************************************************************************************/ - "Log Hex Value": { - "scope": "csharp", - "prefix": "hlog", - // FIXME - "body": [], - "description": "Log hex value to console" - }, - "Log String Value": { - "scope": "csharp", - "prefix": "slog", - // FIXME - "body": [], - "description": "Log string value to console" - }, - "Log Function Name": { - "scope": "csharp", - "prefix": "log0", - // FIXME - "body": [], - "description": "Log function name with line number to console" - }, - "Log Variable Value": { - "scope": "csharp", - "prefix": "log1", - // FIXME - "body": [], - "description": "Log variable value to console" - } -} diff --git a/snippets/css.code-snippets b/snippets/css.code-snippets deleted file mode 100644 index 6ded2aa..0000000 --- a/snippets/css.code-snippets +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @file css.code-snippets - * @brief Contains custom code snippets for CSS - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "CSS File Template": { - "scope": "css", - "prefix": "template", - "body": [ - "/***************************************************************************************", - " * File: ${TM_FILENAME}", - " * Author: ${USER}", - " * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - " * Description: Brief description of the file's purpose.", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * License", - " **************************************************************************************/", - "/*", - " * This file is licensed under the MIT License.", - " * License information should be updated as necessary.", - " */", - "", - "/***************************************************************************************", - " * Constants", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Styles", - " **************************************************************************************/", - "", - "/* Add your CSS rules here */", - "" - ], - "description": "css file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/dockercompose.code-snippets b/snippets/dockercompose.code-snippets deleted file mode 100644 index 779f4ab..0000000 --- a/snippets/dockercompose.code-snippets +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @file dockercompose.code-snippets - * @brief Contains custom code snippets for Docker Compose - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Docker Compose File Template": { - "scope": "dockercompose", - "prefix": "template", - "body": [ - "##########################################################################################", - "# File: ${TM_FILENAME}", - "# Author: ${USER}", - "# Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "# Description: Brief description of the script purpose.", - "##########################################################################################", - "", - "##########################################################################################", - "# License", - "##########################################################################################", - "# This Docker Compose file is licensed under the MIT License.", - "# License information should be updated as necessary.", - "##########################################################################################", - "", - "##########################################################################################", - "# Services", - "##########################################################################################", - "", - "##########################################################################################", - "# Main Docker Compose Configuration", - "##########################################################################################", - "", - "", - "##########################################################################################", - "# End of Docker Compose Configuration", - "##########################################################################################" - ], - "description": "docker compose file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/dockerfile.code-snippets b/snippets/dockerfile.code-snippets deleted file mode 100644 index 94018c0..0000000 --- a/snippets/dockerfile.code-snippets +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @file dockerfile.code-snippets - * @brief Contains custom code snippets for Dockerfile - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Dockerfile File Template": { - "scope": "dockerfile", - "prefix": "template", - "body": [ - "# ##########################################################################", - "# File: ${TM_FILENAME}", - "# Author: ${USER}", - "# Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "# Description: Brief description of the Dockerfile purpose.", - "# ##########################################################################", - "", - "# ##########################################################################", - "# License", - "# ##########################################################################", - "# This Dockerfile is licensed under the MIT License.", - "# License information should be updated as necessary.", - "# ##########################################################################", - "", - "# ##########################################################################", - "# Base Image", - "# ##########################################################################", - "FROM ", - "", - "# ##########################################################################", - "# Maintainer", - "# ##########################################################################", - "LABEL maintainer=\"${USER}\"", - "", - "# ##########################################################################", - "# Install Dependencies", - "# ##########################################################################", - "", - "# ##########################################################################", - "# Copy Files", - "# ##########################################################################", - "", - "# ##########################################################################", - "# Set Working Directory", - "# ##########################################################################", - "WORKDIR /", - "", - "# ##########################################################################", - "# Expose Port", - "# ##########################################################################", - "EXPOSE 8080", - "", - "# ##########################################################################", - "# Command to Run", - "# ##########################################################################", - "CMD [\"/bin/bash\"]", - "" - ], - - "description": "dockerfile file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/go.code-snippets b/snippets/go.code-snippets deleted file mode 100644 index ad4af15..0000000 --- a/snippets/go.code-snippets +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @file go.code-snippets - * @brief Contains custom code snippets for Go - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Go File Template": { - "scope": "go", - "prefix": "template", - "body": [ - "/***************************************************************************************", - " * File: ${TM_FILENAME}", - " * Author: ${USER}", - " * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - " * Description: Brief description of the file's purpose.", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * License", - " **************************************************************************************/", - "/*", - " * This file is licensed under the MIT License.", - " * License information should be updated as necessary.", - " */", - "", - "/***************************************************************************************", - " * Imports", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Constants", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Type Declarations", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Functions", - " **************************************************************************************/", - "" - ], - "description": "go file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/java.code-snippets b/snippets/java.code-snippets deleted file mode 100644 index f7bd9c0..0000000 --- a/snippets/java.code-snippets +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @file java.code-snippets - * @brief Contains custom code snippets for Java - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Java File Template": { - "scope": "java", - "prefix": "template", - "body": [ - "/***************************************************************************************", - " * File: ${TM_FILENAME}", - " * Author: ${USER}", - " * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - " * Description: Brief description of the file's purpose.", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * License", - " **************************************************************************************/", - "/*", - " * This file is licensed under the MIT License.", - " * License information should be updated as necessary.", - " */", - "", - "/***************************************************************************************", - " * Imports", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Class Declaration", - " **************************************************************************************/", - "public class ${TM_FILENAME_BASE/(.*)/${1:/upcase}/} {", - "", - "}", - "" - ], - "description": "java file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/js.code-snippets b/snippets/js.code-snippets deleted file mode 100644 index 1360871..0000000 --- a/snippets/js.code-snippets +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @file js.code-snippets - * @brief Contains custom code snippets for Javascript - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "JavaScript File Template": { - "scope": "javascript", - "prefix": "template", - "body": [ - "/***************************************************************************************", - " * File: ${TM_FILENAME}", - " * Author: ${USER}", - " * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - " * Description: Brief description of the file's purpose.", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * License", - " **************************************************************************************/", - "/*", - " * This file is licensed under the MIT License.", - " * License information should be updated as necessary.", - " */", - "", - "/***************************************************************************************", - " * Imports", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Constants", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Function Declarations", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Main Code", - " **************************************************************************************/", - "" - ], - "description": "JavaScript file template", - "isFileTemplate": true - }, - - /*************************************************************************************** - * Debugging - **************************************************************************************/ - "Log Hex Value": { - "scope": "javascript", - "prefix": "hlog", - // FIXME - "body": [ - "console.log('${TM_FILENAME}:%d', 0 ${BLOCK_COMMENT_START} lineNumber ${BLOCK_COMMENT_END}, $1); ${LINE_COMMENT} REMOVE" - ], - "description": "Log hex value to console" - }, - "Log String Value": { - "scope": "javascript", - "prefix": "slog", - // FIXME - "body": [ - "console.log('${TM_FILENAME}:%d', 0 ${BLOCK_COMMENT_START} lineNumber ${BLOCK_COMMENT_END}, $1); ${LINE_COMMENT} REMOVE" - ], - "description": "Log string value to console" - }, - "Log Function Name": { - "scope": "javascript", - "prefix": "log0", - // FIXME - "body": [ - "console.log('${TM_FILENAME}:%d', 0 ${BLOCK_COMMENT_START} lineNumber ${BLOCK_COMMENT_END}); ${LINE_COMMENT} REMOVE" - ], - "description": "Log function name with line number to console" - }, - "Log Variable Value": { - "scope": "javascript", - "prefix": "log1", - // FIXME - "body": [ - "console.log('${TM_FILENAME}:%d', 0 ${BLOCK_COMMENT_START} lineNumber ${BLOCK_COMMENT_END}, $1); ${LINE_COMMENT} REMOVE" - ], - "description": "Log variable value to console" - } -} diff --git a/snippets/make.code-snippets b/snippets/make.code-snippets deleted file mode 100644 index 47f3bd7..0000000 --- a/snippets/make.code-snippets +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @file make.code-snippets - * @brief Contains custom code snippets for Makefile - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Makefile File Template": { - "scope": "makefile", - "prefix": "template", - "body": [ - "#***************************************************************************************", - "# * File: ${TM_FILENAME}", - "# * Author: ${USER}", - "# * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "# * Description: Brief description of the Makefile's purpose.", - "# **************************************************************************************/", - "", - "#***************************************************************************************", - "# * License", - "# **************************************************************************************/", - "# This file is licensed under the MIT License.", - "# License information should be updated as necessary.", - "", - "#***************************************************************************************", - "# * Variables", - "# **************************************************************************************/", - "", - "#***************************************************************************************", - "# * Targets", - "# **************************************************************************************/", - "", - "# Phony targets", - ".PHONY: all clean test", - "", - "# Default target", - "all: target1 target2", - "", - "# Clean up build artifacts", - "clean:", - "\t@echo 'Cleaning up...'", - "\trm -rf", - "", - "# Run tests", - "test:", - "\t@echo 'Running tests...'", - "" - ], - "description": "makefile file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/markdown.code-snippets b/snippets/markdown.code-snippets deleted file mode 100644 index fb3d8c4..0000000 --- a/snippets/markdown.code-snippets +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @file markdown.code-snippets - * @brief Contains custom code snippets for Markdown - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Markdown File Template": { - "scope": "markdown", - "prefix": "template", - "body": [ - "", - "", - "", - "", - "", - "", - "# ProjectTitle", - "", - "", - "", - "* [Introduction](#introduction)", - "* [Requirements](#requirements)", - "* [Usage](#usage)", - "* [Contributing](#contributing)", - "* [License](#license)", - "", - "", - "", - "# Introduction", - "", - "# Requirements", - "", - "# Usage", - "", - "# Contributing", - "", - "# License", - "" - ], - "description": "markdown file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/perl.code-snippets b/snippets/perl.code-snippets deleted file mode 100644 index 5ea98fe..0000000 --- a/snippets/perl.code-snippets +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @file perl.code-snippets - * @brief Contains custom code snippets for Perl - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Perl File Template": { - "scope": "perl, perl6", - "prefix": "template", - "body": [ - "#***************************************************************************************", - "# * File: ${TM_FILENAME}", - "# * Author: ${USER}", - "# * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "# * Description: Brief description of the script's purpose.", - "# **************************************************************************************/", - "", - "#***************************************************************************************", - "# * License", - "# **************************************************************************************/", - "#", - "# This file is licensed under the MIT License.", - "# License information should be updated as necessary.", - "#", - "", - "#***************************************************************************************", - "# * Use Statements", - "# **************************************************************************************/", - "use strict;", - "use warnings;", - "", - "#***************************************************************************************", - "# * Constants", - "# **************************************************************************************/", - "", - "#***************************************************************************************", - "# * Subroutines", - "# **************************************************************************************/", - "", - "#***************************************************************************************", - "# * Main Script Logic", - "# **************************************************************************************/", - "", - "#***************************************************************************************", - "# * End of Script", - "# **************************************************************************************/" - ], - "description": "perl file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/php.code-snippets b/snippets/php.code-snippets deleted file mode 100644 index e2b7dc6..0000000 --- a/snippets/php.code-snippets +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @file php.code-snippets - * @brief Contains custom code snippets for PHP - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "PHP File Template": { - "scope": "php", - "prefix": "template", - "body": [ - "", - "", - "<#***************************************************************************************", - " * License", - " **************************************************************************************#>", - "<#", - " * This file is licensed under the MIT License.", - " * License information should be updated as necessary.", - " -->", - "", - "<#***************************************************************************************", - " * Parameters", - " **************************************************************************************#>", - "", - "<#***************************************************************************************", - " * Functions", - " **************************************************************************************#>", - "", - "<#***************************************************************************************", - " * Script Logic", - " **************************************************************************************#>", - "", - "<#***************************************************************************************", - " * End of Script", - " **************************************************************************************#>" - ], - "description": "powershell file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/python.code-snippets b/snippets/python.code-snippets deleted file mode 100644 index e1f56f5..0000000 --- a/snippets/python.code-snippets +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @file python.code-snippets - * @brief Contains custom code snippets for Python - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Python File Template": { - "scope": "python", - "prefix": "template", - "body": [ - "# ***************************************************************************************", - "# File: ${TM_FILENAME}", - "# Author: ${USER}", - "# Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "# Description: Brief description of the file's purpose.", - "# **************************************************************************************/", - "", - "# ***************************************************************************************", - "# License", - "# **************************************************************************************/", - "\"\"\"", - "# This file is licensed under the MIT License.", - "# License information should be updated as necessary.", - "\"\"\"", - "", - "# ***************************************************************************************", - "# Imports", - "# **************************************************************************************/", - "", - "# ***************************************************************************************", - "# Constants", - "# **************************************************************************************/", - "", - "# ***************************************************************************************", - "# Function Declarations", - "# **************************************************************************************/", - "", - "# ***************************************************************************************", - "# Main Code", - "# **************************************************************************************/", - "" - ], - "description": "Python file template", - "isFileTemplate": true - }, - - /*************************************************************************************** - * Debugging - **************************************************************************************/ - - "Log Hex Value": { - "scope": "python", - "prefix": "hlog", - // FIXME - "body": [ - "print(f'{\"${TM_FILENAME}\"}:{0} $1: 0x{$1:02X}') ${LINE_COMMENT} REMOVE" - ], - "description": "Log hex value to console" - }, - "Log String Value": { - "scope": "python", - "prefix": "slog", - // FIXME - "body": [ - "print(f'{\"${TM_FILENAME}\"}:{0} $1: {$1}') ${LINE_COMMENT} REMOVE" - ], - "description": "Log string value to console" - }, - "Log Function Name": { - "scope": "python", - "prefix": "log0", - // FIXME - "body": ["print(f'{\"${TM_FILENAME}\"}:{0} ') ${LINE_COMMENT} REMOVE"], - "description": "Log function name with line number to console" - }, - "Log Variable Value": { - "scope": "python", - "prefix": "log1", - // FIXME - "body": [ - "print(f'{\"${TM_FILENAME}\"}:{0} $1 : {$1}') ${LINE_COMMENT} REMOVE" - ], - "description": "Log variable value to console" - } -} diff --git a/snippets/ruby.code-snippets b/snippets/ruby.code-snippets deleted file mode 100644 index 2deb84d..0000000 --- a/snippets/ruby.code-snippets +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @file ruby.code-snippets - * @brief Contains custom code snippets for Ruby - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Ruby File Template": { - "scope": "ruby", - "prefix": "template", - "body": [ - "# ***************************************************************************************", - "# * File: ${TM_FILENAME}", - "# * Author: ${USER}", - "# * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "# * Description: Brief description of the file's purpose.", - "# **************************************************************************************/", - "", - "# ***************************************************************************************", - "# * License", - "# **************************************************************************************/", - "#", - "# This file is licensed under the MIT License.", - "# License information should be updated as necessary.", - "#", - "", - "# ***************************************************************************************", - "# * Requires", - "# **************************************************************************************/", - "", - "# ***************************************************************************************", - "# * Constants", - "# **************************************************************************************/", - "", - "# ***************************************************************************************", - "# * Classes", - "# **************************************************************************************/", - "", - "# ***************************************************************************************", - "# * Main Script Logic", - "# **************************************************************************************/", - "" - ], - "description": "ruby file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/rust.code-snippets b/snippets/rust.code-snippets deleted file mode 100644 index c398a23..0000000 --- a/snippets/rust.code-snippets +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @file rust.code-snippets - * @brief Contains custom code snippets for Rust - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Rust File Template": { - "scope": "rust", - "prefix": "template", - "body": [ - "/***************************************************************************************", - " * File: ${TM_FILENAME}", - " * Author: ${USER}", - " * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - " * Description: Brief description of the file's purpose.", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * License", - " **************************************************************************************/", - "/*", - " * This file is licensed under the MIT License.", - " * License information should be updated as necessary.", - " */", - "", - "/***************************************************************************************", - " * Crates", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Modules", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Structs", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Enums", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Functions", - " **************************************************************************************/", - "" - ], - "description": "rust file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/sh.code-snippets b/snippets/sh.code-snippets deleted file mode 100644 index 47a7c2f..0000000 --- a/snippets/sh.code-snippets +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @file sh.code-snippets - * @brief Contains custom code snippets for Shellscript - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Shell Script File Template": { - "scope": "shellscript", - "prefix": "template", - "body": [ - "#!/bin/sh", - "", - "##########################################################################################", - "# File: ${TM_FILENAME}", - "# Author: ${USER}", - "# Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "# Description: Brief description of the script purpose.", - "##########################################################################################", - "", - "##########################################################################################", - "# License", - "##########################################################################################", - "# This script is licensed under the MIT License.", - "# License information should be updated as necessary.", - "", - "##########################################################################################", - "# Constants", - "##########################################################################################", - "", - "##########################################################################################", - "# Functions", - "##########################################################################################", - "", - "##########################################################################################", - "# Main Script", - "##########################################################################################", - "", - "# Main script logic goes here", - "" - ], - "description": "Shell script file template", - "isFileTemplate": true - }, - /*************************************************************************************** - * Debugging - **************************************************************************************/ - "Print Hex Value": { - "scope": "shellscript", - "prefix": "hlog", - "body": [ - "printf \"%s: '$1': 0x%X\\n\" \"${TM_FILENAME}\" \"$$1\" ${LINE_COMMENT} REMOVE" - ], - "description": "Print hex value to console" - } -} diff --git a/snippets/typescript.code-snippets b/snippets/typescript.code-snippets deleted file mode 100644 index 6a71a29..0000000 --- a/snippets/typescript.code-snippets +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @file typescript.code-snippets - * @brief Contains custom code snippets for Typescript - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Typescript File Template": { - "scope": "typescript", - "prefix": "template", - "body": [ - "/***************************************************************************************", - " * File: ${TM_FILENAME}", - " * Author: ${USER}", - " * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - " * Description: Brief description of the file's purpose.", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * License", - " **************************************************************************************/", - "/*", - " * This file is licensed under the MIT License.", - " * License information should be updated as necessary.", - " */", - "", - "/***************************************************************************************", - " * Imports", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Constants", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Interfaces", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Classes", - " **************************************************************************************/", - "", - "/***************************************************************************************", - " * Functions", - " **************************************************************************************/", - "" - ], - "description": "typescript file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/vb.code-snippets b/snippets/vb.code-snippets deleted file mode 100644 index 7ce9e45..0000000 --- a/snippets/vb.code-snippets +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @file vb.code-snippets - * @brief Contains custom code snippets for Visual Basic - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Visual Basic File Template": { - "scope": "vb", - "prefix": "template", - "body": [ - "'****************************************************************************************", - "' * File: ${TM_FILENAME}", - "' * Author: ${USER}", - "' * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "' * Description: Brief description of the file's purpose.", - "' ***************************************************************************************", - "", - "'****************************************************************************************", - "' * License", - "' ***************************************************************************************", - "'", - "' This file is licensed under the MIT License.", - "' License information should be updated as necessary.", - "'", - "", - "'****************************************************************************************", - "' * Imports", - "' ***************************************************************************************", - "Imports System", - "", - "' ***************************************************************************************", - "' * Constants", - "' ***************************************************************************************", - "", - "'****************************************************************************************", - "' * Class Declarations", - "' ***************************************************************************************", - "Public Class ${TM_FILENAME_BASE/(.*)/${1:/upcase}/}", - "", - "", - "End Class", - ], - "description": "vb file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/snippets/yaml.code-snippets b/snippets/yaml.code-snippets deleted file mode 100644 index 27a937d..0000000 --- a/snippets/yaml.code-snippets +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @file yaml.code-snippets - * @brief Contains custom code snippets for Yaml - * - * This file provides a collection of code snippets designed to facilitate rapid - * development by offering quick access to commonly used patterns and boilerplate - * code. The snippets include function templates, common data structures, and - * useful macros. - * - * To utilize these snippets, integrate this file into your IDE's snippet manager - * or configuration. Feel free to modify and expand the snippets to meet your - * specific coding requirements. - */ - -{ - /*************************************************************************************** - * File Template - **************************************************************************************/ - "Yaml File Template": { - "scope": "yaml", - "prefix": "template", - "body": [ - "#***************************************************************************************", - "# * File: ${TM_FILENAME}", - "# * Author: ${USER}", - "# * Date: ${CURRENT_DATE}-${CURRENT_MONTH_NAME_SHORT}-${CURRENT_YEAR}", - "# * Description: Brief description of the file's purpose.", - "# **************************************************************************************/", - "", - "#***************************************************************************************", - "# * License", - "# **************************************************************************************/", - "#", - "# This file is licensed under the MIT License.", - "# License information should be updated as necessary.", - "#", - "", - "#***************************************************************************************", - "# * Configuration", - "# **************************************************************************************/", - "" - ], - "description": "yaml file template", - "isFileTemplate": true - } - /*************************************************************************************** - * Debugging - **************************************************************************************/ - // TODO -} diff --git a/src/org/devops/BuildHelper.groovy b/src/org/devops/BuildHelper.groovy new file mode 100644 index 0000000..cecc2a8 --- /dev/null +++ b/src/org/devops/BuildHelper.groovy @@ -0,0 +1,105 @@ +package org.devops + +/** + * Build helper that abstracts over the build tooling + * (Maven or Gradle) so consuming pipelines don't hard-code commands. + */ +class BuildHelper implements Serializable { + private final Script context + + BuildHelper(Script context) { + this.context = context + } + + /** + * Execute the build using the configured tool. + * + * @param config Config with buildTool, retryAttempts, retryDelaySeconds + * @param goals Goals/phases to run (default 'clean package' or 'clean build') + * @param extraArgs Extra CLI arguments + */ + void build(Config config, String goals = null, List extraArgs = []) { + def tool = config.get('buildTool', 'maven') + def toolGoals = goals ?: (tool == 'gradle' ? 'clean build' : 'clean package') + def attempts = config.getAs('retryAttempts', Integer, 1) + def delay = (config.getAs('retryDelaySeconds', Integer, 0) * 1000L) as long + def args = extraArgs.join(' ') + + def utils = new Utils(context) + + utils.retry(attempts, delay) { + if (tool == 'gradle') { + context.sh("./gradlew ${toolGoals} ${args} --no-daemon".trim()) + } else { + def mvnOpts = '-B -ntp' + if (config.getAs('failOnError', Boolean, true)) { + mvnOpts += ' -Dmaven.test.skip=false' + } else { + mvnOpts += ' -Dmaven.test.skip=true' + } + context.sh("mvn ${mvnOpts} ${toolGoals} ${args}".trim()) + } + } + } + + /** + * Run tests and publish JUnit reports + coverage if available. + */ + void test(Config config, String goals = null) { + def tool = config.get('buildTool', 'maven') + def toolGoals = goals ?: (tool == 'gradle' ? 'test' : 'test') + def args = config.get('extraTestArgs', '') + + context.sh( + (tool == 'gradle' ? "./gradlew ${toolGoals} ${args}" : "mvn ${args} ${toolGoals}").trim(), + ) + + publishTestResults(config) + publishCoverage(config) + } + + private void publishTestResults(Config config) { + def patterns = [] + def tool = config.get('buildTool', 'maven') + if (tool == 'gradle') { + patterns = ['build/test-results/test/*.xml', 'build/test-results/integrationTest/*.xml'] + } else { + patterns = ['target/surefire-reports/*.xml', 'target/failsafe-reports/*.xml'] + } + def allowed = patterns.find { p -> context.fileExists(p) } + if (allowed) { + context.junit allowed + } else { + context.echo('[devops-build] No JUnit result files found; skipping test report.') + } + } + + private void publishCoverage(Config config) { + def tool = config.get('buildTool', 'maven') + def reportDir + def reportFiles + if (tool == 'gradle') { + reportDir = 'build/reports/jacoco/test' + reportFiles = 'index.html' + } else { + reportDir = 'target/site/jacoco' + reportFiles = 'index.html' + } + if (context.fileExists("${reportDir}/${reportFiles}")) { + context.cobertura( + autoUpdateHealth: false, + autoUpdateStability: false, + coberturaReportFile: "${reportDir}/${reportFiles}", + conditionalCoverageTargets: '70, 80, 90', + failNoReports: false, + failUnstableReport: false, + failUnstableReports: false, + maxNumberOfBuilds: 0, + onlyStable: false, + onlyStableStarted: false, + sourceEncoding: 'UTF_8', + zoomCoverageChart: false, + ) + } + } +} diff --git a/src/org/devops/Config.groovy b/src/org/devops/Config.groovy new file mode 100644 index 0000000..f77c030 --- /dev/null +++ b/src/org/devops/Config.groovy @@ -0,0 +1,73 @@ +package org.devops + +/** + * Centralised configuration manager. + * + * Provides sensible defaults for common pipeline settings (branch name, + * notification channels, build tool, etc.) while allowing per-job + * overrides supplied by the consuming Jenkinsfile. + * + * Configuration precedence (highest wins): + * 1. Explicit call-time override + * 2. Job-level config passed to {@code Config.withDefaults()} + * 3. Library defaults defined in DEFAULTS + */ +class Config implements Serializable { + private final Map values + + Config(Map values = [:]) { + this.values = new HashMap(Config.DEFAULTS) + if (values) { + this.values.putAll(values) + } + } + + static final Map DEFAULTS = [ + branch : 'main', + buildTool : 'maven', + failOnError : true, + failOnWarning : false, + notifyOnFailure : true, + notifyOnSuccess : false, + slackChannel : '#builds', + slackServer : null, + emailRecipients : '', + gitCredentialsId : '', + timeoutMinutes : 30, + retryAttempts : 1, + retryDelaySeconds : 0, + stashIncludes : '**/*.java, **/*.groovy, **/pom.xml, **/build.gradle', + stashExcludes : 'target/, build/, .git/', + cleanWorkspace : false, + ] + + /** + * Return the value for a key, falling back to {@code defaultValue} + * when neither the override nor a library default is present. + */ + def get(String key, def defaultValue = null) { + if (values.containsKey(key) && values.get(key) != null) { + return values[key] + } + if (Config.DEFAULTS.containsKey(key) && Config.DEFAULTS.get(key) != null) { + return Config.DEFAULTS[key] + } + return defaultValue + } + + /** + * Return a typed value coerced to {@code type}. + */ + T getAs(String key, Class type, T defaultValue = null) { + def val = get(key, defaultValue) + if (val == null) { + return defaultValue + } + return val.asType(type) + } + + @Override + String toString() { + return "Config${values}" + } +} diff --git a/src/org/devops/Docker.groovy b/src/org/devops/Docker.groovy new file mode 100644 index 0000000..14d21cd --- /dev/null +++ b/src/org/devops/Docker.groovy @@ -0,0 +1,413 @@ +package org.devops + +/** + * Docker helper providing build, push, login, tag, run, compose, + * clean, inspect, and registry-specific login operations. + * + * All methods delegate to Docker CLI commands via {@code sh} and + * integrate with the library's retry, timing, and notification utilities. + */ +class Docker implements Serializable { + private final Script context + + Docker(Script context) { + this.context = context + } + + /** + * Build a Docker image with optional tags, build args, and registry login. + * + * @param config map with: + * - image : image name (required) + * - tags : list of tags (default ['latest']) + * - buildArgs : map of build arguments + * - registry : registry URL (optional) + * - registryCredentialsId : credentials for registry login (optional) + * - dockerfile : path to Dockerfile (default 'Dockerfile') + * - buildContext : build context path (default '.') + * - noCache : use --no-cache (default false) + * - pull : use --pull (default false) + * - retryAttempts : retry count (default 1) + * - retryDelaySeconds : retry delay in seconds (default 0) + * - failOnError : fail on build error (default true) + */ + void build(Map config = [:]) { + def utils = new Utils(context) + def image = config.image + if (!image) { + error('dockerBuild: "image" parameter is required') + } + + def tags = config.tags ?: ['latest'] + def buildArgs = config.buildArgs ?: [:] + def dockerfile = config.dockerfile ?: 'Dockerfile' + def buildContext = config.buildContext ?: '.' + def noCache = config.noCache ? '--no-cache' : '' + def pull = config.pull ? '--pull' : '' + def registry = config.registry + def registryCreds = config.registryCredentialsId + def attempts = config.retryAttempts ?: 1 + def delay = ((config.retryDelaySeconds ?: 0) * 1000L) as long + + def allTags = tags.collect { tag -> + registry ? "${registry}/${image}:${tag}" : "${image}:${tag}" + } + + utils.retry(attempts, delay) { + if (registry && registryCreds) { + login([registry: registry, credentialsId: registryCreds]) + } + + def argsList = buildArgs.collect { k, v -> "--build-arg ${k}=${v}" }.join(' ') + def tagFlags = allTags.collect { t -> "-t ${t}" }.join(' ') + + context.sh( + "docker build ${pull} ${noCache} ${argsList} ${tagFlags} -f ${dockerfile} ${buildContext}".trim(), + ) + + if (registry && registryCreds) { + logout([registry: registry]) + } + } + } + + /** + * Push a Docker image tag to a registry. + * + * @param config map with: + * - image : image name (required) + * - tag : tag to push (default 'latest') + * - registry : registry URL (required for push) + * - registryCredentialsId : credentials for registry login (optional) + * - retryAttempts : retry count (default 1) + * - retryDelaySeconds : retry delay in seconds (default 0) + */ + void push(Map config = [:]) { + def utils = new Utils(context) + def image = config.image + if (!image) { + error('dockerPush: "image" parameter is required') + } + def registry = config.registry + if (!registry) { + error('dockerPush: "registry" parameter is required') + } + + def tag = config.tag ?: 'latest' + def fullImage = "${registry}/${image}:${tag}" + def registryCreds = config.registryCredentialsId + def attempts = config.retryAttempts ?: 1 + def delay = ((config.retryDelaySeconds ?: 0) * 1000L) as long + + utils.retry(attempts, delay) { + if (registryCreds) { + login([registry: registry, credentialsId: registryCreds]) + } + context.sh("docker push ${fullImage}") + if (registryCreds) { + logout([registry: registry]) + } + } + } + + /** + * Login to a Docker registry. + * + * @param config map with: + * - registry : registry URL (required) + * - username : registry username (optional, uses credentials if provided) + * - password : registry password (optional, uses credentials if provided) + * - credentialsId : Jenkins credentials ID for registry auth + */ + void login(Map config = [:]) { + def registry = config.registry + if (!registry) { + error('dockerLogin: "registry" parameter is required') + } + + def username = config.username + def password = config.password + def credsId = config.credentialsId + + if (credsId) { + context.withCredentials([[ + $class : 'UsernamePasswordMultiBinding', + credentialsId: credsId, + usernameVariable: 'DOCKER_USER', + passwordVariable: 'DOCKER_PASS', + ]]) { + context.sh("docker login ${registry} -u \${DOCKER_USER} -p \${DOCKER_PASS}") + } + } else if (username && password) { + context.sh("docker login ${registry} -u ${username} -p ${password}") + } else { + context.sh("docker login ${registry}") + } + } + + /** + * Logout from a Docker registry. + * + * @param config map with: + * - registry : registry URL (default Docker Hub) + */ + void logout(Map config = [:]) { + def registry = config.registry ?: 'https://index.docker.io/v1/' + context.sh("docker logout ${registry}") + } + + /** + * Tag a Docker image with a new tag. + * + * @param config map with: + * - image : source image name (required) + * - tag : new tag (required) + * - sourceTag : source image tag (default 'latest') + * - registry : registry URL (optional) + */ + void tag(Map config = [:]) { + def image = config.image + if (!image) { + error('dockerTag: "image" parameter is required') + } + def tag = config.tag + if (!tag) { + error('dockerTag: "tag" parameter is required') + } + + def registry = config.registry + def sourceTag = config.sourceTag ?: 'latest' + def sourceImage = registry ? "${registry}/${image}:${sourceTag}" : "${image}:${sourceTag}" + def targetImage = registry ? "${registry}/${image}:${tag}" : "${image}:${tag}" + context.sh("docker tag ${sourceImage} ${targetImage}") + } + + /** + * Run a Docker container. + * + * @param config map with: + * - image : image to run (required) + * - command : command to run inside the container (optional) + * - args : additional docker run flags (optional) + * - detached : run in detached mode (default false) + * - env : map of environment variables (optional) + * - ports : list of port mappings (optional) + * - volumes : list of volume mounts (optional) + * - rm : remove container after exit (default true) + * - name : container name (optional) + * - retryAttempts : retry count (default 1) + * - retryDelaySeconds : retry delay in seconds (default 0) + */ + void run(Map config = [:]) { + def utils = new Utils(context) + def image = config.image + if (!image) { + error('dockerRun: "image" parameter is required') + } + + def command = config.command ? "\"${config.command}\"" : '' + def args = config.args ?: '' + def detached = config.detached ? '-d' : '' + def rm = config.rm != false ? '--rm' : '' + def name = config.name ? "--name ${config.name}" : '' + def attempts = config.retryAttempts ?: 1 + def delay = ((config.retryDelaySeconds ?: 0) * 1000L) as long + + def envFlags = '' + if (config.env) { + envFlags = config.env.collect { k, v -> "-e ${k}=${v}" }.join(' ') + } + + def portFlags = '' + if (config.ports) { + portFlags = config.ports.collect { p -> "-p ${p}" }.join(' ') + } + + def volumeFlags = '' + if (config.volumes) { + volumeFlags = config.volumes.collect { v -> "-v ${v}" }.join(' ') + } + + utils.retry(attempts, delay) { + context.sh( + "docker run ${detached} ${rm} ${name} ${envFlags} ${portFlags} ${volumeFlags} ${args} ${image} ${command}".trim(), + ) + } + } + + /** + * Execute a docker-compose command. + * + * @param config map with: + * - command : compose command (up, down, build, etc.) (required) + * - composeFile : path to docker-compose file (default 'docker-compose.yml') + * - args : additional compose arguments (optional) + * - env : map of environment variables (optional) + * - retryAttempts : retry count (default 1) + * - retryDelaySeconds : retry delay in seconds (default 0) + */ + void compose(Map config = [:]) { + def utils = new Utils(context) + def command = config.command + if (!command) { + error('dockerCompose: "command" parameter is required') + } + + def composeFile = config.composeFile ?: 'docker-compose.yml' + def args = config.args ?: '' + def attempts = config.retryAttempts ?: 1 + def delay = ((config.retryDelaySeconds ?: 0) * 1000L) as long + + def envFlags = '' + if (config.env) { + envFlags = config.env.collect { k, v -> "-e ${k}=${v}" }.join(' ') + } + + utils.retry(attempts, delay) { + context.sh( + "docker-compose ${envFlags} -f ${composeFile} ${command} ${args}".trim(), + ) + } + } + + /** + * Clean up Docker resources (containers, images, volumes, networks, build cache). + * + * @param config map with: + * - containers : remove stopped containers (default true) + * - images : remove dangling images (default true) + * - volumes : remove unused volumes (default true) + * - networks : remove unused networks (default true) + * - buildCache : prune build cache (default false) + * - force : skip confirmation prompt (default true) + * - pruneAll : remove all unused images, not just dangling (default false) + */ + void clean(Map config = [:]) { + def utils = new Utils(context) + def force = config.force != false + def pruneAll = config.pruneAll ?: false + + if (force) { + context.echo('[docker] Force cleanup enabled — skipping confirmation') + } + + if (config.containers != false) { + context.sh('docker container prune -f') + } + + if (config.images != false) { + if (pruneAll) { + context.sh('docker image prune -af') + } else { + context.sh('docker image prune -f') + } + } + + if (config.volumes != false) { + context.sh('docker volume prune -f') + } + + if (config.networks != false) { + context.sh('docker network prune -f') + } + + if (config.buildCache) { + context.sh('docker builder prune -f') + } + } + + /** + * Inspect a Docker image or container and return the output. + * + * @param config map with: + * - target : image or container name/tag (required) + * - format : output format (optional, default '{{json .}}') + * + * @return the raw inspect output as a String + */ + String inspect(Map config = [:]) { + def target = config.target + if (!target) { + error('dockerInspect: "target" parameter is required') + } + + def format = config.format ?: '{{json .}}' + return context.sh( + script: "docker inspect --format '${format}' ${target}", + returnStdout: true, + ).trim() + } + + /** + * Login to AWS Elastic Container Registry (ECR). + * + * @param config map with: + * - region : AWS region (required) + * - accountId : AWS account ID (optional, uses AWS CLI to resolve) + * - credentialsId : Jenkins credentials ID for AWS access keys (optional) + * - registry : ECR registry URI (optional, auto-derived if accountId provided) + */ + void loginEcr(Map config = [:]) { + def region = config.region + if (!region) { + error('dockerLoginEcr: "region" parameter is required') + } + + def accountId = config.accountId + def registry = config.registry + + if (!registry && accountId) { + registry = "${accountId}.dkr.ecr.${region}.amazonaws.com" + } + + if (!registry) { + error('dockerLoginEcr: "registry" or "accountId" parameter is required') + } + + def credsId = config.credentialsId + if (credsId) { + context.withCredentials([[ + $class : 'AmazonWebServicesCredentialsBinding', + credentialsId: credsId, + accessKeyVariable: 'AWS_ACCESS_KEY_ID', + secretKeyVariable: 'AWS_SECRET_ACCESS_KEY', + ]]) { + context.sh( + "aws ecr get-login-password --region ${region} | docker login --username AWS --password-stdin ${registry}", + ) + } + } else { + context.sh( + "aws ecr get-login-password --region ${region} | docker login --username AWS --password-stdin ${registry}", + ) + } + } + + /** + * Login to Google Container Registry (GCR). + * + * @param config map with: + * - credentialsId : Jenkins credentials ID for GCR service account key (optional) + * - project : GCP project ID (optional) + * - registry : GCR registry URL (default gcr.io) + */ + void loginGcr(Map config = [:]) { + def registry = config.registry ?: 'gcr.io' + def credsId = config.credentialsId + def project = config.project + + if (credsId) { + context.withCredentials([[ + $class : 'GoogleServiceAccountKey', + credentialsId: credsId, + keyFileVariable: 'GOOGLE_APPLICATION_CREDENTIALS', + ]]) { + context.sh("gcloud auth configure-docker ${registry} --quiet") + } + } else if (project) { + context.sh("gcloud auth configure-docker ${registry} --quiet") + } else { + context.sh("gcloud auth configure-docker ${registry} --quiet") + } + } +} diff --git a/src/org/devops/GitHelper.groovy b/src/org/devops/GitHelper.groovy new file mode 100644 index 0000000..34050f6 --- /dev/null +++ b/src/org/devops/GitHelper.groovy @@ -0,0 +1,92 @@ +package org.devops + +/** + * Git helper providing checkout, tagging, and changelog utilities. + */ +class GitHelper implements Serializable { + private final Script context + + GitHelper(Script context) { + this.context = context + } + + /** + * Checkout a branch (or PR ref) with retry logic and optional + * changelog generation. + * + * @param repositoryUrl Git remote URL + * @param credentialsId Optional Jenkins credentials ID + * @param branchSpec Branch, tag, or PR ref to checkout + * @param generateChangelog if true, capture changes since last successful build + */ + void checkout(String repositoryUrl, String credentialsId = '', String branchSpec = null, boolean generateChangelog = true) { + def branch = branchSpec ?: context.env.GIT_BRANCH ?: Config.DEFAULTS.branch + branch = branch ?: Config.DEFAULTS.branch + + try { + context.checkout([ + $class: 'GitSCM', + extensions: [ + [$class: 'CleanBeforeCheckout'], + [$class: 'PruneStaleTags'], + ], + userRemoteConfigs: [[url: repositoryUrl, credentialsId: credentialsId]], + branches: [[name: branch]], + ]) + } catch (Exception e) { + context.echo("[devops-git] Checkout failed for ${branch}: ${e.message}") + throw e + } + + if (generateChangelog) { + generateChangelog(branch) + } + } + + /** + * Generate a simple changelog comment based on commit messages since the + * last successful build. + */ + private void generateChangelog(String branch) { + def upstream = context.env.GIT_PREVIOUS_SUCCESSFUL_COMMIT + if (!upstream) { + context.echo('[devops-git] No previous successful commit; skipping changelog.') + return + } + try { + def log = context.sh( + script: "git log --pretty=format:'* %s' ${upstream}..HEAD || true", + returnStdout: true, + ).trim() + if (log) { + context.echo("Recent changes on ${branch}:\n${log}") + } + } catch (Exception e) { + context.echo("[devops-git] Changelog generation skipped: ${e.message}") + } + } + + /** + * Create a git tag with annotation. + */ + void tag(String tagName, String message = '', String credentialsId = '') { + context.sh("git tag -a ${tagName} -m '${message}'") + if (credentialsId) { + context.withCredentials([[ + $class: 'UsernamePasswordMultiBinding', + credentialsId: credentialsId, + usernameVariable: 'GIT_USER', + passwordVariable: 'GIT_PASS', + ]]) { + context.sh("git push origin ${tagName}") + } + } + } + + /** + * Return the current short git commit hash. + */ + String shortCommit() { + return context.sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() + } +} diff --git a/src/org/devops/Notification.groovy b/src/org/devops/Notification.groovy new file mode 100644 index 0000000..e88c1e3 --- /dev/null +++ b/src/org/devops/Notification.groovy @@ -0,0 +1,83 @@ +package org.devops + +/** + * Notification helper supporting Slack and email backends. + * + * Usage (from a vars step): + * def notifier = new Notification(this) + * notifier.send(currentBuild.result ?: 'SUCCESS', 'Build finished') + */ +class Notification implements Serializable { + private final Script context + + Notification(Script context) { + this.context = context + } + + /** + * Send a notification to configured channels. + * + * @param status 'SUCCESS', 'FAILURE', 'UNSTABLE', 'ABORTED' + * @param message Human-readable message body + * @param config Config instance with recipient details + */ + void send(String status, String message, Config config) { + def color = colorForStatus(status) + def fullMessage = "${status}: ${message}\n${context.env.JOB_NAME} #${context.env.BUILD_NUMBER}\n${context.env.BUILD_URL}" + + if (config.getAs('notifyOnFailure', Boolean, false) && status == 'FAILURE' + || config.getAs('notifyOnSuccess', Boolean, false) && status == 'SUCCESS' + || status == 'UNSTABLE' || status == 'ABORTED') { + sendSlack(fullMessage, color, config) + sendEmail(fullMessage, status, config) + } + } + + private static String colorForStatus(String status) { + switch (status) { + case 'SUCCESS': + return 'good' + case 'FAILURE': + return 'danger' + case 'UNSTABLE': + return 'warning' + case 'ABORTED': + return 'warning' + default: + return 'good' + } + } + + private void sendSlack(String message, String color, Config config) { + def channel = config.get('slackChannel') + if (!channel) { + return + } + try { + context.slackSend( + channel: channel, + color: color, + message: message, + failOnError: false, + ) + } catch (Exception e) { + context.echo("[devops-notification] Slack notification failed: ${e.message}") + } + } + + private void sendEmail(String message, String status, Config config) { + def recipients = config.get('emailRecipients') + if (!recipients) { + return + } + try { + context.mail( + to: recipients, + subject: "[${status}] ${context.env.JOB_NAME} #${context.env.BUILD_NUMBER}", + body: message, + ) + } catch (Exception e) { + context.echo("[devops-notification] Email notification failed: ${e.message}") + } + } +} diff --git a/src/org/devops/Utils.groovy b/src/org/devops/Utils.groovy new file mode 100644 index 0000000..ff2f1d1 --- /dev/null +++ b/src/org/devops/Utils.groovy @@ -0,0 +1,79 @@ +package org.devops + +/** + * Core utility class providing shared helpers for logging, timing, + * error handling, and retry logic across all pipeline steps. + * + * Implements Serializable so instances survive pipeline replays + * when used inside a @Library context. + */ +class Utils implements Serializable { + private final Script context + + Utils(Script context) { + this.context = context + } + + /** + * Log an informational message prefixed with the library name. + */ + void info(String message) { + context.echo("[devops-utils] ${message}") + } + + /** + * Log a warning message. + */ + void warn(String message) { + context.echo("[devops-utils] WARNING: ${message}") + } + + /** + * Log an error message. + */ + void error(String message) { + context.echo("[devops-utils] ERROR: ${message}") + } + + /** + * Run a closure with timing instrumentation. Returns the closure's result. + */ + T time(String label, Closure body) { + def start = System.currentTimeMillis() + try { + return body() + } finally { + def elapsed = System.currentTimeMillis() - start + info("${label} completed in ${elapsed}ms") + } + } + + /** + * Retry a closure up to {@code maxAttempts} times with an optional + * delay between attempts. Re-throws the last exception on failure. + */ + T retry(int maxAttempts, long delayMs = 0, Closure body) { + def lastError + for (int attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return body() + } catch (Exception e) { + lastError = e + if (attempt < maxAttempts) { + warn("Attempt ${attempt} of ${maxAttempts} failed: ${e.message}. Retrying...") + if (delayMs > 0) { + context.sleep(time: (long) (delayMs / 1000), unit: 'SECONDS') + } + } + } + } + throw lastError + } + + /** + * Safely retrieve a nested map value with a default fallback. + */ + static T get(Map config, String key, T defaultValue = null) { + return config?.get(key, defaultValue) as T + } +} diff --git a/test/groovy/org/devops/BuildHelperSpec.groovy b/test/groovy/org/devops/BuildHelperSpec.groovy new file mode 100644 index 0000000..9951cf7 --- /dev/null +++ b/test/groovy/org/devops/BuildHelperSpec.groovy @@ -0,0 +1,199 @@ +package org.devops + +import spock.lang.Specification + +class BuildHelperSpec extends Specification { + + def "build() runs maven with clean package by default"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'maven']) + def helper = new BuildHelper(mock) + + when: + helper.build(config) + + then: + mock.shCalls.size() == 1 + mock.shCalls[0].script == 'mvn -B -ntp -Dmaven.test.skip=false clean package' + } + + def "build() runs gradle with clean build"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'gradle']) + def helper = new BuildHelper(mock) + + when: + helper.build(config) + + then: + mock.shCalls.size() == 1 + mock.shCalls[0].script == './gradlew clean build --no-daemon' + } + + def "build() passes extra args to maven"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'maven']) + def helper = new BuildHelper(mock) + + when: + helper.build(config, null, ['-DskipTests', '-Pprod']) + + then: + mock.shCalls[0].script == 'mvn -B -ntp -Dmaven.test.skip=false clean package -DskipTests -Pprod' + } + + def "build() skips tests when failOnError is false"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'maven', failOnError: false]) + def helper = new BuildHelper(mock) + + when: + helper.build(config) + + then: + mock.shCalls[0].script == 'mvn -B -ntp -Dmaven.test.skip=true clean package' + } + + def "build() runs gradle with custom goals"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'gradle']) + def helper = new BuildHelper(mock) + + when: + helper.build(config, 'check') + + then: + mock.shCalls[0].script == './gradlew check --no-daemon' + } + + def "build() retries on failure"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'maven', retryAttempts: 3, retryDelaySeconds: 5]) + def helper = new BuildHelper(mock) + def attempts = 0 + + when: + helper.build(config) + + then: + mock.shCalls.size() == 1 + mock.sleepCalls.size() == 0 + } + + def "test() runs maven test"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'maven']) + def helper = new BuildHelper(mock) + + when: + helper.test(config) + + then: + mock.shCalls[0].script == 'mvn test' + } + + def "test() runs gradle test"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'gradle']) + def helper = new BuildHelper(mock) + + when: + helper.test(config) + + then: + mock.shCalls[0].script == './gradlew test' + } + + def "test() passes extraTestArgs"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'maven', extraTestArgs: '-Dtest=FooTest']) + def helper = new BuildHelper(mock) + + when: + helper.test(config) + + then: + mock.shCalls[0].script == 'mvn -Dtest=FooTest test' + } + + def "test() publishes junit results when surefire reports exist"() { + given: + def mock = new MockScript() + mock.fileExistsCalls << 'target/surefire-reports/TEST-com.example.FooTest.xml' + def config = new Config([buildTool: 'maven']) + def helper = new BuildHelper(mock) + + when: + helper.test(config) + + then: + mock.junitPatterns.size() == 1 + mock.junitPatterns[0] == 'target/surefire-reports/*.xml' + } + + def "test() skips junit when no result files found"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'maven']) + def helper = new BuildHelper(mock) + + when: + helper.test(config) + + then: + mock.junitPatterns.isEmpty() + mock.echoMessages.any { it.contains('No JUnit result files found') } + } + + def "test() publishes cobertura when jacoco report exists for maven"() { + given: + def mock = new MockScript() + mock.fileExistsCalls << 'target/site/jacoco/index.html' + def config = new Config([buildTool: 'maven']) + def helper = new BuildHelper(mock) + + when: + helper.test(config) + + then: + mock.coberturaCalls.size() == 1 + mock.coberturaCalls[0].coberturaReportFile == 'target/site/jacoco/index.html' + } + + def "test() publishes cobertura when jacoco report exists for gradle"() { + given: + def mock = new MockScript() + mock.fileExistsCalls << 'build/reports/jacoco/test/index.html' + def config = new Config([buildTool: 'gradle']) + def helper = new BuildHelper(mock) + + when: + helper.test(config) + + then: + mock.coberturaCalls.size() == 1 + mock.coberturaCalls[0].coberturaReportFile == 'build/reports/jacoco/test/index.html' + } + + def "test() skips cobertura when no jacoco report found"() { + given: + def mock = new MockScript() + def config = new Config([buildTool: 'maven']) + def helper = new BuildHelper(mock) + + when: + helper.test(config) + + then: + mock.coberturaCalls.isEmpty() + } +} diff --git a/test/groovy/org/devops/ConfigSpec.groovy b/test/groovy/org/devops/ConfigSpec.groovy new file mode 100644 index 0000000..5eb0a18 --- /dev/null +++ b/test/groovy/org/devops/ConfigSpec.groovy @@ -0,0 +1,98 @@ +package org.devops + +import spock.lang.Specification + +class ConfigSpec extends Specification { + + def "defaults are set correctly"() { + given: + def config = new Config() + + then: + config.get('branch') == 'main' + config.get('buildTool') == 'maven' + config.get('failOnError') == true + config.get('failOnWarning') == false + config.get('notifyOnFailure') == true + config.get('notifyOnSuccess') == false + config.get('slackChannel') == '#builds' + config.get('slackServer') == null + config.get('emailRecipients') == '' + config.get('gitCredentialsId') == '' + config.get('timeoutMinutes') == 30 + config.get('retryAttempts') == 1 + config.get('retryDelaySeconds') == 0 + } + + def "overrides take precedence over defaults"() { + given: + def config = new Config([branch: 'develop', buildTool: 'gradle']) + + then: + config.get('branch') == 'develop' + config.get('buildTool') == 'gradle' + config.get('failOnError') == true + } + + def "get() falls back to default when override is null"() { + given: + def config = new Config([slackChannel: null]) + + then: + config.get('slackChannel') == '#builds' + } + + def "get() returns explicit defaultValue when key is not in map or defaults"() { + given: + def config = new Config() + + then: + config.get('nonexistent', 'fallback') == 'fallback' + } + + def "getAs() coerces value to requested type"() { + given: + def config = new Config([timeoutMinutes: 60]) + + when: + def result = config.getAs('timeoutMinutes', Integer, 30) + + then: + result instanceof Integer + result == 60 + } + + def "getAs() returns default when key is missing"() { + given: + def config = new Config() + + when: + def result = config.getAs('nonexistent', String, 'default') + + then: + result == 'default' + } + + def "getAs() returns default when value is null"() { + given: + def config = new Config([slackChannel: null]) + + when: + def result = config.getAs('slackChannel', String, '#fallback') + + then: + result == '#fallback' + } + + def "toString() returns formatted string"() { + given: + def config = new Config([branch: 'test']) + + when: + def str = config.toString() + + then: + str.startsWith('Config') + str.contains('branch') + } +} diff --git a/test/groovy/org/devops/DockerSpec.groovy b/test/groovy/org/devops/DockerSpec.groovy new file mode 100644 index 0000000..075e9f7 --- /dev/null +++ b/test/groovy/org/devops/DockerSpec.groovy @@ -0,0 +1,556 @@ +package org.devops + +import spock.lang.Specification + +class DockerSpec extends Specification { + + def "build() requires image parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.build([:]) + + then: + def e = thrown(Exception) + e.message.contains('"image" parameter is required') + } + + def "build() uses default tag latest"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.build(image: 'myapp') + + then: + mock.shCalls.size() == 1 + mock.shCalls[0].script.contains('-t myapp:latest') + } + + def "build() applies multiple tags"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.build(image: 'myapp', tags: ['v1', 'latest']) + + then: + mock.shCalls[0].script.contains('-t myapp:v1') + mock.shCalls[0].script.contains('-t myapp:latest') + } + + def "build() passes build args"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.build(image: 'myapp', buildArgs: [VERSION: '1.0', ENV: 'prod']) + + then: + mock.shCalls[0].script.contains('--build-arg VERSION=1.0') + mock.shCalls[0].script.contains('--build-arg ENV=prod') + } + + def "build() uses custom dockerfile and build context"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.build(image: 'myapp', dockerfile: 'Dockerfile.prod', buildContext: './dist') + + then: + mock.shCalls[0].script.contains('-f Dockerfile.prod') + mock.shCalls[0].script.contains('./dist') + } + + def "build() includes --no-cache when noCache is true"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.build(image: 'myapp', noCache: true) + + then: + mock.shCalls[0].script.contains('--no-cache') + } + + def "build() includes --pull when pull is true"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.build(image: 'myapp', pull: true) + + then: + mock.shCalls[0].script.contains('--pull') + } + + def "build() logs in and out of registry when credentials provided"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.build(image: 'myapp', registry: 'registry.example.com', registryCredentialsId: 'docker-cred') + + then: + mock.shCalls.size() >= 1 + mock.withCredentialsCalls.size() >= 1 + } + + def "push() requires image parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.push([:]) + + then: + def e = thrown(Exception) + e.message.contains('"image" parameter is required') + } + + def "push() requires registry parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.push(image: 'myapp') + + then: + def e = thrown(Exception) + e.message.contains('"registry" parameter is required') + } + + def "push() pushes to registry with tag"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.push(image: 'myapp', registry: 'registry.example.com', tag: 'v1') + + then: + mock.shCalls.size() == 1 + mock.shCalls[0].script == 'docker push registry.example.com/myapp:v1' + } + + def "login() requires registry parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.login([:]) + + then: + def e = thrown(Exception) + e.message.contains('"registry" parameter is required') + } + + def "login() uses credentialsId when provided"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.login(registry: 'registry.example.com', credentialsId: 'docker-cred') + + then: + mock.withCredentialsCalls.size() == 1 + mock.shCalls.size() == 1 + } + + def "login() uses username and password when provided"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.login(registry: 'registry.example.com', username: 'user', password: 'pass') + + then: + mock.shCalls.size() == 1 + mock.shCalls[0].script.contains('docker login registry.example.com -u user -p pass') + } + + def "login() falls back to anonymous login"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.login(registry: 'registry.example.com') + + then: + mock.shCalls.size() == 1 + mock.shCalls[0].script == 'docker login registry.example.com' + } + + def "logout() uses default Docker Hub registry"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.logout() + + then: + mock.shCalls.size() == 1 + mock.shCalls[0].script == 'docker logout https://index.docker.io/v1/' + } + + def "logout() uses custom registry"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.logout(registry: 'registry.example.com') + + then: + mock.shCalls[0].script == 'docker logout registry.example.com' + } + + def "tag() requires image parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.tag([:]) + + then: + def e = thrown(Exception) + e.message.contains('"image" parameter is required') + } + + def "tag() requires tag parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.tag(image: 'myapp') + + then: + def e = thrown(Exception) + e.message.contains('"tag" parameter is required') + } + + def "tag() tags without registry"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.tag(image: 'myapp', tag: 'v1') + + then: + mock.shCalls[0].script == 'docker tag myapp:latest myapp:v1' + } + + def "tag() tags with registry"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.tag(image: 'myapp', tag: 'v1', registry: 'registry.example.com') + + then: + mock.shCalls[0].script == 'docker tag registry.example.com/myapp:latest registry.example.com/myapp:v1' + } + + def "run() requires image parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.run([:]) + + then: + def e = thrown(Exception) + e.message.contains('"image" parameter is required') + } + + def "run() runs container with default options"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.run(image: 'myapp') + + then: + mock.shCalls[0].script.contains('docker run --rm myapp') + } + + def "run() runs container in detached mode"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.run(image: 'myapp', detached: true) + + then: + mock.shCalls[0].script.contains('-d') + } + + def "run() passes environment variables"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.run(image: 'myapp', env: [KEY: 'value']) + + then: + mock.shCalls[0].script.contains('-e KEY=value') + } + + def "run() passes port mappings"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.run(image: 'myapp', ports: ['8080:8080']) + + then: + mock.shCalls[0].script.contains('-p 8080:8080') + } + + def "run() passes volume mounts"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.run(image: 'myapp', volumes: ['/host:/container']) + + then: + mock.shCalls[0].script.contains('-v /host:/container') + } + + def "run() does not remove container when rm is false"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.run(image: 'myapp', rm: false) + + then: + !mock.shCalls[0].script.contains('--rm') + } + + def "compose() requires command parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.compose([:]) + + then: + def e = thrown(Exception) + e.message.contains('"command" parameter is required') + } + + def "compose() runs up command"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.compose(command: 'up') + + then: + mock.shCalls[0].script == 'docker-compose -f docker-compose.yml up' + } + + def "compose() uses custom compose file"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.compose(command: 'down', composeFile: 'docker-compose.prod.yml') + + then: + mock.shCalls[0].script == 'docker-compose -f docker-compose.prod.yml down' + } + + def "clean() runs container prune by default"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.clean() + + then: + mock.shCalls.any { it.script == 'docker container prune -f' } + } + + def "clean() prunes images with force flag when pruneAll is true"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.clean(pruneAll: true) + + then: + mock.shCalls.any { it.script == 'docker image prune -af' } + } + + def "clean() skips container prune when containers is false"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.clean(containers: false) + + then: + !mock.shCalls.any { it.script == 'docker container prune -f' } + } + + def "inspect() requires target parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.inspect([:]) + + then: + def e = thrown(Exception) + e.message.contains('"target" parameter is required') + } + + def "inspect() returns raw output"() { + given: + def mock = new MockScript() + mock.env['BUILD_NUMBER'] = '1' + def docker = new Docker(mock) + + when: + docker.inspect(target: 'myapp:latest') + + then: + mock.shCalls[0].script == "docker inspect --format '{{json .}}' myapp:latest" + mock.shCalls[0].returnStdout == true + } + + def "inspect() uses custom format"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.inspect(target: 'myapp:latest', format: '{{.Id}}') + + then: + mock.shCalls[0].script == "docker inspect --format '{{.Id}}' myapp:latest" + } + + def "loginEcr() requires region parameter"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.loginEcr([:]) + + then: + def e = thrown(Exception) + e.message.contains('"region" parameter is required') + } + + def "loginEcr() derives registry from accountId"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.loginEcr(region: 'us-east-1', accountId: '123456789012') + + then: + mock.shCalls[0].script.contains('123456789012.dkr.ecr.us-east-1.amazonaws.com') + } + + def "loginEcr() uses provided registry when accountId is absent"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.loginEcr(region: 'us-east-1', registry: '123456789012.dkr.ecr.us-east-1.amazonaws.com') + + then: + mock.shCalls[0].script.contains('123456789012.dkr.ecr.us-east-1.amazonaws.com') + } + + def "loginEcr() uses credentialsId when provided"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.loginEcr(region: 'us-east-1', credentialsId: 'aws-cred') + + then: + mock.withCredentialsCalls.size() == 1 + } + + def "loginGcr() uses default registry"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.loginGcr() + + then: + mock.shCalls[0].script == 'gcloud auth configure-docker gcr.io --quiet' + } + + def "loginGcr() uses custom registry"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.loginGcr(registry: 'us.gcr.io') + + then: + mock.shCalls[0].script == 'gcloud auth configure-docker us.gcr.io --quiet' + } + + def "loginGcr() uses credentialsId when provided"() { + given: + def mock = new MockScript() + def docker = new Docker(mock) + + when: + docker.loginGcr(credentialsId: 'gcr-cred') + + then: + mock.withCredentialsCalls.size() == 1 + } +} diff --git a/test/groovy/org/devops/GitHelperSpec.groovy b/test/groovy/org/devops/GitHelperSpec.groovy new file mode 100644 index 0000000..9613f5d --- /dev/null +++ b/test/groovy/org/devops/GitHelperSpec.groovy @@ -0,0 +1,138 @@ +package org.devops + +import spock.lang.Specification + +class GitHelperSpec extends Specification { + + def "checkout() uses default branch when no branchSpec provided"() { + given: + def mock = new MockScript() + def helper = new GitHelper(mock) + + when: + helper.checkout('https://github.com/org/repo.git') + + then: + mock.checkoutCalls.size() == 1 + mock.checkoutCalls[0].branches == [[name: 'main']] + mock.checkoutCalls[0].userRemoteConfigs == [[url: 'https://github.com/org/repo.git', credentialsId: '']] + } + + def "checkout() uses provided branchSpec"() { + given: + def mock = new MockScript() + def helper = new GitHelper(mock) + + when: + helper.checkout('https://github.com/org/repo.git', '', 'feature/my-branch') + + then: + mock.checkoutCalls[0].branches == [[name: 'feature/my-branch']] + } + + def "checkout() uses provided credentialsId"() { + given: + def mock = new MockScript() + def helper = new GitHelper(mock) + + when: + helper.checkout('https://github.com/org/repo.git', 'git-cred') + + then: + mock.checkoutCalls[0].userRemoteConfigs[0].credentialsId == 'git-cred' + } + + def "checkout() generates changelog by default"() { + given: + def mock = new MockScript() + def helper = new GitHelper(mock) + + when: + helper.checkout('https://github.com/org/repo.git') + + then: + mock.checkoutCalls.size() == 1 + } + + def "checkout() skips changelog when generateChangelog is false"() { + given: + def mock = new MockScript() + def helper = new GitHelper(mock) + + when: + helper.checkout('https://github.com/org/repo.git', '', null, false) + + then: + mock.checkoutCalls.size() == 1 + } + + def "checkout() throws on error and logs message"() { + given: + def mock = new MockScript() + def helper = new GitHelper(mock) + mock.checkoutCalls << null + + when: + helper.checkout('https://github.com/org/repo.git') + + then: + noExceptionThrown() + } + + def "tag() creates annotated tag"() { + given: + def mock = new MockScript() + def helper = new GitHelper(mock) + + when: + helper.tag('v1.0.0', 'release') + + then: + mock.shCalls.size() == 1 + mock.shCalls[0].script == "git tag -a v1.0.0 -m 'release'" + } + + def "tag() pushes to origin when credentialsId is provided"() { + given: + def mock = new MockScript() + def helper = new GitHelper(mock) + + when: + helper.tag('v1.0.0', 'release', 'git-cred') + + then: + mock.shCalls.size() == 2 + mock.shCalls[0].script == "git tag -a v1.0.0 -m 'release'" + mock.shCalls[1].script == 'git push origin v1.0.0' + mock.withCredentialsCalls.size() == 1 + } + + def "tag() does not push when credentialsId is empty"() { + given: + def mock = new MockScript() + def helper = new GitHelper(mock) + + when: + helper.tag('v1.0.0') + + then: + mock.shCalls.size() == 1 + mock.withCredentialsCalls.isEmpty() + } + + def "shortCommit() returns git rev-parse output"() { + given: + def mock = new MockScript() + mock.env['GIT_COMMIT'] = 'abc123def456' + def helper = new GitHelper(mock) + + when: + def result = helper.shortCommit() + + then: + result == '' + mock.shCalls.size() == 1 + mock.shCalls[0].script == 'git rev-parse --short HEAD' + mock.shCalls[0].returnStdout == true + } +} diff --git a/test/groovy/org/devops/MockScript.groovy b/test/groovy/org/devops/MockScript.groovy new file mode 100644 index 0000000..b361bb8 --- /dev/null +++ b/test/groovy/org/devops/MockScript.groovy @@ -0,0 +1,115 @@ +package org.devops + +class MockScript { + + Map env = [:] + String BUILD_NUMBER = '1' + String JOB_NAME = 'test-job' + String BUILD_URL = 'http://jenkins/job/test/1/' + String GIT_BRANCH = 'main' + String GIT_PREVIOUS_SUCCESSFUL_COMMIT = '' + def currentBuild = [result: 'SUCCESS'] + + List echoMessages = [] + List shCalls = [] + List slackSendCalls = [] + List mailCalls = [] + List junitPatterns = [] + List coberturaCalls = [] + List checkoutCalls = [] + List withCredentialsCalls = [] + List sleepCalls = [] + List fileExistsCalls = [] + List errorMessages = [] + List timeoutCalls = [] + List inputCalls = [] + + MockScript() { + env = [ + 'BUILD_NUMBER' : BUILD_NUMBER, + 'JOB_NAME' : JOB_NAME, + 'BUILD_URL' : BUILD_URL, + 'GIT_BRANCH' : GIT_BRANCH, + 'GIT_PREVIOUS_SUCCESSFUL_COMMIT': GIT_PREVIOUS_SUCCESSFUL_COMMIT, + ] + } + + void error(String message) { + errorMessages << message + throw new RuntimeException(message) + } + + Object sh(String script) { + return sh([script: script]) + } + + Object sh(Map config) { + shCalls << config + if (config.returnStdout) { + return config.returnValue ?: '' + } + return null + } + + void echo(String message) { + echoMessages << message + } + + void slackSend(Map config) { + slackSendCalls << config + } + + void mail(Map config) { + mailCalls << config + } + + void junit(String pattern) { + junitPatterns << pattern + } + + void junit(Map config) { + junitPatterns << config.testResults + } + + void cobertura(Map config) { + coberturaCalls << config + } + + void checkout(Map config) { + checkoutCalls << config + } + + void withCredentials(List bindings, Closure body) { + withCredentialsCalls << [bindings: bindings, body: body] + bindings.each { binding -> + if (binding.'$class' == 'UsernamePasswordMultiBinding') { + env[binding.usernameVariable] = 'test-user' + env[binding.passwordVariable] = 'test-pass' + } else if (binding.'$class' == 'AmazonWebServicesCredentialsBinding') { + env[binding.accessKeyVariable] = 'test-access-key' + env[binding.secretKeyVariable] = 'test-secret-key' + } else if (binding.'$class' == 'GoogleServiceAccountKey') { + env[binding.keyFileVariable] = '/tmp/gcloud-key.json' + } + } + body.call() + } + + void sleep(Map config) { + sleepCalls << config + } + + boolean fileExists(String path) { + fileExistsCalls << path + return false + } + + void timeout(Map config, Closure body) { + timeoutCalls << config + body.call() + } + + void input(Map config = [:]) { + inputCalls << config + } +} diff --git a/test/groovy/org/devops/NotificationSpec.groovy b/test/groovy/org/devops/NotificationSpec.groovy new file mode 100644 index 0000000..b0cc17c --- /dev/null +++ b/test/groovy/org/devops/NotificationSpec.groovy @@ -0,0 +1,139 @@ +package org.devops + +import spock.lang.Specification + +class NotificationSpec extends Specification { + + def "send() does nothing when notifyOnFailure is false and status is FAILURE"() { + given: + def mock = new MockScript() + def config = new Config([notifyOnFailure: false, notifyOnSuccess: false]) + def notifier = new Notification(mock) + + when: + notifier.send('FAILURE', 'build failed', config) + + then: + mock.slackSendCalls.isEmpty() + mock.mailCalls.isEmpty() + } + + def "send() sends slack and email on FAILURE when notifyOnFailure is true"() { + given: + def mock = new MockScript() + def config = new Config([notifyOnFailure: true, slackChannel: '#alerts', emailRecipients: 'team@example.com']) + def notifier = new Notification(mock) + + when: + notifier.send('FAILURE', 'build failed', config) + + then: + mock.slackSendCalls.size() == 1 + mock.slackSendCalls[0].channel == '#alerts' + mock.slackSendCalls[0].color == 'danger' + mock.slackSendCalls[0].message.contains('FAILURE') + mock.slackSendCalls[0].message.contains('build failed') + mock.mailCalls.size() == 1 + mock.mailCalls[0].to == 'team@example.com' + mock.mailCalls[0].subject.contains('FAILURE') + } + + def "send() sends slack and email on SUCCESS when notifyOnSuccess is true"() { + given: + def mock = new MockScript() + def config = new Config([notifyOnSuccess: true, slackChannel: '#builds', emailRecipients: 'team@example.com']) + def notifier = new Notification(mock) + + when: + notifier.send('SUCCESS', 'build succeeded', config) + + then: + mock.slackSendCalls.size() == 1 + mock.slackSendCalls[0].color == 'good' + mock.mailCalls.size() == 1 + } + + def "send() sends notifications for UNSTABLE status regardless of flags"() { + given: + def mock = new MockScript() + def config = new Config([notifyOnFailure: false, notifyOnSuccess: false]) + def notifier = new Notification(mock) + + when: + notifier.send('UNSTABLE', 'tests unstable', config) + + then: + mock.slackSendCalls.size() == 1 + mock.slackSendCalls[0].color == 'warning' + mock.mailCalls.size() == 1 + } + + def "send() sends notifications for ABORTED status regardless of flags"() { + given: + def mock = new MockScript() + def config = new Config([notifyOnFailure: false, notifyOnSuccess: false]) + def notifier = new Notification(mock) + + when: + notifier.send('ABORTED', 'build aborted', config) + + then: + mock.slackSendCalls.size() == 1 + mock.slackSendCalls[0].color == 'warning' + } + + def "send() skips slack when slackChannel is not set"() { + given: + def mock = new MockScript() + def config = new Config([notifyOnFailure: true, slackChannel: null, emailRecipients: 'team@example.com']) + def notifier = new Notification(mock) + + when: + notifier.send('FAILURE', 'build failed', config) + + then: + mock.slackSendCalls.isEmpty() + mock.mailCalls.size() == 1 + } + + def "send() skips email when emailRecipients is not set"() { + given: + def mock = new MockScript() + def config = new Config([notifyOnFailure: true, slackChannel: '#alerts', emailRecipients: '']) + def notifier = new Notification(mock) + + when: + notifier.send('FAILURE', 'build failed', config) + + then: + mock.slackSendCalls.size() == 1 + mock.mailCalls.isEmpty() + } + + def "colorForStatus() returns correct colors"() { + given: + def mock = new MockScript() + def notifier = new Notification(mock) + + expect: + notifier.colorForStatus('SUCCESS') == 'good' + notifier.colorForStatus('FAILURE') == 'danger' + notifier.colorForStatus('UNSTABLE') == 'warning' + notifier.colorForStatus('ABORTED') == 'warning' + notifier.colorForStatus('UNKNOWN') == 'good' + } + + def "send() includes job name and build number in message"() { + given: + def mock = new MockScript() + def config = new Config([notifyOnFailure: true, slackChannel: '#alerts', emailRecipients: 'team@example.com']) + def notifier = new Notification(mock) + + when: + notifier.send('FAILURE', 'build failed', config) + + then: + mock.slackSendCalls[0].message.contains('test-job') + mock.slackSendCalls[0].message.contains('1') + } +} diff --git a/test/groovy/org/devops/UtilsSpec.groovy b/test/groovy/org/devops/UtilsSpec.groovy new file mode 100644 index 0000000..9985451 --- /dev/null +++ b/test/groovy/org/devops/UtilsSpec.groovy @@ -0,0 +1,159 @@ +package org.devops + +import spock.lang.Specification + +class UtilsSpec extends Specification { + + def "info() captures echo message with prefix"() { + given: + def mock = new MockScript() + def utils = new Utils(mock) + + when: + utils.info("hello world") + + then: + mock.echoMessages == ["[devops-utils] hello world"] + } + + def "warn() captures echo message with warning prefix"() { + given: + def mock = new MockScript() + def utils = new Utils(mock) + + when: + utils.warn("something looks wrong") + + then: + mock.echoMessages == ["[devops-utils] WARNING: something looks wrong"] + } + + def "error() captures echo message with error prefix"() { + given: + def mock = new MockScript() + def utils = new Utils(mock) + + when: + utils.error("something broke") + + then: + mock.echoMessages == ["[devops-utils] ERROR: something broke"] + } + + def "time() returns closure result and logs duration"() { + given: + def mock = new MockScript() + def utils = new Utils(mock) + + when: + def result = utils.time("myLabel") { 42 } + + then: + result == 42 + mock.echoMessages.size() == 1 + mock.echoMessages[0].startsWith("[devops-utils] myLabel completed in ") + mock.echoMessages[0].endsWith("ms") + } + + def "time() rethrows exception from closure"() { + given: + def mock = new MockScript() + def utils = new Utils(mock) + + when: + utils.time("myLabel") { throw new RuntimeException("boom") } + + then: + def e = thrown(RuntimeException) + e.message == "boom" + } + + def "retry() succeeds on first attempt"() { + given: + def mock = new MockScript() + def utils = new Utils(mock) + def attempts = 0 + + when: + def result = utils.retry(3, 0) { + attempts++ + "success" + } + + then: + result == "success" + attempts == 1 + } + + def "retry() retries on failure and succeeds on later attempt"() { + given: + def mock = new MockScript() + def utils = new Utils(mock) + def attempts = 0 + + when: + def result = utils.retry(3, 0) { + attempts++ + if (attempts < 2) throw new RuntimeException("not yet") + "success" + } + + then: + result == "success" + attempts == 2 + mock.echoMessages.any { it.contains("Attempt 1 of 3 failed") } + } + + def "retry() throws last error after exhausting attempts"() { + given: + def mock = new MockScript() + def utils = new Utils(mock) + + when: + utils.retry(3, 0) { throw new RuntimeException("always fails") } + + then: + def e = thrown(RuntimeException) + e.message == "always fails" + mock.echoMessages.size() == 2 + } + + def "retry() with delay calls sleep between attempts"() { + given: + def mock = new MockScript() + def utils = new Utils(mock) + def attempts = 0 + + when: + utils.retry(2, 5000) { + attempts++ + if (attempts < 2) throw new RuntimeException("not yet") + "success" + } + + then: + mock.sleepCalls.size() == 1 + mock.sleepCalls[0].time == 5 + mock.sleepCalls[0].unit == 'SECONDS' + } + + def "get() returns value from map"() { + expect: + Utils.get([key: "value"], "key") == "value" + } + + def "get() returns default when key missing"() { + expect: + Utils.get([:], "key", "default") == "default" + } + + def "get() returns null when key missing and no default"() { + expect: + Utils.get([:], "key") == null + } + + def "get() handles null map gracefully"() { + expect: + Utils.get(null, "key", "default") == "default" + } +} diff --git a/test/groovy/org/devops/VarsTestHelper.groovy b/test/groovy/org/devops/VarsTestHelper.groovy new file mode 100644 index 0000000..4d6ba51 --- /dev/null +++ b/test/groovy/org/devops/VarsTestHelper.groovy @@ -0,0 +1,26 @@ +package org.devops + +import groovy.lang.GroovyClassLoader +import groovy.lang.GroovyObjectSupport + +class VarsTestHelper { + + static MockScript loadVars(String varsFilePath, Map args = [:]) { + def mockScript = new MockScript() + def classLoader = new GroovyClassLoader() + classLoader.addClasspath('src') + def scriptClass = classLoader.parseClass(new File(varsFilePath)) + def script = scriptClass.newInstance() + + mockScript.metaClass.methods.each { method -> + if (method.name != 'getClass' && method.name != 'getMetaClass' && !method.name.startsWith('$')) { + script.metaClass."$method.name" = { Object... methodArgs -> + mockScript."$method.name"(*methodArgs) + } + } + } + + script.call(args) + return mockScript + } +} diff --git a/test/groovy/vars/buildAppSpec.groovy b/test/groovy/vars/buildAppSpec.groovy new file mode 100644 index 0000000..63cfaef --- /dev/null +++ b/test/groovy/vars/buildAppSpec.groovy @@ -0,0 +1,27 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class buildAppSpec extends Specification { + + def "buildApp delegates to BuildHelper.build() with maven"() { + given: + def mock = VarsTestHelper.loadVars('vars/buildApp.groovy', [tool: 'maven', goals: 'clean package']) + + then: + noExceptionThrown() + mock.shCalls.size() == 1 + mock.shCalls[0].script.contains('mvn') + } + + def "buildApp delegates to BuildHelper.build() with gradle"() { + given: + def mock = VarsTestHelper.loadVars('vars/buildApp.groovy', [tool: 'gradle', goals: 'clean build']) + + then: + noExceptionThrown() + mock.shCalls.size() == 1 + mock.shCalls[0].script.contains('./gradlew') + } +} diff --git a/test/groovy/vars/checkoutCodeSpec.groovy b/test/groovy/vars/checkoutCodeSpec.groovy new file mode 100644 index 0000000..61f0b6e --- /dev/null +++ b/test/groovy/vars/checkoutCodeSpec.groovy @@ -0,0 +1,29 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class checkoutCodeSpec extends Specification { + + def "checkoutCode delegates to GitHelper.checkout() with url"() { + given: + def mock = VarsTestHelper.loadVars('vars/checkoutCode.groovy', [url: 'https://github.com/org/repo.git']) + + then: + noExceptionThrown() + mock.checkoutCalls.size() == 1 + mock.checkoutCalls[0].userRemoteConfigs[0].url == 'https://github.com/org/repo.git' + } + + def "checkoutCode throws error when url is missing"() { + given: + VarsTestHelper.loadVars('vars/checkoutCode.groovy', [:]) + + when: + checkoutCode() + + then: + def e = thrown(RuntimeException) + e.message.contains('"url" parameter is required') + } +} diff --git a/test/groovy/vars/deploySpec.groovy b/test/groovy/vars/deploySpec.groovy new file mode 100644 index 0000000..43434a5 --- /dev/null +++ b/test/groovy/vars/deploySpec.groovy @@ -0,0 +1,28 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class deploySpec extends Specification { + + def "deploy delegates to sh and Notification.send()"() { + given: + def mock = VarsTestHelper.loadVars('vars/deploy.groovy', [command: './scripts/deploy.sh staging']) + + then: + noExceptionThrown() + mock.shCalls.size() >= 1 + } + + def "deploy throws error when command is missing"() { + given: + VarsTestHelper.loadVars('vars/deploy.groovy', [:]) + + when: + deploy() + + then: + def e = thrown(RuntimeException) + e.message.contains('"command" parameter is required') + } +} diff --git a/test/groovy/vars/dockerBuildSpec.groovy b/test/groovy/vars/dockerBuildSpec.groovy new file mode 100644 index 0000000..e170b0d --- /dev/null +++ b/test/groovy/vars/dockerBuildSpec.groovy @@ -0,0 +1,28 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerBuildSpec extends Specification { + + def "dockerBuild delegates to Docker.build()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerBuild.groovy', [image: 'myapp', tags: ['latest']]) + + then: + noExceptionThrown() + mock.shCalls.size() == 1 + } + + def "dockerBuild throws error when image is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerBuild.groovy', [:]) + + when: + dockerBuild([:]) + + then: + def e = thrown(RuntimeException) + e.message.contains('"image" parameter is required') + } +} diff --git a/test/groovy/vars/dockerCleanSpec.groovy b/test/groovy/vars/dockerCleanSpec.groovy new file mode 100644 index 0000000..6cb1270 --- /dev/null +++ b/test/groovy/vars/dockerCleanSpec.groovy @@ -0,0 +1,16 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerCleanSpec extends Specification { + + def "dockerClean delegates to Docker.clean()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerClean.groovy', [:]) + + then: + noExceptionThrown() + mock.shCalls.any { it.script == 'docker container prune -f' } + } +} diff --git a/test/groovy/vars/dockerComposeSpec.groovy b/test/groovy/vars/dockerComposeSpec.groovy new file mode 100644 index 0000000..85758bc --- /dev/null +++ b/test/groovy/vars/dockerComposeSpec.groovy @@ -0,0 +1,28 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerComposeSpec extends Specification { + + def "dockerCompose delegates to Docker.compose()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerCompose.groovy', [command: 'up']) + + then: + noExceptionThrown() + mock.shCalls[0].script == 'docker-compose -f docker-compose.yml up' + } + + def "dockerCompose throws error when command is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerCompose.groovy', [:]) + + when: + dockerCompose([:]) + + then: + def e = thrown(RuntimeException) + e.message.contains('"command" parameter is required') + } +} diff --git a/test/groovy/vars/dockerInspectSpec.groovy b/test/groovy/vars/dockerInspectSpec.groovy new file mode 100644 index 0000000..34c57ae --- /dev/null +++ b/test/groovy/vars/dockerInspectSpec.groovy @@ -0,0 +1,28 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerInspectSpec extends Specification { + + def "dockerInspect delegates to Docker.inspect()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerInspect.groovy', [target: 'myapp:latest']) + + then: + noExceptionThrown() + mock.shCalls[0].returnStdout == true + } + + def "dockerInspect throws error when target is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerInspect.groovy', [:]) + + when: + dockerInspect([:]) + + then: + def e = thrown(RuntimeException) + e.message.contains('"target" parameter is required') + } +} diff --git a/test/groovy/vars/dockerLoginEcrSpec.groovy b/test/groovy/vars/dockerLoginEcrSpec.groovy new file mode 100644 index 0000000..60245bb --- /dev/null +++ b/test/groovy/vars/dockerLoginEcrSpec.groovy @@ -0,0 +1,28 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerLoginEcrSpec extends Specification { + + def "dockerLoginEcr delegates to Docker.loginEcr()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerLoginEcr.groovy', [region: 'us-east-1']) + + then: + noExceptionThrown() + mock.shCalls[0].script.contains('aws ecr get-login-password') + } + + def "dockerLoginEcr throws error when region is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerLoginEcr.groovy', [:]) + + when: + dockerLoginEcr([:]) + + then: + def e = thrown(RuntimeException) + e.message.contains('"region" parameter is required') + } +} diff --git a/test/groovy/vars/dockerLoginGcrSpec.groovy b/test/groovy/vars/dockerLoginGcrSpec.groovy new file mode 100644 index 0000000..f4971e3 --- /dev/null +++ b/test/groovy/vars/dockerLoginGcrSpec.groovy @@ -0,0 +1,16 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerLoginGcrSpec extends Specification { + + def "dockerLoginGcr delegates to Docker.loginGcr()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerLoginGcr.groovy', [:]) + + then: + noExceptionThrown() + mock.shCalls[0].script == 'gcloud auth configure-docker gcr.io --quiet' + } +} diff --git a/test/groovy/vars/dockerLoginSpec.groovy b/test/groovy/vars/dockerLoginSpec.groovy new file mode 100644 index 0000000..c49a53e --- /dev/null +++ b/test/groovy/vars/dockerLoginSpec.groovy @@ -0,0 +1,28 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerLoginSpec extends Specification { + + def "dockerLogin delegates to Docker.login()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerLogin.groovy', [registry: 'registry.example.com']) + + then: + noExceptionThrown() + mock.shCalls[0].script == 'docker login registry.example.com' + } + + def "dockerLogin throws error when registry is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerLogin.groovy', [:]) + + when: + dockerLogin([:]) + + then: + def e = thrown(RuntimeException) + e.message.contains('"registry" parameter is required') + } +} diff --git a/test/groovy/vars/dockerLogoutSpec.groovy b/test/groovy/vars/dockerLogoutSpec.groovy new file mode 100644 index 0000000..031c3a7 --- /dev/null +++ b/test/groovy/vars/dockerLogoutSpec.groovy @@ -0,0 +1,16 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerLogoutSpec extends Specification { + + def "dockerLogout delegates to Docker.logout()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerLogout.groovy', [:]) + + then: + noExceptionThrown() + mock.shCalls[0].script == 'docker logout https://index.docker.io/v1/' + } +} diff --git a/test/groovy/vars/dockerPushSpec.groovy b/test/groovy/vars/dockerPushSpec.groovy new file mode 100644 index 0000000..3af8a9f --- /dev/null +++ b/test/groovy/vars/dockerPushSpec.groovy @@ -0,0 +1,40 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerPushSpec extends Specification { + + def "dockerPush delegates to Docker.push()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerPush.groovy', [image: 'myapp', registry: 'registry.example.com']) + + then: + noExceptionThrown() + mock.shCalls[0].script == 'docker push registry.example.com/myapp:latest' + } + + def "dockerPush throws error when image is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerPush.groovy', [:]) + + when: + dockerPush([:]) + + then: + def e = thrown(RuntimeException) + e.message.contains('"image" parameter is required') + } + + def "dockerPush throws error when registry is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerPush.groovy', [image: 'myapp']) + + when: + dockerPush(image: 'myapp') + + then: + def e = thrown(RuntimeException) + e.message.contains('"registry" parameter is required') + } +} diff --git a/test/groovy/vars/dockerRunSpec.groovy b/test/groovy/vars/dockerRunSpec.groovy new file mode 100644 index 0000000..8b8007f --- /dev/null +++ b/test/groovy/vars/dockerRunSpec.groovy @@ -0,0 +1,28 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerRunSpec extends Specification { + + def "dockerRun delegates to Docker.run()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerRun.groovy', [image: 'myapp']) + + then: + noExceptionThrown() + mock.shCalls[0].script.contains('docker run --rm myapp') + } + + def "dockerRun throws error when image is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerRun.groovy', [:]) + + when: + dockerRun([:]) + + then: + def e = thrown(RuntimeException) + e.message.contains('"image" parameter is required') + } +} diff --git a/test/groovy/vars/dockerTagSpec.groovy b/test/groovy/vars/dockerTagSpec.groovy new file mode 100644 index 0000000..f48052a --- /dev/null +++ b/test/groovy/vars/dockerTagSpec.groovy @@ -0,0 +1,40 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class dockerTagSpec extends Specification { + + def "dockerTag delegates to Docker.tag()"() { + given: + def mock = VarsTestHelper.loadVars('vars/dockerTag.groovy', [image: 'myapp', tag: 'v1']) + + then: + noExceptionThrown() + mock.shCalls[0].script == 'docker tag myapp:latest myapp:v1' + } + + def "dockerTag throws error when image is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerTag.groovy', [:]) + + when: + dockerTag([:]) + + then: + def e = thrown(RuntimeException) + e.message.contains('"image" parameter is required') + } + + def "dockerTag throws error when tag is missing"() { + given: + VarsTestHelper.loadVars('vars/dockerTag.groovy', [image: 'myapp']) + + when: + dockerTag(image: 'myapp') + + then: + def e = thrown(RuntimeException) + e.message.contains('"tag" parameter is required') + } +} diff --git a/test/groovy/vars/notifySpec.groovy b/test/groovy/vars/notifySpec.groovy new file mode 100644 index 0000000..180ba9b --- /dev/null +++ b/test/groovy/vars/notifySpec.groovy @@ -0,0 +1,26 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class notifySpec extends Specification { + + def "notify delegates to Notification.send()"() { + given: + def mock = VarsTestHelper.loadVars('vars/notify.groovy', [status: 'SUCCESS', message: 'Build succeeded']) + + then: + noExceptionThrown() + } + + def "notify with string args delegates to map call"() { + given: + def mock = VarsTestHelper.loadVars('vars/notify.groovy', [:]) + + when: + notify('SUCCESS', 'Build succeeded') + + then: + noExceptionThrown() + } +} diff --git a/test/groovy/vars/runTestsSpec.groovy b/test/groovy/vars/runTestsSpec.groovy new file mode 100644 index 0000000..e05193f --- /dev/null +++ b/test/groovy/vars/runTestsSpec.groovy @@ -0,0 +1,16 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class runTestsSpec extends Specification { + + def "runTests delegates to BuildHelper.test()"() { + given: + def mock = VarsTestHelper.loadVars('vars/runTests.groovy', [tool: 'maven', goals: 'test']) + + then: + noExceptionThrown() + mock.shCalls.size() >= 1 + } +} diff --git a/test/groovy/vars/waitForInputSpec.groovy b/test/groovy/vars/waitForInputSpec.groovy new file mode 100644 index 0000000..d9f1085 --- /dev/null +++ b/test/groovy/vars/waitForInputSpec.groovy @@ -0,0 +1,28 @@ +package vars + +import org.devops.VarsTestHelper +import spock.lang.Specification + +class waitForInputSpec extends Specification { + + def "waitForInput delegates to timeout and input steps"() { + given: + def mock = VarsTestHelper.loadVars('vars/waitForInput.groovy', [message: 'Approve deploy?', timeoutMinutes: 30]) + + then: + noExceptionThrown() + mock.timeoutCalls.size() == 1 + mock.timeoutCalls[0].time == 30 + mock.timeoutCalls[0].unit == 'MINUTES' + mock.inputCalls.size() == 1 + mock.inputCalls[0].message == 'Approve deploy?' + } + + def "waitForInput uses default message when none provided"() { + given: + def mock = VarsTestHelper.loadVars('vars/waitForInput.groovy', [timeoutMinutes: 60]) + + then: + mock.inputCalls[0].message == 'Please approve to continue' + } +} diff --git a/vars/buildApp.groovy b/vars/buildApp.groovy new file mode 100644 index 0000000..b08fcc0 --- /dev/null +++ b/vars/buildApp.groovy @@ -0,0 +1,30 @@ +import org.devops.BuildHelper +import org.devops.Config + +/** + * Build the project using the configured build tool (Maven or Gradle). + * + * Usage: + * buildApp tool: 'maven', goals: 'clean install' + * buildApp tool: 'gradle', goals: 'assemble', extraArgs: ['--parallel'] + * + * @param config map with: + * - tool : 'maven' (default) or 'gradle' + * - goals : build goals/phases + * - extraArgs : list of extra CLI args + * - retryAttempts : retries (default 1) + * - retryDelaySeconds: retry delay (default 0) + * - failOnError : fail build on errors (default true) + */ +def call(Map config = [:]) { + def cfg = new Config([ + buildTool : config.tool ?: Config.DEFAULTS.buildTool, + retryAttempts : config.retryAttempts, + retryDelaySeconds : config.retryDelaySeconds, + failOnError : config.failOnError, + extraTestArgs : config.extraTestArgs, + ]) + + def helper = new BuildHelper(this) + helper.build(cfg, config.goals, config.extraArgs ?: []) +} diff --git a/vars/checkoutCode.groovy b/vars/checkoutCode.groovy new file mode 100644 index 0000000..ffc6252 --- /dev/null +++ b/vars/checkoutCode.groovy @@ -0,0 +1,40 @@ +import org.devops.Config +import org.devops.GitHelper +import org.devops.Utils + +/** + * Checkout source code with retry support and optional changelog. + * + * Usage: + * checkoutCode url: 'https://github.com/org/repo.git', credentialsId: 'git-creds' + * checkoutCode url: '...', branch: 'develop', changelog: true + * + * @param config map with: + * - url : repository URL + * - credentialsId : Jenkins credentials ID (optional) + * - branch : branch/tag/ref to checkout (default 'main') + * - changelog : generate changelog (default true) + * - retryAttempts : number of retry attempts (default 1) + * - retryDelaySeconds: delay between retries (default 0) + */ +def call(Map config = [:]) { + def cfg = new Config([ + gitCredentialsId : config.credentialsId, + branch : config.branch, + retryAttempts : config.retryAttempts, + retryDelaySeconds : config.retryDelaySeconds, + ]) + + def url = config.url + if (!url) { + error('checkoutCode: "url" parameter is required') + } + + def helper = new GitHelper(this) + def utils = new Utils(this) + + utils.retry(cfg.getAs('retryAttempts', Integer, 1) as int, + (cfg.getAs('retryDelaySeconds', Integer, 0) * 1000L) as long) { + helper.checkout(url, cfg.get('gitCredentialsId', ''), cfg.get('branch'), config.changelog != false) + } +} diff --git a/vars/deploy.groovy b/vars/deploy.groovy new file mode 100644 index 0000000..d301f8d --- /dev/null +++ b/vars/deploy.groovy @@ -0,0 +1,62 @@ +import org.devops.Config +import org.devops.GitHelper +import org.devops.Notification + +/** + * Generic deploy helper. Executes the provided deploy command/script, + * tags git on success, and notifies on completion. + * + * Usage: + * deploy( + * command: './scripts/deploy.sh staging', + * version: '1.2.3', + * credentialsId: 'deploy-creds' + * ) + * + * @param config map with: + * - command : shell command(s) to run for deployment + * - version : version string used for git tag (optional) + * - tag : whether to create a git tag (default true when version set) + * - credentialsId : credentials for pushing tags (optional) + * - url : git repo URL for tagging (optional) + * - slackChannel : notification override + * - emailRecipients: notification override + */ +def call(Map config = [:]) { + def cfg = new Config([ + gitCredentialsId : config.credentialsId, + slackChannel : config.slackChannel, + emailRecipients : config.emailRecipients, + ]) + + def command = config.command + if (!command) { + error('deploy: "command" parameter is required') + } + + def utils = new org.devops.Utils(this) + def start = System.currentTimeMillis() + + try { + utils.time('Deploy', { + sh command + } as Closure) + + if (config.version && config.tag != false) { + def gitUrl = config.url + if (gitUrl) { + new GitHelper(this).tag( + config.version, + "Release ${config.version}", + cfg.get('gitCredentialsId', ''), + ) + } + } + + def elapsed = System.currentTimeMillis() - start + new Notification(this).send('SUCCESS', "Deploy succeeded in ${elapsed}ms", cfg) + } catch (Exception e) { + new Notification(this).send('FAILURE', "Deploy failed: ${e.message}", cfg) + throw e + } +} diff --git a/vars/dockerBuild.groovy b/vars/dockerBuild.groovy new file mode 100644 index 0000000..61a909c --- /dev/null +++ b/vars/dockerBuild.groovy @@ -0,0 +1,33 @@ +import org.devops.Docker + +/** + * Build a Docker image with optional tags, build args, and registry login. + * + * Usage: + * dockerBuild image: 'myapp', tags: ['1.0.0', 'latest'] + * dockerBuild image: 'myapp', registry: 'registry.example.com', registryCredentialsId: 'docker-cred' + * dockerBuild image: 'myapp', buildArgs: [JAVA_VERSION: '17'], noCache: true + * + * @param config map with: + * - image : image name (required) + * - tags : list of tags (default ['latest']) + * - buildArgs : map of build arguments + * - registry : registry URL (optional) + * - registryCredentialsId : credentials for registry login (optional) + * - dockerfile : path to Dockerfile (default 'Dockerfile') + * - buildContext : build context path (default '.') + * - noCache : use --no-cache (default false) + * - pull : use --pull (default false) + * - retryAttempts : retry count (default 1) + * - retryDelaySeconds : retry delay in seconds (default 0) + * - failOnError : fail on build error (default true) + */ +def call(Map config = [:]) { + def image = config.image + if (!image) { + error('dockerBuild: "image" parameter is required') + } + + def docker = new Docker(this) + docker.build(config) +} diff --git a/vars/dockerClean.groovy b/vars/dockerClean.groovy new file mode 100644 index 0000000..dd985bb --- /dev/null +++ b/vars/dockerClean.groovy @@ -0,0 +1,23 @@ +import org.devops.Docker + +/** + * Clean up Docker resources (containers, images, volumes, networks, build cache). + * + * Usage: + * dockerClean + * dockerClean containers: false, images: true, volumes: true + * dockerClean buildCache: true, pruneAll: true + * + * @param config map with: + * - containers : remove stopped containers (default true) + * - images : remove dangling images (default true) + * - volumes : remove unused volumes (default true) + * - networks : remove unused networks (default true) + * - buildCache : prune build cache (default false) + * - force : skip confirmation prompt (default true) + * - pruneAll : remove all unused images, not just dangling (default false) + */ +def call(Map config = [:]) { + def docker = new Docker(this) + docker.clean(config) +} diff --git a/vars/dockerCompose.groovy b/vars/dockerCompose.groovy new file mode 100644 index 0000000..d7b4136 --- /dev/null +++ b/vars/dockerCompose.groovy @@ -0,0 +1,27 @@ +import org.devops.Docker + +/** + * Execute a docker-compose command. + * + * Usage: + * dockerCompose command: 'up', args: '-d' + * dockerCompose command: 'down', composeFile: 'docker-compose.prod.yml' + * dockerCompose command: 'build', env: [BUILD_NUMBER: env.BUILD_NUMBER] + * + * @param config map with: + * - command : compose command (up, down, build, etc.) (required) + * - composeFile : path to docker-compose file (default 'docker-compose.yml') + * - args : additional compose arguments (optional) + * - env : map of environment variables (optional) + * - retryAttempts : retry count (default 1) + * - retryDelaySeconds : retry delay in seconds (default 0) + */ +def call(Map config = [:]) { + def command = config.command + if (!command) { + error('dockerCompose: "command" parameter is required') + } + + def docker = new Docker(this) + docker.compose(config) +} diff --git a/vars/dockerInspect.groovy b/vars/dockerInspect.groovy new file mode 100644 index 0000000..9e517f2 --- /dev/null +++ b/vars/dockerInspect.groovy @@ -0,0 +1,22 @@ +import org.devops.Docker + +/** + * Inspect a Docker image or container and return the output. + * + * Usage: + * dockerInspect target: 'myapp:latest' + * dockerInspect target: 'myapp:latest', format: '{{.Id}}' + * + * @param config map with: + * - target : image or container name/tag (required) + * - format : output format (optional, default '{{json .}}') + */ +def call(Map config = [:]) { + def target = config.target + if (!target) { + error('dockerInspect: "target" parameter is required') + } + + def docker = new Docker(this) + return docker.inspect(config) +} diff --git a/vars/dockerLogin.groovy b/vars/dockerLogin.groovy new file mode 100644 index 0000000..2ea1c86 --- /dev/null +++ b/vars/dockerLogin.groovy @@ -0,0 +1,24 @@ +import org.devops.Docker + +/** + * Login to a Docker registry. + * + * Usage: + * dockerLogin registry: 'registry.example.com', credentialsId: 'docker-cred' + * dockerLogin registry: 'registry.example.com', username: 'user', password: 'pass' + * + * @param config map with: + * - registry : registry URL (required) + * - username : registry username (optional, uses credentials if provided) + * - password : registry password (optional, uses credentials if provided) + * - credentialsId : Jenkins credentials ID for registry auth + */ +def call(Map config = [:]) { + def registry = config.registry + if (!registry) { + error('dockerLogin: "registry" parameter is required') + } + + def docker = new Docker(this) + docker.login(config) +} diff --git a/vars/dockerLoginEcr.groovy b/vars/dockerLoginEcr.groovy new file mode 100644 index 0000000..5ece50e --- /dev/null +++ b/vars/dockerLoginEcr.groovy @@ -0,0 +1,24 @@ +import org.devops.Docker + +/** + * Login to AWS Elastic Container Registry (ECR). + * + * Usage: + * dockerLoginEcr region: 'us-east-1', accountId: '123456789012' + * dockerLoginEcr region: 'us-east-1', registry: '123456789012.dkr.ecr.us-east-1.amazonaws.com', credentialsId: 'aws-creds' + * + * @param config map with: + * - region : AWS region (required) + * - accountId : AWS account ID (optional, used to derive registry) + * - registry : ECR registry URI (optional, auto-derived if accountId provided) + * - credentialsId : Jenkins credentials ID for AWS access keys (optional) + */ +def call(Map config = [:]) { + def region = config.region + if (!region) { + error('dockerLoginEcr: "region" parameter is required') + } + + def docker = new Docker(this) + docker.loginEcr(config) +} diff --git a/vars/dockerLoginGcr.groovy b/vars/dockerLoginGcr.groovy new file mode 100644 index 0000000..e182998 --- /dev/null +++ b/vars/dockerLoginGcr.groovy @@ -0,0 +1,19 @@ +import org.devops.Docker + +/** + * Login to Google Container Registry (GCR). + * + * Usage: + * dockerLoginGcr credentialsId: 'gcr-service-account' + * dockerLoginGcr project: 'my-gcp-project' + * dockerLoginGcr registry: 'us.gcr.io', credentialsId: 'gcr-service-account' + * + * @param config map with: + * - credentialsId : Jenkins credentials ID for GCR service account key (optional) + * - project : GCP project ID (optional) + * - registry : GCR registry URL (default 'gcr.io') + */ +def call(Map config = [:]) { + def docker = new Docker(this) + docker.loginGcr(config) +} diff --git a/vars/dockerLogout.groovy b/vars/dockerLogout.groovy new file mode 100644 index 0000000..b4e75f9 --- /dev/null +++ b/vars/dockerLogout.groovy @@ -0,0 +1,16 @@ +import org.devops.Docker + +/** + * Logout from a Docker registry. + * + * Usage: + * dockerLogout registry: 'registry.example.com' + * dockerLogout + * + * @param config map with: + * - registry : registry URL (default Docker Hub) + */ +def call(Map config = [:]) { + def docker = new Docker(this) + docker.logout(config) +} diff --git a/vars/dockerPush.groovy b/vars/dockerPush.groovy new file mode 100644 index 0000000..7d0b429 --- /dev/null +++ b/vars/dockerPush.groovy @@ -0,0 +1,31 @@ +import org.devops.Docker + +/** + * Push a Docker image tag to a registry. + * + * Usage: + * dockerPush image: 'myapp', registry: 'registry.example.com', tag: '1.0.0' + * dockerPush image: 'myapp', registry: 'registry.example.com', registryCredentialsId: 'docker-cred' + * + * @param config map with: + * - image : image name (required) + * - tag : tag to push (default 'latest') + * - registry : registry URL (required for push) + * - registryCredentialsId : credentials for registry login (optional) + * - retryAttempts : retry count (default 1) + * - retryDelaySeconds : retry delay in seconds (default 0) + */ +def call(Map config = [:]) { + def image = config.image + if (!image) { + error('dockerPush: "image" parameter is required') + } + + def registry = config.registry + if (!registry) { + error('dockerPush: "registry" parameter is required') + } + + def docker = new Docker(this) + docker.push(config) +} diff --git a/vars/dockerRun.groovy b/vars/dockerRun.groovy new file mode 100644 index 0000000..9065146 --- /dev/null +++ b/vars/dockerRun.groovy @@ -0,0 +1,32 @@ +import org.devops.Docker + +/** + * Run a Docker container. + * + * Usage: + * dockerRun image: 'myapp', command: 'npm start', detached: true + * dockerRun image: 'myapp', ports: ['8080:8080'], volumes: ['/data:/app/data'] + * dockerRun image: 'myapp', env: [NODE_ENV: 'production'], rm: true + * + * @param config map with: + * - image : image to run (required) + * - command : command to run inside the container (optional) + * - args : additional docker run flags (optional) + * - detached : run in detached mode (default false) + * - env : map of environment variables (optional) + * - ports : list of port mappings (optional) + * - volumes : list of volume mounts (optional) + * - rm : remove container after exit (default true) + * - name : container name (optional) + * - retryAttempts : retry count (default 1) + * - retryDelaySeconds : retry delay in seconds (default 0) + */ +def call(Map config = [:]) { + def image = config.image + if (!image) { + error('dockerRun: "image" parameter is required') + } + + def docker = new Docker(this) + docker.run(config) +} diff --git a/vars/dockerTag.groovy b/vars/dockerTag.groovy new file mode 100644 index 0000000..c473cea --- /dev/null +++ b/vars/dockerTag.groovy @@ -0,0 +1,28 @@ +import org.devops.Docker + +/** + * Tag a Docker image with a new tag. + * + * Usage: + * dockerTag image: 'myapp', tag: '1.0.0' + * dockerTag image: 'myapp', tag: '1.0.0', registry: 'registry.example.com' + * + * @param config map with: + * - image : source image name (required) + * - tag : new tag (required) + * - registry : registry URL (optional) + */ +def call(Map config = [:]) { + def image = config.image + if (!image) { + error('dockerTag: "image" parameter is required') + } + + def tag = config.tag + if (!tag) { + error('dockerTag: "tag" parameter is required') + } + + def docker = new Docker(this) + docker.tag(config) +} diff --git a/vars/notify.groovy b/vars/notify.groovy new file mode 100644 index 0000000..54e2370 --- /dev/null +++ b/vars/notify.groovy @@ -0,0 +1,35 @@ +import org.devops.Config +import org.devops.Notification + +/** + * Send a notification (Slack and/or email) about the current build status. + * + * Usage: + * notify message: 'Build completed', status: 'SUCCESS' + * notify status: currentBuild.result ?: 'SUCCESS', message: 'Done' + * + * @param config optional map overriding defaults: + * - status : SUCCESS | FAILURE | UNSTABLE | ABORTED + * - message : message body + * - slackChannel : override default Slack channel + * - emailRecipients : override default email recipients + */ +def call(Map config = [:]) { + def status = config.status ?: (currentBuild?.result ?: 'SUCCESS') + def message = config.message ?: "Build ${status}" + + def merged = new Config([ + slackChannel : config.slackChannel, + emailRecipients : config.emailRecipients, + notifyOnFailure : config.notifyOnFailure != null ? config.notifyOnFailure : Config.DEFAULTS.notifyOnFailure, + notifyOnSuccess : config.notifyOnSuccess != null ? config.notifyOnSuccess : Config.DEFAULTS.notifyOnSuccess, + slackServer : config.slackServer, + ]) + + def notifier = new Notification(this) + notifier.send(status, message, merged) +} + +def call(String status, String message) { + call([status: status, message: message]) +} diff --git a/vars/runTests.groovy b/vars/runTests.groovy new file mode 100644 index 0000000..56aecdd --- /dev/null +++ b/vars/runTests.groovy @@ -0,0 +1,24 @@ +import org.devops.BuildHelper +import org.devops.Config + +/** + * Run tests, publish JUnit reports, and publish coverage when available. + * + * Usage: + * runTests tool: 'maven' + * runTests tool: 'gradle', goals: 'check' + * + * @param config map with: + * - tool : 'maven' (default) or 'gradle' + * - goals : test goals + * - extraTestArgs : extra CLI args for the test phase + */ +def call(Map config = [:]) { + def cfg = new Config([ + buildTool : config.tool ?: Config.DEFAULTS.buildTool, + extraTestArgs: config.extraTestArgs, + ]) + + def helper = new BuildHelper(this) + helper.test(cfg, config.goals) +} diff --git a/vars/waitForInput.groovy b/vars/waitForInput.groovy new file mode 100644 index 0000000..05a684b --- /dev/null +++ b/vars/waitForInput.groovy @@ -0,0 +1,32 @@ +import org.devops.Config + +/** + * Pause the pipeline for manual approval with an optional timeout. + * + * Usage: + * waitForInput message: 'Approve production deploy?', timeoutMinutes: 1440 + * waitForInput message: 'Safe to proceed?' + * + * @param config map with: + * - message : prompt shown to the user + * - ok : label for the approval button (default 'Approve') + * - submitter : user/group allowed to approve (optional) + * - timeoutMinutes : max wait before failing (default 1440 = 24h) + * - abortOnTimeout : fail the build on timeout (default true) + */ +def call(Map config = [:]) { + def cfg = new Config([ + timeoutMinutes: config.timeoutMinutes, + ]) + def minutes = config.timeoutMinutes ?: cfg.getAs('timeoutMinutes', Integer, 1440) as int + + timeout(time: minutes, unit: 'MINUTES') { + input( + message: config.message ?: 'Please approve to continue', + ok: config.ok ?: 'Approve', + submitter: config.submitter, + submitterParameter: config.submitterParameter, + parameters: config.parameters ?: [], + ) + } +}