name: "CI/CD Pipeline" on: push: branches: - 'ls-release/**' pull_request: types: - opened - synchronize - reopened - ready_for_review branches: - develop - 'ls-release/**' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.pull_request.head.ref || github.ref }} cancel-in-progress: true env: RELEASE_BRANCH_PREFIX: "ls-release/" jobs: changed_files: name: "Changed files" runs-on: ubuntu-latest outputs: src: ${{ steps.changes.outputs.src }} frontend: ${{ steps.changes.outputs.frontend }} docker: ${{ steps.changes.outputs.docker }} commit-message: ${{ steps.commit-details.outputs.commit-message }} timeout-minutes: 25 steps: - uses: hmarr/debug-action@v3.0.0 - name: Checkout if: github.event_name == 'push' uses: actions/checkout@v6 with: ref: ${{ github.ref }} - uses: dorny/paths-filter@v3 id: changes with: filters: | src: - 'label_studio/!(frontend)/**' - 'poetry.lock' - 'pyproject.toml' - 'scripts/*.py' - '.github/workflows/bandit.yml' - '.github/workflows/tests.yml' - '.github/workflows/test_conda.yml' - '.github/workflows/test_migrations.yml' frontend: - 'web/**' - '.github/workflows/tests-yarn-unit.yml' - '.github/workflows/tests-yarn-integration.yml' - '.github/workflows/tests-yarn-e2e.yml' docker: - 'label_studio/**' - 'web/**' - 'deploy/**' - 'Dockerfile**' - 'poetry.lock' - 'pyproject.toml' - '.github/workflows/cicd_pipeline.yml' - '.github/workflows/docker-build.yml' - uses: actions/github-script@v8 id: commit-details with: script: | const { repo, owner } = context.repo; const { data: commit } = await github.rest.repos.getCommit({ owner, repo, ref: '${{ github.event.pull_request.head.sha || github.event.after }}' }); console.log(`Last commit message is "${commit.commit.message}"`) core.setOutput("commit-message", commit.commit.message); gitleaks: name: "Linter" if: github.event_name == 'pull_request' uses: ./.github/workflows/gitleaks.yml with: head_sha: ${{ github.sha }} base_sha: ${{ github.event.pull_request.base.sha || github.event.before }} bandit: name: "Linter" needs: - changed_files if: needs.changed_files.outputs.src == 'true' uses: ./.github/workflows/bandit.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} ruff: name: "Linter" needs: - changed_files if: needs.changed_files.outputs.src == 'true' uses: ./.github/workflows/ruff.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} blue: name: "Linter" needs: - changed_files if: needs.changed_files.outputs.src == 'true' uses: ./.github/workflows/blue.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} biome: name: "Linter" needs: - changed_files if: needs.changed_files.outputs.frontend == 'true' uses: ./.github/workflows/biome.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} stylelint: name: "Linter" needs: - changed_files if: needs.changed_files.outputs.frontend == 'true' uses: ./.github/workflows/stylelint.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} build-frontend-docs: name: "Build" needs: - changed_files if: | github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && needs.changed_files.outputs.frontend == 'true' && !startsWith(needs.changed_files.outputs.commit-message, 'ci: Build tag docs') permissions: contents: write uses: ./.github/workflows/create-tag-docs.yml with: ref: ${{ github.event.pull_request.head.ref || github.ref }} secrets: inherit build-docker: name: "Build" needs: - changed_files if: startsWith(github.ref_name, 'ls-release/') permissions: contents: read checks: write uses: ./.github/workflows/docker-build.yml with: sha: ${{ github.event.pull_request.head.sha || github.event.after }} branch_name: ${{ github.event.pull_request.head.ref || github.ref_name }} secrets: inherit deploy: name: "Deploy" if: startsWith(github.ref_name, 'ls-release/') runs-on: ubuntu-latest needs: - build-docker steps: - uses: actions/github-script@v8 env: DOCKER_IMAGE_VERSION: ${{ needs.build-docker.outputs.build_version }} RELEASE_NAME: 'ls-release' with: github-token: ${{ secrets.GIT_PAT }} script: | const docker_image_version = process.env.DOCKER_IMAGE_VERSION; const release_name = process.env.RELEASE_NAME; github.rest.actions.createWorkflowDispatch({ owner: "HumanSignal", repo: "label-studio-enterprise", workflow_id: "argocd-deploy.yml", ref: "develop", inputs: { docker_image_version: docker_image_version, release_name: release_name, template_name: "lso", } }); pytest: name: "Tests" needs: - changed_files if: needs.changed_files.outputs.src == 'true' uses: ./.github/workflows/tests.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} secrets: inherit migrations: name: "Tests" needs: - changed_files if: needs.changed_files.outputs.src == 'true' uses: ./.github/workflows/test_migrations.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} secrets: inherit conda-test: name: "Tests" needs: - changed_files if: needs.changed_files.outputs.src == 'true' uses: ./.github/workflows/test_conda.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} secrets: inherit draft-release: name: "Draft Release" if: | github.event_name == 'push' && startsWith(github.ref_name, 'ls-release/') runs-on: ubuntu-latest permissions: contents: write outputs: id: ${{ steps.update-draft-release.outputs.id }} rc-version: ${{ steps.create-draft-release.outputs.rc-version }} steps: - uses: hmarr/debug-action@v3.0.0 - name: Checkout uses: actions/checkout@v6 with: token: ${{ secrets.GIT_PAT }} ref: ${{ github.sha }} fetch-depth: 0 - name: Checkout Actions Hub uses: actions/checkout@v6 with: token: ${{ secrets.GIT_PAT }} repository: HumanSignal/actions-hub path: ./.github/actions-hub - name: Create release draft uses: actions/github-script@v8 id: create-draft-release env: TARGET_COMMITISH: "${{ github.ref_name }}" RELEASE_BRANCH_PREFIX: "${{ env.RELEASE_BRANCH_PREFIX }}" DEFAULT_BRANCH: "${{ github.event.repository.default_branch }}" with: script: | const { repo, owner } = context.repo; const target_commitish = process.env.TARGET_COMMITISH; const default_branch = process.env.DEFAULT_BRANCH; let version = target_commitish.replace(process.env.RELEASE_BRANCH_PREFIX, '') const regexp = '^[v]?([0-9]+)\.([0-9]+)\.([0-9]+)(\.post([0-9]+))?$'; const {data: compare} = await github.rest.repos.compareCommits({ owner, repo, base: default_branch, head: target_commitish, }); const rc_version = `${version}rc${ compare.ahead_by }` console.log(`rc-version: ${rc_version}`) core.setOutput("rc-version", rc_version); function compareVersions(a, b) { if (a[1] === b[1]) if (a[2] === b[2]) if (a[3] === b[3]) return (+a[5] || -1) - (+b[5] || -1) else return +a[3] - b[3] else return +a[2] - b[2] else return +a[1] - b[1] } const versionMatch = version.match(regexp) if (!versionMatch) { core.setFailed(`Version "${version}" from branch "${target_commitish}" does not match the regexp ${regexp}`) process.exit() } const tags = await github.paginate( github.rest.repos.listTags, { owner, repo, per_page: 100 }, (response) => response.data ); console.log(`Tags:`) console.log(tags.map(e => e.name)) const matchedTags = tags.filter(e => e.name.indexOf(version) !== -1) console.log(`Tags for ${version}:`) console.log(matchedTags.map(e => e.name)) if (matchedTags.length !== 0) { let newHotfixNumber = 0 for (let matchedTag of matchedTags) { const matchVersion = matchedTag.name.match('^[v]?([0-9]+)\.([0-9]+)\.([0-9]+)(.post([0-9]+))?$') if (matchVersion && matchVersion[5]) { const hotfixNumber = parseInt(matchVersion[5]) if (newHotfixNumber <= hotfixNumber) { newHotfixNumber = hotfixNumber + 1 } } } version = `${version}.post${newHotfixNumber}` } console.log(`New version: ${version}`) const rawTags = tags.map(e => e.name) const tagsWithNew = [...rawTags, version] const sortedTags = tagsWithNew .filter(e => e.match(regexp)) .map((e => e.match(regexp))) .sort(compareVersions) .reverse() .map(e => e[0]) const previousTag = sortedTags[sortedTags.indexOf(version)+1] console.log(`Previous version: ${previousTag}`) console.log('Find or Create a Draft release') const releases = await github.paginate( github.rest.repos.listReleases, { owner, repo, per_page: 100 }, (response) => response.data ); let release = releases.find(e => target_commitish.endsWith(e.target_commitish) && e.draft) if (release) { console.log(`Draft release already exist ${release.html_url}`) } else { console.log(`Draft release is not found creating a new one`) const {data: newDraftRelease} = await github.rest.repos.createRelease({ owner, repo, draft: true, prerelease: false, name: version, tag_name: version, target_commitish: target_commitish, }); console.log(`Draft release is created ${newDraftRelease.html_url}`) release = newDraftRelease; core.setOutput("created", true); } core.setOutput("id", release.id); core.setOutput("tag_name", release.tag_name); - name: Get previous GitHub ref id: previous-tag env: RELEASE_BRANCH: "${{ github.ref_name }}" run: | set -eux version="${RELEASE_BRANCH/#ls-release\//}" previous_tag=$(git tag --sort=-committerdate | grep -v nightly | head -n1) echo "previous_tag_name=tags/${previous_tag}" >> $GITHUB_OUTPUT - name: Set Jira fix version uses: ./.github/actions-hub/actions/jira-set-fix-version continue-on-error: true with: github_token: "${{ secrets.GIT_PAT }}" github_repository: "${{ github.repository }}" github_previous_ref: "${{ steps.previous-tag.outputs.previous_tag_name }}" github_current_ref: "${{ github.event.after }}" jira_server: "${{ vars.JIRA_SERVER }}" jira_username: "${{ secrets.JIRA_USERNAME }}" jira_token: "${{ secrets.JIRA_TOKEN }}" jira_fix_version: "LS OpenSource/${{ steps.create-draft-release.outputs.tag_name }}" - name: Generate release changelog id: changelog uses: ./.github/actions-hub/actions/github-changelog with: release_version: "${{ steps.create-draft-release.outputs.tag_name }}" previous_ref: "${{ steps.previous-tag.outputs.previous_tag_name }}" current_ref: "${{ github.event.after }}" github_token: "${{ secrets.GIT_PAT }}" jira_server: "${{ vars.JIRA_SERVER }}" jira_username: "${{ secrets.JIRA_USERNAME }}" jira_token: "${{ secrets.JIRA_TOKEN }}" jira_release_prefix: "LS OpenSource" launchdarkly_path: "label_studio/feature_flags.json" ignore_tags: "internal" helm_chart_repo: "HumanSignal/charts" helm_chart_path: "heartex/label-studio/Chart.yaml" - name: Update Draft Release uses: actions/github-script@v8 id: update-draft-release env: CHANGELOG: "${{ steps.changelog.outputs.changelog }}" with: github-token: ${{ secrets.GIT_PAT }} script: | const { repo, owner } = context.repo; const changelog = process.env.CHANGELOG; const { data: release } = await github.rest.repos.updateRelease({ owner, repo, release_id: '${{ steps.create-draft-release.outputs.id }}', draft: true, prerelease: false, name: '${{ steps.create-draft-release.outputs.tag_name }}', tag_name: '${{ steps.create-draft-release.outputs.tag_name }}', target_commitish: '${{ github.ref_name }}', body: changelog }); console.log(`Draft release is updated: ${release.html_url}`) core.setOutput("id", release.id); core.setOutput("tag_name", release.tag_name); core.setOutput("html_url", release.html_url); build-pypi: name: "Build" needs: - draft-release if: | github.event_name == 'push' && startsWith(github.ref_name, 'ls-release/') permissions: contents: write uses: ./.github/workflows/build_pypi.yml with: version: ${{ needs.draft-release.outputs.rc-version }} ref: ${{ github.ref_name }} release-id: ${{ needs.draft-release.outputs.id }} secrets: inherit tests-yarn-unit: name: "Tests" needs: - changed_files if: needs.changed_files.outputs.frontend == 'true' uses: ./.github/workflows/tests-yarn-unit.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} secrets: inherit tests-yarn-integration: name: "Tests" needs: - changed_files if: needs.changed_files.outputs.frontend == 'true' uses: ./.github/workflows/tests-yarn-integration.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} secrets: inherit tests-yarn-e2e: name: "Tests" needs: - changed_files if: needs.changed_files.outputs.frontend == 'true' uses: ./.github/workflows/tests-yarn-e2e.yml with: head_sha: ${{ github.event.pull_request.head.sha || github.event.after }} secrets: inherit check_gate: name: "Ready to merge" if: always() needs: - gitleaks - bandit - ruff - blue - biome - stylelint - pytest - migrations - build-docker - tests-yarn-unit - tests-yarn-integration - tests-yarn-e2e runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: allowed-skips: gitleaks, bandit, ruff, blue, biome, stylelint, pytest, migrations, conda-test, build-docker, tests-yarn-unit, tests-yarn-integration, tests-yarn-e2e allowed-failures: | [ "gitleaks" ] jobs: ${{ toJSON(needs) }} dependabot-auto-merge: name: "Auto Merge dependabot PR" if: | always() && needs.check_gate.result == 'success' && github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && ( startsWith(github.head_ref, 'dependabot/npm_and_yarn/') || startsWith(github.head_ref, 'dependabot/pip/') ) runs-on: ubuntu-latest needs: - check_gate steps: - name: Enable auto-merge for Dependabot PRs run: gh pr merge --admin --squash "${PR_URL}" env: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GIT_PAT }}