diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..f06bb2a8f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,290 @@ +name: Release Frogbot + +on: + push: + branches: + - create-new-release + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., 3.0.1)' + required: true + type: string + +# Required permissions +permissions: + contents: write + actions: read + +jobs: + release: + name: Release Frogbot v3 + runs-on: self-hosted + + steps: + - name: Extract version from tag + id: version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Manual trigger: use input version + VERSION="${{ inputs.version }}" + # Add 'v' prefix if not present + if [[ ! "$VERSION" =~ ^v ]]; then + TAG="v$VERSION" + else + TAG="$VERSION" + VERSION="${VERSION#v}" + fi + elif [ "${{ github.event_name }}" == "push" ]; then + # Push trigger: use test version with timestamp + VERSION="3.0.0-test-$(date +%s)" + TAG="v$VERSION" + + # Validate it's a v3.x.x version (allows pre-release suffixes like -testing, -alpha, etc.) + if [[ ! "$TAG" =~ ^v3\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + echo "❌ Error: Version must be v3.x.x format (e.g., v3.0.1, v3.0.0-testing)" + echo "Got: $TAG" + exit 1 + fi + else + # Release trigger: use release tag + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#v}" + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "✅ Release version: $VERSION" + echo "✅ Release tag: $TAG" + + - name: Check if tag already exists + if: github.event_name == 'workflow_dispatch' + uses: actions/github-script@v7 + with: + script: | + const tag = '${{ steps.version.outputs.tag }}'; + + try { + // Check if tag exists + await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${tag}` + }); + + // Tag exists - fail the workflow + core.setFailed(`❌ Tag ${tag} already exists! Please use a different version.`); + } catch (error) { + if (error.status === 404) { + // Tag doesn't exist - good to proceed + console.log(`✅ Tag ${tag} does not exist, proceeding with release`); + } else { + // Some other error + throw error; + } + } + + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || github.event.release.tag_name }} + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: true + + - name: Download JFrog CLI + run: | + curl -fL https://install-cli.jfrog.io | sh + # The install script already moves jf to /usr/local/bin/ + + - name: Configure JFrog CLI + env: + JF_URL: ${{ secrets.JF_URL }} + JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} + run: | + jf c rm --quiet || true + jf c add internal --url="$JF_URL" --access-token="$JF_ACCESS_TOKEN" + jf goc --repo-resolve ecosys-go-virtual + + - name: Generate mocks + run: go generate ./... + + - name: Set up Node.js for Action + uses: actions/setup-node@v4 + with: + node-version: '16' + cache: 'npm' + cache-dependency-path: action/package-lock.json + + - name: Run Frogbot scan before release + uses: jfrog/frogbot@v2 + env: + JF_URL: ${{ secrets.JF_URL }} + JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} + JF_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JF_GIT_BASE_BRANCH: ${{ github.ref_name }} + JF_FAIL: "true" + JF_SKIP_AUTOFIX: "true" + + - name: Build GitHub Action + working-directory: action + run: | + npm ci --ignore-scripts + npm run compile + npm run format-check + npm test + + - name: Commit and update tag with compiled action + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add action/lib/ + + # Check if there are changes + CHANGES=false + if ! git diff --staged --quiet; then + echo "Action files changed, committing..." + git commit -m "Build action for ${{ steps.version.outputs.tag }}" + CHANGES=true + else + echo "No changes to action files" + fi + + # For manual triggers, always create/update the tag + # For release triggers, update the tag only if there were changes + if [ "${{ github.event_name }}" == "workflow_dispatch" ] || [ "$CHANGES" = "true" ]; then + echo "Creating/updating tag ${{ steps.version.outputs.tag }}..." + git tag -f ${{ steps.version.outputs.tag }} + git push origin ${{ steps.version.outputs.tag }} --force + echo "Tag ${{ steps.version.outputs.tag }} created/updated" + fi + + - name: Update GitHub Action major version tag (v3) + run: | + # Update v3 tag to point to the latest v3.x.x release + git tag -f v3 + git push origin v3 --force + echo "Updated v3 tag to ${{ steps.version.outputs.tag }}" + + - name: Build and upload binaries (parallel) + env: + VERSION: ${{ steps.version.outputs.tag }} + JFROG_CLI_BUILD_NAME: ecosystem-frogbot-release + JFROG_CLI_BUILD_NUMBER: ${{ github.run_number }} + JFROG_CLI_BUILD_PROJECT: ecosys + run: | + env -i PATH=$PATH HOME=$HOME \ + JFROG_CLI_BUILD_NAME=$JFROG_CLI_BUILD_NAME \ + JFROG_CLI_BUILD_NUMBER=$JFROG_CLI_BUILD_NUMBER \ + JFROG_CLI_BUILD_PROJECT=$JFROG_CLI_BUILD_PROJECT \ + CI=true \ + release/buildAndUpload.sh "${{ steps.version.outputs.version }}" + + - name: Publish build info + env: + JFROG_CLI_BUILD_NAME: ecosystem-frogbot-release + JFROG_CLI_BUILD_NUMBER: ${{ github.run_number }} + JFROG_CLI_BUILD_PROJECT: ecosys + run: | + jf rt bag + jf rt bce + jf rt bp + + - name: Create and distribute release bundle + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + # Use same JFrog CLI config (internal) for distribution + jf ds rbc ecosystem-frogbot $VERSION \ + --spec="release/specs/frogbot-rbc-spec.json" \ + --spec-vars="VERSION=$VERSION" \ + --sign + jf ds rbd ecosystem-frogbot $VERSION \ + --site="releases.jfrog.io" \ + --sync + + - name: Create GitHub Release + if: github.event_name == 'workflow_dispatch' + uses: actions/github-script@v7 + with: + script: | + const tag = '${{ steps.version.outputs.tag }}'; + + // Check if this is a pre-release version (contains hyphen like v3.0.0-testing) + const isPrerelease = tag.includes('-'); + + console.log(`Creating release for tag ${tag}`); + if (isPrerelease) { + console.log(`⚠️ This will be marked as a pre-release`); + } + + // The tag was already created and pushed in the previous step + // Now create the release with auto-generated notes + const release = await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: tag, + name: `Release ${tag}`, + generate_release_notes: true, + draft: false, + prerelease: isPrerelease, + make_latest: false + }); + + console.log(`✅ Created release ${release.data.id} for ${tag}`); + console.log(`📦 Release URL: ${release.data.html_url}`); + console.log(`ℹ️ This release is NOT marked as "Latest"`); + + - name: Cleanup JFrog config + if: always() + run: jf c rm --quiet || true + + # On failure: delete release and tag + - name: Delete release on failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + const tag = '${{ steps.version.outputs.tag }}'; + + console.log(`Workflow failed, cleaning up tag ${tag}`); + + try { + // Try to find and delete the release + const releases = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + const release = releases.data.find(r => r.tag_name === tag); + if (release) { + console.log(`Deleting release ${release.id}`); + await github.rest.repos.deleteRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id + }); + console.log('Release deleted'); + } else { + console.log('No release found to delete'); + } + + // Delete the tag + try { + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${tag}` + }); + console.log('Tag deleted'); + } catch (error) { + console.log(`Tag deletion failed or tag doesn't exist: ${error.message}`); + } + } catch (error) { + console.error(`Cleanup failed: ${error.message}`); + // Don't fail the workflow if cleanup fails + } + diff --git a/action/node_modules/@kwsites/file-exists/dist/test/__mocks__/fs.d.ts b/action/node_modules/@kwsites/file-exists/dist/test/__mocks__/fs.d.ts new file mode 100644 index 000000000..ab80d332b --- /dev/null +++ b/action/node_modules/@kwsites/file-exists/dist/test/__mocks__/fs.d.ts @@ -0,0 +1,7 @@ +export declare function statSync(...args: any[]): any; +export declare function addStatSyncMock(fn: any): void; +export declare function assertMocksUsed(): void; +declare const mockFs: { + statSync: typeof statSync; +}; +export default mockFs; diff --git a/action/node_modules/@kwsites/file-exists/dist/test/__mocks__/fs.js b/action/node_modules/@kwsites/file-exists/dist/test/__mocks__/fs.js new file mode 100644 index 000000000..cd46c71ca --- /dev/null +++ b/action/node_modules/@kwsites/file-exists/dist/test/__mocks__/fs.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +let statSyncMocks = []; +function statSync(...args) { + const mock = statSyncMocks.shift(); + if (typeof mock !== 'function') { + throw new Error(`fs.statSync called without configuring a mock`); + } + return mock(...args); +} +exports.statSync = statSync; +function addStatSyncMock(fn) { + statSyncMocks.push(fn); +} +exports.addStatSyncMock = addStatSyncMock; +function assertMocksUsed() { + if (statSyncMocks.length) { + throw new Error(`fs.afterEach: statSync has ${statSyncMocks.length} unused mocks`); + } +} +exports.assertMocksUsed = assertMocksUsed; +const mockFs = { + statSync, +}; +exports.default = mockFs; +//# sourceMappingURL=fs.js.map \ No newline at end of file diff --git a/action/node_modules/@kwsites/file-exists/dist/test/__mocks__/fs.js.map b/action/node_modules/@kwsites/file-exists/dist/test/__mocks__/fs.js.map new file mode 100644 index 000000000..e2a3d1688 --- /dev/null +++ b/action/node_modules/@kwsites/file-exists/dist/test/__mocks__/fs.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fs.js","sourceRoot":"","sources":["../../../test/__mocks__/fs.ts"],"names":[],"mappings":";;AACA,IAAI,aAAa,GAAU,EAAE,CAAC;AAE9B,SAAgB,QAAQ,CAAC,GAAG,IAAW;IACpC,MAAO,IAAI,GAAG,aAAa,CAAC,KAAK,EAAE,CAAC;IACpC,IAAI,OAAO,IAAI,KAAK,UAAU,EAAE;QAC7B,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;KACnE;IAED,OAAO,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACxB,CAAC;AAPD,4BAOC;AAED,SAAgB,eAAe,CAAC,EAAO;IACpC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC1B,CAAC;AAFD,0CAEC;AAED,SAAgB,eAAe;IAC5B,IAAI,aAAa,CAAC,MAAM,EAAE;QACvB,MAAM,IAAI,KAAK,CAAC,8BAA8B,aAAa,CAAC,MAAM,eAAe,CAAC,CAAC;KACrF;AACJ,CAAC;AAJD,0CAIC;AAED,MAAM,MAAM,GAAG;IACZ,QAAQ;CACV,CAAA;AAED,kBAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/action/test/main.spec.ts b/action/test/main.spec.ts index fd407430a..f29883cc9 100644 --- a/action/test/main.spec.ts +++ b/action/test/main.spec.ts @@ -21,7 +21,12 @@ describe('Frogbot Action Tests', () => { describe('Frogbot URL Tests', () => { const myOs: jest.Mocked = os as any; let cases: string[][] = [ - ['win32' as NodeJS.Platform, 'amd64', 'jfrog.exe', 'https://releases.jfrog.io/artifactory/frogbot/v1/1.2.3/frogbot-windows-amd64/jfrog.exe',], + [ + 'win32' as NodeJS.Platform, + 'amd64', + 'jfrog.exe', + 'https://releases.jfrog.io/artifactory/frogbot/v1/1.2.3/frogbot-windows-amd64/jfrog.exe', + ], ['darwin' as NodeJS.Platform, 'amd64', 'jfrog', 'https://releases.jfrog.io/artifactory/frogbot/v1/1.2.3/frogbot-mac-386/jfrog'], ['darwin' as NodeJS.Platform, 'arm64', 'jfrog', 'https://releases.jfrog.io/artifactory/frogbot/v1/1.2.3/frogbot-mac-arm64/jfrog'], ['linux' as NodeJS.Platform, 'amd64', 'jfrog', 'https://releases.jfrog.io/artifactory/frogbot/v1/1.2.3/frogbot-linux-amd64/jfrog'], diff --git a/release/buildAndUpload.sh b/release/buildAndUpload.sh index 8f671fe7a..eb6e1e16f 100755 --- a/release/buildAndUpload.sh +++ b/release/buildAndUpload.sh @@ -14,7 +14,7 @@ build () { # Run verification after building plugin for the correct platform of this image. if [[ "$pkg" = "frogbot-linux-386" ]]; then - verifyVersionMatching + verifyVersionMatching "$exeName" fi } @@ -24,19 +24,26 @@ buildAndUpload () { goos="$2" goarch="$3" fileExtension="$4" - exeName="frogbot$fileExtension" + # Use unique filename during build to avoid parallel conflicts + uniqueExeName="${pkg}${fileExtension}" + finalExeName="frogbot$fileExtension" - build "$pkg" "$goos" "$goarch" "$exeName" + build "$pkg" "$goos" "$goarch" "$uniqueExeName" - destPath="$pkgPath/$version/$pkg/$exeName" - echo "Uploading $exeName to $destPath ..." - jf rt u "./$exeName" "$destPath" + destPath="$pkgPath/$version/$pkg/$finalExeName" + echo "Uploading $uniqueExeName to $destPath ..." + jf rt u "./$uniqueExeName" "$destPath" + + # Clean up the unique build file after upload + rm -f "./$uniqueExeName" } # Verify version provided in pipelines UI matches version in frogbot source code. +# Takes the executable name as parameter verifyVersionMatching () { + local exePath="$1" echo "Verifying provided version matches built version..." - res=$(eval "./frogbot -v") + res=$(eval "./$exePath -v") exitCode=$? if [[ $exitCode -ne 0 ]]; then echo "Error: Failed verifying version matches" @@ -55,19 +62,47 @@ verifyVersionMatching () { } version="$1" -pkgPath="ecosys-frogbot/v2" +# Extract major version (e.g., "3.1.1" -> "3") +majorVersion="${version%%.*}" +# Allow overriding repository name via environment variable +repoName="${FROGBOT_REPO_NAME:-ecosys-frogbot}" +pkgPath="${repoName}/v${majorVersion}" # Build and upload for every architecture. # Keep 'linux-386' first to prevent unnecessary uploads in case the built version doesn't match the provided one. +echo "Building linux-386 first for version verification..." buildAndUpload 'frogbot-linux-386' 'linux' '386' '' -buildAndUpload 'frogbot-linux-amd64' 'linux' 'amd64' '' -buildAndUpload 'frogbot-linux-s390x' 'linux' 's390x' '' -buildAndUpload 'frogbot-linux-arm64' 'linux' 'arm64' '' -buildAndUpload 'frogbot-linux-arm' 'linux' 'arm' '' -buildAndUpload 'frogbot-linux-ppc64' 'linux' 'ppc64' '' -buildAndUpload 'frogbot-linux-ppc64le' 'linux' 'ppc64le' '' -buildAndUpload 'frogbot-mac-386' 'darwin' 'amd64' '' -buildAndUpload 'frogbot-mac-arm64' 'darwin' 'arm64' '' -buildAndUpload 'frogbot-windows-amd64' 'windows' 'amd64' '.exe' + +# Build the rest in parallel for speed +echo "" +echo "Building remaining 9 platforms in parallel..." +pids=() + +buildAndUpload 'frogbot-linux-amd64' 'linux' 'amd64' '' & pids+=($!) +buildAndUpload 'frogbot-linux-s390x' 'linux' 's390x' '' & pids+=($!) +buildAndUpload 'frogbot-linux-arm64' 'linux' 'arm64' '' & pids+=($!) +buildAndUpload 'frogbot-linux-arm' 'linux' 'arm' '' & pids+=($!) +buildAndUpload 'frogbot-linux-ppc64' 'linux' 'ppc64' '' & pids+=($!) +buildAndUpload 'frogbot-linux-ppc64le' 'linux' 'ppc64le' '' & pids+=($!) +buildAndUpload 'frogbot-mac-386' 'darwin' 'amd64' '' & pids+=($!) +buildAndUpload 'frogbot-mac-arm64' 'darwin' 'arm64' '' & pids+=($!) +buildAndUpload 'frogbot-windows-amd64' 'windows' 'amd64' '.exe' & pids+=($!) + +# Wait for all background jobs and check for failures +echo "Waiting for all parallel builds to complete..." +failed=0 +for pid in "${pids[@]}"; do + wait $pid || failed=1 +done + +if [ $failed -eq 1 ]; then + echo "❌ One or more builds failed!" + exit 1 +fi + +echo "" +echo "✅ All builds completed successfully!" +echo "" jf rt u "./buildscripts/getFrogbot.sh" "$pkgPath/$version/" --flat + diff --git a/release/pipelines.yml b/release/pipelines.yml deleted file mode 100644 index c7618a1dd..000000000 --- a/release/pipelines.yml +++ /dev/null @@ -1,89 +0,0 @@ -resources: - - name: frogbotGit - type: GitRepo - configuration: - path: jfrog/frogbot - branches: - include: dev - gitProvider: il_automation - -pipelines: - - name: release_frogbot - configuration: - runtime: - type: image - image: - custom: - name: releases-docker.jfrog.io/jfrog-ecosystem-integration-env - tag: latest - environmentVariables: - readOnly: - NEXT_VERSION: 0.0.0 - - steps: - - name: Release - type: Bash - configuration: - inputResources: - - name: frogbotGit - trigger: false - integrations: - - name: il_automation - - name: ecosys_entplus_deployer - execution: - onExecute: - - cd $res_frogbotGit_resourcePath - - # Set env - - export CI=true - - export JFROG_CLI_BUILD_NAME=ecosystem-frogbotGit-release - - export JFROG_CLI_BUILD_NUMBER=$run_number - - export JFROG_CLI_BUILD_PROJECT=ecosys - - # Make sure version provided - - echo "Checking variables" - - test -n "$NEXT_VERSION" -a "$NEXT_VERSION" != "0.0.0" - - # Configure Git and merge from the dev - - git checkout master - - git remote set-url origin https://$int_il_automation_token@github.com/jfrog/frogbot.git - - git merge origin/dev - - git tag v${NEXT_VERSION} - - # Download JFrog CLI - - curl -fL https://install-cli.jfrog.io | sh - - jf c rm --quiet - - jf c add internal --url=$int_ecosys_entplus_deployer_url --user=$int_ecosys_entplus_deployer_user --password=$int_ecosys_entplus_deployer_apikey - - jf goc --repo-resolve ecosys-go-virtual - - # Generate mocks - - go generate ./... - - # Audit - - jf audit --fail=false - - # Build and upload - - > - env -i PATH=$PATH HOME=$HOME - JFROG_CLI_BUILD_NAME=$JFROG_CLI_BUILD_NAME - JFROG_CLI_BUILD_NUMBER=$JFROG_CLI_BUILD_NUMBER - JFROG_CLI_BUILD_PROJECT=$JFROG_CLI_BUILD_PROJECT - release/buildAndUpload.sh "$NEXT_VERSION" - - jf rt bag && jf rt bce - - jf rt bp - - # Distribute release bundle - - jf ds rbc ecosystem-frogbot $NEXT_VERSION --spec="release/specs/frogbot-rbc-spec.json" --spec-vars="VERSION=$NEXT_VERSION" --sign - - jf ds rbd ecosystem-frogbot $NEXT_VERSION --site="releases.jfrog.io" --sync - - # Push to master - - git clean -fd - - git push - - git push --tags - - # Merge changes to dev - - git checkout dev - - git merge origin/master - - git push - onComplete: - - jf c rm --quiet diff --git a/release/specs/frogbot-rbc-spec.json b/release/specs/frogbot-rbc-spec.json index ea24a35cd..c6818e977 100644 --- a/release/specs/frogbot-rbc-spec.json +++ b/release/specs/frogbot-rbc-spec.json @@ -1,8 +1,9 @@ { "files": [ { - "pattern": "ecosys-frogbot/(v2/${VERSION}/*)", - "target": "frogbot/{1}" + "pattern": "binary-test/v3/${VERSION}/*/*", + "target": "frogbot/v3/${VERSION}/" } ] } + diff --git a/utils/consts.go b/utils/consts.go index f124f9e1a..478d6f3c2 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -11,6 +11,9 @@ const ( // MaxConcurrentScanners represents the maximum number of threads for running JFrog CLI scanners concurrently MaxConcurrentScanners = 5 + // MaxConcurrentScanners represents the maximum number of threads for running JFrog CLI scanners concurrently + MaxConcurrentScanners = 5 + // VCS providers params GitHub vcsProvider = "github" GitLab vcsProvider = "gitlab" diff --git a/utils/getconfiguration.go b/utils/getconfiguration.go index 944b9b49e..824688f0e 100644 --- a/utils/getconfiguration.go +++ b/utils/getconfiguration.go @@ -528,5 +528,6 @@ func getConfigurationProfile(xrayVersion string, jfrogServer *coreconfig.ServerD } log.Info(fmt.Sprintf("Using Config profile '%s'", configProfile.ProfileName)) + configProfile.FrogbotConfig.CreateAutoFixPr = false return }