Your first contribution
This module turns CONTRIBUTING.md, package.json, and the GitHub Actions workflows into a concrete playbook: how to find work you're actually allowed to start, how the colocated *.test.ts suite you've already seen in packages/features/bookings/lib gets run by yarn test in CI, and exactly which jobs in .github/workflows/pr.yml must go green — and under what trust gate — before a PR can merge. By the end you can pick an approved issue, size a PR the way this repo's reviewers expect, and read pr.yml cold to predict whether your own PR needs a maintainer's run-ci label.
## Making a Pull Request ### Keep PRs Small and Focused Large PRs are difficult to review and more prone to errors. We strongly encourage smaller, self-contained PRs: - **Size limits**: Keep PRs under 500 lines of code changed and under 10 code files modified (excludes documentation, lock files, and auto-generated files)- **Single responsibility**: Each PR should address one concern (one feature, one bug fix, or one refactor)- **Split large changes**: If your task requires extensive changes, break it into multiple PRs that can be reviewed and merged independently **How to split large changes:**- Separate database/schema changes from application logic- Split frontend and backend changes when possible- Do preparatory refactoring in a separate PR before adding new features- Create PRs in dependency order (infrastructure first, then features) ### PR Checklist - Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) when creating your PR. (This option isn't available if you're [contributing from a fork belonging to an organization](https://github.com/orgs/community/discussions/5634))- If your PR refers to or fixes an issue, add `refs #XXX` or `fixes #XXX` to the PR description. Replace `XXX` with the respective issue number. See more about [linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).- Fill out the PR template accordingly.- Review the [App Contribution Guidelines](./packages/app-store/CONTRIBUTING.md) when building integrations.- Lastly, make sure to keep your branches updated (e.g., click the `Update branch` button on the GitHub PR page).
CONTRIBUTING.md · lines 193–215Allow edits from maintainers when opening the PR, and add refs #XXX or fixes #XXX in the description so GitHub auto-links (and, on merge, auto-closes) the issue you're addressing.### Repository Files - Repository class files must include the `Repository` suffix.- If the repository is backed by a specific technology (e.g. Prisma), prefix the filename and class name with it.- File name must match the exported class exactly (PascalCase). **Pattern:** `Prisma<Entity>Repository.ts` **Examples:** ```ts// File: PrismaAppRepository.tsexport class PrismaAppRepository { ... } // File: PrismaMembershipRepository.tsexport class PrismaMembershipRepository { ... }``` This avoids ambiguous filenames like app.ts and improves discoverability in editors. ### Service Files - Service class files must include the Service suffix.- File name should be in PascalCase, matching the exported class.- Keep naming specific — avoid generic names like AppService.ts. **Pattern:** `<Entity>Service.ts` **Examples:** ```ts// File: MembershipService.tsexport class MembershipService { ... } // File: HashedLinkService.tsexport class HashedLinkService { ... }```
CONTRIBUTING.md · lines 104–144Repository suffix, prefixed with its backing technology (PrismaAppRepository.ts exports PrismaAppRepository, matching Prisma<Entity>Repository.ts), and a service class file must carry the Service suffix in a specific, non-generic name (MembershipService.ts, not AppService.ts). Because 'file name must match the exported class exactly,' you can jump from an import statement straight to the right file with fuzzy-find, with no .service.ts/.repository.ts dot-suffixes to guess at.How yarn test runs the colocated *.test.ts suite in CI
.github/workflows/unit-tests.yml · lines 1–22The trigger on: workflow_call marks .github/workflows/unit-tests.yml as a reusable workflow: it never runs on its own, only when another workflow calls it. pr.yml's unit-test job does exactly that with uses: ./.github/workflows/unit-tests.yml — this file is what actually executes behind that gate.
The job is named test, capped at timeout-minutes: 20, and runs on ubuntu-latest — a hard ceiling so a hung suite fails the check instead of blocking CI indefinitely.
Before any test executes, the job checks out the repo, restores cached dependencies (cache-checkout, yarn-install), and runs yarn prisma generate so tests that import Prisma-generated types compile.
yarn test -- --no-isolate is package.json's test script — TZ=UTC vitest run — run with Vitest's shared-worker --no-isolate flag for speed. Every *.test.ts file colocated beside its source is what this command picks up: EventManager.test.ts sits right next to EventManager.ts in packages/features/bookings/lib, the calendar-sync class from the App Store integration pattern module, importing it with a plain relative path (./EventManager) — no separate test registry or directory involved.
CI then reruns the identical suite a second time under TZ=America/Los_Angeles with VITEST_MODE=timezone — the same booking-time logic traced in earlier modules only breaks in some timezones, so this second pass is how that class of bug gets caught before merge.
name: Uniton: workflow_call:env: LINGO_DOT_DEV_API_KEY: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY }}permissions: contents: readjobs: test: name: Unit timeout-minutes: 20 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: sparse-checkout: .github - uses: ./.github/actions/cache-checkout - uses: ./.github/actions/yarn-install - run: yarn prisma generate - run: yarn test -- --no-isolate # We could add different timezones here that we need to run our tests in - run: TZ=America/Los_Angeles VITEST_MODE=timezone yarn test -- --no-isolate
// Check 1: Is the author a trusted contributor? if (trustedAssociations.includes(pr.author_association)) { console.log(`Author has trusted association: ${pr.author_association}`); core.setOutput('is-trusted', true); return; } // Check 2: Verify write access via API (author_association can be unreliable) if (await hasWriteAccess(pr.user.login)) { console.log(`Author has write access`); core.setOutput('is-trusted', true); return; } // Check 3: Was 'run-ci' label added AFTER this SHA was pushed by someone with write access? // This enables re-runs triggered by the run-ci.yml workflow // NOTE: We use workflow run created_at instead of commit timestamp because // git commit timestamps can be arbitrarily backdated by attackers if (pr.labels?.some(l => l.name === 'run-ci')) {
.github/workflows/pr.yml · lines 81–99trust-check job's script climbs three checks before it sets is-trusted to true: is the author's author_association one of OWNER/MEMBER/COLLABORATOR, does the author have write access via the API, and — the path an outside contributor actually takes — did a maintainer with write access add the run-ci label to the PR after the current commit was pushed. Only then do the lint, type-check, unit-test, api-v2-unit-test, security-audit, and e2e jobs downstream even start; until a maintainer applies that label, your PR sits with no checks running at all.required: the one job that actually gates the merge button
.github/workflows/pr.yml · lines 416–475The required job's needs array is the entire gate list in one place: trust-check, prepare, lint, type-check, unit-test, api-v2-unit-test, security-audit, check-prisma-migrations, every build job, and every e2e* suite. Branch protection on this repo only has to watch one job — required — instead of listing a dozen separately, and that one job fans back out to every real check.
if: always() overrides GitHub Actions' default behavior of skipping a job when one of its dependencies failed. Without it, required would itself get skipped the moment any upstream job failed — and a skipped required job does not block a merge the way a failed one does. always() guarantees required runs so it can inspect every dependency's result itself.
The first step fails outright if trust-check itself didn't complete successfully — a broken security gate is never treated as a silent pass.
The second step is the merge-time half of the trust gate: if trust-check succeeded but reported is-trusted != 'true', this step exits 1 with an explicit message that a maintainer needs to add run-ci. This is what actually stops an external PR from merging, not just from running checks.
The third step is one long boolean across every conditional job's result — lint, type-check, unit-test, api-v2-unit-test, security-audit, and, only when has-prisma-changes is true, check-prisma-migrations, plus the build and e2e* jobs. If any of them isn't 'success', this step runs exit 1, and since required is the single job GitHub's branch protection rule actually watches, that failure is what blocks the merge button.
required: needs: [ trust-check, prepare, lint, type-check, unit-test, api-v2-unit-test, security-audit, check-prisma-migrations, integration-test, build, build-api-v2, build-atoms, setup-db, e2e, e2e-api-v2, e2e-embed, e2e-embed-react, e2e-app-store, ] if: always() runs-on: ubuntu-latest steps: - name: Fail if trust-check did not succeed run: | echo "::error::Trust check did not complete successfully (result: ${{ needs.trust-check.result }}). Please re-run the workflow." exit 1 if: needs.trust-check.result != 'success' - name: Fail if PR is not trusted (external contributor without run-ci label) run: | echo "::error::This PR is from an external contributor and requires the 'run-ci' label before CI can run." echo "A maintainer must review the code and add the 'run-ci' label to trigger CI checks." exit 1 if: needs.trust-check.outputs.is-trusted != 'true' && needs.trust-check.result == 'success' - name: Fail if conditional jobs failed run: exit 1 if: | ( needs.prepare.outputs.has-files-requiring-all-checks == 'true' && ( needs.lint.result != 'success' || needs.type-check.result != 'success' || needs.unit-test.result != 'success' || needs.api-v2-unit-test.result != 'success' || needs.security-audit.result != 'success' || (needs.prepare.outputs.has-prisma-changes == 'true' && needs.check-prisma-migrations.result != 'success') || needs.build.result != 'success' || needs.build-api-v2.result != 'success' || needs.build-atoms.result != 'success' || needs.setup-db.result != 'success' || needs.integration-test.result != 'success' || needs.e2e.result != 'success' || needs.e2e-api-v2.result != 'success' || needs.e2e-embed.result != 'success' || needs.e2e-embed-react.result != 'success' || needs.e2e-app-store.result != 'success' ) )
Check your understanding
Answered in place — nothing is graded, everything is explained. 0 / 3 passed
An external contributor (no write access, not a trusted association) opens a PR. What has to happen before pr.yml's checks actually run?
Per CONTRIBUTING.md, which issues can you start coding on right away, even while the 🚨 needs approval label is still present?
Where do this repo's unit tests live, and what actually runs them in CI?