From d8ffe694f23ca1f36adf5a041ddd531c7abb8e5e Mon Sep 17 00:00:00 2001 From: Yossi Gartner Date: Tue, 12 Nov 2024 15:55:29 +0200 Subject: [PATCH] add release workflow --- .github/actions/release/action.yaml | 53 +++++++++ .github/workflows/master-push.yaml | 21 ++++ ci/release.js | 165 ++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 .github/actions/release/action.yaml create mode 100644 .github/workflows/master-push.yaml create mode 100644 ci/release.js diff --git a/.github/actions/release/action.yaml b/.github/actions/release/action.yaml new file mode 100644 index 0000000..3e291a6 --- /dev/null +++ b/.github/actions/release/action.yaml @@ -0,0 +1,53 @@ +name: Create Release +inputs: + npm_token: + description: 'Npm Token' + required: true + GH_ACCESS_TOKEN: + description: 'Github access token' + required: true + GH_USERNAME: + description: 'Github username' + required: true + +runs: + using: 'composite' + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + path: current + + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Cache node modules + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: ./node_modules + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Setup npmrc + working-directory: current + run: echo "//registry.npmjs.org/:_authToken=${{ inputs.npm_token }}" > .npmrc + shell: bash + - name: Install node dependencies + working-directory: current + run: npm install + shell: bash + - name: Install NPM Dependencies + run: npm install @octokit/core@5.1.0 semver handlebars + shell: bash + - name: Create Release + run: node ci/release.js "${GH_ACCESS_TOKEN}" "${GH_USERNAME}" + shell: bash + env: + GH_ACCESS_TOKEN: ${{ inputs.GH_ACCESS_TOKEN }} + GH_USERNAME: ${{ inputs.GH_USERNAME }} diff --git a/.github/workflows/master-push.yaml b/.github/workflows/master-push.yaml new file mode 100644 index 0000000..49b9df7 --- /dev/null +++ b/.github/workflows/master-push.yaml @@ -0,0 +1,21 @@ +name: Main Pipeline +on: + workflow_dispatch: + push: + branches: + - master +env: + CI: true +jobs: + release: + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout codes + uses: actions/checkout@v3 + - name: Create Release + uses: ./.github/actions/release + with: + npm_token: ${{ secrets.NPM_TOKEN }} + GH_ACCESS_TOKEN: ${{ secrets.GH_REPOSITORY_ADMIN_TOKEN }} + GH_USERNAME: ${{ secrets.GH_USERNAME }} diff --git a/ci/release.js b/ci/release.js new file mode 100644 index 0000000..bd087db --- /dev/null +++ b/ci/release.js @@ -0,0 +1,165 @@ +const {execSync} = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const semver = require('semver'); +const {Octokit} = require('@octokit/core'); +const Handlebars = require('handlebars'); + +const GH_TOKEN = process.argv[2] ? process.argv[2] : process.env.GH_ACCESS_TOKEN; +const GH_USER = process.argv[3] ? process.argv[3] : 'x-access-token'; + +const githubApi = new Octokit({auth: GH_TOKEN}); +const repoName = 'openapi-public'; +const latestTagDirectory = path.join(__dirname, '../', 'temp', repoName); +const tempDirectory = path.join(__dirname, '../', 'temp'); +const openapisFolder = path.join('/'); +const latestOpenAPIsFolder = path.join(__dirname, '../', openapisFolder); + +async function release() { + console.log('Checking out latest version tag'); + const latestTag = await cloneAndCheckoutLatestTag(); + console.log('Latest version tag:', latestTag); + console.log('Running diffs between current `master` OpenAPIs and latest version tag'); + const {newEndpoints, missingEndpoints, deprecatedEndpoints} = await createDiffs(); + if (newEndpoints.length || missingEndpoints.length || deprecatedEndpoints.length) { + const releaseType = determineReleaseType(missingEndpoints, deprecatedEndpoints); + const newVersion = increaseVersion(latestTag, releaseType); + console.log('Changes found, will create a new release', releaseType, newVersion); + const releaseBody = generateReleaseDescription(newEndpoints, missingEndpoints, deprecatedEndpoints); + await githubApi.request('POST /repos/{owner}/{repo}/releases', { + owner: 'frontegg', + repo: repoName, + tag_name: newVersion, + target_commitish: 'master', + name: newVersion, + body: releaseBody, + draft: false, + prerelease: false, + generate_release_notes: false, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + console.log('Release created successfully!'); + } else { + console.log('No changes found, will skip release'); + } + await clean(); +} + +async function cloneAndCheckoutLatestTag() { + try { + fs.rmdirSync(latestTagDirectory, {recursive: true}); + } catch (e) {} + try { + fs.mkdirSync(latestTagDirectory, {recursive: true}); + } catch (e) {} + execSync(`git clone https://${GH_USER}:${GH_TOKEN}@github.com/frontegg/${repoName}.git ${latestTagDirectory}`); + const latestTag = execSync('git describe --tags --abbrev=0').toString().trim(); + execSync(`git checkout ${latestTag}`, {cwd: latestTagDirectory}); + return latestTag; +} + +async function listOpenAPIs() { + return ['agent.json', 'entitlements.json', 'identity.json', 'scim.json', 'sso.json', 'tenants.json']; +} + +async function createDiffs() { + const latestOpenAPIs = await listOpenAPIs(latestOpenAPIsFolder); + try { + fs.mkdirSync(path.join(tempDirectory, 'diffs'), {recursive: true}); + } catch (e) {} + const totalNewEndpoints = []; + const totalMissingEndpoints = []; + const totalDeprecatedEndpoints = []; + for (const openapi of latestOpenAPIs) { + const execCommand = `docker run -d \ + -v ${path.join(__dirname, '../')}:/specs \ + openapitools/openapi-diff:latest ${path.join( + '/specs', + 'temp', + repoName, + openapisFolder, + openapi, + )} ${path.join('/specs', openapisFolder, openapi)} --json /specs/temp/diffs/${openapi}`; + execSync(execCommand); + const diffFilePath = path.join(__dirname, '../', 'temp/diffs', openapi); + await waitForDiffFileToGenerate(diffFilePath); + const {newEndpoints, missingEndpoints, deprecatedEndpoints} = JSON.parse(fs.readFileSync(diffFilePath).toString()); + totalNewEndpoints.push(...newEndpoints); + totalMissingEndpoints.push(...missingEndpoints); + totalDeprecatedEndpoints.push(...deprecatedEndpoints); + } + + return { + newEndpoints: totalNewEndpoints, + missingEndpoints: totalMissingEndpoints, + deprecatedEndpoints: totalDeprecatedEndpoints, + }; +} + +async function clean() { + fs.rmdirSync(tempDirectory, {recursive: true}); +} + +function determineReleaseType(missingEndpoints, deprecatedEndpoints) { + if (missingEndpoints.length) { + return 'major'; + } + + if (deprecatedEndpoints.length) { + return 'minor'; + } + + return 'patch'; +} + +function increaseVersion(latestVersion, releaseType) { + return `v${semver.inc(latestVersion, releaseType)}`; +} + +function generateReleaseDescription(newEndpoints, missingEndpoints, deprecatedEndpoints) { + return Handlebars.compile(hbReleaseTemplate)({newEndpoints, missingEndpoints, deprecatedEndpoints}); +} + +async function waitForDiffFileToGenerate(path) { + const promise = new Promise((resolve) => { + const interval = setInterval(() => { + const exist = fs.existsSync(path); + if (exist) { + try { + JSON.parse(fs.readFileSync(path).toString()); + resolve(); + clearInterval(interval); + } catch (e) {} + } + }, 100); + }); + + await promise; +} + +release(); + +const hbReleaseTemplate = ` +{{#if newEndpoints.length}} +## New Endpoints +{{#each newEndpoints}} + {{method}} {{pathUrl}} +{{/each}} +{{/if}} + +{{#if deprecatedEndpoints.length}} +## Deprecated Endpoints +{{#each deprecatedEndpoints}} + {{method}} {{pathUrl}} +{{/each}} +{{/if}} + +{{#if missingEndpoints.length}} +## Removed Endpoints +{{#each missingEndpoints}} + {{method}} {{pathUrl}} +{{/each}} +{{/if}} +`;