Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 34 additions & 41 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,36 @@
# Pull Request Template

## Description
<!-- Briefly describe the purpose of this PR. -->

## Motivation
<!-- Explain why this change is needed. What problem does it solve? -->

## Type of Change
- [ ] New Feature
- [ ] Bug Fix
- [ ] Refactor / Code Cleanup
- [ ] Documentation Update
- [ ] Other (please specify)

## Related Issue(s)
<!-- Link to any related issue(s) (e.g., #123) -->

## Screenshots / Video
<!-- Include screenshots or a short video demonstrating the change. If the change adds a new UI feature, attach an image. If it adds functionality best shown via video, embed a video. -->

**Screenshot** (if applicable):

```markdown
![Screenshot Description](path/to/screenshot.png)
```

**Video** (if applicable):

```html
<video src="path/to/video.mp4" controls width="600"></video>
```
## Summary
<!-- Briefly describe the change. -->

## Related issue
<!-- Use "Fixes #123", "Closes #123", or "Resolves #123" only when this PR fully resolves the issue. -->
<!-- Use "Refs #123", "Related to #123", or "Part of #123" when this PR is partial and should not close the issue. -->

Fixes #

## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] Enhancement
- [ ] Documentation
- [ ] Refactor / maintenance
- [ ] Performance
- [ ] Security

## Release impact
- [ ] Patch
- [ ] Minor
- [ ] Major / breaking change
- [ ] No release note needed

## Desktop impact
- [ ] Windows
- [ ] macOS
- [ ] Linux
- [ ] Installer / packaging
- [ ] Not platform-specific

## Screenshots / video
<!-- Include screenshots or a short video for UI or visual changes. -->

## Testing
<!-- Describe how reviewers can test the changes. Include steps, commands, or environment setup. -->

## Checklist
- [ ] I have performed a self-review of my code.
- [ ] I have added any necessary screenshots or videos.
- [ ] I have linked related issue(s) and updated the changelog if applicable.

---
*Thank you for contributing!*
<!-- Describe how this was tested. Include commands, steps, or environment setup. -->
252 changes: 252 additions & 0 deletions .github/workflows/merged-pr-bookkeeping.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
name: Merged PR issue bookkeeping

on:
pull_request_target:
types: [closed]

permissions:
contents: read
issues: write
pull-requests: read

jobs:
mark-linked-issues-fixed:
name: Mark linked issues fixed in main
if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest
steps:
- name: Update closing issues
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const pullRequest = context.payload.pull_request;
const pullNumber = pullRequest.number;

const fixedInMainLabel = {
name: "status: fixed in main",
color: "0E8A16",
description: "Work is merged into main but may not be in a downloadable release yet.",
};
const pendingReleaseLabel = {
name: "status: pending release",
color: "FBCA04",
description: "Merged change is waiting for a packaged desktop release.",
};
const labelsToRemove = ["status: in progress", "status: needs triage"];
const nextReleaseMilestoneTitle = "Next Release";

async function ensureLabel(label) {
try {
await github.rest.issues.getLabel({
owner,
repo,
name: label.name,
});
} catch (error) {
if (error.status !== 404) throw error;
try {
await github.rest.issues.createLabel({
owner,
repo,
name: label.name,
color: label.color,
description: label.description,
});
core.info(`Created label '${label.name}'.`);
} catch (createError) {
if (createError.status !== 422) throw createError;
core.info(`Label '${label.name}' already exists.`);
}
}
}

async function ensureMilestone(title) {
const milestones = await github.paginate(github.rest.issues.listMilestones, {
owner,
repo,
state: "all",
per_page: 100,
});
const existing = milestones.find((milestone) => milestone.title === title);
if (existing?.state === "open") return existing;
if (existing) {
const reopened = await github.rest.issues.updateMilestone({
owner,
repo,
milestone_number: existing.number,
state: "open",
});
core.info(`Reopened milestone '${title}'.`);
return reopened.data;
}

try {
const created = await github.rest.issues.createMilestone({
owner,
repo,
title,
description: "Merged changes queued for the next packaged desktop release.",
});
core.info(`Created milestone '${title}'.`);
return created.data;
} catch (error) {
if (error.status !== 422) throw error;
const refreshed = await github.paginate(github.rest.issues.listMilestones, {
owner,
repo,
state: "all",
per_page: 100,
});
const milestone = refreshed.find((item) => item.title === title);
if (!milestone) throw error;
return milestone;
}
}

async function getClosingIssueRefs() {
const query = `
query($owner: String!, $repo: String!, $pullNumber: Int!, $cursor: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pullNumber) {
closingIssuesReferences(first: 100, after: $cursor) {
nodes {
number
Comment thread
EtienneLescot marked this conversation as resolved.
repository {
name
owner {
login
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`;

const issueRefs = new Map();
let cursor = null;
let hasNextPage = true;

while (hasNextPage) {
const result = await github.graphql(query, {
owner,
repo,
pullNumber,
cursor,
});
const refs = result.repository.pullRequest.closingIssuesReferences;
for (const issue of refs.nodes) {
const issueOwner = issue.repository.owner.login;
const issueRepo = issue.repository.name;
issueRefs.set(`${issueOwner}/${issueRepo}#${issue.number}`, {
owner: issueOwner,
repo: issueRepo,
number: issue.number,
});
}
hasNextPage = refs.pageInfo.hasNextPage;
cursor = refs.pageInfo.endCursor;
}

return [...issueRefs.values()];
}

async function hasBookkeepingComment(issueNumber, marker) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
return comments.some((comment) => comment.body && comment.body.includes(marker));
}

