Skip to content

Commit e8cd6dc

Browse files
authored
Feature(Workflow): New beta tests workflow (#587)
* feat(desktop): add beta version browser with downgrade support * ci: run CI on push to develop and add merge_group trigger Add `develop` to push branches so merges run the full test suite post-merge, and add the `merge_group` trigger to make CI merge-queue ready. Harden the i18n gate on push/merge_group by making the compile step fail hard instead of continuing (auto-translate commit-back only makes sense on pull_request events). * fix(release): parse full semver pre-releases and unify version logic Rewrite the release version model around real semver pre-release identifiers instead of the ad-hoc {beta, staging} fields, so historical tags like v0.7.0-rc.* and v0.4.0-electron parse and order correctly. - scripts/release-version.mjs is now the single source of truth for parsing and comparing release versions; the desktop release catalog delegates to it (via a new @root alias to the repo root) instead of duplicating the regex and comparator - add isBetaVersion/betaNumberOf helpers and restrict the issue-template beta slots to -beta tags, keeping rc/electron pre-releases out of the dropdown - ship a release-version.d.mts declaration so the Electron main process can type-check the shared .mjs module * feat(release): add beta release provenance * refactor: split components in diferent files * feat(release): show cover art and metadata in beta version browser - parse release presentation (title, description, cover image) from release notes and expose it through the desktop release catalog and preload type definitions - redesign the beta version browser with cover thumbnails, a search hero, direction badges, and dedicated loading/empty/search states - replace the downgrade confirmation modal with a discreet popover (no overlay) - group all beta-only browser components under components/updates/beta and add ScrollArea and DotGrid UI primitives - scroll beta browser content through the new ScrollArea and add staggered entrance animations - update i18n catalogs (en, es, fr, pt-BR, sq) and remove the superseded BETA_RELEASE_PROVENANCE spec * feat: add version size on card * fix(release): fail staging listener on conflicting PR instead of silent skip * refactor(release): trigger PR staging manually and require a green CI run * refactor(staging): unify PR staging into a single branch-driven workflow * feat: add skip notarize * feat: add skip notorize to macos * feat: add publish pre-release step * fix: PR detecting and add pre-release cleanup * feat(staging): dispatch staging by PR number from develop * fix(staging): run cleanup via pull_request_target from develop * refactor: remove useless code and deadcode * fix(beta): address PR review findings in beta update flow - Rank numbered betas (beta.N) above per-PR staging builds (beta-<slug>) of the same core in compareReleaseVersions, so staging builds no longer sort above real betas or get auto-selected by the updater's upgrade pick - Restrict release-notes images to the trusted GitHub host allowlist (shared trustedAssetUrl), closing a tracking-pixel vector from attacker-influenceable beta tags - Use isLoading instead of isPending in BetaVersionsView so the loading flag clears when the versions query is disabled (empty state reachable) - Validate TRIGGER_SHA and TAG in compose-release-notes before they reach git as bare arguments, preventing option-injection - Document the load-bearing host-pinning at the runtime setFeedURL call
1 parent 8c4254b commit e8cd6dc

61 files changed

Lines changed: 5709 additions & 873 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "Protect release tags",
3+
"target": "tag",
4+
"enforcement": "active",
5+
"conditions": {
6+
"ref_name": {
7+
"include": ["refs/tags/v*"],
8+
"exclude": []
9+
}
10+
},
11+
"rules": [
12+
{ "type": "creation" },
13+
{ "type": "update" },
14+
{ "type": "deletion" }
15+
],
16+
"bypass_actors": [
17+
{
18+
"actor_id": 5,
19+
"actor_type": "RepositoryRole",
20+
"bypass_mode": "always"
21+
}
22+
]
23+
}

.github/workflows/ci.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ name: CI
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, develop]
66
pull_request:
7+
types: [opened, synchronize, reopened, ready_for_review]
8+
merge_group:
79

810
permissions:
9-
contents: write
11+
contents: read
1012

