name: "Docker release promote" on: workflow_dispatch: inputs: release_tag: description: "Release tag for promotion" required: true type: string workflow_call: inputs: release_tag: required: true type: string env: DOCKER_IMAGE_TAG_CHECK_NAME: "Docker image tag" IMAGE_NAME: "${{ vars.DOCKERHUB_ORG }}/label-studio" RELEASE_DOCKERFILE: "Dockerfile.release" LAUNCHDARKLY_DOWNLOAD_PATH: "feature_flags.json" jobs: docker_release_retag: name: "Docker image (${{ matrix.platform }})" timeout-minutes: 90 runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - platform: linux/amd64 runner: ubuntu-latest - platform: linux/arm64 runner: ubuntu-24.04-arm outputs: image_version: ${{ steps.get_info.outputs.image_version }} ubuntu_tags: ${{ steps.generate-tags.outputs.ubuntu-tags }} sha: ${{ steps.get_info.outputs.sha }} steps: - uses: hmarr/debug-action@v3.0.0 - name: Get an artifact from check suite uses: actions/github-script@v8 id: get_info with: github-token: ${{ secrets.GIT_PAT }} script: | const {repo, owner} = context.repo; const check_runs = await github.paginate( github.rest.checks.listForRef, { owner, repo, ref: 'tags/${{ inputs.release_tag }}', status: "completed", per_page: 100 }, (response) => response.data ); const check = check_runs.find(e => e.name === '${{ env.DOCKER_IMAGE_TAG_CHECK_NAME }}') const details = JSON.parse(check.output.summary) console.log(details) core.setOutput("branch", details.branch); core.setOutput("pretty_branch_name", details.pretty_branch_name); core.setOutput("image_version", details.image_version); core.setOutput("sha", details.sha); - name: Checkout uses: actions/checkout@v6 with: ref: ${{ inputs.release_tag }} - name: Check if the latest tag needs to be updated uses: actions/github-script@v8 id: generate-tags with: github-token: ${{ secrets.GIT_PAT }} script: | const {repo, owner} = context.repo; const newTag = '${{ inputs.release_tag }}'; const regexp = '^[v]?([0-9]+)\.([0-9]+)\.([0-9]+)(\.post([0-9]+))?$'; 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 tags = await github.paginate( github.rest.repos.listTags, { owner, repo, per_page: 100 }, (response) => response.data ); const rawTags = tags.map(e => e.name) const filteredTags = rawTags.filter(e => e.match(regexp)) const sortedTags = filteredTags .map(e => e.match(regexp)) .sort(compareVersions) .reverse() console.log('Sorted tags:') console.log(sortedTags.map(e => e[0])) const newestVersion = sortedTags[0]; console.log(`Newest tag: ${newestVersion[0]}`) let dockerHubUbuntuRawTags = [newTag]; if (compareVersions(newTag.match(regexp), newestVersion) >= 0) { console.log(`new tag ${newTag} is higher that all existing tags`) console.log(dockerHubUbuntuRawTags) dockerHubUbuntuRawTags.push('latest') core.setOutput("latest", true); } else { console.log('not latest') core.setOutput("latest", false); } const ubuntuTags = dockerHubUbuntuRawTags.join("\n"); console.log('Ubuntu tags:') console.log(ubuntuTags) core.setOutput("ubuntu-tags", ubuntuTags); - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.13' - name: Create version_.py run: | python3 $(pwd)/label_studio/core/version.py cat $(pwd)/label_studio/core/version_.py - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.11.1 - name: Login to DockerHub uses: docker/login-action@v3.5.0 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Prepare run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Prepare Release Dockerfile id: release_dockerfile env: VERSION_OVERRIDE: ${{ inputs.release_tag }} BRANCH_OVERRIDE: ${{ steps.get_info.outputs.branch }} shell: bash run: | set -euo pipefail ${ACTIONS_STEP_DEBUG:+-x} release_dir=release_${{ inputs.release_tag }} echo "release_dir=$release_dir" >> $GITHUB_OUTPUT mkdir -p $release_dir cp label_studio/core/version_.py $release_dir/ cd $release_dir cat < "${{ env.RELEASE_DOCKERFILE }}" FROM ${{ env.IMAGE_NAME }}:${{ steps.get_info.outputs.image_version }} COPY --chown=54546:0 version_.py /label-studio/label_studio/core/version_.py EOF - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE_NAME }} - name: Build and Push Release Ubuntu Docker image (${{ matrix.platform }}) uses: docker/build-push-action@v6.18.0 id: docker_build with: context: ${{ steps.release_dockerfile.outputs.release_dir }} file: ${{ steps.release_dockerfile.outputs.release_dir }}/${{ env.RELEASE_DOCKERFILE }} sbom: true provenance: true tags: ${{ env.IMAGE_NAME }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max platforms: ${{ matrix.platform }} outputs: type=image,push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | mkdir -p ${{ runner.temp }}/digests digest="${{ steps.docker_build.outputs.digest }}" touch "${{ runner.temp }}/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v6 with: name: digests-${{ env.PLATFORM_PAIR }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 merge_docker_manifest: runs-on: ubuntu-latest needs: - docker_release_retag steps: - name: Download digests uses: actions/download-artifact@v7 with: path: ${{ runner.temp }}/digests pattern: digests-* merge-multiple: true - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE_NAME }} labels: | org.opencontainers.image.revision=${{ needs.docker_release_retag.outputs.sha }} tags: ${{ needs.docker_release_retag.outputs.ubuntu_tags }} - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} copy_static_files: name: "Copy compiled static from built Docker image" runs-on: ubuntu-latest needs: [docker_release_retag, merge_docker_manifest] steps: - name: Copy compiled static from builded Docker image run: | # Usually it takes 10-20 sec so the image becomes available sleep 10s docker pull ${{ env.IMAGE_NAME }}:${{ inputs.release_tag }} docker run -v ${{ github.workspace }}:/workspace:rw -u root --rm ${{ env.IMAGE_NAME }}:${{ inputs.release_tag }} cp -r /label-studio/web/dist/ /workspace/web/ - name: Create Sentry release @ backend uses: getsentry/action-release@v1 continue-on-error: true env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ vars.SENTRY_ORG }} with: version: label-studio@${{ inputs.release_tag }} projects: opensource-v1-backend - name: Create Sentry release @ frontend uses: getsentry/action-release@v1 continue-on-error: true env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ vars.SENTRY_ORG }} with: version: label-studio@${{ inputs.release_tag }} projects: opensource-v1-frontend sourcemaps: web/dist