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
4 changes: 2 additions & 2 deletions .cursor/commands/create-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Pre-fill:

**From GitHub issue:**

- TLDR, Details, `Closes #NNN` when the PR completes the issue
- TLDR, Details, `Refs #NNN` (never `Closes` / `Fixes` / `Resolves`)

**From branch analysis:**

Expand All @@ -84,7 +84,7 @@ Keep output under ~400 lines; no nested fenced code blocks inside the PR body.

- Detect issue number from branch: `…/173-…` or leading `173-…`
- Fetch with `gh issue view NNN --repo eten-tech-foundation/fluent-mobile`
- Body must include `Closes #NNN` on its own line under Details when the PR completes the issue
- Body must include `Refs #NNN` on its own line under Details (non-closing — issues stay open for QA)

## GitHub (`gh`)

Expand Down
4 changes: 2 additions & 2 deletions .cursor/commands/generate-pr-description.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ gh issue view <N> --repo eten-tech-foundation/fluent-mobile --json title,body
1. Load template from `.cursor/templates/pr-template.md`
2. **Synthesize PR title** (mandatory — see above)
3. Detect branch type: `feature/`, `fix/`, `chore/` → suggest change type checkbox
4. Fetch GitHub issue when issue number present (`Closes #NNN` in Details)
4. Fetch GitHub issue when issue number present (`Refs #NNN` in Details)
5. Pre-fill Technical Changes and Testing from diff + [docs/AGENT_ONBOARDING.md](../../docs/AGENT_ONBOARDING.md) gates

## Output format
Expand Down Expand Up @@ -107,7 +107,7 @@ gh issue view <N> --repo eten-tech-foundation/fluent-mobile --json title,body

## Ticket patterns

- GitHub issues: `[#123]` / `#123` in PR title; `Closes #123` under Details when the PR completes the issue
- GitHub issues: `[#123]` / `#123` in PR title; `Refs #123` under Details (never closing keywords)

## Quality checklist (pre-fill reminders)

Expand Down
6 changes: 3 additions & 3 deletions .cursor/rules/delivery.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Every change — including infra, CI, docs, Dependabot follow-ups, and `expo ins

1. **Ticket on Project 4** — GitHub Issue `#NNN` **on** the [Fluent Mobile Board](https://github.com/orgs/eten-tech-foundation/projects/4/views/9) (org project #4, view 9). Ask if missing. See [docs/issue-tracking.md](../../docs/issue-tracking.md). The repo Issues list is not the triage board. (Not Linear.) Board Status/Priority edits: [docs/guides/project-board.md](../../docs/guides/project-board.md) and [project-board.mdc](./project-board.mdc) — do not sweep Product or `Dev Ready` lanes without named approval.
2. **Feature branch** — `{author}/{type}/{issue-number}-{slug}` (see `/create-pr-branch`)
3. **Pull request** — title `[#NNN]: Description`; body filled from the PR template (below); include `Closes #NNN` under Details when the PR completes the issue; set Project 4 Status to **In PR Review**
4. **Human merge** — agents open PRs and push **feature branches only**; never merge on GitHub unless the user explicitly asks for a Dependabot squash merge in that session
3. **Pull request** — title `[#NNN]: Description`; body filled from the PR template (below); include `Refs #NNN` under Details (never `Closes` / `Fixes` / `Resolves`); set Project 4 Status to **In PR Review**
4. **Human merge** — agents open PRs and push **feature branches only**; never merge on GitHub unless the user explicitly asks for a Dependabot squash merge in that session. After merge, set Project 4 Status to **In QA** (issues stay open for QA — do not auto-close)

Allowed on `main` locally: `git checkout main`, `git pull`, read-only validation after merges. **Not allowed:** `git push origin main`, commits on `main`, or opening PRs with `main` as the head branch.

Expand All @@ -28,7 +28,7 @@ Do **not** substitute a short “Summary / Test plan” or ad-hoc bullet list. L
| ------- | ---------------- |
| **TLDR** | 2–4 sentences: what / why / impact |
| **Reviewer checklist** | Leave items unchecked unless verified |
| **Details** | `Closes #NNN` on its own line when the PR completes the issue; short summary; type-of-change checkboxes |
| **Details** | `Refs #NNN` on its own line (non-closing link); short summary; type-of-change checkboxes |
| **Technical changes** | Key files as `` `path` `` bullets |
| **Testing** | What gates ran (`format:check`, `lint`, `typecheck`, `npm test -- --ci`) or waiver |
| **How to verify** | Numbered steps + **Expected** |
Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/docs.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ alwaysApply: false

## Issue tracking & CI