1113
jobs:
1214
test:
@@ -31,6 +33,8 @@ jobs:
3133

3234
i18n:
3335
runs-on: ubuntu-latest
36+
permissions:
37+
contents: write
3438
steps:
3539
- uses: actions/checkout@v4
3640
- uses: pnpm/action-setup@v4
@@ -62,7 +66,7 @@ jobs:
6266
- name: Check translations are up to date
6367
id: compile
6468
run: pnpm --filter @adt/studio compile --strict
65-
continue-on-error: true
69+
continue-on-error: ${{ github.event_name == 'pull_request' }}
6670
- name: Warn about missing translations
6771
if: steps.compile.outcome == 'failure'
6872
run: |

.github/workflows/release.yml

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,24 @@ on:
55
branches: [main, develop]
66
workflow_dispatch:
77
inputs:
8-
tag:
9-
description: 'Release tag (e.g. v0.8.0 on main, v0.8.0-beta.1 on develop)'
8+
release_type:
9+
description: "Version increment"
1010
required: true
11+
default: beta
12+
type: choice
13+
options:
14+
- beta
15+
- beta-minor
16+
- beta-major
17+
- patch
18+
- minor
19+
- major
1120

1221
permissions:
1322
contents: read
1423

1524
concurrency:
16-
group: release-${{ github.ref_name }}
25+
group: release
1726
cancel-in-progress: false
1827

1928
# Opt JS actions into Node 24 ahead of the June 2026 default — silences the
@@ -47,52 +56,47 @@ jobs:
4756
echo "Total tags: $(git tag --list | wc -l)"
4857
git tag --list | sort -V
4958
50-
- name: Extract tag
59+
- name: Calculate release version
5160
id: tag
5261
env:
5362
EVENT_NAME: ${{ github.event_name }}
54-
INPUT_TAG: ${{ inputs.tag }}
63+
INPUT_RELEASE_TYPE: ${{ inputs.release_type }}
5564
BRANCH: ${{ github.ref_name }}
5665
run: |
5766
MSG=""
5867
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
59-
TAG="$INPUT_TAG"
68+
RELEASE_TYPE="$INPUT_RELEASE_TYPE"
6069
else
6170
MSG=$(git log -1 --pretty=%s)
6271
shopt -s nocasematch
63-
if [[ "$MSG" =~ ^release:[[:space:]]*([^[:space:]]+) ]]; then
64-
TAG="${BASH_REMATCH[1]}"
72+
if [[ "$MSG" =~ ^release:[[:space:]]*(major|minor|patch|beta-minor|beta-major|beta)[[:space:]]*$ ]]; then
73+
RELEASE_TYPE="${BASH_REMATCH[1],,}"
6574
fi
6675
shopt -u nocasematch
6776
fi
68-
if [[ -z "$TAG" ]]; then
69-
echo "::error::Could not extract tag. event=$EVENT_NAME input='$INPUT_TAG' subject='$MSG'"
77+
if [[ -z "$RELEASE_TYPE" ]]; then
78+
echo "::error::Could not resolve release type. event=$EVENT_NAME input='$INPUT_RELEASE_TYPE' subject='$MSG'"
7079
exit 1
7180
fi
72-
[[ "$TAG" =~ ^v ]] || TAG="v$TAG"
73-
# Validate charset before propagating downstream. This both rejects
74-
# malformed tags and blocks shell / GITHUB_OUTPUT injection via the tag.
75-
if [[ ! "$TAG" =~ ^v[0-9A-Za-z.+_-]+$ ]]; then
76-
echo "::error::Invalid tag format: $TAG"
81+
if [[ "$BRANCH" == "develop" && "$RELEASE_TYPE" != beta* ]]; then
82+
echo "::error::develop only ships beta releases. Select beta, beta-minor, or beta-major."
7783
exit 1
7884
fi
79-
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
80-
echo "::error::Tag $TAG already exists on origin. Bump the version."
85+
if [[ "$BRANCH" == "main" && "$RELEASE_TYPE" == beta* ]]; then
86+
echo "::error::main only ships stable releases. Select patch, minor, or major."
8187
exit 1
8288
fi
83-
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-beta(\.[0-9]+)?$ ]]; then
89+
VERSION=$(node scripts/calculate-release-version.mjs "$RELEASE_TYPE")
90+
TAG="v$VERSION"
91+
if [[ "$RELEASE_TYPE" == beta* ]]; then
8492
PRERELEASE=true
85-
elif [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
86-
PRERELEASE=false
8793
else
88-
echo "::error::Tag $TAG must be either vX.Y.Z (stable) or vX.Y.Z-beta[.N]."
89-
exit 1
94+
PRERELEASE=false
9095
fi
91-
if [[ "$BRANCH" == "main" && "$PRERELEASE" == "true" ]]; then
92-
echo "::error::main only ships stable tags (got $TAG). Use vX.Y.Z."
96+
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
97+
echo "::error::Calculated tag $TAG already exists on origin."
9398
exit 1
9499
fi
95-
VERSION="${TAG#v}"
96100
DOCKER_TAGS="ghcr.io/unicef/adt-studio:$TAG"
97101
if [[ "$PRERELEASE" == "false" ]]; then
98102
DOCKER_TAGS="$DOCKER_TAGS"$'\n'"ghcr.io/unicef/adt-studio:latest"
@@ -116,7 +120,7 @@ jobs:
116120
echo "$DOCKER_TAGS"
117121
echo "EOF"
118122
} >> "$GITHUB_OUTPUT"
119-
echo "Resolved tag: $TAG (version: $VERSION, prerelease: $PRERELEASE)"
123+
echo "Resolved tag: $TAG (type: $RELEASE_TYPE, prerelease: $PRERELEASE)"
120124
121125
- uses: pnpm/action-setup@v4
122126

