MIOT Release Train #31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: MIOT Release Train | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| release_version: | |
| description: "Umbrella stack semver without v prefix. Leave blank to bump the latest miot-stack@v* patch." | |
| required: false | |
| type: string | |
| release_notes_version: | |
| description: "Private release-notes semver without v prefix. Leave blank to bump the latest release note patch." | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| packages: write | |
| id-token: write | |
| env: | |
| NODE_VERSION: "24" | |
| GHCR_REGISTRY: ghcr.io | |
| IMAGE_OWNER: microboxlabs | |
| APP_IMAGE_NAME: miot-app | |
| MODULITH_IMAGE_NAME: miot-srv | |
| JAVA_VERSION: "21" | |
| RELEASE_NOTES_MODEL: auto | |
| RELEASE_NOTES_CONTEXT: long_context | |
| jobs: | |
| prepare: | |
| name: Prepare release | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_version: ${{ steps.version.outputs.release_version }} | |
| stack_tag: ${{ steps.plan.outputs.stack_tag }} | |
| stack_plan_file: ${{ steps.plan.outputs.stack_plan_file }} | |
| app_changed: ${{ steps.plan.outputs.app_changed }} | |
| app_version: ${{ steps.plan.outputs.app_version }} | |
| app_tag: ${{ steps.plan.outputs.app_tag }} | |
| modulith_changed: ${{ steps.plan.outputs.modulith_changed }} | |
| modulith_version: ${{ steps.plan.outputs.modulith_version }} | |
| modulith_tag: ${{ steps.plan.outputs.modulith_tag }} | |
| release_notes_version: ${{ steps.version.outputs.release_notes_version }} | |
| private_milestone: ${{ steps.version.outputs.private_milestone }} | |
| oss_milestone: ${{ steps.version.outputs.oss_milestone }} | |
| branch: ${{ steps.branch.outputs.branch }} | |
| pr_number: ${{ steps.pr.outputs.number }} | |
| pr_url: ${{ steps.pr.outputs.url }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: ${{ env.JAVA_VERSION }} | |
| cache: maven | |
| cache-dependency-path: quarkus-srv/pom.xml | |
| - name: Determine release metadata | |
| id: version | |
| run: | | |
| if [[ -n "${{ inputs.release_version }}" ]]; then | |
| version="${{ inputs.release_version }}" | |
| else | |
| latest="$(git tag --list 'miot-stack@v*' | sed 's/^miot-stack@v//' | sort -V | tail -1)" | |
| if [[ -z "$latest" ]]; then | |
| latest="$( | |
| { | |
| git tag --list 'app@v*' | sed 's/^app@v//' | |
| git tag --list 'modulith@v*' | sed 's/^modulith@v//' | |
| } | sort -V | tail -1 | |
| )" | |
| fi | |
| if [[ -z "$latest" ]]; then | |
| version="0.1.0" | |
| else | |
| IFS=. read -r major minor patch <<< "$latest" | |
| version="${major}.${minor}.$((patch + 1))" | |
| fi | |
| fi | |
| if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "Invalid release version: $version" >&2 | |
| exit 1 | |
| fi | |
| if [[ -n "${{ inputs.release_notes_version }}" ]]; then | |
| release_notes_version="${{ inputs.release_notes_version }}" | |
| else | |
| note_dirs=(turbo-repo/apps/app/src/releases) | |
| if [[ -d releases/notes ]]; then | |
| note_dirs+=(releases/notes) | |
| fi | |
| latest_release_notes="$(find "${note_dirs[@]}" \ | |
| -maxdepth 1 \ | |
| -type f \ | |
| -name 'v[0-9]*.[0-9]*.[0-9]*.mdx' \ | |
| -exec basename {} .mdx \; | | |
| sed 's/^v//' | | |
| sort -V | | |
| tail -1)" | |
| if [[ -z "$latest_release_notes" ]]; then | |
| release_notes_version="1.0.0" | |
| else | |
| IFS=. read -r notes_major notes_minor notes_patch <<< "$latest_release_notes" | |
| release_notes_version="${notes_major}.${notes_minor}.$((notes_patch + 1))" | |
| fi | |
| fi | |
| if ! [[ "$release_notes_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "Invalid release notes version: $release_notes_version" >&2 | |
| exit 1 | |
| fi | |
| { | |
| echo "release_version=$version" | |
| echo "release_notes_version=$release_notes_version" | |
| echo "private_milestone=Release $release_notes_version" | |
| echo "oss_milestone=MIOT-$version" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Collect release issues | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} | |
| GH_PROJECT_OWNER: microboxlabs | |
| run: | | |
| ./turbo-repo/generative_ai/tools/sh/fetch-release-issues.sh \ | |
| --release "${{ steps.version.outputs.private_milestone }}" \ | |
| --oss "${{ steps.version.outputs.oss_milestone }}" \ | |
| --state all > release-issues.json | |
| - name: Resolve component release plan | |
| id: plan | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} | |
| STACK_VERSION: ${{ steps.version.outputs.release_version }} | |
| OSS_MILESTONE: ${{ steps.version.outputs.oss_milestone }} | |
| run: node .github/scripts/resolve-stack-release-plan.js | |
| - name: Create release branch | |
| id: branch | |
| run: | | |
| branch="release/miot-v${{ steps.version.outputs.release_version }}" | |
| git switch -c "$branch" | |
| echo "branch=$branch" >> "$GITHUB_OUTPUT" | |
| - name: Bump app package version | |
| if: steps.plan.outputs.app_changed == 'true' | |
| run: bash turbo-repo/generative_ai/tools/sh/bump-app-version.sh "${{ steps.plan.outputs.app_version }}" | |
| - name: Bump modulith Maven version | |
| if: steps.plan.outputs.modulith_changed == 'true' | |
| run: ./mvnw versions:set -DnewVersion="${{ steps.plan.outputs.modulith_version }}" -DgenerateBackupPoms=false -DprocessAllModules=true | |
| working-directory: quarkus-srv | |
| - name: Resolve release date | |
| id: release_date | |
| run: echo "date=$(date +'%d/%m/%Y')" >> "$GITHUB_OUTPUT" | |
| - name: Install Copilot CLI | |
| run: npm install -g @github/copilot | |
| - name: Build release notes prompt | |
| run: | | |
| node - <<'NODE' | |
| const fs = require("fs"); | |
| const prompt = fs.readFileSync(".github/prompts/app-release-notes.prompt.yml", "utf8"); | |
| const issues = fs.readFileSync("release-issues.json", "utf8"); | |
| const indentedIssues = issues.split("\n").join("\n "); | |
| const body = prompt | |
| .replaceAll("{{release_version}}", "${{ steps.version.outputs.release_notes_version }}") | |
| .replaceAll("{{release_date}}", "${{ steps.release_date.outputs.date }}") | |
| .replaceAll("{{private_milestone}}", "${{ steps.version.outputs.private_milestone }}") | |
| .replaceAll("{{oss_milestone}}", "${{ steps.version.outputs.oss_milestone }}") | |
| .replaceAll("{{release_issues}}", indentedIssues); | |
| fs.writeFileSync("release-notes.prompt.yml", body); | |
| NODE | |
| - name: Generate release notes with Copilot CLI | |
| id: release_notes_ai | |
| env: | |
| COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT }} | |
| run: | | |
| copilot --version | |
| ruby -ryaml -e ' | |
| prompt = YAML.load_file("release-notes.prompt.yml") | |
| text = prompt.fetch("messages").map do |message| | |
| "#{message.fetch("role").upcase}:\n#{message.fetch("content")}" | |
| end.join("\n\n") | |
| File.write("release-notes.prompt.txt", text) | |
| ' | |
| copilot \ | |
| --prompt "$(cat release-notes.prompt.txt)" \ | |
| --silent \ | |
| --no-ask-user \ | |
| --model "${{ env.RELEASE_NOTES_MODEL }}" \ | |
| --context "${{ env.RELEASE_NOTES_CONTEXT }}" \ | |
| > release-notes.response.mdx | |
| echo "response-file=release-notes.response.mdx" >> "$GITHUB_OUTPUT" | |
| - name: Write release notes | |
| run: | | |
| release_file="releases/notes/v${{ steps.version.outputs.release_notes_version }}.mdx" | |
| app_release_file="turbo-repo/apps/app/src/releases/v${{ steps.version.outputs.release_notes_version }}.mdx" | |
| mkdir -p "$(dirname "$release_file")" | |
| mkdir -p "$(dirname "$app_release_file")" | |
| node - "${{ steps.release_notes_ai.outputs['response-file'] }}" "$release_file" <<'NODE' | |
| const fs = require("fs"); | |
| const [source, target] = process.argv.slice(2); | |
| let content = fs.readFileSync(source, "utf8").trim(); | |
| content = content.replace(/^```(?:mdx|markdown|md)?\s*/i, "").replace(/\s*```$/i, "").trim(); | |
| if (!content.startsWith("# 🔔 Release Notes")) { | |
| throw new Error("AI response did not start with the expected release-notes heading."); | |
| } | |
| fs.writeFileSync(target, `${content}\n`); | |
| NODE | |
| cp "$release_file" "$app_release_file" | |
| - name: Commit release prep | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add "${{ steps.plan.outputs.stack_plan_file }}" releases/notes/v${{ steps.version.outputs.release_notes_version }}.mdx turbo-repo/apps/app/src/releases/v${{ steps.version.outputs.release_notes_version }}.mdx turbo-repo/apps/app/package.json turbo-repo/package-lock.json quarkus-srv | |
| git commit -m "chore(release): prepare MIOT stack v${{ steps.version.outputs.release_version }}" | |
| git push origin "${{ steps.branch.outputs.branch }}" | |
| - name: Open release PR | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} | |
| run: | | |
| cat > release-pr-body.md <<'EOF' | |
| Prepares ${{ steps.plan.outputs.stack_tag }} from milestone ${{ steps.version.outputs.oss_milestone }}, with release notes v${{ steps.version.outputs.release_notes_version }}. | |
| Components: | |
| - App: ${{ steps.plan.outputs.app_tag }} (changed: ${{ steps.plan.outputs.app_changed }}) | |
| - Modulith: ${{ steps.plan.outputs.modulith_tag }} (changed: ${{ steps.plan.outputs.modulith_changed }}) | |
| Milestones: ${{ steps.version.outputs.private_milestone }}, ${{ steps.version.outputs.oss_milestone }}. | |
| Approve and merge this PR, then approve the protected release job to tag changed components, resolve component images, and deploy the stack manifest. | |
| EOF | |
| url="$(gh pr create \ | |
| --base trunk \ | |
| --head "${{ steps.branch.outputs.branch }}" \ | |
| --title "chore(release): prepare MIOT stack v${{ steps.version.outputs.release_version }}" \ | |
| --body-file release-pr-body.md)" | |
| number="${url##*/}" | |
| echo "number=$number" >> "$GITHUB_OUTPUT" | |
| echo "url=$url" >> "$GITHUB_OUTPUT" | |
| approve: | |
| name: Approval gate | |
| runs-on: ubuntu-latest | |
| needs: prepare | |
| environment: | |
| name: app-release-approval | |
| url: ${{ needs.prepare.outputs.pr_url }} | |
| steps: | |
| - name: Approval acknowledged | |
| run: echo "Release approval granted for MIOT v${{ needs.prepare.outputs.release_version }}" | |
| tag: | |
| name: Tag trunk | |
| runs-on: ubuntu-latest | |
| needs: [prepare, approve] | |
| outputs: | |
| sha: ${{ steps.release_pr.outputs.sha }} | |
| short_sha: ${{ steps.release_pr.outputs.short_sha }} | |
| steps: | |
| - name: Wait for release PR merge | |
| id: release_pr | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} | |
| PR_NUMBER: ${{ needs.prepare.outputs.pr_number }} | |
| run: | | |
| for attempt in {1..60}; do | |
| pr="$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json mergedAt,mergeCommit,state)" | |
| merged_at="$(echo "$pr" | jq -r '.mergedAt')" | |
| merge_sha="$(echo "$pr" | jq -r '.mergeCommit.oid // ""')" | |
| state="$(echo "$pr" | jq -r '.state')" | |
| if [[ "$state" == "CLOSED" && ( "$merged_at" == "null" || -z "$merge_sha" ) ]]; then | |
| echo "::error::Release PR #$PR_NUMBER was closed without being merged. Re-run the release train to create a new release PR." | |
| exit 1 | |
| fi | |
| if [[ "$merged_at" != "null" && -n "$merge_sha" ]]; then | |
| echo "Release PR #$PR_NUMBER merged at $merged_at ($merge_sha)." | |
| echo "sha=$merge_sha" >> "$GITHUB_OUTPUT" | |
| echo "short_sha=${merge_sha::7}" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Release PR #$PR_NUMBER is not merged yet. Waiting before tagging... ($attempt/60)" | |
| sleep 60 | |
| done | |
| echo "::error::Release PR #$PR_NUMBER was approved but is still not merged. Merge it before the release train can tag trunk." | |
| exit 1 | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ steps.release_pr.outputs.sha }} | |
| fetch-depth: 0 | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: ${{ env.JAVA_VERSION }} | |
| cache: maven | |
| cache-dependency-path: quarkus-srv/pom.xml | |
| - name: Verify release prep landed on trunk | |
| run: | | |
| release_file="releases/notes/v${{ needs.prepare.outputs.release_notes_version }}.mdx" | |
| app_release_file="turbo-repo/apps/app/src/releases/v${{ needs.prepare.outputs.release_notes_version }}.mdx" | |
| plan_file="${{ needs.prepare.outputs.stack_plan_file }}" | |
| if [[ "${{ needs.prepare.outputs.app_changed }}" == "true" ]]; then | |
| package_version="$(node -p "require('./turbo-repo/apps/app/package.json').version")" | |
| lock_version="$(node -p "require('./turbo-repo/package-lock.json').packages['apps/app'].version")" | |
| if [[ "$package_version" != "${{ needs.prepare.outputs.app_version }}" ]]; then | |
| echo "trunk package.json is $package_version, expected ${{ needs.prepare.outputs.app_version }}." >&2 | |
| echo "Merge the release PR before approving/tagging." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$lock_version" != "${{ needs.prepare.outputs.app_version }}" ]]; then | |
| echo "trunk package-lock.json is $lock_version, expected ${{ needs.prepare.outputs.app_version }}." >&2 | |
| echo "Merge the release PR before approving/tagging." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [[ "${{ needs.prepare.outputs.modulith_changed }}" == "true" ]]; then | |
| modulith_version="$(cd quarkus-srv && ./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout)" | |
| if [[ "$modulith_version" != "${{ needs.prepare.outputs.modulith_version }}" ]]; then | |
| echo "trunk modulith version is $modulith_version, expected ${{ needs.prepare.outputs.modulith_version }}." >&2 | |
| echo "Merge the release PR before approving/tagging." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [[ ! -f "$release_file" ]]; then | |
| echo "Missing release notes file on trunk: $release_file" >&2 | |
| echo "Merge the release PR before approving/tagging." >&2 | |
| exit 1 | |
| fi | |
| if [[ ! -f "$app_release_file" ]]; then | |
| echo "Missing app release notes file on trunk: $app_release_file" >&2 | |
| echo "Merge the release PR before approving/tagging." >&2 | |
| exit 1 | |
| fi | |
| if [[ ! -f "$plan_file" ]]; then | |
| echo "Missing stack release plan on trunk: $plan_file" >&2 | |
| echo "Merge the release PR before approving/tagging." >&2 | |
| exit 1 | |
| fi | |
| - name: Create and push component tags | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| tags=("${{ needs.prepare.outputs.stack_tag }}") | |
| git tag -a "${{ needs.prepare.outputs.stack_tag }}" -m "${{ needs.prepare.outputs.stack_tag }}" | |
| if [[ "${{ needs.prepare.outputs.app_changed }}" == "true" ]]; then | |
| git tag -a "${{ needs.prepare.outputs.app_tag }}" -m "${{ needs.prepare.outputs.app_tag }}" | |
| tags+=("${{ needs.prepare.outputs.app_tag }}") | |
| fi | |
| if [[ "${{ needs.prepare.outputs.modulith_changed }}" == "true" ]]; then | |
| git tag -a "${{ needs.prepare.outputs.modulith_tag }}" -m "${{ needs.prepare.outputs.modulith_tag }}" | |
| tags+=("${{ needs.prepare.outputs.modulith_tag }}") | |
| fi | |
| git push origin "${tags[@]}" | |
| - name: Create GitHub releases | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh release create "${{ needs.prepare.outputs.stack_tag }}" \ | |
| --title "${{ needs.prepare.outputs.stack_tag }}" \ | |
| --generate-notes \ | |
| --latest | |
| if [[ "${{ needs.prepare.outputs.app_changed }}" == "true" ]]; then | |
| gh release create "${{ needs.prepare.outputs.app_tag }}" \ | |
| --title "${{ needs.prepare.outputs.app_tag }}" \ | |
| --generate-notes | |
| fi | |
| if [[ "${{ needs.prepare.outputs.modulith_changed }}" == "true" ]]; then | |
| gh release create "${{ needs.prepare.outputs.modulith_tag }}" \ | |
| --title "${{ needs.prepare.outputs.modulith_tag }}" \ | |
| --generate-notes | |
| fi | |
| build-app: | |
| name: Resolve app image | |
| runs-on: ubuntu-latest | |
| needs: [prepare, tag] | |
| outputs: | |
| image: ${{ steps.image.outputs.image }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| if: needs.prepare.outputs.app_changed == 'true' | |
| with: | |
| ref: ${{ needs.tag.outputs.sha }} | |
| - uses: actions/setup-node@v6 | |
| if: needs.prepare.outputs.app_changed == 'true' | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| cache-dependency-path: turbo-repo/package-lock.json | |
| - name: Install dependencies | |
| if: needs.prepare.outputs.app_changed == 'true' | |
| run: npm ci | |
| working-directory: turbo-repo | |
| - name: Build app | |
| if: needs.prepare.outputs.app_changed == 'true' | |
| run: npx turbo run build --filter=@modulariot/app | |
| working-directory: turbo-repo | |
| - name: Stage app artifact | |
| if: needs.prepare.outputs.app_changed == 'true' | |
| run: | | |
| mkdir -p build-context/.next | |
| cp -r turbo-repo/apps/app/.next/standalone build-context/.next/standalone | |
| cp -r turbo-repo/apps/app/.next/static build-context/.next/static | |
| cp -r turbo-repo/apps/app/public build-context/public | |
| cp turbo-repo/docker/nextjs.standalone.Dockerfile build-context/ | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push image | |
| if: needs.prepare.outputs.app_changed == 'true' | |
| id: docker | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: build-context/ | |
| file: build-context/nextjs.standalone.Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.APP_IMAGE_NAME }}:${{ needs.prepare.outputs.app_version }} | |
| ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.APP_IMAGE_NAME }}:app-v${{ needs.prepare.outputs.app_version }} | |
| ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.APP_IMAGE_NAME }}:sha-${{ needs.tag.outputs.short_sha }} | |
| build-args: | | |
| APP_NAME=app | |
| provenance: true | |
| sbom: true | |
| - name: Export image ref | |
| id: image | |
| run: | | |
| if [[ "${{ needs.prepare.outputs.app_changed }}" == "true" ]]; then | |
| digest="${{ steps.docker.outputs.digest }}" | |
| else | |
| digest="$(docker buildx imagetools inspect "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.APP_IMAGE_NAME }}:app-v${{ needs.prepare.outputs.app_version }}" | awk '/^Digest:/ { print $2; exit }')" | |
| fi | |
| echo "image=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.APP_IMAGE_NAME }}@${digest}" >> "$GITHUB_OUTPUT" | |
| build-modulith: | |
| name: Resolve modulith image | |
| runs-on: ubuntu-latest | |
| needs: [prepare, tag] | |
| outputs: | |
| image: ${{ steps.image.outputs.image }} | |
| steps: | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Wait for release modulith image | |
| if: needs.prepare.outputs.modulith_changed == 'true' | |
| env: | |
| SOURCE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}:sha-${{ needs.tag.outputs.short_sha }} | |
| SOURCE_NATIVE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}:sha-${{ needs.tag.outputs.short_sha }}-native | |
| run: | | |
| for attempt in {1..60}; do | |
| if docker buildx imagetools inspect "$SOURCE_IMAGE" >/dev/null && | |
| docker buildx imagetools inspect "$SOURCE_NATIVE_IMAGE" >/dev/null; then | |
| echo "Promoting release modulith images $SOURCE_IMAGE and $SOURCE_NATIVE_IMAGE." | |
| exit 0 | |
| fi | |
| echo "Waiting for Quarkus trunk CI to publish release images... ($attempt/60)" | |
| sleep 60 | |
| done | |
| echo "::error::Timed out waiting for Quarkus trunk CI to publish release modulith images." | |
| exit 1 | |
| - name: Promote image tags | |
| if: needs.prepare.outputs.modulith_changed == 'true' | |
| env: | |
| SOURCE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}:sha-${{ needs.tag.outputs.short_sha }} | |
| SOURCE_NATIVE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}:sha-${{ needs.tag.outputs.short_sha }}-native | |
| RELEASE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}:${{ needs.prepare.outputs.modulith_version }} | |
| COMPONENT_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}:modulith-v${{ needs.prepare.outputs.modulith_version }} | |
| RELEASE_NATIVE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}:${{ needs.prepare.outputs.modulith_version }}-native | |
| COMPONENT_NATIVE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}:modulith-v${{ needs.prepare.outputs.modulith_version }}-native | |
| run: | | |
| docker buildx imagetools create \ | |
| --tag "$RELEASE_IMAGE" \ | |
| --tag "$COMPONENT_IMAGE" \ | |
| "$SOURCE_IMAGE" | |
| docker buildx imagetools create \ | |
| --tag "$RELEASE_NATIVE_IMAGE" \ | |
| --tag "$COMPONENT_NATIVE_IMAGE" \ | |
| "$SOURCE_NATIVE_IMAGE" | |
| - name: Export image ref | |
| id: image | |
| run: | | |
| digest="$(docker buildx imagetools inspect "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}:modulith-v${{ needs.prepare.outputs.modulith_version }}" | awk '/^Digest:/ { print $2; exit }')" | |
| echo "image=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}@${digest}" >> "$GITHUB_OUTPUT" | |
| deploy: | |
| name: Dispatch configured environment deploys | |
| runs-on: ubuntu-latest | |
| needs: [prepare, tag, build-app, build-modulith] | |
| environment: | |
| name: app-deploy | |
| steps: | |
| - name: Dispatch private deployment workflow | |
| env: | |
| GH_TOKEN: ${{ secrets.DEPLOY_DISPATCH_TOKEN }} | |
| DEPLOY_DISPATCH_REPO: ${{ secrets.DEPLOY_DISPATCH_REPO }} | |
| RELEASE_VERSION: ${{ needs.prepare.outputs.release_version }} | |
| STACK_TAG: ${{ needs.prepare.outputs.stack_tag }} | |
| APP_TAG: ${{ needs.prepare.outputs.app_tag }} | |
| MODULITH_TAG: ${{ needs.prepare.outputs.modulith_tag }} | |
| APP_IMAGE_REPOSITORY: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.APP_IMAGE_NAME }} | |
| APP_IMAGE_TAG: app-v${{ needs.prepare.outputs.app_version }} | |
| APP_IMAGE_REF: ${{ needs.build-app.outputs.image }} | |
| APP_CHANGED: ${{ needs.prepare.outputs.app_changed }} | |
| APP_VERSION: ${{ needs.prepare.outputs.app_version }} | |
| MODULITH_IMAGE_REPOSITORY: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }} | |
| MODULITH_IMAGE_TAG: modulith-v${{ needs.prepare.outputs.modulith_version }} | |
| MODULITH_IMAGE_REF: ${{ needs.build-modulith.outputs.image }} | |
| MODULITH_CHANGED: ${{ needs.prepare.outputs.modulith_changed }} | |
| MODULITH_VERSION: ${{ needs.prepare.outputs.modulith_version }} | |
| run: | | |
| DEPLOY_DISPATCH_REPO="${DEPLOY_DISPATCH_REPO:-microboxlabs/miot-deployments}" | |
| if [[ -z "$GH_TOKEN" ]]; then | |
| echo "DEPLOY_DISPATCH_TOKEN is required to dispatch the private deployment repository." >&2 | |
| exit 1 | |
| fi | |
| jq -n \ | |
| --arg release_version "$RELEASE_VERSION" \ | |
| --arg stack_tag "$STACK_TAG" \ | |
| --arg app_tag "$APP_TAG" \ | |
| --arg app_changed "$APP_CHANGED" \ | |
| --arg app_version "$APP_VERSION" \ | |
| --arg modulith_tag "$MODULITH_TAG" \ | |
| --arg modulith_changed "$MODULITH_CHANGED" \ | |
| --arg modulith_version "$MODULITH_VERSION" \ | |
| --arg app_image_repository "$APP_IMAGE_REPOSITORY" \ | |
| --arg app_image_tag "$APP_IMAGE_TAG" \ | |
| --arg app_image_ref "$APP_IMAGE_REF" \ | |
| --arg modulith_image_repository "$MODULITH_IMAGE_REPOSITORY" \ | |
| --arg modulith_image_tag "$MODULITH_IMAGE_TAG" \ | |
| --arg modulith_image_ref "$MODULITH_IMAGE_REF" \ | |
| '{ | |
| event_type: "miot-stack-release", | |
| client_payload: { | |
| release_version: $release_version, | |
| app_tag: $app_tag, | |
| modulith_tag: $modulith_tag, | |
| app_image_repository: $app_image_repository, | |
| app_image_tag: $app_image_tag, | |
| app_image_ref: $app_image_ref, | |
| modulith_image_repository: $modulith_image_repository, | |
| modulith_image_tag: $modulith_image_tag, | |
| modulith_image_ref: $modulith_image_ref | |
| } | |
| }' > stack-dispatch-payload.json | |
| jq -n \ | |
| --arg release_version "$RELEASE_VERSION" \ | |
| --arg stack_tag "$STACK_TAG" \ | |
| --arg app_tag "$APP_TAG" \ | |
| --arg app_changed "$APP_CHANGED" \ | |
| --arg app_version "$APP_VERSION" \ | |
| --arg modulith_tag "$MODULITH_TAG" \ | |
| --arg modulith_changed "$MODULITH_CHANGED" \ | |
| --arg modulith_version "$MODULITH_VERSION" \ | |
| --arg app_image_repository "$APP_IMAGE_REPOSITORY" \ | |
| --arg app_image_tag "$APP_IMAGE_TAG" \ | |
| --arg app_image_ref "$APP_IMAGE_REF" \ | |
| --arg modulith_image_repository "$MODULITH_IMAGE_REPOSITORY" \ | |
| --arg modulith_image_tag "$MODULITH_IMAGE_TAG" \ | |
| --arg modulith_image_ref "$MODULITH_IMAGE_REF" \ | |
| '{ | |
| release_version: $release_version, | |
| stack_version: $release_version, | |
| stack_tag: $stack_tag, | |
| manifest_version: 1, | |
| app_tag: $app_tag, | |
| modulith_tag: $modulith_tag, | |
| app_image_repository: $app_image_repository, | |
| app_image_tag: $app_image_tag, | |
| app_image_ref: $app_image_ref, | |
| modulith_image_repository: $modulith_image_repository, | |
| modulith_image_tag: $modulith_image_tag, | |
| modulith_image_ref: $modulith_image_ref, | |
| components: { | |
| app: { | |
| changed: ($app_changed == "true"), | |
| version: $app_version, | |
| tag: $app_tag, | |
| image_repository: $app_image_repository, | |
| image_tag: $app_image_tag, | |
| image_ref: $app_image_ref | |
| }, | |
| modulith: { | |
| changed: ($modulith_changed == "true"), | |
| version: $modulith_version, | |
| tag: $modulith_tag, | |
| image_repository: $modulith_image_repository, | |
| image_tag: $modulith_image_tag, | |
| image_ref: $modulith_image_ref | |
| } | |
| } | |
| }' > stack-manifest.json | |
| cat stack-dispatch-payload.json | | |
| gh api "repos/${DEPLOY_DISPATCH_REPO}/dispatches" \ | |
| --method POST \ | |
| --input - | |
| echo "Dispatched MIOT v${RELEASE_VERSION} deployment to ${DEPLOY_DISPATCH_REPO}." | |
| - name: Upload stack manifest to release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: gh release upload "${{ needs.prepare.outputs.stack_tag }}" stack-manifest.json --clobber --repo "$GITHUB_REPOSITORY" | |
| close-milestones: | |
| name: Close release milestones | |
| runs-on: ubuntu-latest | |
| needs: [prepare, tag, deploy] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.tag.outputs.sha }} | |
| - name: Close private and OSS milestones | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} | |
| GH_PROJECT_OWNER: microboxlabs | |
| run: | | |
| ./turbo-repo/generative_ai/tools/sh/close-release-milestones.sh \ | |
| --release "${{ needs.prepare.outputs.private_milestone }}" \ | |
| --oss "${{ needs.prepare.outputs.oss_milestone }}" |