name: Release HealthChat # Tag-driven release pipeline for HealthChat builds of element-web. # Mirrors ../element-ios/.github/workflows/release-healthchat.yml. # Runbook: .claude/skills/releasing-healthchat/SKILL.md on: push: tags: - "healthchat-v*" workflow_dispatch: inputs: version: description: "Version (e.g. 1.4.0). Required when running manually." required: true type: string workspace_ref: description: "Override healthchat-workspace ref (defaults to .healthchat-workspace-version)." required: false type: string release_notes_override: description: "Optional release notes; falls back to HEALTHCHAT_CHANGES.md section for the given version." required: false type: string permissions: contents: write # create GitHub Release packages: write # push Docker image id-token: write # attestations attestations: write concurrency: group: release-healthchat-${{ github.event.inputs.version || github.ref_name }} cancel-in-progress: false jobs: release: name: Build & publish HealthChat image runs-on: ubuntu-latest env: DOCKER_REGISTRY: "ghcr.io" DOCKER_IMAGE: ${{ format('{0}/healthchat-web', github.repository) }} DOCKER_PUSH: true DOCKER_SIGNING: false FORCE_COLOR: 1 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Resolve version id: version run: | set -euo pipefail if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then VERSION="${{ github.event.inputs.version }}" else VERSION="${GITHUB_REF_NAME#healthchat-v}" fi if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then echo "::error::Invalid version '$VERSION' — expected e.g. 1.4.0 or 1.4.0-rc1" exit 1 fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" # `:latest` is what the ansible inventory deploys from, so only a clean # X.Y.Z tag push may move it — never an -rc/-hotfix/-dryrun tag, and never # a manual re-run. Otherwise a release candidate or a pipeline test would # become production on the next deploy of any kind. if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ && "$GITHUB_EVENT_NAME" == "push" ]]; then IS_STABLE=true else IS_STABLE=false fi echo "is_stable=$IS_STABLE" >> "$GITHUB_OUTPUT" echo "Resolved version: $VERSION (moves :latest: $IS_STABLE)" - name: Resolve workspace ref id: workspace env: OVERRIDE: ${{ github.event.inputs.workspace_ref }} run: | set -euo pipefail if [[ -n "$OVERRIDE" ]]; then REF="$OVERRIDE" echo "Using workspace ref from workflow input: $REF" elif [[ -f .healthchat-workspace-version ]]; then REF=$(head -n1 .healthchat-workspace-version | tr -d '[:space:]') if [[ -z "$REF" ]]; then echo "::error::.healthchat-workspace-version is empty" exit 1 fi echo "Using workspace ref from .healthchat-workspace-version: $REF" else echo "::error::.healthchat-workspace-version missing and no workspace_ref override provided" exit 1 fi echo "ref=$REF" >> "$GITHUB_OUTPUT" - name: Extract release notes id: notes env: VERSION: ${{ steps.version.outputs.version }} OVERRIDE: ${{ github.event.inputs.release_notes_override }} run: | set -euo pipefail if [[ -n "$OVERRIDE" ]]; then NOTES="$OVERRIDE" echo "Using release notes from workflow input" else NOTES=$(awk -v ver="$VERSION" ' /^## Changes in / { if (found) { exit } if ($0 ~ "## Changes in " ver "( |$|\\()") { found = 1; next } } found { print } ' HEALTHCHAT_CHANGES.md | sed '/^[[:space:]]*$/{N;/^\n$/D;}') if [[ -z "$NOTES" ]]; then NOTES="HealthChat ${VERSION} — automated build from ${GITHUB_REF_NAME}." echo "::warning::No HEALTHCHAT_CHANGES.md section found for $VERSION — using fallback" fi fi { echo 'notes<> "$GITHUB_OUTPUT" - name: Checkout healthchat-workspace uses: actions/checkout@v4 with: repository: Novaloop-AG/healthchat-workspace path: healthchat-workspace token: ${{ secrets.HEALTHCHAT_WORKSPACE_TOKEN }} ref: ${{ steps.workspace.outputs.ref }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 - name: Build element-plugin run: | cd healthchat-workspace/element-plugin npm ci npm run build cd module npm ci npm run build - name: Stage plugin artifacts for Docker run: | mkdir -p plugin-dist/elements cp healthchat-workspace/element-plugin/module/dist/healthchat-module.js plugin-dist/healthchat-module.js cp healthchat-workspace/element-plugin/dist/elements/browser/main.js plugin-dist/elements/healthchat-elements.js - name: Log in to the Github Container registry uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: metadata uses: docker/metadata-action@v5 with: images: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }} tags: | type=raw,value=${{ steps.version.outputs.version }} type=raw,value=latest,enable=${{ steps.version.outputs.is_stable == 'true' }} type=raw,value=${{ github.sha }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and push Docker images id: push uses: docker/build-push-action@v6 with: context: . file: ./apps/web/Dockerfile push: ${{ env.DOCKER_PUSH }} tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} - name: Generate artifact attestation if: ${{ env.DOCKER_SIGNING == 'true' }} uses: actions/attest-build-provenance@v1 with: subject-name: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: ${{ env.DOCKER_PUSH }} - name: Create GitHub Release if: github.event_name == 'push' uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: HealthChat ${{ steps.version.outputs.version }} body: ${{ steps.notes.outputs.notes }}