@@ -165,6 +169,7 @@ jobs:
165169
contents: read
166170
env:
167171
APP_VERSION: ${{ needs.prepare.outputs.version }}
172+
SKIP_NOTARIZE: ${{ vars.SKIP_NOTARIZE }}
168173
steps:
169174
- uses: actions/checkout@v4
170175

@@ -206,9 +211,8 @@ jobs:
206211
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
207212
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
208213

209-
210214
- name: Acquire Azure code signing token
211-
if: matrix.os == 'windows-latest'
215+
if: matrix.os == 'windows-latest' && env.SKIP_NOTARIZE != 'true'
212216
shell: bash
213217
env:
214218
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
@@ -236,7 +240,7 @@ jobs:
236240
uses: actions/setup-java@v4
237241
with:
238242
distribution: temurin
239-
java-version: '21'
243+
java-version: "21"
240244

241245
- name: Cache jsign.jar
242246
if: matrix.os == 'windows-latest'
@@ -309,6 +313,7 @@ jobs:
309313
timeout-minutes: 15
310314
permissions:
311315
contents: write
316+
pull-requests: read
312317
outputs:
313318
tag: ${{ needs.prepare.outputs.tag }}
314319
version: ${{ needs.prepare.outputs.version }}
@@ -317,6 +322,11 @@ jobs:
317322
- uses: actions/checkout@v4
318323
with:
319324
token: ${{ secrets.RELEASE_PAT }}
325+
fetch-depth: 0
326+
327+
- uses: actions/setup-node@v4
328+
with:
329+
node-version: 22
320330

321331
- name: Restore staged release metadata
322332
uses: actions/download-artifact@v4
@@ -331,6 +341,23 @@ jobs:
331341
path: dist
332342
merge-multiple: true # flatten the per-OS artifacts into one dir
333343

