fix(runtime): improve read-aloud page navigation (#608) #165
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: Release | |
| on: | |
| push: | |
| branches: [main, develop] | |
| workflow_dispatch: | |
| inputs: | |
| release_type: | |
| description: "Version increment" | |
| required: true | |
| default: beta | |
| type: choice | |
| options: | |
| - beta | |
| - beta-minor | |
| - beta-major | |
| - patch | |
| - minor | |
| - major | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| # Opt JS actions into Node 24 ahead of the June 2026 default — silences the | |
| # Node 20 deprecation warning until upstream actions ship Node 24 majors. | |
| # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| prepare: | |
| if: >- | |
| ${{ | |
| contains(fromJSON('["main", "develop"]'), github.ref_name) | |
| && (github.event_name == 'workflow_dispatch' || startsWith(github.event.head_commit.message, 'RELEASE:')) | |
| }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| outputs: | |
| tag: ${{ steps.tag.outputs.tag }} | |
| version: ${{ steps.tag.outputs.version }} | |
| prerelease: ${{ steps.tag.outputs.prerelease }} | |
| docker_tags: ${{ steps.tag.outputs.docker_tags }} | |
| prev_tag: ${{ steps.tag.outputs.prev_tag }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Debug — list git tags visible to the runner | |
| run: | | |
| echo "Total tags: $(git tag --list | wc -l)" | |
| git tag --list | sort -V | |
| - name: Calculate release version | |
| id: tag | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| INPUT_RELEASE_TYPE: ${{ inputs.release_type }} | |
| BRANCH: ${{ github.ref_name }} | |
| run: | | |
| MSG="" | |
| if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then | |
| RELEASE_TYPE="$INPUT_RELEASE_TYPE" | |
| else | |
| MSG=$(git log -1 --pretty=%s) | |
| shopt -s nocasematch | |
| if [[ "$MSG" =~ ^release:[[:space:]]*(major|minor|patch|beta-minor|beta-major|beta)[[:space:]]*$ ]]; then | |
| RELEASE_TYPE="${BASH_REMATCH[1],,}" | |
| fi | |
| shopt -u nocasematch | |
| fi | |
| if [[ -z "$RELEASE_TYPE" ]]; then | |
| echo "::error::Could not resolve release type. event=$EVENT_NAME input='$INPUT_RELEASE_TYPE' subject='$MSG'" | |
| exit 1 | |
| fi | |
| if [[ "$BRANCH" == "develop" && "$RELEASE_TYPE" != beta* ]]; then | |
| echo "::error::develop only ships beta releases. Select beta, beta-minor, or beta-major." | |
| exit 1 | |
| fi | |
| if [[ "$BRANCH" == "main" && "$RELEASE_TYPE" == beta* ]]; then | |
| echo "::error::main only ships stable releases. Select patch, minor, or major." | |
| exit 1 | |
| fi | |
| VERSION=$(node scripts/calculate-release-version.mjs "$RELEASE_TYPE") | |
| TAG="v$VERSION" | |
| if [[ "$RELEASE_TYPE" == beta* ]]; then | |
| PRERELEASE=true | |
| else | |
| PRERELEASE=false | |
| fi | |
| if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then | |
| echo "::error::Calculated tag $TAG already exists on origin." | |
| exit 1 | |
| fi | |
| DOCKER_TAGS="ghcr.io/unicef/adt-studio:$TAG" | |
| if [[ "$PRERELEASE" == "false" ]]; then | |
| DOCKER_TAGS="$DOCKER_TAGS"$'\n'"ghcr.io/unicef/adt-studio:latest" | |
| fi | |
| # Changelog baseline: pin the previous tag so release-note generation | |
| # never falls back to the repo's first commit when GitHub can't infer | |
| # an ancestor (stable and beta tags live on divergent branch lines). | |
| # Stable releases diff against the previous stable so the notes cover | |
| # the whole beta cycle; prereleases diff against the most recent tag. | |
| if [[ "$PRERELEASE" == "false" ]]; then | |
| PREV_TAG=$(git tag --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1) | |
| else | |
| PREV_TAG=$(git tag --list 'v*' | sort -V | tail -1) | |
| fi | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT" | |
| echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "docker_tags<<EOF" | |
| echo "$DOCKER_TAGS" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Resolved tag: $TAG (type: $RELEASE_TYPE, prerelease: $PRERELEASE)" | |
| - uses: pnpm/action-setup@v4 | |
| # No `cache: pnpm` here: this job never runs `pnpm install`, so the | |
| # pnpm store is never created and the post-run cache save fails with | |
| # "Path Validation Error: Path(s) specified ... do(es) not exist". | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Update issue template version dropdowns | |
| env: | |
| TAG: ${{ steps.tag.outputs.tag }} | |
| run: node scripts/update-issue-template-versions.mjs "$TAG" | |
| - name: Bump apps/desktop package.json | |
| env: | |
| VERSION: ${{ steps.tag.outputs.version }} | |
| run: | | |
| cd apps/desktop | |
| pnpm version "$VERSION" --no-git-tag-version | |
| - name: Upload staged release metadata | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-metadata | |
| include-hidden-files: true # .github is a dotfile dir | |
| retention-days: 1 | |
| path: | | |
| .github/ISSUE_TEMPLATE | |
| apps/desktop/package.json | |
| desktop: | |
| needs: prepare | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # macos-26 (Tahoe): Xcode 26 is the native default and frameworks match. | |
| # On macos-latest (currently macos-15), Xcode 26's actool dyld-loads | |
| # mismatched CoreMedia/MediaToolbox symbols and crashes. | |
| os: [macos-26, windows-latest, ubuntu-latest] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 60 # mac signing/notarization can take a while | |
| environment: desktop | |
| permissions: | |
| contents: read | |
| env: | |
| APP_VERSION: ${{ needs.prepare.outputs.version }} | |
| # SKIP_NOTARIZE accepts ALL, MAC, WIN, LINUX (comma/space separated). | |
| # "true" is treated as ALL for backward compatibility. | |
| SKIP_NOTARIZE: ${{ vars.SKIP_NOTARIZE }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build workspace (root) | |
| run: pnpm build | |
| - name: Build desktop | |
| run: pnpm --filter @adt/desktop build | |
| # electron-builder >=26 calls `actool` to compose the macOS icon asset | |
| # catalog and requires actool >= 26.0 (Xcode 26+). The runner has it | |
| # installed but defaults to an older Xcode; select the latest stable. | |
| - name: Select latest Xcode (for actool) | |
| if: matrix.os == 'macos-26' | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Package MacOS | |
| if: matrix.os == 'macos-26' | |
| run: pnpm --filter @adt/desktop exec electron-builder --mac --publish never | |
| env: | |
| # macOS signing + notarization | |
| # CSC_LINK — the base64-encoded .p12 content | |
| # CSC_KEY_PASSWORD — the certificate password | |
| CSC_LINK: ${{ secrets.CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| - name: Resolve Windows signing skip | |
| id: skip | |
| if: matrix.os == 'windows-latest' | |
| shell: bash | |
| run: | | |
| tokens=" $(echo "${SKIP_NOTARIZE:-}" | tr '[:lower:]' '[:upper:]' | tr ',' ' ') " | |
| skip=false | |
| case "$tokens" in | |
| *" ALL "*|*" TRUE "*|*" WIN "*) skip=true ;; | |
| esac | |
| echo "win=$skip" >> "$GITHUB_OUTPUT" | |
| - name: Acquire Azure code signing token | |
| if: matrix.os == 'windows-latest' && steps.skip.outputs.win != 'true' | |
| shell: bash | |
| env: | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} | |
| run: | | |
| if [ -z "${AZURE_TENANT_ID:-}" ] || [ -z "${AZURE_CLIENT_ID:-}" ] || [ -z "${AZURE_CLIENT_SECRET:-}" ]; then | |
| echo "::error::Missing Azure credentials (AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET)." | |
| exit 1 | |
| fi | |
| TOKEN=$(curl -fsS -X POST \ | |
| -H "Content-Type: application/x-www-form-urlencoded" \ | |
| -d "grant_type=client_credentials&client_id=${AZURE_CLIENT_ID}&client_secret=${AZURE_CLIENT_SECRET}&resource=https%3A%2F%2Fcodesigning.azure.net" \ | |
| "https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/token" \ | |
| | jq -r '.access_token') | |
| if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then | |
| echo "::error::Failed to obtain AZ_TOKEN from Azure OAuth endpoint." | |
| exit 1 | |
| fi | |
| echo "::add-mask::$TOKEN" | |
| echo "AZ_TOKEN=$TOKEN" >> "$GITHUB_ENV" | |
| - name: Set up Java (for jsign) | |
| if: matrix.os == 'windows-latest' | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: "21" | |
| - name: Cache jsign.jar | |
| if: matrix.os == 'windows-latest' | |
| id: jsign-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: apps/desktop/jsign.jar | |
| key: jsign-7.1 | |
| - name: Download jsign.jar | |
| if: matrix.os == 'windows-latest' && steps.jsign-cache.outputs.cache-hit != 'true' | |
| shell: bash | |
| run: | | |
| curl -fsSL -o apps/desktop/jsign.jar \ | |
| https://repo1.maven.org/maven2/net/jsign/jsign/7.1/jsign-7.1.jar | |
| - name: Package Windows | |
| if: matrix.os == 'windows-latest' | |
| run: pnpm --filter @adt/desktop exec electron-builder --win --publish never | |
| - name: Package Linux | |
| if: matrix.os == 'ubuntu-latest' | |
| run: pnpm --filter @adt/desktop exec electron-builder --linux --publish never | |
| - name: Collect installers | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: installers-${{ matrix.os }} | |
| retention-days: 1 | |
| if-no-files-found: error | |
| path: | | |
| apps/desktop/release/*.dmg | |
| apps/desktop/release/*.zip | |
| apps/desktop/release/*.exe | |
| apps/desktop/release/*.deb | |
| apps/desktop/release/*.AppImage | |
| apps/desktop/release/*.blockmap | |
| apps/desktop/release/latest*.yml | |
| apps/desktop/release/beta*.yml | |
| docker: | |
| needs: [prepare, desktop] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: app | |
| push: true | |
| tags: ${{ needs.prepare.outputs.docker_tags }} | |
| finalize: | |
| needs: [prepare, desktop, docker] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write | |
| pull-requests: read | |
| outputs: | |
| tag: ${{ needs.prepare.outputs.tag }} | |
| version: ${{ needs.prepare.outputs.version }} | |
| sha: ${{ steps.commit.outputs.sha }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.RELEASE_PAT }} | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Restore staged release metadata | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-metadata | |
| path: . | |
| - name: Download installers | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: installers-* | |
| path: dist | |
| merge-multiple: true # flatten the per-OS artifacts into one dir | |
| - name: Compose beta release notes | |
| if: needs.prepare.outputs.prerelease == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| REPO: ${{ github.repository }} | |
| BRANCH: ${{ github.ref_name }} | |
| TRIGGER_SHA: ${{ github.sha }} | |
| run: | | |
| rm -f release-notes.tmp release-notes.md | |
| if node scripts/compose-release-notes.mjs > release-notes.tmp && [ -s release-notes.tmp ]; then | |
| mv release-notes.tmp release-notes.md | |
| else | |
| rm -f release-notes.tmp release-notes.md | |
| echo "::warning::Could not compose release notes; gh release create will generate them." | |
| fi | |
| - name: Commit release metadata and push tag | |
| id: commit | |
| env: | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| BRANCH: ${{ github.ref_name }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add .github/ISSUE_TEMPLATE/*.yml apps/desktop/package.json | |
| if git diff --staged --quiet; then | |
| echo "No metadata changes to commit; pushing tag only." | |
| git tag "$TAG" | |
| git push origin "$TAG" | |
| else | |
| git commit -m "chore(release): $TAG [skip ci]" | |
| git tag "$TAG" | |
| git push origin --atomic HEAD:"$BRANCH" "refs/tags/$TAG" | |
| fi | |
| echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Generate standalone docker-compose.yml | |
| env: | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| run: | | |
| sed "s/__TAG__/${TAG}/g" docker/compose-release.yml.template > docker-compose.yml | |
| - name: Create GitHub release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| REPO: ${{ github.repository }} | |
| PRERELEASE: ${{ needs.prepare.outputs.prerelease }} | |
| PREV_TAG: ${{ needs.prepare.outputs.prev_tag }} | |
| run: | | |
| PRERELEASE_FLAG="" | |
| [[ "$PRERELEASE" == "true" ]] && PRERELEASE_FLAG="--prerelease" | |
| START_TAG_FLAG=() | |
| [[ -n "$PREV_TAG" ]] && START_TAG_FLAG=(--notes-start-tag "$PREV_TAG") | |
| NOTES_ARGS=(--generate-notes "${START_TAG_FLAG[@]}") | |
| if [[ "$PRERELEASE" == "true" && -s release-notes.md ]]; then | |
| NOTES_ARGS=(--notes-file release-notes.md) | |
| fi | |
| gh release create "$TAG" --repo "$REPO" \ | |
| --title "ADT Studio $TAG" \ | |
| "${NOTES_ARGS[@]}" $PRERELEASE_FLAG \ | |
| dist/* \ | |
| docker-compose.yml \ | |
| scripts/windows-setup-and-run.bat |