Skip to content

Commit 493b222

Browse files
committed
Merge branch 'upstream-development'
2 parents cd3f3cb + 5e5aae2 commit 493b222

32 files changed

Lines changed: 3121 additions & 58 deletions

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ This project adheres to the Contributor Covenant [Code of Conduct](../CODE_OF_CO
192192

193193
1. **Keep changes minimal**: Make the smallest possible changes to achieve the goal
194194
2. **Run tests frequently**: Test after each meaningful change
195-
3. **Lint before committing**: Ensure code passes all linting checks
195+
3. **Run `yarn lint:fix` after any code change**: This runs Prettier and ESLint with auto-fix to ensure formatting and lint rules are satisfied before committing
196196
4. **Update documentation**: Update docs if changes affect documented behavior
197197
5. **Follow existing patterns**: Match the style and patterns already in the codebase
198198
6. **Don't remove working code**: Only modify what's necessary for the task
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# GitHub Desktop Release Notes Style Guide
2+
3+
## Important: Use existing `Notes:` lines
4+
5+
Many PRs include a `Notes:` line in their body (e.g., `Notes: [Fixed] Keep PR badge on top of progress bar`).
6+
7+
- If the `Notes:` line is `Notes: no-notes`, **skip that PR entirely** — it should not appear in the release notes.
8+
- If a PR has a `Notes:` line that already follows the style guide (correct `[Tag]` prefix, user-facing language, present tense), **use it as-is**.
9+
- If a PR has a `Notes:` line but it is missing a tag, uses developer-facing language, or doesn't follow the writing style below, **use it as the basis** for your entry but clean it up to match the style guide. Stay as close to the author's intent as possible.
10+
- Only generate your own entry from scratch when a PR has **no `Notes:` line at all**.
11+
12+
## Tags
13+
14+
Prefix each entry with one of these tags, sorted in this order:
15+
16+
1. `[New]` — Shiniest, most significant features (use sparingly — these are release highlights)
17+
2. `[Added]` — Smaller features, new commands, or discrete additions
18+
3. `[Fixed]` — Bug fixes (describe what was done and how behavior improved, not what was wrong)
19+
4. `[Improved]` — Enhancements to existing features that weren't broken
20+
5. `[Removed]` — Removed functionality (rare)
21+
22+
**Rule of thumb:** If it's a small new end-to-end feature, use `[Added]`. If it's a change to a portion of an existing feature, use `[Improved]`.
23+
24+
## Entry Format
25+
26+
```
27+
[Tag] Description of work or change - #PR_NUMBER
28+
```
29+
30+
If it was done by an external contributor (not a member of the `desktop` org), add attribution:
31+
32+
```
33+
[Tag] Description of work or change - #PR_NUMBER. Thanks @contributor!
34+
```
35+
36+
## What to Skip
37+
38+
Do NOT generate entries for:
39+
- CI/CD changes, test-only changes, internal refactoring
40+
- Dependency bumps (unless fixing a security vulnerability)
41+
- Build system or developer tooling changes
42+
- Documentation updates
43+
- PRs with `Notes: no-notes` in their body
44+
45+
**Exception:** Security vulnerability fixes should always be included, even if they are dependency updates. Keep them general — do not include CVE numbers. Example:
46+
```
47+
[Fixed] Update embedded Git to address security vulnerability - #4791
48+
```
49+
50+
## Writing Style
51+
52+
1. **Write for users, not developers** — describe impact on user workflow, not technical process
53+
-`[Fixed] Keep PR badge on top of progress bar - #8622`
54+
-`[Fixed] Increase z-index of the progress bar PR badge - #8622`
55+
56+
2. **Use present tense** (unless it significantly reduces clarity)
57+
-`[Added] Add external editor integration for Xcode - #8255`
58+
-`[Added] Adding external editor integration for Xcode - #8255`
59+
60+
3. **Keep the description readable independently from the tag**
61+
-`[Improved] Always fast forward recent branches after fetch - #7761`
62+
-`[Improved] Branch fast-forwarding after fetch - #7761`
63+
64+
4. **For bug fixes, describe what works now** — not what was broken
65+
-`[Fixed] Keep conflicting untracked files when bringing changes to another branch - #8084`
66+
-`[Fixed] Conflicting untracked files are lost when bringing changes to another branch - #8084`
67+
68+
## Uncertainty
69+
70+
If you cannot confidently determine the correct tag or whether a PR is user-facing, prefix the entry with `[???]` instead. These will be flagged for human review.
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
name: 'Draft Release'
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
channel:
7+
description: 'Release channel'
8+
required: true
9+
type: choice
10+
options:
11+
- beta
12+
- production
13+
ref:
14+
description:
15+
'Branch or tag to release from (default: development for beta, latest
16+
beta tag for production). Use for hotfixes.'
17+
required: false
18+
type: string
19+
dry-run:
20+
description: 'Generate notes without creating a release branch'
21+
required: false
22+
type: boolean
23+
default: false
24+
25+
permissions:
26+
contents: write
27+
pull-requests: write
28+
29+
jobs:
30+
draft-release:
31+
name: Draft Release
32+
runs-on: ubuntu-latest
33+
34+
steps:
35+
- name: Checkout repository
36+
uses: actions/checkout@v4
37+
with:
38+
fetch-depth: 0
39+
fetch-tags: true
40+
ref: ${{ inputs.ref || github.event.repository.default_branch }}
41+
42+
- name: Set up Node.js
43+
uses: actions/setup-node@v4
44+
with:
45+
node-version-file: '.nvmrc'
46+
cache: yarn
47+
48+
- name: Install dependencies
49+
run: yarn --frozen-lockfile
50+
51+
- name: Determine previous and next versions
52+
id: version
53+
run: >
54+
yarn ts-node -P script/tsconfig.json script/draft-release/ci.ts
55+
version ${{ inputs.channel }}
56+
57+
- name: Generate release notes (beta only)
58+
id: notes
59+
if: ${{ inputs.channel == 'beta' }}
60+
uses: github/copilot-release-notes@v1
61+
with:
62+
base-ref: release-${{ steps.version.outputs.previous }}
63+
head-ref: ${{ inputs.ref || 'development' }}
64+
instructions: .github/release-notes-instructions.md
65+
env:
66+
GITHUB_TOKEN: ${{ github.token }}
67+
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
68+
69+
- name: Parse changelog entries
70+
id: changelog
71+
env:
72+
CHANNEL: ${{ inputs.channel }}
73+
RELEASE_NOTES: ${{ steps.notes.outputs.release-notes }}
74+
UNCERTAIN: ${{ steps.notes.outputs.uncertain-entries }}
75+
SKIPPED: ${{ steps.notes.outputs.skipped-prs }}
76+
PREVIOUS: ${{ steps.version.outputs.previous }}
77+
NEXT: ${{ steps.version.outputs.next }}
78+
run: |
79+
if [ "$CHANNEL" = "production" ]; then
80+
# Production: aggregate existing beta entries via shared script
81+
ENTRIES=$(yarn --silent ts-node -P script/tsconfig.json \
82+
script/draft-release/ci.ts changelog-entries "$PREVIOUS")
83+
else
84+
# Beta: parse AI-generated notes, extracting [Tag] lines
85+
ENTRIES=$(echo "$RELEASE_NOTES" | grep -E '^\[' || true)
86+
ENTRIES=$(echo "$ENTRIES" | jq -c -R -s 'split("\n") | map(select(length > 0))')
87+
fi
88+
89+
# Write entries as single-line JSON to output
90+
echo "entries=$ENTRIES" >> "$GITHUB_OUTPUT"
91+
COUNT=$(echo "$ENTRIES" | jq 'length')
92+
93+
# Step summary
94+
if [ "$CHANNEL" = "production" ]; then
95+
echo "## Production Release Notes — $NEXT" >> "$GITHUB_STEP_SUMMARY"
96+
echo "" >> "$GITHUB_STEP_SUMMARY"
97+
echo "Aggregated $COUNT entries from beta releases since $PREVIOUS:" >> "$GITHUB_STEP_SUMMARY"
98+
echo "" >> "$GITHUB_STEP_SUMMARY"
99+
echo "$ENTRIES" | jq -r '.[]' >> "$GITHUB_STEP_SUMMARY"
100+
else
101+
echo "## Draft Release Notes — $NEXT" >> "$GITHUB_STEP_SUMMARY"
102+
echo "" >> "$GITHUB_STEP_SUMMARY"
103+
echo "$RELEASE_NOTES" >> "$GITHUB_STEP_SUMMARY"
104+
105+
if [ -n "$UNCERTAIN" ] && [ "$UNCERTAIN" != "[]" ] && [ "$UNCERTAIN" != "" ]; then
106+
echo "" >> "$GITHUB_STEP_SUMMARY"
107+
echo "### ⚠️ Entries needing human review" >> "$GITHUB_STEP_SUMMARY"
108+
echo "$UNCERTAIN" >> "$GITHUB_STEP_SUMMARY"
109+
fi
110+
111+
if [ -n "$SKIPPED" ] && [ "$SKIPPED" != "[]" ] && [ "$SKIPPED" != "" ]; then
112+
SKIPPED_COUNT=$(echo "$SKIPPED" | jq 'length' 2>/dev/null || echo 0)
113+
echo "" >> "$GITHUB_STEP_SUMMARY"
114+
echo "<details><summary>$SKIPPED_COUNT PRs excluded from notes</summary>" >> "$GITHUB_STEP_SUMMARY"
115+
echo "" >> "$GITHUB_STEP_SUMMARY"
116+
echo "$SKIPPED" >> "$GITHUB_STEP_SUMMARY"
117+
echo "</details>" >> "$GITHUB_STEP_SUMMARY"
118+
fi
119+
fi
120+
121+
echo "📝 Parsed $COUNT changelog entries"
122+
123+
- name: Create release branch and commit
124+
if: ${{ inputs.dry-run == false }}
125+
env:
126+
CHANNEL: ${{ inputs.channel }}
127+
NEXT_VERSION: ${{ steps.version.outputs.next }}
128+
LATEST_BETA: ${{ steps.version.outputs.latest-beta }}
129+
REF_OVERRIDE: ${{ inputs.ref }}
130+
ENTRIES: ${{ steps.changelog.outputs.entries }}
131+
run: |
132+
BRANCH="releases/$NEXT_VERSION"
133+
134+
# Check if the release branch already exists
135+
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
136+
echo "::error::Branch $BRANCH already exists. Delete it first or use dry-run."
137+
exit 1
138+
fi
139+
140+
git config user.name "github-actions[bot]"
141+
git config user.email "github-actions[bot]@users.noreply.github.com"
142+
143+
if [ -n "$REF_OVERRIDE" ]; then
144+
# Hotfix: already on the correct ref from checkout step
145+
git checkout -b "$BRANCH"
146+
echo "🔧 Hotfix: branched from $REF_OVERRIDE"
147+
elif [ "$CHANNEL" = "production" ]; then
148+
# Production: branch from the latest beta tag
149+
if [ -z "$LATEST_BETA" ]; then
150+
echo "::error::Cannot create a production release branch because no latest beta version was found."
151+
exit 1
152+
fi
153+
git checkout -b "$BRANCH" "release-$LATEST_BETA"
154+
else
155+
# Beta: already on development from checkout step
156+
git checkout -b "$BRANCH"
157+
fi
158+
159+
# Bump app/package.json and update changelog.json
160+
yarn --silent ts-node -P script/tsconfig.json \
161+
script/draft-release/ci.ts prepare "$NEXT_VERSION" "$ENTRIES"
162+
163+
git add app/package.json changelog.json
164+
git commit -m "Draft release $NEXT_VERSION"
165+
166+
- name: Push release branch
167+
if: ${{ inputs.dry-run == false }}
168+
env:
169+
NEXT_VERSION: ${{ steps.version.outputs.next }}
170+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
171+
run: |
172+
git push origin "releases/$NEXT_VERSION"
173+
echo "✅ Pushed releases/$NEXT_VERSION — release-pr.yml will create the draft PR"

