From 7644dec3a384099aa0b06a3ffeabe4ba7b32328a Mon Sep 17 00:00:00 2001 From: DerEchteAlec Date: Wed, 19 Aug 2026 15:26:50 +0200 Subject: [PATCH] BLD - add GitHub contribution and release automation (#3) * BLD - add GitHub contribution and release automation * BLD - restrict dev merges to maintainers * BLD - add automated review and PR test resources * DOC - require AI governance checks * FIX - pin patched nanoid dependency * TRY - trigger webhook delivery * TRY - verify webhook routing * TRY - rerun pull request checks --- .github/ISSUE_TEMPLATE/bug_report.yml | 114 +++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/documentation.yml | 35 ++++ .github/ISSUE_TEMPLATE/feature_request.yml | 82 +++++++++ .github/PULL_REQUEST_TEMPLATE.md | 46 +++++ .github/rulesets/README.md | 22 +++ .github/rulesets/protect-dev.json | 68 ++++++++ .github/rulesets/protect-release-tags.json | 36 ++++ .github/scripts/validate-repository.mjs | 161 ++++++++++++++++++ .github/workflows/automated-code-review.yml | 52 ++++++ .github/workflows/ci.yml | 119 +++++++++++++ .github/workflows/pr-policy.yml | 61 +++++++ .../workflows/pr-test-resource-comment.yml | 103 +++++++++++ .github/workflows/release.yml | 115 +++++++++++++ AGENTS.md | 38 +++++ CONTRIBUTING.md | 86 ++++++++++ SECURITY.md | 15 ++ frontend/package.json | 5 + frontend/pnpm-lock.yaml | 11 +- 19 files changed, 1173 insertions(+), 4 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/rulesets/README.md create mode 100644 .github/rulesets/protect-dev.json create mode 100644 .github/rulesets/protect-release-tags.json create mode 100644 .github/scripts/validate-repository.mjs create mode 100644 .github/workflows/automated-code-review.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pr-policy.yml create mode 100644 .github/workflows/pr-test-resource-comment.yml create mode 100644 .github/workflows/release.yml create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..fed07a1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,114 @@ +name: Bug report +description: Report a reproducible problem in Sky Phone. +title: "[Bug] " +body: + - type: markdown + attributes: + value: | + Thanks for helping improve Sky Phone. Search existing issues first and remove secrets, tokens, player identifiers, and private URLs from all logs. + + - type: input + id: version + attributes: + label: Sky Phone version + description: Use the release tag or exact commit SHA. "Latest" is not a version. + placeholder: 0.1.0 or 0123456789abcdef... + validations: + required: true + + - type: dropdown + id: framework + attributes: + label: Framework + options: + - ESX Legacy + - QBCore + - Qbox + - Other or custom bridge + validations: + required: true + + - type: input + id: artifacts + attributes: + label: FiveM server artifact + description: Provide the exact artifact build number. + placeholder: "12345" + validations: + required: true + + - type: input + id: database + attributes: + label: Database + description: Include product and version, for example MariaDB 11.4. + placeholder: MariaDB 11.4 + validations: + required: true + + - type: textarea + id: integrations + attributes: + label: Relevant integrations + description: List the configured inventory, voice, housing, garage, media, and other providers involved. + placeholder: | + Inventory: ox_inventory 2.x + Voice: pma-voice 1.x + validations: + required: false + + - type: textarea + id: description + attributes: + label: Problem description + description: Describe what happens and which player or server state is affected. + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: Reproduction steps + description: Provide the smallest reliable sequence, starting from a clean resource restart where possible. + placeholder: | + 1. Start ... + 2. Open ... + 3. Select ... + 4. Observe ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Client, server, and NUI evidence + description: Attach relevant F8, server console, NUI console, network, or database output. Include the first error, not only follow-up errors. + validations: + required: true + + - type: textarea + id: regression + attributes: + label: Regression information + description: State the last known working tag or commit, if applicable. + validations: + required: false + + - type: checkboxes + id: confirmations + attributes: + label: Confirmation + options: + - label: I reproduced this with the resource from this repository and included its exact version. + required: true + - label: I removed secrets, credentials, private URLs, and player-identifying data. + required: true + - label: This is not an exploitable security vulnerability that should be reported privately. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d6deb4c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/sky-systems/sky_phone/security/advisories/new + about: Report exploitable vulnerabilities privately. Do not open a public issue. + - name: Installation and configuration support + url: https://discord.gg/sky-systems + about: Ask for setup help in the Sky-Systems community. diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..e14eec7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,35 @@ +name: Documentation improvement +description: Report missing, incorrect, or unclear project documentation. +title: "[Docs] " +body: + - type: input + id: location + attributes: + label: Documentation location + description: Link the section or provide the repository path and heading. + placeholder: README.md, Quick installation + validations: + required: true + + - type: textarea + id: problem + attributes: + label: What is unclear or incorrect? + validations: + required: true + + - type: textarea + id: correction + attributes: + label: Suggested correction + description: Include verified runtime, configuration, or version context where relevant. + validations: + required: true + + - type: checkboxes + id: confirmations + attributes: + label: Confirmation + options: + - label: I checked the current default branch before reporting this. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..9d2a194 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,82 @@ +name: Feature request +description: Propose a focused improvement to Sky Phone. +title: "[Feature] " +body: + - type: markdown + attributes: + value: | + Explain the user problem before the proposed implementation. New features must preserve Sky Phone's standalone architecture and server-authoritative state changes. + + - type: textarea + id: problem + attributes: + label: Problem or opportunity + description: Who needs this, and what can they not do today? + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed behavior + description: Describe the user-visible result and the expected client, server, NUI, or persistence flow. + validations: + required: true + + - type: dropdown + id: scope + attributes: + label: Primary area + options: + - Phone app or NUI + - Client behavior + - Server behavior + - Framework or third-party integration + - Configuration or localization + - Database or migration + - Developer API or custom apps + - Build, release, or documentation + validations: + required: true + + - type: textarea + id: authority + attributes: + label: Authority and data ownership + description: For state-changing behavior, explain what the server validates and which sky_phone-owned data changes. + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Describe existing configuration, integrations, or workflows that were considered. + validations: + required: false + + - type: textarea + id: compatibility + attributes: + label: Compatibility impact + description: Note affected frameworks, providers, configs, locales, schemas, exports, or public events. + validations: + required: false + + - type: textarea + id: references + attributes: + label: References or mockups + description: Attach concise examples, diagrams, or screenshots when they materially clarify the request. + validations: + required: false + + - type: checkboxes + id: confirmations + attributes: + label: Confirmation + options: + - label: This proposal does not require sky_base, sky_jobs_base, or another Sky resource. + required: true + - label: I searched for an existing issue covering the same request. + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f891ced --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,46 @@ +## Summary + + + +Closes # + +## Root cause and approach + + + +## Changes + +- + +## Compatibility and migrations + + + +## Validation + +- [ ] I ran the narrowest relevant automated tests. +- [ ] I ran `pnpm typecheck`, `pnpm lint`, and `pnpm test` for frontend changes. +- [ ] I ran a production frontend build for frontend changes. +- [ ] I tested affected Lua/native behavior with experimental OAL enabled, or marked the live runtime test as pending below. +- [ ] I verified every changed NUI callback responds on every reachable path. +- [ ] I reviewed the final diff and excluded unrelated work and generated-only edits. + +Commands and results: + +```text + +``` + +## Runtime evidence + + + +## Security and architecture + +- [ ] Consequential actions remain server-authoritative and validate identity, permissions, ownership, limits, and payloads. +- [ ] This change introduces no dependency, event, export, global, config, persistence, or fallback connection to another `sky_*` resource. +- [ ] No secret, credential, private URL, or personal player data is included. + +## Reviewer notes + + diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md new file mode 100644 index 0000000..0a63579 --- /dev/null +++ b/.github/rulesets/README.md @@ -0,0 +1,22 @@ +# Repository rulesets + +These JSON files are import-ready repository rulesets for `sky-systems/sky_phone`. + +## Activation order + +1. Merge the governance files and workflows into the default `dev` branch. +2. Let `CI`, `Automated code review`, and `Pull request policy` run once so GitHub registers the check names. +3. Open **Settings > Rules > Rulesets > New ruleset > Import a ruleset**. +4. Import `protect-dev.json`, review its target and required checks, then activate it. +5. Import `protect-release-tags.json`, review the numeric tag pattern, then activate it. +6. Open a test pull request and confirm all five required checks and the test-resource comment are reported before relying on the ruleset. + +`protect-dev.json` blocks deletion and force pushes, restricts all updates of `dev` to the built-in `Maintain` role, requires one approval, dismisses stale reviews, requires approval after the last push, resolves review conversations, and requires the `Repository policy`, `Frontend`, `CodeQL`, `Dependency review`, and `Pull request policy` checks. Maintainers can bypass rules only through a pull request, so they cannot use this bypass for a direct push to `dev`. + +Successful pull requests receive a 14-day test-resource artifact containing the deployable `sky_phone` folder and built NUI. `Pull request test resource link` runs after `CI` and maintains one download comment in the pull request. It does not check out, download, or execute pull-request content with its write-capable token. + +`protect-release-tags.json` accepts stable numeric semantic versions such as `0.2.0`, rejects a leading `v`, and makes created tags immutable. + +The built-in repository `Maintain` role uses `RepositoryRole` actor ID `2`. Its branch bypass is limited to pull requests and is what permits maintainers to merge; its tag bypass is always available so maintainers can create and recover releases. Changing the committed JSON alone does not update an already imported ruleset. + +Rulesets are GitHub settings, not live configuration files. Committing or editing these JSON files does not activate or update protection automatically; an administrator must import or reconcile them in GitHub. diff --git a/.github/rulesets/protect-dev.json b/.github/rulesets/protect-dev.json new file mode 100644 index 0000000..5b1bdc9 --- /dev/null +++ b/.github/rulesets/protect-dev.json @@ -0,0 +1,68 @@ +{ + "name": "Protect the default development branch", + "target": "branch", + "source_type": "Repository", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["~DEFAULT_BRANCH"], + "exclude": [] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "update", + "parameters": { + "update_allows_fetch_and_merge": false + } + }, + { + "type": "pull_request", + "parameters": { + "allowed_merge_methods": ["merge", "squash", "rebase"], + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": true, + "required_approving_review_count": 1, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "do_not_enforce_on_create": true, + "required_status_checks": [ + { + "context": "Repository policy" + }, + { + "context": "Frontend" + }, + { + "context": "CodeQL" + }, + { + "context": "Dependency review" + }, + { + "context": "Pull request policy" + } + ], + "strict_required_status_checks_policy": true + } + } + ], + "bypass_actors": [ + { + "actor_id": 2, + "actor_type": "RepositoryRole", + "bypass_mode": "pull_request" + } + ] +} diff --git a/.github/rulesets/protect-release-tags.json b/.github/rulesets/protect-release-tags.json new file mode 100644 index 0000000..53fa4d1 --- /dev/null +++ b/.github/rulesets/protect-release-tags.json @@ -0,0 +1,36 @@ +{ + "name": "Protect semantic release tags", + "target": "tag", + "source_type": "Repository", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["~ALL"], + "exclude": [] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "tag_name_pattern", + "parameters": { + "name": "Numeric semantic version without v prefix", + "negate": false, + "operator": "regex", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+\\n?$" + } + } + ], + "bypass_actors": [ + { + "actor_id": 2, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + } + ] +} diff --git a/.github/scripts/validate-repository.mjs b/.github/scripts/validate-repository.mjs new file mode 100644 index 0000000..11feff4 --- /dev/null +++ b/.github/scripts/validate-repository.mjs @@ -0,0 +1,161 @@ +import { readFile, readdir } from "node:fs/promises"; +import { dirname, extname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = resolve(scriptDirectory, "..", ".."); +const failures = []; + +function fail(message) { + failures.push(message); +} + +const manifestPath = join(repositoryRoot, "sky_phone", "fxmanifest.lua"); +const manifest = await readFile(manifestPath, "utf8"); + +for (const requiredFragment of [ + "fx_version 'cerulean'", + "node_version '22'", + "use_experimental_fxv2_oal 'yes'", + "ui_page 'source/html/index.html'", +]) { + if (!manifest.includes(requiredFragment)) { + fail(`fxmanifest.lua is missing required contract: ${requiredFragment}`); + } +} + +const rulesetDirectory = join(repositoryRoot, ".github", "rulesets"); +const rulesetFiles = (await readdir(rulesetDirectory)).filter((file) => + file.endsWith(".json"), +); +const requiredContexts = new Set([ + "Repository policy", + "Frontend", + "CodeQL", + "Dependency review", + "Pull request policy", +]); + +for (const file of rulesetFiles) { + const path = join(rulesetDirectory, file); + let ruleset; + + try { + ruleset = JSON.parse(await readFile(path, "utf8")); + } catch (error) { + fail(`${file} is not valid JSON: ${error.message}`); + continue; + } + + if (!ruleset.name || !["branch", "tag"].includes(ruleset.target)) { + fail(`${file} must define a name and a branch or tag target`); + } + + if (!Array.isArray(ruleset.rules) || ruleset.rules.length === 0) { + fail(`${file} must contain at least one rule`); + } + + const expectedBypassMode = + ruleset.target === "branch" ? "pull_request" : "always"; + const maintainBypass = ruleset.bypass_actors?.some( + (actor) => + actor.actor_type === "RepositoryRole" && + actor.actor_id === 2 && + actor.bypass_mode === expectedBypassMode, + ); + if (!maintainBypass) { + fail(`${file} must retain the expected Maintain role bypass`); + } + + if ( + ruleset.target === "branch" && + !ruleset.rules?.some((rule) => rule.type === "update") + ) { + fail(`${file} must restrict default-branch updates to bypass actors`); + } + + const statusRule = ruleset.rules?.find( + (rule) => rule.type === "required_status_checks", + ); + if (statusRule) { + const contexts = new Set( + statusRule.parameters?.required_status_checks?.map( + (check) => check.context, + ) ?? [], + ); + for (const context of requiredContexts) { + if (!contexts.has(context)) { + fail(`${file} is missing required status context: ${context}`); + } + } + } +} + +const sourceRoots = [ + join(repositoryRoot, "sky_phone"), + join(repositoryRoot, "frontend", "src"), +]; +const inspectedExtensions = new Set([ + ".cjs", + ".js", + ".json", + ".lua", + ".mjs", + ".sql", + ".ts", + ".vue", +]); +const forbiddenPatterns = [ + { + label: "a forbidden Sky resource reference", + pattern: /\bsky_(?:base|jobs_base)(?::|\b)/i, + }, + { + label: "a forbidden shared Sky global", + pattern: /\bSky\.(?:FW|Cb|DB|Query)\b/, + }, +]; + +async function inspectDirectory(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + if ( + entry.name === "html" && + directory.endsWith(join("sky_phone", "source")) + ) { + continue; + } + + const path = join(directory, entry.name); + if (entry.isDirectory()) { + await inspectDirectory(path); + continue; + } + + if (!inspectedExtensions.has(extname(entry.name))) { + continue; + } + + const content = await readFile(path, "utf8"); + for (const { label, pattern } of forbiddenPatterns) { + if (pattern.test(content)) { + fail(`${path.slice(repositoryRoot.length + 1)} contains ${label}`); + } + } + } +} + +for (const sourceRoot of sourceRoots) { + await inspectDirectory(sourceRoot); +} + +if (failures.length > 0) { + console.error("Repository policy validation failed:"); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exit(1); +} + +console.log( + `Repository policy validation passed (${rulesetFiles.length} rulesets checked).`, +); diff --git a/.github/workflows/automated-code-review.yml b/.github/workflows/automated-code-review.yml new file mode 100644 index 0000000..3ebad9d --- /dev/null +++ b/.github/workflows/automated-code-review.yml @@ -0,0 +1,52 @@ +name: Automated code review + +on: + pull_request: + push: + branches: + - dev + workflow_dispatch: + +concurrency: + group: automated-review-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + codeql: + name: CodeQL + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + packages: read + security-events: write + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: javascript-typescript + + - name: Analyze JavaScript and TypeScript + uses: github/codeql-action/analyze@v4 + with: + category: /language:javascript-typescript + + dependency-review: + name: Dependency review + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Review dependency changes + uses: actions/dependency-review-action@v5 + with: + fail-on-severity: high diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d02c0f2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,119 @@ +name: CI + +on: + pull_request: + push: + branches: + - dev + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + repository-policy: + name: Repository policy + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + + - name: Validate repository contracts + run: node .github/scripts/validate-repository.mjs + + - name: Install Lua compiler + run: | + sudo apt-get update + sudo apt-get install --yes lua5.4 + + - name: Check Lua syntax + shell: bash + run: | + set -euo pipefail + while IFS= read -r -d '' file; do + luac5.4 -p "$file" + done < <(find sky_phone tests -type f -name '*.lua' -print0) + + frontend: + name: Frontend + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + working-directory: frontend + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up pnpm + uses: pnpm/action-setup@v6 + with: + version: 10.33.0 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check repository formatting and schemas + run: >- + pnpm exec prettier --check + "../.github/**/*.{yml,yaml,json,md}" + "../.github/scripts/*.mjs" + "../CONTRIBUTING.md" + "../SECURITY.md" + + - name: Lint frontend + run: pnpm lint + + - name: Typecheck frontend + run: pnpm typecheck + + - name: Test frontend + run: pnpm test + + - name: Build deployable NUI + run: pnpm build-only + + - name: Verify published NUI entrypoint + run: test -f ../sky_phone/source/html/index.html + + - name: Package pull request test resource + if: github.event_name == 'pull_request' + shell: bash + working-directory: . + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + artifact_name="sky_phone-pr-${PR_NUMBER}-${HEAD_SHA}" + mkdir -p artifacts + zip -r "artifacts/${artifact_name}.zip" sky_phone + unzip -t "artifacts/${artifact_name}.zip" + unzip -Z1 "artifacts/${artifact_name}.zip" | grep -Fxq 'sky_phone/fxmanifest.lua' + unzip -Z1 "artifacts/${artifact_name}.zip" | grep -Fxq 'sky_phone/source/html/index.html' + + - name: Upload pull request test resource + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + path: artifacts/sky_phone-pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}.zip + archive: false + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml new file mode 100644 index 0000000..a607110 --- /dev/null +++ b/.github/workflows/pr-policy.yml @@ -0,0 +1,61 @@ +name: Pull request policy + +on: + pull_request: + types: + - opened + - edited + - reopened + - synchronize + - ready_for_review + +permissions: + contents: read + +jobs: + policy: + name: Pull request policy + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + BASE_REF: ${{ github.base_ref }} + HEAD_REF: ${{ github.head_ref }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + steps: + - name: Validate target branch + shell: bash + run: | + set -euo pipefail + if [[ "$BASE_REF" != "dev" ]]; then + echo "Pull requests must target dev; received: $BASE_REF" + exit 1 + fi + + - name: Validate source branch + shell: bash + run: | + set -euo pipefail + branch_pattern='^(feat|feature|fix|hotfix|docs|refactor|perf|test|build|ci|chore|release)/[a-z0-9]+([._-][a-z0-9]+)*$' + if [[ "$HEAD_REF" == dependabot/* ]]; then + exit 0 + fi + if [[ ! "$HEAD_REF" =~ $branch_pattern ]]; then + echo "Invalid branch name: $HEAD_REF" + echo "Expected type/lowercase-kebab-case; see CONTRIBUTING.md." + exit 1 + fi + + - name: Validate pull request title + shell: bash + run: | + set -euo pipefail + if [[ "$PR_AUTHOR" == "dependabot[bot]" ]]; then + exit 0 + fi + title_pattern='^(ENH|ADD|FIX|DOC|BLD|PERF|CLN|TRY) - [^[:space:]].{4,72}$' + if [[ ! "$PR_TITLE" =~ $title_pattern ]] || (( ${#PR_TITLE} > 80 )); then + echo "Invalid pull request title: $PR_TITLE" + echo "Expected: TAG - short imperative summary (maximum 80 characters)." + exit 1 + fi diff --git a/.github/workflows/pr-test-resource-comment.yml b/.github/workflows/pr-test-resource-comment.yml new file mode 100644 index 0000000..7249d75 --- /dev/null +++ b/.github/workflows/pr-test-resource-comment.yml @@ -0,0 +1,103 @@ +name: Pull request test resource link + +on: + workflow_run: + workflows: + - CI + types: + - completed + +permissions: + actions: read + contents: read + pull-requests: write + +jobs: + comment: + name: Link test resource + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Create or update pull request comment + uses: actions/github-script@v9 + with: + script: | + const marker = ''; + const run = context.payload.workflow_run; + const pullRequest = run.pull_requests?.[0]; + + if (!pullRequest) { + core.notice('The CI run is not associated with a pull request.'); + return; + } + + const { owner, repo } = context.repo; + const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${run.id}`; + let body; + + if (run.conclusion === 'success') { + const response = await github.rest.actions.listWorkflowRunArtifacts({ + owner, + repo, + run_id: run.id, + per_page: 100, + }); + const prefix = `sky_phone-pr-${pullRequest.number}-`; + const artifact = response.data.artifacts.find( + (candidate) => + !candidate.expired && + candidate.name.startsWith(prefix) && + /^sky_phone-pr-\d+-[0-9a-f]{40}\.zip$/.test(candidate.name), + ); + + if (!artifact) { + core.setFailed('CI succeeded without a pull request test resource artifact.'); + return; + } + + const artifactUrl = `https://github.com/${owner}/${repo}/actions/runs/${run.id}/artifacts/${artifact.id}`; + body = [ + marker, + '## Sky Phone Test-Resource', + '', + `✅ Der aktuelle Pull Request wurde erfolgreich geprüft und gebaut. [Test-Resource herunterladen](${artifactUrl}).`, + '', + 'Das ZIP enthält den deploybaren `sky_phone`-Ordner inklusive gebautem Frontend und bleibt 14 Tage verfügbar.', + '', + `[Workflow-Lauf öffnen](${runUrl})`, + ].join('\n'); + } else { + body = [ + marker, + '## Sky Phone Test-Resource', + '', + `❌ Für den aktuellen Stand wurde keine freigegebene Test-Resource erzeugt, weil der [CI-Lauf](${runUrl}) nicht erfolgreich war.`, + ].join('\n'); + } + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: pullRequest.number, + per_page: 100, + }); + const existing = comments.find( + (comment) => comment.user?.type === 'Bot' && comment.body?.includes(marker), + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pullRequest.number, + body, + }); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..39b271f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,115 @@ +name: Release + +on: + push: + tags: + - "*.*.*" + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + +jobs: + release: + name: Build and publish release + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out tagged source + uses: actions/checkout@v6 + + - name: Validate tag and manifest version + shell: bash + run: | + set -euo pipefail + if [[ ! "$GITHUB_REF_NAME" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Release tags must use numeric semantic versions without v: $GITHUB_REF_NAME" + exit 1 + fi + manifest_version=$(sed -n "s/^version '\([^']*\)'/\1/p" sky_phone/fxmanifest.lua) + if [[ "$manifest_version" != "$GITHUB_REF_NAME" ]]; then + echo "Tag $GITHUB_REF_NAME does not match fxmanifest version $manifest_version" + exit 1 + fi + + - name: Set up pnpm + uses: pnpm/action-setup@v6 + with: + version: 10.33.0 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + + - name: Install frontend dependencies + working-directory: frontend + run: pnpm install --frozen-lockfile + + - name: Validate, test, and build frontend + working-directory: frontend + run: | + pnpm lint + pnpm test + pnpm build + + - name: Verify deployable phone resource + shell: bash + run: | + set -euo pipefail + test -f sky_phone/fxmanifest.lua + test -f sky_phone/source/html/index.html + test -d sky_phone/source/html/assets + + - name: Validate repository contracts + run: node .github/scripts/validate-repository.mjs + + - name: Install Lua compiler + run: | + sudo apt-get update + sudo apt-get install --yes lua5.4 + + - name: Check Lua syntax + shell: bash + run: | + set -euo pipefail + while IFS= read -r -d '' file; do + luac5.4 -p "$file" + done < <(find sky_phone tests -type f -name '*.lua' -print0) + + - name: Create release archive + shell: bash + run: | + set -euo pipefail + archive="sky_phone-${GITHUB_REF_NAME}.zip" + zip -r "$archive" sky_phone + unzip -t "$archive" + unzip -Z1 "$archive" | grep -Fx "sky_phone/fxmanifest.lua" + unzip -Z1 "$archive" | grep -Fx "sky_phone/source/html/index.html" + sha256sum "$archive" > "${archive}.sha256" + + - name: Upload workflow artifact + uses: actions/upload-artifact@v7 + with: + name: sky_phone-${{ github.ref_name }} + path: | + sky_phone-${{ github.ref_name }}.zip + sky_phone-${{ github.ref_name }}.zip.sha256 + if-no-files-found: error + retention-days: 30 + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: >- + gh release create "$GITHUB_REF_NAME" + "sky_phone-${GITHUB_REF_NAME}.zip" + "sky_phone-${GITHUB_REF_NAME}.zip.sha256" + --verify-tag + --generate-notes + --title "Sky Phone $GITHUB_REF_NAME" diff --git a/AGENTS.md b/AGENTS.md index 1138622..7f5f0d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,44 @@ This repository contains the standalone FiveM resource `sky_phone`. These rules apply throughout the repository unless a more specific `AGENTS.md` adds stricter requirements. +## Repository governance for AI contributors + +AI agents and automated coding tools must treat the repository governance files +as project instructions, not as optional documentation. + +- Before changing code, read `CONTRIBUTING.md` and every more specific + `AGENTS.md` that applies to the target files. For issue or pull-request work, + also read the matching file under `.github/ISSUE_TEMPLATE` and + `.github/PULL_REQUEST_TEMPLATE.md`. +- Before changing CI, packaging, releases, contribution policy, branch policy, + or dependency handling, inspect all relevant files under `.github/workflows`, + `.github/rulesets`, and `.github/scripts`. Keep their contracts synchronized. +- Preserve the required check names `Repository policy`, `Frontend`, `CodeQL`, + `Dependency review`, and `Pull request policy`. If a task intentionally + renames or replaces one, update `.github/rulesets/protect-dev.json`, + `.github/scripts/validate-repository.mjs`, `.github/rulesets/README.md`, and + the corresponding workflow in the same change. +- Anyone may open a pull request, but only collaborators with GitHub's built-in + `Maintain` role may merge into `dev`, and maintainers must merge through a + pull request. Preserve this behavior in branch rules and documentation. +- Treat `.github/rulesets/*.json` as the version-controlled ruleset baseline. + Editing or merging these files does not update GitHub settings automatically; + report that an administrator must import or reconcile the live ruleset. +- Preserve the secure PR artifact boundary: untrusted pull-request code may + build the test resource only with read permissions. A workflow with write + permissions may inspect trusted GitHub metadata and maintain the PR comment, + but must never check out, download, extract, import, or execute PR-controlled + code or artifacts. +- PR and release packages must contain one top-level `sky_phone` directory with + `fxmanifest.lua` and the built NUI at `source/html/index.html`. Build generated + NUI from `frontend`; never hand-edit `sky_phone/source/html`. +- Keep automated review and test-resource findings visible. Do not weaken, + skip, or silence validation merely to make a check pass. Automated review + complements and never replaces the required human maintainer review. +- After governance changes, run the repository validator, Prettier, and + actionlint where available. Inspect the final diff and stage only files that + belong to the task. + ## Architecture and security - Keep `sky_phone` independent. Do not add dependencies or integrations with diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dd61cce --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Contributing to Sky Phone + +Sky Phone uses `dev` as its default integration branch. All normal changes reach `dev` through a pull request; do not push feature work directly to it. + +## Branches + +Create a short-lived branch from an up-to-date `dev` branch. Use lowercase kebab-case after one of these prefixes: + +| Prefix | Purpose | +| ----------- | ------------------------------------------------ | +| `feat/` | New behavior or app capability | +| `fix/` | Bug fix | +| `hotfix/` | Urgent release repair | +| `docs/` | Documentation only | +| `refactor/` | Internal change without intended behavior change | +| `perf/` | Performance work | +| `test/` | Test-only work | +| `build/` | Build or packaging work | +| `ci/` | GitHub Actions and repository automation | +| `chore/` | Focused maintenance | +| `release/` | Release preparation | + +`feature/` remains accepted for existing branches, but new feature branches should use `feat/`. + +Examples: `feat/mail-signatures`, `fix/radio-focus`, `ci/release-package`. Avoid personal names, issue titles, uppercase characters, spaces, and branches that combine unrelated work. + +## Commits and pull requests + +Use the repository commit format: + +```text +TAG - short imperative summary +``` + +Allowed tags are `ENH`, `ADD`, `FIX`, `DOC`, `BLD`, `PERF`, `CLN`, and `TRY`. Examples: + +```text +FIX - validate mail ownership before deletion +ENH - add per-account notification settings +DOC - clarify Qbox installation order +``` + +Use the same format for the pull request title. Keep commits focused, stage only task files, link the issue, and complete the pull request template with actual commands and results. + +## Architecture and security rules + +- Sky Phone is standalone. It must not depend on or exchange state with `sky_base`, `sky_jobs_base`, or another `sky_*` resource. +- The server validates and decides permissions, identity, money, inventory, ownership, proximity, limits, and all other consequential state. +- Treat NUI and client payloads as untrusted. Every NUI callback must invoke its response callback on every reachable path. +- Parameterize SQL, keep persistence owned by `sky_phone`, and send only the required data over the network. +- Keep credentials, tokens, private endpoints, and player-identifying data out of source, fixtures, logs, issues, and pull requests. + +## Schema, configuration, and compatibility + +- Prefix new resource-owned tables, persistent keys, convars, callbacks, and events with `sky_phone` where the technology permits it. +- Keep the runtime schema in `sky_phone/source/server/db_migrate.lua` and the clean-install schema in `sky_phone/sql/install.sql` aligned. +- Prefer additive, idempotent migrations. Destructive or lossy migrations require an explicit migration plan, backup guidance, and reviewer approval. +- Do not force a database charset or collation unless a documented compatibility requirement has been reviewed. +- Preserve public events, callbacks, exports, configuration defaults, and stored data unless the linked issue explicitly authorizes a breaking change. +- Update both English and German locales for user-facing text. Logs and developer diagnostics remain in English. +- New or substantially changed NUI screens use the public Sky UI components and semantic tokens under `frontend/src/ui`. + +## Local validation + +Install frontend dependencies with pnpm, then run the checks relevant to the change: + +```powershell +cd frontend +pnpm install --frozen-lockfile +pnpm typecheck +pnpm lint +pnpm test +pnpm build +``` + +The build publishes the generated NUI into `sky_phone/source/html`. Do not hand-edit generated output. A successful build proves source/build consistency, not behavior inside FiveM; report live runtime testing separately. + +Every pull request also receives an automated CodeQL scan and dependency review. After the full CI run succeeds, GitHub packages the deployable `sky_phone` folder as a test-resource ZIP and adds or updates a download link in the pull request. The artifact is retained for 14 days. It is suitable for manual testing on a test server, but it is not a release and does not replace live FiveM validation. + +Lua, config, manifest, locale, SQL, and native changes must also be tested in a restarted FiveM resource with experimental OAL enabled. Pass native coordinates as separate numeric arguments and verify native signatures against authoritative documentation. + +## Review and merge + +A pull request is ready when the repository policy, frontend, CodeQL, dependency review, and pull-request policy checks pass; review conversations are resolved; the latest push is approved by someone other than its author; and migrations or operational steps are explicit. Automated findings complement rather than replace the human maintainer review. Anyone may open a pull request, but only collaborators with the built-in GitHub `Maintain` role may merge into `dev`. Maintainers must merge through a pull request; the ruleset does not permit direct pushes to `dev`. The default ruleset allows merge, squash, and rebase so maintainers can preserve meaningful merge history when needed. + +Release tags use numeric semantic versions without a `v` prefix, for example `0.2.0`. Tags are immutable after creation. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e2a0d18 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security policy + +## Reporting a vulnerability + +Do not open a public issue for an exploitable vulnerability or include exploit details in Discord, logs, screenshots, or pull requests. + +Use [GitHub private vulnerability reporting](https://github.com/sky-systems/sky_phone/security/advisories/new). If that form is unavailable, use the private contact listed on the [official Sky-Systems contact page](https://www.sky-systems.net/impressum). + +Include the affected release tag or commit, framework and integration context, impact, minimal reproduction, and any proposed mitigation. Remove credentials, tokens, private server addresses, and player-identifying data. + +We will acknowledge the report, reproduce and assess the impact, coordinate a fix, and publish details after affected users have a reasonable update path. Please do not disclose the issue publicly before that coordination is complete. + +## Supported versions + +Security fixes target the latest published release and the current `dev` branch. Older releases may require upgrading before a fix can be applied. diff --git a/frontend/package.json b/frontend/package.json index b562569..2e410a3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,11 @@ "test": "vitest run && pnpm test:browser-mocks", "test:browser-mocks": "node testserver/smoke.cjs" }, + "pnpm": { + "overrides": { + "nanoid@<3.3.18": "3.3.18" + } + }, "dependencies": { "@tiptap/core": "^3.29.2", "@tiptap/extension-placeholder": "^3.29.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2d07394..5c01961 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + nanoid@<3.3.18: 3.3.18 + importers: .: @@ -1940,8 +1943,8 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - nanoid@3.3.17: - resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -4351,7 +4354,7 @@ snapshots: muggle-string@0.4.1: {} - nanoid@3.3.17: {} + nanoid@3.3.18: {} natural-compare@1.4.0: {} @@ -4449,7 +4452,7 @@ snapshots: postcss@8.5.25: dependencies: - nanoid: 3.3.17 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1