From 1bdc1c4ff3f608626c773310a5ff7a56df575065 Mon Sep 17 00:00:00 2001 From: Josh Hufford Date: Mon, 21 Sep 2026 23:41:12 -0400 Subject: [PATCH 1/9] Moslty documentation updates --- .browserslistrc | 5 +- .github/ISSUE_TEMPLATE/bug_report.yml | 5 +- .../ISSUE_TEMPLATE/compatibility_issue.yml | 15 +- .github/ISSUE_TEMPLATE/config.yml | 7 + .github/ISSUE_TEMPLATE/enhancement.yml | 5 +- .github/ISSUE_TEMPLATE/feature_request.yml | 11 +- AGENTS.md | 5 + AI_POLICY.md | 360 +++++------------- CONTRIBUTING.md | 33 +- README.md | 82 +++- SUPPORT.md | 39 +- docs/README.md | 58 +++ docs/backend/api.md | 33 ++ docs/backend/blob-storage.md | 0 docs/backend/database.md | 0 docs/community/stewardship.md | 57 +++ docs/foundation-checklist.md | 119 +++++- docs/frontend/browser-support.md | 93 +++++ docs/licenses/aspire-MIT.txt | 23 ++ docs/licenses/aspire-skills-MIT.txt | 21 + docs/mygamebuilder/bugs.md | 0 docs/mygamebuilder/data-archive.md | 5 + docs/mygamebuilder/decompilation.md | 0 docs/mygamebuilder/mgb-local.md | 0 docs/quality/coding-standards.md | 0 docs/quality/naming-conventions.md | 0 docs/quality/performance.md | 0 docs/quality/testing.md | 9 +- docs/resources/asp-net-core.md | 97 ----- docs/resources/blazor.md | 99 ----- docs/resources/browser-devtools.md | 40 -- docs/resources/caddy.md | 34 -- docs/resources/copilot.md | 0 docs/resources/csharp.md | 78 ---- docs/resources/css.md | 0 docs/resources/docker.md | 0 docs/resources/dotnet.md | 0 docs/resources/entity-framework-core.md | 0 docs/resources/git.md | 0 docs/resources/github.md | 0 docs/resources/html.md | 0 docs/resources/json.md | 0 docs/resources/markdown.md | 0 docs/resources/minio.md | 0 docs/resources/postgresql.md | 0 docs/resources/visual-studio.md | 0 docs/resources/vscode.md | 0 docs/resources/xunit.md | 0 docs/setup/ai-tooling.md | 117 ++++++ docs/setup/github.md | 7 +- 50 files changed, 757 insertions(+), 700 deletions(-) create mode 100644 docs/README.md delete mode 100644 docs/backend/blob-storage.md delete mode 100644 docs/backend/database.md create mode 100644 docs/community/stewardship.md create mode 100644 docs/licenses/aspire-MIT.txt create mode 100644 docs/licenses/aspire-skills-MIT.txt delete mode 100644 docs/mygamebuilder/bugs.md delete mode 100644 docs/mygamebuilder/decompilation.md delete mode 100644 docs/mygamebuilder/mgb-local.md delete mode 100644 docs/quality/coding-standards.md delete mode 100644 docs/quality/naming-conventions.md delete mode 100644 docs/quality/performance.md delete mode 100644 docs/resources/asp-net-core.md delete mode 100644 docs/resources/blazor.md delete mode 100644 docs/resources/browser-devtools.md delete mode 100644 docs/resources/caddy.md delete mode 100644 docs/resources/copilot.md delete mode 100644 docs/resources/csharp.md delete mode 100644 docs/resources/css.md delete mode 100644 docs/resources/docker.md delete mode 100644 docs/resources/dotnet.md delete mode 100644 docs/resources/entity-framework-core.md delete mode 100644 docs/resources/git.md delete mode 100644 docs/resources/github.md delete mode 100644 docs/resources/html.md delete mode 100644 docs/resources/json.md delete mode 100644 docs/resources/markdown.md delete mode 100644 docs/resources/minio.md delete mode 100644 docs/resources/postgresql.md delete mode 100644 docs/resources/visual-studio.md delete mode 100644 docs/resources/vscode.md delete mode 100644 docs/resources/xunit.md create mode 100644 docs/setup/ai-tooling.md diff --git a/.browserslistrc b/.browserslistrc index 70c38b7..a266268 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,5 +1,6 @@ -# Browser support policy for frontend compatibility tooling. -# See docs/frontend/browser-support.md. +# Compatibility-tooling declaration; no current build step consumes this file. +# These queries do not implement or verify browser support. +# See docs/frontend/browser-support.md for targets and recorded checks. last 2 Chrome major versions last 2 Edge major versions diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b002ebb..5bebb94 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,6 @@ name: Bug Report description: Report something broken or not working as expected. -labels: ["needs triage"] -type: bug +type: Bug body: - type: markdown @@ -11,7 +10,7 @@ body: Please do not include passwords, tokens, private information, security vulnerability details, sensitive material, archived game dumps, original proprietary assets, or decompiled source code. - Security issues should be reported privately using `SECURITY.md`. + Security issues should follow the [private reporting instructions](https://github.com/OpenGameBuilder/opengamebuilder/blob/main/SECURITY.md). - type: checkboxes id: preflight diff --git a/.github/ISSUE_TEMPLATE/compatibility_issue.yml b/.github/ISSUE_TEMPLATE/compatibility_issue.yml index e2351ee..3d57ff2 100644 --- a/.github/ISSUE_TEMPLATE/compatibility_issue.yml +++ b/.github/ISSUE_TEMPLATE/compatibility_issue.yml @@ -1,7 +1,6 @@ name: Compatibility Issue description: Report behavior that differs from MyGameBuilder or affects restored games. -labels: ["needs triage"] -type: compatibility +type: Bug body: - type: markdown @@ -53,18 +52,6 @@ body: validations: required: false - - type: dropdown - id: area - attributes: - label: Affected area - description: Best guess is fine. - multiple: true - options: - - TODO - - Other - validations: - required: false - - type: textarea id: evidence attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3ba13e0..27d0bbd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,8 @@ blank_issues_enabled: false +contact_links: + - name: Questions and ideas + url: https://github.com/OpenGameBuilder/opengamebuilder/discussions + about: Ask for help, discuss an idea, or explore a possible contribution in public. + - name: Private security report + url: https://github.com/OpenGameBuilder/opengamebuilder/security/advisories/new + about: Report a suspected vulnerability privately. Do not open a public issue. diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml index 9d9820e..c53e206 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yml +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -1,7 +1,6 @@ name: Enhancement description: Suggest an improvement to something that already exists. -labels: ["needs triage"] -type: enhancement +type: Enhancement body: - type: markdown @@ -11,7 +10,7 @@ body: Use this form for improvements to existing behavior, UI, UX, developer experience, documentation, tooling, or project workflow. - For entirely new capabilities, use the Feature Request form instead. If the idea is broad or exploratory, the Discord server may be a better fit. + For entirely new capabilities, use the Feature Request form instead. For broad or exploratory ideas, start a [GitHub Discussion](https://github.com/OpenGameBuilder/opengamebuilder/discussions). Please do not include private information, security vulnerability details, sensitive material, archived game dumps, original proprietary assets, or decompiled source code. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 11b7024..38f212d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,15 +1,14 @@ name: Feature Request -description: Suggest an improvement or new feature. -labels: ["needs triage"] -type: feature +description: Propose a new capability. +type: Feature body: - type: markdown attributes: value: | - Thanks for suggesting an improvement. + Thanks for suggesting a new capability. - Use this form for specific feature requests. If the idea is broad or exploratory, the Discord server may be a better fit. + Use this form for specific new capabilities. For improvements to existing behavior, use the Enhancement form. For broad or exploratory ideas, start a [GitHub Discussion](https://github.com/OpenGameBuilder/opengamebuilder/discussions). Please do not include private information, security vulnerability details, sensitive material, archived game dumps, original proprietary assets, or decompiled source code. @@ -27,7 +26,7 @@ body: id: summary attributes: label: What would you like to see? - description: Briefly describe the feature or improvement. + description: Briefly describe the new capability. validations: required: true diff --git a/AGENTS.md b/AGENTS.md index 2373712..124fc19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,11 @@ If `aspire` is newly installed, reopen the terminal if it is not on `PATH`. The setup guide is authoritative for prerequisites, certificate trust, endpoint checks, and editor debugging. +Before changing vendored skills or agent setup, read +[AI tooling maintenance](docs/setup/ai-tooling.md) for source pins, licenses, +update steps, and supported local-agent scope. Generic cloud/deployment skills +do not override this repository's workflows or authorization requirements. + ## Project map and boundaries - `src/OpenGameBuilder.Api.Contracts`: browser-compatible API DTOs; no server or diff --git a/AI_POLICY.md b/AI_POLICY.md index 7d0a2b8..612b160 100644 --- a/AI_POLICY.md +++ b/AI_POLICY.md @@ -1,307 +1,119 @@ # AI Policy -## Purpose - -OpenGameBuilder allows responsible use of AI-assisted development tools. - -AI tools can be useful for learning, exploring the codebase, drafting changes, writing tests, debugging, refactoring, documenting behavior, reviewing code, and reducing repetitive work. The project may include AI guidance files such as `AGENTS.md` and `.github/instructions/*.instructions.md` so contributors and AI tools can work with the repository more effectively. - -This policy is not meant to discourage AI use. It is meant to make sure AI-assisted work is understandable, maintainable, properly reviewed, and compatible with OpenGameBuilder's goals. - -Using AI is optional. Not using AI is also fine. +AI assistance is welcome and optional. It can help with learning, implementation, +tests, debugging, documentation, and review. Contributors remain responsible for +the work they submit. ## Core Rule -You are responsible for everything you submit. - -It does not matter whether a change was written by hand, suggested by autocomplete, drafted in chat, generated by an agent, or heavily revised with AI. If you submit it, you are expected to understand it, review it, test it where practical, and maintain it during review. - -Do not submit work you do not understand. - -## Good Uses of AI - -AI tools are welcome for tasks such as: - -- exploring the repository; -- explaining unfamiliar code; -- drafting implementation approaches; -- generating small or repetitive code; -- suggesting tests and edge cases; -- debugging failing tests; -- summarizing errors or logs; -- improving documentation; -- refactoring for readability; -- reviewing code for possible issues; -- drafting commit messages and pull request descriptions; -- helping new contributors learn the project. - -AI is most useful when paired with the project's documentation, tests, CI/CD checks, `.editorconfig`, GitHub Code Quality checks, `AGENTS.md`, Copilot instruction files, and maintainer review. - -Use the tools. Do not outsource your judgment to them. - -## Repository AI Instructions - -OpenGameBuilder may include repository-specific AI guidance such as: - -- `AGENTS.md`; -- `.github/copilot-instructions.md`; -- `.github/instructions/*.instructions.md`; -- prompt files; -- future model-specific or agent-specific guidance. - -Contributors using AI tools should follow these files when they apply. - -These files may describe project architecture, build commands, testing expectations, coding standards, documentation style, review rules, and area-specific guidance. - -If an AI tool suggests something that conflicts with project documentation, repository instructions, CI/CD, GitHub Code Quality, maintainer review, or this policy, the project documentation and maintainer review win. - -## Quality Standard - -AI-assisted work must meet the same quality standard as any other contribution. - -Submitted work should be: - -- correct; -- readable; -- maintainable; -- tested where appropriate; -- consistent with the existing architecture; -- consistent with project coding standards; -- free of unnecessary abstractions; -- free of invented APIs or fake behavior; -- safe from obvious security and privacy issues. +Understand, review, test where practical, and maintain every contribution during +review, regardless of how it was produced. Do not submit work you do not understand. +Be able to explain what changed, why the approach fits the project, what tests +cover it, and its risks, dependencies, and maintenance cost. -Do not submit code just because an AI tool generated it and it appears plausible. Plausible is not the same thing as correct. +AI suggestions are input for human judgment. Confidence, compilation, passing +tests, or an automated review do not establish correctness. Check suggestions in +context, reject incorrect advice, and ask maintainers when requirements are unclear. +AI review does not replace human review, security review, or maintainer judgment. -Maintainers may reject AI-assisted work that appears unreviewed, poorly understood, overcomplicated, insecure, untested, or inconsistent with the project's architecture. +## Repository Instructions and Validation -## AI Suggestions Are Not Authority +Follow applicable [AGENTS.md](AGENTS.md), Copilot and area-specific instructions, +[contribution guidance](CONTRIBUTING.md), and this policy. Repository requirements +and maintainer review take precedence over an AI tool's suggestions. -AI tools can make mistakes, misunderstand context, invent APIs, misread requirements, suggest unnecessary abstractions, produce insecure code, or confidently recommend changes that are incorrect or irrelevant. +AI-assisted work must meet the same standards as other contributions: correct, +readable, maintainable, consistent with the architecture and coding rules, and +free of unnecessary abstractions, invented APIs, and security or privacy defects. +Use the normal build, test, formatting, linting, CI/CD, and GitHub Code Quality +checks. See [development setup](docs/setup/development.md) and +[testing guidance](docs/quality/testing.md) for the current workflow. -This applies to all forms of AI assistance, including: - -- autocomplete suggestions; -- chat responses; -- generated tests; -- architecture suggestions; -- AI-generated documentation; -- AI code review comments; -- automated pull request review systems; -- coding agents. - -Treat AI output as input for human judgment, not as an authority. - -Do not assume a suggestion is correct just because: - -- it sounds confident; -- it references best practices; -- it compiles; -- it passes tests; -- it came from a well-known model or tool; -- it was generated automatically during review. - -Review suggestions critically and in context. - -Contributors are encouraged to question AI output, reject bad suggestions, simplify overengineered solutions, and ask maintainers for clarification when unsure. - -## Understanding and Review - -Before submitting AI-assisted work, review it as if another developer handed it to you and asked you to put your name on it. - -You should be able to answer: - -- What does this change do? -- Why is this approach appropriate? -- What behavior changed? -- What tests cover it? -- What could break? -- Does this fit the project architecture? -- Does this introduce new dependencies, security risks, privacy risks, licensing concerns, or maintenance burden? - -It is fine to use AI to help explain generated code. That can be a good learning step. The final responsibility is still yours. - -## Testing and Validation - -AI-assisted code should be validated with the same tools as any other code. - -Use the project's normal build, test, formatting, linting, and CI/CD workflows. When applicable, add or update tests. - -AI tools may help generate tests, but generated tests still need review. A bad test can be worse than no test if it locks in incorrect behavior or only tests the implementation instead of the requirement. - -For bug fixes, prefer tests that fail before the fix and pass afterward. - -For behavior changes, make sure the expected behavior is documented somewhere durable: an issue, pull request, test, architecture note, or project document. +Add or update tests when appropriate. Review generated tests against requirements, +not just the implementation; for bug fixes, prefer a test that fails before the +fix and passes afterward. Record expected behavior changes in an issue, PR, test, +architecture note, or project document. Applying AI review feedback carries the +same responsibility to verify it as any other change. ## Disclosure -Routine AI use does not need to be disclosed. - -You do not need to disclose normal autocomplete, small wording suggestions, grammar fixes, exploratory questions, commit-message help, or using AI to explain code to yourself. - -Mention AI assistance when it is materially relevant to review. This includes: - -- mostly AI-generated pull requests; -- agent-authored pull requests; -- AI-generated architecture proposals; -- AI-generated compatibility analysis; -- AI-generated tests that define important behavior; -- cases where reviewers should know AI played a major role. - -A simple note is enough: +Routine autocomplete, wording or grammar fixes, exploratory questions, +commit-message help, and private code explanations need no disclosure. -```text -AI assistance was used to draft part of this implementation. I reviewed and tested the changes. -``` - -or: - -```text -This PR was initially generated by an AI coding agent. I reviewed the code, adjusted it, and verified the tests. -``` - -Disclosure is not a confession booth. It helps reviewers understand how the work was produced and where to look closely. +Mention AI assistance when materially relevant to review, including mostly +AI-generated or agent-authored PRs, architecture proposals, compatibility +analysis, and generated tests that define important behavior. A short note +describing the assistance and the review and validation actually performed is +enough. Do not claim checks that were not run. ## Autonomous Agents and Bots -AI coding agents are allowed, but they must remain accountable to a human contributor. +Agents must remain accountable to a human contributor. Do not run unattended +agents that mass-create issues, PRs, comments, reviews, or discussions. -Do not run unattended agents that mass-create issues, pull requests, comments, reviews, or discussions. +The contributor who starts an agent must review its PR before requesting +maintainer review. Remove mistakes, irrelevant or noisy changes, broken +formatting, and false explanations first. -Agent-authored pull requests should be reviewed by the contributor who started the agent before asking maintainers to review them. Clean up obvious mistakes, irrelevant output, broken formatting, fake explanations, noisy changes, and anything that does not belong in the pull request. - -Agents should not be given broad write access, secrets, production credentials, private reports, or sensitive data unless maintainers have explicitly approved that setup. - -Maintainers may limit, close, or block agent-generated activity that creates review burden, security risk, moderation burden, or low-quality noise. +Do not give agents broad write access, secrets, production credentials, private +reports, or sensitive data without explicit maintainer approval of that setup. +Maintainers may limit, close, or block agent activity that creates security risk, +review or moderation burden, or low-quality noise. ## Decompiled Source and Original Client Material -OpenGameBuilder is a reimplementation and extension of MyGameBuilder, not a copy of the original Flash client. - -There is decompiled source material from the original MyGameBuilder Flash client available outside this repository. That material must be handled carefully. - -Do not use AI tools to copy, translate, port, rewrite, adapt, refactor, or mechanically convert decompiled source code from the original client into OpenGameBuilder code. - -Do not paste decompiled source code into AI tools and ask them to: - -- convert it to C#, JavaScript, TypeScript, or another language; -- explain it for the purpose of recreating the same implementation; -- produce equivalent classes, methods, names, control flow, or architecture; -- rewrite it in a cleaner style; -- generate tests, comments, documentation, or pseudocode derived directly from that source; -- "modernize" it into OpenGameBuilder code. - -Using AI as an intermediate step does not make copied or source-derived work acceptable. - -## Clean Reimplementation - -Compatibility work should focus on observed behavior, not copied implementation. - -Acceptable inputs for AI-assisted compatibility work include: - -- descriptions of original behavior; -- screenshots or recordings that are safe to share; -- public user-facing behavior; -- independently written notes; -- expected input/output examples; -- tests that describe behavior without copying source code; -- safe sample data created for the project; -- community knowledge about how MyGameBuilder worked. - -Unacceptable inputs include: - -- decompiled source code; -- proprietary source code; -- private original site data; -- original assets not approved for use; -- copied comments, names, structures, or implementation details from proprietary material. - -When in doubt, describe what the original client did, not how its source code did it. - -For example, this is okay: - -```text -The original editor let users attach movement behavior to a sprite and configure speed through -the UI. Help design a modern implementation that supports equivalent behavior. -``` - -This is not okay: - -```text -Here is decompiled ActionScript from the original client. Convert it to C# for OpenGameBuilder. -``` - -If a compatibility detail seems important but can only be justified by reference to decompiled source, ask maintainers before implementing it. +OpenGameBuilder independently reimplements and extends MyGameBuilder. Decompiled +original-client source exists outside this repository; it is not implementation +input. + +Do not use AI to copy, translate, port, rewrite, adapt, refactor, or mechanically +convert decompiled or proprietary source into OpenGameBuilder work. Do not supply +that source to AI tools to explain it for recreating its implementation or to +derive equivalent classes, methods, names, control flow, or architecture. The ban +also covers source-derived tests, comments, documentation, and pseudocode. +Intermediate AI output does not make copied or source-derived work acceptable. + +Use independently observed behavior for compatibility work: public user-facing +behavior, safe screenshots or recordings, independently written notes, expected +input/output examples, behavior tests, project-created safe fixtures, and +community knowledge. Describe what the original client did rather than how its +source implemented it. + +Do not use private original-site data, unapproved original assets, or copied +comments, names, structures, or implementation details from proprietary material +as compatibility inputs. If an important behavior can only be justified by +decompiled source, ask maintainers before implementing it; this does not permit +using AI to derive an implementation from that source. ## Privacy and Sensitive Information -Do not paste private, sensitive, identifying, confidential, or non-public project information into AI tools unless maintainers have explicitly approved that use. - -This includes: - -- passwords; -- access tokens; -- API keys; -- private user information; -- private reports; -- sensitive historical material; -- unpublished security details; -- private maintainer discussions; -- non-public infrastructure details; -- anything that should not appear in a public issue or pull request. - -When in doubt, do not send it to an AI tool. +Do not put private, sensitive, identifying, confidential, or non-public project +information into AI tools without explicit maintainer approval of that use. +This includes passwords, tokens, API keys, user information, private reports, +sensitive historical material, unpublished security details, maintainer +discussions, infrastructure details, and anything unsuitable for a public issue +or PR. When in doubt, do not send it. ## Licensing and Attribution -AI assistance does not remove licensing or attribution responsibilities. - -Do not submit code, assets, documentation, or text copied from another project unless the license allows it and proper attribution is included. - -If an AI tool produces output that appears copied from another source, do not submit it unless you can verify that the source is compatible with OpenGameBuilder's license and attribution requirements. - -Do not use AI tools to hide, obscure, or launder the origin of copied material. - -## Documentation and AI Output - -AI tools may be used to draft documentation, but documentation should still be accurate, specific, and useful. - -Remove generic filler, fake certainty, fake citations, invented commands, invented architecture, and anything that describes a project other than the one that actually exists. - -Documentation should describe OpenGameBuilder, not what an AI tool guessed OpenGameBuilder probably looks like. +AI does not remove licensing, attribution, or source-origin responsibilities. +Submit copied code, assets, documentation, or text only when its license permits +the use and proper attribution is included. If generated output appears copied, +verify the source is compatible with the project's license and attribution +requirements before submitting it. Do not use AI to hide or launder its origin. -## Issues and Bug Reports +## Documentation, Issues, and Evidence -AI tools may help investigate bugs, summarize errors, or draft issue reports. - -Do not submit speculative AI-generated bug reports without checking them yourself. - -Useful reports include reproduction steps, expected behavior, actual behavior, relevant logs or screenshots, and enough context for maintainers to act on them. - -Low-effort AI-generated issues may be closed. - -## AI-Assisted Review - -AI tools may be used to review code, summarize pull requests, find edge cases, or suggest improvements. - -AI review is advisory. It does not replace human review, project tests, CI/CD, GitHub Code Quality, security review, or maintainer judgment. - -If you apply AI review feedback, you are responsible for deciding whether the feedback is correct. +Documentation must describe the actual project accurately and usefully. Remove +filler, invented commands or architecture, fake citations, and unsupported +certainty. Verify AI-generated bug reports yourself before submitting them; +include reproduction steps, expected and actual behavior, relevant logs or +screenshots, and enough context to act. AI speculation is not a verified finding. ## Enforcement -Maintainers may ask contributors to explain, revise, test, rewrite, or remove AI-assisted work. - -Maintainers may reject or close contributions that: - -- appear to be unreviewed AI output; -- are not understood by the contributor submitting them; -- contain hallucinated APIs, fake facts, or false claims; -- create unnecessary maintenance burden; -- introduce security, privacy, licensing, or attribution concerns; -- are derived from decompiled or proprietary source code; -- use AI tools to translate or adapt decompiled source code; -- generate excessive bot or agent noise; -- repeatedly ignore this policy. - -The goal is not to police tool usage. The goal is to keep OpenGameBuilder understandable, maintainable, legally safer, trustworthy, and worth contributing to. +Maintainers may require explanations, revisions, tests, rewrites, or removal. +They may reject or close work that is unreviewed or not understood by its +contributor, contains invented facts or behavior, creates unnecessary maintenance +burden, violates security, privacy, licensing, attribution, or source-material +rules, generates excessive agent noise, or repeatedly ignores this policy. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c74352..3230069 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,13 +28,19 @@ Useful contributions include: - issue triage; - community support. -If you are unsure where to start, look for issues labeled `good first issue`, `help wanted`, `documentation`, `bug`, or `compatibility`. +If you are unsure where to start, look for issues labeled `good first issue`, +`help wanted`, or `documentation`, or issues with the `Bug` type. The public +[OpenGameBuilder Roadmap](https://github.com/orgs/OpenGameBuilder/projects/3) +tracks work, including the `Triage` status; triage is not an issue label. ## Before You Start For small fixes, documentation improvements, typo fixes, straightforward bugs, and focused cleanup, feel free to open a pull request. -For larger changes, please open an issue or discussion first. This is especially important for changes involving: +For larger changes, please open an issue or +[GitHub Discussion](https://github.com/OpenGameBuilder/opengamebuilder/discussions) +first. Use Discussions for exploratory questions and ideas; private Discord +access is not required to contribute. This is especially important for changes involving: - major architecture; - game runtime behavior; @@ -68,7 +74,12 @@ Before opening a pull request, please make sure the project builds and tests pas ## Issues -Issues are useful for bug reports, feature requests, compatibility problems, documentation gaps, and project discussion. +Use the [issue forms](https://github.com/OpenGameBuilder/opengamebuilder/issues/new/choose) +for concrete work: Bug Report for broken behavior, Compatibility Issue for +observed differences from MyGameBuilder (also tracked as `Bug`), Enhancement for +improving an existing capability, and Feature Request for a new capability. +Use [Discussions](https://github.com/OpenGameBuilder/opengamebuilder/discussions) +for questions and proposals that are still exploratory. When reporting a bug, please include: @@ -117,6 +128,11 @@ Significant decisions should leave a written record where practical, such as an OpenGameBuilder includes preservation work related to MyGameBuilder games and historical material. +Before adding games, assets, submissions, or test fixtures, follow the short +[material-intake and creator-request checklist](docs/community/stewardship.md#material-intake-and-creator-requests). +Implementation tests use independently created fixtures. The application's +Apache-2.0 license does not grant rights to original MyGameBuilder material. + Please handle archival material carefully. The existence of archived material does not automatically mean every item should be public, searchable, or restored without context. Do not post private, sensitive, identifying, or personal information from archived material in public issues, pull requests, comments, screenshots, or documentation. @@ -152,13 +168,18 @@ See [`AI_POLICY.md`](AI_POLICY.md) for the full policy. Please do not report security vulnerabilities in public issues or discussions. -Security issues, privacy concerns, Code of Conduct reports, and sensitive archival concerns should be reported privately using the process described in [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) or any dedicated security reporting process provided by the project. +Report security vulnerabilities through [the private security reporting process](SECURITY.md). +For Code of Conduct reports, privacy concerns, or sensitive archival concerns, +use the private contact in the organization-wide +[Code of Conduct](https://github.com/OpenGameBuilder/.github/blob/main/CODE_OF_CONDUCT.md#reporting-an-issue). ## Community Standards -All contributors are expected to follow the project’s [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). +All contributors are expected to follow the organization-wide +[Code of Conduct](https://github.com/OpenGameBuilder/.github/blob/main/CODE_OF_CONDUCT.md). -Project governance, roles, and decision-making authority are described in [`GOVERNANCE.md`](GOVERNANCE.md). +Project governance, roles, and decision-making authority are described in the +organization-wide [governance document](https://github.com/OpenGameBuilder/.github/blob/main/GOVERNANCE.md). ## Licensing diff --git a/README.md b/README.md index 49242bc..aee7a29 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,83 @@ # OpenGameBuilder -Open-source community reimplementation of the original 2007-2017 Flash site mygamebuilder.com! +OpenGameBuilder is an independent, community-led open-source reimplementation +and extension of MyGameBuilder, the original Flash game-making site. It is not +an official continuation of MyGameBuilder. -[![🧱 CI](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/ci.yml) +## What works today -[![🛰️ CD Staging](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/cd-staging.yml/badge.svg)](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/cd-staging.yml) +The project is in early development. This repository currently provides a +.NET API and a standalone Blazor WebAssembly frontend. The home page fetches +application information from `/api/about` and displays the name and version; +the API also exposes `/api/alive` for liveness checks. Aspire launches the two +applications locally, and automated tests cover API and client behavior. -[![🚀 CD Production](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/cd-production.yml/badge.svg)](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/cd-production.yml) +There is no playable game runtime, game importer, or editor yet. APIs and +architecture may change before v1; original-game compatibility is not established. -**under construction**. +The next milestone is to make the foundation ready for engine development. +The first engine milestone's compatibility goal, independent fixture, and +acceptance criteria are still to be chosen in +[section 9 of the foundation checklist](docs/foundation-checklist.md#9-record-the-engine-boundary-and-first-milestone). +Full original-site recreation and broad game compatibility are outside this +initial foundation work. + +## Start contributing + +1. Follow the [Windows development setup](docs/setup/development.md#command-line-workflow-start-here) + to install the supported tools, build, test, and run the application. It also + covers Visual Studio and VS Code. Local development does not require Docker, + a database, production credentials, or Discord access. +2. Read the [contribution guide](CONTRIBUTING.md) and + [testing guidance](docs/quality/testing.md) before changing code. If you use AI + tools, follow the [AI policy](AI_POLICY.md). +3. Pick a bounded task from the [foundation roadmap](docs/foundation-checklist.md) + or [open issues](https://github.com/OpenGameBuilder/opengamebuilder/issues). + Check existing issues before starting, and propose larger changes before + implementation as described in the contribution guide. + +The [documentation index](docs/README.md) maps the application, operating guides, +and browser/accessibility expectations. + +Useful starting tasks include following the setup guide in your editor and +reporting a reproducible failure, improving an unclear setup instruction, or +adding a focused regression test for an API/client bug. Section 3 of the +[checklist](docs/foundation-checklist.md#3-repair-the-documented-development-workflow) +tracks the remaining fresh-checkout editor verification. Engine contributors +can help define the small, independently testable milestone in section 9. + +Use the [issue forms](https://github.com/OpenGameBuilder/opengamebuilder/issues/new/choose) +for reproducible bugs and concrete proposals. See [support](SUPPORT.md) for help +and [security reporting](SECURITY.md) for private vulnerability reports. + +## Implementation, archive, and original site + +- **This repository** contains the new OpenGameBuilder implementation, its tests, + and contributor documentation. +- **The [MyGameBuilder archive](https://github.com/OpenGameBuilder/mygamebuilder-archive)** + is a separate preservation repository for historical MyGameBuilder material. + You do not need it to build or run this application. +- **Original MyGameBuilder** is the historical Flash site whose behavior informs + the community's compatibility goals. This project does not claim to be the + original service or its official successor. + +Compatibility work should use observed behavior, independently written notes, +and safe project-created fixtures. Do not copy or translate decompiled original +client source into this implementation; see the +[source-material policy](AI_POLICY.md#decompiled-source-and-original-client-material). +Do not post private archived data or original proprietary assets in public issues. + +## License + +This implementation is licensed under [Apache License 2.0](LICENSE). That license +does not grant rights to original MyGameBuilder assets or archived material. + +## Build and deployment + +For deployment operations, see [hosting setup](docs/setup/hosting.md). Aspire is +the local launcher; production hosting uses Docker Compose. These workflow +badges describe automation status, not game compatibility or feature completeness. + +[![CI](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/ci.yml) +[![CD Staging](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/cd-staging.yml/badge.svg)](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/cd-staging.yml) +[![CD Production](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/cd-production.yml/badge.svg)](https://github.com/OpenGameBuilder/opengamebuilder/actions/workflows/cd-production.yml) diff --git a/SUPPORT.md b/SUPPORT.md index 5022247..992d640 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -6,7 +6,7 @@ Please choose the most appropriate channel so maintainers can respond without tu ## Questions and Discussion -The official Discord server (if you have access) for: +Use public [GitHub Discussions](https://github.com/OpenGameBuilder/opengamebuilder/discussions) for: - general questions; - project ideas; @@ -17,11 +17,15 @@ The official Discord server (if you have access) for: - "is this worth doing?" conversations; - help deciding where a contribution belongs. -Otherwise, open a GitHub issue only when the question is directly tied to actionable project work. +Start with [Q&A](https://github.com/OpenGameBuilder/opengamebuilder/discussions/categories/q-a) +for help or [Ideas](https://github.com/OpenGameBuilder/opengamebuilder/discussions/categories/ideas) +for exploratory proposals. The reunion Discord remains private; access is not +required to ask questions or contribute. ## Bugs -Use GitHub Issues for reproducible bugs. +Use the [Bug Report form](https://github.com/OpenGameBuilder/opengamebuilder/issues/new?template=bug_report.yml) +for reproducible bugs. A good bug report includes: @@ -35,11 +39,11 @@ Do not include passwords, tokens, private data, sensitive archival material, or ## Feature Requests -Use GitHub Issues or the Discord server for feature requests. - -Use an issue when the request is specific and actionable. - -Use the Discord server when the idea is still exploratory, broad, or likely to need design conversation before it becomes work. +Use the [issue chooser](https://github.com/OpenGameBuilder/opengamebuilder/issues/new/choose) +when the request is specific and actionable: Enhancement is for improvements to +existing capabilities, and Feature Request is for new capabilities. Use +[Discussions](https://github.com/OpenGameBuilder/opengamebuilder/discussions) +when the idea needs exploration first. ## Development Setup Help @@ -63,7 +67,10 @@ Contribution guidelines are in: Project governance is described in: -[`GOVERNANCE.md`](GOVERNANCE.md) +the organization-wide [governance document](https://github.com/OpenGameBuilder/.github/blob/main/GOVERNANCE.md). + +[Practical stewardship](docs/community/stewardship.md) records current operating +responsibilities and the unfilled backup and independent-reporting roles. AI-assisted contribution expectations are described in: @@ -81,7 +88,7 @@ Follow the private reporting process in: Code of Conduct reports and other sensitive community concerns should be reported privately using the process described in: -[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) +the organization-wide [Code of Conduct](https://github.com/OpenGameBuilder/.github/blob/main/CODE_OF_CONDUCT.md#reporting-an-issue). Do not post private, sensitive, identifying, or personal information publicly. @@ -89,7 +96,10 @@ Do not post private, sensitive, identifying, or personal information publicly. OpenGameBuilder is connected to the history of MyGameBuilder.com, but the completed archive is not maintained in this repository. -Use public issues or the Discord server for: +Use [Discussions](https://github.com/OpenGameBuilder/opengamebuilder/discussions) +for questions and historical context, or the +[issue forms](https://github.com/OpenGameBuilder/opengamebuilder/issues/new/choose) +for concrete work such as: - safe compatibility notes; - observed behavior differences; @@ -99,7 +109,12 @@ Use public issues or the Discord server for: Do not post archived game dumps, private user data, sensitive material, or original proprietary assets in this repository unless maintainers have explicitly requested them. -Sensitive archive-related concerns should be reported privately. +Sensitive archive-related concerns should go to the private contact in the +[Code of Conduct](https://github.com/OpenGameBuilder/.github/blob/main/CODE_OF_CONDUCT.md#reporting-an-issue). +Use that route for creator, privacy, or removal requests; identify the material +and requested action without posting sensitive evidence publicly. Changes to the +separate archive belong with its maintainers. There is currently no designated +independent contact for concerns involving the primary contact. ## Maintainer Response Expectations diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6c67d20 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,58 @@ +# Documentation + +Start with [development setup](setup/development.md) to build, test, and run the +application. The [foundation checklist](foundation-checklist.md) records current +work, acceptance evidence, and deferred features. There is no game runtime or +editor yet; section 9 tracks the first engine milestone decision. + +## Working on the application + +| Task | Read | +| --- | --- | +| Install tools, run locally, or debug | [Development setup](setup/development.md) | +| Choose a task or propose a change | [Contributing](../CONTRIBUTING.md) and [foundation checklist](foundation-checklist.md) | +| Understand the current HTTP surface and health checks | [API](backend/api.md) | +| Run tests and understand their limits | [Testing](quality/testing.md) | +| Maintain agent instructions and vendored skills | [AI tooling maintenance](setup/ai-tooling.md) and [AI policy](../AI_POLICY.md) | +| Check browser and accessibility expectations | [Browser support](frontend/browser-support.md) | +| Host, deploy, or recover a release | [Hosting](setup/hosting.md), [GitHub setup](setup/github.md), and [SSH host-key verification](setup/deployment-host-key.md) | +| Prepare a release or patch | [Release process](release/README.md) and [versioning](release/versioning.md) | +| Ask a question or report privately | [Support](../SUPPORT.md) and [security](../SECURITY.md) | +| Check responsibilities or bring in historical material | [Practical stewardship](community/stewardship.md) | + +## Repository map + +- [API contracts](../src/OpenGameBuilder.Api.Contracts/) contain browser-compatible + DTOs. The [API client](../src/OpenGameBuilder.Api.Client/) depends on these + contracts, not on the server host. +- The [API host](../src/OpenGameBuilder.Api/) serves HTTP endpoints and uses + [service defaults](../src/OpenGameBuilder.ServiceDefaults/) for health checks, + resilience, and telemetry. +- The [web client](../src/OpenGameBuilder.Web.Client/) is standalone Blazor + WebAssembly and calls the typed API client. +- [AppHost](../src/OpenGameBuilder.AppHost/) launches the API and frontend locally. + [Compose deployment](../deploy/) is separate from local Aspire orchestration. +- [Tests](../tests/) cover the API/client and deployment tooling. Engine code, + when introduced, should remain testable without Blazor, HTTP, or storage. + +Formatting and compiler rules live in [`.editorconfig`](../.editorconfig) and +[`Directory.Build.props`](../Directory.Build.props); the contribution and testing +guides describe the current workflow. Database, blob-storage, and performance +design documents should accompany real features rather than empty placeholders. + +## MyGameBuilder history and source boundaries + +The original material is separate from this implementation. Start with the +[archive pointer](mygamebuilder/data-archive.md) and +[format documentation pointer](mygamebuilder/data-formats.md). Follow the +[AI/source-material policy](../AI_POLICY.md#decompiled-source-and-original-client-material) +for independent compatibility work. [Forum preservation notes](community/forum-archive.md) +and [reunion Discord information](community/discord.md) provide historical +context; private chat is not required for contributing. + +## General learning resources + +General framework and tooling references belong in the organization's +[shared learning resources](https://github.com/OpenGameBuilder/.github/tree/6aa0795e068256f672801a75b656473ec82883c1/resources). +Keep application-specific instructions here and link to shared material rather +than maintaining another reference library. diff --git a/docs/backend/api.md b/docs/backend/api.md index e69de29..2287fe4 100644 --- a/docs/backend/api.md +++ b/docs/backend/api.md @@ -0,0 +1,33 @@ +# Current API + +The ASP.NET Core host currently exposes application information and health +checks. There are no game, editor, account, database, or blob-storage endpoints. +Use [development setup](../setup/development.md) to run the API locally over +HTTPS; the default API origin is `https://localhost:7000`. + +| Endpoint | Availability | Behavior | +| --- | --- | --- | +| `GET /api/about` | All environments | Application name, informational version, API environment, and source revision | +| `GET /api/alive` | All environments | Plain-text liveness status from checks tagged `live` | +| `/health` | Development only, on the API origin | Readiness result from all registered health checks | +| `/alive` | Development only, on the API origin | Liveness result from checks tagged `live` | +| `/openapi/v1.json` and `/scalar` | Development only | OpenAPI description and interactive API documentation | + +`/api/about` uses the [AboutResponse contract](../../src/OpenGameBuilder.Api.Contracts/About/AboutResponse.cs). +Its JSON properties are `applicationName`, `version`, `apiEnvironmentName`, and +`sourceRevision`. The source revision comes from the deployment's `SOURCE_SHA` +and may be null in a local run. The +[typed client](../../src/OpenGameBuilder.Api.Client/About/IAboutApiClient.cs) +is the frontend entry point for this request. + +Health checks are implemented in +[ServiceDefaults](../../src/OpenGameBuilder.ServiceDefaults/Extensions.cs). +The current `self` check reports process liveness. A passing `/api/alive` does not +prove frontend startup, a frontend-to-API request, or future dependency readiness. +The [integration tests](../../tests/OpenGameBuilder.Api.Tests/HealthEndpointTests.cs) +cover environment availability and health-check failure behavior. + +On a deployed host, Caddy proxies `/api/*` to the API and serves the web client. +Caddy's public `/health` is an edge response, distinct from the API's +Development-only readiness endpoint. See [hosting](../setup/hosting.md) for +routing and [testing](../quality/testing.md) for the deployment browser smoke. diff --git a/docs/backend/blob-storage.md b/docs/backend/blob-storage.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/backend/database.md b/docs/backend/database.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/community/stewardship.md b/docs/community/stewardship.md new file mode 100644 index 0000000..0fdbfaa --- /dev/null +++ b/docs/community/stewardship.md @@ -0,0 +1,57 @@ +# Practical stewardship + +The [organization governance](https://github.com/OpenGameBuilder/.github/blob/main/GOVERNANCE.md) +remains authoritative. This page records responsibilities and material-handling +rules for the current small project; it does not appoint additional maintainers. + +## Responsibilities and continuity + +GitHub inspection on 2026-09-22 UTC confirmed `ostomachion` as the sole organization +owner and the only application collaborator with write/admin access. + +| Area | Current responsibility and handoff | +| --- | --- | +| Project decisions, repository access, and releases | `ostomachion`; see [governance](https://github.com/OpenGameBuilder/.github/blob/main/GOVERNANCE.md) and [GitHub setup](../setup/github.md#deployment-authority-and-recovery). | +| Hosting and application recovery | `ostomachion` is the documented release/recovery operator; use [hosting recovery](../setup/hosting.md#application-activation-and-rollback). A working rollback procedure does not establish backup operator access. | +| Domains, DNS, and renewals | No separate operator or recovery handoff is recorded. `ostomachion` owns arranging and privately documenting that handoff before delegating operations; registrar/account access has not been verified here. | +| Historical archive | Managed in the separate [archive repository](https://github.com/OpenGameBuilder/mygamebuilder-archive). Application permissions do not confer ownership of archived material. | + +There is no agreed backup maintainer or independent private reporting contact +recorded. Both remain deferred, owned by `ostomachion`: before wider participation +or an operational handoff, agree a role with a willing trusted person, establish +the minimum required access, and rehearse recovery. Record their consent, scope, +and public contact route; keep account recovery details private. + +Do not add `CODEOWNERS` until real area owners have accepted responsibility and +can satisfy the review requirement. Repository read access alone is not a +maintainer, recovery, or reporting role. + +[Private vulnerability reporting](../../SECURITY.md) remains enabled. General +sensitive reports use the contact in the +[Code of Conduct](https://github.com/OpenGameBuilder/.github/blob/main/CODE_OF_CONDUCT.md#reporting-an-issue). +Neither route currently provides an independent escalation path for a concern +involving the primary contact. Do not describe that gap as resolved or publish +sensitive details as a workaround. + +## Material intake and creator requests + +Before adding a historical game, submitted asset, or fixture, record a short +provenance note alongside it or in the reviewing PR: + +- Source, creator, and where and when it was obtained. +- License or explicit permission, the uses it permits, and required attribution. + Technical access or presence in an archive is not permission to import or publish. +- A privacy review covering personal information and sensitive historical content. +- Any creator restrictions or requests, who reviewed them, and the agreed action. + +If permission or privacy is unclear, leave the material out until reviewed. +The application's Apache-2.0 license does not grant rights to original games, +assets, or other submissions. Implementation tests use independently created +fixtures with recorded provenance; follow the [source-material policy](../../AI_POLICY.md#decompiled-source-and-original-client-material). + +Creator, privacy, and removal requests go through [Support](../../SUPPORT.md#mygamebuilder-archive-and-compatibility-questions). +Pause disputed use while the responsible maintainer reviews the request. For +copies in this application, record what was removed or replaced and any remaining +distribution limits; avoid copying sensitive details into public records. Refer +archive changes to the archive maintainers. Do not promise that deleting a current +file also erases Git history, existing downloads, or third-party copies. diff --git a/docs/foundation-checklist.md b/docs/foundation-checklist.md index 65157e2..a60b980 100644 --- a/docs/foundation-checklist.md +++ b/docs/foundation-checklist.md @@ -353,30 +353,57 @@ application deployments do not apply edge-image changes. ### 16. Replace the placeholder README with a contributor front door -- [ ] Describe what works today, the first milestone, non-goals, and project status. -- [ ] Link working setup, contribution, testing, support, roadmap, and license +- [x] Describe what works today, the first milestone, non-goals, and project status. +- [x] Link working setup, contribution, testing, support, roadmap, and license information before emphasizing deployment badges. -- [ ] Explain the distinction between this implementation, the archive repository, +- [x] Explain the distinction between this implementation, the archive repository, and the original MyGameBuilder; do not imply official continuation. **Acceptance:** an unfamiliar contributor can identify a useful task and reach the run instructions from the README without asking a maintainer. +**Verified locally (2026-09-21):** the README now describes the API/frontend +foundation and its current limitations, links directly to the run instructions, +and gives concrete setup, documentation, and regression-test contribution paths. +It points to section 9 for the still-unselected first engine milestone rather +than claiming a compatibility target has been accepted. Setup, contribution, +testing, support, roadmap, and license links precede workflow badges. Local link +targets and heading anchors were checked, current behavior was compared with +the source, and the public archive repository and enabled issue tracker were +confirmed through GitHub. This documentation review does not close section 3's +fresh-checkout editor verification or section 9's milestone decisions. + ### 17. Make public support and issue intake usable -- [ ] Make GitHub Discussions the discoverable default for exploratory questions. +- [x] Make GitHub Discussions the discoverable default for exploratory questions. Keep the reunion Discord private if desired, but not a prerequisite for contributing. -- [ ] Link directly to the authoritative organization-wide Code of Conduct and +- [x] Link directly to the authoritative organization-wide Code of Conduct and governance documents from `CONTRIBUTING.md` and `SUPPORT.md`. -- [ ] Add question and private-security-reporting links to the issue chooser. -- [ ] Create or replace the missing `needs triage` label used by templates and - Dependabot. Remove the compatibility form's TODO option. -- [ ] Simplify overlapping enhancement/feature forms if they do not help triage. +- [x] Add question and private-security-reporting links to the issue chooser. +- [x] Use the existing project `Triage` status; remove stale `needs triage` + label references from issue forms. Dependabot is already corrected. Remove + the compatibility form's TODO option. +- [x] Simplify overlapping enhancement/feature forms if they do not help triage. Make the issue labels/types and contributor guidance agree. **Acceptance:** a newcomer can ask a question, report a bug, propose a change, and find private reporting instructions without access to private chat. +**Verified locally and against GitHub (2026-09-21):** Discussions (including Q&A +and Ideas), private vulnerability reporting, and the public Roadmap's `Triage` +status already exist. No settings or labels needed creating, and Dependabot +already omits the stale label. The four issue forms now omit it too. Bug, +Feature, and Enhancement match enabled organization issue types; the nonexistent +compatibility type is replaced with Bug while keeping its behavior/evidence form. +The unused TODO dropdown is removed. Enhancement and Feature remain separate +because both types exist and the forms distinguish existing and new capabilities. +Support, contribution guidance, and issue-chooser links now lead to public +questions, concrete reports/proposals, and private security reporting. Broken +local policy links now point to the verified organization-wide documents. +YAML, configured types, and documentation links were checked locally; the hosted +chooser will reflect these changes after merge. No test issues or reports were +submitted, and project automation was not changed. + ### 18. Create a small, genuinely actionable contributor backlog - [ ] Prepare a few `good first issue` and `help wanted` tasks with acceptance @@ -391,53 +418,103 @@ database layer, deployment system, or entire engine. ### 19. Consolidate documentation and define browser expectations -- [ ] Remove empty Markdown placeholders or turn the intended work into issues. +- [x] Remove empty Markdown placeholders or turn the intended work into issues. Keep only useful setup, architecture, testing, hosting, and compatibility documents. -- [ ] Add a concise documentation index and repair broken local links. -- [ ] Correct stale claims, including the statement that health checks are absent. +- [x] Add a concise documentation index and repair broken local links. +- [x] Correct stale claims, including the statement that health checks are absent. Move general learning resources to the shared location planned by the project, retaining useful links rather than duplicating a reference library. -- [ ] Fill `docs\frontend\browser-support.md` with a tested support policy. +- [x] Fill `docs\frontend\browser-support.md` with a tested support policy. Explain that `.browserslistrc` alone neither implements nor verifies compatibility. -- [ ] Include keyboard operation, focus, accessible loading/errors, and a concrete +- [x] Include keyboard operation, focus, accessible loading/errors, and a concrete supported-browser smoke matrix for the editor as it develops. **Acceptance:** referenced documents contain real instructions; local links resolve; the claimed browser/accessibility baseline has recorded checks. +**Verified (2026-09-22 UTC):** removed 23 empty placeholders and filled the API +and browser guides. The [documentation index](README.md) links current operating +instructions, the repository map, and compatibility boundaries. Five substantive +learning-resource pages moved to the organization's `.github` repository in +[draft PR #1](https://github.com/OpenGameBuilder/.github/pull/1), retaining all 211 +original external link targets and replacing broken local references. The index +links the published shared commit so it works before merge; issue +[#56](https://github.com/OpenGameBuilder/opengamebuilder/issues/56) remains open +pending the cross-repository review and merge. The API guide documents existing +health checks, and testing guidance now distinguishes the existing Chromium +deployment smoke from section 6's future component and failure-state coverage. + +The browser guide records the successful Chromium smoke from staging run +[35681727268](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35681727268) +and source-inspected accessibility gaps. Other browsers, keyboard, focus, screen +readers, and physical mobile devices remain explicitly unverified; their concrete +acceptance matrix accompanies the first functional frontend feature in section 6. +This is not an editor or accessibility-conformance acceptance. Markdown paths, +heading anchors, nonempty documents, preserved resource links, and whitespace +were checked. No new services or browser sessions were started, and section 18 +was left unchanged. + ### 20. Finish AI tooling maintenance and simplify policy -- [ ] Record vendored skills' upstream source/revision, applicable licenses and +- [x] Record vendored skills' upstream source/revision, applicable licenses and attribution, update/regeneration procedure, and local-edit policy. -- [ ] Trim unused skill coverage where useful, or explicitly distinguish generic +- [x] Trim unused skill coverage where useful, or explicitly distinguish generic cloud/deployment capabilities from approved repository workflows. -- [ ] Shorten repetitive sections of `AI_POLICY.md` without weakening human +- [x] Shorten repetitive sections of `AI_POLICY.md` without weakening human accountability, disclosure, privacy, or proprietary-material restrictions. -- [ ] If using cloud coding agents, add a minimal reproducible setup workflow and +- [x] If using cloud coding agents, add a minimal reproducible setup workflow and validate it on their actual runner. Keep production secrets out of that environment. Otherwise record cloud-agent setup as not applicable. **Acceptance:** local and any supported cloud agents use the same documented checks. A maintainer can update the skills deliberately, with provenance preserved. +**Verified (2026-09-22 UTC):** [AI tooling maintenance](setup/ai-tooling.md) +records matching immutable source revisions for all 37 vendored files, the +embedded Aspire bundle checksum, dotnet-inspect attribution, complete MIT notices, +and reproduction/update and local-patch procedures. A clean upstream checkout +matched all six Aspire skill trees; the extracted dotnet-inspect source matched +its recorded SHA-256. The vendored files themselves are unchanged. Generic cloud +and deployment coverage is explicitly separate from this repository's approved +Compose workflow and authorization boundaries. `AI_POLICY.md` was reduced from +307 to 119 lines while preserving accountability, disclosure, privacy, licensing, +and proprietary-source restrictions; its linked source-material heading remains. +The maintainer confirmed local agents only, so cloud setup and runner validation +are not applicable. Local links, license contents, and whitespace checks passed; +no services, cloud environment, or new application tests were needed. + ### 21. Establish practical stewardship and project continuity - [ ] Keep lightweight governance, but identify a backup maintainer and document repository, hosting, domain, release, and recovery responsibilities. - [ ] Provide an alternate private reporting route for concerns involving the primary contact. Confirm private vulnerability reporting remains enabled. -- [ ] Add `CODEOWNERS` when real area owners exist; do not create fictional ownership +- [x] Add `CODEOWNERS` when real area owners exist; do not create fictional ownership or an approval requirement nobody can satisfy. -- [ ] Document rights/provenance checks for historical games, submissions, assets, +- [x] Document rights/provenance checks for historical games, submissions, assets, and fixtures: source, permitted use, attribution, privacy review, creator requests, and removal handling. Do not imply Apache-2.0 grants rights to original material. -- [ ] Keep archive ownership separate and use independently created fixtures for +- [x] Keep archive ownership separate and use independently created fixtures for implementation tests. **Acceptance:** contributors know who decides and who can help; someone other than the primary maintainer has an agreed recovery role; material is not imported merely because it is technically accessible. +**Practical work completed (2026-09-22 UTC):** [stewardship](community/stewardship.md) +records current responsibilities, material-intake checks, creator/removal handling, +and the separate archive boundary. Private vulnerability reporting was confirmed +enabled. No real area owners are assigned, so `CODEOWNERS` is not applicable yet. +The stale statement that rollback had never been rehearsed is corrected. + +**Deferred:** the first two items remain incomplete because no agreed backup +operator or independent private contact is recorded. Owner: `ostomachion`. +Trigger: before wider participation or an operational handoff, obtain a willing +delegate's agreement, record responsibilities and a private reporting route, +verify necessary repository/hosting/domain access, and rehearse recovery. A +read-only collaborator is not a substitute for an agreed role. Full continuity +acceptance is not claimed; local documentation links and whitespace were checked. + ## Final gate: open the project to wider participation - [ ] Phases 2 and 3 are complete, or genuinely inapplicable items have an explicit diff --git a/docs/frontend/browser-support.md b/docs/frontend/browser-support.md index e69de29..d4eb6e1 100644 --- a/docs/frontend/browser-support.md +++ b/docs/frontend/browser-support.md @@ -0,0 +1,93 @@ +# Browser and accessibility expectations + +The current frontend is a Blazor WebAssembly placeholder that displays application +information from the API. There is no editor yet. This policy separates recorded +checks from the browser and accessibility targets to verify with the first +functional frontend feature, alongside [foundation section 6](../foundation-checklist.md#6-establish-frontend-failure-handling-and-a-smoke-test). + +## Recorded browser check + +The [staging deployment run on 2026-09-22 UTC](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35681727268) +passed the existing [deployment smoke test](../../tests/deploy-smoke/smoke.mjs) +for source revision `e164922c19213ae1ca2936554cca3970269cfac6` and release +`35681727268-1-e164922c1921`. Its browser installation log records Playwright +Chromium build `v1243`, Chrome Headless Shell `153.0.8010.12`, on the Linux runner. + +That check loads the published frontend, verifies the release URL and base path, +observes a successful `/api/about` request with the expected source revision, +checks the rendered application heading, and rejects startup page errors. It +does not check editor interactions, keyboard operation, screen readers, error +recovery, or other browser engines. This is the only recorded browser baseline +here; it does not establish support for the targets below. + +## Browser targets + +For each target, check the latest two stable major versions available when the +feature is accepted. Record exact browser and operating-system versions; do not +mark a target tested based on another browser's result. + +| Platform | Target browsers | Recorded status | +| --- | --- | --- | +| Windows | Chrome, Edge, Firefox | Unverified | +| macOS | Safari | Unverified | +| Android | Chrome | Unverified | +| iOS | Safari | Unverified | + +The desktop targets cover the complete implemented workflow. Mobile checks cover +loading, navigation, readable layout, and touch operation of available controls; +decide and document the intended mobile editing scope when editor controls exist. +Until these checks are recorded, browser coverage and mobile editing remain +targets, not an acceptance claim. + +[`.browserslistrc`](../../.browserslistrc) is a declaration for compatibility +tooling. No current build step consumes it. The file does not add polyfills, +transform application code, select test browsers, or verify compatibility. Its +existing queries are not a test record or a substitute for this matrix; review +their resolved targets if a build consumer is introduced. + +## Accessibility baseline and outstanding work + +A source inspection of the placeholder found the following on 2026-09-22 UTC. +These are implementation observations, not a runtime accessibility pass: + +- [Route navigation](../../src/OpenGameBuilder.Web.Client/App.razor) uses + `FocusOnNavigate` with selector `h1`; the + [not-found page](../../src/OpenGameBuilder.Web.Client/Pages/NotFound.razor) has + only an `h3`, so it has no matching focus target. +- The [home page](../../src/OpenGameBuilder.Web.Client/Pages/Home.razor.cs) + changes its heading from loading to success or an HTTP-failure message. It has + no explicit live region or retry control; announcements and recovery have not + been tested. +- The [startup page](../../src/OpenGameBuilder.Web.Client/wwwroot/index.html) + provides a loading SVG and CSS-generated loading text without an explicit live + region. Its unhandled-error UI has a reload link, but its dismiss control is a + `span` without keyboard or button semantics in the markup. + +Resolve these gaps with the first functional frontend feature under section 6. +There is currently no recorded keyboard, focus, or screen-reader acceptance, and +no claim of accessibility conformance. + +## Acceptance procedure for the first feature and later editor work + +Use the published build of the revision under review. Reuse and extend the +existing deployment smoke test for startup and API checks; add component coverage +for the feature's loading, success, expected failure, and invalid-response states +as described in [testing guidance](../quality/testing.md). Record manual checks +separately from automated results. + +| Check | Procedure and required result | +| --- | --- | +| Startup and API | In each browser target, load and reload the published page, follow a route, and verify the expected API-backed content without unhandled page errors. | +| Keyboard | On desktop, complete every implemented workflow using Tab, Shift+Tab, Enter, Space, and the controls' documented keys. Every action must be reachable, focus visible and ordered, and no control may trap focus. Provide a keyboard alternative for any editor action that otherwise requires dragging. | +| Focus | Navigate between routes, including not-found; open and close any dialogs or menus. Verify a useful destination receives focus, dismissal returns focus to the invoking control, and asynchronous updates do not steal focus. | +| Loading and failures | Throttle loading, interrupt the API request, and supply an invalid response in a controlled test. Verify loading and errors are understandable and announced, recovery is reachable with keyboard and touch, and retry or reload restores a usable state. Check the unhandled-error controls too. | +| Screen reader | Check Windows with NVDA and Chrome, Edge, and Firefox; macOS and iOS with VoiceOver and Safari; Android with TalkBack and Chrome. Verify headings, control names and states, loading/error announcements, navigation, and the available workflow. | +| Mobile layout and touch | On Android and iOS devices, check portrait and landscape layouts, navigation, and available controls. Verify essential content and actions remain reachable without accidental activation; record any explicitly unsupported editor operations. | + +For each result, record the date, revision or release, URL, browser and OS versions, +device/input method, screen-reader version when used, steps, and pass/fail or +unverified status in the feature's PR or a linked test record. Link failures to +the work needed before accepting the affected target. Browser automation does not +replace keyboard, assistive-technology, or physical-device checks. Extend the +matrix with concrete editor workflows as those features arrive rather than +claiming coverage for controls that do not yet exist. diff --git a/docs/licenses/aspire-MIT.txt b/docs/licenses/aspire-MIT.txt new file mode 100644 index 0000000..a616ed1 --- /dev/null +++ b/docs/licenses/aspire-MIT.txt @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/licenses/aspire-skills-MIT.txt b/docs/licenses/aspire-skills-MIT.txt new file mode 100644 index 0000000..22aed37 --- /dev/null +++ b/docs/licenses/aspire-skills-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/mygamebuilder/bugs.md b/docs/mygamebuilder/bugs.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/mygamebuilder/data-archive.md b/docs/mygamebuilder/data-archive.md index f181f7a..658d835 100644 --- a/docs/mygamebuilder/data-archive.md +++ b/docs/mygamebuilder/data-archive.md @@ -1,3 +1,8 @@ # MyGameBuilder S3 Archive *This document has been moved to the [mygamebuilder-archive](https://github.com/OpenGameBuilder/mygamebuilder-archive/blob/main/README.md) repo.* + +Archive stewardship and changes belong to that separate project. Inclusion in +the archive does not grant permission to import material into this application. +See [material intake and removal handling](../community/stewardship.md#material-intake-and-creator-requests) +for copies proposed or introduced here. diff --git a/docs/mygamebuilder/decompilation.md b/docs/mygamebuilder/decompilation.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/mygamebuilder/mgb-local.md b/docs/mygamebuilder/mgb-local.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/quality/coding-standards.md b/docs/quality/coding-standards.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/quality/naming-conventions.md b/docs/quality/naming-conventions.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/quality/performance.md b/docs/quality/performance.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/quality/testing.md b/docs/quality/testing.md index 3eb46d5..2c6bb5a 100644 --- a/docs/quality/testing.md +++ b/docs/quality/testing.md @@ -12,10 +12,13 @@ See [developer setup](../setup/development.md) for SDK prerequisites. | `tests\OpenGameBuilder.Api.Tests` | In-process HTTP integration tests of the real API, including a real API-client round trip | | `tests\OpenGameBuilder.Api.Client.Tests` | Client registration, configuration, JSON contracts, HTTP/transport failures, and cancellation | -Component tests and a published-app browser smoke test accompany the first +The existing deployment smoke below checks published frontend startup and a +real API round trip in Chromium. Component tests and broader browser coverage +for loading, success, network failure, and invalid responses accompany the first functional frontend feature, as scoped in section 6 of the -[foundation checklist](../foundation-checklist.md). The placeholder page does -not need a dedicated test project or browser infrastructure. +[foundation checklist](../foundation-checklist.md). See the +[browser and accessibility matrix](../frontend/browser-support.md) for recorded +evidence, unverified targets, and the manual checks required as the editor develops. [`tests/Directory.Build.props`](../../tests/Directory.Build.props) imports the repository-wide build properties and supplies the common test flags, xUnit diff --git a/docs/resources/asp-net-core.md b/docs/resources/asp-net-core.md deleted file mode 100644 index 2891daf..0000000 --- a/docs/resources/asp-net-core.md +++ /dev/null @@ -1,97 +0,0 @@ -# ASP.NET Core Resources - -ASP.NET Core is a cross-platform, high-performance, open-source framework for building modern web apps using .NET. -This page includes a curated list of links on ASP.NET Core relevant to OpenGameBuilder. -These links are some of the best documentation around. - -OpenGameBuilder's API back-end is an ASP.NET Core 10 Web API. The front-end is a [Blazor](./blazor.md) web app, -but many links on this page are still relevant for the front-end since Blazor is a part of ASP.NET Core. - -- [Overview of ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/overview?view=aspnetcore-10.0) -- [Website](https://dotnet.microsoft.com/en-us/apps/aspnet) -- [ASP.NET documentation](https://learn.microsoft.com/en-us/aspnet/core/?view=aspnetcore-10.0) -- [ASP.NET Core fundamentals overview](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/?view=aspnetcore-10.0&tabs=windows) - -## Learn -- [Getting started with ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/get-started?view=aspnetcore-10.0) -- [ASP.NET Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oW8nviYduHq7bmKode-p8Wy) -- [Front-end Web Development with .NET for Beginners](https://learn.microsoft.com/en-us/shows/frontend-web-development-with-dotnet-for-beginners/) -- [Back-end Web Development with .NET for Beginners](https://learn.microsoft.com/en-us/shows/back-end-web-development-with-dotnet-for-beginners/) -- [Create a web API with ASP.NET Core controllers](https://learn.microsoft.com/en-us/training/modules/build-web-api-aspnet-core/) - -## What's New - -Like [.NET](./dotnet.md), ASP.NET Core has a new major release every November, with even-numbered releases begin Long Term Support (LTS) releases and odd-numbered releases being Short Term Support (STS) releases. OpenGameBuilder currently uses ASP.NET Core 10, but will stay up to date with each release, both LTS and STS. - -- *(Upcoming) [What's new in ASP.NET Core 11](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11?view=aspnetcore-10.0)* -- [What's new in ASP.NET Core 10](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-10.0?view=aspnetcore-10.0) -- [What's new in ASP.NET Core 9](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-10.0) -- [What's new in ASP.NET Core 8](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-10.0) - -## ASP.NET Core Specifics - -### Fundamentals -- [Best practices](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/best-practices?view=aspnetcore-10.0) -- [App startup](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-10.0) -- [Dependency Injection](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-10.0) -- [Middleware](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-10.0) -- [Write custom middleware](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-10.0) -- [.NET Generic Host](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-10.0) -- [Web Host](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-10.0) -- [Configuration](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-10.0) -- [Options pattern](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-10.0) -- [Runtime environments](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-10.0) -- [Logging](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-10.0) -- [Health checks](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-10.0) - *Not yet implemented in OpenGameBuilder.* -- [Routing](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-10.0) -- [Handle errors](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-10.0) -- [Make HTTP requests with IHttpClientFactory](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-10.0) -- [Static files](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/static-files?view=aspnetcore-10.0) -- [Session and state management](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-10.0) -- [OpenAPI support](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview?view=aspnetcore-10.0) - -## APIs -- [APIs overview](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/apis?view=aspnetcore-10.0) -- [Minimal API quick reference](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-10.0) -- [WebApplication and WebApplicationBuilder](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/webapplication?view=aspnetcore-10.0) -- [Route handlers](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/route-handlers?view=aspnetcore-10.0) -- [Parameter binding](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding?view=aspnetcore-10.0) -- [Create responses](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/responses?view=aspnetcore-10.0) -- [Unit and integration tests](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/test-min-api?view=aspnetcore-10.0) -- [Authentication and Authorization](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/security?view=aspnetcore-10.0) - -### Models -- [Model binding](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-10.0) -- [Model validation](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-10.0) - -### Development, Debugging, and Testing -- [.NET Hot Reload support](https://learn.microsoft.com/en-us/aspnet/core/test/hot-reload?view=aspnetcore-10.0) -- [Unit test controller logic](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/testing?view=aspnetcore-10.0) -- [Integration tests](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-10.0&pivots=xunit) -- [Debugging](https://learn.microsoft.com/en-us/aspnet/core/test/debug-aspnetcore-source?view=aspnetcore-10.0) -- [Troubleshoot and debug](https://learn.microsoft.com/en-us/aspnet/core/test/troubleshoot?view=aspnetcore-10.0) - -### Hosting and Deploying -- [Host and deploy](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/?view=aspnetcore-10.0) -- [DevOps](https://github.com/dotnet-architecture/eBooks/blob/1ed30275281b9060964fcb2a4c363fe7797fe3f3/current/devops-aspnet-core/DevOps-for-ASP.NET-Core-Developers.pdf) -- [Host in Docker containers](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/?view=aspnetcore-10.0) - -### Security -- [Security topics](https://learn.microsoft.com/en-us/aspnet/core/security/?view=aspnetcore-10.0) -- [Overview of authentication](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-10.0) -- [Introduction to authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-10.0) -- [Data protection overview](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-10.0) -- [Safe storage of app secrets in development](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-10.0&tabs=windows%2Cpowershell) -- [Enforce HTTPS](https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-10.0&tabs=visual-studio%2Clinux-ubuntu) -- [Hosting images with Docker Compose over HTTPS](https://learn.microsoft.com/en-us/aspnet/core/security/docker-compose-https?view=aspnetcore-10.0) - -### Performance -- [Overview of caching](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/overview?view=aspnetcore-10.0) -- [Memory management and garbage collection](https://learn.microsoft.com/en-us/aspnet/core/performance/memory?view=aspnetcore-10.0) -- [Response compression](https://learn.microsoft.com/en-us/aspnet/core/performance/response-compression?view=aspnetcore-10.0) - -## See also -- [Blazor Resources](./blazor.md) -- [Entity Framework Core Resources](./entity-framework-core.md) -- [.NET Resources](./dotnet.md) -- [C# Resources](./csharp.md) diff --git a/docs/resources/blazor.md b/docs/resources/blazor.md deleted file mode 100644 index 377224e..0000000 --- a/docs/resources/blazor.md +++ /dev/null @@ -1,99 +0,0 @@ -# Blazor Resources - -ASP.NET Core Blazor is a modern front-end web framework based on HTML, CSS, and C# that helps you build web apps faster. With Blazor, build web apps using reusable components that can be run from both the client and the server so that you can deliver great web experiences. This page includes a curated list of links on Blazor relevant to OpenGameBuilder. - -OpenGameBuilder's front-end is a Blazor WebAssembly app. - -- [Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-10.0&WT.mc_id=dotnet-35129-website) -- [Website](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor) - -## Learn -- [Build your first web app with ASP.NET Core using Blazor](https://dotnet.microsoft.com/en-us/learn/aspnet/blazor-tutorial/intro) -- [Build web apps with Blazor](https://learn.microsoft.com/en-us/training/paths/build-web-apps-with-blazor/?WT.mc_id=dotnet-35129-website) -- [Build a Blazor todo list app](https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/build-a-blazor-app?view=aspnetcore-10.0) - -## What's New -See the **What's New** section in [ASP.NET Core Resources](./asp-net-core.md) which includes Blazor updates. - -## Blazor Specifics -- [Tooling](https://learn.microsoft.com/en-us/aspnet/core/blazor/tooling?view=aspnetcore-10.0&pivots=vs) -- [WebAssembly build tools and ahead-of-time (AOT) compilation](https://learn.microsoft.com/en-us/aspnet/core/blazor/webassembly-build-tools-and-aot?view=aspnetcore-10.0) -- [Hosting models](https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-10.0) -- [Project Structure](https://learn.microsoft.com/en-us/aspnet/core/blazor/project-structure?view=aspnetcore-10.0) - -### Fundamentals -- [Fundamentals Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/?view=aspnetcore-10.0) -- [Routing](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing?view=aspnetcore-10.0) -- [Navigation](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/navigation?view=aspnetcore-10.0) -- [Configuration](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/configuration?view=aspnetcore-10.0) -- [Dependency injection](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-10.0) -- [Startup](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/startup?view=aspnetcore-10.0) -- [Environments](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/environments?view=aspnetcore-10.0) -- [Logging](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/logging?view=aspnetcore-10.0) -- [Handle errors](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/handle-errors?view=aspnetcore-10.0) -- [Static files](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/static-files?view=aspnetcore-10.0) -- [Call a web API](https://learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api?view=aspnetcore-10.0) - -### Components -- [Razor components](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/?view=aspnetcore-10.0) -- [Render modes](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-10.0) -- [Retain element, component, and model relationships in](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/element-component-model-relationships?view=aspnetcore-10.0) -- [Attribute splatting and arbitrary parameters](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/splat-attributes-and-arbitrary-parameters?view=aspnetcore-10.0) -- [Layouts](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/layouts?view=aspnetcore-10.0) -- [Sections](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/sections?view=aspnetcore-10.0) -- [Control `` content](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/control-head-content?view=aspnetcore-10.0) -- [Event handling](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-10.0) -- [Data binding](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/data-binding?view=aspnetcore-10.0) -- [Lifecycle](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle?view=aspnetcore-10.0) -- [Disposal](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/component-disposal?view=aspnetcore-10.0) -- [Virtualization](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/virtualization?view=aspnetcore-10.0) -- [Rendering](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/rendering?view=aspnetcore-10.0) -- [CSS isolation](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation?view=aspnetcore-10.0) -- [Class libraries](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/class-libraries?view=aspnetcore-10.0&tabs=visual-studio) -- [Built-in components](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/built-in-components?view=aspnetcore-10.0) - -### JavaScript Interop -- [Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-10.0) -- [JavaScript location](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/location-of-javascript?view=aspnetcore-10.0) -- [Call JavaScript functions from .NET methods](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0) -- [Call .NET methods from JavaScript functions](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0) - -### Security -- [Authentication and Authorization](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-10.0&tabs=visual-studio) -- [Secure ASP.NET Core Blazor WebAssembly](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/?view=aspnetcore-10.0) - -### State Management -- [Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/?view=aspnetcore-10.0) -- [WebAssembly state management](https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/webassembly?view=aspnetcore-10.0) -- [Protected browser storage](https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/protected-browser-storage?view=aspnetcore-10.0) - -### Development, Debugging, and Testing -- [Debug](https://learn.microsoft.com/en-us/aspnet/core/blazor/debug?view=aspnetcore-10.0&tabs=visual-studio) -- [Test components](https://learn.microsoft.com/en-us/aspnet/core/blazor/test?view=aspnetcore-10.0) - -### Performance -- [Performance best practices](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/?view=aspnetcore-10.0) -- [Rendering performance best practices](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/rendering?view=aspnetcore-10.0) -- [App download size performance best practices](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/app-download-size?view=aspnetcore-10.0) -- [JavaScript interoperability (JS interop) performance best practices](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/javascript-interoperability?view=aspnetcore-10.0) -- [WebAssembly runtime performance](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/webassembly-runtime-performance?view=aspnetcore-10.0) -- [WebAssembly browser developer tools diagnostics](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/webassembly-browser-developer-tools-diagnostics?view=aspnetcore-10.0) -- [Lazy-load assemblies](https://learn.microsoft.com/en-us/aspnet/core/blazor/webassembly-lazy-load-assemblies?view=aspnetcore-10.0) -- [Web Workers](https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-with-dotnet-on-web-workers?view=aspnetcore-10.0) - -### Progressive Web Applications (PWA) -- [Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/progressive-web-app/?view=aspnetcore-10.0&tabs=visual-studio) - -### Host and Deploy -- [Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/?view=aspnetcore-10.0&tabs=visual-studio) -- [Host and deploy ASP.NET Core Blazor WebAssembly](https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/?view=aspnetcore-10.0&tabs=windows) -- [App base path](https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/app-base-path?view=aspnetcore-10.0) -- [.NET bundle caching and integrity check failures](https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures?view=aspnetcore-10.0) -- [Avoid HTTP caching issues when upgrading ASP.NET Core Blazor apps](https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/http-caching-issues?view=aspnetcore-10.0) -- [Configure the trimmer](https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/configure-trimmer?view=aspnetcore-10.0) - -## See also -- [ASP.NET Core Resources](./asp-net-core.md) -- [Entity Framework Core Resources](./entity-framework-core.md) -- [.NET Resources](./dotnet.md) -- [C# Resources](./csharp.md) diff --git a/docs/resources/browser-devtools.md b/docs/resources/browser-devtools.md deleted file mode 100644 index 8528ff8..0000000 --- a/docs/resources/browser-devtools.md +++ /dev/null @@ -1,40 +0,0 @@ -# DevTools Resources - -All modern browsers come with DevTools. A curated set of documentation for Chrome's DevTools is listed here, -but the general principles will apply to most browsers. - -Note that when debugging the OpenGameBuilder front-end locally, only Chromium based browsers (like Chrome or Edge) -will attach the Blazor app to the Visual Studio debugger. - -## Chrome - -### Overview -- [How to open Chrome DevTools](https://developer.chrome.com/docs/devtools/open) -- [Docs main page](https://developer.chrome.com/docs/devtools) -- [Overview](https://developer.chrome.com/docs/devtools/overview) -- [Tips](https://developer.chrome.com/docs/devtools/tips) -- [What's new](https://developer.chrome.com/docs/devtools/release-notes) - -### Elements Panel -- [Overview](https://developer.chrome.com/docs/devtools/elements) -- [Get started with viewing and changing the DOM](https://developer.chrome.com/docs/devtools/dom) -- [View the properties of DOM objects](https://developer.chrome.com/docs/devtools/dom/properties) -- [Badges reference](https://developer.chrome.com/docs/devtools/elements/badges) -- [View and change CSS](https://developer.chrome.com/docs/devtools/css) -- [Find invalid, overridden, inactive, and other CSS](https://developer.chrome.com/docs/devtools/css/issues) -- [Inspect grid layouts](https://developer.chrome.com/docs/devtools/css/grid) -- [Inspect and debug CSS flexbox layouts](https://developer.chrome.com/docs/devtools/css/flexbox) -- [Inspect and debug CSS container queries](https://developer.chrome.com/docs/devtools/css/container-queries) -- [Feature reference](https://developer.chrome.com/docs/devtools/css/reference) - -### Console Panel -- [Overview](https://developer.chrome.com/docs/devtools/console) -- [Understand errors and warnings better with console insights](https://developer.chrome.com/docs/devtools/console/understand-messages) -- [Log messages](https://developer.chrome.com/docs/devtools/console/log) -- [Feature reference](https://developer.chrome.com/docs/devtools/console/reference) -- [API reference](https://developer.chrome.com/docs/devtools/console/api) - -### Network Panel -- [Overview](https://developer.chrome.com/docs/devtools/network/overview) -- [Inspect network activity](https://developer.chrome.com/docs/devtools/network) -- [Feature reference](https://developer.chrome.com/docs/devtools/network/reference) diff --git a/docs/resources/caddy.md b/docs/resources/caddy.md deleted file mode 100644 index 94d4d45..0000000 --- a/docs/resources/caddy.md +++ /dev/null @@ -1,34 +0,0 @@ -# Caddy Resources - -Caddy is a modern web server and reverse proxy focused on simplicity, automatic HTTPS, and easy configuration. It fills a role similar to Nginx or Apache, but is generally much simpler to configure and maintain for small-to-medium deployments. - -OpenGameBuilder uses Caddy as the public-facing edge server for handling HTTPS, routing requests, serving the frontend files, and forwarding API traffic to the backend services. In practice, Caddy is responsible for things like: - -- Automatically managing SSL/TLS certificates -- Redirecting HTTP to HTTPS -- Serving the Blazor frontend -- Proxying `/api/*` requests to the backend API container -- Applying compression such as gzip and zstd -- Acting as the single public entry point for the deployment servers - -Most OpenGameBuilder contributors will not have to work with Caddy directly. It is only used on the deployment servers. Contributors working purely on the frontend, backend, engine, or editor code generally do not need to understand or modify the Caddy configuration. - -## General -- [Website](https://caddyserver.com/) -- [Docs](https://caddyserver.com/docs/) -- [Getting started](https://caddyserver.com/docs/getting-started) -- [Command line reference](https://caddyserver.com/docs/command-line) -- [Automatic HTTPS](https://caddyserver.com/docs/automatic-https) -- [Conventions](https://caddyserver.com/docs/conventions) -- [Keep Caddy running](https://caddyserver.com/docs/running) -- [Troubleshooting strategies](https://caddyserver.com/docs/troubleshooting) - -## Caddyfile -- [Overview](https://caddyserver.com/docs/caddyfile) -- [Quick-start](https://caddyserver.com/docs/quick-starts/caddyfile) -- [Tutorial](https://caddyserver.com/docs/caddyfile-tutorial) -- [Concepts](https://caddyserver.com/docs/caddyfile/concepts) -- [Directives](https://caddyserver.com/docs/caddyfile/directives) -- [Request matchers](https://caddyserver.com/docs/caddyfile/matchers) -- [Global options](https://caddyserver.com/docs/caddyfile/options) -- [Common patterns](https://caddyserver.com/docs/caddyfile/patterns) diff --git a/docs/resources/copilot.md b/docs/resources/copilot.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/csharp.md b/docs/resources/csharp.md deleted file mode 100644 index 2f425d2..0000000 --- a/docs/resources/csharp.md +++ /dev/null @@ -1,78 +0,0 @@ -# C# Resources - -The C# language is the most popular language for the .NET platform, a free, cross-platform, open source development environment. -This page includes a curated list of links on ASP.NET Core relevant to OpenGameBuilder for contributors from beginners to experts. - -OpenGameBuilder is implemented almost entirely in C# and uses many modern language features. - -- [Main site](https://learn.microsoft.com/en-us/dotnet/csharp/) -- [C# language reference](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/) -- [A tour of the C# language](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/overview) - -## Learn -- [C# for beginners](https://learn.microsoft.com/en-us/shows/csharp-for-beginners/) -- [Learn C#](https://learn.microsoft.com/en-us/collections/yz26f8y64n7k07) - -## Fundamentals -- [Program organization](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/program-organization) -- [Explore object oriented programming with classes and objects](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/classes) -- [Object-oriented C#](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/oop) -- [Inheritance in C# and .NET](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/inheritance) -- [Choose between tuples, records, structs, and classes](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/choosing-types) -- [Use pattern matching to build type-driven and data-driven algorithms](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/pattern-matching) -- [Use record types](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/records) -- [Explore indexes and ranges](https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/ranges-indexes) -- [Express your design intent more clearly with nullable and non-nullable reference types](https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/nullable-reference-types) -- [Working with LINQ](https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/working-with-linq) -- [Members](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/members) -- [Methods](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/methods) -- [Properties](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/properties) -- [Iterators](https://learn.microsoft.com/en-us/dotnet/csharp/iterators) -- [Strings and string literals](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/strings/) -- [Generic type parameters](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-type-parameters) - -## Types -- [Type system overview](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/) -- [Classes](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/classes) -- [Structs](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/structs) -- [Records types](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/records) -- [Interfaces](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/interfaces) -- [Enumerations](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/enums) -- [Generics](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics) - -## Null -- [Null safety](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/null-safety/) -- [Nullable value types](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/null-safety/nullable-value-types) -- [Null operators](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/null-safety/null-operators) -- [Nullable reference types](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references) - -## Object-Oriented Programming -- [Overview](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/) -- [Objects](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/objects) -- [Inheritance](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance) -- [Polymorphism](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/polymorphism) - -## Functional Techniques -- [Pattern matching](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching) -- [Discards](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards) -- [Deconstructing tuples and other types](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/deconstruct) - -## LINQ -- [Overview](https://learn.microsoft.com/en-us/dotnet/csharp/linq/) -- [Introduction to LINQ queries](https://learn.microsoft.com/en-us/dotnet/csharp/linq/get-started/introduction-to-linq-queries) - -## Asynchronous Programming -- [Asynchronous programming with async and await](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/) -- [Asynchronous programming scenarios](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios)- [Task asynchronous programming model](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/task-asynchronous-programming-model) -- [Async return types](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-return-types) - -## Performance -- [Reduce memory allocations using new C# features](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/) - -OpenGameBuilder uses C# 14, the latest version supported by [.NET](./dotnet) 10. See [C# language versioning](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-versioning). - -## What's New -- *(Upcoming) [What's new in C# 15](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-15)* -- [What's new in C# 14](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14) -- [What's new in C# 13](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13) -- [What's new in C# 12](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12) diff --git a/docs/resources/css.md b/docs/resources/css.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/docker.md b/docs/resources/docker.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/dotnet.md b/docs/resources/dotnet.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/entity-framework-core.md b/docs/resources/entity-framework-core.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/git.md b/docs/resources/git.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/github.md b/docs/resources/github.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/html.md b/docs/resources/html.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/json.md b/docs/resources/json.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/markdown.md b/docs/resources/markdown.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/minio.md b/docs/resources/minio.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/postgresql.md b/docs/resources/postgresql.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/visual-studio.md b/docs/resources/visual-studio.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/vscode.md b/docs/resources/vscode.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/resources/xunit.md b/docs/resources/xunit.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/setup/ai-tooling.md b/docs/setup/ai-tooling.md new file mode 100644 index 0000000..9a55a14 --- /dev/null +++ b/docs/setup/ai-tooling.md @@ -0,0 +1,117 @@ +# AI tooling maintenance + +AI tools are optional. Local agents follow [AGENTS.md](../../AGENTS.md), the +[AI policy](../../AI_POLICY.md), and the same +[development and validation workflow](development.md#command-line-workflow-start-here) +as human contributors. Skills supply reference material, not permission to act. + +## Repository scope + +The six vendored Aspire skills form one upstream bundle: `aspire`, +`aspire-init`, `aspireify`, `aspire-orchestration`, `aspire-monitoring`, and +`aspire-deployment`. Keep the bundle together so its routing and reference links +remain intact. `dotnet-inspect` is a separate skill for inspecting .NET APIs. + +Aspire is the local launcher. The generic deployment skill includes Azure, AWS, +Kubernetes, and Aspire publishing guidance; those are not approved production +workflows for this repository. Use [hosting](hosting.md) and the +[release process](../release/README.md) for the existing Compose deployment. +Installed skills do not authorize deployments, releases, credential handling, +destructive operations, new infrastructure, or changes to this architecture. +Documentation-only work does not start services. + +The [MCP configuration](../../.mcp.json) invokes `aspire agent mcp`; its CLI +installation and verification belong to [development setup](development.md). +The `dotnet-inspect` skill's `dnx` examples are optional tool invocations, not +pinned build dependencies. Review any tool execution separately from updating +the skill text. Never use its source/IL inspection features on original +MyGameBuilder proprietary material; the AI policy's source restrictions apply. + +## Verified provenance + +All seven skills were introduced in application commit +`f69f43d9d36c26d500678f8874831b71f0459e33`. The original import did not record an +upstream pin. The following matching snapshots were reconstructed and verified +on 2026-09-22 UTC; they establish content provenance, not the original install command. + +| Vendored content | Verified source | License and attribution | +| --- | --- | --- | +| Six Aspire skill directories, 36 files | [microsoft/aspire-skills at `35f41b0`](https://github.com/microsoft/aspire-skills/tree/35f41b013fb0e1cb7860c47ccc26d827ed5fba8b/skills) | [MIT, Microsoft Corporation](../licenses/aspire-skills-MIT.txt) | +| `dotnet-inspect/SKILL.md` | `DotnetInspectSkillFileContent` in [Aspire CLI 13.4.2 at `d7d0b67`](https://github.com/microsoft/aspire/blob/d7d0b6759ce4b936c76bc4775814d27db560dd6d/src/Aspire.Cli/Agents/CommonAgentApplicators.cs) | [.NET Foundation and Contributors, MIT](../licenses/aspire-MIT.txt); original skill by Richard Lander | + +The original dotnet-inspect skill is from +[richlander/dotnet-inspect v0.5.0](https://github.com/richlander/dotnet-inspect/blob/0fe16f7ffc8a1ece0c9a8607ebae0db1cbb65d20/skills/dotnet-inspect/SKILL.md), +whose project declares MIT licensing. Aspire embeds an adapted copy with +frontmatter and final-newline differences; use the embedded source above to +reproduce the vendored file. + +The 36 Aspire files also match the `aspire-skills-v0.0.1.tgz` bundle embedded in +that pinned CLI revision. Its SHA-256 is +`8f0aa535917bb6d2589acbf8f986c7b0d622ee7744e39c526bb7166c0664b53c`. +The current upstream `v0.0.1` release artifact differs, so a version label alone +is insufficient to reproduce these files. The dotnet-inspect file's SHA-256, +after converting CRLF to LF without adding a final newline, is +`944d0d4130c9286cbffcf7347e5127c8828c48525f6b0489c52c0e369ac0faab`. +All 37 vendored files match their recorded sources after CRLF-to-LF normalization; +there are no repository content patches. These third-party notices remain MIT; +the application repository's Apache license does not replace them. + +## Reproduce or update deliberately + +Use a focused branch and a separate temporary candidate directory. Do not run an +unpinned installer over the working skills or regenerate them during restore, +build, CI, or ordinary agent work. + +For the current Aspire snapshot, from the repository root in PowerShell: + +```pwsh +$repositoryRoot = (Get-Location).Path +$skillSource = Join-Path $env:TEMP ("ogb-aspire-skills-" + [guid]::NewGuid()) +git -c core.autocrlf=false clone https://github.com/microsoft/aspire-skills.git $skillSource +git -C $skillSource checkout --detach 35f41b013fb0e1cb7860c47ccc26d827ed5fba8b +$skillNames = 'aspire', 'aspire-init', 'aspireify', 'aspire-orchestration', 'aspire-monitoring', 'aspire-deployment' +foreach ($name in $skillNames) { + foreach ($part in 'SKILL.md', 'references') { + git diff --no-index --ignore-cr-at-eol -- "$repositoryRoot/.agents/skills/$name/$part" "$skillSource/skills/$name/$part" + } +} +``` + +No diff means the current snapshot matches. Git returns 1 for a content difference +and values above 1 for errors; inspect every comparison before copying anything. +These paths contain all 36 installed files at this revision. Upstream `evals` +directories are excluded by the bundle's install manifest and are not vendored. +For later revisions, inspect the manifest for added assets or changed exclusions. + +For dotnet-inspect, retrieve `CommonAgentApplicators.cs` at the pinned Aspire +commit. Extract the C# raw string named `DotnetInspectSkillFileContent`: remove +the opening/closing delimiters and the closing delimiter's eight-space indentation +from every content line, keep blank lines, use LF, and add no trailing newline. +Compare its SHA-256 with the value above and the local skill. Do not copy the +current richlander `main` file and describe it as the embedded version. + +For an update, choose explicit upstream commits and verify compatibility with the +repository's selected CLI/SDK. Review the entire candidate diff, including +commands, permissions, external references, additional files, and licenses. +Replace only the selected skill trees after review, explicitly removing obsolete +files rather than leaving them behind in an overlay. Retain full license notices +and attribution, then update this source record and checksum evidence in the same PR. + +Keep repository-specific instructions in AGENTS.md and these setup documents; +avoid editing vendored text. If a local patch is necessary, record its exact files, +reason, and upstream base here, and reapply/review it deliberately on each update. + +Before completing an update, compare all installed files with the chosen sources, +check referenced local assets and notices, review the diff, and run +`git diff --check`. A text-only skill or policy update does not require starting +Aspire. If the toolchain or application changes, run the documented solution gate +and any relevant runtime checks; a source match is not a runtime test. + +## Cloud coding agents + +Not applicable: the maintainer confirmed local agents only on 2026-09-22 UTC. +Automated Copilot review does not constitute a supported cloud coding environment. +There is no cloud setup workflow or claim of runner validation. If a cloud coding +agent is adopted, add a minimal setup using the same SDK and validation commands, +verify it on that actual runner, and record the result before calling it supported. +Keep production secrets out of that environment. diff --git a/docs/setup/github.md b/docs/setup/github.md index e8234b6..242ce12 100644 --- a/docs/setup/github.md +++ b/docs/setup/github.md @@ -286,9 +286,10 @@ PR failed, keep that source branch at the same commit and follow the [release rerun guidance](../release/README.md#what-happens-on-failure). The bot may create the tag, Release, and follow-up PR after deployment; it cannot approve its own PR, bypass CI, move a release tag, or recover the server. -There is no agreed backup operator or rehearsed artifact rollback yet; those -remain foundation sections 21 and 14 respectively. Do not equate an authorized -GitHub rerun with a tested rollback. +There is no agreed backup operator; see [practical stewardship](../community/stewardship.md). +Artifact rollback was rehearsed in staging as recorded in +[foundation section 14](../foundation-checklist.md#14-make-rollout-atomic-and-rollback-explicit). +That evidence does not establish a second operator's access or recovery readiness. ## Acceptance evidence From ba4203a6283c0f1b50a00b4028ba812b33aff0dc Mon Sep 17 00:00:00 2001 From: Josh Hufford Date: Tue, 22 Sep 2026 08:13:50 -0400 Subject: [PATCH 2/9] Validate frontend packaging before merge --- .github/actions/validate/action.yml | 46 +- .github/workflows/_deploy.yml | 2 + README.md | 13 +- docs/README.md | 8 +- docs/foundation-checklist.md | 1163 +++++++++++++++------------ docs/frontend/browser-support.md | 4 +- docs/quality/testing.md | 40 +- docs/release/README.md | 4 +- docs/setup/github.md | 8 +- docs/setup/hosting.md | 79 +- scripts/deploy-app.sh | 76 +- tests/deploy-app/run.sh | 175 +++- 12 files changed, 1033 insertions(+), 585 deletions(-) diff --git a/.github/actions/validate/action.yml b/.github/actions/validate/action.yml index 8781cfc..4a3ffbe 100644 --- a/.github/actions/validate/action.yml +++ b/.github/actions/validate/action.yml @@ -1,5 +1,5 @@ name: Validate solution -description: Shared restore, formatting, Release build, and test gate for CI and deployment. +description: Shared solution, frontend packaging, and smoke-package validation for CI and deployment. inputs: checkout-ref: @@ -13,6 +13,10 @@ inputs: artifact-name: description: Unique name for this job's failure diagnostics. required: true + publish-web: + description: Publish and verify the frontend; disable only when the caller has a separate required packaging job. + required: false + default: "true" runs: using: composite @@ -61,6 +65,46 @@ runs: dotnet test --solution opengamebuilder.slnx --configuration Release --no-build 2>&1 | tee artifacts/validation/test.log + - name: Publish web client + if: ${{ inputs.publish-web == 'true' }} + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + set -o pipefail + dotnet publish src/OpenGameBuilder.Web.Client/OpenGameBuilder.Web.Client.csproj \ + --configuration Release --no-build --output artifacts/web \ + 2>&1 | tee artifacts/validation/web-publish.log + + - name: Verify portable web configuration + if: ${{ inputs.publish-web == 'true' }} + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + set -o pipefail + bash scripts/verify-web-publish.sh artifacts/web/wwwroot \ + 2>&1 | tee artifacts/validation/web-configuration.log + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + package-manager-cache: false + + - name: Validate browser smoke dependencies + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + set -o pipefail + npm ci --prefix tests/deploy-smoke \ + 2>&1 | tee artifacts/validation/smoke-dependencies.log + + - name: Check browser smoke syntax + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + set -o pipefail + node --check tests/deploy-smoke/smoke.mjs \ + 2>&1 | tee artifacts/validation/smoke-syntax.log + - name: Test release scripts without external writes shell: bash working-directory: ${{ inputs.working-directory }} diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml index 759b9f3..3a66cd1 100644 --- a/.github/workflows/_deploy.yml +++ b/.github/workflows/_deploy.yml @@ -69,6 +69,8 @@ jobs: checkout-ref: ${{ needs.resolve-source.outputs.protected-revision }} working-directory: deployment-source artifact-name: ${{ inputs.environment-slug }}-validation + # The required package job below publishes and verifies the deployable frontend. + publish-web: "false" package: name: Package portable web client diff --git a/README.md b/README.md index aee7a29..c9f3181 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ architecture may change before v1; original-game compatibility is not establishe The next milestone is to make the foundation ready for engine development. The first engine milestone's compatibility goal, independent fixture, and -acceptance criteria are still to be chosen in -[section 9 of the foundation checklist](docs/foundation-checklist.md#9-record-the-engine-boundary-and-first-milestone). +acceptance criteria are still to be agreed through a +[public proposal](https://github.com/OpenGameBuilder/opengamebuilder/discussions). Full original-site recreation and broad game compatibility are outside this initial foundation work. @@ -31,7 +31,7 @@ initial foundation work. 2. Read the [contribution guide](CONTRIBUTING.md) and [testing guidance](docs/quality/testing.md) before changing code. If you use AI tools, follow the [AI policy](AI_POLICY.md). -3. Pick a bounded task from the [foundation roadmap](docs/foundation-checklist.md) +3. Pick a bounded task from the [public roadmap](https://github.com/orgs/OpenGameBuilder/projects/3) or [open issues](https://github.com/OpenGameBuilder/opengamebuilder/issues). Check existing issues before starting, and propose larger changes before implementation as described in the contribution guide. @@ -41,10 +41,9 @@ and browser/accessibility expectations. Useful starting tasks include following the setup guide in your editor and reporting a reproducible failure, improving an unclear setup instruction, or -adding a focused regression test for an API/client bug. Section 3 of the -[checklist](docs/foundation-checklist.md#3-repair-the-documented-development-workflow) -tracks the remaining fresh-checkout editor verification. Engine contributors -can help define the small, independently testable milestone in section 9. +adding a focused regression test for an API/client bug. Fresh-checkout editor +verification remains open. Engine contributors can help define a small, +independently testable milestone through a public proposal. Use the [issue forms](https://github.com/OpenGameBuilder/opengamebuilder/issues/new/choose) for reproducible bugs and concrete proposals. See [support](SUPPORT.md) for help diff --git a/docs/README.md b/docs/README.md index 6c67d20..b11ca8c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,16 +1,16 @@ # Documentation Start with [development setup](setup/development.md) to build, test, and run the -application. The [foundation checklist](foundation-checklist.md) records current -work, acceptance evidence, and deferred features. There is no game runtime or -editor yet; section 9 tracks the first engine milestone decision. +application. The [public roadmap](https://github.com/orgs/OpenGameBuilder/projects/3) +tracks project work. There is no game runtime or editor yet; the first engine +milestone's behavior and acceptance criteria remain to be agreed. ## Working on the application | Task | Read | | --- | --- | | Install tools, run locally, or debug | [Development setup](setup/development.md) | -| Choose a task or propose a change | [Contributing](../CONTRIBUTING.md) and [foundation checklist](foundation-checklist.md) | +| Choose a task or propose a change | [Contributing](../CONTRIBUTING.md) and [open issues](https://github.com/OpenGameBuilder/opengamebuilder/issues) | | Understand the current HTTP surface and health checks | [API](backend/api.md) | | Run tests and understand their limits | [Testing](quality/testing.md) | | Maintain agent instructions and vendored skills | [AI tooling maintenance](setup/ai-tooling.md) and [AI policy](../AI_POLICY.md) | diff --git a/docs/foundation-checklist.md b/docs/foundation-checklist.md index a60b980..2acbe32 100644 --- a/docs/foundation-checklist.md +++ b/docs/foundation-checklist.md @@ -1,530 +1,637 @@ # Foundation checklist -Work through these steps in order within each phase. Prefer one focused pull request -per numbered step; split a step further when needed. Check items off only after the -acceptance check passes. Paths are relative to the repository root. - -Track current work here, with completed steps marked done and deferred steps -given a concrete trigger. Check dependency advisories and GitHub settings against -the current repository. - -## Phase 1: Establish a foundation for engine development - -### 1. Establish the current dependency and toolchain baseline - -(done) - -### 2. Include the developer launcher in build validation - -(done) - -### 3. Repair the documented development workflow - -- [ ] Follow `docs\setup\development.md` from a fresh checkout in Visual Studio - and VS Code, verifying F5 launching and debugger attachment. - -**Acceptance:** both editor workflows work as documented, and the frontend loads -application information from the local API without undocumented steps or -production credentials. - -### 4. Make formatting and build policy consistent - -(done) - -### 5. Establish API and client behavior tests - -(done) - -### 6. Establish frontend failure handling and a smoke test - -**Deferred until the first functional frontend feature.** The current page is a -placeholder; add the following with that feature rather than introducing test -projects or browser infrastructure now. - -- [ ] Define the expected UI for loading, success, network failure, and invalid - API responses. Handle expected failures explicitly without hiding unexpected - errors behind broad catch blocks. -- [ ] Add component tests for those states and a published-app browser smoke test - that verifies a real frontend-to-API request. -- [ ] Preserve useful diagnostic logging without exposing sensitive response data. - -**Acceptance:** the smoke test fails if the API URL is wrong or the frontend cannot -start, even when the API's liveness endpoint is healthy. - -### 7. Make CI an effective merge gate - -- [x] Keep the same build/test/format checks in pull-request CI and deployment - validation. Add useful failure artifacts and explicit workflow timeouts. -- [x] Inspect both rulesets and legacy branch protection. Require the actual - stable build/test job, not only CodeQL, code-quality checks, or review. -- [x] Align protected branches with `main` and `patch/v*`. Ensure the intended - CodeQL checks run for patch PRs too. -- [x] Verify human approval, stale-review dismissal, and release-tag protection. - Document any deliberate maintainer or release-bot bypasses. - -**Acceptance:** a failing test blocks merging a representative PR. A patch branch -receives the intended protections and runnable checks without blocking the -release bot's narrowly authorized work. - -**Verified (2026-09-15):** shared validation, failure artifacts, workflow timeouts, -and patch CodeQL triggers are implemented. Authenticated inspection confirmed -there are no legacy protection rules. Live rules now require `build-test` from -GitHub Actions on `main` and `patch/v*`, with no CI bypass. Release-App bypasses -are creation-only; tags cannot be changed or deleted even by the bot. - -The only maintainer is the sole eligible reviewer, so a documented PR-only -exception is retained in a separate human-review ruleset, not in the CI gate. -Remove that exception when a second trusted reviewer can review maintainer PRs. -Local formatting, builds, all 62 tests, and failure-exit propagation passed. -Failing-test merge blocking was verified on PRs #82 and #83, including uploaded -assertion diagnostics. The deliberate test was removed. The aggregate CodeQL -gate caught selectable-source action execution; protected source resolution and -trusted action loading address that boundary. Both main and patch validation -then passed all 62 tests, the Docker build, code-quality analysis, and aggregate -CodeQL with zero open alerts. Temporary verification refs were cleaned up. - -After the owner approved Workflows write permission, -[Prepare Patch run 35002126742](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35002126742) -successfully created the protected patch branch and bot-authored PR #84. The PR -still required human review and CI; its inherited old-release dependency failed -NuGet Audit, correctly blocking merging rather than bypassing the gate. -The verification PR and both new refs were cleaned up without merging, deploying, -or creating a release. Section 7 is complete; [GitHub setup](setup/github.md) -records the live rules, deliberate exceptions, current-baseline passing checks, -and bot acceptance evidence. - -### 8. Write useful repository-specific AI instructions - -- [x] Document a short project map and dependency boundaries in `AGENTS.md`, with - prerequisites, exact validation commands, and links to authoritative policies. -- [x] Explain that Aspire is the local launcher while production currently uses - Compose. Require explicit authorization for deployments, releases, credentials, - and destructive operations; documentation-only work should not start services. -- [x] Add thin Copilot-specific guidance only where needed for tool support, or - correct documentation claiming those files already exist. Avoid duplicated rules. -- [x] Document the Aspire executable required by `.mcp.json` and how contributors - install and verify it without making AI tools mandatory. - -**Acceptance:** a fresh local agent can perform a small code/test change using the -instructions, without guessing commands or accessing deployment credentials. - -**Verified (2026-09-19):** `AGENTS.md` now maps the solution's dependency -boundaries, names the local validation gate, and points to the authoritative setup, -testing, policy, hosting, and CI documentation. It documents `aspire` as the -executable used by `.mcp.json`, with the matching 13.4.2 install and verification -commands. The installed executable reported 13.4.2. Aspire is explicitly scoped -to local orchestration; production Compose, releases, credentials, deployments, -and destructive operations require authorization. `CONTRIBUTING.md` now identifies -`AGENTS.md` as the existing repository guidance instead of implying uncommitted -Copilot instruction files exist. - -### 9. Record the engine boundary and first milestone - -- [ ] Write a short architecture/repository map: API, contracts, API client, - frontend, AppHost, service defaults, and the separate archive repository. -- [ ] Choose the first compatibility goal and its non-goals: playback, import, - editor behavior, or one narrowly defined combination. -- [ ] Define a small, independently created fixture and observable acceptance - criteria for the first engine milestone. -- [ ] Keep game logic testable without Blazor, HTTP, or a database. Introduce - boundaries for rendering, input, time, and randomness with actual engine work, - not as empty projects or generic infrastructure. - -**Acceptance:** the next engine task is small enough to implement and test without -first adding a new architectural framework. - -### Gate: return to engine implementation - -- [ ] Steps 1-5 and 7-9 pass their acceptance checks. Section 6 accompanies the - first functional frontend feature. -- [ ] Local setup works, meaningful tests run, CI enforces them, and agent - instructions match reality. -- [ ] Resume the scoped engine milestone. Do not wait for every community or - documentation refinement below. - -Complete Phase 2 before relying on further public deployments. If deployments -continue during engine work, bring those steps forward rather than accepting the -known release risks. - -## Phase 2: Make deployment and releases dependable - -### 10. Test and repair release-script behavior - -- [x] Fix `scripts\validate-release.sh` returning failure after successful patch - validation because its final optional-output condition is false. -- [x] Support documented reruns when a patch tag/release already exists at the - expected commit. Continue rejecting mismatched tags and invalid version progressions. -- [x] Avoid resolving all of `Directory.Build.props` with `--ours` in - `scripts\post-release.sh`; preserve non-version changes or require manual resolution. -- [x] Add isolated tests for standard and patch releases, tag conflicts, reruns, - failed GitHub calls, follow-up PR handling, and merge-back conflicts. - -**Acceptance:** the valid-next-patch and already-published-patch cases both pass. -Tests mock external operations and never push branches, tags, or releases. -If simplifying the release workflow instead, remove superseded paths and update -`docs\release` so there is only one supported process. - -**Verified locally (2026-09-19):** 20 isolated Bash cases passed with GitHub -operations and pushes mocked, including valid-next and already-published patch -releases. A conflicting props merge-back now stops for manual resolution without -pushing. The test suite is part of shared CI/deployment validation; no actual -release or deployment was run. Restore, formatting verification, Release build, -and all 62 solution tests passed (the documented ASPIRE010 build warning remains). - -### 11. Align deployment permissions with the release process - -- [x] Audit environment branch/tag restrictions against the actual workflow. - Distinguish the branch dispatching the workflow from the source SHA it checks out; - do not blindly replace every environment selector with `patch/*`. -- [x] Verify production approval, release-bot permissions, protected-tag creation, - and narrowly scoped environment secrets. -- [x] Record who can deploy and recover a release in `docs\setup\github.md`. - -**Acceptance:** approved standard and patch workflows are allowed, unintended -dispatch paths are rejected, and normal pull-request validation receives no -production credentials. - -**Verified (2026-09-20):** authenticated read-back found production approval by -`ostomachion`, self-review and administrator bypass enabled, environment-scoped -deployment secrets, repository-scoped release-bot tokens, and creation-only bot -tag permission. The stale production `release/**/*` tag selector and staging -`release/**/*` branch selector were removed; both environments now allow only -the `main` dispatch branch. Both CD workflows on merged `main` reject non-`main` -dispatches before source selection. A [live invalid-source run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35531224662) -rejected a tag input at the protected-source resolver; validation, production -deployment, and publication were skipped. A second -[non-`main` dispatch](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35531484408) -failed at the first workflow guard, with all downstream jobs skipped. The -first merge-triggered -[staging run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35531071912) -passed source resolution and build/test but failed before syncing files or -restarting services: its environment secrets were empty in the reusable workflow -after `secrets: inherit` was removed. The merged fix restored the caller handoff -and added a credential-presence check before image publication. Its -[staging run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35531733842) -passed source resolution, build/test, credential check, image publication, SSH, -file sync, service restart, and the API liveness smoke test at commit -`65cf767afd587ce5ea72368df8d888c69bd0a7e7`. -[GitHub setup](setup/github.md#deployment-authority-and-recovery) records the -dispatch/source distinction, actual operator and recovery limits, and the -read-only permission audit. The signed-in organization Actions settings page -reported no organization secrets; the audit CLI token still receives 403 for -that API inventory. Normal PR CI references no deployment environment or -production credentials. The protected `main`/patch source paths are configured -for standard and patch releases, but no positive production release, patch -deployment, tag, or GitHub Release was performed for this permission audit. -Administrator bypass is retained for sole-operator emergency recovery, not -routine releases; while enabled, the `main`-only selector is not an absolute -barrier to an administrator forcing a waiting job. - -### 12. Isolate shared-edge changes from application deployments - -- [x] Stop routine staging/application deployments from recreating the shared - production Caddy service. -- [x] Validate a candidate Caddy configuration before activating it; use a - graceful reload when configuration changes. -- [x] Coordinate operations that modify shared edge files or services across - staging and production. Avoid cancelling an in-flight mutation midway through. - -**Acceptance:** deploy staging while checking production availability. An invalid -candidate edge configuration is rejected without replacing the working configuration. - -**Implemented locally (2026-09-20):** application deployments no longer sync or -recreate shared Caddy. The `main`-only, production-approved shared-edge workflow -serializes edge updates without cancellation. Its apply script validates a staged -candidate before touching active files, reloads Caddy for Caddyfile-only changes, -and restores prior files after a failed reload. The isolated Docker-mock suite -passed invalid-candidate, reload, rollback, Compose-update, and first-setup cases. -Staging now probes production API liveness before and after its deployment. -Live staging deployment and production availability read-back remain to be -verified after the protected workflow change is merged; no edge or application -deployment was run for this local implementation. - -**Host independence follow-up:** deployment environments explicitly select a -`shared`, `staging`, or `production` edge profile. Each host owns its own network, -proxy, and certificate volumes; an isolated profile contains no routes or web -mounts for the other environment. Only shared staging deployments probe -production availability. Profile changes require explicit production approval, -and application preflight checks the installed profile before transferring a -release. See [hosting setup](setup/hosting.md#host-edge-changes) for adoption and -future separation. Local regression/configuration validation does not establish -live separate-host acceptance; no host migration is performed by this change. - -### 13. Make builds portable and promote identifiable artifacts - -- [x] Prefer deployed frontend requests to the current origin's `/api` rather than - hardcoded official hosts. Keep an explicit local development override. -- [x] Verify forks and self-hosted Release builds cannot accidentally call the - official API. Do not introduce API subdomains without a concrete requirement. -- [x] Reduce duplicated endpoint/port configuration. Support parallel worktrees - when practical; otherwise document the fixed-port limitation. -- [x] Build frontend and API artifacts once where practical and promote the - tested pair. Address environment-specific frontend publishing before claiming - that the same artifact is promoted unchanged. -- [x] Record image digests, frontend artifact identity, and source revision. - Do not rely on a mutable commit-named image tag or version string alone. - -**Acceptance:** the same tested release can be identified unambiguously and hosted -on an alternate hostname without rebuilding just to change its API hostname. - -**Implemented locally (2026-09-20):** Release web builds use the hosting origin -and retain only a Development localhost override; staging and production no longer -publish different API URLs. The packaging job follows source validation and builds -the frontend archive once; after environment approval, deployment verifies that -archive, builds the API image once, and records its digest reference, the archive -checksum, and source revision on the host. Fixed development ports are documented -as a single-stack limitation. Local build/publish and workflow structure checks -validate the package contract; live deployment and alternate-host browser behavior -remain unverified until a controlled deployment. Staging and production workflow -runs still package separately; each run's manifest identifies its actual pair. - -### 14. Make rollout atomic and rollback explicit - -- [x] Replace in-place frontend `rsync --delete` with versioned release directories - and an atomic activation step. -- [x] Retain the previous compatible frontend/API pair and document rollback. - Account for clients still requesting assets from an older loaded page. -- [x] Extend smoke tests to verify the expected revision and a frontend-to-API - interaction, not only `/api/alive` or Caddy's static `/health`. -- [x] Document failure recovery in `docs\setup\hosting.md` and `docs\release`, - including a deploy succeeding before tag or follow-up PR creation fails. - -**Acceptance:** rehearse deployment failure and rollback in staging. Recover the -previous working release without rebuilding it or guessing which image it used. - -**Accepted in staging (2026-09-20):** [normal run 35542650898](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35542650898) -passed build/test, activation, and Chromium smoke against source revision -`14e7582d01059f0408be501504a5689e53a74f36`. The controlled -[rollback rehearsal 35542855238](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35542855238) -activated a new release, passed the same browser check, then failed on purpose. -Its recovery step restored release `35542650898-1-14e7582d0105` using the -recorded prior image digest without rebuilding. The rehearsal run is red by -design. Independent HTTPS checks afterward found the root page pointing to the -restored release, `/api/about` returning the expected revision, both versioned -web directories serving, and production `/api/alive` healthy. See the -[hosting recovery procedure](setup/hosting.md#application-activation-and-rollback) -and [release failure guidance](release/README.md#what-happens-on-failure). - -### 15. Harden the existing hosting and supply chain - -- [x] Pin Actions to reviewed commit SHAs and deployed container images to digests. - Keep Dependabot/update automation capable of maintaining those pins. -- [ ] Verify the deployment SSH host key through a trusted channel and pin it, - rather than trusting a fresh `ssh-keyscan` result during each deployment. -- [x] Run the API container as an explicit non-root user and verify permissions. -- [x] Configure trusted forwarded headers for Caddy before middleware relying on - request scheme or client IP. Test HTTPS redirects; do not trust arbitrary proxies. -- [x] Recheck NuGet configuration inheritance on a clean machine. Explicitly clear - inherited source mappings for the single-feed setup, or document and adopt - reviewed source-specific mappings. Do not treat a global wildcard as namespace - isolation between feeds. - -**Acceptance:** image startup, proxy behavior, restore, dependency updates, and -deployment still work with the hardened configuration and least required privileges. - -**Implemented locally (2026-09-21):** third-party Actions and base/edge images -are immutable while the existing Dependabot ecosystems remain enabled. A local -image build and runtime probe confirmed that the API starts with a nonzero UID, -can read its assembly, cannot write `/app`, and returns its liveness response; CI -repeats that probe. The API -trusts one forwarded hop only from the deployed `ogb-edge` network, with redirect -and spoofing coverage. An isolated empty-cache restore passes with inherited -package sources and mappings cleared. Deployment now requires a pinned -`DEPLOY_KNOWN_HOSTS` environment variable and never learns trust with -`ssh-keyscan`. Keep the SSH item and live acceptance open until an administrator -verifies and records the key through an existing trusted SSH connection or an -independent authenticated channel, configures both environments, CI passes, and -a merged staging deployment passes. Follow the -[host-key setup guide](setup/deployment-host-key.md) for commands and trust limits. - -**Live application evidence (2026-09-22 UTC):** the -[staging retry](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35673447966/attempts/2) -and [production v0.10.0 release](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35674772060) -passed the image permission/liveness probe, strict pinned-host SSH connections, -activation, browser revision checks, and finalization. The automatic version -bump and [staging rollout of 0.11.0](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35675216560) -also passed. These prove that the configured pins work, not independently how -the administrator authenticated the original host key. Keep that trust-source -confirmation explicit. The running Caddy digest and native-service boot state -still need [host verification](setup/hosting.md#host-caddy-conflicts-and-read-only-verification); -application deployments do not apply edge-image changes. - -## Phase 3: Prepare to welcome community contributors - -### 16. Replace the placeholder README with a contributor front door - -- [x] Describe what works today, the first milestone, non-goals, and project status. -- [x] Link working setup, contribution, testing, support, roadmap, and license - information before emphasizing deployment badges. -- [x] Explain the distinction between this implementation, the archive repository, - and the original MyGameBuilder; do not imply official continuation. - -**Acceptance:** an unfamiliar contributor can identify a useful task and reach the -run instructions from the README without asking a maintainer. - -**Verified locally (2026-09-21):** the README now describes the API/frontend -foundation and its current limitations, links directly to the run instructions, -and gives concrete setup, documentation, and regression-test contribution paths. -It points to section 9 for the still-unselected first engine milestone rather -than claiming a compatibility target has been accepted. Setup, contribution, -testing, support, roadmap, and license links precede workflow badges. Local link -targets and heading anchors were checked, current behavior was compared with -the source, and the public archive repository and enabled issue tracker were -confirmed through GitHub. This documentation review does not close section 3's -fresh-checkout editor verification or section 9's milestone decisions. - -### 17. Make public support and issue intake usable - -- [x] Make GitHub Discussions the discoverable default for exploratory questions. - Keep the reunion Discord private if desired, but not a prerequisite for contributing. -- [x] Link directly to the authoritative organization-wide Code of Conduct and - governance documents from `CONTRIBUTING.md` and `SUPPORT.md`. -- [x] Add question and private-security-reporting links to the issue chooser. -- [x] Use the existing project `Triage` status; remove stale `needs triage` - label references from issue forms. Dependabot is already corrected. Remove - the compatibility form's TODO option. -- [x] Simplify overlapping enhancement/feature forms if they do not help triage. - Make the issue labels/types and contributor guidance agree. - -**Acceptance:** a newcomer can ask a question, report a bug, propose a change, -and find private reporting instructions without access to private chat. - -**Verified locally and against GitHub (2026-09-21):** Discussions (including Q&A -and Ideas), private vulnerability reporting, and the public Roadmap's `Triage` -status already exist. No settings or labels needed creating, and Dependabot -already omits the stale label. The four issue forms now omit it too. Bug, -Feature, and Enhancement match enabled organization issue types; the nonexistent -compatibility type is replaced with Bug while keeping its behavior/evidence form. -The unused TODO dropdown is removed. Enhancement and Feature remain separate -because both types exist and the forms distinguish existing and new capabilities. -Support, contribution guidance, and issue-chooser links now lead to public -questions, concrete reports/proposals, and private security reporting. Broken -local policy links now point to the verified organization-wide documents. -YAML, configured types, and documentation links were checked locally; the hosted -chooser will reflect these changes after merge. No test issues or reports were -submitted, and project automation was not changed. - -### 18. Create a small, genuinely actionable contributor backlog - -- [ ] Prepare a few `good first issue` and `help wanted` tasks with acceptance - criteria, likely files, validation steps, and a willing maintainer contact. -- [ ] Include non-code opportunities such as accessibility testing, documentation, - independently written behavior examples, and UI feedback. -- [ ] Record consequential decisions in public issues, discussions, or short - architecture notes rather than only in Discord. - -**Acceptance:** someone new can take a bounded task without first designing a -database layer, deployment system, or entire engine. - -### 19. Consolidate documentation and define browser expectations - -- [x] Remove empty Markdown placeholders or turn the intended work into issues. - Keep only useful setup, architecture, testing, hosting, and compatibility documents. -- [x] Add a concise documentation index and repair broken local links. -- [x] Correct stale claims, including the statement that health checks are absent. - Move general learning resources to the shared location planned by the project, - retaining useful links rather than duplicating a reference library. -- [x] Fill `docs\frontend\browser-support.md` with a tested support policy. - Explain that `.browserslistrc` alone neither implements nor verifies compatibility. -- [x] Include keyboard operation, focus, accessible loading/errors, and a concrete - supported-browser smoke matrix for the editor as it develops. - -**Acceptance:** referenced documents contain real instructions; local links resolve; -the claimed browser/accessibility baseline has recorded checks. - -**Verified (2026-09-22 UTC):** removed 23 empty placeholders and filled the API -and browser guides. The [documentation index](README.md) links current operating -instructions, the repository map, and compatibility boundaries. Five substantive -learning-resource pages moved to the organization's `.github` repository in -[draft PR #1](https://github.com/OpenGameBuilder/.github/pull/1), retaining all 211 -original external link targets and replacing broken local references. The index -links the published shared commit so it works before merge; issue -[#56](https://github.com/OpenGameBuilder/opengamebuilder/issues/56) remains open -pending the cross-repository review and merge. The API guide documents existing -health checks, and testing guidance now distinguishes the existing Chromium -deployment smoke from section 6's future component and failure-state coverage. - -The browser guide records the successful Chromium smoke from staging run -[35681727268](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35681727268) -and source-inspected accessibility gaps. Other browsers, keyboard, focus, screen -readers, and physical mobile devices remain explicitly unverified; their concrete -acceptance matrix accompanies the first functional frontend feature in section 6. -This is not an editor or accessibility-conformance acceptance. Markdown paths, -heading anchors, nonempty documents, preserved resource links, and whitespace -were checked. No new services or browser sessions were started, and section 18 -was left unchanged. - -### 20. Finish AI tooling maintenance and simplify policy - -- [x] Record vendored skills' upstream source/revision, applicable licenses and - attribution, update/regeneration procedure, and local-edit policy. -- [x] Trim unused skill coverage where useful, or explicitly distinguish generic - cloud/deployment capabilities from approved repository workflows. -- [x] Shorten repetitive sections of `AI_POLICY.md` without weakening human - accountability, disclosure, privacy, or proprietary-material restrictions. -- [x] If using cloud coding agents, add a minimal reproducible setup workflow and - validate it on their actual runner. Keep production secrets out of that environment. - Otherwise record cloud-agent setup as not applicable. - -**Acceptance:** local and any supported cloud agents use the same documented -checks. A maintainer can update the skills deliberately, with provenance preserved. - -**Verified (2026-09-22 UTC):** [AI tooling maintenance](setup/ai-tooling.md) -records matching immutable source revisions for all 37 vendored files, the -embedded Aspire bundle checksum, dotnet-inspect attribution, complete MIT notices, -and reproduction/update and local-patch procedures. A clean upstream checkout -matched all six Aspire skill trees; the extracted dotnet-inspect source matched -its recorded SHA-256. The vendored files themselves are unchanged. Generic cloud -and deployment coverage is explicitly separate from this repository's approved -Compose workflow and authorization boundaries. `AI_POLICY.md` was reduced from -307 to 119 lines while preserving accountability, disclosure, privacy, licensing, -and proprietary-source restrictions; its linked source-material heading remains. -The maintainer confirmed local agents only, so cloud setup and runner validation -are not applicable. Local links, license contents, and whitespace checks passed; -no services, cloud environment, or new application tests were needed. - -### 21. Establish practical stewardship and project continuity - -- [ ] Keep lightweight governance, but identify a backup maintainer and document - repository, hosting, domain, release, and recovery responsibilities. -- [ ] Provide an alternate private reporting route for concerns involving the - primary contact. Confirm private vulnerability reporting remains enabled. -- [x] Add `CODEOWNERS` when real area owners exist; do not create fictional ownership - or an approval requirement nobody can satisfy. -- [x] Document rights/provenance checks for historical games, submissions, assets, - and fixtures: source, permitted use, attribution, privacy review, creator requests, - and removal handling. Do not imply Apache-2.0 grants rights to original material. -- [x] Keep archive ownership separate and use independently created fixtures for - implementation tests. - -**Acceptance:** contributors know who decides and who can help; someone other than -the primary maintainer has an agreed recovery role; material is not imported merely -because it is technically accessible. - -**Practical work completed (2026-09-22 UTC):** [stewardship](community/stewardship.md) -records current responsibilities, material-intake checks, creator/removal handling, -and the separate archive boundary. Private vulnerability reporting was confirmed -enabled. No real area owners are assigned, so `CODEOWNERS` is not applicable yet. -The stale statement that rollback had never been rehearsed is corrected. - -**Deferred:** the first two items remain incomplete because no agreed backup -operator or independent private contact is recorded. Owner: `ostomachion`. -Trigger: before wider participation or an operational handoff, obtain a willing -delegate's agreement, record responsibilities and a private reporting route, -verify necessary repository/hosting/domain access, and rehearse recovery. A -read-only collaborator is not a substitute for an agreed role. Full continuity -acceptance is not claimed; local documentation links and whitespace were checked. - -## Final gate: open the project to wider participation - -- [ ] Phases 2 and 3 are complete, or genuinely inapplicable items have an explicit - explanation and owner for any future trigger. -- [ ] The scoped engine milestone from step 9 works with independently created data. -- [ ] A person unfamiliar with the repo successfully follows setup and completes a - small contribution through the actual review/check process. -- [ ] Public deployment has a rehearsed recovery path and community reporting works. - -Stop foundation work here. Do not add microservices, generic repositories, mediator -pipelines, event buses, Kubernetes, a large committee structure, or a coverage target -merely to look mature. Improve these foundations further when engine or community -work demonstrates a specific need. +This is a temporary implementation plan, to be deleted when the foundation work +is complete. Other repository files must not link to it or depend on its step +numbers. Put lasting procedures, decisions, support promises, and acceptance +evidence in the appropriate permanent guide or issue as each step is completed. + +The plan combines the audit of `1bdc1c4` with the community-infrastructure review +on 2026-09-22. The application is still an API/frontend shell; the first engine +milestone has not been selected. Recommendations below are planned changes, not +claims that the tools, settings, or support coverage already exist. + +Prefer one focused PR per step, splitting implementation from human or hosted +acceptance when necessary. Check off work only after its acceptance check passes. +Record a short result and evidence link, not a running history. An unchecked item +is planned work, not a claim that this documentation change implemented it. + +Step numbers are stable identifiers for work already in progress. Use this +delivery order; the first engine slice need not wait for the entire checklist: + +| When | Work | +| --- | --- | +| Now | Operational and documentation repairs (1-6), repeatable commands and formatting (13-14), AI tooling alignment (19), targeted security enforcement (20), maintained-deployment monitoring (22) | +| Before inviting wider contributions | Windows CI and browser smoke (15-16), useful starter work and contributor rehearsal (9-10), documented branching rules (18) | +| Alongside the first engine slice | Behavior and implementation (7-8), searchable/executable documentation (17), curated release notes (18) | +| When the stated need exists | Feature accessibility (11), release maintenance (12), richer engine checks (21), persistent-data recovery (22), distributed packages (23) | +| Finish | Preserve durable outcomes, transfer genuinely future work, and delete this file (24) | + +Do not leave this plan open indefinitely for hypothetical features. A future item +may be explicitly deferred to a durable issue with its owner, trigger, and +acceptance criteria; deferral is not implementation and must not be marked as a +passing check. Step 24 defines completion and deletion. + +## Baseline to preserve + +- Keep the Contracts, API Client, API, Web Client, ServiceDefaults, and local + AppHost boundaries. They separate real responsibilities without an engine framework. +- Keep central package/build settings, public-behavior tests, the shared validation + action, and the protected `build-test` merge gate. The audit confirmed no bypass + for that gate; the sole-maintainer review exception is separate. +- Keep immutable action/image references, pinned SSH trust, the non-root API, + versioned release identities, and separation of edge and application deployment. +- Keep the archive separate and use independently created implementation fixtures. + Preserve the existing source-material, privacy, and licensing policies. + +**Audit validation:** restore, format verification, Release build with no warnings, +72 .NET tests, all five release/deployment shell suites, frontend Release publish, +and the published-configuration guard passed. Relative Markdown file targets +resolved. A separate mocked first-deployment failure exposed the recovery defect +in step 1 despite those passing suites. The audit did not start services, deploy, +or establish fresh-editor, browser, or live-host acceptance. + +Use [development setup](setup/development.md) and [testing guidance](quality/testing.md) +for validation commands. Code changes need the normal solution gate and relevant +script checks; documentation-only changes need link, reference, and diff checks. +Workflow changes also need their actual hosted checks before hosted success is claimed. +Deployments, releases, credential handling, and destructive operations still need +explicit authorization under [AGENTS.md](../AGENTS.md). + +## Phase 1: Close operational gaps + +Address these before relying on fresh-host deployment or expanding deployment +capabilities. They do not require adding new hosting infrastructure. + +### 1. Recover a failed first deployment + +**Finding:** [activation](../scripts/deploy-app.sh) writes `pending` even without a +predecessor. Rollback requires `previous`, fails when it is absent, and leaves +subsequent activations blocked. Existing tests always seed a legacy installation. + +- [x] Add explicit transaction state for an initial deployment. Distinguish a + legitimately absent predecessor from a missing or corrupt expected predecessor. +- [x] On initial failure, stop any partially started candidate and restore a + defined undeployed state. Clear `pending` only after successful recovery; + preserve useful diagnostics. Do not just ignore a missing `previous` file. +- [x] Extend [deployment tests](../tests/deploy-app/run.sh) for first-start failure, + first-deployment smoke failure after activation, successful retry, and failed + cleanup. Preserve existing upgrade and rollback behavior. +- [x] Document initial-deployment recovery in [hosting](setup/hosting.md). + +**Acceptance:** mocked failure/recovery/retry scenarios pass without SSH or Docker +services. An expected predecessor disappearing still fails safely. A live staging +rehearsal, when authorized, is recorded separately from local regression evidence. + +**Result (2026-09-22):** The [mocked deployment suite](../tests/deploy-app/run.sh) +passes all ten scenario groups, covering initial failure/recovery/retry, failed +container and file cleanup, missing candidate files, and missing or corrupt +predecessor records and manifests. Restore, format verification, Release build +(zero warnings), and all 72 .NET tests passed. The +[recovery procedure](setup/hosting.md#application-activation-and-rollback) describes +the undeployed state, retained diagnostics, and retry. No live staging rehearsal +was performed for this step. + +### 2. Validate frontend packaging before merge + +**Finding:** PR CI builds the web project, but Release publishing and the existing +portability guard run only in [deployment](../.github/workflows/_deploy.yml). +Smoke JavaScript and its package are also absent from PR validation. + +- [x] Add frontend Release publish and [verify-web-publish.sh](../scripts/verify-web-publish.sh) + to PR validation, reusing existing checks and avoiding unnecessary duplicate work. +- [x] Validate the smoke package with `npm ci` and check `smoke.mjs` syntax before + deployment. Preserve the stable required `build-test` check and failure diagnostics. +- [x] Document local equivalents and distinguish build/package checks from browser + acceptance. Dependency installation already precedes activation; browser smoke + execution follows activation and can trigger recovery. + +**Acceptance:** CI rejects a broken publish or leaked environment-specific web +configuration before merge. Smoke dependency/syntax errors fail validation. +These checks require no deployment credentials or public deployment. + +**Result (2026-09-22):** [Shared validation](../.github/actions/validate/action.yml) +now publishes and checks the frontend in PR CI and validates the smoke package +in both callers. Deployment retains its required packaging job without an extra +publish. Restore, format verification, Release build (zero warnings), all 72 .NET +tests, all five script suites, Release publish, portability checks, and Node 22 +dependency/syntax checks passed locally. Nine injected packaging/configuration, +dependency, and syntax failures were rejected with their diagnostics retained. +[Local commands and browser boundaries](quality/testing.md#ci-and-deployment-validation) +are documented. The first hosted PR `build-test` run remains unverified; no +deployment or live browser acceptance was performed for this step. + +### 3. Maintain browser-smoke dependencies + +**Finding:** [Playwright is pinned](../tests/deploy-smoke/package.json), but +[Dependabot](../.github/dependabot.yml) has no npm entry for this package. + +- [ ] Add version updates for `/tests/deploy-smoke` using the existing update cadence. +- [ ] Extend the [supply-chain declaration check](../tests/supply-chain/run.sh) to + catch omission of this package without tying the check to a particular version. +- [ ] Validate the package/lockfile and review browser-smoke behavior when updating + Playwright; version-update configuration alone is not a browser acceptance test. + +**Acceptance:** the declaration check passes and Dependabot recognizes the npm +directory after merge. Dependency updates receive the checks from step 2. + +### 4. Correct operational documentation and unresolved host evidence + +**Finding:** [GitHub setup](setup/github.md) describes a separate unprivileged +smoke job and old timeouts; smoke now runs in the 45-minute deployment job with +`packages: write`, after SSH setup. Its opening unpublished-workflow snapshot and +[hosting's](setup/hosting.md) "before merging" instructions are also obsolete. + +- [ ] Describe the actual job, token, and on-disk credential boundaries. Review + whether the combined job is intentional; record that decision or make a focused + change with recovery coverage. Do not imply this audit demonstrated exploitation. +- [ ] State that production workflows must be dispatched from `main`, separately + from the protected application source `ref`, in [release guidance](release/README.md). +- [ ] Replace completed rollout instructions and conflicting verification diaries + with current procedures and concise evidence links. Keep unresolved host adoption + explicit rather than assuming merge means host configuration was applied. +- [ ] Carry forward administrator confirmation of the SSH host key's trusted + source and [host verification](setup/hosting.md#host-caddy-conflicts-and-read-only-verification) + of the running Caddy digest and competing native-service boot state. + +**Acceptance:** workflow code, instructions, and any inspected settings agree. +Every remaining host action names its owner and unverified state. Documentation +work does not perform a deployment; successful SSH use alone does not establish +how the original host key was authenticated. + +## Phase 2: Make the supported contributor path accurate + +### 5. Repair development instructions and launch leftovers + +**Finding:** [CONTRIBUTING](../CONTRIBUTING.md) promises commit-time formatting, +although Husky is opt-in. The web project's IIS Express profile advertises origins +not allowed by development CORS. Fresh-checkout editor acceptance remains open. + +- [ ] Say that format verification is required and hooks are optional; link the + authoritative setup commands instead of duplicating them. +- [ ] Remove the unsupported IIS Express profile, or explicitly support and test + it with matching configuration. Preserve the documented direct and Aspire paths. +- [ ] Correct the old `OpenGameBuilder.Web` startup title and scoped-CSS filename + hint in [index.html](../src/OpenGameBuilder.Web.Client/wwwroot/index.html). +- [ ] Follow the setup guide from a fresh checkout in Visual Studio and VS Code, + including F5, debugger attachment, frontend startup, and the API request. + +**Acceptance:** the solution gate passes after configuration changes. Both +documented editor paths work without undocumented steps, Docker, or production +credentials; record actual editor verification separately from CLI results. + +### 6. Remove unused scaffolding and duplicate documentation + +**Finding:** root placeholders, unused configuration, template examples, and +repeated contributor prose still create maintenance work without current benefit. + +- [ ] Remove or give a useful contributors pointer to `CONTRIBUTORS.md`. Replace + the empty `CHANGELOG.md` with the curated release process in step 18. +- [ ] Remove `.browserslistrc` while it has no consumer and update its references. + Keep the actual [browser policy and acceptance matrix](frontend/browser-support.md). +- [ ] Remove unused gRPC/Azure/service-discovery examples, unused Bootstrap/form + CSS, and stale template hints. Shorten AppHost history while retaining the + explanation of its current fixed-port, CORS, and launch-profile constraints. +- [ ] Consolidate significant-change guidance in CONTRIBUTING. Keep SUPPORT + focused on choosing a contact route; link policies and forms instead of repeating + them. Retain short source-material/privacy reminders at submission points. +- [ ] Replace local reunion rosters and unowned forum-reconstruction plans with + useful pointers to the owning archive/community location. Preserve substantive + material until an appropriate destination is agreed. +- [ ] Finish the reviewed shared-resource move tracked by [issue #56](https://github.com/OpenGameBuilder/opengamebuilder/issues/56) + and [organization PR #1](https://github.com/OpenGameBuilder/.github/pull/1), then + replace the temporary shared-content pointer. Recheck their status first. + +**Acceptance:** no empty promises or unused declarations remain in scope, and all +affected local paths/anchors resolve. Code/style removal preserves existing +behavior. Record cross-repository completion separately from local cleanup. + +## Phase 3: Deliver the first engine slice + +Begin this once the supported development path is usable. Do not wait for optional +documentation cleanup, new hosting capabilities, or additional governance machinery. + +### 7. Choose the first engine milestone + +**Finding:** the project has useful application boundaries but no selected engine +behavior, independent fixture, or acceptance contract. + +- [ ] Choose one observable behavior and explicit non-goals; decide whether the + slice concerns playback, import, or another narrowly defined capability. +- [ ] Write a small independently created fixture with provenance and expected + outputs, including relevant invalid-input behavior. Use no decompiled source. +- [ ] Record the contract in a short public issue or engine document and update + the README's next milestone. Link the existing repository map rather than + creating another architecture inventory. + +**Acceptance:** another contributor can understand what to implement and how to +judge it without designing the whole engine, renderer, storage layer, or editor. + +### 8. Implement and test that slice + +**Depends on:** step 7's accepted behavior and fixture. + +- [ ] Implement the selected behavior in a plain library testable without Blazor, + HTTP, or a database. Introduce rendering/input/time/randomness seams only when + the implemented behavior needs them. +- [ ] Test the agreed observable results and relevant failure cases. Verify + deterministic behavior where the contract requires it. +- [ ] Add the project/tests to normal validation and document the runnable example + and remaining limits. Use the independently authored fixture as a small reference + scene or game and a checked documentation example (step 17). Do not infer broad + original-game compatibility from it. + +**Acceptance:** the fixture works through the normal test gate and demonstrates +the agreed behavior. No empty generic engine framework or speculative services +are prerequisites. Further foundation work responds to actual engine needs. + +## Phase 4: Prepare for wider participation + +### 9. Publish an actionable contributor backlog + +**Finding:** the audit found no open `good first issue` or `help wanted` tasks. +The open database, blob-storage, API-subdomain, and Minimal API proposals describe +large additions or alternatives rather than bounded onboarding work. + +- [ ] Triage [database #37](https://github.com/OpenGameBuilder/opengamebuilder/issues/37), + [blob storage #36](https://github.com/OpenGameBuilder/opengamebuilder/issues/36), + [API subdomains #46](https://github.com/OpenGameBuilder/opengamebuilder/issues/46), + and [Minimal APIs #38](https://github.com/OpenGameBuilder/opengamebuilder/issues/38). + Record decisions or concrete deferral triggers; do not present speculative + infrastructure or a controller rewrite as required engine work. +- [ ] Prepare three to five small tasks with acceptance criteria, likely files, + validation commands, and a willing maintainer contact. Apply the appropriate + newcomer/help labels after checking the tasks are actually approachable. +- [ ] Include non-code work: setup verification, independent behavior examples, + documentation, or accessibility checks for implemented features. Keep consequential + decisions public and align README/contribution links with the real queue. +- [ ] Verify native Project auto-add/status workflows handle agreed issue/PR + bookkeeping. Keep triage human-owned; do not close valid reports merely for + inactivity or add bots that generate unreviewed issues and comments. + +**Acceptance:** a newcomer can select useful work without first designing a major +subsystem. Preparing task text alone does not count as publishing a usable backlog. + +### 10. Establish continuity and rehearse a contribution + +**Finding:** [stewardship](community/stewardship.md) records no agreed backup +operator or independent private-reporting contact. Written procedures do not +establish another person's access or a newcomer's successful experience. + +- [ ] Before broader participation or operational handoff, the primary maintainer + obtains a willing delegate's agreement on repository, hosting, domain, release, + and recovery responsibilities; verify necessary access and rehearse recovery. +- [ ] Establish an independent private route for concerns involving the primary + contact. Keep sensitive account-recovery details private. +- [ ] Add CODEOWNERS only when actual area owners accept responsibility. Revisit + the sole-maintainer review exception when a second trusted reviewer can routinely + review maintainer PRs; preserve the no-bypass CI gate. +- [ ] Have someone unfamiliar with the repo follow setup and complete a small + contribution through the actual review/check process; fix the friction found. + +**Acceptance:** agreed people and contact routes are recorded, a backup can perform +the agreed recovery, and an independent contributor completes the workflow. +Owner until delegation: `ostomachion`. This remains open until people and access +are available; no additional committee or policy framework is needed. + +## Work tied to a specific trigger + +### 11. Complete frontend failure handling and accessibility + +**Trigger:** the first functional frontend feature. The placeholder catches HTTP +errors but lets malformed/null-response exceptions escape; loading, recovery, +focus, and error controls also have [documented gaps](frontend/browser-support.md). + +- [ ] Define loading, success, expected transport/timeout failures, invalid + responses, and recovery. Handle known failures without broadly hiding defects; + retain useful diagnostics without sensitive response data. +- [ ] Add meaningful component coverage for those states. Extend the existing + published-app PR harness from step 16, sharing applicable assertions with the + deployment smoke test. Keep component, browser, and live-deployment evidence + separate; this feature-triggered work does not postpone the basic PR harness. +- [ ] Fix the heading/focus mismatch, loading/error announcements, and keyboard + semantics of error controls. Verify the implemented workflow against the browser, + keyboard, assistive-technology, and mobile matrix; record unverified targets. +- [ ] Add axe-based checks to meaningful rendered states, including failures and + dialogs when present. Perform manual keyboard, focus, zoom, screen-reader, and + touch checks; automated accessibility results do not establish conformance. + +**Acceptance:** a wrong API URL or unusable frontend fails the browser check even +with healthy API liveness. Expected failures offer accessible recovery, and exact +browser/manual acceptance evidence accompanies the feature. + +### 12. Reassess release maintenance only when needed + +**Trigger:** supporting an older released version, handing off operations, or +release accumulation creating measurable operational cost. Owner: `ostomachion`. + +- [ ] Decide whether the current standard/patch/tag/merge-back automation earns + its maintenance cost. Retaining it with a concrete support need is a valid + decision; simplify only with one documented replacement and regression coverage. +- [ ] Before removing legacy in-place migration code, verify that every supported + installation has migrated. Do not infer host state from merged code. +- [ ] Define release retention/cleanup when needed, preserving active/rollback + artifacts and assets needed by older browser sessions. No speculative cleanup job. +- [ ] Consider a merge queue when concurrent merges cause repeated update/retest + work. Add `merge_group` handling to required workflows and verify the gate before + enabling it; a queue is not needed to establish the branching policy in step 18. + +**Acceptance:** a recorded keep/simplify decision answers an actual need. Any +change preserves artifact identity, recovery, authorization, and required checks. +This step does not block engine development or justify further deployment expansion. + +## Additional contributor and maintenance infrastructure + +### 13. Make setup and validation reproducible + +- [ ] Add a small `doctor` command that reports selected SDK/tool versions, + missing prerequisites, and actionable remedies. Keep installation and trust + changes explicit; ordinary checks must not change the developer's environment. +- [ ] Provide documented formatting, quick-check, full-check, and browser-test + commands, with shared implementations used locally and by CI. Keep the headless + build/test path usable without deployment credentials or running services. +- [ ] Replace accidental SDK drift from `global.json`'s `latestMinor` roll-forward + with a deliberate supported baseline for formatting, analyzers, and builds. + Log the selected SDK and update it through reviewed dependency changes. +- [ ] Introduce committed NuGet lockfiles for application entry points and locked + CI restores after SDK selection is settled. Keep npm tools exactly pinned with + committed lockfiles. Document how intentional dependency updates refresh them; + a library lockfile does not constrain downstream consumers. + +**Acceptance:** a clean checkout runs the documented commands with the declared +toolchain. Missing prerequisites produce useful diagnostics, CI rejects dependency +drift, and local/CI checks have equivalent scope. A fresh editor rehearsal remains +the separate acceptance in step 5. See [NuGet lockfiles](https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#locking-dependencies). + +### 14. Format and lint first-party content consistently + +- [ ] Keep `dotnet format` and existing analyzers for C#. Promote selected useful + style rules to enforced warnings rather than enabling a large rule set wholesale. +- [ ] Add exactly pinned Prettier for Markdown, JSON, YAML, CSS, and JavaScript; + choose explicit indentation/prose wrapping consistent with scoped EditorConfig + settings. Use markdownlint-cli2's Prettier-compatible preset for structural rules. +- [ ] Add actionlint for workflows, ShellCheck for shell defects, and shfmt for + shell formatting. Use versions compatible with the workflow syntax in this repo. +- [ ] Add pinned lychee checks for local file/image links and anchors, including + root and first-party GitHub documents. Check generated HTML when step 17 lands. + Schedule bounded external-link reports separately; remote outages must not block + unrelated PRs. Keep exclusions narrow and explained. +- [ ] Share configurations between editor, optional hooks, command line, and CI. + Exclude generated artifacts, dependencies, and vendored skills; avoid competing + formatters for a file type. Make the initial formatting sweep a separate PR. + +**Acceptance:** deliberate formatting, Markdown-structure, missing-target/anchor, +workflow, and shell defects fail their appropriate checks with clear fixes. Clean +files pass on Windows and Linux. CI enforces the rules without requiring hooks. +Defer aggressive prose-style linting unless a recurring problem warrants it. +Sources: [Prettier](https://prettier.io/docs/install), +[markdownlint compatibility](https://github.com/DavidAnson/markdownlint/blob/main/doc/Prettier.md), +[actionlint](https://github.com/rhysd/actionlint/blob/main/docs/checks.md), +[ShellCheck](https://github.com/koalaman/shellcheck), [shfmt](https://github.com/mvdan/sh), +[lychee](https://lychee.cli.rs/guides/cli/). + +### 15. Validate the supported platform and keep CI understandable + +- [ ] Add a Windows restore/format/Release-build/test lane alongside Linux. Keep + Linux shell/container checks in their suitable environment; do not multiply + every job across an unnecessary OS matrix. +- [ ] Use the commands from step 13, with focused document checks for document + changes and full relevant checks for application/workflow changes. Path-based + selection must not silently omit required validation or strand required checks. +- [ ] Preserve the stable `build-test` gate. If it becomes an aggregate, explicitly + verify every required dependency's result, including failure/cancellation cases. + Retain actionable logs, reports, selected versions, and bounded artifact retention. + +**Acceptance:** real hosted Windows and Linux runs pass, a failed required lane +prevents merging, and a documentation-only PR receives its intended checks. +Record local results separately; Windows CI does not establish F5/debugger support. + +### 16. Exercise the published application in PRs + +**Depends on:** step 2's packaging checks; this adds browser behavior, not another +claim that syntax or publication proves rendering. + +- [ ] Add an `@playwright/test` harness that serves the Release-published frontend + and real API locally with the intended same-origin API path and release base path. + Reuse appropriate deployment assertions; require no SSH, registry write access, + production secrets, or public deployment. +- [ ] Run Chromium, Firefox, and WebKit checks for startup, API-backed content, + expected revision, routes/reload, assets, and unhandled page errors. Manage local + server lifecycle in the harness and start with a small predictable worker count. +- [ ] When an interactive feature exists, exercise a meaningful user action and + assert its visible result. Add this with the feature rather than inventing an + interaction for the current placeholder or postponing the startup/API harness. +- [ ] Retain failure screenshots, traces, and a readable report. Forbid focused + tests; bound retries and surface flaky passes rather than treating retries as + proof of health. Keep deployment retry policy separate from PR test policy. +- [ ] Revise the permanent browser policy to distinguish checked current-stable + targets from aspirational previous-version/device coverage. Record exact browser + and OS versions; Playwright engines, branded browsers, emulation, and physical + Safari/iOS checks are different evidence. Promise only coverage that is performed. + +**Acceptance:** wrong API routing, a broken release base path, missing assets, or +an unusable frontend fails PR validation even when API liveness is healthy. Three +engine results are recorded; live hosting and manual accessibility remain separate. +Sources: [Playwright servers](https://playwright.dev/docs/test-webserver), +[browser coverage](https://playwright.dev/docs/browsers), +[accessibility checks](https://playwright.dev/docs/accessibility-testing). + +### 17. Publish searchable documentation and checked examples + +- [ ] Build a DocFX site with its modern template from the existing Markdown. + Keep one source copy per document, generated HTML untracked, and concise + navigation for setup, contribution, architecture, testing, and operations. + Exclude this temporary plan from site content and navigation. +- [ ] Add search and edit links, a PR site build, and checks of rendered local + links/anchors. Use appropriate failing diagnostic severities for broken content. + Keep external-link maintenance separate as described in step 14. +- [ ] Configure publication from validated protected content when authorized, + and verify the actual hosted navigation, assets, search, and links. Do not treat + a successful local site build as publication acceptance. +- [ ] Make the engine example from step 8 compile/run in CI and reuse its checked + content in tutorials. Add filtered public .NET API reference when meaningful + engine APIs exist; keep HTTP OpenAPI/Scalar documentation in its appropriate role. + +**Acceptance:** contributors can find supported setup and an executable example +without maintaining a separate wiki or copied guides. Site build/link failures are +actionable, and hosted acceptance is recorded separately. Custom branding and API +generation do not block the first engine slice. +Sources: [DocFX template](https://dotnet.github.io/docfx/docs/template.html), +[.NET reference](https://dotnet.github.io/docfx/docs/dotnet-api-docs.html). + +### 18. Clarify branches and make release notes useful + +- [ ] Document protected `main`, short-lived branches, draft PRs, squash merges, + and deletion of merged branches in permanent contribution/release guidance. + Reserve `patch/vX.Y.Z` for the existing released-hotfix path and its merge-back. + Do not introduce a permanent `develop` branch without a demonstrated need. +- [ ] Make `CHANGELOG.md` the canonical curated account of notable changes. + Prepare a reviewed entry before tagging, covering observable changes, breaking + behavior, and migration guidance; omit mechanical maintenance noise. +- [ ] Feed the selected version's entry into the existing production release + workflow after deployment validation. Use its existing generated PR notes as + drafting material or supplemental references/credits, with `.github/release.yml` + categories based on actual PR labels. Avoid two separately maintained summaries. +- [ ] Keep one owner of version numbers, tags, and publication. Do not bolt + Release Please or semantic-release onto the current deploy/smoke/tag contract. + Require descriptive PR titles; enforce commit grammar only if a chosen workflow + actually uses it. Preserve idempotency and patch-flow regression coverage. + +**Acceptance:** a contributor can select a branch/PR path, and a rehearsed release +selects the correct reviewed changelog entry without changing tag timing or creating +duplicate releases. Actual publication still requires release authorization. +Sources: [GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow), +[Common Changelog](https://common-changelog.org/), +[generated notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes). + +### 19. Align AI tooling and verify that it helps + +**Finding (2026-09-22):** documented Aspire CLI and AppHost SDK are 13.4.2 while +the hosting package is 13.5.4. The vendored Aspire skills describe 13.4, and the +dotnet-inspect guide derives from 0.5.0 without pinning the executable. Verified +upstream candidates are [Aspire 13.5.4](https://github.com/microsoft/aspire/releases/tag/v13.5.4), +[skills 0.0.2](https://github.com/microsoft/aspire-skills/releases/tag/v0.0.2) +(describing 13.5.3), and [dotnet-inspect 0.25.0](https://github.com/richlander/dotnet-inspect/releases/tag/v0.25.0). +These are update candidates, not proven compatible upgrades; recheck at execution. + +- [ ] Update the compatible CLI, AppHost SDK, skill bundle, and documented + commands together using [AI tooling maintenance](setup/ai-tooling.md). Preserve + source pins, licenses, checksums, local-only scope, and Compose production. + Review newly supplied hook/extension assets separately rather than enabling them. +- [ ] Replace the long dotnet-inspect reference with the upstream entry point + that retrieves its installed tool's version-matched guide. Decide and document + how the optional executable is pinned/updated without making AI mandatory. +- [ ] Keep AGENTS.md authoritative and concise; add client-specific adapters only + for supported clients that need them. Verify actual instruction/MCP discovery; + do not assume a configuration filename works in every client. +- [ ] Add a read-only tool/version/provenance check and an owned review cadence + for pins not covered by Dependabot. Changes arrive as bounded reviewed updates, + not unpinned regeneration during build or routine agent work. +- [ ] Rehearse two or three representative tasks after significant tooling + changes, checking commands, repository boundaries, reviewability, and truthful + evidence. Use a small manual checklist, not an agent-evaluation service. + +**Acceptance:** source checks and the normal gate pass, Windows Aspire startup +and MCP discovery work on the selected toolchain, and supported clients load the +intended guidance. Record runtime evidence separately from text provenance. +Defer extra MCP servers, cloud environments, agent fleets, and repository plugins +until a recurring workflow justifies them. Enforce permissions outside prompts too. +Sources: [Codex instructions](https://learn.chatgpt.com/docs/agent-configuration/agents-md), +[Copilot support](https://docs.github.com/en/copilot/reference/custom-instructions-support). + +### 20. Enforce the remaining supply-chain boundaries + +**Finding:** secret scanning, push protection, private vulnerability reporting, +and dependency security updates are already enabled. Full-SHA action references +exist, but the GitHub setting requiring them was disabled at the audit. + +- [ ] Enable the repository's full-SHA action-pinning requirement and verify it + rejects an unpinned external action before execution. Keep declaration checks + as complementary coverage; review reusable-workflow policy separately. +- [ ] Add PR dependency review with an explicit severity/triage policy. Cover npm + updates from step 3 and newly added tooling alongside existing NuGet updates; + check that resolved/transitive dependencies are visible to the chosen checks. +- [ ] Keep fork-PR validation credential-free and read-only. Review privileged + workflow boundaries so untrusted PR code/artifacts are not executed with write + tokens or deployment credentials. Preserve protected-source deployment checks. +- [ ] Scan the actual runtime container, including OS packages, with a pinned + scanner and an owned triage policy. Explain narrow exceptions and their review + date; avoid hiding old findings behind an unexplained permanent baseline. + +**Acceptance:** controlled policy/dependency/scan failures produce actionable +results and block the intended path. Record actual GitHub setting and hosted PR +behavior separately from local configuration tests. Extra scanners must cover a +real gap rather than duplicate existing alerts. +Sources: [Actions controls](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository), +[dependency review](https://docs.github.com/en/code-security/concepts/supply-chain-security/dependency-review). + +## Later work with concrete adoption triggers + +### 21. Grow engine evidence with implemented behavior + +**Trigger:** replay, serialization, editing, import, or performance-sensitive +behavior exists. The initial fixture and runnable example belong to steps 7-8. + +- [ ] Record fixture authorship, source, permitted use, and expected behavior. + Distinguish independently observed compatibility behavior from assumptions; + keep decompiled source and unapproved/private material out of implementation inputs. +- [ ] Make reproducible bug reports possible with engine version, safe minimal + fixture, input sequence, expected result, and seed where relevant. Review/redact + diagnostic material before sharing; do not require private archived games. +- [ ] Add property tests for actual invariants such as serialization round trips, + deterministic replay, and edit/undo. Use focused behavioral coverage rather than + an arbitrary repository-wide coverage percentage. +- [ ] Fuzz importers and malformed inputs when an import format exists. Enforce + concrete file-size, decompression, path, and execution/resource limits at the + relevant boundary and retain minimized failing inputs as safe regressions. +- [ ] Establish small repeatable performance baselines when engine workloads + justify them. Record environment and tolerances before gating regressions; avoid + broad benchmark infrastructure or speculative architecture-test frameworks. + +**Acceptance:** each added check proves an implemented contract, finds a deliberate +violation, and produces a reproducible diagnostic. Document limitations and source +provenance in permanent engine/testing guidance; never infer broad compatibility. + +### 22. Make operations and recurring automation actionable + +**Trigger:** a maintained public deployment exists, as it does now. Monitoring +is current work; the separate backup/restore work begins before persistent +user/project data is relied upon. + +- [ ] Add external uptime/API, certificate-expiry, and host-capacity monitoring + with a named recipient and a permanent response procedure. Alert on sustained + failure and recovery; do not rely solely on GitHub scheduled jobs for uptime. +- [ ] Give every recurring automation an owner, purpose, cadence/cost or retention + limit, and expected failure action. Prefer bounded maintenance reports or PRs; + avoid unattended activity that exceeds available review capacity. +- [ ] When persistent data arrives, document backup scope, retention, recovery + objectives, and ownership, then rehearse restoration in an isolated environment. + A successful backup job alone does not establish recoverability. + +**Acceptance:** a controlled failure reaches the intended operator and the runbook +leads to a verified response. Data recovery has measured restore evidence when +applicable. Setup, credentials, and operational rehearsals retain their normal +authorization requirements; procedures and evidence live in permanent guides. + +### 23. Verify packages when distributing them + +**Trigger:** engine packages, binaries, or other downloadable release artifacts +are offered to consumers beyond the in-repository application. + +- [ ] Install the produced package into a separate consumer project and exercise + its public example. Check dependencies, metadata, license notices, and debugging + information; successful in-solution project references are insufficient. +- [ ] Attach checksums, an SBOM, and provenance attestations to the exact released + artifacts, and implement/document verification by consumers or deployment. + Generated attestations do not themselves establish correctness. +- [ ] Evaluate immutable releases. Prepare a draft, attach all intended assets, + and publish through the single release owner; verify the policy for assets as + well as tags and preserve the existing release authorization boundary. + +**Acceptance:** an independent consumer uses the exact package, verifies its +identity/provenance, and can trace it to source and validation. Release artifacts +and permanent distribution instructions agree. Defer until distribution exists. +Sources: [attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations), +[immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases). + +## Completion and removal + +### 24. Retire this temporary plan + +- [ ] Confirm required near-term work has passed its acceptance checks. Resolve + pending hosted/manual evidence explicitly; do not equate local tests with it. +- [ ] Move genuinely future, trigger-dependent work to durable issues with an + owner, trigger, and acceptance criteria, or record a reasoned decision not to + pursue it. Do not mark deferred work as implemented. +- [ ] Ensure permanent setup, testing, browser, AI, release, and hosting guides + describe current behavior. Preserve useful decisions, source records, and + historical evidence there; ordinary readers must never need this plan. +- [ ] Verify other tracked files, site navigation, and generated documentation + inputs contain no references to this file or its step numbers. Keep this rule + throughout implementation, not just at deletion time. +- [ ] Delete this file in the completion PR and rerun the applicable documentation + build/link/reference checks. The Git history retains the completed plan. + +**Acceptance:** deleting this file loses no operational instructions, support +contract, accepted decision, meaningful evidence, or actionable remaining work, +and introduces no broken links. Historical rollout/rollback records belong in +[hosting evidence](setup/hosting.md#recorded-deployment-evidence). diff --git a/docs/frontend/browser-support.md b/docs/frontend/browser-support.md index d4eb6e1..fb103e3 100644 --- a/docs/frontend/browser-support.md +++ b/docs/frontend/browser-support.md @@ -3,7 +3,7 @@ The current frontend is a Blazor WebAssembly placeholder that displays application information from the API. There is no editor yet. This policy separates recorded checks from the browser and accessibility targets to verify with the first -functional frontend feature, alongside [foundation section 6](../foundation-checklist.md#6-establish-frontend-failure-handling-and-a-smoke-test). +functional frontend feature. ## Recorded browser check @@ -63,7 +63,7 @@ These are implementation observations, not a runtime accessibility pass: region. Its unhandled-error UI has a reload link, but its dismiss control is a `span` without keyboard or button semantics in the markup. -Resolve these gaps with the first functional frontend feature under section 6. +Resolve these gaps with the first functional frontend feature. There is currently no recorded keyboard, focus, or screen-reader acceptance, and no claim of accessibility conformance. diff --git a/docs/quality/testing.md b/docs/quality/testing.md index 2c6bb5a..ae19458 100644 --- a/docs/quality/testing.md +++ b/docs/quality/testing.md @@ -15,8 +15,7 @@ See [developer setup](../setup/development.md) for SDK prerequisites. The existing deployment smoke below checks published frontend startup and a real API round trip in Chromium. Component tests and broader browser coverage for loading, success, network failure, and invalid responses accompany the first -functional frontend feature, as scoped in section 6 of the -[foundation checklist](../foundation-checklist.md). See the +functional frontend feature. See the [browser and accessibility matrix](../frontend/browser-support.md) for recorded evidence, unverified targets, and the manual checks required as the editor develops. @@ -54,14 +53,39 @@ analysis or claiming this repository change fixed that managed workflow. See [CI](../../.github/workflows/ci.yml) and [deployment validation](../../.github/workflows/_deploy.yml) use the same [validation action](../../.github/actions/validate/action.yml), so restore, -formatting verification, Release build, and solution tests cannot drift between -the two paths. The local equivalents are in +formatting verification, Release build, solution tests, and smoke-package checks +cannot drift between the two paths. The solution commands are in [developer setup](../setup/development.md#command-line-workflow-start-here). +PR validation also publishes the frontend in Release using the completed build +and runs [the portability guard](../../scripts/verify-web-publish.sh). Deployment +disables this extra publish in the shared action because its required `package` +job already publishes and checks the frontend before any deployment job runs. +Both paths reject environment-specific configuration in the published artifact. + +After the solution gate, run these local equivalents from the repository root +with Git Bash and Node.js 22/npm available: + +```pwsh +dotnet publish src/OpenGameBuilder.Web.Client/OpenGameBuilder.Web.Client.csproj --configuration Release --no-build --output artifacts/web +& 'C:\Program Files\Git\bin\bash.exe' scripts/verify-web-publish.sh artifacts/web/wwwroot +npm ci --prefix tests/deploy-smoke +node --check tests/deploy-smoke/smoke.mjs +``` + +The Node checks install the locked smoke dependencies and parse `smoke.mjs`; +they do not install or launch a browser. These build/package checks need no +deployment credentials or public URL and do not establish browser acceptance. +The deployment job still installs dependencies and Chromium on its own runner +before activation. Live browser smoke runs after activation and can trigger +recovery if it fails. + A failing phase fails the job. Available console logs and a formatting report are uploaded on failure and retained for seven days; assertion details are in -`test.log`. The test command uses the repository's Microsoft.Testing.Platform -runner without adding a separate test-reporting dependency. +`test.log`. The shared action also captures `web-publish.log`, +`web-configuration.log`, `smoke-dependencies.log`, and `smoke-syntax.log` for +the new checks when they run. The test command uses the repository's +Microsoft.Testing.Platform runner without adding a separate test-reporting dependency. Release-script behavior has an additional Bash test gate: @@ -110,6 +134,10 @@ Application activation and rollback have an isolated host-script test: It uses temporary releases and a mocked Docker command to check legacy migration, asset retention, rollback, API startup failure, and archive rejection. +First-deployment cases cover partial startup failure, recovery after activation +(the browser-smoke failure boundary), successful retry, cleanup failure, and +missing or corrupt expected predecessor state. These checks run without SSH or +Docker services; they do not establish live browser or staging acceptance. Deployment additionally runs a Chromium smoke test from `tests/deploy-smoke` that loads the published frontend, observes its API request, and checks the expected source revision. That live test requires a deployed staging or production URL. diff --git a/docs/release/README.md b/docs/release/README.md index fdad51f..9998091 100644 --- a/docs/release/README.md +++ b/docs/release/README.md @@ -29,8 +29,8 @@ manifest and workflow artifact when identifying an installed release. The host's [hosting setup](../setup/hosting.md#application-activation-and-rollback). The browser smoke check follows the activated page, observes its `/api/about` call, and compares the running API's source revision with the selected protected -commit. The section 14 staging rollback rehearsal passed on 2026-09-20; the -[checklist records the live evidence](../foundation-checklist.md#14-make-rollout-atomic-and-rollback-explicit). +commit. The staging rollback rehearsal passed on 2026-09-20; the +[hosting guide records the deployment evidence](../setup/hosting.md#recorded-deployment-evidence). ## Standard release (X.Y.0) diff --git a/docs/setup/github.md b/docs/setup/github.md index 242ce12..c96318f 100644 --- a/docs/setup/github.md +++ b/docs/setup/github.md @@ -287,8 +287,8 @@ PR failed, keep that source branch at the same commit and follow the The bot may create the tag, Release, and follow-up PR after deployment; it cannot approve its own PR, bypass CI, move a release tag, or recover the server. There is no agreed backup operator; see [practical stewardship](../community/stewardship.md). -Artifact rollback was rehearsed in staging as recorded in -[foundation section 14](../foundation-checklist.md#14-make-rollout-atomic-and-rollback-explicit). +Artifact rollback was rehearsed in staging; the +[hosting guide records the evidence](hosting.md#recorded-deployment-evidence). That evidence does not establish a second operator's access or recovery readiness. ## Acceptance evidence @@ -376,7 +376,7 @@ PR #84 was closed without merging and both refs created by this run were deleted Only the exact verification patch ref was temporarily excluded for deletion; read-back confirmed the exclusion was removed and the gate has no bypass actors. The successful creation/PR operation, enforced review/check requirements, and -unchanged tag-immutability rules complete section 7's bot acceptance. No human +unchanged tag-immutability rules complete the release-bot acceptance check. No human approval was fabricated and no deployment, tag change, or release was performed. ## Administrator verification @@ -429,7 +429,7 @@ Then perform a controlled acceptance check without deploying: details; do not create, move, or delete a real release tag merely to test rules. Rehearse actual release/tag operations only during an authorized release. 6. Record PR URLs, head SHAs, check-run URLs, merge-blocking evidence, and the - authenticated ruleset/bypass inventory here before completing section 7. + authenticated ruleset/bypass inventory here before claiming merge-gate acceptance. Close temporary PRs; do not merge them just to exercise the gate. See GitHub's [available rules](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets) diff --git a/docs/setup/hosting.md b/docs/setup/hosting.md index 37caf27..6e44996 100644 --- a/docs/setup/hosting.md +++ b/docs/setup/hosting.md @@ -227,11 +227,12 @@ maps the concrete `index.html` URL to the home page. Loaded pages continue to request their own release's assets, and the prior web directory remains available. The first deployment with this layout copies the old in-place web files into a `legacy-*` release and retains its original root -assets when a prior `release-manifest.txt` exists. A first deployment without -that manifest records `previous release: none`; it has no managed rollback -target until a subsequent successful release retains this one. Do not infer -rollback readiness from a successful first release or create a release solely -to manufacture a predecessor. +assets when a prior `release-manifest.txt` exists. For an initial deployment, +there is no current or previous release, root manifest, or root index. The +candidate's `releases//predecessor` records `none`; an upgrade records +the exact `releases/` path instead. This record is written before +the `pending` marker and before starting the API. Recovery never interprets a +missing `previous` file as evidence of an initial deployment. `scripts/deploy-app.sh` checks the archive checksum, stages the new files, records the previous release, pulls and starts the API by its digest reference, @@ -240,8 +241,28 @@ the root page. The deploy job uses a browser to follow that page, observe the frontend's `/api/about` request, and compare the API's source revision with the protected commit being deployed. It also checks that the page renders the returned application name and version. A failed activation or browser check -invokes `rollback` and restarts the recorded previous API image without rebuilding -it. A successful browser check clears the pending marker with `finalize`. +invokes `rollback`. For an upgrade, it validates the previous release against the +candidate's predecessor record and restarts that API image without rebuilding it. +A missing or corrupt expected predecessor fails recovery and retains `pending` +for operator investigation. A successful browser check clears the pending marker +with `finalize`. + +For a pending initial deployment, rollback captures available container logs in +`releases//recovery.log`, then uses the candidate's Compose files to +stop and remove its API container, including one partially started by a failed +activation. It removes the root index and compression sidecars, `current`, and +the root release manifest. This is the defined undeployed state: no running +application API or active root page. The shared edge remains in place. Incoming +files, candidate metadata, recovery logs, and versioned web assets are retained; +retained assets may still be reached directly by their release URLs. + +Recovery clears `pending` only after cleanup succeeds. If stopping the API or +removing active files fails, inspect the error and recovery log, correct the +cause, and repeat rollback for the same pending release. New activations remain +blocked until recovery succeeds. Retry deployment with a new release ID (a new +workflow run or attempt), since failed candidates are retained. After a first +release is finalized, it has no previous release to restore; a later successful +upgrade establishes that rollback target. For a controlled staging rehearsal after a successful normal rollout, dispatch **CD Staging** from `main` with **rehearse-rollback** enabled. It first passes @@ -250,29 +271,32 @@ must restore `previous`. This run is expected to be red, so inspect the recovery step and independently verify the public page and `/api/about` afterward. Leave the input off for normal staging updates. -Inspect `current`, `previous`, `pending`, and the manifests before manual -recovery. From a deployment checkout with SSH access, the same -rollback command is: +Inspect `current`, `previous`, `pending`, the pending candidate's `predecessor`, +and the manifests before manual recovery. From a deployment checkout with SSH +access, recover a pending activation with its exact release ID: ```bash ssh -i @ \ - bash -s -- rollback /srv/opengamebuilder/staging < scripts/deploy-app.sh + bash -s -- rollback /srv/opengamebuilder/staging < scripts/deploy-app.sh ``` -Use `production` in place of `staging` for production. The rollback script reads -the previous manifest and Compose file, restarts its cached image by digest, -restores its index, and updates `current`. Verify the public page and `/api/about` after it -returns; inspect Docker and Caddy logs if it cannot restart the old image. Do not -delete a release directory while old browser sessions may still request its -assets. Coordinate manual recovery with the environment's deployment queue. +Use `production` in place of `staging` for production. Without a pending +activation, omit the release ID to restore the retained previous release. For +an upgrade rollback, verify the public page and `/api/about` after it returns; +for initial-deployment recovery, verify the API is stopped and the active root +files are absent. If predecessor state is missing or damaged, investigate and +repair it from verified release records; do not delete `pending` or invent `none` +to bypass recovery. Do not delete a release directory while old browser sessions +may still request its assets. Coordinate manual recovery with the environment's +deployment queue. The Compose service is replaced before the root web redirect switches. During that short interval, an already loaded page can call the new API, so API changes must remain compatible with the retained frontend until its clients have aged out. This mechanism provides an atomic web switch and a recoverable pair; it is -not a zero-downtime atomic swap of the API and web processes. The section 14 +not a zero-downtime atomic swap of the API and web processes. The staging failure and rollback rehearsal passed on 2026-09-20; see the -[checklist evidence](../foundation-checklist.md#14-make-rollout-atomic-and-rollback-explicit). +[recorded deployment evidence](#recorded-deployment-evidence). To recover an edge change, fix the candidate on `main` and dispatch **CD Edge** for the affected host again. For an urgent host-side recovery, use the last known-good edge @@ -280,3 +304,20 @@ files retained in the host's `.rollback.*` directory after a failed activation, validate them with `caddy validate`, and reload Caddy. Do not restart or recreate Caddy for a Caddyfile-only correction. Coordinate host-side changes with any active edge workflow so its next write does not overwrite the repair. + +## Recorded deployment evidence + +The [successful staging rollout](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35542650898) +and [2026-09-20 rollback rehearsal](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35542855238) +record activation and recovery of an existing installation. The rehearsal +deliberately failed after browser acceptance and restored the prior release +without rebuilding; its red status is intentional. It did not exercise recovery +of a failed initial deployment with no predecessor. That recovery path has +local mocked regression coverage, but no live rehearsal is recorded here. + +The [production v0.10.0 release](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35674772060) +and [subsequent staging rollout](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35675216560) +record later application acceptance. These historical runs do not establish +current host state, original SSH-key provenance, separate-host migration, or a +backup operator's readiness. Use the procedures above for current operations +and record new live verification separately from local checks. diff --git a/scripts/deploy-app.sh b/scripts/deploy-app.sh index d2cfd19..a79791e 100644 --- a/scripts/deploy-app.sh +++ b/scripts/deploy-app.sh @@ -28,11 +28,21 @@ manifest_value() { } release_path() { - local link="$1" target + local link="$1" target release_manifest [[ -f "$link" ]] || die "missing release pointer: $link" target="$(cat "$link")" [[ "$target" =~ ^releases/[a-zA-Z0-9][a-zA-Z0-9.-]{0,100}$ ]] || die "invalid release pointer: $link" [[ -f "$app_dir/$target/release-manifest.txt" ]] || die "release manifest is missing: $target" + [[ -f "$app_dir/$target/compose.yml" && -f "$app_dir/$target/.env" && -f "$web_dir/$target/index.html" ]] || die "release files are missing: $target" + release_manifest="$app_dir/$target/release-manifest.txt" + [[ "$(manifest_value "$release_manifest" SOURCE_SHA)" =~ ^[0-9a-f]{40}$ ]] || die "invalid release source SHA: $target" + [[ "$(manifest_value "$release_manifest" API_IMAGE)" =~ ^ghcr\.io/[a-z0-9_./-]+@sha256:[0-9a-f]{64}$ ]] || die "invalid release API image: $target" + [[ "$(manifest_value "$release_manifest" WEB_SHA256)" =~ ^[0-9a-f]{64}$ ]] || die "invalid release web checksum: $target" + # In-place installations predate RELEASE_ID; their preserved legacy manifests + # still identify the original source, image digest and web checksum. + if [[ "$target" != releases/legacy-* ]]; then + [[ "$(manifest_value "$release_manifest" RELEASE_ID)" == "${target#releases/}" ]] || die "release id differs from pointer: $target" + fi printf '%s' "$app_dir/$target" } @@ -83,11 +93,46 @@ restore_release() { } if [[ "$command_name" == rollback ]]; then + pending="" + if [[ -e "$app_dir/pending" || -L "$app_dir/pending" ]]; then + [[ -f "$app_dir/pending" ]] || die 'invalid pending activation' + pending="$(cat "$app_dir/pending")" + [[ "$pending" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]{0,100}$ ]] || die 'invalid pending release id' + fi if [[ -n "$release_id" ]]; then - [[ -f "$app_dir/pending" ]] || { echo 'No pending activation to recover.'; exit 0; } - [[ "$(cat "$app_dir/pending")" == "$release_id" ]] || die 'pending release differs from requested rollback' + [[ -n "$pending" ]] || { echo 'No pending activation to recover.'; exit 0; } + [[ "$pending" == "$release_id" ]] || die 'pending release differs from requested rollback' + fi + if [[ -n "$pending" ]]; then + candidate="$release_dir/$pending" + [[ -f "$candidate/predecessor" ]] || die 'pending activation lacks predecessor state; manual recovery required' + predecessor="$(cat "$candidate/predecessor")" + if [[ "$predecessor" == none ]]; then + [[ ! -e "$app_dir/previous" && ! -L "$app_dir/previous" ]] || die 'initial activation has an unexpected previous release' + if [[ -e "$app_dir/current" || -L "$app_dir/current" ]]; then + [[ -f "$app_dir/current" && "$(cat "$app_dir/current")" == "releases/$pending" ]] || die 'current release differs from initial activation' + fi + [[ -f "$candidate/compose.yml" && -f "$candidate/.env" ]] || die 'initial activation lacks Compose files; manual recovery required' + # A failed up may still have started a container. Stop/remove the candidate + # before removing its active entry points; the external edge is untouched. + if ! docker compose --env-file "$candidate/.env" -f "$candidate/compose.yml" logs --no-color >> "$candidate/recovery.log" 2>&1; then + echo 'Could not capture initial API logs; continuing recovery.' >&2 + fi + docker compose --env-file "$candidate/.env" -f "$candidate/compose.yml" down >> "$candidate/recovery.log" 2>&1 || die "failed to stop initial API; pending retained; inspect $candidate/recovery.log" + rm -f "$web_dir/index.html" "$web_dir/index.html.br" "$web_dir/index.html.gz" "$web_dir/index.html.zst" \ + "$app_dir/current" "$app_dir/release-manifest.txt" + # Keep the immutable candidate and incoming files for diagnosis and assets. + # Clear pending last so a failed cleanup can be retried with the same ID. + rm "$app_dir/pending" + echo "Recovered initial deployment $pending to undeployed state; candidate files retained." + exit 0 + fi + [[ "$predecessor" =~ ^releases/[a-zA-Z0-9][a-zA-Z0-9.-]{0,100}$ ]] || die 'invalid predecessor state' + previous="$(release_path "$app_dir/previous")" + [[ "$previous" == "$app_dir/$predecessor" ]] || die 'previous release differs from recorded predecessor' + else + previous="$(release_path "$app_dir/previous")" fi - previous="$(release_path "$app_dir/previous")" restore_release "$previous" rm "$app_dir/previous" rm -f "$app_dir/pending" @@ -103,6 +148,8 @@ if [[ "$command_name" == finalize ]]; then exit 0 fi +[[ ! -e "$app_dir/pending" && ! -L "$app_dir/pending" ]] || die 'another activation requires recovery' + incoming="$app_dir/incoming/$release_id" manifest="$incoming/release-manifest.txt" [[ -f "$manifest" && -f "$incoming/web-release.tar.gz" && -f "$incoming/compose.yml" ]] || die 'incomplete incoming release' @@ -148,7 +195,7 @@ mv "$temp_release" "$candidate" mv "$temp_web" "$candidate_web" # The first rollout preserves the in-place installation as a rollback target. -if [[ ! -f "$app_dir/current" && -f "$app_dir/release-manifest.txt" ]]; then +if [[ ! -e "$app_dir/current" && ! -L "$app_dir/current" && -f "$app_dir/release-manifest.txt" ]]; then legacy_sha="$(manifest_value "$app_dir/release-manifest.txt" SOURCE_SHA)" [[ "$legacy_sha" =~ ^[0-9a-f]{40}$ ]] || die 'invalid legacy source SHA' legacy_id="legacy-${legacy_sha:0:12}" @@ -162,14 +209,23 @@ if [[ ! -f "$app_dir/current" && -f "$app_dir/release-manifest.txt" ]]; then fi old="" -if [[ -f "$app_dir/current" ]]; then +if [[ -e "$app_dir/current" || -L "$app_dir/current" ]]; then old="$(release_path "$app_dir/current")" fi -[[ ! -e "$app_dir/pending" ]] || die 'another activation requires recovery' -if [[ -n "$old" ]]; then point_to previous "$(basename "$old")"; fi -printf '%s\n' "$release_id" > "$app_dir/pending" +if [[ -n "$old" ]]; then + point_to previous "$(basename "$old")" + printf 'releases/%s\n' "$(basename "$old")" > "$candidate/predecessor" +else + [[ ! -e "$app_dir/previous" && ! -L "$app_dir/previous" ]] || die 'no current release but previous exists; manual recovery required' + [[ ! -e "$app_dir/release-manifest.txt" && ! -L "$app_dir/release-manifest.txt" ]] || die 'unmanaged release manifest exists; manual recovery required' + [[ ! -e "$web_dir/index.html" && ! -L "$web_dir/index.html" ]] || die 'unmanaged root index exists; manual recovery required' + printf 'none\n' > "$candidate/predecessor" +fi +# Publish pending only after its recovery state is complete, before starting API. +printf '%s\n' "$release_id" > "$app_dir/.pending.$$" +mv -f "$app_dir/.pending.$$" "$app_dir/pending" if ! start_release "$candidate" true; then - die 'new API failed; recovery will restore the recorded previous release' + die 'new API failed; run rollback to recover the recorded deployment state' fi write_index "$release_id" diff --git a/tests/deploy-app/run.sh b/tests/deploy-app/run.sh index ce3de52..3079f63 100644 --- a/tests/deploy-app/run.sh +++ b/tests/deploy-app/run.sh @@ -6,7 +6,7 @@ test_root="$(mktemp -d)" trap 'rm -rf "$test_root"' EXIT app_dir="$test_root/staging" mkdir -p "$test_root/bin" "$app_dir/web" "$app_dir/incoming" -export PATH="$test_root/bin:$PATH" DOCKER_CALLS="$test_root/docker-calls" +export PATH="$test_root/bin:$PATH" DOCKER_CALLS="$test_root/docker-calls" DOCKER_ACTIVE="$test_root/docker-active" cat > "$test_root/bin/docker" <<'EOF' #!/usr/bin/env bash @@ -14,7 +14,21 @@ printf '%s\n' "$*" >> "$DOCKER_CALLS" if [[ "$*" == *'network inspect ogb-edge --format'* ]]; then printf '%s\n' '172.30.0.0/24' fi -if [[ -n "${FAIL_RELEASE:-}" && "$*" == *'up -d'* && "$*" == *"$FAIL_RELEASE"* ]]; then exit 1; fi +if [[ "$*" == *'up -d'* ]]; then + # Compose can start the candidate before reporting an unhealthy service. + printf '%s\n' "$*" > "$DOCKER_ACTIVE" + if [[ -n "${FAIL_RELEASE:-}" && "$*" == *"$FAIL_RELEASE"* ]]; then exit 1; fi +fi +if [[ "$*" == *'logs --no-color'* ]]; then + printf 'candidate API diagnostics\n' +fi +if [[ "$*" == *' down'* ]]; then + if [[ -n "${FAIL_CLEANUP:-}" && "$*" == *"$FAIL_CLEANUP"* ]]; then + printf 'candidate cleanup failed\n' >&2 + exit 1 + fi + rm -f "$DOCKER_ACTIVE" +fi EOF chmod +x "$test_root/bin/docker" @@ -80,3 +94,160 @@ printf 'corruption' >> "$app_dir/incoming/103-1-444444444444/web-release.tar.gz" if run_app activate 103-1-444444444444 >/dev/null 2>&1; then fail 'corrupt archive passed'; fi [[ ! -f "$app_dir/pending" ]] || fail 'corrupt archive created a pending activation' echo 'PASS archive mismatch fails before the live release changes' + +fresh_app() { + app_dir="$test_root/$1/staging" + mkdir -p "$app_dir/incoming" + export DOCKER_CALLS="$test_root/$1/docker-calls" DOCKER_ACTIVE="$test_root/$1/docker-active" + : > "$DOCKER_CALLS" + unset FAIL_RELEASE FAIL_CLEANUP +} + +assert_undeployed() { + local id="$1" index_expected="${2:-true}" file + [[ ! -e "$DOCKER_ACTIVE" ]] || fail 'initial recovery left the candidate API running' + for file in pending current previous release-manifest.txt web/index.html web/index.html.br web/index.html.gz web/index.html.zst; do + [[ ! -e "$app_dir/$file" ]] || fail "initial recovery left live state: $file" + done + [[ "$(cat "$app_dir/releases/$id/predecessor")" == none ]] || fail 'initial recovery lost its explicit predecessor state' + [[ -f "$app_dir/releases/$id/release-manifest.txt" && -f "$app_dir/releases/$id/compose.yml" && -f "$app_dir/releases/$id/.env" ]] || fail 'initial recovery removed candidate diagnostics' + [[ -f "$app_dir/web/releases/$id/asset-$id.txt" && -f "$app_dir/incoming/$id/web-release.tar.gz" ]] || fail 'initial recovery removed the failed release artifacts' + if [[ "$index_expected" == true ]]; then + [[ -f "$app_dir/web/releases/$id/index.html" ]] || fail 'initial recovery removed the candidate index' + fi + grep -Fq 'candidate API diagnostics' "$app_dir/releases/$id/recovery.log" || fail 'initial recovery did not retain candidate logs' + grep -F "releases/$id/.env" "$DOCKER_CALLS" | grep -Fq ' down' || fail 'initial recovery did not clean up the recorded candidate' +} + +assert_retry_succeeds() { + local id="$1" sha="$2" + unset FAIL_RELEASE FAIL_CLEANUP + make_release "$id" "$sha" + run_app activate "$id" >/dev/null + [[ "$(cat "$app_dir/current")" == "releases/$id" ]] || fail 'retry did not activate the new release' + [[ "$(cat "$app_dir/releases/$id/predecessor")" == none && ! -e "$app_dir/previous" ]] || fail 'retry invented a deployed predecessor' + grep -Fq "releases/$id/.env" "$DOCKER_ACTIVE" || fail 'retry did not start the new API' + grep -Fq "/releases/$id/index.html" "$app_dir/web/index.html" || fail 'retry did not publish the new root redirect' + run_app finalize "$id" >/dev/null + [[ ! -e "$app_dir/pending" ]] || fail 'successful retry was not finalized' +} + +fresh_app first-start +first_id=201-1-555555555555 +make_release "$first_id" "$(printf '5%.0s' {1..40})" +export FAIL_RELEASE="$first_id" +if run_app activate "$first_id" >/dev/null 2>&1; then fail 'first API startup failure passed'; fi +[[ -f "$DOCKER_ACTIVE" ]] || fail 'startup failure fixture did not partially start the API' +[[ "$(cat "$app_dir/pending")" == "$first_id" ]] || fail 'first API startup failure lost pending state' +run_app rollback "$first_id" >/dev/null +assert_undeployed "$first_id" +assert_retry_succeeds 202-1-666666666666 "$(printf '6%.0s' {1..40})" +echo 'PASS failed first API startup stops the partial candidate and permits a new-release retry' + +fresh_app first-smoke +first_id=301-1-777777777777 +make_release "$first_id" "$(printf '7%.0s' {1..40})" +run_app activate "$first_id" >/dev/null +[[ "$(cat "$app_dir/current")" == "releases/$first_id" && -f "$app_dir/release-manifest.txt" ]] || fail 'smoke failure fixture was not activated' +for suffix in br gz zst; do printf 'stale compressed index\n' > "$app_dir/web/index.html.$suffix"; done +# The workflow requests rollback when its post-activation browser smoke fails. +run_app rollback "$first_id" >/dev/null +assert_undeployed "$first_id" +assert_retry_succeeds 302-1-888888888888 "$(printf '8%.0s' {1..40})" +echo 'PASS first-deployment smoke failure clears published state and permits a new-release retry' + +fresh_app failed-cleanup +first_id=401-1-999999999999 +make_release "$first_id" "$(printf '9%.0s' {1..40})" +run_app activate "$first_id" >/dev/null +cp "$app_dir/web/index.html" "$test_root/failed-cleanup/index-before" +cp "$app_dir/release-manifest.txt" "$test_root/failed-cleanup/manifest-before" +export FAIL_CLEANUP="$first_id" +if run_app rollback "$first_id" >/dev/null 2>&1; then fail 'failed initial cleanup passed'; fi +[[ -f "$DOCKER_ACTIVE" && "$(cat "$app_dir/pending")" == "$first_id" ]] || fail 'failed cleanup lost its running candidate or pending state' +[[ "$(cat "$app_dir/current")" == "releases/$first_id" && "$(cat "$app_dir/releases/$first_id/predecessor")" == none ]] || fail 'failed cleanup changed current or transaction state' +grep -Fq 'candidate API diagnostics' "$app_dir/releases/$first_id/recovery.log" || fail 'failed cleanup lost candidate logs' +grep -Fq 'candidate cleanup failed' "$app_dir/releases/$first_id/recovery.log" || fail 'failed cleanup did not record its failure' +cmp -s "$app_dir/web/index.html" "$test_root/failed-cleanup/index-before" || fail 'failed cleanup changed the root index' +cmp -s "$app_dir/release-manifest.txt" "$test_root/failed-cleanup/manifest-before" || fail 'failed cleanup changed the live manifest' +retry_id=402-1-aaaaaaaaaaaa +make_release "$retry_id" "$(printf 'a%.0s' {1..40})" +calls_before="$(wc -l < "$DOCKER_CALLS")" +if run_app activate "$retry_id" >/dev/null 2>&1; then fail 'unrecovered initial activation permitted another activation'; fi +[[ "$(cat "$app_dir/pending")" == "$first_id" ]] || fail 'blocked activation changed the pending release' +[[ ! -e "$app_dir/releases/$retry_id" && ! -e "$app_dir/web/releases/$retry_id" ]] || fail 'blocked activation staged another candidate' +[[ "$(wc -l < "$DOCKER_CALLS")" == "$calls_before" ]] || fail 'blocked activation contacted Docker' +unset FAIL_CLEANUP +run_app rollback "$first_id" >/dev/null +assert_undeployed "$first_id" +assert_retry_succeeds "$retry_id" "$(printf 'a%.0s' {1..40})" +echo 'PASS failed cleanup retains recovery state and blocks activation until cleanup succeeds' + +fresh_app missing-candidate-index +first_id=601-1-dddddddddddd +make_release "$first_id" "$(printf 'd%.0s' {1..40})" +run_app activate "$first_id" >/dev/null +rm "$app_dir/web/releases/$first_id/index.html" +run_app rollback "$first_id" >/dev/null +assert_undeployed "$first_id" false +echo 'PASS initial recovery stops the API even when the candidate web index is missing' + +fresh_app failed-file-cleanup +first_id=701-1-eeeeeeeeeeee +make_release "$first_id" "$(printf 'e%.0s' {1..40})" +run_app activate "$first_id" >/dev/null +mv "$app_dir/web/index.html" "$test_root/failed-file-cleanup/index-before" +mkdir "$app_dir/web/index.html" +if run_app rollback "$first_id" >/dev/null 2>&1; then fail 'failed initial file cleanup passed'; fi +[[ ! -e "$DOCKER_ACTIVE" ]] || fail 'file cleanup failure fixture did not stop the candidate API' +[[ "$(cat "$app_dir/pending")" == "$first_id" && -d "$app_dir/web/index.html" ]] || fail 'failed file cleanup lost pending state or ignored the root index directory' +[[ "$(cat "$app_dir/releases/$first_id/predecessor")" == none ]] || fail 'failed file cleanup lost transaction state' +grep -Fq 'candidate API diagnostics' "$app_dir/releases/$first_id/recovery.log" || fail 'failed file cleanup lost candidate logs' +rmdir "$app_dir/web/index.html" +run_app rollback "$first_id" >/dev/null +assert_undeployed "$first_id" +echo 'PASS failed file cleanup retains pending until a later recovery removes the live files' + +fresh_app expected-predecessor +old_id=501-1-bbbbbbbbbbbb +candidate_id=502-1-cccccccccccc +make_release "$old_id" "$(printf 'b%.0s' {1..40})" +run_app activate "$old_id" >/dev/null +run_app finalize "$old_id" >/dev/null +make_release "$candidate_id" "$(printf 'c%.0s' {1..40})" +run_app activate "$candidate_id" >/dev/null +[[ "$(cat "$app_dir/releases/$candidate_id/predecessor")" == "releases/$old_id" ]] || fail 'upgrade did not record its expected predecessor' +cp "$app_dir/web/index.html" "$test_root/expected-predecessor/index-before" +cp "$app_dir/release-manifest.txt" "$test_root/expected-predecessor/manifest-before" + +assert_recovery_blocked() { + local reason="$1" calls_before + calls_before="$(wc -l < "$DOCKER_CALLS")" + if run_app rollback "$candidate_id" >/dev/null 2>&1; then fail "$reason allowed recovery without a valid predecessor"; fi + [[ "$(cat "$app_dir/pending")" == "$candidate_id" && "$(cat "$app_dir/current")" == "releases/$candidate_id" ]] || fail "$reason changed deployment pointers" + [[ "$(wc -l < "$DOCKER_CALLS")" == "$calls_before" ]] || fail "$reason changed running services" + cmp -s "$app_dir/web/index.html" "$test_root/expected-predecessor/index-before" || fail "$reason changed the root index" + cmp -s "$app_dir/release-manifest.txt" "$test_root/expected-predecessor/manifest-before" || fail "$reason changed the live manifest" +} + +mv "$app_dir/previous" "$test_root/expected-predecessor/previous-before" +assert_recovery_blocked 'missing previous pointer' +printf '../unexpected\n' > "$app_dir/previous" +assert_recovery_blocked 'corrupt previous pointer' +printf 'releases/%s\n' "$candidate_id" > "$app_dir/previous" +assert_recovery_blocked 'previous pointer differing from recorded predecessor' +mv "$test_root/expected-predecessor/previous-before" "$app_dir/previous" +mv "$app_dir/releases/$old_id/release-manifest.txt" "$test_root/expected-predecessor/old-manifest-before" +assert_recovery_blocked 'missing predecessor manifest' +printf 'corrupt\n' > "$app_dir/releases/$old_id/release-manifest.txt" +assert_recovery_blocked 'corrupt predecessor manifest' +mv "$test_root/expected-predecessor/old-manifest-before" "$app_dir/releases/$old_id/release-manifest.txt" +mv "$app_dir/releases/$candidate_id/predecessor" "$test_root/expected-predecessor/predecessor-before" +assert_recovery_blocked 'missing transaction state' +printf 'invalid\n' > "$app_dir/releases/$candidate_id/predecessor" +assert_recovery_blocked 'corrupt transaction state' +mv "$test_root/expected-predecessor/predecessor-before" "$app_dir/releases/$candidate_id/predecessor" +run_app rollback "$candidate_id" >/dev/null +[[ "$(cat "$app_dir/current")" == "releases/$old_id" && ! -e "$app_dir/pending" ]] || fail 'repaired predecessor did not restore the upgrade' +grep -Fq "releases/$old_id/.env" "$DOCKER_ACTIVE" || fail 'repaired predecessor did not restart the old API' +echo 'PASS missing or corrupt expected predecessor fails safely and can be recovered after repair' From f4494e45fc97e7292a979d7a52a88813514f1bb7 Mon Sep 17 00:00:00 2001 From: Josh Hufford Date: Tue, 22 Sep 2026 09:10:40 -0400 Subject: [PATCH 3/9] Maintain browser-smoke dependencies --- .github/dependabot.yml | 17 +++++++++++++++++ docs/foundation-checklist.md | 17 ++++++++++++++--- docs/quality/testing.md | 23 +++++++++++++++++++++++ tests/supply-chain/run.sh | 10 ++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5311134..ea28ecc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -39,6 +39,23 @@ updates: cooldown: default-days: 7 + # Browser-smoke dependencies, including Playwright and its browser revisions. + - package-ecosystem: "npm" + directory: "/tests/deploy-smoke" + schedule: + interval: "weekly" + day: "monday" + time: "09:30" + timezone: "America/Indiana/Indianapolis" + open-pull-requests-limit: 5 + commit-message: + prefix: "deps" + include: "scope" + cooldown: + semver-major-days: 14 + semver-minor-days: 3 + semver-patch-days: 1 + # GitHub Actions used by workflows in .github/workflows. - package-ecosystem: "github-actions" directory: "/" diff --git a/docs/foundation-checklist.md b/docs/foundation-checklist.md index 2acbe32..111c39b 100644 --- a/docs/foundation-checklist.md +++ b/docs/foundation-checklist.md @@ -125,15 +125,26 @@ deployment or live browser acceptance was performed for this step. **Finding:** [Playwright is pinned](../tests/deploy-smoke/package.json), but [Dependabot](../.github/dependabot.yml) has no npm entry for this package. -- [ ] Add version updates for `/tests/deploy-smoke` using the existing update cadence. -- [ ] Extend the [supply-chain declaration check](../tests/supply-chain/run.sh) to +- [x] Add version updates for `/tests/deploy-smoke` using the existing update cadence. +- [x] Extend the [supply-chain declaration check](../tests/supply-chain/run.sh) to catch omission of this package without tying the check to a particular version. -- [ ] Validate the package/lockfile and review browser-smoke behavior when updating +- [x] Validate the package/lockfile and review browser-smoke behavior when updating Playwright; version-update configuration alone is not a browser acceptance test. **Acceptance:** the declaration check passes and Dependabot recognizes the npm directory after merge. Dependency updates receive the checks from step 2. +**Result (2026-09-22):** Weekly npm updates now cover the smoke package with the +existing dependency cooldowns. The declaration suite passes; isolated cases +reject omitted coverage, the wrong directory or ecosystem, and coverage split +across unrelated entries. Node 22 `npm ci` and syntax checks passed with +Playwright 1.63.0 unchanged. Restore, format verification, Release build (zero +warnings), and all 72 .NET tests passed. The existing smoke assertions were +reviewed, and [maintenance guidance](quality/testing.md#browser-smoke-dependency-updates) +records the package checks and browser evidence needed for future updates. +Dependabot recognition of the npm directory remains unverified until merge; +no deployment or live browser acceptance was performed for this step. + ### 4. Correct operational documentation and unresolved host evidence **Finding:** [GitHub setup](setup/github.md) describes a separate unprivileged diff --git a/docs/quality/testing.md b/docs/quality/testing.md index ae19458..00b4f62 100644 --- a/docs/quality/testing.md +++ b/docs/quality/testing.md @@ -142,6 +142,27 @@ Deployment additionally runs a Chromium smoke test from `tests/deploy-smoke` tha loads the published frontend, observes its API request, and checks the expected source revision. That live test requires a deployed staging or production URL. +### Browser-smoke dependency updates + +[Dependabot](../../.github/dependabot.yml) checks `/tests/deploy-smoke` weekly, +using the repository's dependency-update cadence and cooldowns. Its npm entry +targets the directory containing both `package.json` and `package-lock.json`, as +described in [GitHub's configuration reference](https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference#directories-or-directory). +After merging configuration changes, check GitHub's Dependabot update-job list +for that npm directory and inspect its first run for configuration errors. + +For Playwright updates, review the release notes and the manifest/lockfile diff, +then run the `npm ci` and syntax commands above with Node.js 22. Dependency PRs +receive the same required `build-test` validation. Package installation and +syntax checks do not establish compatibility with the updated Chromium build. +Review the browser smoke result from an authorized staging deployment: the +release URL and base path, successful `/api/about` request, expected source +revision, API-backed heading, and absence of page errors. Record that workflow +run separately from the package checks; if it has not run, browser acceptance +remains unverified. + +### Supply-chain declarations + Supply-chain declarations have an additional deterministic check: ```pwsh @@ -151,6 +172,8 @@ Supply-chain declarations have an additional deterministic check: It rejects third-party Actions that are not full commit SHAs, mutable API base or edge image references, a missing explicit API user, deployment-time `ssh-keyscan`, inherited NuGet source mappings, or missing Dependabot ecosystems. +The npm declaration must cover `/tests/deploy-smoke` in the same update entry; +the check does not depend on a particular Playwright version. It also checks that the API-image group patterns match Dependabot's normalized dependency names (without their registry). CI loads the locally built API image and runs `scripts/verify-api-image.sh`; that Docker-backed check verifies the runtime user, diff --git a/tests/supply-chain/run.sh b/tests/supply-chain/run.sh index 28080a1..cf8cb45 100644 --- a/tests/supply-chain/run.sh +++ b/tests/supply-chain/run.sh @@ -52,6 +52,16 @@ grep -Fq 'directory: "/.github/actions/validate"' .github/dependabot.yml || fail 'Dependabot does not monitor the composite validation action pins' echo 'PASS NuGet mapping inheritance is cleared and pin update automation remains enabled' +# Require npm coverage for this package in the same update entry; an npm entry +# elsewhere or the smoke directory under another ecosystem is not sufficient. +awk ' + /^ - package-ecosystem:/ { is_npm = ($3 == "\"npm\"") } + is_npm && /^ directory: "\/tests\/deploy-smoke"[[:space:]]*$/ { found = 1 } + END { exit(found ? 0 : 1) } +' .github/dependabot.yml || fail 'Dependabot does not monitor npm dependencies in /tests/deploy-smoke' + +echo 'PASS Dependabot monitors browser-smoke npm dependencies' + # Dependabot separates the Docker registry from the dependency name. Exercise # the configured glob against the same names it uses, not the full image URLs. mapfile -t api_image_patterns < <(awk ' From 5c5fdbaf0930db009d4d9c6a3f3ee5644a689d60 Mon Sep 17 00:00:00 2001 From: Josh Hufford Date: Tue, 22 Sep 2026 09:22:03 -0400 Subject: [PATCH 4/9] Correct operational documentation and unresolved host evidence --- docs/foundation-checklist.md | 21 ++- docs/release/README.md | 38 +++-- docs/setup/deployment-host-key.md | 5 + docs/setup/github.md | 240 ++++++++++++++---------------- docs/setup/hosting.md | 73 ++++++--- 5 files changed, 211 insertions(+), 166 deletions(-) diff --git a/docs/foundation-checklist.md b/docs/foundation-checklist.md index 111c39b..54ce663 100644 --- a/docs/foundation-checklist.md +++ b/docs/foundation-checklist.md @@ -152,15 +152,15 @@ smoke job and old timeouts; smoke now runs in the 45-minute deployment job with `packages: write`, after SSH setup. Its opening unpublished-workflow snapshot and [hosting's](setup/hosting.md) "before merging" instructions are also obsolete. -- [ ] Describe the actual job, token, and on-disk credential boundaries. Review +- [x] Describe the actual job, token, and on-disk credential boundaries. Review whether the combined job is intentional; record that decision or make a focused change with recovery coverage. Do not imply this audit demonstrated exploitation. -- [ ] State that production workflows must be dispatched from `main`, separately +- [x] State that production workflows must be dispatched from `main`, separately from the protected application source `ref`, in [release guidance](release/README.md). -- [ ] Replace completed rollout instructions and conflicting verification diaries +- [x] Replace completed rollout instructions and conflicting verification diaries with current procedures and concise evidence links. Keep unresolved host adoption explicit rather than assuming merge means host configuration was applied. -- [ ] Carry forward administrator confirmation of the SSH host key's trusted +- [x] Carry forward administrator confirmation of the SSH host key's trusted source and [host verification](setup/hosting.md#host-caddy-conflicts-and-read-only-verification) of the running Caddy digest and competing native-service boot state. @@ -169,6 +169,19 @@ Every remaining host action names its owner and unverified state. Documentation work does not perform a deployment; successful SSH use alone does not establish how the original host key was authenticated. +**Result (2026-09-22):** [GitHub setup](setup/github.md#deployment-job-credential-boundary) +now matches the workflow's job permissions, retained Docker/SSH credentials, +timeouts, and combined activation/smoke/recovery decision. Release guidance +requires `main` dispatch separately from protected application source. Read-only +GitHub checks confirmed environment branch rules, production approval settings, +both shared-profile variables, and the completed edge adoption run. The +[hosting evidence](setup/hosting.md#recorded-deployment-evidence) distinguishes +that run from the earlier staging browser check; original host-key provenance, +current Caddy digest/boot state, and application acceptance after adoption remain +explicitly unverified and assigned to `ostomachion`. Documentation link, anchor, +workflow-reference, and diff checks passed. No workflow code, credentials, +services, or deployment state were changed; no new deployment was performed. + ## Phase 2: Make the supported contributor path accurate ### 5. Repair development instructions and launch leftovers diff --git a/docs/release/README.md b/docs/release/README.md index 9998091..4d8fd30 100644 --- a/docs/release/README.md +++ b/docs/release/README.md @@ -5,8 +5,8 @@ workflows you need to know about: | Workflow | Trigger | What it does | | --------------------- | -------------------------------------------------- | --------------------------------------------------------------------------- | -| 🛰️ **CD Staging** | Every push to `main` | Build, test, deploy to staging, smoke test | -| 🚀 **CD Production** | Manually dispatched (usually from `main`) with a `ref` input | Validate, build, test, deploy to production, smoke test, tag, release, follow-up PR | +| 🛰️ **CD Staging** | Every push to `main`, or manual dispatch from `main` | Build, test, deploy to staging, smoke test | +| 🚀 **CD Production** | Manually dispatched **from `main`**, with a separate source `ref` input | Validate, build, test, deploy to production, smoke test, tag, release, follow-up PR | | 🩹 **Prepare Patch** | Manually dispatched | Create `patch/vX.Y.(Z+1)` branch and a version-bump PR off the latest tag | Host infrastructure is separate: **🌐 CD Edge** updates the selected host's @@ -29,14 +29,22 @@ manifest and workflow artifact when identifying an installed release. The host's [hosting setup](../setup/hosting.md#application-activation-and-rollback). The browser smoke check follows the activated page, observes its `/api/about` call, and compares the running API's source revision with the selected protected -commit. The staging rollback rehearsal passed on 2026-09-20; the -[hosting guide records the deployment evidence](../setup/hosting.md#recorded-deployment-evidence). +commit. The [hosting guide records deployment and recovery evidence](../setup/hosting.md#recorded-deployment-evidence). + +For **CD Production**, the Actions branch picker must be **`main`**: it selects +the workflow definition and the run ref checked by the production environment. +The separate `ref` input selects application source: protected `main` for a +standard release or protected `patch/vX.Y.Z` for a patch. The resolver fixes that +source to a commit; tags, arbitrary SHAs, and unprotected branches are rejected. +The workflow rejects non-`main` dispatches before source resolution. See +[deployment authority](../setup/github.md#deployment-authority-and-recovery). ## Standard release (X.Y.0) 1. `main` already has `X.Y.0` (set by the post-release bump PR from the previous release). -2. Go to **Actions → 🚀 CD Production → Run workflow**. Leave `ref` as `main`. +2. Go to **Actions → 🚀 CD Production → Run workflow**. Select `main` in the + branch picker and leave the separate `ref` input as `main`. 3. The `production` environment requires reviewer approval — approve when ready. 4. On success the workflow tags `vX.Y.0`, creates the GitHub Release, and opens `chore: bump version to X.(Y+1).0` against `main`. Merge that PR. @@ -54,15 +62,16 @@ commit. The staging rollback rehearsal passed on 2026-09-20; the 5. On success the workflow tags `vX.Y.(Z+1)`, creates the GitHub Release, and opens `chore: merge vX.Y.(Z+1) into main`. Review and merge that PR. -> **Note:** CD Production runs the workflow file from the branch it is -> dispatched on (usually `main`), but it checks out the specified `ref` before -> running validation and release scripts. Ensure patch branches contain -> compatible deployment and release code for their release run, including the -> `/api/about` source revision used by the browser smoke test. +The workflow definition and shared validation action come from the dispatched +`main` workflow commit; application files and release/deployment scripts come +from the resolved protected source commit. Ensure patch branches contain +compatible deployment and release code, including the `/api/about` source +revision used by the browser smoke test. A patch release still uses `main` in +the workflow branch picker. ## What the production workflow validates -It detects whether the dispatched ref is a standard or patch release from the +It detects whether the selected source is a standard or patch release from the version number itself (`Z == 0` → standard, `Z > 0` → patch) and checks: - Version in `Directory.Build.props` is plain `X.Y.Z` @@ -75,8 +84,11 @@ version number itself (`Z == 0` → standard, `Z > 0` → patch) and checks: The tag, GitHub Release, and follow-up PR are only created **after** a successful production deploy and smoke test. If activation or smoke testing -fails, the workflow attempts to restore the previous web/API pair. Inspect the -rollback job result and the host's `current` and `previous` files before rerunning. +fails, the deployment job attempts recovery in its rollback step. An upgrade +restores the previous web/API pair; a failed first deployment returns to the +defined undeployed state. Inspect that step's result and the host's `current`, +`previous`, `pending`, and candidate `predecessor` records before rerunning; see +[the recovery procedure](../setup/hosting.md#application-activation-and-rollback). If deployment and smoke testing succeed but tag creation, GitHub Release creation, or the follow-up PR fails, production is already running the new pair. diff --git a/docs/setup/deployment-host-key.md b/docs/setup/deployment-host-key.md index cb6a57c..c9e9e41 100644 --- a/docs/setup/deployment-host-key.md +++ b/docs/setup/deployment-host-key.md @@ -7,6 +7,11 @@ Staging and production may use the same server or different servers; complete this guide separately for each environment. The normal path needs no root password, password reset, server restart, or SSH configuration change. +Administrator confirmation of each existing pin's original trust source is still +unverified in the [host evidence record](hosting.md#outstanding-host-verification). +Completing this procedure must record that source as well as successful pin +transfer; a passing deployment alone does not resolve the provenance gap. + ## What we are doing SSH authenticates both ends of the connection. Your personal SSH private key diff --git a/docs/setup/github.md b/docs/setup/github.md index c96318f..cbf1742 100644 --- a/docs/setup/github.md +++ b/docs/setup/github.md @@ -1,10 +1,10 @@ # GitHub Setup -This guide describes the intended workflow code in this checkout and separately -records live GitHub settings. At the 2026-09-19 audit, remote `main` was still -`f40de883662f2fbe35859879f772b5a9f329256d`; this checkout's newer -protected-source workflow and validation action had not been published. Do not -assume a local workflow change is active on GitHub until it is merged. +This guide describes the checked-in workflows and separately dates inspected +GitHub settings and acceptance evidence. Protected-source deployment and explicit +host profiles are published; see the [deployment evidence](hosting.md#recorded-deployment-evidence). +Later local changes still need their own hosted checks before hosted success is +claimed. A merged workflow does not establish that host configuration was applied. ## CI merge gate @@ -16,9 +16,12 @@ tests in its place. CI runs on every pull request, without path filters, and on pushes to `patch/v*`. The [shared validation action](../../.github/actions/validate/action.yml) runs restore, formatting verification, a Release solution build (including AppHost), -and all tests. Both CI and [deployment validation](../../.github/workflows/_deploy.yml) -use this action. Deployment requires its validation job to succeed before entering -the environment job. CI additionally builds the API container without pushing it. +and all tests, plus smoke-package installation and JavaScript syntax checks. +CI also publishes the frontend and checks its portable configuration; deployment +does that in its required `package` job. Both CI and +[deployment validation](../../.github/workflows/_deploy.yml) use the shared action. +Deployment requires validation and packaging to succeed before entering the +environment job. CI additionally builds the API container without pushing it. No deployment credentials are supplied to pull-request CI; the deployment validation job also has a read-only repository token. @@ -42,20 +45,31 @@ both sets continue to receive reviewable version-update PRs. Validation commands use explicit Bash shells, whose `-e -o pipefail` behavior keeps a failing command from being hidden by `tee`. Failures upload the available -restore, formatting, build, and test console logs, plus the formatter's JSON -report, as `ci-validation`, `staging-validation`, or `production-validation`. +solution, script-suite, frontend-package (when run), and smoke-package console +logs, plus the formatter's JSON report, as `ci-validation`, `staging-validation`, +or `production-validation`. Each name includes the run-attempt suffix so reruns do not collide. Artifacts expire after seven days. These are diagnostic logs, not TRX reports; test failures and stack traces are in `test.log`. A cancellation, runner loss, or job timeout can prevent the upload, so also consult the Actions job log. Do not log credentials or sensitive response bodies. -Every runner job has an explicit timeout: CI and CodeQL 30 minutes, deployment -validation 20, deployment 30, smoke tests 5, and release-script jobs 10. Reusable -source-resolution jobs have a 5-minute timeout. Reusable -workflow callers use the timeouts on their called jobs. These are upper bounds, -not targets. Do not add `continue-on-error`, conditional skipping, or path filters -to the required job. +Every runner job has an explicit timeout: + +| Runner job | Timeout (minutes) | +| --- | --- | +| CI `build-test`; each CodeQL analysis | 30 | +| Deployment `build-test` | 20 | +| Deployment `package` | 25 | +| Deployment `deploy`, including browser setup, activation, smoke, and recovery | 45 | +| Production release validation/publication; patch preparation | 10 | +| Source resolution; main-dispatch guards; edge migration authorization | 5 | +| Edge validation and apply | 15 | + +Reusable workflow callers use the timeouts on their called jobs. Browser smoke +has no separate job or workflow step timeout. These are upper bounds, not targets. +Do not add `continue-on-error`, conditional skipping, or path filters to the +required job. ## Live protection @@ -160,10 +174,8 @@ separate administrator setting, not a reason to broaden workflow tokens. ## Deployment authority and recovery -Authenticated read-back on **2026-09-19** found that both `production` and -`staging` select only the **`main` branch** for deployment. The obsolete -`release/**/*` tag rule on production and `release/**/*` branch rule on staging -were removed; neither belongs to the current process. GitHub matches an +Authenticated read-back on **2026-09-22** confirmed that both `production` and +`staging` select only the **`main` branch** for deployment. GitHub matches an environment's deployment rule against the workflow run's `GITHUB_REF`, not the commit checked out inside a job. For both release kinds, dispatch **CD Production** with the branch picker on `main`: the `ref` input chooses `main` @@ -181,23 +193,18 @@ explains this distinction. | `patch/vX.Y.Z` or a tag | Any | Workflow guard fails; production also denies | Not run | | `main` | Tag, arbitrary SHA, or unprotected branch | Environment permits the dispatch ref | Source resolver rejects the input | -Staging runs on a push to `main` or a manual dispatch from `main`; other -dispatch refs are denied by its environment policy in the normal path. On -2026-09-20, `main` at `93666e325c8c3bf02e9a5fbdbdffa4b7097198c9` -contained the guards and source resolver; they remained in the merged fix at -`65cf767afd587ce5ea72368df8d888c69bd0a7e7`. A +Staging runs on a push to `main` or a manual dispatch from `main`; the workflow +guard and environment policy reject other dispatch refs in the normal path. The [deliberately invalid production dispatch](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35531224662) from `main` with a tag as the source input failed in the resolver; validation, deployment, and release jobs were skipped. A [non-`main` dispatch](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35531484408) -from the fix branch failed at the first guard, with all downstream jobs skipped. -Neither run tested an approved production release. -The [merge-triggered staging run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35531733842) -at that commit passed protected-source resolution, validation, deployment, and -the API liveness smoke test. It verifies the corrected secret handoff and the -staging path, not production approval or a standard/patch release. - -The production environment has one required reviewer, `ostomachion`. +failed at the first guard, with all downstream jobs skipped. These rejection +checks did not deploy. Successful application and edge runs are recorded in +[hosting](hosting.md#recorded-deployment-evidence). + +The production environment has one required reviewer, `ostomachion`, confirmed +along with self-review and administrator bypass settings on **2026-09-22**. Self-approval is allowed because there is no second eligible release reviewer; administrator bypass is also enabled. **Decision (2026-09-19): retain bypass for emergency recovery while there is only one release operator.** It is not @@ -234,26 +241,21 @@ variable from an unverified `ssh-keyscan` result. During rotation, authenticate the replacement before changing the variable. The workflow requires an exact host match, enables strict host-key checking, and prints the pinned fingerprint to the job log without printing private credentials. +Administrator confirmation of the original trust source remains unverified; +see the [owned host checks](hosting.md#outstanding-host-verification). The `RELEASE_BOT_PRIVATE_KEY` is a repository secret because **Prepare Patch** needs the App before any deployment environment is entered; the client ID and smoke-test URLs are repository variables. Deployment -callers use `secrets: inherit`: the -[merged staging run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35531071912) -showed that omitting this handoff left the selected environment's `DEPLOY_HOST` -and `DEPLOY_SSH_KEY` empty inside the reusable deployment workflow, despite their -configured names. It failed at SSH setup after publishing an image, before -syncing files or restarting services. The workflow checks the three deployment +callers use `secrets: inherit`. The workflow checks the three deployment secrets and the pinned host-key variable for presence in the environment job -before publishing an image; it never logs secret values. The successful staging -run above passed the earlier three-secret check, -SSH setup, file sync, service restart, and the smoke test. Inheritance also makes +before publishing an image; it never logs secret values. Inheritance also makes the repository-scoped `RELEASE_BOT_PRIVATE_KEY` available to the trusted reusable workflow's secret context, although no deployment step references it. Keep the reusable workflow definition trusted and the bot key out of scripts/checkout. Revisit isolation if release credentials move to a separate -approval boundary. Only the deployment call receives `packages: write`; -validation has `contents: read`, and the smoke test has no token permissions. +approval boundary. The deployment job's permissions and retained credentials +also cover its browser-smoke steps, as detailed below. The App installation has contents, pull requests, and workflows write plus metadata read; its creation-only branch/tag bypasses and the no-bypass tag-immutability rule are recorded above. The token-creation action requests these permissions @@ -264,7 +266,44 @@ GitHub makes environment secrets available only after that environment's rules pass. On 2026-09-20, the signed-in organization Actions secrets settings page explicitly reported that OpenGameBuilder has no organization secrets. This was a read-only UI metadata check; no secret values were viewed. The audit CLI -token still receives 403 for the organization secret API inventory. +token received 403 for the organization secret API inventory at that inspection; +organization-secret inventory was not refreshed in the 2026-09-22 documentation check. + +### Deployment job credential boundary + +In [_deploy.yml](../../.github/workflows/_deploy.yml), source resolution, +validation, and packaging use separate jobs with `contents: read`. Only `deploy` +enters the selected environment and receives `contents: read` plus +`packages: write`. These are job-wide token permissions; a later smoke step +does not reduce them. See [GitHub's permissions reference](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions). + +The deployment runner logs in to GHCR with its `GITHUB_TOKEN`, builds/pushes the +API image, then sets up SSH. [Docker login](https://docs.docker.com/reference/cli/docker/login/) +retains authentication in the runner's +Docker credential configuration (`~/.docker/config.json` or its configured +credential store). SSH setup writes `~/.ssh/deploy_key` and `~/.ssh/config` with +mode 600, and the public pin to `~/.ssh/known_hosts`. Checkout uses +`persist-credentials: false`; this prevents persisted Git checkout credentials, +but does not remove Docker or SSH credentials. Node/Playwright installation, +activation, browser smoke, finalization, and recovery all run afterward on the +same runner. The key and registry login remain available through those steps; +there is no explicit logout or key-removal step. File modes protect against other +users, not later code running as the same runner user. The smoke command receives +only its URL and expected identities as explicit step environment variables, +but it is not an isolated credential-free job. + +**Decision (2026-09-22): retain the combined deployment job.** It keeps activation, +smoke acceptance, finalization, and failure recovery in one transaction using +the same SSH connection configuration and candidate identity. Dependencies are +installed before activation; a subsequent failed activation or smoke check +triggers rollback in that job. Cancellation, timeout, or runner loss can prevent +recovery; inspect the host's pending transaction before retrying. This choice +trusts the reviewed protected-source deployment scripts and locked smoke +dependencies with the deployment runner's authority. It records an exposure +boundary, not evidence of exploitation. Splitting jobs would need an explicit +handoff and recovery coverage; it is not part of this documentation correction. + +### Settings inspection and operator ownership Read back these settings without revealing secret values: @@ -278,8 +317,8 @@ gh api repos/OpenGameBuilder/opengamebuilder/actions/secrets --jq '.secrets[].na gh api orgs/OpenGameBuilder/installations --jq '.installations[] | select(.app_id == 3815756) | {repository_selection, permissions}' ``` -Today `ostomachion` is the only human who can both initiate and approve a -production release and handle recovery. If deployment fails before publishing, +At the recorded access inspection, `ostomachion` was the only human who could +both initiate and approve a production release and handle recovery. If deployment fails before publishing, fix the cause and rerun from `main` with the intended protected source branch. If deployment succeeded but tagging, GitHub Release creation, or the follow-up PR failed, keep that source branch at the same commit and follow the @@ -293,91 +332,28 @@ That evidence does not establish a second operator's access or recovery readines ## Acceptance evidence -The validation baseline is published in -[PR #82](https://github.com/OpenGameBuilder/opengamebuilder/pull/82). Its temporary -companion [patch PR #83](https://github.com/OpenGameBuilder/opengamebuilder/pull/83) -was closed without merging after verification. The disposable target -`patch/v0.0.0-ci-gate-check` was deleted; it was never a release to deploy or tag. -The original developer worktree and staged changes remain untouched by the -isolated verification commits. - -Local verification used SDK `10.0.401` and Git for Windows Bash with the runner's -fail-fast/pipefail options. Restore, formatting verification, Release build, and -all **62 tests** passed (none skipped); Visual Studio build also passed. The known -ASPIRE010 warning remains. All four simulated failing `dotnet` pipelines retained -their nonzero exit codes and captured logs. Workflow structure, wiring, branch -patterns, timeouts, and documentation links were checked. - -At head `7b9b1e54f070abb3cda809e3c33c5280273dfaa5`, both PRs reported `BLOCKED`, -and `gh pr checks --required` identified the failing `build-test` check. - -| Target | Deliberately failing CI run | Result | -| --- | --- | --- | -| `main` (PR #82) | [34996043271](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/34996043271) | 62 passed, 1 deliberate assertion failed; merge blocked | -| `patch/v*` (PR #83) | [34996046998](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/34996046998) | 62 passed, 1 deliberate assertion failed; merge blocked | - -Both runs passed restore, formatting, and build, and uploaded `ci-validation-1` -with the failing assertion and stack trace in `test.log`. Both CodeQL Advanced -language jobs ran for both targets. The aggregate CodeQL gate also correctly -caught a cache-poisoning risk from executing the shared action at an unchecked -deployment ref. Protected-source resolution addresses that trust boundary rather -than suppressing the alert. Its exact shell was checked with nine allowed, -rejected, moved-ref, unprotected-ref, and malformed-response cases, plus a real -read-only lookup of protected `main`. - -After removing the deliberate test and completing trusted action/source -separation, head `ad8f7d7223a2908e48a828d85b74e78d01dab401` passed: - -| Target | Passing CI run | Result | +These are historical acceptance checks, not the current test count or a fresh +inspection of every repository setting. Current local commands and evidence +boundaries are in [testing guidance](../quality/testing.md). + +| Check | Evidence | What it establishes | | --- | --- | --- | -| `main` (PR #82) | [34998082400](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/34998082400) | All 62 tests passed; Docker image built without pushing | -| `patch/v*` (PR #83) | [34998087065](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/34998087065) | All 62 tests passed; Docker image built without pushing | - -Both CodeQL Advanced language analyses and the managed code-quality analysis -passed. The [aggregate CodeQL check](https://github.com/OpenGameBuilder/opengamebuilder/runs/104479502143) -passed with zero annotations, and both PR merge refs had zero open code-scanning -alerts. No alert was dismissed and no security query was disabled. The shared -action was also executed locally against a separate source worktree: all 62 -tests passed and all five diagnostic files appeared in that source directory. - -The authenticated review rule verifies the approval count, stale-review dismissal, -latest-push approval, and the documented sole-maintainer exception. No fake human -approval was submitted. Resolve genuine review findings before merging; a green -test/scan check is not approval to bypass outstanding review conversations. - -Cleanup temporarily excluded only the disposable patch ref from ruleset -`16765458` to allow deletion. Immediate read-back confirmed that the exclusion -list was restored to empty, with no bypass actors. The separate disposable bot -probe branch was also deleted. `main` and all existing release tags were unchanged; -no PR was merged and no deployment or release was performed. - -### Successful release-bot acceptance - -[Prepare Patch run 35002126742](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35002126742) -used the approved App token and completed successfully. It created: - -- `patch/v0.9.1` at the existing `v0.9.0` commit - `4fc9f816443eb6011958d6c6d43d64c82d9bfad3`, preserving its historical merges. -- `chore/prepare-v0.9.1` at `6167c2bc2a801b525ff8bf502d52121c80cd04a9`. -- Bot-authored [PR #84](https://github.com/OpenGameBuilder/opengamebuilder/pull/84), - changing only `VersionPrefix` from `0.9.0` to `0.9.1`. - -The PR reported `REVIEW_REQUIRED` and `BLOCKED`, with `build-test` still required. -[Its CI run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35002166207) -failed restore with NU1903 for the old release's `Microsoft.OpenApi` 2.0.0 -([advisory](https://github.com/advisories/GHSA-v5pm-xwqc-g5wc)). -That is the gate correctly rejecting an old dependency baseline, not a bot -permission failure. The current dependency/test baseline's green main and patch -runs are recorded above. Real patch preparation from older tags must receive the -current dependency and validation baseline before merging; do not disable Audit -or required checks to make an old release green. - -PR #84 was closed without merging and both refs created by this run were deleted. -Only the exact verification patch ref was temporarily excluded for deletion; -read-back confirmed the exclusion was removed and the gate has no bypass actors. -The successful creation/PR operation, enforced review/check requirements, and -unchanged tag-immutability rules complete the release-bot acceptance check. No human -approval was fabricated and no deployment, tag change, or release was performed. +| Required CI rejects a behavior failure on `main` and `patch/v*` | Deliberately failing [main run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/34996043271) and [patch run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/34996046998) | `build-test` failed, diagnostic artifacts contained the assertion, and both PRs were blocked | +| Corrected validation baseline passes on both branch families | Passing [main run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/34998082400) and [patch run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/34998087065) | Tests and API image build passed; CodeQL and code-quality checks passed without suppressing alerts | +| Release App can prepare a patch without bypassing the PR gate | [Prepare Patch run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35002126742), [PR #84](https://github.com/OpenGameBuilder/opengamebuilder/pull/84), and [its CI run](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35002166207) | Branch/PR creation succeeded; review remained required and NU1903 blocked the old dependency baseline | + +The baseline was published through [PR #82](https://github.com/OpenGameBuilder/opengamebuilder/pull/82). +Temporary [patch PR #83](https://github.com/OpenGameBuilder/opengamebuilder/pull/83) +and PR #84 were closed without merging, and their disposable refs were removed. +Temporary deletion exclusions were removed afterward; read-back found no +merge-gate bypass actors. No release tags were changed by these acceptance checks. +The recorded human-review settings do not establish independent approval of +maintainer-authored PRs; the sole-maintainer exception above still applies. + +Patch branches created from older tags must receive the current dependency and +validation baseline through their preparation PR. Do not disable NuGet Audit, +required checks, or review to make an old release pass. Deployment and rollback +acceptance is recorded separately in [hosting](hosting.md#recorded-deployment-evidence). ## Administrator verification @@ -408,7 +384,7 @@ again. Do not display tokens or copy credentials into the repository. Then perform a controlled acceptance check without deploying: -1. Publish this CI/action/test baseline and run CI on a representative PR. +1. After a CI/action/test change is merged, run CI on a representative PR. Confirm the reported check is exactly `build-test`, and require it from GitHub Actions in the ruleset. 2. On a temporary PR to `main`, deliberately break an existing test. Confirm the diff --git a/docs/setup/hosting.md b/docs/setup/hosting.md index 6e44996..d5dec7b 100644 --- a/docs/setup/hosting.md +++ b/docs/setup/hosting.md @@ -103,20 +103,25 @@ deployments queue instead of cancelling an in-flight deployment. Production profile is `shared`**. Isolated staging neither requires production's URL nor depends on production being available. -### Adopt explicit profiles on the current server - -Before merging this workflow change, set the environment variable -`EDGE_PROFILE=shared` in **both** GitHub environments under **Settings > -Environments > staging/production > Environment variables**. Keep their existing -SSH secrets and verified host-key entries. These are configuration changes, not -an instruction to recreate the server or reset a password. - -After merge, run **CD Edge** from `main`, select `production`, leave -`allow-profile-change` off, and approve it. This adopts the existing shared -layout while preserving the Compose project and certificate-volume names. -The old unmarked shared layout remains recognizable by application preflight -during this one-time adoption. Confirm the host-local HTTPS checks and the next -staging application's browser check pass. +### Shared-profile adoption + +The explicit-profile workflow is merged. Read-only GitHub inspection on +**2026-09-22** confirmed `EDGE_PROFILE=shared` in both environments and a +successful CD Edge run using the production environment and shared profile; see the +[recorded evidence](#recorded-deployment-evidence). This replaces the earlier +pre-merge setup instructions. Current host state and application acceptance +after that edge run still need the checks [listed below](#outstanding-host-verification). + +For a host still using the unmarked legacy shared layout, the deployment +administrator first confirms `EDGE_PROFILE=shared` in both environments under +**Settings > Environments > staging/production > Environment variables** and +confirms each environment's SSH host/key configuration. With deployment +authorization, dispatch **CD Edge** from `main`, select `production`, leave +`allow-profile-change` off, and approve it. This adopts the shared marker while +preserving the Compose project and certificate-volume names. Application +preflight accepts the legacy shared layout during adoption. Record the edge +run, both host-local HTTPS checks, and subsequent application smoke evidence; +merging the workflow alone does not complete adoption. ### Later: move staging to its own server @@ -169,6 +174,7 @@ systemctl is-active caddy ss -ltnp '( sport = :80 or sport = :443 )' docker ps --all --filter name=ogb- --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' docker inspect --format '{{.Name}} image={{.Config.Image}} status={{.State.Status}} restarts={{.RestartCount}}' ogb-edge-caddy-1 +head -n 1 /srv/opengamebuilder/edge/Caddyfile docker logs --since 2h --tail 150 ogb-edge-caddy-1 2>&1 # Choose an application actually deployed on this host; repeat for the other # only if this is a shared host. @@ -199,6 +205,25 @@ approve it. A Compose image change can briefly interrupt the sites on that host. Verify the running image reference matches `deploy/edge/compose.yml` and that the selected host's edge checks and its deployed applications' smoke tests pass. +### Outstanding host verification + +The remaining evidence belongs to **`ostomachion`, the deployment and host +administrator**. As of the **2026-09-22** documentation check, the following +items remain unverified; no SSH session or host change was performed for this +check. Record dated, redacted results here when completed. + +| Remaining check | Owner | Required evidence | +| --- | --- | --- | +| Original SSH host-key trust source for each environment | `ostomachion` (deployment administrator) | Confirm whether each pin came from an authenticated independent channel or an established trusted SSH connection, using the [host-key guide](deployment-host-key.md). Successful strict SSH alone does not establish how the first key was authenticated. | +| Current installed profile and running Caddy digest | `ostomachion` (host administrator) | Capture the marker and running container image reference with the read-only commands above; compare with the intended profile and the pinned image in the reviewed `deploy/edge/compose.yml`. A successful past edge apply is not a fresh host inspection. | +| Competing native Caddy boot and listener state | `ostomachion` (host administrator) | Record `systemctl is-enabled/is-active caddy` and port-owner output. The native service must be disabled/inactive or absent, and the Docker edge must own 80/443. Remediation, if needed, is a separately authorized operation. | +| Application acceptance after shared-profile adoption | `ostomachion` (deployment operator) | Record the next authorized staging browser smoke and production application check after edge run `35681866962`. Its host-local `/health` checks establish edge readiness only; the latest inspected staging browser run preceded the edge apply. | + +Separate-host migration and backup-operator readiness are not established by +shared-host acceptance. Follow the migration procedure only when that move is +authorized; operator continuity remains tracked in +[practical stewardship](../community/stewardship.md). + ## Application artifacts After source validation, the package job produces one Release web archive. @@ -294,9 +319,8 @@ The Compose service is replaced before the root web redirect switches. During that short interval, an already loaded page can call the new API, so API changes must remain compatible with the retained frontend until its clients have aged out. This mechanism provides an atomic web switch and a recoverable pair; it is -not a zero-downtime atomic swap of the API and web processes. The -staging failure and rollback rehearsal passed on 2026-09-20; see the -[recorded deployment evidence](#recorded-deployment-evidence). +not a zero-downtime atomic swap of the API and web processes. See the +[recorded deployment evidence](#recorded-deployment-evidence) for rehearsal results. To recover an edge change, fix the candidate on `main` and dispatch **CD Edge** for the affected host again. For an urgent host-side recovery, use the last known-good edge @@ -307,6 +331,21 @@ with any active edge workflow so its next write does not overwrite the repair. ## Recorded deployment evidence +Read-only GitHub inspection on **2026-09-22** confirmed that +[PR #113](https://github.com/OpenGameBuilder/opengamebuilder/pull/113) merged at +`e164922c19213ae1ca2936554cca3970269cfac6`, and both environment `EDGE_PROFILE` +values are `shared`. [CD Edge run 35681866962](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35681866962) +at that commit selected production credentials and the shared profile. Its apply +step recreated and started Caddy, and both selected host-local edge checks passed +at **03:27 UTC on 2026-09-22**. This establishes recorded shared-profile adoption, +not just a merged configuration change. + +The [staging run at the same commit](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35681727268) +passed profile preflight and browser smoke by **03:06 UTC**, before that edge +apply. It does not establish application acceptance after adoption. The +[outstanding checks](#outstanding-host-verification) retain that follow-up and +current-host/key-provenance gaps with their owner. + The [successful staging rollout](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35542650898) and [2026-09-20 rollback rehearsal](https://github.com/OpenGameBuilder/opengamebuilder/actions/runs/35542855238) record activation and recovery of an existing installation. The rehearsal From 8fbbe9c70819a317e5e44c95dd91fd4df20ab132 Mon Sep 17 00:00:00 2001 From: Josh Hufford Date: Tue, 22 Sep 2026 09:49:05 -0400 Subject: [PATCH 5/9] Repair development instructions and launch leftovers --- CONTRIBUTING.md | 7 ++- docs/foundation-checklist.md | 17 +++++-- docs/setup/development.md | 50 +++++++++++++++++++ .../Properties/launchSettings.json | 17 +------ .../wwwroot/index.html | 4 +- 5 files changed, 73 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3230069..ee0535d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,12 @@ The repository is intended to work smoothly from a fresh checkout. If the setup ## Repository Standards -The repository includes an `.editorconfig`, and formatting runs on commit. +The repository's [`.editorconfig`](.editorconfig) defines formatting rules. +Formatting verification is required; Git hooks are optional and run only after +you opt in. Follow [development setup](docs/setup/development.md#command-line-workflow-start-here) +for the required validation commands and +[formatting and optional hooks](docs/setup/development.md#formatting-warnings-and-optional-git-hooks) +for applying fixes or enabling the pre-commit hook. Please keep changes consistent with the existing style and project structure. When future coding standards or architecture documentation are added, contributors should follow those documents as well. diff --git a/docs/foundation-checklist.md b/docs/foundation-checklist.md index 54ce663..caae223 100644 --- a/docs/foundation-checklist.md +++ b/docs/foundation-checklist.md @@ -190,11 +190,11 @@ services, or deployment state were changed; no new deployment was performed. although Husky is opt-in. The web project's IIS Express profile advertises origins not allowed by development CORS. Fresh-checkout editor acceptance remains open. -- [ ] Say that format verification is required and hooks are optional; link the +- [x] Say that format verification is required and hooks are optional; link the authoritative setup commands instead of duplicating them. -- [ ] Remove the unsupported IIS Express profile, or explicitly support and test +- [x] Remove the unsupported IIS Express profile, or explicitly support and test it with matching configuration. Preserve the documented direct and Aspire paths. -- [ ] Correct the old `OpenGameBuilder.Web` startup title and scoped-CSS filename +- [x] Correct the old `OpenGameBuilder.Web` startup title and scoped-CSS filename hint in [index.html](../src/OpenGameBuilder.Web.Client/wwwroot/index.html). - [ ] Follow the setup guide from a fresh checkout in Visual Studio and VS Code, including F5, debugger attachment, frontend startup, and the API request. @@ -203,6 +203,17 @@ not allowed by development CORS. Fresh-checkout editor acceptance remains open. documented editor paths work without undocumented steps, Docker, or production credentials; record actual editor verification separately from CLI results. +**Result (2026-09-22, editor acceptance partial):** CONTRIBUTING now requires +format verification and links the optional-hook setup. The unsupported web IIS +Express profile/settings are removed, and the startup title and scoped-CSS hint +match the application and project. Restore, format verification, Release build +(zero warnings), all 72 .NET tests, frontend Release publish, and the portability +guard passed. Fresh-checkout Visual Studio and VS Code F5 rehearsals started the +API and frontend, hit the API breakpoint, and displayed the Development heading. +[Setup verification evidence](setup/development.md#recorded-setup-verification) +records the editor versions, successful checks, and remaining Blazor debugger +acceptance; the full editor item remains unchecked. + ### 6. Remove unused scaffolding and duplicate documentation **Finding:** root placeholders, unused configuration, template examples, and diff --git a/docs/setup/development.md b/docs/setup/development.md index dc396f3..ad8dc0c 100644 --- a/docs/setup/development.md +++ b/docs/setup/development.md @@ -195,6 +195,56 @@ load its application information. Stop the compound to stop both debuggers. In Visual Studio, use **Configure Startup Projects** to select the API and web client for a local multiple-startup configuration instead of the shared Aspire profile. Keep their named project launch profiles and the fixed ports above. +The web client supports the project profile only; there is no IIS Express profile. + +### Verify editor debugging from a fresh checkout + +Use a fresh checkout without copied `.vs`, `bin`, `obj`, or user settings, and +complete the prerequisites and command-line checks above. Rehearse each editor +separately, stopping the previous stack before starting the next one. + +1. In Visual Studio, open the solution and select **Aspire** as described above. + In VS Code, open the repository root and select **Launch All (API + Web)**. +2. Set an API breakpoint in `AboutController.Get` in + [`AboutController.cs`](../../src/OpenGameBuilder.Api/Controllers/AboutController.cs) + and a frontend breakpoint on the `_title` assignment immediately after + `await Client.GetAboutAsync()` in + [`Home.razor.cs`](../../src/OpenGameBuilder.Web.Client/Pages/Home.razor.cs). +3. Press F5. Use the debugger's Edge or Chrome window, rather than an unrelated + browser tab, and confirm the API and browser debug sessions attach. Open + `https://localhost:7001`, confirm that the API breakpoint is hit, and continue. + Blazor's debug proxy can start after the first page's `OnInitializedAsync` + has run, so an initial missed frontend breakpoint is inconclusive. Retry + after the browser debugger is ready and record whether the frontend + breakpoint is hit; see [Microsoft's Blazor debugging guidance](https://learn.microsoft.com/aspnet/core/blazor/debug?view=aspnetcore-10.0#debug-a-blazor-webassembly-app-in-an-ide). +4. Continue execution and perform the browser success check above: the API + request returns 200 and the home heading displays the Development version. +5. Stop debugging and confirm both application processes stop before switching + editors or launch methods. + +Record the source revision, Windows/editor/browser versions, launch profile, +breakpoint results, and browser request result. Build and CLI startup checks +alone do not establish F5, debugger attachment, or fresh-editor acceptance. + +### Recorded setup verification + +On 2026-09-22, a fresh local clone of `5c5fdbaf0930db009d4d9c6a3f3ee5644a689d60` +with the contributor-guidance, web launch-profile, and startup-HTML repairs was +opened without copied editor state or build outputs. The host was Windows 11 +(build 26200), with .NET SDK 10.0.401, Aspire CLI 13.4.2, and an already trusted +development certificate. No Docker or production credentials were needed. + +| Check | Result | +| --- | --- | +| Solution validation in the working checkout | Restore, format verification, Release build (zero warnings), and all 72 tests passed; frontend Release publish and the portability guard also passed. | +| Visual Studio Insiders 18.11.12210.170, shared Aspire profile | F5 built and started the fresh clone; Aspire reported both resources healthy. A browser request hit `AboutController.Get`, and continuing displayed `OpenGameBuilder 0.11.0 (Development)` in Chrome 153.0.8010.53. | +| VS Code 1.138.0, Launch All (API + Web) | F5 started both projects. Reloading the launched Edge page hit `AboutController.Get` in VS Code; the frontend displayed the Development heading. | +| Remaining editor acceptance | The frontend `Home.OnInitializedAsync` breakpoint was not hit in the externally opened Visual Studio Chrome tab. Full Blazor breakpoint verification in each editor's debugger-owned browser remains open; neither API debugging nor the heading alone proves it. | + +The fresh clone's initial CLI restore hit a local NuGet scratch-lock access error; +Visual Studio subsequently restored and built it successfully. This records a +rehearsal on an existing development machine, not a clean-machine installation. +Repeat the debugger procedure above when closing the remaining editor acceptance. ## Formatting, warnings, and optional Git hooks diff --git a/src/OpenGameBuilder.Web.Client/Properties/launchSettings.json b/src/OpenGameBuilder.Web.Client/Properties/launchSettings.json index 6dfab55..ce114ea 100644 --- a/src/OpenGameBuilder.Web.Client/Properties/launchSettings.json +++ b/src/OpenGameBuilder.Web.Client/Properties/launchSettings.json @@ -9,21 +9,6 @@ "dotnetRunMessages": true, "applicationUrl": "https://localhost:7001;http://localhost:5001", "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - }, - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:54419/", - "sslPort": 44313 } } -} \ No newline at end of file +} diff --git a/src/OpenGameBuilder.Web.Client/wwwroot/index.html b/src/OpenGameBuilder.Web.Client/wwwroot/index.html index a0dcfb6..a6a5d78 100644 --- a/src/OpenGameBuilder.Web.Client/wwwroot/index.html +++ b/src/OpenGameBuilder.Web.Client/wwwroot/index.html @@ -4,12 +4,12 @@ - OpenGameBuilder.Web + OpenGameBuilder + --> From 7f12eda4959d463b6aa7812cdd535c33e8fdcd01 Mon Sep 17 00:00:00 2001 From: Josh Hufford Date: Tue, 22 Sep 2026 10:06:04 -0400 Subject: [PATCH 6/9] Remove unused scaffolding and duplicate documentation --- .browserslistrc | 13 -- CHANGELOG.md | 18 ++- CONTRIBUTING.md | 37 ++--- CONTRIBUTORS.md | 11 +- SUPPORT.md | 138 +++++------------- docs/README.md | 8 +- docs/community/discord.md | 37 ++--- docs/community/forum-archive.md | 10 +- docs/foundation-checklist.md | 31 +++- docs/frontend/browser-support.md | 8 +- docs/quality/testing.md | 12 ++ docs/release/README.md | 23 +++ src/OpenGameBuilder.Api/Program.cs | 5 - src/OpenGameBuilder.AppHost/AppHost.cs | 20 +-- .../Extensions.cs | 18 --- .../wwwroot/css/app.css | 25 ---- .../wwwroot/index.html | 2 - 17 files changed, 171 insertions(+), 245 deletions(-) delete mode 100644 .browserslistrc diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index a266268..0000000 --- a/.browserslistrc +++ /dev/null @@ -1,13 +0,0 @@ -# Compatibility-tooling declaration; no current build step consumes this file. -# These queries do not implement or verify browser support. -# See docs/frontend/browser-support.md for targets and recorded checks. - -last 2 Chrome major versions -last 2 Edge major versions -last 2 Firefox major versions -last 2 Safari major versions -last 2 iOS major versions -last 2 Android major versions -not dead -not ie <= 11 -not op_mini all diff --git a/CHANGELOG.md b/CHANGELOG.md index 644270b..7e1b974 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,18 @@ - # Changelog + +Notable changes are curated here. See the [release-note process](docs/release/README.md#curated-release-notes) +for preparing an entry and [GitHub Releases](https://github.com/OpenGameBuilder/opengamebuilder/releases) +for published versions predating this changelog. + +## Unreleased + +### Changed + +- Staging and production can use separate hosts and SSH keys. Each environment + must select its edge profile; see the [hosting configuration](docs/setup/hosting.md). + +### Fixed + +- A failed first deployment can recover to an undeployed state and be retried. + Failed cleanup retains the pending transaction and diagnostics; see + [activation and recovery](docs/setup/hosting.md#application-activation-and-rollback). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee0535d..1d834b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,22 +37,8 @@ tracks work, including the `Triage` status; triage is not an issue label. For small fixes, documentation improvements, typo fixes, straightforward bugs, and focused cleanup, feel free to open a pull request. -For larger changes, please open an issue or -[GitHub Discussion](https://github.com/OpenGameBuilder/opengamebuilder/discussions) -first. Use Discussions for exploratory questions and ideas; private Discord -access is not required to contribute. This is especially important for changes involving: - -- major architecture; -- game runtime behavior; -- editor behavior; -- public APIs; -- data formats; -- compatibility decisions; -- licensing; -- governance; -- community or moderation policy. - -This helps avoid wasted work and gives maintainers a chance to discuss the direction before implementation starts. +For larger changes, follow [Significant Changes](#significant-changes) before +implementation. Private Discord access is not required to contribute. ## Development Setup @@ -73,7 +59,8 @@ for the required validation commands and [formatting and optional hooks](docs/setup/development.md#formatting-warnings-and-optional-git-hooks) for applying fixes or enabling the pre-commit hook. -Please keep changes consistent with the existing style and project structure. When future coding standards or architecture documentation are added, contributors should follow those documents as well. +Keep changes consistent with the existing style and +[project boundaries](docs/README.md#repository-map). Before opening a pull request, please make sure the project builds and tests pass locally when practical. CI/CD also runs tests for pull requests and protected branches. @@ -123,11 +110,17 @@ Draft pull requests are welcome when you want early feedback. ## Significant Changes -Significant changes should usually be discussed before implementation. - -A change is significant if it affects project direction, architecture, compatibility, archival policy, contributor expectations, public APIs, data formats, releases, governance, or how archived MyGameBuilder games are restored, presented, or made playable. - -Significant decisions should leave a written record where practical, such as an issue, discussion, pull request, architecture decision record, or documentation update. +Discuss significant changes before implementation. This includes project +direction, architecture, game runtime or editor behavior, compatibility, public +APIs, data formats, releases, licensing, archival policy, contributor expectations, +governance, and community or moderation policy. + +Use a [GitHub Discussion](https://github.com/OpenGameBuilder/opengamebuilder/discussions) +for exploratory ideas or an [issue](https://github.com/OpenGameBuilder/opengamebuilder/issues/new/choose) +for a concrete proposal. Describe the problem, intended behavior, and scope so +maintainers can agree on the direction before substantial work begins. Record +the decision in the issue, discussion, PR, or a lasting document, including how +it affects the restoration or presentation of archived games when relevant. ## Preservation and Archive Contributions diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index e75515c..d33da00 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1 +1,10 @@ -TODO +# Contributors + +See the [GitHub contributors list](https://github.com/OpenGameBuilder/opengamebuilder/graphs/contributors) +for code and documentation contributors, and the repository's +[issues](https://github.com/OpenGameBuilder/opengamebuilder/issues) and +[discussions](https://github.com/OpenGameBuilder/opengamebuilder/discussions) +for reporting, testing, research, and community contributions. Commit counts do +not capture every contribution. + +To take part, start with the [contribution guide](CONTRIBUTING.md). diff --git a/SUPPORT.md b/SUPPORT.md index 992d640..29e16c9 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,125 +1,65 @@ # Support -This document explains where to get help with OpenGameBuilder and where to report different kinds of issues. - -Please choose the most appropriate channel so maintainers can respond without turning the issue tracker into a junk drawer with a search box. +Choose the route that fits your question or report. OpenGameBuilder is +volunteer-run; response times vary, and private Discord access is not required. ## Questions and Discussion -Use public [GitHub Discussions](https://github.com/OpenGameBuilder/opengamebuilder/discussions) for: - -- general questions; -- project ideas; -- design discussion; -- architecture discussion; -- MyGameBuilder history and context; -- compatibility research; -- "is this worth doing?" conversations; -- help deciding where a contribution belongs. - -Start with [Q&A](https://github.com/OpenGameBuilder/opengamebuilder/discussions/categories/q-a) -for help or [Ideas](https://github.com/OpenGameBuilder/opengamebuilder/discussions/categories/ideas) -for exploratory proposals. The reunion Discord remains private; access is not -required to ask questions or contribute. - -## Bugs - -Use the [Bug Report form](https://github.com/OpenGameBuilder/opengamebuilder/issues/new?template=bug_report.yml) -for reproducible bugs. - -A good bug report includes: - -- what happened; -- what you expected to happen; -- steps to reproduce the problem; -- your operating system and browser, if relevant; -- screenshots, video, logs, or error messages, if useful. +Use [Q&A](https://github.com/OpenGameBuilder/opengamebuilder/discussions/categories/q-a) +for help and [Ideas](https://github.com/OpenGameBuilder/opengamebuilder/discussions/categories/ideas) +for exploratory proposals, design questions, or MyGameBuilder history. -Do not include passwords, tokens, private data, sensitive archival material, or security vulnerability details in public issues. +## Bugs and Feature Requests -## Feature Requests +Use the [issue forms](https://github.com/OpenGameBuilder/opengamebuilder/issues/new/choose) +for reproducible bugs, observed compatibility differences, enhancements to +existing capabilities, and concrete new features. The forms request the evidence +needed for each report. -Use the [issue chooser](https://github.com/OpenGameBuilder/opengamebuilder/issues/new/choose) -when the request is specific and actionable: Enhancement is for improvements to -existing capabilities, and Feature Request is for new capabilities. Use -[Discussions](https://github.com/OpenGameBuilder/opengamebuilder/discussions) -when the idea needs exploration first. +Do not post passwords, tokens, private data, sensitive archival material, or +security vulnerability details in public reports. ## Development Setup Help -Development setup instructions are in: - -[`docs/setup/development.md`](docs/setup/development.md) - -If the setup guide does not work, open an issue and include: - -- your operating system; -- whether you are using Visual Studio or VS Code; -- installed SDK/runtime versions; -- the command or step that failed; -- the full error message, if available. +Start with [development setup](docs/setup/development.md). If a documented step +fails, use the Bug Report form and include the failing step, operating system, +editor, SDK version, and error message with private details removed. ## Contributing -Contribution guidelines are in: - -[`CONTRIBUTING.md`](CONTRIBUTING.md) - -Project governance is described in: - -the organization-wide [governance document](https://github.com/OpenGameBuilder/.github/blob/main/GOVERNANCE.md). - -[Practical stewardship](docs/community/stewardship.md) records current operating -responsibilities and the unfilled backup and independent-reporting roles. - -AI-assisted contribution expectations are described in: - -[`AI_POLICY.md`](AI_POLICY.md) +Read [CONTRIBUTING](CONTRIBUTING.md), including +[significant-change guidance](CONTRIBUTING.md#significant-changes), and the +[AI policy](AI_POLICY.md). Responsibilities and decision-making are described in +[organization governance](https://github.com/OpenGameBuilder/.github/blob/main/GOVERNANCE.md) +and [practical stewardship](docs/community/stewardship.md). ## Security Issues -Do not report security vulnerabilities in public issues, pull requests, the Discord server, comments, or chat channels. - -Follow the private reporting process in: - -[`SECURITY.md`](SECURITY.md) +Use the [private security reporting process](SECURITY.md). Do not report +vulnerabilities in public issues, discussions, pull requests, or chat. ## Code of Conduct Reports -Code of Conduct reports and other sensitive community concerns should be reported privately using the process described in: - -the organization-wide [Code of Conduct](https://github.com/OpenGameBuilder/.github/blob/main/CODE_OF_CONDUCT.md#reporting-an-issue). - -Do not post private, sensitive, identifying, or personal information publicly. +Use the private contact in the +[Code of Conduct](https://github.com/OpenGameBuilder/.github/blob/main/CODE_OF_CONDUCT.md#reporting-an-issue) +for conduct reports and sensitive community concerns. Keep identifying details +and private evidence out of public reports. ## MyGameBuilder Archive and Compatibility Questions -OpenGameBuilder is connected to the history of MyGameBuilder.com, but the completed archive is not maintained in this repository. +Archive changes belong with the maintainers of the separate +[MyGameBuilder archive](https://github.com/OpenGameBuilder/mygamebuilder-archive). +Use application Discussions for historical context and the Compatibility Issue +form for observed application behavior. -Use [Discussions](https://github.com/OpenGameBuilder/opengamebuilder/discussions) -for questions and historical context, or the -[issue forms](https://github.com/OpenGameBuilder/opengamebuilder/issues/new/choose) -for concrete work such as: +Before submitting historical material or fixtures, follow the +[material-intake guidance](docs/community/stewardship.md#material-intake-and-creator-requests) +and [source-material policy](AI_POLICY.md#decompiled-source-and-original-client-material). +Do not attach archived dumps, private user data, or original proprietary assets +unless maintainers have explicitly requested them. -- safe compatibility notes; -- observed behavior differences; -- public historical context; -- documentation improvements; -- recreated behavior examples. - -Do not post archived game dumps, private user data, sensitive material, or original proprietary assets in this repository unless maintainers have explicitly requested them. - -Sensitive archive-related concerns should go to the private contact in the +Creator, privacy, or removal requests go to the private contact in the [Code of Conduct](https://github.com/OpenGameBuilder/.github/blob/main/CODE_OF_CONDUCT.md#reporting-an-issue). -Use that route for creator, privacy, or removal requests; identify the material -and requested action without posting sensitive evidence publicly. Changes to the -separate archive belong with its maintainers. There is currently no designated -independent contact for concerns involving the primary contact. - -## Maintainer Response Expectations - -OpenGameBuilder is a volunteer-run project. Maintainers will do their best to respond, but response times may vary. - -Actionable issues with clear reproduction steps are easier to handle than broad or vague reports. - -If an issue does not receive a response right away, please do not repeatedly ping maintainers. Additional information is welcome when it materially helps move the issue forward. +Identify the material and requested action without publishing sensitive evidence. +There is currently no designated independent contact for concerns involving the +primary contact. diff --git a/docs/README.md b/docs/README.md index b11ca8c..a74e4c8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ milestone's behavior and acceptance criteria remain to be agreed. | Maintain agent instructions and vendored skills | [AI tooling maintenance](setup/ai-tooling.md) and [AI policy](../AI_POLICY.md) | | Check browser and accessibility expectations | [Browser support](frontend/browser-support.md) | | Host, deploy, or recover a release | [Hosting](setup/hosting.md), [GitHub setup](setup/github.md), and [SSH host-key verification](setup/deployment-host-key.md) | -| Prepare a release or patch | [Release process](release/README.md) and [versioning](release/versioning.md) | +| Prepare a release or patch | [Release process](release/README.md), [changelog](../CHANGELOG.md), and [versioning](release/versioning.md) | | Ask a question or report privately | [Support](../SUPPORT.md) and [security](../SECURITY.md) | | Check responsibilities or bring in historical material | [Practical stewardship](community/stewardship.md) | @@ -46,13 +46,13 @@ The original material is separate from this implementation. Start with the [archive pointer](mygamebuilder/data-archive.md) and [format documentation pointer](mygamebuilder/data-formats.md). Follow the [AI/source-material policy](../AI_POLICY.md#decompiled-source-and-original-client-material) -for independent compatibility work. [Forum preservation notes](community/forum-archive.md) -and [reunion Discord information](community/discord.md) provide historical +for independent compatibility work. [Forum preservation pointers](community/forum-archive.md) +and [community contact information](community/discord.md) provide historical context; private chat is not required for contributing. ## General learning resources General framework and tooling references belong in the organization's -[shared learning resources](https://github.com/OpenGameBuilder/.github/tree/6aa0795e068256f672801a75b656473ec82883c1/resources). +[shared learning resources](https://github.com/OpenGameBuilder/.github/tree/main/resources). Keep application-specific instructions here and link to shared material rather than maintaining another reference library. diff --git a/docs/community/discord.md b/docs/community/discord.md index 9e5087f..06fae93 100644 --- a/docs/community/discord.md +++ b/docs/community/discord.md @@ -1,28 +1,15 @@ -The Discord is currently a private community for original MyGameBuilder members. +# MyGameBuilder community -If you were a part of the original community and want to reconnect, please reach out! +The reunion Discord is a private community for original MyGameBuilder members. +For reconnection questions, use the community contact in the organization's +[Code of Conduct](https://github.com/OpenGameBuilder/.github/blob/main/CODE_OF_CONDUCT.md#reporting-an-issue). +Do not post other members' contact details or private conversations publicly. -Current users who have joined include (listed by original forum name): +Application questions and contribution discussions belong in +[GitHub Discussions](https://github.com/OpenGameBuilder/opengamebuilder/discussions); +Discord membership is not required. -- Mr. Pig -- awesomebros. -- Yaomon17 -- Sparten117 -- Nmb910 -- shakeandbake -- TheGreatHero -- VincentStudios -- benimo -- fantasylightning -- gamecat -- domo155 -- comio92 -- samperson -- treskro -- greatfulbliss -- hooliganza -- Eggnog -- sitton76 -- legowego -- TheUltimateYoshi -- starmaster5 +The [historical reunion roster](https://github.com/OpenGameBuilder/opengamebuilder/blob/e164922c19213ae1ca2936554cca3970269cfac6/docs/community/discord.md) +is preserved in repository history. It is not a current membership list. +`ostomachion` owns arranging any further move with the community; no destination +for that roster has been agreed here. diff --git a/docs/community/forum-archive.md b/docs/community/forum-archive.md index 37adea4..e8f95fa 100644 --- a/docs/community/forum-archive.md +++ b/docs/community/forum-archive.md @@ -3,7 +3,11 @@ The original MyGameBuilder forums (mygamebuilder.com/forum) are partially archived through the Internet Archive Wayback Machine. -https://web.archive.org/web/20090101235246/http://mygamebuilder.com/forum +See the [preserved forum snapshot](https://web.archive.org/web/20090101235246/http://mygamebuilder.com/forum). -There are future plans to pull this data using the Wayback CDX Server API and -reconstruct as much as of the forums as possible in a more browseable format. +Preservation tooling and proposals belong in the separate +[MyGameBuilder archive](https://github.com/OpenGameBuilder/mygamebuilder-archive). +This application has no owned forum-reconstruction project or delivery promise. +Follow the archive's contribution guidance before proposing further capture or +publication; a public historical snapshot does not remove privacy or attribution +responsibilities. diff --git a/docs/foundation-checklist.md b/docs/foundation-checklist.md index caae223..1423b03 100644 --- a/docs/foundation-checklist.md +++ b/docs/foundation-checklist.md @@ -219,20 +219,20 @@ acceptance; the full editor item remains unchecked. **Finding:** root placeholders, unused configuration, template examples, and repeated contributor prose still create maintenance work without current benefit. -- [ ] Remove or give a useful contributors pointer to `CONTRIBUTORS.md`. Replace +- [x] Remove or give a useful contributors pointer to `CONTRIBUTORS.md`. Replace the empty `CHANGELOG.md` with the curated release process in step 18. -- [ ] Remove `.browserslistrc` while it has no consumer and update its references. +- [x] Remove `.browserslistrc` while it has no consumer and update its references. Keep the actual [browser policy and acceptance matrix](frontend/browser-support.md). -- [ ] Remove unused gRPC/Azure/service-discovery examples, unused Bootstrap/form +- [x] Remove unused gRPC/Azure/service-discovery examples, unused Bootstrap/form CSS, and stale template hints. Shorten AppHost history while retaining the explanation of its current fixed-port, CORS, and launch-profile constraints. -- [ ] Consolidate significant-change guidance in CONTRIBUTING. Keep SUPPORT +- [x] Consolidate significant-change guidance in CONTRIBUTING. Keep SUPPORT focused on choosing a contact route; link policies and forms instead of repeating them. Retain short source-material/privacy reminders at submission points. -- [ ] Replace local reunion rosters and unowned forum-reconstruction plans with +- [x] Replace local reunion rosters and unowned forum-reconstruction plans with useful pointers to the owning archive/community location. Preserve substantive material until an appropriate destination is agreed. -- [ ] Finish the reviewed shared-resource move tracked by [issue #56](https://github.com/OpenGameBuilder/opengamebuilder/issues/56) +- [x] Finish the reviewed shared-resource move tracked by [issue #56](https://github.com/OpenGameBuilder/opengamebuilder/issues/56) and [organization PR #1](https://github.com/OpenGameBuilder/.github/pull/1), then replace the temporary shared-content pointer. Recheck their status first. @@ -240,6 +240,25 @@ repeated contributor prose still create maintenance work without current benefit affected local paths/anchors resolve. Code/style removal preserves existing behavior. Record cross-repository completion separately from local cleanup. +**Result (2026-09-22):** Root contributor and changelog placeholders now provide +useful contributor links and a [curated release-note process](release/README.md#curated-release-notes). +Unused browser configuration, template examples, and form styles are removed; +contribution/support guidance is consolidated. Community/archive pointers retain +the historical roster at an existing public revision without claiming current +membership or promising forum reconstruction. The +[local cleanup checks](quality/testing.md#frontend-asset-cleanup-evidence) passed, +including the solution gate, 72 tests, frontend publish, configuration guard, +and local documentation paths/anchors. No new browser acceptance was performed. + +Cross-repository result: [organization PR #1](https://github.com/OpenGameBuilder/.github/pull/1) +was merged after standards and scope reviews found no issues; all 211 original +external links, the source license, and local links were verified. Its published +resources tree matches the reviewed revision, and the documentation index now +links to `main/resources`. [Issue #56](https://github.com/OpenGameBuilder/opengamebuilder/issues/56) +remains open until the application-side removal and pointer changes reach `main`; +these local changes have not been published. Automated changelog selection for +releases remains in step 18. + ## Phase 3: Deliver the first engine slice Begin this once the supported development path is usable. Do not wait for optional diff --git a/docs/frontend/browser-support.md b/docs/frontend/browser-support.md index fb103e3..f4f5906 100644 --- a/docs/frontend/browser-support.md +++ b/docs/frontend/browser-support.md @@ -39,11 +39,9 @@ decide and document the intended mobile editing scope when editor controls exist Until these checks are recorded, browser coverage and mobile editing remain targets, not an acceptance claim. -[`.browserslistrc`](../../.browserslistrc) is a declaration for compatibility -tooling. No current build step consumes it. The file does not add polyfills, -transform application code, select test browsers, or verify compatibility. Its -existing queries are not a test record or a substitute for this matrix; review -their resolved targets if a build consumer is introduced. +This matrix is the browser policy. The current build has no Browserslist consumer; +browser targets are checked through the acceptance procedure below, not selected +or verified by build configuration. ## Accessibility baseline and outstanding work diff --git a/docs/quality/testing.md b/docs/quality/testing.md index 00b4f62..7b932ef 100644 --- a/docs/quality/testing.md +++ b/docs/quality/testing.md @@ -26,6 +26,18 @@ only its own production references and additional dependencies. Package versions remain in [`Directory.Packages.props`](../../Directory.Packages.props), and the same compiler warnings-as-errors policy applies to tests and production code. +## Frontend asset cleanup evidence + +On 2026-09-22, source inspection found no consumers for the removed form-validation, +Bootstrap placeholder, or `code` styles in +[`app.css`](../../src/OpenGameBuilder.Web.Client/wwwroot/css/app.css). +Loading and error styles remain. C# and startup HTML changes were limited to +comments; active service defaults, endpoints, and launch profiles were preserved. +Restore, format verification, Release build (zero warnings), all 72 .NET tests, +frontend Release publish, and the published-configuration guard passed. Local +documentation paths and anchors also resolved. This establishes local cleanup +and packaging evidence, not a new browser or accessibility acceptance result. + ## CodeQL build coverage The repository-owned [CodeQL workflow](../../.github/workflows/codeql.yml) traces diff --git a/docs/release/README.md b/docs/release/README.md index 4d8fd30..58c11d2 100644 --- a/docs/release/README.md +++ b/docs/release/README.md @@ -39,6 +39,29 @@ source to a commit; tags, arbitrary SHAs, and unprotected branches are rejected. The workflow rejects non-`main` dispatches before source resolution. See [deployment authority](../setup/github.md#deployment-authority-and-recovery). +## Curated release notes + +[`CHANGELOG.md`](../../CHANGELOG.md) is the curated account of notable changes. +Keep an `Unreleased` section for observable application, contributor, and operator +changes. Include breaking behavior and migration or configuration steps where +needed; leave out mechanical dependency and formatting noise. Published releases +before this changelog remain documented in GitHub Releases. + +Before dispatching a production release, review the entry against the selected +source and version in `Directory.Build.props`, then replace `Unreleased` with that +version and its planned release date in the release-source PR. A patch entry +belongs on its patch branch and returns to `main` through the existing merge-back. +A changelog heading records prepared notes, not proof that deployment succeeded; +GitHub Releases records publication. + +The current workflow still generates GitHub Release notes from PRs; it does not +read the changelog. After successful publication, the release maintainer copies +the selected version's curated entry into the release body, retaining generated +PR links as supplemental references or credits. Correct the entry here first +when updating the published summary so there is one maintained account. This +manual notes step does not change deployment validation, tag timing, version +ownership, or the requirement for release authorization. + ## Standard release (X.Y.0) 1. `main` already has `X.Y.0` (set by the diff --git a/src/OpenGameBuilder.Api/Program.cs b/src/OpenGameBuilder.Api/Program.cs index d79c1d8..34d97c7 100644 --- a/src/OpenGameBuilder.Api/Program.cs +++ b/src/OpenGameBuilder.Api/Program.cs @@ -5,13 +5,9 @@ var builder = WebApplication.CreateBuilder(args); -// Add .NET Aspire service defaults (OpenTelemetry, health checks, service discovery, resilient HTTP). builder.AddServiceDefaults(); -// Add services to the container. - builder.Services.AddControllers(); -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); if (!builder.Environment.IsDevelopment()) { @@ -59,7 +55,6 @@ // endpoints in development only. app.MapDefaultEndpoints(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); diff --git a/src/OpenGameBuilder.AppHost/AppHost.cs b/src/OpenGameBuilder.AppHost/AppHost.cs index 72176ed..34adb12 100644 --- a/src/OpenGameBuilder.AppHost/AppHost.cs +++ b/src/OpenGameBuilder.AppHost/AppHost.cs @@ -1,21 +1,9 @@ var builder = DistributedApplication.CreateBuilder(args); -// This solution pairs an ASP.NET Core API with a *standalone* Blazor WebAssembly app. The WASM -// app runs entirely in the browser and cannot read Aspire's service-discovery environment -// variables. Development overrides the page origin with https://localhost:7000. -// -// By default Aspire fronts every endpoint with a DCP reverse proxy on a dynamically-assigned host -// port. That breaks this setup two ways: the API's public port no longer matches the Development -// override, and the browser page itself is served from an unpredictable origin whose dev-cert binding the -// browser silently rejects for cross-origin fetches. Either way the request dies as -// "TypeError: Failed to fetch" before it reaches Kestrel. -// -// WithHttpsEndpoint(isProxied: false) disables the proxy and binds each project directly to the fixed -// port declared in its launch profile, so the static client config and the API's CORS origins stay -// valid with no code changes. The launch profile is also passed explicitly so each project keeps its -// profile-defined bindings (and, for the WASM dev server, its /_framework/debug/ws-proxy inspectUri; -// without it Aspire launches with --no-launch-profile and Visual Studio reports -// "Failed to launch debug adapter"). +// The standalone WASM client reads its development API origin from static configuration, so the +// browser and API must keep fixed origins that match the API's CORS policy. Direct endpoints retain +// the launch-profile ports instead of Aspire's dynamic proxy ports. Passing each launch profile +// explicitly also preserves its bindings and the WASM dev server's debugger inspectUri. var api = builder.AddProject("api", launchProfileName: "OpenGameBuilder.Api") .WithHttpsEndpoint(port: 7000, targetPort: 7000, isProxied: false); diff --git a/src/OpenGameBuilder.ServiceDefaults/Extensions.cs b/src/OpenGameBuilder.ServiceDefaults/Extensions.cs index eb3a73e..a4cbde1 100644 --- a/src/OpenGameBuilder.ServiceDefaults/Extensions.cs +++ b/src/OpenGameBuilder.ServiceDefaults/Extensions.cs @@ -11,9 +11,6 @@ namespace OpenGameBuilder.ServiceDefaults; -// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. -// This project should be referenced by each service project in your solution. -// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { private const string HealthEndpointPath = "/health"; @@ -43,12 +40,6 @@ public TBuilder AddServiceDefaults() http.AddServiceDiscovery(); }); - // Uncomment the following to restrict the allowed schemes for service discovery. - // builder.Services.Configure(options => - // { - // options.AllowedSchemes = ["https"]; - // }); - return builder; } @@ -71,8 +62,6 @@ public TBuilder ConfigureOpenTelemetry() { tracing.AddSource(builder.Environment.ApplicationName) .AddAspNetCoreInstrumentation() - // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) - //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -90,13 +79,6 @@ private TBuilder AddOpenTelemetryExporters() builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) - //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - //} - return builder; } diff --git a/src/OpenGameBuilder.Web.Client/wwwroot/css/app.css b/src/OpenGameBuilder.Web.Client/wwwroot/css/app.css index 3b9a456..ff7be73 100644 --- a/src/OpenGameBuilder.Web.Client/wwwroot/css/app.css +++ b/src/OpenGameBuilder.Web.Client/wwwroot/css/app.css @@ -1,15 +1,3 @@ -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid red; -} - -.validation-message { - color: red; -} - #blazor-error-ui { color-scheme: light only; background: lightyellow; @@ -74,16 +62,3 @@ .loading-progress-text:after { content: var(--blazor-load-percentage-text, "Loading"); } - -code { - color: #c02d76; -} - -.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { - color: var(--bs-secondary-color); - text-align: end; -} - -.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { - text-align: start; -} \ No newline at end of file diff --git a/src/OpenGameBuilder.Web.Client/wwwroot/index.html b/src/OpenGameBuilder.Web.Client/wwwroot/index.html index a6a5d78..5421b0a 100644 --- a/src/OpenGameBuilder.Web.Client/wwwroot/index.html +++ b/src/OpenGameBuilder.Web.Client/wwwroot/index.html @@ -8,8 +8,6 @@ - From ccd1d18ba8ce5ef7065b4efbc8c21c75a3d67713 Mon Sep 17 00:00:00 2001 From: Josh Hufford Date: Tue, 22 Sep 2026 10:29:40 -0400 Subject: [PATCH 7/9] Make setup and validation reproducible --- .github/actions/validate/action.yml | 105 +-- .github/workflows/_deploy.yml | 13 +- .github/workflows/codeql.yml | 3 +- Directory.Build.targets | 11 + docs/foundation-checklist.md | 20 +- docs/quality/testing.md | 109 ++- docs/setup/development.md | 84 +- global.json | 3 +- scripts/check.ps1 | 90 ++ scripts/doctor.ps1 | 178 ++++ scripts/tooling.ps1 | 48 ++ src/OpenGameBuilder.Api/Dockerfile | 1 + .../OpenGameBuilder.Api.csproj | 4 + src/OpenGameBuilder.Api/packages.lock.json | 210 +++++ .../OpenGameBuilder.AppHost.csproj | 3 + .../packages.linux-x64.lock.json | 704 ++++++++++++++++ .../packages.win-x64.lock.json | 704 ++++++++++++++++ .../OpenGameBuilder.Web.Client.csproj | 1 + .../packages.lock.json | 311 +++++++ tests/Directory.Build.props | 1 + .../packages.lock.json | 302 +++++++ .../packages.lock.json | 774 ++++++++++++++++++ 22 files changed, 3523 insertions(+), 156 deletions(-) create mode 100644 scripts/check.ps1 create mode 100644 scripts/doctor.ps1 create mode 100644 scripts/tooling.ps1 create mode 100644 src/OpenGameBuilder.Api/packages.lock.json create mode 100644 src/OpenGameBuilder.AppHost/packages.linux-x64.lock.json create mode 100644 src/OpenGameBuilder.AppHost/packages.win-x64.lock.json create mode 100644 src/OpenGameBuilder.Web.Client/packages.lock.json create mode 100644 tests/OpenGameBuilder.Api.Client.Tests/packages.lock.json create mode 100644 tests/OpenGameBuilder.Api.Tests/packages.lock.json diff --git a/.github/actions/validate/action.yml b/.github/actions/validate/action.yml index 4a3ffbe..ab933f0 100644 --- a/.github/actions/validate/action.yml +++ b/.github/actions/validate/action.yml @@ -33,112 +33,17 @@ runs: with: global-json-file: ${{ inputs.working-directory }}/global.json - - name: Prepare validation diagnostics - shell: bash - working-directory: ${{ inputs.working-directory }} - run: mkdir -p artifacts/validation - - - name: Restore - shell: bash - working-directory: ${{ inputs.working-directory }} - run: dotnet restore opengamebuilder.slnx 2>&1 | tee artifacts/validation/restore.log - - - name: Verify formatting - shell: bash - working-directory: ${{ inputs.working-directory }} - run: > - dotnet format opengamebuilder.slnx --verify-no-changes --no-restore - --report artifacts/validation/format.json - 2>&1 | tee artifacts/validation/format.log - - - name: Build - shell: bash - working-directory: ${{ inputs.working-directory }} - run: > - dotnet build opengamebuilder.slnx --configuration Release --no-restore - 2>&1 | tee artifacts/validation/build.log - - - name: Test - shell: bash - working-directory: ${{ inputs.working-directory }} - run: > - dotnet test --solution opengamebuilder.slnx --configuration Release --no-build - 2>&1 | tee artifacts/validation/test.log - - - name: Publish web client - if: ${{ inputs.publish-web == 'true' }} - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - dotnet publish src/OpenGameBuilder.Web.Client/OpenGameBuilder.Web.Client.csproj \ - --configuration Release --no-build --output artifacts/web \ - 2>&1 | tee artifacts/validation/web-publish.log - - - name: Verify portable web configuration - if: ${{ inputs.publish-web == 'true' }} - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - bash scripts/verify-web-publish.sh artifacts/web/wwwroot \ - 2>&1 | tee artifacts/validation/web-configuration.log - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '22' package-manager-cache: false - - name: Validate browser smoke dependencies - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - npm ci --prefix tests/deploy-smoke \ - 2>&1 | tee artifacts/validation/smoke-dependencies.log - - - name: Check browser smoke syntax - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - node --check tests/deploy-smoke/smoke.mjs \ - 2>&1 | tee artifacts/validation/smoke-syntax.log - - - name: Test release scripts without external writes - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - bash tests/release-scripts/run.sh 2>&1 | tee artifacts/validation/release-scripts.log - - - name: Test edge deployment without Docker or SSH - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - bash tests/deploy-edge/run.sh 2>&1 | tee artifacts/validation/deploy-edge.log - - - name: Test shared and separate host configuration without a Docker daemon - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - bash tests/deploy-topology/run.sh 2>&1 | tee artifacts/validation/deploy-topology.log - - - name: Test application rollback without Docker or SSH - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - bash tests/deploy-app/run.sh 2>&1 | tee artifacts/validation/deploy-app.log - - - name: Verify supply-chain pins - shell: bash + - name: Run shared full check + shell: pwsh working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - bash tests/supply-chain/run.sh 2>&1 | tee artifacts/validation/supply-chain.log + env: + PUBLISH_WEB: ${{ inputs.publish-web }} + run: ./scripts/check.ps1 full -SkipWebPublish:($env:PUBLISH_WEB -eq 'false') - name: Upload validation failure diagnostics if: ${{ failure() && !cancelled() }} diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml index 3a66cd1..3abd160 100644 --- a/.github/workflows/_deploy.yml +++ b/.github/workflows/_deploy.yml @@ -97,11 +97,10 @@ jobs: cache: false - name: Publish web client - run: > - dotnet publish - src/OpenGameBuilder.Web.Client/OpenGameBuilder.Web.Client.csproj - --configuration Release - --output ./artifacts/web + run: | + dotnet --version + dotnet publish src/OpenGameBuilder.Web.Client/OpenGameBuilder.Web.Client.csproj \ + --configuration Release -p:RestoreLockedMode=true --output ./artifacts/web - name: Locate web publish output run: | @@ -301,7 +300,7 @@ jobs: run: | npm ci --prefix tests/deploy-smoke cd tests/deploy-smoke - npx playwright install --with-deps chromium + node node_modules/playwright/cli.js install --with-deps chromium - name: Transfer candidate release env: @@ -360,7 +359,7 @@ jobs: echo "Repository variable '${{ inputs.smoke-test-url-var }}' is not set." >&2 exit 1 fi - node tests/deploy-smoke/smoke.mjs + pwsh -NoProfile -File scripts/check.ps1 browser - name: Rehearse staging rollback after a successful browser check if: ${{ inputs.environment-slug == 'staging' && inputs.rehearse-rollback }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9747d90..9f8399b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -67,7 +67,8 @@ jobs: if: matrix.build-mode == 'manual' shell: bash run: | - dotnet restore opengamebuilder.slnx + dotnet --version + dotnet restore opengamebuilder.slnx --locked-mode dotnet build opengamebuilder.slnx --configuration Release --no-restore --no-incremental - name: Perform CodeQL Analysis diff --git a/Directory.Build.targets b/Directory.Build.targets index 4c83226..d32db06 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -4,4 +4,15 @@ true + + + + <_RequiredNuGetLockFile Condition="'$(NuGetLockFilePath)' == ''">packages.lock.json + <_RequiredNuGetLockFile Condition="'$(NuGetLockFilePath)' != ''">$(NuGetLockFilePath) + + + + diff --git a/docs/foundation-checklist.md b/docs/foundation-checklist.md index 1423b03..71dfede 100644 --- a/docs/foundation-checklist.md +++ b/docs/foundation-checklist.md @@ -397,16 +397,16 @@ This step does not block engine development or justify further deployment expans ### 13. Make setup and validation reproducible -- [ ] Add a small `doctor` command that reports selected SDK/tool versions, +- [x] Add a small `doctor` command that reports selected SDK/tool versions, missing prerequisites, and actionable remedies. Keep installation and trust changes explicit; ordinary checks must not change the developer's environment. -- [ ] Provide documented formatting, quick-check, full-check, and browser-test +- [x] Provide documented formatting, quick-check, full-check, and browser-test commands, with shared implementations used locally and by CI. Keep the headless build/test path usable without deployment credentials or running services. -- [ ] Replace accidental SDK drift from `global.json`'s `latestMinor` roll-forward +- [x] Replace accidental SDK drift from `global.json`'s `latestMinor` roll-forward with a deliberate supported baseline for formatting, analyzers, and builds. Log the selected SDK and update it through reviewed dependency changes. -- [ ] Introduce committed NuGet lockfiles for application entry points and locked +- [x] Introduce committed NuGet lockfiles for application entry points and locked CI restores after SDK selection is settled. Keep npm tools exactly pinned with committed lockfiles. Document how intentional dependency updates refresh them; a library lockfile does not constrain downstream consumers. @@ -416,6 +416,18 @@ toolchain. Missing prerequisites produce useful diagnostics, CI rejects dependen drift, and local/CI checks have equivalent scope. A fresh editor rehearsal remains the separate acceptance in step 5. See [NuGet lockfiles](https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#locking-dependencies). +**Result (2026-09-22):** The read-only doctor and shared formatting, quick, full, +and browser commands are documented in [development setup](setup/development.md) +and used by CI. SDK selection is exact; application and test entry points have +committed locks, including separate Windows/Linux AppHost graphs. Locked restores +also cover CodeQL and packaging. A fresh source snapshot passed the full local +gate with serialized MSBuild: zero build warnings, 72 tests, frontend packaging, +smoke-package checks, and all five shell suites. Missing prerequisites, incorrect +versions, and missing/stale dependency locks were rejected. The +[validation evidence](quality/testing.md#reproducible-command-validation) separates +these results from the unverified hosted CI and live browser runs. No deployment +or editor rehearsal was performed. + ### 14. Format and lint first-party content consistently - [ ] Keep `dotnet format` and existing analyzers for C#. Promote selected useful diff --git a/docs/quality/testing.md b/docs/quality/testing.md index 7b932ef..e014c90 100644 --- a/docs/quality/testing.md +++ b/docs/quality/testing.md @@ -66,8 +66,25 @@ analysis or claiming this repository change fixed that managed workflow. See [deployment validation](../../.github/workflows/_deploy.yml) use the same [validation action](../../.github/actions/validate/action.yml), so restore, formatting verification, Release build, solution tests, and smoke-package checks -cannot drift between the two paths. The solution commands are in -[developer setup](../setup/development.md#command-line-workflow-start-here). +cannot drift between the two paths. Start with the canonical local commands: + +```pwsh +pwsh ./scripts/check.ps1 quick +pwsh ./scripts/check.ps1 full +pwsh ./scripts/check.ps1 full -Serial +``` + +`quick` is the normal solution gate: quick doctor checks, locked restore, +format verification, Release build, and the current 72 tests. `full` includes +that gate plus frontend Release publish and its portability guard, `npm ci` and +the smoke-script syntax check, and all five existing Bash script suites. It +requires Git for Windows/Git Bash, Git, Node.js 22/npm, and the Docker CLI with +Compose support. It does not require a Docker daemon, deployment credentials, +services, or a browser. + +Use `-Serial` when Windows task-host or pipe contention affects validation. It +serializes restore, build, and publish work and disables MSBuild node reuse; +the checks and their scope are otherwise unchanged. PR validation also publishes the frontend in Release using the completed build and runs [the portability guard](../../scripts/verify-web-publish.sh). Deployment @@ -75,22 +92,12 @@ disables this extra publish in the shared action because its required `package` job already publishes and checks the frontend before any deployment job runs. Both paths reject environment-specific configuration in the published artifact. -After the solution gate, run these local equivalents from the repository root -with Git Bash and Node.js 22/npm available: - -```pwsh -dotnet publish src/OpenGameBuilder.Web.Client/OpenGameBuilder.Web.Client.csproj --configuration Release --no-build --output artifacts/web -& 'C:\Program Files\Git\bin\bash.exe' scripts/verify-web-publish.sh artifacts/web/wwwroot -npm ci --prefix tests/deploy-smoke -node --check tests/deploy-smoke/smoke.mjs -``` - -The Node checks install the locked smoke dependencies and parse `smoke.mjs`; -they do not install or launch a browser. These build/package checks need no -deployment credentials or public URL and do not establish browser acceptance. -The deployment job still installs dependencies and Chromium on its own runner -before activation. Live browser smoke runs after activation and can trigger -recovery if it fails. +The Node portion of `full` installs the locked smoke dependencies and parses +`smoke.mjs`; it does not install or launch a browser. These build/package checks +need no deployment credentials or public URL and do not establish browser +acceptance. The deployment job installs its dependencies and Chromium on its own +runner before activation. Live browser smoke runs after activation and can +trigger recovery if it fails. A failing phase fails the job. Available console logs and a formatting report are uploaded on failure and retained for seven days; assertion details are in @@ -99,13 +106,15 @@ are uploaded on failure and retained for seven days; assertion details are in the new checks when they run. The test command uses the repository's Microsoft.Testing.Platform runner without adding a separate test-reporting dependency. -Release-script behavior has an additional Bash test gate: +The five Bash suites exercised by `full` are `release-scripts`, `deploy-edge`, +`deploy-topology`, `deploy-app`, and `supply-chain`. Run an individual suite +with Git Bash when working on it, for example: ```pwsh -bash tests/release-scripts/run.sh +& 'C:\Program Files\Git\bin\bash.exe' tests/release-scripts/run.sh ``` -Run this with Git Bash on Windows. The suite creates temporary local Git +The release-script suite creates temporary local Git repositories, mocks GitHub CLI calls and Git pushes, and never publishes a branch, tag, or release. CI and deployment validation run it after the solution tests and upload its log on failure. @@ -154,8 +163,54 @@ Deployment additionally runs a Chromium smoke test from `tests/deploy-smoke` tha loads the published frontend, observes its API request, and checks the expected source revision. That live test requires a deployed staging or production URL. +### Reproducible command validation + +On 2026-09-22, a fresh source snapshot passed `pwsh ./scripts/check.ps1 full -Serial` +with SDK 10.0.401, PowerShell 7.6.5, and Node.js 22.23.2/npm 10.9.9. This covered +locked restore, format verification, a Release build with zero warnings, all 72 +.NET tests, frontend Release publish and its portability guard, locked smoke +dependencies and syntax, and all five isolated shell suites. The serialized +option avoided a Windows MSBuild task-host failure; it did not omit checks. + +Both Windows and Linux AppHost graphs passed locked restore; the Linux graph was +selected explicitly on Windows, not executed on a Linux host. Deliberately +missing entry-point locks and changed package requirements stopped the shared +quick command at restore, before build. Doctor fixtures rejected a missing SDK, +wrong SDK selection/policy, Node 24, and non-exact or mismatched Playwright pins. +Missing smoke URL inputs failed before launching Chromium. PowerShell/YAML +parsing, changed documentation targets/anchors, and diff checks passed. + +The shared commands and workflow wiring have local evidence. Hosted `build-test`, +CodeQL, Docker image packaging, and live browser smoke after these changes remain +unverified. No services, deployments, certificate trust changes, or editor +rehearsals were performed for this validation. + ### Browser-smoke dependency updates +`pwsh ./scripts/check.ps1 browser` runs the existing +[`smoke.mjs`](../../tests/deploy-smoke/smoke.mjs) Chromium test against an +authorized, already deployed HTTPS release. It requires the pinned Playwright +Chromium browser to be installed and these environment variables: + +```pwsh +$env:SMOKE_TEST_BASE_URL = 'https://authorized-release.example' +$env:EXPECTED_SOURCE_SHA = '' +$env:EXPECTED_RELEASE_ID = '' +pwsh ./scripts/check.ps1 browser +``` + +Install the pinned test dependency and Chromium explicitly when needed: + +```pwsh +npm ci --prefix tests/deploy-smoke +node tests/deploy-smoke/node_modules/playwright/cli.js install chromium +``` + +The browser command never deploys, installs a browser, or installs operating +system packages. On Linux, `--with-deps` remains an explicit operating-system +installation decision, as in the workflow. This smoke is evidence for the +specified release only; it is not broader browser or hosted acceptance. + [Dependabot](../../.github/dependabot.yml) checks `/tests/deploy-smoke` weekly, using the repository's dependency-update cadence and cooldowns. Its npm entry targets the directory containing both `package.json` and `package-lock.json`, as @@ -163,9 +218,15 @@ described in [GitHub's configuration reference](https://docs.github.com/en/code- After merging configuration changes, check GitHub's Dependabot update-job list for that npm directory and inspect its first run for configuration errors. -For Playwright updates, review the release notes and the manifest/lockfile diff, -then run the `npm ci` and syntax commands above with Node.js 22. Dependency PRs -receive the same required `build-test` validation. Package installation and +For an intentional Playwright update, use the exact-version command below, +review the release notes and manifest/lockfile diff, then run `full` with Node.js +22. Dependency PRs receive the same required `build-test` validation. + +```pwsh +npm install --save-dev --save-exact playwright@ --prefix tests/deploy-smoke +``` + +Package installation and syntax checks do not establish compatibility with the updated Chromium build. Review the browser smoke result from an authorized staging deployment: the release URL and base path, successful `/api/about` request, expected source diff --git a/docs/setup/development.md b/docs/setup/development.md index ad8dc0c..433acf9 100644 --- a/docs/setup/development.md +++ b/docs/setup/development.md @@ -2,16 +2,20 @@ ## Supported environment and prerequisites -The supported editor workflow in this guide is **Windows 11**, using PowerShell +The supported editor workflow in this guide is **Windows 11**, using +[PowerShell 7](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows) with either Visual Studio 2026 or VS Code. Linux, macOS, and WSL development are not yet validated by this guide; Linux CI builds do not establish editor or browser-certificate support on those platforms. +- [PowerShell 7](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows), + invoked as `pwsh`. - [Git for Windows](https://git-scm.com/download/win). -- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0), version - **10.0.401 or a compatible later 10.0 feature band**, as selected by - [`global.json`](../../global.json). Run `dotnet --version` from the repository - root to check the selected SDK. Update Visual Studio if its bundled SDK is older. +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0), exactly + **10.0.401**, as selected by [`global.json`](../../global.json). The repository + disables SDK roll-forward and prerelease selection so a different feature band + is a setup failure, not a substitute. Update Visual Studio if its bundled SDK + is older. - **Aspire CLI 13.4.2**, the version used for this workflow and the AppHost SDK. The SDK and hosting package are separately versioned in the [AppHost project](../../src/OpenGameBuilder.AppHost/OpenGameBuilder.AppHost.csproj) @@ -37,8 +41,10 @@ development server. **Docker, a database, production credentials, and Discord access are not required.** Aspire is the local launcher, not the production deployment mechanism. -When updating the SDK requirement in `global.json`, keep the API Dockerfile's -SDK image compatible. CI builds the API image without pushing it. +When updating the SDK requirement in `global.json`, review the compatible pinned +SDK image in the API Dockerfile and the generated dependency lockfiles together. +Dependabot SDK updates are reviewed with those files; CI builds the API image +without pushing it. ## Command-line workflow (start here) @@ -47,8 +53,25 @@ Clone the repository, then run the remaining commands from its root: ```pwsh git clone https://github.com/OpenGameBuilder/opengamebuilder.git Set-Location opengamebuilder -dotnet --version -aspire --version +pwsh ./scripts/doctor.ps1 +pwsh ./scripts/check.ps1 quick +``` + +`doctor.ps1` is a read-only prerequisite report. Its default `development` +scope reports PowerShell, the SDK, Aspire CLI, Git, and Bash with their remedies; +it does not install tools, trust certificates, start services, or open a browser. +Use `-Scope quick`, `full`, `browser`, or `development` when checking a narrower +workflow. + +The normal solution gate is `pwsh ./scripts/check.ps1 quick`: it runs the quick +doctor check, a locked restore, C# formatting verification, a Release build, and +the current 72 solution tests. `check.ps1 format` performs only the formatting +verification after its locked restore. To apply formatter changes deliberately, +run `pwsh ./scripts/check.ps1 format -Fix`, then review the diff. + +The initial browser-debugging setup is an explicit, interactive operation: + +```pwsh dotnet dev-certs https --trust dotnet dev-certs https --check --trust ``` @@ -59,16 +82,6 @@ connections. See Microsoft's [development certificate guidance](https://learn.microsoft.com/aspnet/core/security/enforcing-ssl#trust-the-aspnet-core-https-development-certificate) if the check fails. -Restore, check formatting, build, and test before starting services: - -```pwsh -dotnet restore opengamebuilder.slnx -dotnet format opengamebuilder.slnx --verify-no-changes --no-restore -dotnet build opengamebuilder.slnx --configuration Debug --no-restore -dotnet build opengamebuilder.slnx --configuration Release --no-restore -dotnet test --solution opengamebuilder.slnx --configuration Release --no-build -``` - Ordinary builds do not restore local tools or install Git hooks. The formatter ships with the .NET SDK; no Husky installation is needed for these checks. CI and shared deployment validation run the same formatting verification with @@ -78,6 +91,39 @@ Tests use Microsoft.Testing.Platform, selected in `global.json`. To run only one test project, replace `--solution opengamebuilder.slnx` with, for example, `--project tests\OpenGameBuilder.Api.Tests\OpenGameBuilder.Api.Tests.csproj`. +Direct `dotnet restore`, `build`, `test`, and `format` commands remain useful for +focused editor work. Use `--locked-mode` for a manual restore that must reproduce +the committed graph. After an intentional SDK or +[`Directory.Packages.props`](../../Directory.Packages.props) change, refresh +locks with: + +```pwsh +dotnet restore opengamebuilder.slnx --force-evaluate -p:RestoreLockedMode=false +``` + +Review every generated lockfile and run the full check before committing. See +[NuGet's lock-file documentation](https://learn.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies) +for the restore model. The API, Web Client, and two test entry points commit +`packages.lock.json`; shared-library locks cannot constrain the graph selected +by a downstream consuming application, so shared libraries do not duplicate +them. The AppHost's implicit SDK packages vary by host, so it commits reviewed +`packages.win-x64.lock.json` and `packages.linux-x64.lock.json` baselines. For an +intentional dependency refresh, update the native graph with the solution command +above, then refresh the Linux AppHost graph and confirm the native graph remains +locked: + +```pwsh +dotnet restore src/OpenGameBuilder.AppHost/OpenGameBuilder.AppHost.csproj --force-evaluate -p:RestoreLockedMode=false -p:NETCoreSdkRuntimeIdentifier=linux-x64 -m:1 +dotnet restore opengamebuilder.slnx --locked-mode +``` + +Refresh each AppHost lock on its matching host, or review an explicit +cross-target restore as above. A new host platform needs its own reviewed +AppHost lock before it is supported. `Directory.Build.targets` rejects a missing +entry-point lock before a locked restore can create one; `--locked-mode` then +rejects stale dependency graphs. CI restores with `--locked-mode`, and packaging's +implicit restore is locked as well. + The root `NuGet.Config` deliberately has one source, `nuget.org`, and clears both inherited package sources and inherited package-source mappings. Its `*` mapping means every package uses that one feed; it is not a claim of namespace diff --git a/global.json b/global.json index d323864..cf3f086 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,8 @@ { "sdk": { "version": "10.0.401", - "rollForward": "latestMinor" + "rollForward": "disable", + "allowPrerelease": false }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/scripts/check.ps1 b/scripts/check.ps1 new file mode 100644 index 0000000..c65d0ac --- /dev/null +++ b/scripts/check.ps1 @@ -0,0 +1,90 @@ +#Requires -Version 7.0 +<# +.SYNOPSIS +Runs the same validation locally and in CI; see docs/quality/testing.md. +#> +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [ValidateSet('format', 'quick', 'full', 'browser')] + [string] $Mode = 'quick', + [switch] $Fix, + [switch] $Serial, + [switch] $SkipWebPublish +) + +$ErrorActionPreference = 'Stop' +# Capture native failures ourselves so their output is retained in the step log. +$PSNativeCommandUseErrorActionPreference = $false +if ($Fix -and $Mode -ne 'format') { throw '-Fix is only valid with format.' } +if ($SkipWebPublish -and $Mode -ne 'full') { throw '-SkipWebPublish is only valid with full.' } + +$repoRoot = Split-Path $PSScriptRoot -Parent +Push-Location $repoRoot +try { + New-Item -ItemType Directory -Force artifacts/validation | Out-Null + + function Invoke-Check { + param([string] $Name, [string] $Command, [string[]] $Arguments) + Write-Host "Checking $Name" + & $Command @Arguments 2>&1 | Tee-Object -FilePath "artifacts/validation/$Name.log" + if ($LASTEXITCODE -ne 0) { + throw "$Name failed (exit $LASTEXITCODE). See artifacts/validation/$Name.log." + } + } + + $scope = if ($Mode -eq 'format') { 'quick' } else { $Mode } + Invoke-Check 'doctor' (Join-Path $PSHOME $(if ($IsWindows) { 'pwsh.exe' } else { 'pwsh' })) @( + '-NoProfile', '-File', "$PSScriptRoot/doctor.ps1", '-Scope', $scope + ) + + if ($Mode -eq 'browser') { + # Browser installation is an explicit setup step, never part of a check. + if (-not (Test-Path tests/deploy-smoke/node_modules/playwright/package.json)) { + throw 'Run npm ci --prefix tests/deploy-smoke, then node tests/deploy-smoke/node_modules/playwright/cli.js install chromium.' + } + $expected = (Get-Content tests/deploy-smoke/package.json -Raw | ConvertFrom-Json).devDependencies.playwright + $installed = (Get-Content tests/deploy-smoke/node_modules/playwright/package.json -Raw | ConvertFrom-Json).version + if ($installed -ne $expected) { + throw "Playwright $installed is installed; run npm ci --prefix tests/deploy-smoke for pinned version $expected." + } + Invoke-Check 'browser-smoke' 'node' @('tests/deploy-smoke/smoke.mjs') + } + else { + $msbuildArguments = if ($Serial) { @('-m:1', '-p:BuildInParallel=false', '-nodeReuse:false') } else { @() } + Invoke-Check 'restore' 'dotnet' (@('restore', 'opengamebuilder.slnx', '--locked-mode') + $msbuildArguments) + $formatArguments = @('format', 'opengamebuilder.slnx', '--no-restore', '--report', 'artifacts/validation/format.json') + if (-not $Fix) { $formatArguments += '--verify-no-changes' } + Invoke-Check 'format' 'dotnet' $formatArguments + + if ($Mode -ne 'format') { + Invoke-Check 'build' 'dotnet' (@('build', 'opengamebuilder.slnx', '--configuration', 'Release', '--no-restore') + $msbuildArguments) + Invoke-Check 'test' 'dotnet' @('test', '--solution', 'opengamebuilder.slnx', '--configuration', 'Release', '--no-build') + } + if ($Mode -eq 'full') { + . "$PSScriptRoot/tooling.ps1" + $bash = Resolve-CheckBash + if (-not $SkipWebPublish) { + Invoke-Check 'web-publish' 'dotnet' (@( + 'publish', 'src/OpenGameBuilder.Web.Client/OpenGameBuilder.Web.Client.csproj', + '--configuration', 'Release', '--no-build', '--output', 'artifacts/web' + ) + $msbuildArguments) + Invoke-Check 'web-configuration' $bash @('scripts/verify-web-publish.sh', 'artifacts/web/wwwroot') + } + $npm = if ($IsWindows) { 'npm.cmd' } else { 'npm' } + Invoke-Check 'smoke-dependencies' $npm @('ci', '--prefix', 'tests/deploy-smoke') + Invoke-Check 'smoke-syntax' 'node' @('--check', 'tests/deploy-smoke/smoke.mjs') + foreach ($suite in @('release-scripts', 'deploy-edge', 'deploy-topology', 'deploy-app', 'supply-chain')) { + Invoke-Check $suite $bash @("tests/$suite/run.sh") + } + } + } + Write-Host "PASS $Mode checks" +} +catch { + Write-Error $_ -ErrorAction Continue + exit 1 +} +finally { + Pop-Location +} diff --git a/scripts/doctor.ps1 b/scripts/doctor.ps1 new file mode 100644 index 0000000..9151c39 --- /dev/null +++ b/scripts/doctor.ps1 @@ -0,0 +1,178 @@ +[CmdletBinding()] +param( + [ValidateSet('quick', 'full', 'browser', 'development')] + [string] $Scope = 'development' +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $false +$repoRoot = Split-Path -Parent $PSScriptRoot +$failureCount = 0 +. (Join-Path $PSScriptRoot 'tooling.ps1') + +function Write-Check { + param( + [ValidateSet('PASS', 'FAIL', 'WARN', 'INFO')][string] $Status, + [string] $Message + ) + if ($Status -eq 'FAIL') { $script:failureCount++ } + Write-Host "[$Status] $Message" +} + +function Find-Tool { + param([string] $Name) + $command = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($command.Path) { return $command.Path } + return $command.Source +} + +function Read-Version { + param([string] $Command, [string[]] $Arguments = @('--version')) + try { + $global:LASTEXITCODE = 0 + $output = & $Command @Arguments 2>&1 + if ($LASTEXITCODE -ne 0) { return $null } + return ($output | ForEach-Object { $_.ToString().Trim() } | Where-Object { $_ } | Select-Object -First 1) + } + catch { return $null } +} + +function Test-Tool { + param( + [string] $Label, + [string] $CommandName, + [string] $Required, + [string] $VersionPattern, + [string[]] $Arguments = @('--version') + ) + $command = Find-Tool $CommandName + if (-not $command) { + Write-Check FAIL "$Label is missing. $Required" + return + } + $version = Read-Version $command $Arguments + if (-not $version) { + Write-Check FAIL "$Label was found at '$command' but did not report a version. $Required" + } + elseif ($VersionPattern -and $version -notmatch $VersionPattern) { + Write-Check FAIL "$Label $version is unsupported. $Required" + } + else { Write-Check PASS "$Label $version" } +} + +function Test-DotNetSdk { + try { $globalJson = Get-Content -Raw (Join-Path $repoRoot 'global.json') | ConvertFrom-Json } + catch { + Write-Check FAIL 'Cannot read global.json. Restore a valid repository copy.' + return + } + $expected = [string]$globalJson.sdk.version + if (-not $expected) { + Write-Check FAIL 'global.json does not declare an SDK version.' + return + } + if ($globalJson.sdk.rollForward -ne 'disable' -or $globalJson.sdk.allowPrerelease -ne $false) { + Write-Check FAIL "global.json must pin SDK $expected with rollForward=disable and allowPrerelease=false." + } + else { Write-Check INFO "Declared .NET SDK: $expected (exact)" } + + $dotnet = Find-Tool dotnet + if (-not $dotnet) { + Write-Check FAIL "The .NET SDK is missing. Install SDK $expected from https://dotnet.microsoft.com/download/dotnet/10.0." + return + } + Push-Location $repoRoot + try { $selected = Read-Version $dotnet } + finally { Pop-Location } + if ($selected -eq $expected) { Write-Check PASS ".NET SDK $selected selected by global.json" } + elseif ($selected) { Write-Check FAIL ".NET SDK $selected is selected. Install the required SDK $expected." } + else { Write-Check FAIL "SDK $expected is not selectable. Install that exact SDK." } +} + +function Test-Bash { + $bash = Resolve-CheckBash -AllowMissing + if (-not $bash) { + $remedy = if ($IsWindows) { 'Install Git for Windows; the WSL bash launcher is not used.' } else { 'Install Bash and add it to PATH.' } + Write-Check FAIL "Bash is missing. $remedy" + return + } + $version = Read-Version $bash + if ($version) { Write-Check PASS "Bash: $version ($bash)" } + else { Write-Check FAIL "Bash was found at '$bash' but did not report a version." } +} + +function Test-DockerCompose { + $docker = Find-Tool docker + if (-not $docker) { + Write-Check FAIL 'Docker CLI is missing. Install a Docker CLI with the Compose plugin; a running daemon is not required.' + return + } + $dockerVersion = Read-Version $docker + $composeVersion = Read-Version $docker @('compose', 'version') + if ($dockerVersion) { Write-Check PASS $dockerVersion } + else { Write-Check FAIL "Docker at '$docker' did not report a CLI version." } + if ($composeVersion) { Write-Check PASS $composeVersion } + else { Write-Check FAIL 'Docker Compose is unavailable. Install the Compose CLI plugin; no daemon is required.' } +} + +function Test-Aspire { + $aspire = Find-Tool aspire + if (-not $aspire) { + Write-Check FAIL "Aspire CLI is missing. Run 'dotnet tool install --global Aspire.Cli --version 13.4.2', then reopen the terminal." + return + } + $version = Read-Version $aspire + if ($version -match '^13\.4\.2(?:\+|$)') { Write-Check PASS "Aspire CLI $version" } + elseif ($version) { Write-Check FAIL "Aspire CLI $version is installed; install version 13.4.2." } + else { Write-Check FAIL "Aspire at '$aspire' did not report a version; install version 13.4.2." } +} + +function Show-ManifestPins { + param([switch] $RequirePlaywright) + try { + $tools = Get-Content -Raw (Join-Path $repoRoot '.config/dotnet-tools.json') | ConvertFrom-Json + $husky = [string]$tools.tools.husky.version + if ($husky) { Write-Check INFO "Husky manifest pin: $husky (opt-in; not restored by doctor)" } + else { Write-Check WARN 'The Husky pin is missing from .config/dotnet-tools.json.' } + } + catch { Write-Check WARN 'Cannot read the Husky pin from .config/dotnet-tools.json.' } + + try { + $package = Get-Content -Raw (Join-Path $repoRoot 'tests/deploy-smoke/package.json') | ConvertFrom-Json + $lock = Get-Content -Raw (Join-Path $repoRoot 'tests/deploy-smoke/package-lock.json') | ConvertFrom-Json -AsHashtable + $pin = [string]$package.devDependencies.playwright + $lockedPin = [string]$lock['packages']['']['devDependencies']['playwright'] + $lockedVersion = [string]$lock['packages']['node_modules/playwright']['version'] + if ($pin -match '^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$' -and $pin -eq $lockedPin -and $pin -eq $lockedVersion) { + Write-Check INFO "Playwright manifest pin: $pin (not installed or restored by doctor)" + } + else { + Write-Check $(if ($RequirePlaywright) { 'FAIL' } else { 'WARN' }) 'The Playwright manifest and lockfile need one matching exact version.' + } + } + catch { Write-Check $(if ($RequirePlaywright) { 'FAIL' } else { 'WARN' }) 'Cannot read the Playwright pins from tests/deploy-smoke.' } +} + +Write-Host "OpenGameBuilder doctor (scope: $Scope)" +$powerShellVersion = $PSVersionTable.PSVersion.ToString() +if ($PSVersionTable.PSVersion.Major -ge 7) { Write-Check PASS "PowerShell $powerShellVersion" } +else { Write-Check FAIL "PowerShell $powerShellVersion is unsupported. Install PowerShell 7 and rerun with pwsh." } +Show-ManifestPins -RequirePlaywright:($Scope -in @('full', 'browser')) + +if ($Scope -in @('quick', 'full', 'development')) { Test-DotNetSdk } +if ($Scope -in @('full', 'development')) { + Test-Tool 'Git' 'git' 'Install Git (Git for Windows on Windows).' '^git version ' + Test-Bash +} +if ($Scope -in @('full', 'browser')) { + Test-Tool 'Node.js' 'node' 'Install Node.js 22.' '^v?22(?:\.|$)' + Test-Tool 'npm' 'npm' 'Install npm with Node.js 22.' '^\d+\.' +} +if ($Scope -eq 'full') { Test-DockerCompose } +if ($Scope -eq 'development') { Test-Aspire } + +if ($failureCount -gt 0) { + Write-Host "Doctor found $failureCount required prerequisite problem(s)." + exit 1 +} +Write-Host 'All required prerequisites for this scope are available.' diff --git a/scripts/tooling.ps1 b/scripts/tooling.ps1 new file mode 100644 index 0000000..85fc8ec --- /dev/null +++ b/scripts/tooling.ps1 @@ -0,0 +1,48 @@ +function Resolve-CheckBash { + [CmdletBinding()] + param([switch] $AllowMissing) + + if ($IsWindows) { + if ($env:ProgramFiles) { + $preferred = Join-Path $env:ProgramFiles 'Git\bin\bash.exe' + if (Test-Path -LiteralPath $preferred -PathType Leaf) { + return $preferred + } + } + + $git = Get-Command git -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($git -and $git.Path) { + $gitRoot = Split-Path -Parent (Split-Path -Parent $git.Path) + $alongsideGit = Join-Path $gitRoot 'bin\bash.exe' + if (Test-Path -LiteralPath $alongsideGit -PathType Leaf) { + return $alongsideGit + } + } + + $pathBash = Get-Command bash -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($pathBash -and $pathBash.Path) { + $wslLauncher = Join-Path $env:SystemRoot 'System32\bash.exe' + if ($pathBash.Path -ne $wslLauncher) { + return $pathBash.Path + } + } + } + else { + $bash = Get-Command bash -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($bash -and $bash.Path) { + return $bash.Path + } + } + + if ($AllowMissing) { + return $null + } + + $remedy = if ($IsWindows) { + 'Install Git for Windows; the WSL bash launcher is not used for repository checks.' + } + else { + 'Install Bash and add it to PATH.' + } + throw "Bash is required. $remedy" +} diff --git a/src/OpenGameBuilder.Api/Dockerfile b/src/OpenGameBuilder.Api/Dockerfile index 6350add..2c7004a 100644 --- a/src/OpenGameBuilder.Api/Dockerfile +++ b/src/OpenGameBuilder.Api/Dockerfile @@ -12,6 +12,7 @@ COPY src/ ./src/ RUN dotnet publish src/OpenGameBuilder.Api/OpenGameBuilder.Api.csproj \ --configuration Release \ + -p:RestoreLockedMode=true \ --output /app/publish FROM mcr.microsoft.com/dotnet/aspnet:10.0.12@sha256:2d584d8147faddb0d678c5748d47953e5b8e18621ed4fb7049a91381d9d7746f AS final diff --git a/src/OpenGameBuilder.Api/OpenGameBuilder.Api.csproj b/src/OpenGameBuilder.Api/OpenGameBuilder.Api.csproj index 44c1d15..364ad59 100644 --- a/src/OpenGameBuilder.Api/OpenGameBuilder.Api.csproj +++ b/src/OpenGameBuilder.Api/OpenGameBuilder.Api.csproj @@ -1,5 +1,9 @@ + + true + + diff --git a/src/OpenGameBuilder.Api/packages.lock.json b/src/OpenGameBuilder.Api/packages.lock.json new file mode 100644 index 0000000..a02076b --- /dev/null +++ b/src/OpenGameBuilder.Api/packages.lock.json @@ -0,0 +1,210 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.AspNetCore.OpenApi": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "7RnQ/UKn+kK3JDLmTpW+vVqNErWthGPAVAy1aIiBzv4ucQ6g9NFvOJUuT0PHcpvcJxGVxctMxj6t+NESWMrgHQ==", + "dependencies": { + "Microsoft.OpenApi": "[2.12.0, 3.0.0)" + } + }, + "Scalar.AspNetCore": { + "type": "Direct", + "requested": "[2.17.6, )", + "resolved": "2.17.6", + "contentHash": "Qho7zxnjr5620jueYl40XlPNDJdn871MPe352AfYE2zvUkneMjOR5U9Cj+4f1Na7ODSSp5DUL0WM3MTdoKOazA==" + }, + "Microsoft.Extensions.AmbientMetadata.Application": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "Sa4xD2ab5yNRJYt4piS05C9b5Gpd1G3TZpvVJFgHyqLYdRThUNLcLHpREI4yjkmElCjHXoPcF+SHUim9cZ8cvQ==" + }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "Sh3ujDRJpCMNY4x+43ljCROt4qBOoWfsL8l816RPYZS1S+Tw277r5002rbB15Sh6hFcCT8mGYBO7NtpfSYrQ0Q==" + }, + "Microsoft.Extensions.DependencyInjection.AutoActivation": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "AJ2lFnzzJkN2ZRvfPBKQ5k3EJFkhmsm1tEhWzK58KsxU+WK3GVOh/5WHUhQ56TWjnxGZCGgr0+U1QUuJ4rJVuA==" + }, + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "/6C1XjuRYRXst3QUoFnQdL0Q2ZihDlxr8PQ4jei8MyjVRZEGFg+le6N90d/CdpdgIbDz/h8JIKY1psACYDhPRQ==" + }, + "Microsoft.Extensions.Http.Diagnostics": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "yt9uQJSwh3hJag5NSDGEyWFbPKHaYem0w17TlSgv533NP8Kn56c5si870grJWGSL7aQyeUqxfKvDZxZpFaQUhw==", + "dependencies": { + "Microsoft.Extensions.Telemetry": "10.10.0" + } + }, + "Microsoft.Extensions.Resilience": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "aYIiuCSqsIqpaqya2JchArkaWibMhiXTNj4+wkjv0s7A/Uy1dvc4MsCPTOtvOU3ffz0+vYRHi7GHcXqynZfdbQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.10.0", + "Microsoft.Extensions.Telemetry.Abstractions": "10.10.0", + "Polly.Extensions": "8.4.2", + "Polly.RateLimiting": "8.4.2" + } + }, + "Microsoft.Extensions.ServiceDiscovery.Abstractions": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "4zhzINrA/ChIzU47J5EaJdVenCJvSXpcNPo7arvTXy8VrUijkjWqOksBSdoxTmlpSo1XBK/H4pKoNbNdLL4n1w==" + }, + "Microsoft.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "GeJofTliWSVoYAeTfRbnp+TaOUl502yZIcZ+qHFiyOoAIzwTk12eKypnSR9uN2oiHgGW32Y0XN1i+VaQizl0IQ==", + "dependencies": { + "Microsoft.Extensions.AmbientMetadata.Application": "10.10.0", + "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.10.0", + "Microsoft.Extensions.Telemetry.Abstractions": "10.10.0" + } + }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "I76SAZ95I5lYh4whVZvLpKhpvHI1o21FfGqiASU85GtIVA4ULv+TVIE5+swKhnC63+xI4XjCcIWa9LbyR3pFNw==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "10.10.0" + } + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "2.12.0", + "contentHash": "0xB/be+f6qYOhvQPUrVsLrau+fkowYv6gt9RIkajMhwT7sZnqtcA797yW3bS5kgSje+AQW4xZgyLnW641YFioQ==" + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.19.0", + "contentHash": "tH3Y3ABOCcUXbnz/jntnfEXcSxEy+GzS2niNqwJPIcNdAYUpP09QHLaAwcJExeS+ibfmlcSkmIfWlKhe7yVjIA==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.19.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.19.0", + "contentHash": "ZupI0cvo9Jyk2IwLLMrXQm3PvX67tCBIiFws5fslm2Xw7gYctq5wZiGPS+mKmMususAAE9+jyv5Jp7ADxdZDJw==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.19.0", + "contentHash": "V+nYo7IIqH7cXagkERfGZUNstdoZQkplpn+hZMkLBlJ0ZJNQBvTMv2Cvjq8eqqxkB9lmBw+CUH78hWxJB1Vdcg==", + "dependencies": { + "OpenTelemetry.Api": "1.19.0" + } + }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, + "Polly.Extensions": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", + "dependencies": { + "Polly.Core": "8.4.2" + } + }, + "Polly.RateLimiting": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", + "dependencies": { + "Polly.Core": "8.4.2" + } + }, + "opengamebuilder.api.contracts": { + "type": "Project" + }, + "opengamebuilder.servicedefaults": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Http.Resilience": "[10.10.0, )", + "Microsoft.Extensions.ServiceDiscovery": "[10.10.0, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.19.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.19.0, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.19.0, )", + "OpenTelemetry.Instrumentation.Http": "[1.19.0, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.19.0, )" + } + }, + "Microsoft.Extensions.Http.Resilience": { + "type": "CentralTransitive", + "requested": "[10.10.0, )", + "resolved": "10.10.0", + "contentHash": "KZw4jCU4RxHwjOXS7BUal3CnaupEJdGkJ9tPzE1Y2CR5x6IHMuxc6vNNWDiFIlnMa1TKGkta/Dfz3DQiFF1fxQ==", + "dependencies": { + "Microsoft.Extensions.Http.Diagnostics": "10.10.0", + "Microsoft.Extensions.Resilience": "10.10.0" + } + }, + "Microsoft.Extensions.ServiceDiscovery": { + "type": "CentralTransitive", + "requested": "[10.10.0, )", + "resolved": "10.10.0", + "contentHash": "YdTl+XNxLiy8OYCFsYuSKkTGE9VyBaJlL+hlN+g9fSLwn77z37mkkdsjMnw3JhUZLd5nHotl9yQMI4s57FX1Dw==", + "dependencies": { + "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.10.0" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.19.0, )", + "resolved": "1.19.0", + "contentHash": "x1QtTiMXvM4POUv92FX0S3KPLCwaFpZ76NwNsd4Os7jWDT+oESkfDSdP7JisoTdMP3x+s4xjXlc7eXEuU5SBMw==", + "dependencies": { + "OpenTelemetry": "1.19.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.19.0, )", + "resolved": "1.19.0", + "contentHash": "XmjQIPG36zgkpc2nYkveQvmVe7zqffmXG+/7rGlE67Be59clUlPyJA8NKMi1XeZheoNkQG6SZcZ5NykzGRHrdA==", + "dependencies": { + "OpenTelemetry": "1.19.0" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.19.0, )", + "resolved": "1.19.0", + "contentHash": "reypo7g0Nzyssg1EkbfWU6W58CVFgtgkSEGrFXtbpA+bNit7HUs7587DOkOdPCgqWr3IL4f6X9B2yhnU6POrFA==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.19.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Http": { + "type": "CentralTransitive", + "requested": "[1.19.0, )", + "resolved": "1.19.0", + "contentHash": "+egJpWBRfYP3ldfexV2bmJyzOWkkOPVYikw/vUEtbUvf2exJfY3RGInXrYlubhBIH6rmNEn6sI/FmqenZJwkjw==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.19.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "CentralTransitive", + "requested": "[1.19.0, )", + "resolved": "1.19.0", + "contentHash": "VIH2tQ2h31j4wQyZ9Utk8ZXIyj7Z1j0xIiyPRGjYu1MmZjJtpxRh0Y/NnB6fEBimNg/WrX0jDyfMeEjPQqmaMA==", + "dependencies": { + "OpenTelemetry.Api": "[1.19.0, 2.0.0)" + } + } + } + } +} \ No newline at end of file diff --git a/src/OpenGameBuilder.AppHost/OpenGameBuilder.AppHost.csproj b/src/OpenGameBuilder.AppHost/OpenGameBuilder.AppHost.csproj index a20a942..3b9471b 100644 --- a/src/OpenGameBuilder.AppHost/OpenGameBuilder.AppHost.csproj +++ b/src/OpenGameBuilder.AppHost/OpenGameBuilder.AppHost.csproj @@ -3,6 +3,9 @@ + true + + packages.$(NETCoreSdkRuntimeIdentifier).lock.json Exe true + + ## Summary Describe what this pull request changes. diff --git a/.github/workflows/content-links.yml b/.github/workflows/content-links.yml new file mode 100644 index 0000000..f2de2df --- /dev/null +++ b/.github/workflows/content-links.yml @@ -0,0 +1,48 @@ +name: External documentation links + +on: + schedule: + - cron: "17 10 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: external-documentation-links + cancel-in-progress: true + +jobs: + report: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + package-manager-cache: false + - name: Install pinned tools + shell: pwsh + run: ./scripts/install-content-tools.ps1 + - name: Report external links + id: links + continue-on-error: true + shell: pwsh + run: ./scripts/check.ps1 external-links + - name: Summarize maintenance result + if: ${{ always() }} + env: + LINK_OUTCOME: ${{ steps.links.outcome }} + run: | + printf 'External link report: %s. Owner: ostomachion. Review the report artifact; remote outages do not block PRs.\n' "$LINK_OUTCOME" >> "$GITHUB_STEP_SUMMARY" + - name: Retain report + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: external-links-${{ github.run_attempt }} + path: artifacts/validation/ + if-no-files-found: warn + retention-days: 7 diff --git a/.husky/pre-commit b/.husky/pre-commit index cae9224..1ab2cfc 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,6 @@ #!/usr/bin/env sh +# Husky generates this bootstrap only when a contributor opts into hooks. +# shellcheck source=/dev/null . "$(dirname -- "$0")/_/husky.sh" dotnet husky run --group pre-commit diff --git a/.husky/task-runner.json b/.husky/task-runner.json index bf9ef74..97be3ce 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -1,6 +1,28 @@ { "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", "tasks": [ + { + "name": "check-first-party-content", + "group": "pre-commit", + "command": "pwsh", + "args": ["-NoProfile", "-File", "scripts/check.ps1", "content"], + "include": [ + "**/*.md", + "**/*.json", + "**/*.jsonc", + "**/*.yml", + "**/*.yaml", + "**/*.css", + "**/*.js", + "**/*.mjs", + "**/*.cjs", + "**/*.sh", + ".husky/pre-commit", + ".editorconfig", + ".prettierignore", + ".lychee*.toml" + ] + }, { "name": "dotnet-format-staged", "group": "pre-commit", diff --git a/.lychee-external.toml b/.lychee-external.toml new file mode 100644 index 0000000..15a266a --- /dev/null +++ b/.lychee-external.toml @@ -0,0 +1,8 @@ +# Weekly maintenance report; never part of required PR validation. +scheme = ["https", "http"] +max_concurrency = 4 +max_retries = 1 +timeout = 15 +no_progress = true +format = "markdown" +output = "artifacts/validation/external-links.md" diff --git a/.lychee.toml b/.lychee.toml new file mode 100644 index 0000000..f0fb4ce --- /dev/null +++ b/.lychee.toml @@ -0,0 +1,4 @@ +# PR checks are offline: remote availability cannot affect this gate. +offline = true +include_fragments = "anchor-only" +no_progress = true diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..72df43a --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,11 @@ +{ + "ignores": [ + "**/bin/**", + "**/obj/**", + "**/node_modules/**", + "artifacts/**", + ".agents/skills/**", + ".git/**", + ".vs/**", + ], +} diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..55d87f8 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "extends": "./node_modules/markdownlint/style/prettier.json", + "MD060": false +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c1cc460 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,24 @@ +# Prettier owns only these content types (gitignore syntax, not brace globs). +* +!*/ +!*.md +!*.json +!*.jsonc +!*.yml +!*.yaml +!*.css +!*.js +!*.mjs +!*.cjs +# Generated output and dependencies have their own producers. +**/bin/** +**/obj/** +**/node_modules/** +artifacts/** +.git/** +.vs/** +# Preserve upstream skills and their provenance byte for byte. +.agents/skills/** +# Package managers own their lockfile formatting. +**/packages*.lock.json +**/package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..4708700 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 80, + "proseWrap": "preserve", + "endOfLine": "lf" +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2331366..aa5903d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,8 @@ "ms-dotnettools.csdevkit", "ms-dotnettools.blazorwasm-companion", "EditorConfig.EditorConfig", + "esbenp.prettier-vscode", + "DavidAnson.vscode-markdownlint", "streetsidesoftware.code-spell-checker" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 722993d..c6cf0b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,12 +8,11 @@ "[razor]": { "editor.defaultFormatter": "ms-dotnettools.csharp" }, - "[json]": { - "editor.defaultFormatter": "vscode.json-language-features" - }, - "[jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features" + "[markdown][json][jsonc][yaml][css][javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "prettier.requireConfig": true, + "prettier.resolveGlobalModules": false, "cSpell.words": [ "accessibilities", "apphost", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 213cc8a..6364136 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,29 @@ { "version": "2.0.0", "tasks": [ + { + "label": "check-content", + "command": "pwsh", + "type": "process", + "args": [ + "-NoProfile", + "-File", + "${workspaceFolder}/scripts/check.ps1", + "content" + ], + "problemMatcher": [] + }, + { + "label": "format-first-party-content", + "command": "node", + "type": "process", + "args": [ + "${workspaceFolder}/scripts/check-content.mjs", + "format", + "--fix" + ], + "problemMatcher": [] + }, { "label": "build-api", "command": "dotnet", diff --git a/docs/foundation-checklist.md b/docs/foundation-checklist.md index 551c58a..ceaed4d 100644 --- a/docs/foundation-checklist.md +++ b/docs/foundation-checklist.md @@ -430,20 +430,23 @@ or editor rehearsal was performed. ### 14. Format and lint first-party content consistently -- [ ] Keep `dotnet format` and existing analyzers for C#. Promote selected useful +- [x] Keep `dotnet format` and existing analyzers for C#. Promote selected useful style rules to enforced warnings rather than enabling a large rule set wholesale. -- [ ] Add exactly pinned Prettier for Markdown, JSON, YAML, CSS, and JavaScript; +- [x] Add exactly pinned Prettier for Markdown, JSON, YAML, CSS, and JavaScript; choose explicit indentation/prose wrapping consistent with scoped EditorConfig settings. Use markdownlint-cli2's Prettier-compatible preset for structural rules. -- [ ] Add actionlint for workflows, ShellCheck for shell defects, and shfmt for +- [x] Add actionlint for workflows, ShellCheck for shell defects, and shfmt for shell formatting. Use versions compatible with the workflow syntax in this repo. -- [ ] Add pinned lychee checks for local file/image links and anchors, including +- [x] Add pinned lychee checks for local file/image links and anchors, including root and first-party GitHub documents. Check generated HTML when step 17 lands. Schedule bounded external-link reports separately; remote outages must not block unrelated PRs. Keep exclusions narrow and explained. -- [ ] Share configurations between editor, optional hooks, command line, and CI. +- [x] Share configurations between editor, optional hooks, command line, and CI. Exclude generated artifacts, dependencies, and vendored skills; avoid competing - formatters for a file type. Make the initial formatting sweep a separate PR. + formatters for a file type. +- [ ] Publish the initial formatting sweep as a separate PR. It is isolated in + local commit `1f646a6` on `codex/foundation-formatting-sweep`; tooling follows + on `codex/foundation-section-14-content`. Neither branch has been published. **Acceptance:** deliberate formatting, Markdown-structure, missing-target/anchor, workflow, and shell defects fail their appropriate checks with clear fixes. Clean @@ -455,6 +458,16 @@ Sources: [Prettier](https://prettier.io/docs/install), [ShellCheck](https://github.com/koalaman/shellcheck), [shfmt](https://github.com/mvdan/sh), [lychee](https://lychee.cli.rs/guides/cli/). +**Result (2026-09-22, local implementation complete):** The +[shared content workflow](quality/content-checks.md) pins and verifies the tools, +preserves formatter ownership, checks local links offline, and schedules bounded +external reports. The full Windows gate passed with zero build warnings and 72 +.NET tests; all seven content regression groups and the final content gate passed. +[Validation evidence](quality/content-checks.md#recorded-validation) records the +injected C# failures, installer checks, and narrow actionlint cache-mode exception. +Linux execution, hosted CI, the scheduled report, and separate PR publication +remain unverified. No application services or deployments were started. + ### 15. Validate the supported platform and keep CI understandable - [ ] Add a Windows restore/format/Release-build/test lane alongside Linux. Keep diff --git a/docs/quality/content-checks.md b/docs/quality/content-checks.md new file mode 100644 index 0000000..95a1518 --- /dev/null +++ b/docs/quality/content-checks.md @@ -0,0 +1,140 @@ +# First-party content checks + +Use Node.js 22 and PowerShell 7 from the repository root. Install the locked npm +dependencies and checksum-pinned native tools once, then run the shared checks: + +```pwsh +npm ci --ignore-scripts +pwsh ./scripts/install-content-tools.ps1 +pwsh ./scripts/check.ps1 content +``` + +The native installer supports Windows x64 and Linux x64. It downloads into ignored +`artifacts/content-tools`, verifies both archive and executable SHA-256 hashes, +and reuses an installation only when the executable still matches the manifest. +`pwsh ./scripts/install-content-tools.ps1 -Verify` checks it without downloading. +Windows uses upstream ShellCheck's x86 executable under WoW64. No global installs, +Docker daemon, application service, browser, or deployment credentials are needed. + +`check.ps1 full` installs locked npm dependencies and includes these checks and +their rejection fixtures alongside the existing solution and packaging checks. +Native-tool installation is an explicit prerequisite; CI performs it before +the shared full command. The required `build-test` check keeps its name and fails +when any content check fails, independently of Git hooks. + +## Formatter and linter ownership + +| Content | Formatter | Additional checks | +| ------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------- | +| C# | SDK `dotnet format` | Existing analyzers; explicit accessibility (IDE0040) and readonly fields (IDE0044) are warnings | +| Markdown, JSON/JSONC, YAML, CSS, JavaScript | Exactly pinned Prettier | Markdown structure via markdownlint-cli2's Prettier-compatible preset | +| Bash scripts | Exactly pinned shfmt | ShellCheck, including Bash workflow run blocks through actionlint | +| GitHub workflows | Prettier | actionlint and the protected-source cache policy | +| Markdown links and images | None | Offline lychee file and anchor checks | + +[EditorConfig](../../.editorconfig) and [Prettier](../../.prettierrc.json) use two +spaces for first-party content and shell scripts, LF line endings, and an 80-column +code target. Markdown preserves authored prose wrapping; Prettier still formats +tables, lists, and code fences. C# retains its existing four-space conventions. +Razor, HTML, XML, and PowerShell are outside the new formatter's scope. + +Apply formatting with `pwsh ./scripts/check.ps1 format -Fix`, then inspect the diff. +That command includes C#, Prettier, and shfmt. `npm run format` applies only the +content formatters; `npm run format:check` verifies them without changing files. +Neither command rewrites prose to meet a style guide. Markdownlint's compatible +preset disables overlapping rules; MD060 is also disabled because Prettier owns +table layout. The PR body template alone permits a level-two opening heading, +since GitHub supplies the title. + +The runner enumerates tracked and new non-ignored files through Git, including +root documentation and `.github` documents. It excludes generated output, +dependencies, and `.agents/skills`. Package-manager lockfiles keep their generated +formatting. [Prettier's ignore file](../../.prettierignore) and +[Markdownlint's config](../../.markdownlint-cli2.jsonc) keep editor checks within +the same boundary. Vendored skills and their provenance are preserved. + +## Editor and optional hook use + +The VS Code recommendations select local Prettier for its file types and the C# +extension for C#/Razor. Markdownlint reads the repository configuration. Use the +`check-content` and `format-first-party-content` tasks for the same command-line +checks, including shell formatting. Visual Studio users can invoke those commands +from its PowerShell terminal; EditorConfig remains authoritative for C#. + +The existing opt-in Husky hook retains staged C# formatting and adds a content +check when relevant files are staged. That content check verifies the working +tree, including unstaged work; it never rewrites or stages content. Install the +content prerequisites before opting into the hook. See +[hook setup](../setup/development.md#formatting-warnings-and-optional-git-hooks). + +## Workflow compatibility and exceptions + +GitHub supports [`cache-mode: none`](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#cache-mode), +but actionlint 1.7.12 does not recognize that key. The +[actionlint configuration](../../.github/actionlint.yaml) suppresses only that +specific root-key diagnostic in the three protected-source deployment workflows. +The [cache-policy check](../../scripts/workflow-cache-policy.mjs) independently +requires `none` and rejects job overrides that permit caching. Other workflow +errors still fail, and the regression suite covers missing/changed cache policy. +Remove these exceptions once the pinned actionlint supports the key. + +ShellCheck exceptions sit next to the affected code or at the top of fixture +scripts, with a reason: intentional literal variable expressions, a sourced +library constant, the EXIT-trap callback, or deliberate glob matching. There is no +repository-wide suppression of shell diagnostics. + +## Recorded validation + +On 2026-09-22, Windows x64 with Node.js 22.23.2 and PowerShell 7.6.5 passed the +shared full check: locked restore, format verification, Release build with zero +warnings, all 72 .NET tests, frontend publish and portability guard, smoke-package +checks, and all five isolated shell suites. After review fixes, the complete +content gate and all seven content regression groups passed again. The fixtures +reject formatting drift, skipped Markdown heading levels, missing files/images +and anchors, invalid workflow syntax, shell word splitting, and changed deployment +cache policy; formatter fixes pass the same check. They also verify Prettier's +file-type and vendor exclusions. + +Injected C# accessibility defects failed the Release build with IDE0040; a +runtime-initialized mutable field failed `dotnet format` with IDE0044. The latter +is enforced by formatting verification, not by this SDK's build analyzer. Native +tool installation, reuse without downloads, and rejection of a corrupted +executable were verified on Windows. + +Linux asset and executable checksums were verified, but Linux execution and hosted +CI remain unverified. The external-link schedule has not run from this change. +These checks did not start application services, deploy, or establish browser +acceptance. The initial mechanical formatting sweep is kept separate for review. + +## Local links and external maintenance + +[Local lychee configuration](../../.lychee.toml) uses offline mode and anchor +checking. Missing files, images, and fragments fail PR validation. Remote URLs +are excluded from this gate; their availability cannot block unrelated work. +Rendered HTML can be added when a documentation-site build exists. + +[External-link maintenance](../../.github/workflows/content-links.yml) runs weekly +and can be dispatched manually. It has read-only permissions, four concurrent +requests, a 15-second request timeout, one retry, a 15-minute job limit, and +seven-day artifact retention. It produces a report without posting issues or +comments. Owner `ostomachion` reviews failures, distinguishes temporary outages +from stale links, and submits focused corrections. It is not a required PR check. +Run the same report locally with `pwsh ./scripts/check.ps1 external-links`. + +## Maintaining tool pins + +[package.json](../../package.json) and its lockfile pin npm tools; Dependabot +checks the root tooling package weekly. The native +[tool manifest](../../scripts/content-tools.json) records exact versions, upstream +release URLs, archive checksums, and executable checksums for each platform. +`ostomachion` reviews native-tool releases monthly and whenever workflow syntax +outgrows a pin. Verify upstream release assets and executable hashes, change the +manifest in a focused PR, and run content checks and rejection fixtures on both +platforms. A new formatter version may require a separate mechanical sweep PR. + +Upstream references: [Prettier installation](https://prettier.io/docs/install), +[Markdownlint compatibility](https://github.com/DavidAnson/markdownlint/blob/main/doc/Prettier.md), +[actionlint configuration](https://github.com/rhysd/actionlint/blob/v1.7.12/docs/config.md), +[ShellCheck](https://github.com/koalaman/shellcheck), +[shfmt](https://github.com/mvdan/sh), and +[lychee anchors](https://lychee.cli.rs/recipes/anchors/). diff --git a/docs/quality/testing.md b/docs/quality/testing.md index 7fe9603..b8c20cc 100644 --- a/docs/quality/testing.md +++ b/docs/quality/testing.md @@ -74,6 +74,11 @@ pwsh ./scripts/check.ps1 full pwsh ./scripts/check.ps1 full -Serial ``` +See [first-party content checks](content-checks.md) for the pinned tool setup, +formatter ownership, offline link checks, and separate external-link reports. +`content` runs that gate without requiring .NET or deployment tools. `full` +includes it and its deliberate-defect regression tests. + `quick` is the normal solution gate: quick doctor checks, locked restore, format verification, Release build, and the current 72 tests. `full` includes that gate plus frontend Release publish and its portability guard, `npm ci` and diff --git a/docs/setup/development.md b/docs/setup/development.md index 7325b44..13688ff 100644 --- a/docs/setup/development.md +++ b/docs/setup/development.md @@ -61,14 +61,22 @@ pwsh ./scripts/check.ps1 quick `doctor.ps1` is a read-only prerequisite report. Its default `development` scope reports PowerShell, the SDK, Aspire CLI, Git, and Bash with their remedies; it does not install tools, trust certificates, start services, or open a browser. -Use `-Scope quick`, `full`, `browser`, or `development` when checking a narrower +Use `-Scope quick`, `full`, `content`, `format`, `browser`, or `development` when checking a narrower workflow. The normal solution gate is `pwsh ./scripts/check.ps1 quick`: it runs the quick doctor check, a locked restore, C# formatting verification, a Release build, and -the current 72 solution tests. `check.ps1 format` performs only the formatting -verification after its locked restore. To apply formatter changes deliberately, -run `pwsh ./scripts/check.ps1 format -Fix`, then review the diff. +the current 72 solution tests. `check.ps1 format` verifies C# after locked restore +and checks first-party content with Prettier and shfmt. To apply formatter changes +deliberately, run `pwsh ./scripts/check.ps1 format -Fix`, then review the diff. +Install the [content-checking prerequisites](../quality/content-checks.md) before +using `format`, `content`, or `full`: + +```pwsh +npm ci --ignore-scripts +pwsh ./scripts/install-content-tools.ps1 +pwsh ./scripts/check.ps1 content +``` The initial browser-debugging setup is an explicit, interactive operation: @@ -328,6 +336,8 @@ dotnet husky install ``` The hook formats staged C# files using the same solution and formatting rules. +For staged content files it also runs the shared first-party content check against +the working tree, including unstaged work, without rewriting or staging content. Review any resulting changes before committing. If `HUSKY=0` is set in your terminal, remove that setting before opting in. To disable hook execution temporarily in PowerShell, set `$env:HUSKY = '0'`; use diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..888578e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1403 @@ +{ + "name": "opengamebuilder-tooling", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opengamebuilder-tooling", + "devDependencies": { + "markdownlint-cli2": "0.23.3", + "prettier": "3.9.8" + }, + "engines": { + "node": "22.x" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.4.tgz", + "integrity": "sha512-c8B/VNLmxRcmqqenRA9t+9IyOjf9+V6lTxPaUJLqOCONdQkWZ0ETYgX0qbtJqPsgCNusT9MZ5Jeidw8Eb9tn2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "micromatch": "^4.0.8", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz", + "integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/linkify-it": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^3.0.0" + } + }, + "node_modules/markdown-it": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.1.tgz", + "integrity": "sha512-9/7gE95FNPkfUWrjJIoHZza2iLmuJlPD0UNMxPi7bxUrbCR525YZY0r+zyfes0dZI5ZZ/uNIXUJca0pJvtw41g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^3.0.0", + "entities": "^8.0.0", + "linkify-it": "^6.0.0", + "mdurl": "^2.1.0", + "punycode.js": "^2.3.1", + "uc.micro": "^3.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.2.tgz", + "integrity": "sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, + "node_modules/markdownlint": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.41.1.tgz", + "integrity": "sha512-qHKeU2E1bdyNAT077go2FVTNXvYcktN5IHtF6XyeD1l0PClxzSp2tUApAV14ORI8DGX4H9bNKZEzelZp4qn8IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "4.0.2", + "micromark-core-commonmark": "2.0.3", + "micromark-extension-directive": "4.0.0", + "micromark-extension-gfm-autolink-literal": "2.1.0", + "micromark-extension-gfm-footnote": "2.1.0", + "micromark-extension-gfm-table": "2.1.1", + "micromark-extension-math": "3.1.0", + "micromark-util-types": "2.0.2", + "string-width": "8.2.1" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.23.3.tgz", + "integrity": "sha512-xAr5o/TGpC3v6lE6cKIW4b5eOFRrRX5u7Vtjae9ix3RALv8nNOd94XMkD/1OXXBtpMcJ4uQGbpSo3hv5UqS4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "16.2.4", + "js-yaml": "5.4.1", + "jsonc-parser": "3.3.1", + "jsonpointer": "5.0.1", + "markdown-it": "15.0.1", + "markdownlint": "0.41.1", + "markdownlint-cli2-formatter-default": "0.0.6", + "micromatch": "4.0.8", + "smol-toml": "1.8.0" + }, + "bin": { + "markdownlint-cli2": "markdownlint-cli2-bin.mjs" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2-formatter-default": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz", + "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + }, + "peerDependencies": { + "markdownlint-cli2": ">=0.0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.8.tgz", + "integrity": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smol-toml": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cf14100 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "opengamebuilder-tooling", + "private": true, + "engines": { + "node": "22.x" + }, + "scripts": { + "format": "node scripts/check-content.mjs format --fix", + "format:check": "node scripts/check-content.mjs format", + "lint": "node scripts/check-content.mjs all", + "links:external": "node scripts/check-content.mjs external-links" + }, + "devDependencies": { + "markdownlint-cli2": "0.23.3", + "prettier": "3.9.8" + } +} diff --git a/scripts/check-content.mjs b/scripts/check-content.mjs new file mode 100644 index 0000000..6d671ed --- /dev/null +++ b/scripts/check-content.mjs @@ -0,0 +1,221 @@ +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { checkDeploymentCachePolicy } from "./workflow-cache-policy.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +process.chdir(root); +const args = process.argv.slice(2); +const mode = args.shift() ?? "all"; +const fix = args[0] === "--fix"; +if (fix) args.shift(); +const explicit = args[0] === "--files"; +if (explicit) args.shift(); +const modes = [ + "all", + "format", + "prettier", + "markdown", + "workflows", + "shellcheck", + "shell-format", + "links", + "external-links", +]; +if ( + !modes.includes(mode) || + (fix && !["format", "prettier", "shell-format"].includes(mode)) || + (!explicit && args.length) || + (explicit && !args.length) +) { + throw new Error( + "Usage: node scripts/check-content.mjs [all|format|prettier|markdown|workflows|shellcheck|shell-format|links|external-links] [--fix] [--files paths...]", + ); +} +if (process.versions.node.split(".")[0] !== "22") { + throw new Error( + "Content checks require Node.js 22. Select it on PATH before running this command.", + ); +} + +function run(command, arguments_, label = command) { + console.log(`Checking ${label}`); + const result = spawnSync(command, arguments_, { + cwd: root, + stdio: "inherit", + shell: false, + }); + if (result.error) throw result.error; + if (result.status !== 0) + throw new Error( + `${label} failed (exit ${result.status}). See the diagnostic above.`, + ); +} + +function npmTool(name, bin) { + const expected = JSON.parse(readFileSync("package.json", "utf8")) + .devDependencies[name]; + const manifest = path.join(root, "node_modules", name, "package.json"); + if ( + !existsSync(manifest) || + JSON.parse(readFileSync(manifest, "utf8")).version !== expected + ) { + throw new Error( + `Install pinned ${name} ${expected}: npm ci --ignore-scripts`, + ); + } + return path.join(root, "node_modules", name, bin); +} + +function nativeTool(name) { + const platform = `${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`; + const manifest = JSON.parse( + readFileSync("scripts/content-tools.json", "utf8"), + ); + const tool = manifest.tools[name]; + const spec = tool?.platforms[platform]; + if (!spec) throw new Error(`Unsupported content-tool platform: ${platform}`); + const executable = path.join( + root, + "artifacts", + "content-tools", + platform, + name, + name + (process.platform === "win32" ? ".exe" : ""), + ); + if ( + !existsSync(executable) || + createHash("sha256").update(readFileSync(executable)).digest("hex") !== + spec.executableSha256 + ) { + throw new Error( + `Missing or incorrect ${name} ${tool.version}. Run pwsh ./scripts/install-content-tools.ps1`, + ); + } + return executable; +} + +// Git enumerates source without traversing ignored dependencies or build output. +// Include new files so local checks cover work before it is staged. +const listing = explicit + ? args + : (() => { + const result = spawnSync( + "git", + ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], + { encoding: "utf8" }, + ); + if (result.error) throw result.error; + if (result.status !== 0) + throw new Error(`Cannot enumerate source files: ${result.stderr}`); + return result.stdout.split("\0").filter(Boolean); + })(); +const files = [...new Set(listing)].map((file) => + path.relative(root, path.resolve(root, file)).replaceAll("\\", "/"), +); +if (files.some((file) => file.startsWith("../") || path.isAbsolute(file))) + throw new Error("Content inputs must be inside this checkout."); +const source = files.filter( + (file) => + existsSync(file) && + !/(^|\/)(bin|obj|node_modules|artifacts|\.git|\.vs)\//i.test(file) && + !file.startsWith(".agents/skills/"), +); +const content = source.filter((file) => + /\.(md|jsonc?|ya?ml|css|[cm]?js)$/.test(file), +); +const markdown = source.filter((file) => file.endsWith(".md")); +const shell = source.filter( + (file) => file.endsWith(".sh") || file === ".husky/pre-commit", +); +const workflows = source.filter( + (file) => + /\.ya?ml$/.test(file) && + (explicit || file.startsWith(".github/workflows/")), +); +const selected = (name) => + mode === "all" || + mode === name || + (mode === "format" && ["prettier", "shell-format"].includes(name)); + +try { + if (selected("prettier") && content.length) { + run( + process.execPath, + [ + npmTool("prettier", "bin/prettier.cjs"), + fix ? "--write" : "--check", + ...content, + ], + "Prettier (fix: pwsh ./scripts/check.ps1 format -Fix)", + ); + } + if (selected("markdown") && markdown.length) { + run( + process.execPath, + [npmTool("markdownlint-cli2", "markdownlint-cli2-bin.mjs"), ...markdown], + "Markdown structure", + ); + } + if (selected("workflows") && workflows.length) { + npmTool("markdownlint-cli2", "markdownlint-cli2-bin.mjs"); + const { default: parseYaml } = + await import("markdownlint-cli2/parsers/yaml"); + for (const file of workflows) { + checkDeploymentCachePolicy(file, parseYaml(readFileSync(file, "utf8"))); + } + // Explicit ShellCheck path keeps Bash run-block checks identical on both OSes. + run( + nativeTool("actionlint"), + [ + "-color", + "-shellcheck", + nativeTool("shellcheck"), + "-pyflakes=", + ...workflows, + ], + "workflow syntax and Bash run blocks", + ); + } + if (selected("shellcheck") && shell.length) { + run( + nativeTool("shellcheck"), + [ + "--external-sources", + "--source-path=SCRIPTDIR", + "--severity=style", + ...shell, + ], + "ShellCheck", + ); + } + if (selected("shell-format") && shell.length) { + run( + nativeTool("shfmt"), + [fix ? "-w" : "-d", ...shell], + "shfmt (fix: pwsh ./scripts/check.ps1 format -Fix)", + ); + } + if ((selected("links") || mode === "external-links") && markdown.length) { + const external = mode === "external-links"; + run( + nativeTool("lychee"), + [ + "--config", + external ? ".lychee-external.toml" : ".lychee.toml", + "--root-dir", + root, + ...markdown, + ], + external + ? "external links (maintenance report)" + : "local links and anchors", + ); + } + console.log(`PASS content ${mode}`); +} catch (error) { + console.error(error.message); + process.exitCode = 1; +} diff --git a/scripts/check.ps1 b/scripts/check.ps1 index c65d0ac..0d037b2 100644 --- a/scripts/check.ps1 +++ b/scripts/check.ps1 @@ -6,7 +6,7 @@ Runs the same validation locally and in CI; see docs/quality/testing.md. [CmdletBinding()] param( [Parameter(Position = 0)] - [ValidateSet('format', 'quick', 'full', 'browser')] + [ValidateSet('format', 'content', 'external-links', 'quick', 'full', 'browser')] [string] $Mode = 'quick', [switch] $Fix, [switch] $Serial, @@ -33,11 +33,29 @@ try { } } - $scope = if ($Mode -eq 'format') { 'quick' } else { $Mode } + if ($Mode -eq 'full') { + $npm = if ($IsWindows) { 'npm.cmd' } else { 'npm' } + Invoke-Check 'content-dependencies' $npm @('ci', '--ignore-scripts') + } + $scope = if ($Mode -eq 'external-links') { 'content' } else { $Mode } Invoke-Check 'doctor' (Join-Path $PSHOME $(if ($IsWindows) { 'pwsh.exe' } else { 'pwsh' })) @( '-NoProfile', '-File', "$PSScriptRoot/doctor.ps1", '-Scope', $scope ) + if ($Mode -in @('content', 'external-links', 'full', 'format')) { + $contentMode = switch ($Mode) { + 'external-links' { 'external-links' } + 'format' { 'format' } + default { 'all' } + } + $contentArguments = @('scripts/check-content.mjs', $contentMode) + if ($Fix) { $contentArguments += '--fix' } + Invoke-Check 'content' 'node' $contentArguments + if ($Mode -eq 'full') { + Invoke-Check 'content-regressions' 'node' @('--test', 'tests/content-checks/check.test.mjs') + } + } + if ($Mode -eq 'browser') { # Browser installation is an explicit setup step, never part of a check. if (-not (Test-Path tests/deploy-smoke/node_modules/playwright/package.json)) { @@ -50,7 +68,7 @@ try { } Invoke-Check 'browser-smoke' 'node' @('tests/deploy-smoke/smoke.mjs') } - else { + elseif ($Mode -notin @('content', 'external-links')) { $msbuildArguments = if ($Serial) { @('-m:1', '-p:BuildInParallel=false', '-nodeReuse:false') } else { @() } Invoke-Check 'restore' 'dotnet' (@('restore', 'opengamebuilder.slnx', '--locked-mode') + $msbuildArguments) $formatArguments = @('format', 'opengamebuilder.slnx', '--no-restore', '--report', 'artifacts/validation/format.json') diff --git a/scripts/content-tools.json b/scripts/content-tools.json new file mode 100644 index 0000000..1d83d4b --- /dev/null +++ b/scripts/content-tools.json @@ -0,0 +1,85 @@ +{ + "schemaVersion": 1, + "tools": { + "actionlint": { + "version": "1.7.12", + "releaseUrl": "https://github.com/rhysd/actionlint/releases/tag/v1.7.12", + "platforms": { + "windows-x64": { + "url": "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_windows_amd64.zip", + "archiveType": "zip", + "archiveSha256": "6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9", + "executable": "actionlint.exe", + "executableSha256": "54ca21be3de4c7cfa26914aa8b61bd76bf573ef3caac5f80d110558cdf241718" + }, + "linux-x64": { + "url": "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz", + "archiveType": "tar.gz", + "archiveSha256": "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8", + "executable": "actionlint", + "executableSha256": "c872d6db8c6bf83a8eaa704fc93999f027d55dffbc63b8a6abdccb47df5f4cd4" + } + } + }, + "shellcheck": { + "version": "0.11.0", + "releaseUrl": "https://github.com/koalaman/shellcheck/releases/tag/v0.11.0", + "platforms": { + "windows-x64": { + "url": "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.zip", + "archiveType": "zip", + "archiveSha256": "8a4e35ab0b331c85d73567b12f2a444df187f483e5079ceffa6bda1faa2e740e", + "executable": "shellcheck.exe", + "executableSha256": "c9e82ada36ef4b8d4caf1f97fa89289048c8f4a33c2c76ffffc88bfe09ff00c5" + }, + "linux-x64": { + "url": "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.gz", + "archiveType": "tar.gz", + "archiveSha256": "b7af85e41cc99489dcc21d66c6d5f3685138f06d34651e6d34b42ec6d54fe6f6", + "executable": "shellcheck", + "executableSha256": "4da528ddb3a4d1b7b24a59d4e16eb2f5fd960f4bd9a3708a15baddbdf1d5a55b" + } + } + }, + "shfmt": { + "version": "3.14.1", + "releaseUrl": "https://github.com/mvdan/sh/releases/tag/v3.14.1", + "platforms": { + "windows-x64": { + "url": "https://github.com/mvdan/sh/releases/download/v3.14.1/shfmt_v3.14.1_windows_amd64.exe", + "archiveType": "file", + "archiveSha256": "13629ce28442ca80b6b5a819f7574ab39e1c28c6e26734ca816c9714e04851df", + "executable": "shfmt.exe", + "executableSha256": "13629ce28442ca80b6b5a819f7574ab39e1c28c6e26734ca816c9714e04851df" + }, + "linux-x64": { + "url": "https://github.com/mvdan/sh/releases/download/v3.14.1/shfmt_v3.14.1_linux_amd64", + "archiveType": "file", + "archiveSha256": "76e77641faa025814b77f153b29796b8e6fa2fca03e0c76a691608b86c7ea7bf", + "executable": "shfmt", + "executableSha256": "76e77641faa025814b77f153b29796b8e6fa2fca03e0c76a691608b86c7ea7bf" + } + } + }, + "lychee": { + "version": "0.24.2", + "releaseUrl": "https://github.com/lycheeverse/lychee/releases/tag/lychee-v0.24.2", + "platforms": { + "windows-x64": { + "url": "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-x86_64-pc-windows-msvc.zip", + "archiveType": "zip", + "archiveSha256": "32975d1493ee1a975d6bb41e4fb56fe419cb442ded628bb772ba2e614acfacad", + "executable": "lychee.exe", + "executableSha256": "2d15a3f78ac680103720b0c59667dc6f1da7de35f7e9c95171bdf4b76fb6829b" + }, + "linux-x64": { + "url": "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-x86_64-unknown-linux-gnu.tar.gz", + "archiveType": "tar.gz", + "archiveSha256": "1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a", + "executable": "lychee", + "executableSha256": "87e6e75195df5753f08c53b5c0a13694b8328edd8df009914ae9e29b5000162d" + } + } + } + } +} diff --git a/scripts/doctor.ps1 b/scripts/doctor.ps1 index 9151c39..c67c0cd 100644 --- a/scripts/doctor.ps1 +++ b/scripts/doctor.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [ValidateSet('quick', 'full', 'browser', 'development')] + [ValidateSet('format', 'content', 'quick', 'full', 'browser', 'development')] [string] $Scope = 'development' ) @@ -159,15 +159,22 @@ if ($PSVersionTable.PSVersion.Major -ge 7) { Write-Check PASS "PowerShell $power else { Write-Check FAIL "PowerShell $powerShellVersion is unsupported. Install PowerShell 7 and rerun with pwsh." } Show-ManifestPins -RequirePlaywright:($Scope -in @('full', 'browser')) -if ($Scope -in @('quick', 'full', 'development')) { Test-DotNetSdk } -if ($Scope -in @('full', 'development')) { +if ($Scope -in @('format', 'quick', 'full', 'development')) { Test-DotNetSdk } +if ($Scope -in @('format', 'content', 'full', 'development')) { Test-Tool 'Git' 'git' 'Install Git (Git for Windows on Windows).' '^git version ' - Test-Bash } -if ($Scope -in @('full', 'browser')) { +if ($Scope -in @('full', 'development')) { Test-Bash } +if ($Scope -in @('format', 'content', 'full', 'browser')) { Test-Tool 'Node.js' 'node' 'Install Node.js 22.' '^v?22(?:\.|$)' Test-Tool 'npm' 'npm' 'Install npm with Node.js 22.' '^\d+\.' } +if ($Scope -in @('format', 'content', 'full')) { + try { + & (Join-Path $PSScriptRoot 'install-content-tools.ps1') -Verify + Write-Check PASS 'Pinned content-tool executable checksums verified.' + } + catch { Write-Check FAIL $_.Exception.Message } +} if ($Scope -eq 'full') { Test-DockerCompose } if ($Scope -eq 'development') { Test-Aspire } diff --git a/scripts/install-content-tools.ps1 b/scripts/install-content-tools.ps1 new file mode 100644 index 0000000..2cd34ba --- /dev/null +++ b/scripts/install-content-tools.ps1 @@ -0,0 +1,232 @@ +#Requires -Version 7.0 +<# +.SYNOPSIS +Installs or verifies the repository's pinned native content-checking tools. +#> +[CmdletBinding()] +param( + [switch] $Verify +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $false + +function Get-ContentToolsPlatform { + $architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture + if ($architecture -ne [System.Runtime.InteropServices.Architecture]::X64) { + throw "Content tools support only x64 hosts; detected $architecture." + } + + if ($IsWindows) { return 'windows-x64' } + if ($IsLinux) { return 'linux-x64' } + throw 'Content tools support only Windows x64 and Linux x64.' +} + +function Resolve-ChildPath { + param( + [Parameter(Mandatory)] [string] $Parent, + [Parameter(Mandatory)] [string] $Child + ) + + $parentPath = [System.IO.Path]::GetFullPath($Parent) + $childPath = [System.IO.Path]::GetFullPath($Child) + $separator = [System.IO.Path]::DirectorySeparatorChar + $parentPrefix = $parentPath.TrimEnd( + [System.IO.Path]::DirectorySeparatorChar, + [System.IO.Path]::AltDirectorySeparatorChar + ) + $separator + $comparison = if ($IsWindows) { + [System.StringComparison]::OrdinalIgnoreCase + } + else { + [System.StringComparison]::Ordinal + } + + if (-not $childPath.StartsWith($parentPrefix, $comparison)) { + throw "Path '$childPath' is outside '$parentPath'." + } + return $childPath +} + +function Get-Sha256 { + param([Parameter(Mandatory)] [string] $Path) + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Assert-Sha256Value { + param( + [Parameter(Mandatory)] [string] $Name, + [Parameter(Mandatory)] [string] $Value + ) + + if ($Value -cnotmatch '^[0-9a-f]{64}$') { + throw "Manifest value '$Name' must be a lowercase SHA-256 hash." + } +} + +function Test-InstalledTool { + param( + [Parameter(Mandatory)] [string] $Path, + [Parameter(Mandatory)] [string] $ExpectedSha256 + ) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + if ((Get-Sha256 -Path $Path) -cne $ExpectedSha256) { return $false } + if (-not $IsWindows) { + $executeBits = [System.IO.UnixFileMode]::UserExecute -bor + [System.IO.UnixFileMode]::GroupExecute -bor + [System.IO.UnixFileMode]::OtherExecute + $mode = [System.IO.File]::GetUnixFileMode($Path) + if (($mode -band $executeBits) -ne $executeBits) { return $false } + } + return $true +} + +$repositoryRoot = [System.IO.Path]::GetFullPath((Split-Path $PSScriptRoot -Parent)) +$manifestPath = Resolve-ChildPath -Parent $repositoryRoot -Child (Join-Path $PSScriptRoot 'content-tools.json') +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schemaVersion -ne 1) { + throw "Unsupported content-tools manifest schema '$($manifest.schemaVersion)'." +} + +$platform = Get-ContentToolsPlatform +$contentToolsRoot = Resolve-ChildPath -Parent $repositoryRoot -Child ( + Join-Path $repositoryRoot "artifacts/content-tools/$platform" +) +$installCommand = 'pwsh ./scripts/install-content-tools.ps1' + +$tools = @($manifest.tools.PSObject.Properties) +if ($tools.Count -eq 0) { throw 'The content-tools manifest contains no tools.' } + +if ($Verify) { + $problems = [System.Collections.Generic.List[string]]::new() + foreach ($toolProperty in $tools) { + $name = $toolProperty.Name + $tool = $toolProperty.Value + $asset = $tool.platforms.$platform + if (-not $asset) { + $problems.Add("$name has no $platform asset") + continue + } + + Assert-Sha256Value -Name "$name executableSha256" -Value $asset.executableSha256 + $toolDirectory = Resolve-ChildPath -Parent $contentToolsRoot -Child (Join-Path $contentToolsRoot $name) + $executablePath = Resolve-ChildPath -Parent $toolDirectory -Child ( + Join-Path $toolDirectory $asset.executable + ) + if (-not (Test-Path -LiteralPath $executablePath -PathType Leaf)) { + $problems.Add("$name $($tool.version) is missing at $executablePath") + continue + } + if (-not (Test-InstalledTool -Path $executablePath -ExpectedSha256 $asset.executableSha256)) { + $problems.Add("$name $($tool.version) is stale or corrupt at $executablePath") + continue + } + + Write-Host "$name $($tool.version) $executablePath" + } + + if ($problems.Count -gt 0) { + throw (($problems -join [Environment]::NewLine) + + "`nInstall the pinned tools with: $installCommand") + } + return +} + +New-Item -ItemType Directory -Path $contentToolsRoot -Force | Out-Null +foreach ($toolProperty in $tools) { + $name = $toolProperty.Name + $tool = $toolProperty.Value + $asset = $tool.platforms.$platform + if (-not $asset) { throw "$name has no $platform asset." } + if ($asset.archiveType -notin @('file', 'zip', 'tar.gz')) { + throw "$name has unsupported archive type '$($asset.archiveType)'." + } + $assetUri = $null + if (-not [System.Uri]::TryCreate( + [string] $asset.url, + [System.UriKind]::Absolute, + [ref] $assetUri + ) -or $assetUri.Scheme -ne [System.Uri]::UriSchemeHttps) { + throw "$name asset URL must use HTTPS." + } + if ([System.IO.Path]::GetFileName([string] $asset.executable) -cne $asset.executable) { + throw "$name executable must be a file name without directory components." + } + Assert-Sha256Value -Name "$name archiveSha256" -Value $asset.archiveSha256 + Assert-Sha256Value -Name "$name executableSha256" -Value $asset.executableSha256 + + $toolDirectory = Resolve-ChildPath -Parent $contentToolsRoot -Child (Join-Path $contentToolsRoot $name) + $executablePath = Resolve-ChildPath -Parent $toolDirectory -Child ( + Join-Path $toolDirectory $asset.executable + ) + if (Test-InstalledTool -Path $executablePath -ExpectedSha256 $asset.executableSha256) { + Write-Host "$name $($tool.version) $executablePath" + continue + } + + $temporaryRoot = Resolve-ChildPath -Parent $contentToolsRoot -Child ( + Join-Path $contentToolsRoot ('.install-' + $name + '-' + [guid]::NewGuid().ToString('N')) + ) + New-Item -ItemType Directory -Path $temporaryRoot | Out-Null + try { + $sourceRoot = Resolve-ChildPath -Parent $temporaryRoot -Child (Join-Path $temporaryRoot 'source') + New-Item -ItemType Directory -Path $sourceRoot | Out-Null + $downloadName = if ($asset.archiveType -eq 'file') { $asset.executable } else { 'asset' } + $downloadPath = Resolve-ChildPath -Parent $sourceRoot -Child (Join-Path $sourceRoot $downloadName) + + Write-Host "Downloading $name $($tool.version) from $($asset.url)" + Invoke-WebRequest -Uri $asset.url -OutFile $downloadPath + $downloadSha256 = Get-Sha256 -Path $downloadPath + if ($downloadSha256 -cne $asset.archiveSha256) { + throw "$name asset checksum mismatch: expected $($asset.archiveSha256), got $downloadSha256." + } + + if ($asset.archiveType -ne 'file') { + $extractRoot = Resolve-ChildPath -Parent $temporaryRoot -Child (Join-Path $temporaryRoot 'extracted') + New-Item -ItemType Directory -Path $extractRoot | Out-Null + if ($asset.archiveType -eq 'zip') { + Expand-Archive -LiteralPath $downloadPath -DestinationPath $extractRoot + } + else { + & tar '--extract' '--gzip' '--file' $downloadPath '--directory' $extractRoot + if ($LASTEXITCODE -ne 0) { + throw "$name archive extraction failed with exit code $LASTEXITCODE." + } + } + $sourceRoot = $extractRoot + } + + $candidates = @(Get-ChildItem -LiteralPath $sourceRoot -Recurse -File | + Where-Object { $_.Name -ceq $asset.executable }) + if ($candidates.Count -ne 1) { + throw "$name asset must contain exactly one '$($asset.executable)'; found $($candidates.Count)." + } + + $candidatePath = Resolve-ChildPath -Parent $sourceRoot -Child $candidates[0].FullName + $candidateSha256 = Get-Sha256 -Path $candidatePath + if ($candidateSha256 -cne $asset.executableSha256) { + throw "$name executable checksum mismatch: expected $($asset.executableSha256), got $candidateSha256." + } + + New-Item -ItemType Directory -Path $toolDirectory -Force | Out-Null + Copy-Item -LiteralPath $candidatePath -Destination $executablePath -Force + if (-not $IsWindows) { + & chmod '755' '--' $executablePath + if ($LASTEXITCODE -ne 0) { + throw "$name chmod failed with exit code $LASTEXITCODE." + } + } + if (-not (Test-InstalledTool -Path $executablePath -ExpectedSha256 $asset.executableSha256)) { + throw "$name failed verification after installation." + } + } + finally { + if (Test-Path -LiteralPath $temporaryRoot) { + $temporaryRoot = Resolve-ChildPath -Parent $contentToolsRoot -Child $temporaryRoot + Remove-Item -LiteralPath $temporaryRoot -Recurse -Force + } + } + + Write-Host "$name $($tool.version) $executablePath" +} diff --git a/scripts/release-lib.sh b/scripts/release-lib.sh index 0e8e134..faa59e0 100644 --- a/scripts/release-lib.sh +++ b/scripts/release-lib.sh @@ -6,6 +6,8 @@ set -euo pipefail # Plain SemVer X.Y.Z without prerelease/build metadata. SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' +# Used by callers that source this library. +# shellcheck disable=SC2034 TAG_REGEX='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' repo_root() { diff --git a/scripts/verify-api-image.sh b/scripts/verify-api-image.sh index b46b781..ccdbacc 100644 --- a/scripts/verify-api-image.sh +++ b/scripts/verify-api-image.sh @@ -12,6 +12,8 @@ docker run --rm --entrypoint sh "$image" -c ' probe_network="ogb-api-probe-$$" container_id='' +# Invoked by the EXIT trap below, including failed image checks. +# shellcheck disable=SC2329 cleanup() { if [[ -n "$container_id" ]]; then docker stop "$container_id" >/dev/null 2>&1 || true @@ -36,7 +38,7 @@ container_id="$(docker run --rm -d \ "$image")" port="$(docker port "$container_id" 8080/tcp | sed -n '1s/.*://p')" test -n "$port" -for attempt in {1..30}; do +for _attempt in {1..30}; do if response="$(curl --fail --silent \ --header 'X-Forwarded-Proto: https' \ "http://127.0.0.1:${port}/api/alive")" && [[ "$response" == 'Healthy' ]]; then diff --git a/scripts/workflow-cache-policy.mjs b/scripts/workflow-cache-policy.mjs new file mode 100644 index 0000000..f6c77d8 --- /dev/null +++ b/scripts/workflow-cache-policy.mjs @@ -0,0 +1,19 @@ +// Compensate only for actionlint's documented cache-mode schema gap. +export function checkDeploymentCachePolicy(file, workflow) { + if ( + !/^\.github\/workflows\/(_deploy|cd-production|cd-staging)\.yml$/.test(file) + ) + return; + if (workflow["cache-mode"] !== "none") { + throw new Error( + `${file}: protected-source deployment requires root cache-mode: none.`, + ); + } + for (const [job, definition] of Object.entries(workflow.jobs ?? {})) { + if ("cache-mode" in definition && definition["cache-mode"] !== "none") { + throw new Error( + `${file}: job ${job} must not override cache-mode: none.`, + ); + } + } +} diff --git a/tests/content-checks/check.test.mjs b/tests/content-checks/check.test.mjs new file mode 100644 index 0000000..0a1af73 --- /dev/null +++ b/tests/content-checks/check.test.mjs @@ -0,0 +1,160 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; +import { getFileInfo } from "prettier"; +import { checkDeploymentCachePolicy } from "../../scripts/workflow-cache-policy.mjs"; + +test("Prettier respects formatter ownership and generated/vendor boundaries", async () => { + const ignored = [ + "scripts/apply-edge.sh", + "scripts/check.ps1", + "src/OpenGameBuilder.Web.Client/wwwroot/index.html", + "src/OpenGameBuilder.Api/OpenGameBuilder.Api.csproj", + ".agents/skills/aspire/SKILL.md", + "src/OpenGameBuilder.Api/packages.lock.json", + "node_modules/prettier/README.md", + ]; + const included = [ + "README.md", + ".github/workflows/ci.yml", + "scripts/check-content.mjs", + "src/OpenGameBuilder.Web.Client/wwwroot/css/app.css", + ]; + for (const file of [...ignored, ...included]) { + const info = await getFileInfo(path.join(root, file), { + ignorePath: path.join(root, ".prettierignore"), + }); + assert.equal(info.ignored, ignored.includes(file), file); + } +}); + +test("deployment cache exception still rejects missing, writable, and overridden cache modes", () => { + for (const file of ["_deploy", "cd-production", "cd-staging"]) { + const name = `.github/workflows/${file}.yml`; + for (const mode of [undefined, "full", "read-only", true, "None"]) { + assert.throws( + () => checkDeploymentCachePolicy(name, { "cache-mode": mode }), + /requires root cache-mode: none/, + ); + } + assert.throws( + () => + checkDeploymentCachePolicy(name, { + "cache-mode": "none", + jobs: { build: { "cache-mode": "full" } }, + }), + /must not override/, + ); + checkDeploymentCachePolicy(name, { + "cache-mode": "none", + jobs: { build: {} }, + }); + } +}); + +const root = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../..", +); + +// Each fixture is an ordinary, untracked first-party input. The runner must see +// new work before staging; keep these outside its generated-output exclusions. +function fixture(action) { + const directory = mkdtempSync(path.join(root, ".content-fixture-")); + const write = (name, content) => { + const file = path.join(directory, name); + writeFileSync(file, content); + return file; + }; + try { + action(write); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +function check(mode, files, expected, diagnostic, fix = false) { + const result = spawnSync( + process.execPath, + [ + path.join(root, "scripts/check-content.mjs"), + mode, + ...(fix ? ["--fix"] : []), + "--files", + ...files, + ], + { cwd: root, encoding: "utf8" }, + ); + if (result.error) throw result.error; + const output = result.stdout + result.stderr; + assert.equal(result.status, expected, output); + if (diagnostic) assert.match(output, diagnostic); +} + +test("formatting failure has a fix that passes the same check", () => + fixture((write) => { + const file = write("style.json", '{"hello":1}\n'); + check("prettier", [file], 1, /Code style issues/); + check("prettier", [file], 0, undefined, true); + check("prettier", [file], 0); + assert.equal(readFileSync(file, "utf8"), '{ "hello": 1 }\n'); + })); + +test("Markdown structure rejects a skipped heading level", () => + fixture((write) => { + const bad = write("structure.md", "# Title\n\n### Skipped level\n"); + check("markdown", [bad], 1, /MD001/); + const good = write("structure.md", "# Title\n\n## Next level\n"); + check("markdown", [good], 0); + })); + +test("offline links check files, images, anchors and valid cross-document links", () => + fixture((write) => { + write("target.md", "# Target\n\n## Useful heading\n"); + const good = write( + "links.md", + "# Links\n\n[Target](target.md#useful-heading)\n\n[Remote ignored](https://unreachable.invalid/)\n", + ); + check("links", [good], 0); + for (const link of [ + "[Missing](missing.md)", + "![Missing image](missing.png)", + "[Bad anchor](target.md#not-a-heading)", + ]) { + const bad = write("links.md", `# Links\n\n${link}\n`); + check("links", [bad], 1, /local links and anchors failed/); + } + })); + +test("actionlint rejects invalid workflow keys and checks Bash run blocks", () => + fixture((write) => { + const workflow = (run) => + `name: Fixture\non: push\njobs:\n check:\n runs-on: ubuntu-latest\n steps:\n - run: ${run}\n`; + const bad = write( + "workflow.yml", + workflow("echo ok").replace("runs-on:", "runs-onn:"), + ); + check("workflows", [bad], 1, /runs-onn/); + const shell = write("workflow.yml", workflow("echo $GITHUB_WORKSPACE")); + check("workflows", [shell], 1, /SC2086/); + const good = write("workflow.yml", workflow('echo "$GITHUB_WORKSPACE"')); + check("workflows", [good], 0); + })); + +test("ShellCheck rejects word splitting and shfmt supplies a stable fix", () => + fixture((write) => { + const bad = write("script.sh", "#!/usr/bin/env bash\necho $1\n"); + check("shellcheck", [bad], 1, /SC2086/); + const good = write("script.sh", '#!/usr/bin/env bash\necho "$1"\n'); + check("shellcheck", [good], 0); + const ugly = write( + "script.sh", + '#!/usr/bin/env bash\nif true; then\necho "hello"; fi\n', + ); + check("shell-format", [ugly], 1, /shfmt.*failed/); + check("shell-format", [ugly], 0, undefined, true); + check("shell-format", [ugly], 0); + })); diff --git a/tests/deploy-app/run.sh b/tests/deploy-app/run.sh index a2adb3b..ca4ca18 100644 --- a/tests/deploy-app/run.sh +++ b/tests/deploy-app/run.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# Fixture Compose files must retain literal variable references for Docker. +# shellcheck disable=SC2016 set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" diff --git a/tests/deploy-topology/run.sh b/tests/deploy-topology/run.sh index 80b369d..8d849e6 100644 --- a/tests/deploy-topology/run.sh +++ b/tests/deploy-topology/run.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# These assertions match literal workflow expressions and shell source text. +# shellcheck disable=SC2016 set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" diff --git a/tests/supply-chain/run.sh b/tests/supply-chain/run.sh index 8ec3afb..a1cc219 100644 --- a/tests/supply-chain/run.sh +++ b/tests/supply-chain/run.sh @@ -23,7 +23,7 @@ done < <(grep -RhE --include='*.yml' --include='*.yaml' '^[[:space:]]*(- )?uses: echo "PASS ${action_count} third-party action references use reviewed commit SHAs" base_count=0 -while read -r directive image_ref remainder; do +while read -r directive image_ref _remainder; do [[ "$directive" == FROM ]] || continue [[ "$image_ref" == scratch ]] && continue [[ "$image_ref" =~ @sha256:[0-9a-f]{64}$ ]] || fail "Dockerfile base image is not pinned by digest: $image_ref" @@ -34,6 +34,8 @@ grep -Eq '^[[:space:]]+image:[[:space:]]+[^[:space:]@]+@sha256:[0-9a-f]{64}$' de fail 'edge image is not pinned by digest' echo "PASS ${base_count} API base images and the edge image use immutable digests" +# Match the Dockerfile variable reference without expanding it in this test. +# shellcheck disable=SC2016 grep -Fq 'USER $APP_UID' src/OpenGameBuilder.Api/Dockerfile || fail 'API image lacks an explicit non-root user' if grep -R -n -F 'ssh-keyscan' .github/workflows; then fail 'deployment workflow still learns SSH trust with ssh-keyscan' @@ -75,6 +77,8 @@ mapfile -t api_image_patterns < <(awk ' for dependency in dotnet/sdk dotnet/aspnet; do matched=false for pattern in "${api_image_patterns[@]}"; do + # Dependabot patterns intentionally match globs, not literal strings. + # shellcheck disable=SC2053 if [[ "$dependency" == $pattern ]]; then matched=true break