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 }}
|