344+
- name: Compose beta release notes
345+
if: needs.prepare.outputs.prerelease == 'true'
346+
env:
347+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
348+
TAG: ${{ needs.prepare.outputs.tag }}
349+
REPO: ${{ github.repository }}
350+
BRANCH: ${{ github.ref_name }}
351+
TRIGGER_SHA: ${{ github.sha }}
352+
run: |
353+
rm -f release-notes.tmp release-notes.md
354+
if node scripts/compose-release-notes.mjs > release-notes.tmp && [ -s release-notes.tmp ]; then
355+
mv release-notes.tmp release-notes.md
356+
else
357+
rm -f release-notes.tmp release-notes.md
358+
echo "::warning::Could not compose release notes; gh release create will generate them."
359+
fi
360+
334361
- name: Commit release metadata and push tag
335362
id: commit
336363
env:
@@ -369,9 +396,13 @@ jobs:
369396
[[ "$PRERELEASE" == "true" ]] && PRERELEASE_FLAG="--prerelease"
370397
START_TAG_FLAG=()
371398
[[ -n "$PREV_TAG" ]] && START_TAG_FLAG=(--notes-start-tag "$PREV_TAG")
399+
NOTES_ARGS=(--generate-notes "${START_TAG_FLAG[@]}")
400+
if [[ "$PRERELEASE" == "true" && -s release-notes.md ]]; then
401+
NOTES_ARGS=(--notes-file release-notes.md)
402+
fi
372403
gh release create "$TAG" --repo "$REPO" \
373404
--title "ADT Studio $TAG" \
374-
--generate-notes "${START_TAG_FLAG[@]}" $PRERELEASE_FLAG \
405+
"${NOTES_ARGS[@]}" $PRERELEASE_FLAG \
375406
dist/* \
376407
docker-compose.yml \
377408
scripts/windows-setup-and-run.bat
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Staging cleanup
2+
3+
on:
4+
pull_request_target:
5+
types: [closed]
6+
workflow_dispatch:
7+
inputs:
8+
pr:
9+
description: "Pull request number whose staging release to delete"
10+
required: true
11+
type: string
12+
13+
permissions:
14+
contents: write
15+
16+
concurrency:
17+
group: staging-cleanup-pr-${{ github.event.pull_request.number || inputs.pr }}
18+
cancel-in-progress: false
19+
20+
jobs:
21+
cleanup:
22+
if: >-
23+
${{ github.event_name == 'workflow_dispatch'
24+
|| github.event.pull_request.base.ref == 'develop' }}
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 10
27+
steps:
28+
- name: Delete staging pre-release and branch
29+
shell: bash
30+
env:
31+
GH_TOKEN: ${{ github.token }}
32+
REPO: ${{ github.repository }}
33+
PR: ${{ github.event.pull_request.number || inputs.pr }}
34+
run: |
35+
if [[ ! "$PR" =~ ^[0-9]+$ ]]; then
36+
echo "No valid PR number ('$PR'); nothing to clean up."
37+
exit 0
38+
fi
39+
SLUG="pr-$PR"
40+
STAGING_BRANCH="staging/$SLUG"
41+
42+
RELEASES=$(gh release list --repo "$REPO" --limit 200 --json tagName \
43+
--jq '.[].tagName' | { grep -E -- "-beta-${SLUG}$" || true; })
44+
if [[ -z "$RELEASES" ]]; then
45+
echo "No staging pre-release found for '$SLUG'; skipping."
46+
else
47+
for release in $RELEASES; do
48+
echo "Deleting staging pre-release $release"
49+
gh release delete "$release" --repo "$REPO" --yes --cleanup-tag || true
50+
done
51+
fi
52+
53+
if gh api "repos/$REPO/git/refs/heads/$STAGING_BRANCH" >/dev/null 2>&1; then
54+
echo "Deleting staging branch $STAGING_BRANCH"
55+
gh api -X DELETE "repos/$REPO/git/refs/heads/$STAGING_BRANCH" >/dev/null || true
56+
else
57+
echo "No staging branch '$STAGING_BRANCH' found; skipping."
58+
fi

0 commit comments

Comments
 (0)