Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 8 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
48 changes: 48 additions & 0 deletions .github/workflows/pr-duplicate-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: PR Duplicate Guard

# Stops duplicate same-fix pull requests from silently piling up (the fleet
# kept dispatching the same finding to multiple lanes: #36/#44/#55/#56,
# #39/#49, #40/#52, #38/#48/#51, ...). On every PR event this job compares
# the PR's diff against every other OPEN pull request. When another open PR
# covers the same fix (>= 80% shared changed-file coverage and >= 50% patch
# similarity on the shared files), the check fails loudly and the job posts
# one marker comment naming the duplicate(s) and the canonical PR. The check
# is deliberately not a required status, so legitimate work never blocks on
# it; it exists to make the duplication visible the moment it happens.
#
# Run locally before opening a PR:
# node scripts/check-pr-duplicates.mjs --pr 0 --no-fail --repo OWNER/REPO
# (use the real PR number once the PR exists).

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recheck surviving matches when a peer PR closes

In the inspected pr-duplicate-guard.yml workflow, closing or merging a matched PR triggers none of these event types, so the remaining PR is never reevaluated. If the recommended canonical PR is closed without merging, the survivor retains both a failed check and a marker comment instructing maintainers to close it in favor of a PR that is no longer open; cleaning up only during later synchronizations does not help when its head remains unchanged. Add handling that reevaluates affected open peers when a PR closes, or periodically reconcile outstanding reports.

Useful? React with 👍 / 👎.

workflow_dispatch:
inputs:
pr-number:
description: "PR number to check (defaults to the PR that triggered the run)"
required: false
type: string
Comment on lines +22 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require a PR number for manual dispatches

A workflow_dispatch run has no pull_request payload, so when this optional input is omitted the PR_NUMBER expression resolves empty and the script immediately rejects it as missing. The description's claimed fallback only exists for pull-request-triggered runs, not manual dispatches; make this input required or provide a valid manual-run default.

Useful? React with 👍 / 👎.


permissions:
contents: read
pull-requests: write

concurrency:
group: pr-duplicate-guard-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
duplicate-guard:
runs-on: [self-hosted, linux, x64, vps-verify]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
Comment on lines +40 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pin privileged workflow actions to immutable commits

Unlike the existing repo-check workflow, these steps reference mutable major-version tags. If either tag is retargeted or its upstream release is compromised, replacement code runs on the self-hosted runner; checkout also receives the workflow's write-capable token and persists credentials for subsequent steps, enabling unauthorized PR mutations or runner compromise without any change to this repository. Pin both actions to reviewed full commit SHAs, as .github/workflows/codex-ci.yml already does.

Useful? React with 👍 / 👎.

node-version: 20
- name: Detect same-fix duplicate open PRs
run: node scripts/check-pr-duplicates.mjs --pr "$PR_NUMBER" --comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid executing PR-controlled code with a write token

When a same-repository PR modifies scripts/check-pr-duplicates.mjs, the checkout loads that PR's version and this step executes it with the write-capable GITHUB_TOKEN from the job environment. Unreviewed PR code can therefore perform arbitrary pull-request API mutations; execute an immutable copy of the script from the base revision, or separate detection from a trusted privileged commenting job.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve duplicate reports for fork pull requests

In this pull_request workflow, when a fork-authored or Dependabot PR is detected as a duplicate, the standard GITHUB_TOKEN is read-only despite the requested YAML permission, so upsertComment fails with 403 while deleting or posting the marker comment. The check still turns red, but the promised report never appears on the PR; GitHub documents the fork token restriction under workflows in forked repositories. Route commenting through a trusted privileged workflow while keeping PR-controlled detection unprivileged.

Useful? React with 👍 / 👎.

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.inputs.pr-number || github.event.pull_request.number }}
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ node scripts/test-cross-repo-service.mjs --public-repo "/absolute/path/to/TinySt
git diff --check
```

## PR duplicate guard

Same-fix PR pairs kept appearing because the same finding gets dispatched to
multiple lanes (`fix/operator-export-cli-help` #36 and `fix/operator-export-cli-help-lane1`
#44 were byte-identical patches; #39/#49 and #40/#52 followed the same pattern).
The `PR Duplicate Guard` workflow now compares every PR's diff against all
other open PRs and fails loudly (with a comment naming the canonical PR) when
another open PR covers the same fix with the same changed-file set. Run the
same check locally before opening a PR:

```bash
node scripts/check-pr-duplicates.mjs --pr 0 --repo nish3451/tinystudio-in --no-fail
```
Comment on lines +86 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not advertise an unsupported pre-PR invocation

This documented command always exits with --pr NUMBER is required because argument parsing explicitly rejects --pr 0; moreover, detection expects that number to identify an open PR returned by GitHub. Consequently the advertised local-before-opening check cannot inspect anything. Document a real existing PR number, or implement a mode that compares the local diff without requiring an open PR.

Useful? React with 👍 / 👎.


The check is informational, not a required status, so it never blocks
legitimate work; it exists to make duplication visible the moment it happens.

## SaaS graduation evidence gate

TinyStudio does not become SaaS by assumption. Graduation requires **at least 10 paid sprints**, the **same problem in at least 7**, **at least 70% workflow repeatability**, **usefulness at least 8/10**, **approval at least 70%**, a **recurring need**, and **at least 3 deposits or preorders**. Until every threshold is evidenced, this remains a managed service.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@
"product:truth": "node scripts/check-product-truth.mjs",
"mobbin:run": "node scripts/run-design-system-proving-lab.mjs",
"mobbin:check": "node scripts/run-design-system-proving-lab.mjs --dry-run",
"ci": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-direction-proof-gate.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && node scripts/test-public-structured-data.mjs && node scripts/test-public-brand-disambiguation.mjs && node scripts/test-public-heading-hierarchy.mjs && node scripts/test-public-link-targets.mjs && node scripts/test-public-social-preview.mjs && node scripts/test-public-soft-404.mjs && node scripts/test-public-deploy-bundle.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check",
"ci": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-direction-proof-gate.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && node scripts/test-public-structured-data.mjs && node scripts/test-public-brand-disambiguation.mjs && node scripts/test-public-heading-hierarchy.mjs && node scripts/test-public-link-targets.mjs && node scripts/test-public-social-preview.mjs && node scripts/test-public-soft-404.mjs && node scripts/test-public-deploy-bundle.mjs && node scripts/test-pr-duplicates.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check",
"check": "npm test",
"test": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-direction-proof-gate.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && node scripts/test-public-structured-data.mjs && node scripts/test-public-brand-disambiguation.mjs && node scripts/test-public-heading-hierarchy.mjs && node scripts/test-public-link-targets.mjs && node scripts/test-public-social-preview.mjs && node scripts/test-public-soft-404.mjs && node scripts/test-public-deploy-bundle.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check",
"test": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-direction-proof-gate.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && node scripts/test-public-structured-data.mjs && node scripts/test-public-brand-disambiguation.mjs && node scripts/test-public-heading-hierarchy.mjs && node scripts/test-public-link-targets.mjs && node scripts/test-public-social-preview.mjs && node scripts/test-public-soft-404.mjs && node scripts/test-public-deploy-bundle.mjs && node scripts/test-pr-duplicates.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check",
"site:publish": "node scripts/publish-public-site.mjs",
"site:publish:check": "node scripts/publish-public-site.mjs --prepare-only"
},
Expand Down
Loading