mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
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
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,46 @@
|
||||
## Summary
|
||||
|
||||
<!-- What user or developer problem does this PR solve? -->
|
||||
|
||||
Closes #
|
||||
|
||||
## Root cause and approach
|
||||
|
||||
<!-- For fixes: trace the real failing path. For features: explain the chosen boundary and behavior. -->
|
||||
|
||||
## Changes
|
||||
|
||||
-
|
||||
|
||||
## Compatibility and migrations
|
||||
|
||||
<!-- List config, locale, SQL, framework, public API, event, export, or deployment impact. Write "None" when not applicable. -->
|
||||
|
||||
## 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
|
||||
|
||||
<!-- Distinguish local/static/build checks from an actual restarted FiveM runtime test. Add screenshots for visible UI changes. -->
|
||||
|
||||
## 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
|
||||
|
||||
<!-- Call out risky areas, intentional tradeoffs, or follow-up work. -->
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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).`,
|
||||
);
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = '<!-- sky-phone-test-resource -->';
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user