diff --git a/.github/workflows/publish.vscode.yml b/.github/workflows/publish.vscode.yml new file mode 100644 index 0000000..5ac191b --- /dev/null +++ b/.github/workflows/publish.vscode.yml @@ -0,0 +1,285 @@ +# Harness CI Keeper Plugin - Release Workflow +# +# HOW TO PUBLISH A NEW RELEASE: +# 1. Update the version in package.json (e.g., from 1.0.0 to 1.0.1) +# 2. Commit your changes: git commit -am "Bump version to 1.0.1" +# 3. Create a git tag matching the version: git tag v1.0.1 +# 4. Push the tag to GitHub: git push origin v1.0.1 +# 5. The workflow will automatically: +# - Run linting +# - Run tests +# - Build the Docker image +# - Generate SBOM +# - Create a GitHub release +# +# Note: This workflow triggers on version tags (v*.*.*) or manual workflow dispatch + +name: Publish Harness CI Keeper Plugin + +on: + workflow_dispatch: + push: + tags: + - 'v*.*.*' # Triggers on version tags like v1.0.0, v1.0.1, etc. + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linting + run: npm run lint + + - name: Run tests + run: npm run test + + build: + runs-on: ubuntu-latest + needs: [test] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Get version + id: get_version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + echo "Version: ${VERSION}" + + - name: Log in to Docker Hub (if credentials provided) + if: ${{ secrets.DOCKERHUB_USERNAME }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set Docker image tags + id: docker_tags + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + run: | + if [ -n "${DOCKERHUB_USERNAME}" ]; then + IMAGE_TAG="${DOCKERHUB_USERNAME}/harness-keeper-plugin:${{ steps.get_version.outputs.version }}" + LATEST_TAG="${DOCKERHUB_USERNAME}/harness-keeper-plugin:latest" + else + IMAGE_TAG="harness-keeper-plugin:${{ steps.get_version.outputs.version }}" + LATEST_TAG="harness-keeper-plugin:latest" + fi + echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_OUTPUT + echo "LATEST_TAG=${LATEST_TAG}" >> $GITHUB_OUTPUT + echo "Image tag: ${IMAGE_TAG}" + echo "Latest tag: ${LATEST_TAG}" + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/') }} + tags: | + ${{ steps.docker_tags.outputs.IMAGE_TAG }} + ${{ steps.docker_tags.outputs.LATEST_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: harness-keeper-plugin-build + path: | + Dockerfile + package.json + retention-days: 30 + + generate-sbom: + runs-on: ubuntu-latest + needs: build + + steps: + - name: Get the source code + uses: actions/checkout@v4 + + - name: Install Syft + run: | + echo "Installing Syft v1.18.1..." + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /tmp/bin v1.18.1 + echo "/tmp/bin" >> $GITHUB_PATH + + - name: Install Manifest CLI + run: | + echo "Installing Manifest CLI v0.18.3..." + curl -sSfL https://raw.githubusercontent.com/manifest-cyber/cli/main/install.sh | sh -s -- -b /tmp/bin v0.18.3 + + - name: Create Syft configuration + run: | + cat > syft-config.yaml << 'EOF' + package: + search: + scope: all-layers + cataloger: + enabled: true + java: + enabled: false + python: + enabled: false + nodejs: + enabled: true + EOF + + - name: Generate and upload SBOM + env: + MANIFEST_API_KEY: ${{ secrets.MANIFEST_TOKEN }} + run: | + # Get version from package.json + echo "Detecting Harness CI Keeper Plugin version..." + if [ -f "package.json" ]; then + VERSION=$(grep -o '"version": "[^"]*"' "package.json" | cut -d'"' -f4) + echo "Detected version: ${VERSION}" + else + VERSION="1.0.0" + echo "Could not detect version, using default: ${VERSION}" + fi + + echo "Generating SBOM with Manifest CLI..." + /tmp/bin/manifest sbom "." \ + --generator=syft \ + --name=harness-keeper-plugin \ + --version=${VERSION} \ + --output=spdx-json \ + --file=harness-keeper-plugin-sbom.json \ + --api-key=${MANIFEST_API_KEY} \ + --publish=true \ + --asset-label=application,sbom-generated,nodejs,harness-plugin,docker \ + --generator-config=syft-config.yaml + + echo "SBOM generated and uploaded successfully: harness-keeper-plugin-sbom.json" + echo "---------- SBOM Preview (first 20 lines) ----------" + head -n 20 harness-keeper-plugin-sbom.json + + # Docker registry publish job - reserved for future use + # publish-docker-registry: + # runs-on: ubuntu-latest + # environment: prod + # needs: [test, build, generate-sbom] + # + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 + # + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v3 + # + # - name: Get version + # id: get_version + # run: | + # VERSION=$(node -p "require('./package.json').version") + # echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + # + # - name: Log in to Docker Registry + # uses: docker/login-action@v3 + # with: + # registry: ${{ secrets.DOCKER_REGISTRY }} + # username: ${{ secrets.DOCKER_USERNAME }} + # password: ${{ secrets.DOCKER_PASSWORD }} + # + # - name: Build and push Docker image + # uses: docker/build-push-action@v5 + # with: + # context: . + # push: true + # tags: | + # ${{ secrets.DOCKER_REGISTRY }}/harness-keeper-plugin:${{ steps.get_version.outputs.version }} + # ${{ secrets.DOCKER_REGISTRY }}/harness-keeper-plugin:latest + + create-release: + runs-on: ubuntu-latest + needs: [test, build, generate-sbom] + # Only run when triggered by a tag push + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Get version + id: get_version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + echo "Version: ${VERSION}" + + - name: Set Docker image name for release + id: docker_image + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + run: | + if [ -n "${DOCKERHUB_USERNAME}" ]; then + IMAGE_NAME="${DOCKERHUB_USERNAME}/harness-keeper-plugin:${{ steps.get_version.outputs.version }}" + else + IMAGE_NAME="harness-keeper-plugin:${{ steps.get_version.outputs.version }}" + fi + echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + body: | + ## Harness CI Keeper Plugin ${{ github.ref_name }} + + ### Installation + + #### Docker Image: + The Docker image for this release is available at: + ``` + ${{ steps.docker_image.outputs.IMAGE_NAME }} + ``` + + #### Usage in Harness CI: + ```yaml + - step: + type: Plugin + name: Fetch_Keeper_Secrets + identifier: Fetch_Keeper_Secrets + spec: + image: ${{ steps.docker_image.outputs.IMAGE_NAME }} + settings: + secrets: | + VeYTRo-PHElAwfQT6f0TIA/field/password > DB_PASSWORD + VeYTRo-PHElAwfQT6f0TIA/field/login > DB_USERNAME + envVariables: + KSM_CONFIG: <+secrets.getValue("Keeper_Config_Secret")> + ``` + + ### What's Changed + See the [full changelog](https://github.com/${{ github.repository }}/compare/previous-tag...${{ github.ref_name }}) + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e4d90b1 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,43 @@ +const js = require('@eslint/js'); +const globals = require('globals'); + +module.exports = [ + js.configs.recommended, + { + languageOptions: { + ecmaVersion: 2021, + sourceType: 'script', + globals: { + ...globals.node + } + }, + rules: { + 'no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + }], + 'no-console': 'off', + 'indent': ['error', 4], + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'] + } + }, + { + // Jest test files configuration + files: ['**/*.test.js', '**/__tests__/**/*.js'], + languageOptions: { + globals: { + ...globals.jest + } + } + }, + { + ignores: [ + 'node_modules/**', + '*.vsix', + 'dist/**', + 'build/**', + '.git/**' + ] + } +]; diff --git a/package.json b/package.json index bae7134..82832b2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "main": "index.js", "scripts": { "test": "jest --coverage", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix" }, "keywords": [], "author": "", @@ -16,6 +18,9 @@ "@keeper-security/secrets-manager-core": "^17.3.0" }, "devDependencies": { + "@eslint/js": "^9.39.2", + "eslint": "^9.39.2", + "globals": "^17.0.0", "jest": "^29.7.0" } } diff --git a/src/index.js b/src/index.js index 3af32c4..f96e813 100644 --- a/src/index.js +++ b/src/index.js @@ -46,7 +46,7 @@ const processToken = (rawToken) => { token = Buffer.from(token, 'base64').toString('utf-8').trim(); } catch (e) { // Not base64, use as-is - console.log(e) + console.log(e); } } @@ -132,16 +132,16 @@ const runPlugin = async () => { try { const fileData = await downloadFile(secret); data = Buffer.isBuffer(fileData) ? fileData : - fileData instanceof Uint8Array ? Buffer.from(fileData) : - Buffer.from(fileData); + fileData instanceof Uint8Array ? Buffer.from(fileData) : + Buffer.from(fileData); } catch (downloadError) { core.error(`Failed to download file for notation ${input.notation}: ${downloadError.message}`); continue; } } else { data = Buffer.isBuffer(secret) ? secret : - typeof secret === 'string' ? Buffer.from(secret, 'utf8') : - Buffer.from(String(secret), 'utf8'); + typeof secret === 'string' ? Buffer.from(secret, 'utf8') : + Buffer.from(String(secret), 'utf8'); } if (input.destinationType === 'file') { diff --git a/src/test/index.test.js b/src/test/index.test.js index f6fc89c..861c357 100644 --- a/src/test/index.test.js +++ b/src/test/index.test.js @@ -296,7 +296,7 @@ describe('index.js - Complete Test Suite', () => { require('../index'); await waitForAsync(); - expect(mockConsoleLog).toHaveBeenCalledWith("ENV:VAR_NAME='secret-value'"); + expect(mockConsoleLog).toHaveBeenCalledWith('ENV:VAR_NAME=\'secret-value\''); }); test('should parse file: destination type', async () => { @@ -705,7 +705,7 @@ describe('index.js - Complete Test Suite', () => { require('../index'); await waitForAsync(); - expect(mockConsoleLog).toHaveBeenCalledWith("ENV:VAR_NAME='env-value'"); + expect(mockConsoleLog).toHaveBeenCalledWith('ENV:VAR_NAME=\'env-value\''); expect(fs.mkdirSync).toHaveBeenCalledWith('/harness/secrets', { recursive: true }); expect(fs.writeFileSync).toHaveBeenCalled(); expect(fs.chmodSync).toHaveBeenCalled(); @@ -721,7 +721,7 @@ describe('index.js - Complete Test Suite', () => { require('../index'); await waitForAsync(); - expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining("ENV:VAR_NAME")); + expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('ENV:VAR_NAME')); }); test('should handle environment destination with non-Buffer data (line 157 branch)', async () => { @@ -735,8 +735,8 @@ describe('index.js - Complete Test Suite', () => { require('../index'); await waitForAsync(); - expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining("ENV:VAR_NAME")); - expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining("12345")); + expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('ENV:VAR_NAME')); + expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('12345')); });