- [docs/issue-tracking.md](../../docs/issue-tracking.md) — Project 4 Fluent Mobile Board, branch naming, PR linking (`Closes #NNN`)
- [docs/issue-tracking.md](../../docs/issue-tracking.md) — Project 4 Fluent Mobile Board, branch naming, PR linking (`Refs #NNN`; never auto-close)
- [docs/guides/project-board.md](../../docs/guides/project-board.md) — board mutation hard rules (agents)
- [docs/guides/expo-first-dependencies.md](../../docs/guides/expo-first-dependencies.md) — Expo-first package preference
- [docs/ci.md](../../docs/ci.md) — workflow inventory + future required-check guardrail
Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/project-board.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ Canonical policy: [docs/guides/project-board.md](../../docs/guides/project-board
- **Do not** edit Status or Priority on `In Progress (Product)` (or other Product-owned columns) unless the tech lead **explicitly lists** those issue numbers.
- **No board sweeps** without a proposed change set + explicit approval. Ambiguous = no.
- **Do not merge PRs** unless explicitly asked (Dependabot process is the standing exception when requested).
- For the **assigned** ticket only: add to Project 4 if missing; set `In PR Review` when the PR opens; set `Done` **only after merge** (or with completion evidence on `main`) — never before.
- For the **assigned** ticket only: add to Project 4 if missing; set `In PR Review` when the PR opens; set **`In QA`** after merge (or when ready for QA) — never auto-close the issue; do **not** set `Done` until QA completes (human/QA process).

Tracker overview: [docs/issue-tracking.md](../../docs/issue-tracking.md). Delivery: [delivery.mdc](./delivery.mdc).
4 changes: 2 additions & 2 deletions .cursor/templates/pr-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

### Reviewer checklist

- [ ] GitHub issue linked in Details (`Closes #NNN` when this PR completes it)
- [ ] GitHub issue linked in Details (`Refs #NNN` — do **not** use `Closes` / `Fixes` / `Resolves`)
- [ ] How to verify steps completed or valid waiver noted below
- [ ] Acceptance criteria met, **or** unmet AC waived **in the issue** with linked follow-up (see `AGENTS.md`)
- [ ] Scope limited to this issue — no adjacent tickets implemented/stubbed without approval
- [ ] Android device tested when required (native / mic / camera / filesystem / permissions) — do **not** check unless verified on a device

### Details

Closes #<!-- issue number this PR completes -->
Refs #<!-- issue number this PR implements -->

<!-- Short summary. For fixes: root cause + solution. -->

Expand Down
4 changes: 2 additions & 2 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ See [`.eas/README.md`](../.eas/README.md) for Expo GitHub app and Play Store set
## PR preview (QA)

1. Create a `preview-build` label on the repo (if missing).
2. Add the label to a PR when ready for QA (PR should reference its ticket with `Closes #NNN`).
3. Workflow starts a **fresh Android preview APK** (binary only — no OTA / `eas update`), posts the same install comment on the **PR** and each **linked GitHub issue**, and best-effort moves Project 4 cards from `In PR Review` / `In Progress (Dev)` → **`In QA`**.
2. Add the label to a PR when ready for QA (PR should reference its ticket with `Refs #NNN` on its own Details line — not `Part of #NNN`).
3. Workflow starts a **fresh Android preview APK** (binary only — no OTA / `eas update`), posts the same install comment on the **PR** and each **linked GitHub issue** (`Refs` / legacy closing keywords via [`.github/scripts/preview-notify-linked-issues.cjs`](scripts/preview-notify-linked-issues.cjs)), and best-effort moves Project 4 cards from `In PR Review` / `In Progress (Dev)` → **`In QA`**.

**QA guide (non-technical):** [`docs/guides/qa-preview-testing.md`](../docs/guides/qa-preview-testing.md)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
*
* Soft-fails: preview success must not fail if issue/board side effects fail.
*
* Linked tickets are resolved from PR title/body via `Refs #NNN` (preferred)
* and legacy closing keywords (`Closes` / `Fixes` / `Resolves`). `Part of #NNN`
* is intentionally ignored so stacked/partial work does not get preview
* comments or an In QA board move.
*
* Env (optional):
* PROJECT_BOARD_TOKEN — PAT with org project write (preferred for Project 4)
* FLUENT_PROJECT_ID — Projects V2 node id (default: Fluent Project 4)
Expand All @@ -18,7 +23,7 @@
* @param {string[]} args.commentMarkers — substrings that identify prior bot preview comments
* @param {typeof import('@actions/github').getOctokit} [args.getOctokit]
*/
module.exports = async function notifyLinkedIssues({
async function notifyLinkedIssues({
github,
context,
core,
Expand Down Expand Up @@ -78,25 +83,75 @@ module.exports = async function notifyLinkedIssues({
} catch (error) {
core.warning(`Could not update Project 4 Status: ${error.message}`);
}
};
}

/**
* Preferred non-closing link (`Refs #NNN`) plus legacy GitHub closing keywords.
* Does not match `Part of #NNN`.
*/
const LINKED_ISSUE_RE =
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|refs)\s*:?\s*#(\d+)/gi;
Comment on lines +92 to +93

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add token boundaries to LINKED_ISSUE_RE.

unresolved #42 matches `resolved `#42 because the expression does not require a word boundary. The resolver then returns 42, and notifyLinkedIssues can comment on and move that unrelated Project 4 card to In QA. Match complete keywords only. Add a regression test for this input.

Proposed fix
-  /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|refs)\s*:?\s*#(\d+)/gi;
+  /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|refs)\b\s*:?\s*#(\d+)/gi;
+  it('does not match a keyword inside ordinary prose', () => {
+    expect(
+      collectLinkedIssueNumbersFromText(
+        'Title',
+        'The dependency remains unresolved `#42`.',
+        99,
+      ),
+    ).toEqual([]);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/preview-notify-linked-issues.js around lines 92 - 93, Update
LINKED_ISSUE_RE to require word boundaries around the linked-issue action
keywords so substrings such as “unresolved” do not match “resolved”; preserve
valid close, fix, resolve, and refs forms. Add a regression test covering
“unresolved `#42`” and verify notifyLinkedIssues does not process issue 42 for
that input.


const CLOSING_KEYWORD_RE =
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s*#(\d+)/gi;
/**
* Stacked / partial work — must not trigger preview ticket comments or In QA.
* Exported for regression tests only.
*/
const PART_OF_ISSUE_RE = /\bpart\s+of\s*:?\s*#(\d+)/gi;

async function resolveLinkedIssueNumbers(github, owner, repo, prNumber) {
/**
* Collect issue numbers from PR title + body text.
* Includes `Refs #NNN` and closing keywords; excludes `Part of #NNN`.
*
* @param {string} title
* @param {string | null | undefined} body
* @param {number} prNumber — excluded so the PR is never treated as a ticket
* @returns {number[]}
*/
function collectLinkedIssueNumbersFromText(title, body, prNumber) {
const numbers = new Set();
const text = `${title}\n${body || ''}`;

for (const match of text.matchAll(LINKED_ISSUE_RE)) {
numbers.add(Number(match[1]));
}

// Never treat the PR itself as a ticket (same number space)
numbers.delete(prNumber);
return [...numbers].sort((a, b) => a - b);
}

/**
* True when the number appears only as `Part of #NNN` (or not as a linked
* Refs/closing keyword). Used in tests to document the Part of contract.
*
* @param {string} title
* @param {string | null | undefined} body
* @param {number} issueNumber
*/
function isPartOfOnlyReference(title, body, issueNumber) {
const text = `${title}\n${body || ''}`;
const linked = new Set();
for (const match of text.matchAll(LINKED_ISSUE_RE)) {
linked.add(Number(match[1]));
}
if (linked.has(issueNumber)) return false;

for (const match of text.matchAll(PART_OF_ISSUE_RE)) {
if (Number(match[1]) === issueNumber) return true;
}
return false;
}

async function resolveLinkedIssueNumbers(github, owner, repo, prNumber) {
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber,
});

for (const match of `${pr.title}\n${pr.body || ''}`.matchAll(
CLOSING_KEYWORD_RE,
)) {
numbers.add(Number(match[1]));
}
const numbers = new Set(
collectLinkedIssueNumbersFromText(pr.title, pr.body, prNumber),
);

try {
const result = await github.graphql(
Expand Down Expand Up @@ -305,3 +360,13 @@ async function findProjectItem(github, projectId, owner, repo, issueNumber) {
statusName: match.fieldValueByName?.name || '',
};
}

module.exports = notifyLinkedIssues;
module.exports.collectLinkedIssueNumbersFromText =
collectLinkedIssueNumbersFromText;
module.exports.resolveLinkedIssueNumbers = resolveLinkedIssueNumbers;
module.exports.isPartOfOnlyReference = isPartOfOnlyReference;
module.exports.LINKED_ISSUE_RE = LINKED_ISSUE_RE;
module.exports.PART_OF_ISSUE_RE = PART_OF_ISSUE_RE;
module.exports.ALLOWED_FROM_STATUS_NAMES = ALLOWED_FROM_STATUS_NAMES;
module.exports.moveIssuesToInQa = moveIssuesToInQa;
Loading
Loading