app/src/ui/multi-commit-operation/dialog/conflicts-dialog.tsx

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ interface IConflictsDialogState {
4747
readonly isCommitting: boolean
4848
readonly isAborting: boolean
4949
readonly isFileResolutionOptionsMenuOpen: boolean
50-
readonly countResolved: number | null
5150
}
5251

5352
/**
@@ -59,13 +58,15 @@ export class ConflictsDialog extends React.Component<
5958
IConflictsDialogProps,
6059
IConflictsDialogState
6160
> {
61+
/** Tracks whether we've ever seen resolved files, for the "undone" banner */
62+
private hasSeenResolvedFiles = false
63+
6264
public constructor(props: IConflictsDialogProps) {
6365
super(props)
6466
this.state = {
6567
isCommitting: false,
6668
isAborting: false,
6769
isFileResolutionOptionsMenuOpen: false,
68-
countResolved: null,
6970
}
7071
}
7172

@@ -99,19 +100,6 @@ export class ConflictsDialog extends React.Component<
99100
}
100101
}
101102

102-
public componentDidUpdate(): void {
103-
const { workingDirectory, manualResolutions } = this.props
104-
105-
const resolvedConflicts = getResolvedFiles(
106-
workingDirectory,
107-
manualResolutions
108-
)
109-
110-
if (resolvedConflicts.length !== (this.state.countResolved ?? 0)) {
111-
this.setState({ countResolved: resolvedConflicts.length })
112-
}
113-
}
114-
115103
/**
116104
* Invokes submit callback and dismisses modal
117105
*/
@@ -196,14 +184,21 @@ export class ConflictsDialog extends React.Component<
196184
/**
197185
* Renders the banner based on count of resolved files.
198186
*
199-
* If the count of resolved files is null, then the banner is
200-
* not rendered as no conflicts have been resolved, yet. If the count of resolved
201-
* files is 0, then there have been conflicts resolved, but they have been
202-
* undone, we show an undone banner.
187+
* Always shows the resolved count when there are resolved files. If the
188+
* count drops to 0 after having been non-zero, shows the "undone" banner.
203189
*/
204190
public renderBanner(conflictedFilesCount: number) {
205-
const { countResolved } = this.state
206-
if (countResolved === null) {
191+
const { workingDirectory, manualResolutions } = this.props
192+
const countResolved = getResolvedFiles(
193+
workingDirectory,
194+
manualResolutions
195+
).length
196+
197+
if (countResolved > 0) {
198+
this.hasSeenResolvedFiles = true
199+
}
200+
201+
if (countResolved === 0 && !this.hasSeenResolvedFiles) {
207202
return
208203
}
209204

0 commit comments

Comments
 (0)