Skip to content

Commit 7635c7a

Browse files
authored
feat: add automated backport request process and Shexli review scan (#45)
- Documented the process for requesting maintenance backports via GitHub labels in CONTRIBUTING.md. - Added a Shexli review scan command to the CI pipeline and documentation for GNOME Extensions Review. - Updated README.md to clarify testing commands and improve readability. fix: update Portuguese translations - Updated the POT-Creation-Date and fixed various translation strings in pt_BR.po. fix: change default value for auto theme switcher module - Updated the default value for the `module-auto-theme-switcher` key in gschema.xml from true to false. chore: enhance justfile with Shexli packaging command - Added a new command in the justfile to run Shexli for static analysis of the extension package. refactor: improve module disabling logic - Refactored the disable method in AuroraShellExtension to call a new private method for disabling all modules. - Improved BluetoothDeviceItemPatcher to handle disposal and restoration of original children more gracefully. style: update logging levels from log to debug - Changed logger calls from log to debug in various modules for better log level management. fix: clean up disconnect logic in various modules - Removed unnecessary disconnectObject calls in several modules to prevent potential errors.
1 parent cb1b603 commit 7635c7a

24 files changed

Lines changed: 688 additions & 106 deletions
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
name: GNOME Backport
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, synchronize, reopened, labeled, closed]
6+
7+
permissions:
8+
contents: write
9+
issues: write
10+
pull-requests: write
11+
12+
jobs:
13+
backport:
14+
name: Create GNOME maintenance PR
15+
runs-on: ubuntu-latest
16+
if: github.event.action != 'closed' || github.event.pull_request.merged == true
17+
steps:
18+
- uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
21+
22+
- name: Configure Git
23+
run: |
24+
git config user.name "github-actions[bot]"
25+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
26+
27+
- name: Create or update backport PR
28+
env:
29+
GH_TOKEN: ${{ github.token }}
30+
PR_NUMBER: ${{ github.event.pull_request.number }}
31+
PR_TITLE: ${{ github.event.pull_request.title }}
32+
run: |
33+
set -euo pipefail
34+
35+
LABEL=$(jq -r '
36+
.pull_request.labels[]
37+
| select(.name | test("^GNOME [0-9]+$"))
38+
| .name
39+
' "$GITHUB_EVENT_PATH" | head -n1)
40+
41+
if [ -z "$LABEL" ]; then
42+
echo "No GNOME release label found; skipping."
43+
exit 0
44+
fi
45+
46+
MAJOR="${LABEL#GNOME }"
47+
export MAJOR
48+
BASE_BRANCH="release/v${MAJOR}.x"
49+
BACKPORT_BRANCH="backport/pr-${PR_NUMBER}-gnome-${MAJOR}"
50+
ERROR_BODY="/tmp/backport-error.md"
51+
52+
on_error() {
53+
git cherry-pick --abort >/dev/null 2>&1 || true
54+
{
55+
echo "Backport to \`${BASE_BRANCH}\` failed."
56+
echo ""
57+
echo "Label: \`${LABEL}\`"
58+
echo "Branch: \`${BACKPORT_BRANCH}\`"
59+
echo ""
60+
echo "Please create the backport manually or resolve the conflict locally."
61+
} > "$ERROR_BODY"
62+
gh pr comment "$PR_NUMBER" --body-file "$ERROR_BODY" || true
63+
}
64+
trap on_error ERR
65+
66+
git fetch origin "+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}"
67+
git fetch origin "+refs/pull/${PR_NUMBER}/head:refs/remotes/origin/pr/${PR_NUMBER}/head"
68+
git checkout -B "$BACKPORT_BRANCH" "origin/${BASE_BRANCH}"
69+
70+
NEXT_VERSION=$(node -e '
71+
const fs = require("fs");
72+
const major = process.env.MAJOR;
73+
const metadata = JSON.parse(fs.readFileSync("metadata.json", "utf8"));
74+
const version = metadata["version-name"];
75+
const match = new RegExp(`^${major}\\.([0-9]+)$`).exec(version);
76+
if (!match) {
77+
throw new Error(`metadata.json version-name must be ${major}.x on release branch, got ${version}`);
78+
}
79+
process.stdout.write(`${major}.${Number(match[1]) + 1}`);
80+
')
81+
export NEXT_VERSION
82+
83+
COMMITS=$(gh pr view "$PR_NUMBER" --json commits --jq '.commits[].oid')
84+
for SHA in $COMMITS; do
85+
git cherry-pick -x "$SHA"
86+
done
87+
88+
node -e '
89+
const fs = require("fs");
90+
const metadata = JSON.parse(fs.readFileSync("metadata.json", "utf8"));
91+
metadata["version-name"] = process.env.NEXT_VERSION;
92+
fs.writeFileSync("metadata.json", `${JSON.stringify(metadata, null, 4)}\n`);
93+
'
94+
git add metadata.json
95+
if git diff --cached --quiet; then
96+
git commit --allow-empty -m "Bump version to ${NEXT_VERSION}"
97+
else
98+
git commit -m "Bump version to ${NEXT_VERSION}"
99+
fi
100+
101+
git push --force-with-lease origin "$BACKPORT_BRANCH"
102+
103+
EXISTING_PR=$(gh pr list \
104+
--head "$BACKPORT_BRANCH" \
105+
--base "$BASE_BRANCH" \
106+
--state open \
107+
--json number \
108+
--jq '.[0].number // empty')
109+
110+
BODY=$(mktemp)
111+
{
112+
echo "Backport of #${PR_NUMBER} to \`${BASE_BRANCH}\`."
113+
echo ""
114+
echo "This branch also bumps \`metadata.json\` to \`${NEXT_VERSION}\`."
115+
} > "$BODY"
116+
117+
if [ -n "$EXISTING_PR" ]; then
118+
gh pr edit "$EXISTING_PR" \
119+
--title "Backport #${PR_NUMBER} to GNOME ${MAJOR}" \
120+
--body-file "$BODY"
121+
echo "Updated backport PR #${EXISTING_PR}."
122+
else
123+
gh pr create \
124+
--base "$BASE_BRANCH" \
125+
--head "$BACKPORT_BRANCH" \
126+
--title "Backport #${PR_NUMBER} to GNOME ${MAJOR}" \
127+
--body-file "$BODY"
128+
fi

.github/workflows/post-hygiene.yml

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: Post Hygiene
2+
3+
on:
4+
issues:
5+
types: [opened, edited, reopened, labeled, unlabeled]
6+
pull_request_target:
7+
types: [opened, edited, reopened, labeled, unlabeled]
8+
schedule:
9+
- cron: '17 8 * * *'
10+
workflow_dispatch:
11+
12+
permissions:
13+
actions: write
14+
issues: write
15+
pull-requests: write
16+
17+
jobs:
18+
validate-post:
19+
name: Validate duplicate/stale post
20+
runs-on: ubuntu-latest
21+
if: github.event_name == 'issues' || github.event_name == 'pull_request_target'
22+
steps:
23+
- name: Check duplicate title and stale state
24+
uses: actions/github-script@v7
25+
with:
26+
script: |
27+
const marker = '<!-- aurora-post-hygiene -->';
28+
const owner = context.repo.owner;
29+
const repo = context.repo.repo;
30+
const item = context.payload.issue ?? context.payload.pull_request;
31+
const number = item.number;
32+
const title = item.title ?? '';
33+
const labels = (item.labels ?? []).map((label) => label.name.toLowerCase());
34+
const normalizedTitle = title
35+
.toLowerCase()
36+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
37+
.trim()
38+
.replace(/\s+/g, ' ');
39+
40+
async function ensureLabel(name, color, description) {
41+
try {
42+
await github.rest.issues.getLabel({ owner, repo, name });
43+
} catch (error) {
44+
if (error.status !== 404) throw error;
45+
await github.rest.issues.createLabel({ owner, repo, name, color, description });
46+
}
47+
}
48+
49+
const openItems = await github.paginate(github.rest.issues.listForRepo, {
50+
owner,
51+
repo,
52+
state: 'open',
53+
per_page: 100,
54+
});
55+
56+
const duplicates = openItems.filter((candidate) => {
57+
if (candidate.number === number) return false;
58+
const candidateTitle = (candidate.title ?? '')
59+
.toLowerCase()
60+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
61+
.trim()
62+
.replace(/\s+/g, ' ');
63+
return candidateTitle === normalizedTitle;
64+
});
65+
66+
const problems = [];
67+
if (labels.includes('stale')) {
68+
problems.push('This post is labeled `stale`; remove the label or close/triage it before continuing.');
69+
}
70+
71+
if (duplicates.length > 0) {
72+
await ensureLabel('duplicate', 'cfd3d7', 'Potential duplicate issue or pull request');
73+
await github.rest.issues.addLabels({
74+
owner,
75+
repo,
76+
issue_number: number,
77+
labels: ['duplicate'],
78+
});
79+
const duplicateLinks = duplicates
80+
.map((candidate) => `- #${candidate.number}: ${candidate.title}`)
81+
.join('\n');
82+
problems.push(`Possible duplicate title detected:\n${duplicateLinks}`);
83+
} else if (labels.includes('duplicate')) {
84+
await github.rest.issues.removeLabel({
85+
owner,
86+
repo,
87+
issue_number: number,
88+
name: 'duplicate',
89+
}).catch((error) => {
90+
if (error.status !== 404) throw error;
91+
});
92+
}
93+
94+
const comments = await github.paginate(github.rest.issues.listComments, {
95+
owner,
96+
repo,
97+
issue_number: number,
98+
per_page: 100,
99+
});
100+
const previous = comments.find((comment) => comment.body?.startsWith(marker));
101+
102+
if (problems.length > 0) {
103+
const body = `${marker}\n${problems.map((problem) => `- ${problem}`).join('\n')}`;
104+
if (previous) {
105+
await github.rest.issues.updateComment({
106+
owner,
107+
repo,
108+
comment_id: previous.id,
109+
body,
110+
});
111+
} else {
112+
await github.rest.issues.createComment({
113+
owner,
114+
repo,
115+
issue_number: number,
116+
body,
117+
});
118+
}
119+
core.setFailed(problems.join('\n'));
120+
} else if (previous) {
121+
await github.rest.issues.deleteComment({
122+
owner,
123+
repo,
124+
comment_id: previous.id,
125+
});
126+
}
127+
128+
mark-stale:
129+
name: Mark and close stale posts
130+
runs-on: ubuntu-latest
131+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
132+
steps:
133+
- name: Mark inactive posts as stale and close stale issues
134+
uses: actions/stale@v10
135+
with:
136+
stale-issue-label: stale
137+
stale-pr-label: stale
138+
days-before-issue-stale: 45
139+
days-before-issue-close: 14
140+
days-before-pr-stale: 30
141+
days-before-close: -1
142+
days-before-pr-close: -1
143+
close-issue-label: closed-as-stale
144+
close-issue-reason: not_planned
145+
remove-stale-when-updated: true
146+
operations-per-run: 100
147+
stale-issue-message: |
148+
This issue has been marked as stale because it has not had recent activity. Add a comment or remove the `stale` label if it is still relevant.
149+
close-issue-message: |
150+
This issue has been closed because it stayed stale without further activity. Reopen it or file a new issue if it is still relevant.
151+
stale-pr-message: |
152+
This pull request has been marked as stale because it has not had recent activity. Update it, add a comment, or remove the `stale` label if it should stay active.
153+
exempt-issue-labels: pinned,security,release-blocker
154+
exempt-pr-labels: pinned,security,release-blocker

AGENTS.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,18 @@ Do not leave a task incomplete if either command reports errors or failures.
2626
## Commands
2727

2828
- **Install deps:** `just deps` — runs `yarn install`; use once or when updating packages
29-
- **Build:** `just build` — compiles TypeScript and SCSS, copies metadata/schemas, compiles `.mo` files
29+
- **Build:** `just build` — compiles TypeScript and SCSS, copies metadata/schemas, and compiles `.mo` files
3030
- **Package:** `just package` — packs the extension as a `.zip` in `dist/target/` (depends on `build`)
3131
- **Install:** `just install` — installs the already-packaged `.zip` to GNOME Shell (requires `just package` first)
3232
- **Full install:** `just full-install` — packages + installs in one step
3333
- **All:** `just all` — clean + full-install
3434
- **Uninstall:** `just uninstall` — disables and removes the extension
35-
- **Run (host):** `just run` — launches a devkit GNOME Shell session (headless, Wayland)
36-
- **Run (toolbox):** `just toolbox run`same as above, but inside the Fedora toolbox
35+
- **Run (host):** `just run`packages, installs, then launches a devkit GNOME Shell session
36+
- **Run (toolbox):** `just toolbox run`packages/installs on the host, then runs GNOME Shell inside the Fedora toolbox
3737
- **Create toolbox:** `just toolbox create` — create the `aurora-shell-devel` Fedora toolbox
3838
- **Remove toolbox:** `just toolbox remove` — delete the toolbox
3939
- **Validate:** `just validate` — runs tsc, ESLint, Prettier check, and Stylelint
40+
- **Shexli:** `just shexli` — packages the extension and runs the extensions.gnome.org static analyzer on the generated ZIP
4041
- **Lint:** `just lint` — runs ESLint only
4142
- **Watch SCSS:** `just watch` — watches `src/styles/` and recompiles on change
4243
- **View logs:** `just logs` — shows recent `aurora` entries from the current boot journal
@@ -52,7 +53,7 @@ Do not leave a task incomplete if either command reports errors or failures.
5253

5354
### Translation commands
5455

55-
- **Regenerate POT template:** `just pot` — builds then scans compiled JS (`dist/`) and writes the `.pot` into `dist/` (a build artifact, **not** committed — avoids `POT-Creation-Date` churn). Run this whenever translatable strings are added or removed.
56+
- **Regenerate POT template:** `just pot` — builds, then scans compiled JS (`dist/`) and writes the `.pot` into `dist/` (a build artifact, **not** committed — avoids `POT-Creation-Date` churn). Run this whenever translatable strings are added or removed.
5657
- **Merge new strings into .po files:** `just update-po` — depends on `pot`; regenerates the template into `dist/` then runs `msgmerge` on every `data/po/*.po` against it. The hand-translated `data/po/*.po` files are the committed source of truth.
5758
- **Compile .mo binaries:** `just compile-mo` — compiles each `po/*.po` into `dist/locale/<lang>/LC_MESSAGES/*.mo`. Called automatically by `just build`.
5859

@@ -68,7 +69,8 @@ Do not leave a task incomplete if either command reports errors or failures.
6869
- `context.ts``ExtensionContext` interface and implementation
6970
- `logger.ts` — Abstracted logging
7071
- `settings.ts``SettingsManager` abstraction for GSettings
71-
- `modules/` — one **folder** per feature module, named after the module (e.g., `dock/dock.ts`); the main entry file shares the folder name
72+
- `modules/` — feature modules registered by `registry.ts`
73+
- regular modules use one **folder** per feature module, named after the module (e.g., `dock/dock.ts`); the main entry file shares the folder name
7274
- `dev/` — developer-only tooling (e.g., `devTool.ts`), gated behind the `AURORA_DEVTOOLS=1` env var. **Not** a feature module: it is not in the registry, prefs, or gschema, and is instantiated directly by `extension.ts`
7375
- `shared/` — shared utilities used across modules (e.g., `quickSettings.ts`)
7476
- `styles/` — SCSS stylesheets (compiled to light + dark CSS)
@@ -98,7 +100,7 @@ Do not leave a task incomplete if either command reports errors or failures.
98100

99101
## Adding a Module
100102

101-
1. Create `src/modules/myModule/myModule.ts` with a `Module` subclass **and** a co-located `definition` export. Every module **must** live in its own folder named after the module (e.g., `dock/dock.ts`, `panel/panel.ts`):
103+
1. Create `src/modules/myModule/myModule.ts` with a `Module` subclass **and** a co-located `definition` export. Regular modules **must** live in their own folder named after the module (e.g., `dock/dock.ts`, `panel/panel.ts`).
102104

103105
```typescript
104106
import { gettext as _ } from 'gettext';
@@ -161,7 +163,7 @@ return [/* …, */ myModule];
161163
</key>
162164
```
163165

164-
`tests/unit/registry.test.ts` enforces that step 2, step 3, and step 4 stay in parity — including that every module's `section` is a known section id and matches between the registry and `prefsMetadata.ts`. A half-finished addition will fail CI.
166+
`tests/unit/registry.test.ts` enforces that steps 2, 3, and 4 stay in parity — including that every module's `section` is a known section id and matches between the registry and `prefsMetadata.ts`. A half-finished addition will fail CI.
165167

166168
### Prefs sections
167169

@@ -191,6 +193,19 @@ Per the GNOME review guidelines, clipboard-related keyboard shortcuts must not s
191193
- Keep `enable()` and `disable()` symmetric.
192194
- Read settings through `this.context.settings`. Importing `Main`/`Shell`/`St` directly is fine — keep heavy algorithms in shell-free pure files so they stay unit-testable.
193195

196+
## Human Review Quality Bar
197+
198+
Avoid code that only looks plausible. A human reviewer should be able to read a change and see a real contract, not a guess.
199+
200+
- Do not add optional calls such as `object?.method?.(...)` unless that method is a real, documented API or the local type intentionally models it. Never use patterns like `this.disconnectObject?.(this)` on objects that do not own that signal connection contract.
201+
- Do not ship fake behavior. If a UI label, schema description, README entry, or module subtitle says a feature is wired to NetworkManager, ModemManager, UPower, sensors, widgets, or GNOME internals, the code must actually call the relevant API or clearly describe itself as a fallback.
202+
- Keep runtime capability checks honest. Hardware-specific modules must detect missing services/devices at runtime and stay inactive or degrade explicitly.
203+
- Do not scatter `as unknown as ...` casts through feature modules. If GObject construction or Shell internals require a cast, isolate it in a small shared helper/factory with a clear name.
204+
- Do not leave placeholder helpers, legacy duplicates, or unused compatibility functions after a refactor. Remove dead code instead of keeping it “just in case”.
205+
- Keep strings and metadata truthful and synchronized across module `definition`, `src/prefsMetadata.ts`, schema XML, README/architecture docs, and `.po` files when strings change.
206+
- Search for obvious generated-code artifacts before finishing: broken joined words in docs, stale project names, obsolete env vars, and UI descriptions that exceed what is implemented.
207+
- Prefer explicit D-Bus/property handling over no-op calls that only log success. If a feature cannot be safely implemented yet, make the limitation visible in the title/subtitle/docs rather than implying it works.
208+
194209
## Logging Style
195210

196211
Prefix every log message with the module name in `[PascalCase]` brackets. Use the global `logger` from `~/core/logger.ts` — never `console.log/warn` or `GLib.log_structured` directly from module code.

0 commit comments

Comments
 (0)