await ensureLabel(fixedInMainLabel);
await ensureLabel(pendingReleaseLabel);
const fallbackMilestone = pullRequest.milestone || await ensureMilestone(nextReleaseMilestoneTitle);

const issueRefs = await getClosingIssueRefs();
if (issueRefs.length === 0) {
core.info(`PR #${pullNumber} did not declare closing issue references. Nothing to update.`);
return;
}

for (const issueRef of issueRefs) {
if (
issueRef.owner.toLowerCase() !== owner.toLowerCase() ||
issueRef.repo.toLowerCase() !== repo.toLowerCase()
) {
core.warning(
`Skipping cross-repository closing reference ${issueRef.owner}/${issueRef.repo}#${issueRef.number}; ` +
`this workflow only updates issues in ${owner}/${repo}.`,
);
continue;
}

const issueNumber = issueRef.number;
const issueResponse = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber,
});
const issue = issueResponse.data;
const existingLabels = issue.labels.map((label) =>
typeof label === "string" ? label : label.name,
);
const milestoneTitle = issue.milestone?.title || fallbackMilestone.title;
const milestoneNumber = issue.milestone?.number || fallbackMilestone.number;

await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: [fixedInMainLabel.name, pendingReleaseLabel.name],
});

for (const label of labelsToRemove) {
if (!existingLabels.includes(label)) continue;
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: label,
});
} catch (error) {
if (error.status !== 404) throw error;
}
}

await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
milestone: milestoneNumber,
state: "closed",
state_reason: "completed",
});

const marker = `<!-- openscreen-merged-pr-bookkeeping:${pullNumber} -->`;
if (!(await hasBookkeepingComment(issueNumber, marker))) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: [
marker,
`Fixed by #${pullNumber} and merged into \`main\`.`,
"",
`This change is assigned to the \`${milestoneTitle}\` release milestone and is not necessarily available in the latest downloadable desktop release yet. It is currently marked as \`${pendingReleaseLabel.name}\` until a packaged release containing it is published.`,
].join("\n"),
});
}

core.info(`Updated issue #${issueNumber} for merged PR #${pullNumber}.`);
}
26 changes: 26 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,32 @@ Thank you for considering contributing to this project! By contributing, you hel

If you encounter a bug or have a feature request, please open an issue in the [Issues](https://github.com/EtienneLescot/openscreen/issues) section of this repository. Provide as much detail as possible to help us address the issue effectively.

## Issue lifecycle

Issues are closed when the corresponding fix or feature is merged into `main`.

For desktop users, this does not always mean the change is already available in the latest downloadable release. When relevant, closed issues are marked as `status: fixed in main` and `status: pending release`.

Once a GitHub Release containing the change is published, the issue can be marked as `status: released`.

The next version number is not always known when a PR is merged. In that case, issues are assigned to the `Next Release` milestone. When preparing a release, this milestone can be renamed to the actual version, such as `v1.6.0` or `v2.0.0`, and a new `Next Release` milestone can be created.

When a PR fully resolves an issue, link it with a GitHub closing keyword:

```txt
Fixes #123
Closes #123
Resolves #123
```

If a PR only partially addresses an issue, use a non-closing reference instead:

```txt
Refs #123
Part of #123
Related to #123
```

## Style Guide

- Write clear, concise, and descriptive commit messages.
Expand Down
2 changes: 2 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ interface Window {
}>;
selectSource: (source: ProcessedDesktopSource) => Promise<ProcessedDesktopSource | null>;
getSelectedSource: () => Promise<ProcessedDesktopSource | null>;
onSelectedSourceChanged: (callback: (source: ProcessedDesktopSource) => void) => () => void;
onSourceSelectorClosed: (callback: () => void) => () => void;
requestCameraAccess: () => Promise<{
success: boolean;
granted: boolean;
Expand Down
4 changes: 4 additions & 0 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,10 @@ export function registerIpcHandlers(
selectedDesktopSource = null;
}
}
const mainWin = getMainWindow();
if (mainWin && !mainWin.isDestroyed()) {
mainWin.webContents.send("selected-source-changed", selectedSource);
}
const sourceSelectorWin = getSourceSelectorWindow();
if (sourceSelectorWin) {
sourceSelectorWin.close();
Expand Down
3 changes: 3 additions & 0 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@ function createSourceSelectorWindowWrapper() {
sourceSelectorWindow = createSourceSelectorWindow();
sourceSelectorWindow.on("closed", () => {
sourceSelectorWindow = null;
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("source-selector-closed");
}
});
return sourceSelectorWindow;
}
Expand Down
Loading
Loading