Skip to content

MIOT Release Train

MIOT Release Train #37

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
DOCS_IMAGE_NAME: miot-docs
MODULITH_IMAGE_NAME: miot-srv
HARNESS_IMAGE_NAME: miot-harness
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 }}
docs_changed: ${{ steps.plan.outputs.docs_changed }}
docs_version: ${{ steps.plan.outputs.docs_version }}
docs_tag: ${{ steps.plan.outputs.docs_tag }}
modulith_changed: ${{ steps.plan.outputs.modulith_changed }}
modulith_version: ${{ steps.plan.outputs.modulith_version }}
modulith_tag: ${{ steps.plan.outputs.modulith_tag }}
harness_changed: ${{ steps.plan.outputs.harness_changed }}
harness_version: ${{ steps.plan.outputs.harness_version }}
harness_tag: ${{ steps.plan.outputs.harness_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 'docs@v*' | sed 's/^docs@v//'
git tag --list 'modulith@v*' | sed 's/^modulith@v//'
git tag --list 'harness@v*' | sed 's/^harness@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: Create release branch
id: branch
run: |
branch="release/miot-v${{ steps.version.outputs.release_version }}"
if git ls-remote --exit-code --heads origin "$branch" >/dev/null; then
git fetch origin "$branch"
git switch -C "$branch" "origin/$branch"
else
git switch -c "$branch"
fi
echo "branch=$branch" >> "$GITHUB_OUTPUT"
- name: Restore release automation from workflow ref
run: |
git checkout "$GITHUB_SHA" -- \
.github/scripts/resolve-stack-release-plan.js \
.github/prompts/app-release-notes.prompt.yml \
turbo-repo/generative_ai/tools/sh/fetch-release-issues.sh \
turbo-repo/generative_ai/tools/sh/bump-app-version.sh
- 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: 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: Bump harness package version
if: steps.plan.outputs.harness_changed == 'true'
run: |
node - <<'NODE'
const fs = require("fs");
const path = "miot-harness/pyproject.toml";
const version = "${{ steps.plan.outputs.harness_version }}";
const content = fs.readFileSync(path, "utf8");
const match = content.match(/^version = "([^"]+)"$/m);
if (!match) {
throw new Error(`Could not update project.version in ${path}`);
}
if (match[1] !== version) {
fs.writeFileSync(path, content.replace(/^version = ".*"$/m, `version = "${version}"`));
}
NODE
- 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("{{stack_version}}", "${{ steps.version.outputs.release_version }}")
.replaceAll("{{private_milestone}}", "${{ steps.version.outputs.private_milestone }}")
.replaceAll("{{oss_milestone}}", "${{ steps.version.outputs.oss_milestone }}")
.replaceAll("{{stack_tag}}", "${{ steps.plan.outputs.stack_tag }}")
.replaceAll("{{app_tag}}", "${{ steps.plan.outputs.app_tag }}")
.replaceAll("{{app_changed}}", "${{ steps.plan.outputs.app_changed }}")
.replaceAll("{{docs_tag}}", "${{ steps.plan.outputs.docs_tag }}")
.replaceAll("{{docs_changed}}", "${{ steps.plan.outputs.docs_changed }}")
.replaceAll("{{modulith_tag}}", "${{ steps.plan.outputs.modulith_tag }}")
.replaceAll("{{modulith_changed}}", "${{ steps.plan.outputs.modulith_changed }}")
.replaceAll("{{harness_tag}}", "${{ steps.plan.outputs.harness_tag }}")
.replaceAll("{{harness_changed}}", "${{ steps.plan.outputs.harness_changed }}")
.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 miot-harness/pyproject.toml
if git diff --cached --quiet; then
echo "No release prep changes to commit."
else
git commit -m "chore(release): prepare MIOT stack v${{ steps.version.outputs.release_version }}"
git push origin "${{ steps.branch.outputs.branch }}"
fi
- 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 }})
- Docs: ${{ steps.plan.outputs.docs_tag }} (changed: ${{ steps.plan.outputs.docs_changed }})
- Modulith: ${{ steps.plan.outputs.modulith_tag }} (changed: ${{ steps.plan.outputs.modulith_changed }})
- Harness: ${{ steps.plan.outputs.harness_tag }} (changed: ${{ steps.plan.outputs.harness_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 list \
--base trunk \
--head "${{ steps.branch.outputs.branch }}" \
--state open \
--json url \
--jq '.[0].url // ""')"
if [[ -z "$url" ]]; then
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)"
fi
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 [[ "${{ needs.prepare.outputs.harness_changed }}" == "true" ]]; then
harness_version="$(node -p "require('fs').readFileSync('miot-harness/pyproject.toml', 'utf8').match(/^version = \\\"([^\\\"]+)\\\"$/m)?.[1] || process.exit(1)")"
if [[ "$harness_version" != "${{ needs.prepare.outputs.harness_version }}" ]]; then
echo "trunk harness version is $harness_version, expected ${{ needs.prepare.outputs.harness_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.docs_changed }}" == "true" ]]; then
git tag -a "${{ needs.prepare.outputs.docs_tag }}" -m "${{ needs.prepare.outputs.docs_tag }}"
tags+=("${{ needs.prepare.outputs.docs_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
if [[ "${{ needs.prepare.outputs.harness_changed }}" == "true" ]]; then
git tag -a "${{ needs.prepare.outputs.harness_tag }}" -m "${{ needs.prepare.outputs.harness_tag }}"
tags+=("${{ needs.prepare.outputs.harness_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.docs_changed }}" == "true" ]]; then
gh release create "${{ needs.prepare.outputs.docs_tag }}" \
--title "${{ needs.prepare.outputs.docs_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
if [[ "${{ needs.prepare.outputs.harness_changed }}" == "true" ]]; then
gh release create "${{ needs.prepare.outputs.harness_tag }}" \
--title "${{ needs.prepare.outputs.harness_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 }}
fetch-depth: 0
- 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: Resolve build credits
id: credits
if: needs.prepare.outputs.app_changed == 'true'
env:
GIT_SHA: ${{ needs.tag.outputs.sha }}
STACK_TAG: ${{ needs.prepare.outputs.stack_tag }}
GITHUB_TOKEN: ${{ github.token }}
run: |
previous_stack_tag="$(
git tag --list 'miot-stack@v*' --sort=-v:refname |
grep -vx "$STACK_TAG" |
head -1 || true
)"
range="$GIT_SHA"
if [[ -n "$previous_stack_tag" ]]; then
range="${previous_stack_tag}..${GIT_SHA}"
fi
credit_log="$(git log --format='%H%x09%aN%x09%aE' --numstat "$range")"
if [[ -z "$credit_log" ]]; then
credit_log="$(git log -1 --format='%H%x09%aN%x09%aE' --numstat "$GIT_SHA")"
fi
credits_json="$(
GIT_CREDIT_LOG="$credit_log" GITHUB_TOKEN="$GITHUB_TOKEN" node <<'NODE'
const crypto = require("crypto");
const lines = (process.env.GIT_CREDIT_LOG ?? "").split("\n");
const contributors = new Map();
let currentKey = null;
function githubUsername(email) {
return email?.match(/^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/)?.[1];
}
function avatarUrl(email, username) {
if (username) {
return `https://github.com/${username}.png?size=96`;
}
if (!email) {
return undefined;
}
const hash = crypto.createHash("md5").update(email.trim().toLowerCase()).digest("hex");
return `https://www.gravatar.com/avatar/${hash}?d=identicon&s=96`;
}
async function githubProfile(username) {
if (!username) {
return {};
}
const response = await fetch(`https://api.github.com/users/${username}`, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!response.ok) {
return {};
}
const profile = await response.json();
return {
profileName: profile.name || undefined,
bio: profile.bio || undefined,
company: profile.company || undefined,
location: profile.location || undefined,
followers: Number.isFinite(profile.followers) ? profile.followers : undefined,
publicRepos: Number.isFinite(profile.public_repos) ? profile.public_repos : undefined,
};
}
for (const line of lines) {
if (!line.trim()) {
continue;
}
const parts = line.split("\t");
if (parts.length === 3 && /^[0-9a-f]{40}$/.test(parts[0])) {
const [, name, email] = parts;
const username = githubUsername(email);
currentKey = (email || username || name).toLowerCase();
if (!contributors.has(currentKey)) {
contributors.set(currentKey, {
name,
...(email ? { email } : {}),
...(username ? { username, url: `https://github.com/${username}` } : {}),
avatarUrl: avatarUrl(email, username),
commitCount: 0,
filesChanged: 0,
additions: 0,
deletions: 0,
});
}
contributors.get(currentKey).commitCount += 1;
continue;
}
if (!currentKey || parts.length < 3) {
continue;
}
const [added, deleted] = parts;
const credit = contributors.get(currentKey);
credit.filesChanged += 1;
credit.additions += /^\d+$/.test(added) ? Number(added) : 0;
credit.deletions += /^\d+$/.test(deleted) ? Number(deleted) : 0;
}
const allCredits = [...contributors.values()].map((credit) => ({
...credit,
impactScore: credit.commitCount * 25 + credit.filesChanged * 3 + Math.ceil((credit.additions + credit.deletions) / 50),
}));
Promise.all(
allCredits.map(async (credit) => ({
...credit,
...(await githubProfile(credit.username)),
}))
)
.then((enrichedCredits) => {
const humanCredits = enrichedCredits.filter(
(credit) => !credit.name.endsWith("[bot]") && credit.username !== "github-actions"
);
const credits = (humanCredits.length > 0 ? humanCredits : enrichedCredits)
.sort((left, right) => right.commitCount - left.commitCount || right.impactScore - left.impactScore || left.name.localeCompare(right.name))
.map((credit, index) => ({
...credit,
rank: index + 1,
role: index === 0 ? "Top contributor" : "Contributor",
}));
console.log(JSON.stringify(credits));
})
.catch((error) => {
console.error(error);
process.exit(1);
});
NODE
)"
{
echo "json<<EOF"
echo "$credits_json"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Install dependencies
if: needs.prepare.outputs.app_changed == 'true'
run: npm ci
working-directory: turbo-repo
- name: Write packaged build info
if: needs.prepare.outputs.app_changed == 'true'
env:
RELEASE_VERSION: ${{ needs.prepare.outputs.release_version }}
RELEASE_NOTES_VERSION: v${{ needs.prepare.outputs.release_notes_version }}
STACK_TAG: ${{ needs.prepare.outputs.stack_tag }}
GIT_SHA: ${{ needs.tag.outputs.sha }}
SHORT_SHA: ${{ needs.tag.outputs.short_sha }}
WORKFLOW_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
APP_VERSION: ${{ needs.prepare.outputs.app_version }}
APP_TAG: ${{ needs.prepare.outputs.app_tag }}
APP_IMAGE_REPOSITORY: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.APP_IMAGE_NAME }}
APP_IMAGE_TAG: app-v${{ needs.prepare.outputs.app_version }}
DOCS_VERSION: ${{ needs.prepare.outputs.docs_version }}
DOCS_TAG: ${{ needs.prepare.outputs.docs_tag }}
DOCS_IMAGE_REPOSITORY: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.DOCS_IMAGE_NAME }}
DOCS_IMAGE_TAG: docs-v${{ needs.prepare.outputs.docs_version }}
MODULITH_VERSION: ${{ needs.prepare.outputs.modulith_version }}
MODULITH_TAG: ${{ needs.prepare.outputs.modulith_tag }}
MODULITH_IMAGE_REPOSITORY: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.MODULITH_IMAGE_NAME }}
MODULITH_IMAGE_TAG: modulith-v${{ needs.prepare.outputs.modulith_version }}
HARNESS_VERSION: ${{ needs.prepare.outputs.harness_version }}
HARNESS_TAG: ${{ needs.prepare.outputs.harness_tag }}
HARNESS_IMAGE_REPOSITORY: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.HARNESS_IMAGE_NAME }}
HARNESS_IMAGE_TAG: harness-v${{ needs.prepare.outputs.harness_version }}
BUILD_CREDITS_JSON: ${{ steps.credits.outputs.json }}
run: |
mkdir -p turbo-repo/apps/app/public
jq -n \
--arg release_version "$RELEASE_VERSION" \
--arg release_notes_version "$RELEASE_NOTES_VERSION" \
--arg stack_tag "$STACK_TAG" \
--arg git_sha "$GIT_SHA" \
--arg short_sha "$SHORT_SHA" \
--arg built_at "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
--arg workflow_run_url "$WORKFLOW_RUN_URL" \
--arg app_version "$APP_VERSION" \
--arg app_tag "$APP_TAG" \
--arg app_image_repository "$APP_IMAGE_REPOSITORY" \
--arg app_image_tag "$APP_IMAGE_TAG" \
--arg docs_version "$DOCS_VERSION" \
--arg docs_tag "$DOCS_TAG" \
--arg docs_image_repository "$DOCS_IMAGE_REPOSITORY" \
--arg docs_image_tag "$DOCS_IMAGE_TAG" \
--arg modulith_version "$MODULITH_VERSION" \
--arg modulith_tag "$MODULITH_TAG" \
--arg modulith_image_repository "$MODULITH_IMAGE_REPOSITORY" \
--arg modulith_image_tag "$MODULITH_IMAGE_TAG" \
--arg harness_version "$HARNESS_VERSION" \
--arg harness_tag "$HARNESS_TAG" \
--arg harness_image_repository "$HARNESS_IMAGE_REPOSITORY" \
--arg harness_image_tag "$HARNESS_IMAGE_TAG" \
--argjson credits "$BUILD_CREDITS_JSON" \
'{
product: "ModularIoT",
release_version: $release_version,
release_notes_version: $release_notes_version,
channel: "release",
stack_tag: $stack_tag,
git_sha: $git_sha,
short_sha: $short_sha,
built_at: $built_at,
workflow_run_url: $workflow_run_url,
manifest_version: 1,
credits: $credits,
components: {
app: {
version: $app_version,
tag: $app_tag,
image_repository: $app_image_repository,
image_tag: $app_image_tag
},
docs: {
version: $docs_version,
tag: $docs_tag,
image_repository: $docs_image_repository,
image_tag: $docs_image_tag
},
modulith: {
version: $modulith_version,
tag: $modulith_tag,
image_repository: $modulith_image_repository,
image_tag: $modulith_image_tag
},
harness: {
version: $harness_version,
tag: $harness_tag,
image_repository: $harness_image_repository,
image_tag: $harness_image_tag
}
}
}' > turbo-repo/apps/app/public/build-info.json
- 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-docs:
name: Resolve docs image
runs-on: ubuntu-latest
needs: [prepare, tag]
outputs:
image: ${{ steps.image.outputs.image }}
steps:
- uses: actions/checkout@v5
if: needs.prepare.outputs.docs_changed == 'true'
with:
ref: ${{ needs.tag.outputs.sha }}
fetch-depth: 0
- uses: actions/setup-node@v6
if: needs.prepare.outputs.docs_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.docs_changed == 'true'
run: npm ci
working-directory: turbo-repo
- name: Build docs
if: needs.prepare.outputs.docs_changed == 'true'
run: npx turbo run build --filter=@modulariot/docs
working-directory: turbo-repo
- name: Stage docs artifact
if: needs.prepare.outputs.docs_changed == 'true'
run: |
mkdir -p build-context/.next
cp -r turbo-repo/apps/docs/.next/standalone build-context/.next/standalone
cp -r turbo-repo/apps/docs/.next/static build-context/.next/static
cp -r turbo-repo/apps/docs/public build-context/public
cp turbo-repo/docker/nextjs.standalone-docs.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.docs_changed == 'true'
id: docker
uses: docker/build-push-action@v6
with:
context: build-context/
file: build-context/nextjs.standalone-docs.Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.DOCS_IMAGE_NAME }}:${{ needs.prepare.outputs.docs_version }}
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.DOCS_IMAGE_NAME }}:docs-v${{ needs.prepare.outputs.docs_version }}
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.DOCS_IMAGE_NAME }}:sha-${{ needs.tag.outputs.short_sha }}
provenance: true
sbom: true
- name: Export image ref
id: image
run: |
if [[ "${{ needs.prepare.outputs.docs_changed }}" == "true" ]]; then
digest="${{ steps.docker.outputs.digest }}"
else
digest="$(docker buildx imagetools inspect "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.DOCS_IMAGE_NAME }}:docs-v${{ needs.prepare.outputs.docs_version }}" | awk '/^Digest:/ { print $2; exit }')"
fi
echo "image=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.DOCS_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"
build-harness:
name: Resolve harness 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 harness image
if: needs.prepare.outputs.harness_changed == 'true'
env:
SOURCE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.HARNESS_IMAGE_NAME }}:sha-${{ needs.tag.outputs.short_sha }}
run: |
for attempt in {1..60}; do
if docker buildx imagetools inspect "$SOURCE_IMAGE" >/dev/null; then
echo "Promoting release harness image $SOURCE_IMAGE."
exit 0
fi
echo "Waiting for Harness CI to publish release image... ($attempt/60)"
sleep 60
done
echo "::error::Timed out waiting for Harness CI to publish release image."
exit 1
- name: Promote image tags
if: needs.prepare.outputs.harness_changed == 'true'
env:
SOURCE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.HARNESS_IMAGE_NAME }}:sha-${{ needs.tag.outputs.short_sha }}
RELEASE_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.HARNESS_IMAGE_NAME }}:${{ needs.prepare.outputs.harness_version }}
COMPONENT_IMAGE: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.HARNESS_IMAGE_NAME }}:harness-v${{ needs.prepare.outputs.harness_version }}
run: |
docker buildx imagetools create \
--tag "$RELEASE_IMAGE" \
--tag "$COMPONENT_IMAGE" \
"$SOURCE_IMAGE"
- name: Export image ref
id: image
run: |
digest="$(docker buildx imagetools inspect "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.HARNESS_IMAGE_NAME }}:harness-v${{ needs.prepare.outputs.harness_version }}" | awk '/^Digest:/ { print $2; exit }')"
echo "image=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.HARNESS_IMAGE_NAME }}@${digest}" >> "$GITHUB_OUTPUT"
deploy:
name: Dispatch configured environment deploys
runs-on: ubuntu-latest
needs: [prepare, tag, build-app, build-docs, build-modulith, build-harness]
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 }}
RELEASE_NOTES_VERSION: v${{ needs.prepare.outputs.release_notes_version }}
BUILD_CHANNEL: release
STACK_TAG: ${{ needs.prepare.outputs.stack_tag }}
GIT_SHA: ${{ needs.tag.outputs.sha }}
SHORT_SHA: ${{ needs.tag.outputs.short_sha }}
WORKFLOW_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
APP_TAG: ${{ needs.prepare.outputs.app_tag }}
DOCS_TAG: ${{ needs.prepare.outputs.docs_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 }}
DOCS_IMAGE_REPOSITORY: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.DOCS_IMAGE_NAME }}
DOCS_IMAGE_TAG: docs-v${{ needs.prepare.outputs.docs_version }}
DOCS_IMAGE_REF: ${{ needs.build-docs.outputs.image }}
DOCS_CHANGED: ${{ needs.prepare.outputs.docs_changed }}
DOCS_VERSION: ${{ needs.prepare.outputs.docs_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 }}
HARNESS_TAG: ${{ needs.prepare.outputs.harness_tag }}
HARNESS_IMAGE_REPOSITORY: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.HARNESS_IMAGE_NAME }}
HARNESS_IMAGE_TAG: harness-v${{ needs.prepare.outputs.harness_version }}
HARNESS_IMAGE_REF: ${{ needs.build-harness.outputs.image }}
HARNESS_CHANGED: ${{ needs.prepare.outputs.harness_changed }}
HARNESS_VERSION: ${{ needs.prepare.outputs.harness_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
BUILT_AT="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
jq -n \
--arg release_version "$RELEASE_VERSION" \
--arg release_notes_version "$RELEASE_NOTES_VERSION" \
--arg build_channel "$BUILD_CHANNEL" \
--arg stack_tag "$STACK_TAG" \
--arg git_sha "$GIT_SHA" \
--arg short_sha "$SHORT_SHA" \
--arg built_at "$BUILT_AT" \
--arg workflow_run_url "$WORKFLOW_RUN_URL" \
--arg app_tag "$APP_TAG" \
--arg app_changed "$APP_CHANGED" \
--arg app_version "$APP_VERSION" \
--arg docs_tag "$DOCS_TAG" \
--arg docs_changed "$DOCS_CHANGED" \
--arg docs_version "$DOCS_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 docs_image_repository "$DOCS_IMAGE_REPOSITORY" \
--arg docs_image_tag "$DOCS_IMAGE_TAG" \
--arg docs_image_ref "$DOCS_IMAGE_REF" \
--arg modulith_image_repository "$MODULITH_IMAGE_REPOSITORY" \
--arg modulith_image_tag "$MODULITH_IMAGE_TAG" \
--arg modulith_image_ref "$MODULITH_IMAGE_REF" \
--arg harness_tag "$HARNESS_TAG" \
--arg harness_changed "$HARNESS_CHANGED" \
--arg harness_version "$HARNESS_VERSION" \
--arg harness_image_repository "$HARNESS_IMAGE_REPOSITORY" \
--arg harness_image_tag "$HARNESS_IMAGE_TAG" \
--arg harness_image_ref "$HARNESS_IMAGE_REF" \
'{
event_type: "miot-stack-release",
client_payload: {
release_version: $release_version,
release_notes_version: $release_notes_version,
channel: $build_channel,
stack_tag: $stack_tag,
git_sha: $git_sha,
short_sha: $short_sha,
built_at: $built_at,
workflow_run_url: $workflow_run_url,
manifest_version: 1,
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
},
docs: {
changed: ($docs_changed == "true"),
version: $docs_version,
tag: $docs_tag,
image_repository: $docs_image_repository,
image_tag: $docs_image_tag,
image_ref: $docs_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
},
harness: {
changed: ($harness_changed == "true"),
version: $harness_version,
tag: $harness_tag,
image_repository: $harness_image_repository,
image_tag: $harness_image_tag,
image_ref: $harness_image_ref
}
}
}
}' > stack-dispatch-payload.json
jq -n \
--arg release_version "$RELEASE_VERSION" \
--arg release_notes_version "$RELEASE_NOTES_VERSION" \
--arg build_channel "$BUILD_CHANNEL" \
--arg stack_tag "$STACK_TAG" \
--arg git_sha "$GIT_SHA" \
--arg short_sha "$SHORT_SHA" \
--arg built_at "$BUILT_AT" \
--arg workflow_run_url "$WORKFLOW_RUN_URL" \
--arg app_tag "$APP_TAG" \
--arg app_changed "$APP_CHANGED" \
--arg app_version "$APP_VERSION" \
--arg docs_tag "$DOCS_TAG" \
--arg docs_changed "$DOCS_CHANGED" \
--arg docs_version "$DOCS_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 docs_image_repository "$DOCS_IMAGE_REPOSITORY" \
--arg docs_image_tag "$DOCS_IMAGE_TAG" \
--arg docs_image_ref "$DOCS_IMAGE_REF" \
--arg modulith_image_repository "$MODULITH_IMAGE_REPOSITORY" \
--arg modulith_image_tag "$MODULITH_IMAGE_TAG" \
--arg modulith_image_ref "$MODULITH_IMAGE_REF" \
--arg harness_tag "$HARNESS_TAG" \
--arg harness_changed "$HARNESS_CHANGED" \
--arg harness_version "$HARNESS_VERSION" \
--arg harness_image_repository "$HARNESS_IMAGE_REPOSITORY" \
--arg harness_image_tag "$HARNESS_IMAGE_TAG" \
--arg harness_image_ref "$HARNESS_IMAGE_REF" \
'{
release_version: $release_version,
stack_version: $release_version,
release_notes_version: $release_notes_version,
channel: $build_channel,
stack_tag: $stack_tag,
git_sha: $git_sha,
short_sha: $short_sha,
built_at: $built_at,
workflow_run_url: $workflow_run_url,
manifest_version: 1,
app_tag: $app_tag,
docs_tag: $docs_tag,
modulith_tag: $modulith_tag,
harness_tag: $harness_tag,
app_image_repository: $app_image_repository,
app_image_tag: $app_image_tag,
app_image_ref: $app_image_ref,
docs_image_repository: $docs_image_repository,
docs_image_tag: $docs_image_tag,
docs_image_ref: $docs_image_ref,
modulith_image_repository: $modulith_image_repository,
modulith_image_tag: $modulith_image_tag,
modulith_image_ref: $modulith_image_ref,
harness_image_repository: $harness_image_repository,
harness_image_tag: $harness_image_tag,
harness_image_ref: $harness_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
},
docs: {
changed: ($docs_changed == "true"),
version: $docs_version,
tag: $docs_tag,
image_repository: $docs_image_repository,
image_tag: $docs_image_tag,
image_ref: $docs_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
},
harness: {
changed: ($harness_changed == "true"),
version: $harness_version,
tag: $harness_tag,
image_repository: $harness_image_repository,
image_tag: $harness_image_tag,
image_ref: $harness_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 }}"