diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index 70c38b7..0000000 --- a/.browserslistrc +++ /dev/null @@ -1,12 +0,0 @@ -# Browser support policy for frontend compatibility tooling. -# See docs/frontend/browser-support.md. - -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/.config/dotnet-tools.json b/.config/dotnet-tools.json index db9b9d8..614254c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -4,10 +4,8 @@ "tools": { "husky": { "version": "0.9.1", - "commands": [ - "husky" - ], + "commands": ["husky"], "rollForward": false } } -} \ No newline at end of file +} diff --git a/.editorconfig b/.editorconfig index 5b29542..524cb0a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,6 +24,10 @@ generated_code = true # C# files [*.cs] end_of_line = lf +# Enforce two focused, existing code-style expectations in format/build checks. +dotnet_diagnostic.IDE0040.severity = warning +dotnet_diagnostic.IDE0044.severity = warning +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning # New line preferences csharp_new_line_before_open_brace = all csharp_new_line_before_else = true @@ -179,12 +183,17 @@ trim_trailing_whitespace = false [*.{props,targets,config}] indent_size = 2 -# Data serialization -[*.{json,yaml,yml}] +# Prettier owns first-party content formatting. Preserve authored prose wrapping. +[*.{md,json,jsonc,yaml,yml,css,js,mjs,cjs}] indent_size = 2 +end_of_line = lf # Shell scripts [*.sh] +indent_size = 2 +end_of_line = lf +[.husky/pre-commit] +indent_size = 2 end_of_line = lf [*.{cmd,bat}] end_of_line = crlf diff --git a/.gitattributes b/.gitattributes index 74718db..ffc93eb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,11 +15,15 @@ *.html text eol=lf *.css text eol=lf *.js text eol=lf +*.mjs text eol=lf +*.cjs text eol=lf +*.jsonc text eol=lf *.ts text eol=lf Caddyfile* text eol=lf # Scripts *.sh text eol=lf +.husky/pre-commit text eol=lf *.ps1 text eol=crlf *.bat text eol=crlf 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/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..ba381c7 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,13 @@ +# actionlint 1.7.12 predates GitHub's cache-mode key. Keep the security setting; +# check-content.mjs separately requires none in these deployment entry points. +# Remove only these exceptions when a pinned actionlint release supports the key. +paths: + .github/workflows/_deploy.yml: + ignore: + - '^unexpected key "cache-mode" for "workflow" section\.' + .github/workflows/cd-production.yml: + ignore: + - '^unexpected key "cache-mode" for "workflow" section\.' + .github/workflows/cd-staging.yml: + ignore: + - '^unexpected key "cache-mode" for "workflow" section\.' diff --git a/.github/actions/validate/action.yml b/.github/actions/validate/action.yml index 8781cfc..56e7f1e 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 @@ -29,72 +33,22 @@ 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: 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 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + package-manager-cache: false - - name: Test application rollback without Docker or SSH - shell: bash + - name: Install pinned content tools + shell: pwsh working-directory: ${{ inputs.working-directory }} - run: | - set -o pipefail - bash tests/deploy-app/run.sh 2>&1 | tee artifacts/validation/deploy-app.log + run: ./scripts/install-content-tools.ps1 - - 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/dependabot.yml b/.github/dependabot.yml index 5311134..6fb7186 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -39,6 +39,37 @@ 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 + + # First-party formatting and Markdown lint dependencies. + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:45" + timezone: "America/Indiana/Indianapolis" + open-pull-requests-limit: 3 + 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/.github/pull_request_template.md b/.github/pull_request_template.md index cac8129..493362c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,6 @@ + + + ## Summary Describe what this pull request changes. diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml index 759b9f3..9aaf3b2 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 @@ -95,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: | @@ -293,13 +294,13 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '22' + node-version: "22" - name: Install browser smoke dependencies 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: @@ -358,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..349d619 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,11 +2,11 @@ name: "CodeQL Advanced" on: push: - branches: [ "main", "patch/v*" ] + branches: ["main", "patch/v*"] pull_request: - branches: [ "main", "patch/v*" ] + branches: ["main", "patch/v*"] schedule: - - cron: '38 8 * * 2' + - cron: "38 8 * * 2" # Cancel superseded in-progress runs so the PR diff check compares against the # latest commit's analysis instead of a stale or incomplete run. @@ -34,43 +34,44 @@ jobs: fail-fast: false matrix: include: - - language: actions - build-mode: none - - language: csharp - # Trace the real project build, including Razor source generators. - # No-build extraction uses a synthetic Razor compilation that can log - # compiler failures even while reporting a successful analysis. - build-mode: manual + - language: actions + build-mode: none + - language: csharp + # Trace the real project build, including Razor source generators. + # No-build extraction uses a synthetic Razor compilation that can log + # compiler failures even while reporting a successful analysis. + build-mode: manual steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: Set up .NET - if: matrix.language == 'csharp' - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - global-json-file: global.json - cache: false + - name: Set up .NET + if: matrix.language == 'csharp' + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + global-json-file: global.json + cache: false - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # Do not persist dependencies restored while executing pull-request code. - dependency-caching: false + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # Do not persist dependencies restored while executing pull-request code. + dependency-caching: false - - name: Build C# for CodeQL - if: matrix.build-mode == 'manual' - shell: bash - run: | - dotnet restore opengamebuilder.slnx - dotnet build opengamebuilder.slnx --configuration Release --no-restore --no-incremental + - name: Build C# for CodeQL + if: matrix.build-mode == 'manual' + shell: bash + run: | + dotnet --version + dotnet restore opengamebuilder.slnx --locked-mode + dotnet build opengamebuilder.slnx --configuration Release --no-restore --no-incremental - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 + with: + category: "/language:${{matrix.language}}" 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 86a6c85..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", @@ -13,9 +35,7 @@ "--verbosity", "minimal" ], - "include": [ - "**/*.cs" - ] + "include": ["**/*.cs"] } ] -} \ No newline at end of file +} 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/.mcp.json b/.mcp.json index abe98b5..bf8f46a 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,10 +2,7 @@ "mcpServers": { "aspire": { "command": "aspire", - "args": [ - "agent", - "mcp" - ] + "args": ["agent", "mcp"] } } -} \ No newline at end of file +} 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 d19ed2d..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" ] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index dfe13d5..603486c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,10 +37,7 @@ "compounds": [ { "name": "Launch All (API + Web)", - "configurations": [ - "Launch API", - "Launch Web (Blazor WASM)" - ], + "configurations": ["Launch API", "Launch Web (Blazor WASM)"], "stopAll": true, "presentation": { "hidden": false, @@ -49,4 +46,4 @@ } } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e361bee..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", @@ -45,4 +44,4 @@ "Xunit", "zstd" ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b240728..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", @@ -112,4 +135,4 @@ } } ] -} \ No newline at end of file +} 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/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 1c74352..1d834b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,25 +28,17 @@ 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: - -- 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 @@ -60,15 +52,26 @@ 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. +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. ## 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: @@ -107,16 +110,27 @@ Draft pull requests are welcome when you want early feedback. ## Significant Changes -Significant changes should usually be discussed before implementation. +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. -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. +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 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 +166,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/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/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/README.md b/README.md index 49242bc..c9f3181 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,82 @@ # 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 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. + +## 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 [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. + +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. 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 +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/SECURITY.md b/SECURITY.md index 579a40b..91b46d1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,13 +6,13 @@ OpenGameBuilder is in active development. Until stable releases are established, security support applies to: -| Version or branch | Supported | -| --- | --- | -| Current `main` branch | Yes | -| Current public production deployment | Yes | -| Current public staging deployment | Yes, for security reports | -| Older commits, branches, forks, or local deployments | No, unless maintainers say otherwise | -| Archived MyGameBuilder material outside this repository | No | +| Version or branch | Supported | +| ------------------------------------------------------- | ------------------------------------ | +| Current `main` branch | Yes | +| Current public production deployment | Yes | +| Current public staging deployment | Yes, for security reports | +| Older commits, branches, forks, or local deployments | No, unless maintainers say otherwise | +| Archived MyGameBuilder material outside this repository | No | Once OpenGameBuilder has stable public releases, this section will be updated to describe which release lines receive security fixes. diff --git a/SUPPORT.md b/SUPPORT.md index 5022247..29e16c9 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,110 +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 -The official Discord server (if you have access) 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. - -Otherwise, open a GitHub issue only when the question is directly tied to actionable project work. - -## Bugs - -Use GitHub Issues 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. - -Do not include passwords, tokens, private data, sensitive archival material, or security vulnerability details in public issues. +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. -## Feature Requests +## Bugs and Feature Requests -Use GitHub Issues or the Discord server for 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 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. +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: - -[`GOVERNANCE.md`](GOVERNANCE.md) - -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: - -[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) - -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. - -Use public issues or the Discord server for: - -- 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 be reported privately. - -## 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. +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. + +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. + +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). +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/aspire.config.json b/aspire.config.json index 1571235..ae67f01 100644 --- a/aspire.config.json +++ b/aspire.config.json @@ -2,4 +2,4 @@ "appHost": { "path": "src/OpenGameBuilder.AppHost/OpenGameBuilder.AppHost.csproj" } -} \ No newline at end of file +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..36c7af2 --- /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 [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 [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) | +| 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), [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) | + +## 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 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/main/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..e16aa2a 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/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/community/stewardship.md b/docs/community/stewardship.md new file mode 100644 index 0000000..5bd9a26 --- /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..ceaed4d 100644 --- a/docs/foundation-checklist.md +++ b/docs/foundation-checklist.md @@ -1,453 +1,716 @@ # 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 - -- [ ] Describe what works today, the first milestone, non-goals, and project status. -- [ ] Link working setup, contribution, testing, support, roadmap, and license - information before emphasizing deployment badges. -- [ ] 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. - -### 17. Make public support and issue intake usable - -- [ ] 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 - 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. - 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. - -### 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 - -- [ ] 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. - 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. - Explain that `.browserslistrc` alone neither implements nor verifies compatibility. -- [ ] 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. - -### 20. Finish AI tooling maintenance and simplify policy - -- [ ] 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 - cloud/deployment capabilities from approved repository workflows. -- [ ] 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 - 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. - -### 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 - or an approval requirement nobody can satisfy. -- [ ] 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 - 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. - -## 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. + +- [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. +- [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 +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. + +- [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. +- [x] State that production workflows must be dispatched from `main`, separately + from the protected application source `ref`, in [release guidance](release/README.md). +- [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. +- [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. + +**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. + +**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 + +**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. + +- [x] Say that format verification is required and hooks are optional; link the + authoritative setup commands instead of duplicating them. +- [x] Remove the unsupported IIS Express profile, or explicitly support and test + it with matching configuration. Preserve the documented direct and Aspire paths. +- [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. + +**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. + +**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 +repeated contributor prose still create maintenance work without current benefit. + +- [x] Remove or give a useful contributors pointer to `CONTRIBUTORS.md`. Replace + the empty `CHANGELOG.md` with the curated release process in step 18. +- [x] Remove `.browserslistrc` while it has no consumer and update its references. + Keep the actual [browser policy and acceptance matrix](frontend/browser-support.md). +- [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. +- [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. +- [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. +- [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. + +**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. + +**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 +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 + +- [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. +- [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. +- [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. +- [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. + +**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). + +**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 + +- [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. +- [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. +- [x] Add actionlint for workflows, ShellCheck for shell defects, and shfmt for + shell formatting. Use versions compatible with the workflow syntax in this repo. +- [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. +- [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. +- [ ] 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 +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/). + +**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 + 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 e69de29..09e85b5 100644 --- a/docs/frontend/browser-support.md +++ b/docs/frontend/browser-support.md @@ -0,0 +1,91 @@ +# 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. + +## 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. + +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 + +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. +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..511265e 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.* +_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/data-formats.md b/docs/mygamebuilder/data-formats.md index 7f46a7b..7955a67 100644 --- a/docs/mygamebuilder/data-formats.md +++ b/docs/mygamebuilder/data-formats.md @@ -1,3 +1,3 @@ # MyGameBuilder archive - piece file formats -*This document has been moved to the [mygamebuilder-archive](https://github.com/OpenGameBuilder/mygamebuilder-archive/blob/main/FORMATS.md) repo.* +_This document has been moved to the [mygamebuilder-archive](https://github.com/OpenGameBuilder/mygamebuilder-archive/blob/main/FORMATS.md) repo._ 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/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/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..b8c20cc 100644 --- a/docs/quality/testing.md +++ b/docs/quality/testing.md @@ -7,15 +7,17 @@ See [developer setup](../setup/development.md) for SDK prerequisites. ## Project boundaries -| Project | Responsibility | -| --- | --- | -| `tests\OpenGameBuilder.Api.Tests` | In-process HTTP integration tests of the real API, including a real API-client round trip | +| Project | Responsibility | +| ---------------------------------------- | --------------------------------------------------------------------------------------------- | +| `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 -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. +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. 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 @@ -24,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 @@ -51,22 +65,61 @@ 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 -[developer setup](../setup/development.md#command-line-workflow-start-here). +formatting verification, Release build, solution tests, and smoke-package checks +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 +``` + +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 +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 +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. + +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 -`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: +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. @@ -107,10 +160,86 @@ 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. +### 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 +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 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 +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 @@ -120,6 +249,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/docs/release/README.md b/docs/release/README.md index fdad51f..71a2d90 100644 --- a/docs/release/README.md +++ b/docs/release/README.md @@ -3,11 +3,11 @@ OpenGameBuilder ships through GitHub Actions. There are exactly three release 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 | -| 🩹 **Prepare Patch** | Manually dispatched | Create `patch/vX.Y.(Z+1)` branch and a version-bump PR off the latest tag | +| Workflow | Trigger | What it does | +| -------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| 🛰️ **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 explicit edge profile, not an application release. Staging and production may @@ -29,14 +29,45 @@ 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 [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). + +## 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 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 +85,16 @@ commit. The section 14 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 +107,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/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..723b731 --- /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/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/development.md b/docs/setup/development.md index dc396f3..13688ff 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) @@ -24,6 +28,7 @@ browser-certificate support on those platforms. ``` Reopen the terminal after installation if `aspire` is not on `PATH`. + - Microsoft Edge or Google Chrome for Blazor WebAssembly debugging. - For Visual Studio: **[Visual Studio 2026](https://visualstudio.microsoft.com/vs/)** (Community is fine), with **ASP.NET and web development**. The repository's @@ -37,8 +42,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 +54,33 @@ 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`, `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` 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: + +```pwsh dotnet dev-certs https --trust dotnet dev-certs https --check --trust ``` @@ -59,16 +91,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 +100,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 @@ -109,13 +164,13 @@ For a background session instead, use `aspire start`, `aspire wait api`, ### Expected endpoints and success check -| Endpoint | Purpose | -| --- | --- | -| `https://localhost:7001` | Frontend; the home heading shows `OpenGameBuilder (Development)` after loading | -| `https://localhost:7000/api/about` | Application name, version, and API environment JSON | -| `https://localhost:7000/api/alive` | API liveness | -| `https://localhost:7000/scalar` | Interactive API documentation (Development only) | -| `https://localhost:17170` | Aspire dashboard with the default HTTPS profile; use the login URL printed by the launcher | +| Endpoint | Purpose | +| ---------------------------------- | ------------------------------------------------------------------------------------------ | +| `https://localhost:7001` | Frontend; the home heading shows `OpenGameBuilder (Development)` after loading | +| `https://localhost:7000/api/about` | Application name, version, and API environment JSON | +| `https://localhost:7000/api/alive` | API liveness | +| `https://localhost:7000/scalar` | Interactive API documentation (Development only) | +| `https://localhost:17170` | Aspire dashboard with the default HTTPS profile; use the login URL printed by the launcher | HTTP bindings also exist at `http://localhost:5000` (API) and `http://localhost:5001` (web); use **HTTPS** for this workflow. The AppHost profile @@ -195,6 +250,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 @@ -231,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/docs/setup/github.md b/docs/setup/github.md index e8234b6..6cdb4b4 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 @@ -65,14 +79,14 @@ the following active repository rulesets. Branch rules target **`main` and rules: the GraphQL collection was empty, and the authenticated `main` protection endpoint returned "Branch not protected" (404). Rulesets still protect that branch. -| Ruleset | Requirements | Bypass | -| --- | --- | --- | -| [Main and patch merge gate (16765458)](https://github.com/OpenGameBuilder/opengamebuilder/rules/16765458) | `build-test` from GitHub Actions (`15368`), up-to-date branches, CodeQL, code quality, no deletion or force push | None, including administrators and the release bot | -| [Main linear history (23469610)](https://github.com/OpenGameBuilder/opengamebuilder/rules/23469610) | Linear history on `main` | None | -| [Main and patch human review (23468686)](https://github.com/OpenGameBuilder/opengamebuilder/rules/23468686) | One approval, stale-review dismissal, latest-push approval, resolved conversations, squash-only merges | Organization administrators, **PR-only**, as explained below | -| [Protected branch creation (23468683)](https://github.com/OpenGameBuilder/opengamebuilder/rules/23468683) | Restrict creation of protected branches | Release App, always mode, **creation only** | -| [Release tag creation (23468685)](https://github.com/OpenGameBuilder/opengamebuilder/rules/23468685) | Restrict creation of release tags | Release App, always mode, **creation only** | -| [Release tag immutability (16754313)](https://github.com/OpenGameBuilder/opengamebuilder/rules/16754313) | No tag updates, deletion, or force pushes | None, including administrators and the release bot | +| Ruleset | Requirements | Bypass | +| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| [Main and patch merge gate (16765458)](https://github.com/OpenGameBuilder/opengamebuilder/rules/16765458) | `build-test` from GitHub Actions (`15368`), up-to-date branches, CodeQL, code quality, no deletion or force push | None, including administrators and the release bot | +| [Main linear history (23469610)](https://github.com/OpenGameBuilder/opengamebuilder/rules/23469610) | Linear history on `main` | None | +| [Main and patch human review (23468686)](https://github.com/OpenGameBuilder/opengamebuilder/rules/23468686) | One approval, stale-review dismissal, latest-push approval, resolved conversations, squash-only merges | Organization administrators, **PR-only**, as explained below | +| [Protected branch creation (23468683)](https://github.com/OpenGameBuilder/opengamebuilder/rules/23468683) | Restrict creation of protected branches | Release App, always mode, **creation only** | +| [Release tag creation (23468685)](https://github.com/OpenGameBuilder/opengamebuilder/rules/23468685) | Restrict creation of release tags | Release App, always mode, **creation only** | +| [Release tag immutability (16754313)](https://github.com/OpenGameBuilder/opengamebuilder/rules/16754313) | No tag updates, deletion, or force pushes | None, including administrators and the release bot | The review rule also requires an extra approval for unattributed Copilot PRs. Automated review is not human approval. No code-owner requirement is configured @@ -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` @@ -174,30 +186,25 @@ source or entering a deployment environment. [GitHub's environment rule reference](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments#deployment-branches-and-tags) explains this distinction. -| Workflow run ref | Source input | Environment result without admin bypass | Source check on merged `main` | -| --- | --- | --- | --- | -| `main` | `main` | Production allowed, then reviewer approval | Protected `main` SHA | -| `main` | `patch/vX.Y.Z` | Production allowed, then reviewer approval | Protected matching patch SHA | -| `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 +| Workflow run ref | Source input | Environment result without admin bypass | Source check on merged `main` | +| ----------------------- | ----------------------------------------- | -------------------------------------------- | --------------------------------- | +| `main` | `main` | Production allowed, then reviewer approval | Protected `main` SHA | +| `main` | `patch/vX.Y.Z` | Production allowed, then reviewer approval | Protected matching patch SHA | +| `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`; 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,105 +317,43 @@ 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 [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; 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 -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 | -| --- | --- | --- | -| `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 section 7's bot acceptance. No human -approval was fabricated and no deployment, tag change, or release was performed. +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 | +| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| 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 @@ -407,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 @@ -428,7 +405,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..5e72982 100644 --- a/docs/setup/hosting.md +++ b/docs/setup/hosting.md @@ -8,10 +8,10 @@ launcher; application hosting uses `deploy/staging` and `deploy/production`. Each server runs **one host-local Caddy edge**, explicitly configured through the `EDGE_PROFILE` variable on its GitHub deployment environment: -| Layout | Staging environment's `EDGE_PROFILE` | Production environment's `EDGE_PROFILE` | -| --- | --- | --- | -| Both applications on one server (current layout) | `shared` | `shared` | -| Separate servers | `staging` | `production` | +| Layout | Staging environment's `EDGE_PROFILE` | Production environment's `EDGE_PROFILE` | +| ------------------------------------------------ | ------------------------------------ | --------------------------------------- | +| Both applications on one server (current layout) | `shared` | `shared` | +| Separate servers | `staging` | `production` | There is no default. Missing, unknown, or mismatched profiles fail closed. `shared` is an intentional configuration, not an inferred relationship between @@ -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. @@ -227,11 +252,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 +266,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 +296,31 @@ 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 -staging failure and rollback rehearsal passed on 2026-09-20; see the -[checklist evidence](../foundation-checklist.md#14-make-rollout-atomic-and-rollback-explicit). +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 @@ -280,3 +328,35 @@ 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 + +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 +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/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/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/apply-edge.sh b/scripts/apply-edge.sh index e1988e8..aab3ce4 100644 --- a/scripts/apply-edge.sh +++ b/scripts/apply-edge.sh @@ -15,7 +15,7 @@ if [[ "$allow_profile_change" != true && "$allow_profile_change" != false ]]; th echo "Expected allow-profile-change to be true or false." >&2 exit 1 fi -if (( $# > 3 )); then +if (($# > 3)); then echo "Usage: apply-edge.sh [allow-profile-change=false]" >&2 exit 1 fi @@ -95,8 +95,8 @@ docker run --rm --entrypoint caddy \ # Create only this profile's web roots as the deploying user, before Docker can # create missing bind-mount directories as root during first-time edge setup. case "$candidate_profile" in - shared) edge_environments=(staging production) ;; - staging|production) edge_environments=("$candidate_profile") ;; +shared) edge_environments=(staging production) ;; +staging | production) edge_environments=("$candidate_profile") ;; esac for environment in "${edge_environments[@]}"; do mkdir -p "${edge_dir}/../${environment}/web" 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-edge-health.sh b/scripts/check-edge-health.sh index f74ecac..5478f3b 100644 --- a/scripts/check-edge-health.sh +++ b/scripts/check-edge-health.sh @@ -5,8 +5,14 @@ set -euo pipefail # pre-cutover DNS cannot accidentally validate the old server instead. environment="${1:-}" base_url="${2:-}" -[[ "$environment" == staging || "$environment" == production ]] || { echo 'Unknown edge environment.' >&2; exit 1; } -[[ "$base_url" =~ ^https://([a-zA-Z0-9.-]+)/?$ ]] || { echo 'Expected an HTTPS origin without a port or path.' >&2; exit 1; } +[[ "$environment" == staging || "$environment" == production ]] || { + echo 'Unknown edge environment.' >&2 + exit 1 +} +[[ "$base_url" =~ ^https://([a-zA-Z0-9.-]+)/?$ ]] || { + echo 'Expected an HTTPS origin without a port or path.' >&2 + exit 1 +} hostname="${BASH_REMATCH[1]}" response="$(curl --fail --show-error --silent \ --noproxy '*' \ @@ -14,5 +20,8 @@ response="$(curl --fail --show-error --silent \ --connect-timeout 10 --max-time 30 --retry-max-time 180 \ --retry 5 --retry-delay 5 --retry-all-errors \ "${base_url%/}/health")" -[[ "$response" == "ok $environment" ]] || { echo "Unexpected ${environment} edge health response." >&2; exit 1; } +[[ "$response" == "ok $environment" ]] || { + echo "Unexpected ${environment} edge health response." >&2 + exit 1 +} echo "Verified ${environment} HTTPS edge on this host." diff --git a/scripts/check.ps1 b/scripts/check.ps1 new file mode 100644 index 0000000..0d037b2 --- /dev/null +++ b/scripts/check.ps1 @@ -0,0 +1,108 @@ +#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', 'content', 'external-links', '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." + } + } + + 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)) { + 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') + } + 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') + 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/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/deploy-app.sh b/scripts/deploy-app.sh index d2cfd19..d0c9dea 100644 --- a/scripts/deploy-app.sh +++ b/scripts/deploy-app.sh @@ -7,7 +7,10 @@ command_name="${1:-}" app_dir="${2:-}" release_id="${3:-}" -die() { echo "deploy-app: $*" >&2; exit 1; } +die() { + echo "deploy-app: $*" >&2 + exit 1 +} [[ "$command_name" == activate || "$command_name" == rollback || "$command_name" == finalize ]] || die 'expected activate, rollback or finalize' [[ -d "$app_dir" ]] || die 'application directory is missing' environment="$(basename "$app_dir")" @@ -28,18 +31,28 @@ 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" } point_to() { local name="$1" id="$2" temp temp="$app_dir/.${name}.$$" - printf 'releases/%s\n' "$id" > "$temp" + printf 'releases/%s\n' "$id" >"$temp" mv -f "$temp" "$app_dir/$name" } @@ -48,7 +61,7 @@ write_index() { if [[ "$id" == legacy-* ]]; then cp "$web_dir/releases/$id/index.html" "$temp" else - cat > "$temp" <"$temp" < OpenGameBuilder @@ -83,11 +96,49 @@ 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 +154,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' @@ -138,7 +191,7 @@ mkdir "$temp_release" "$temp_web" cp "$manifest" "$temp_release/release-manifest.txt" cp "$incoming/compose.yml" "$temp_release/compose.yml" printf 'API_IMAGE=%s\nSOURCE_SHA=%s\nTRUSTED_PROXY_NETWORKS=%s\n' \ - "$api_image" "$source_sha" "$trusted_proxy_networks" > "$temp_release/.env" + "$api_image" "$source_sha" "$trusted_proxy_networks" >"$temp_release/.env" tar -xzf "$incoming/web-release.tar.gz" -C "$temp_web" [[ -f "$temp_web/index.html" ]] || die 'web archive lacks index.html' grep -Fq " "$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/scripts/doctor.ps1 b/scripts/doctor.ps1 new file mode 100644 index 0000000..c67c0cd --- /dev/null +++ b/scripts/doctor.ps1 @@ -0,0 +1,185 @@ +[CmdletBinding()] +param( + [ValidateSet('format', 'content', '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 @('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 ' +} +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 } + +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/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/post-release.sh b/scripts/post-release.sh index 5fc3f84..62ab48e 100644 --- a/scripts/post-release.sh +++ b/scripts/post-release.sh @@ -20,7 +20,7 @@ set -euo pipefail : "${TAG:?TAG is required}" assert_semver "${VERSION}" -read -r major minor _ <<< "$(semver_parts "${VERSION}")" +read -r major minor _ <<<"$(semver_parts "${VERSION}")" git fetch --force --tags origin >/dev/null git fetch origin main >/dev/null @@ -28,97 +28,99 @@ git fetch origin main >/dev/null configure_release_bot_git case "${KIND}" in - standard) - : "${NEXT_MAIN_VERSION:?NEXT_MAIN_VERSION is required for standard releases}" - assert_semver "${NEXT_MAIN_VERSION}" - - bump_branch="chore/bump-version-to-${NEXT_MAIN_VERSION}" - - if remote_ref_exists "refs/heads/${bump_branch}"; then - echo "Bump branch '${bump_branch}' already exists." - else - git switch --detach origin/main - git switch -c "${bump_branch}" - write_version "${NEXT_MAIN_VERSION}" - - if [ -z "$(git status --porcelain)" ]; then - echo "main is already at ${NEXT_MAIN_VERSION}; nothing to do." - exit 0 - fi - - git add Directory.Build.props - git commit -m "chore: bump version to ${NEXT_MAIN_VERSION}" - git push origin "HEAD:refs/heads/${bump_branch}" - fi - - existing_pr="$(gh pr list --base main --head "${bump_branch}" --state open --json number --jq '.[0].number // empty')" - if [ -z "${existing_pr}" ]; then - body=$(cat </dev/null - - if remote_ref_exists "refs/heads/${merge_branch}"; then - echo "Merge-back branch '${merge_branch}' already exists." - else - git switch --detach origin/main - main_version="$(read_version)" - - # Target main version is whichever of (main, X.(Y+1).0) is greater. - min_main="${major}.$((minor + 1)).0" - if [ "$(semver_compare "${main_version}" "${min_main}")" = "-1" ]; then - target_main_version="${min_main}" - else - target_main_version="${main_version}" - fi - - echo "Main version before merge-back: ${main_version}" - echo "Target main version after merge-back: ${target_main_version}" - - git switch -c "${merge_branch}" - - # A props conflict can include non-version changes; leave it for manual resolution. - if ! git merge --no-ff --no-commit "origin/${patch_branch}"; then - conflicts="$(git diff --name-only --diff-filter=U)" - git status --short - echo "Merge-back has unresolved conflicts (${conflicts}). Resolve manually; no branch was pushed." >&2 - exit 1 - fi - - write_version "${target_main_version}" - git add -A - - if [ -z "$(git status --porcelain)" ]; then - echo "Merge-back produced no changes. No PR needed." - git merge --abort >/dev/null 2>&1 || true - exit 0 - fi - - git commit -m "chore: merge ${TAG} into main" - git push origin "HEAD:refs/heads/${merge_branch}" - fi - - existing_pr="$(gh pr list --base main --head "${merge_branch}" --state open --json number --jq '.[0].number // empty')" - if [ -z "${existing_pr}" ]; then - body=$(cat </dev/null + + if remote_ref_exists "refs/heads/${merge_branch}"; then + echo "Merge-back branch '${merge_branch}' already exists." + else + git switch --detach origin/main + main_version="$(read_version)" + + # Target main version is whichever of (main, X.(Y+1).0) is greater. + min_main="${major}.$((minor + 1)).0" + if [ "$(semver_compare "${main_version}" "${min_main}")" = "-1" ]; then + target_main_version="${min_main}" + else + target_main_version="${main_version}" + fi + + echo "Main version before merge-back: ${main_version}" + echo "Target main version after merge-back: ${target_main_version}" + + git switch -c "${merge_branch}" + + # A props conflict can include non-version changes; leave it for manual resolution. + if ! git merge --no-ff --no-commit "origin/${patch_branch}"; then + conflicts="$(git diff --name-only --diff-filter=U)" + git status --short + echo "Merge-back has unresolved conflicts (${conflicts}). Resolve manually; no branch was pushed." >&2 + exit 1 + fi + + write_version "${target_main_version}" + git add -A + + if [ -z "$(git status --porcelain)" ]; then + echo "Merge-back produced no changes. No PR needed." + git merge --abort >/dev/null 2>&1 || true + exit 0 + fi + + git commit -m "chore: merge ${TAG} into main" + git push origin "HEAD:refs/heads/${merge_branch}" + fi + + existing_pr="$(gh pr list --base main --head "${merge_branch}" --state open --json number --jq '.[0].number // empty')" + if [ -z "${existing_pr}" ]; then + body=$( + cat <&2 - exit 1 - ;; + ) + gh pr create \ + --base main \ + --head "${merge_branch}" \ + --title "chore: merge ${TAG} into main" \ + --body "${body}" + else + echo "Merge-back PR already exists: #${existing_pr}" + fi + ;; + +*) + echo "Unknown KIND: '${KIND}'. Expected 'standard' or 'patch'." >&2 + exit 1 + ;; esac diff --git a/scripts/prepare-patch.sh b/scripts/prepare-patch.sh index 8c7963d..b909966 100644 --- a/scripts/prepare-patch.sh +++ b/scripts/prepare-patch.sh @@ -18,12 +18,12 @@ git fetch origin main >/dev/null base_tag="$(latest_release_tag)" if [ -z "${base_tag}" ]; then - echo "No stable GitHub Release was found. Publish a standard release first." >&2 - exit 1 + echo "No stable GitHub Release was found. Publish a standard release first." >&2 + exit 1 fi base_version="${base_tag#v}" -read -r major minor patch <<< "$(semver_parts "${base_version}")" +read -r major minor patch <<<"$(semver_parts "${base_version}")" next_patch=$((patch + 1)) next_version="${major}.${minor}.${next_patch}" next_tag="v${next_version}" @@ -31,61 +31,62 @@ patch_branch="patch/${next_tag}" prepare_branch="chore/prepare-${next_tag}" if remote_ref_exists "refs/tags/${next_tag}"; then - echo "Tag '${next_tag}' already exists." >&2 - exit 1 + echo "Tag '${next_tag}' already exists." >&2 + exit 1 fi configure_release_bot_git # Ensure patch branch exists at the base tag. if remote_ref_exists "refs/heads/${patch_branch}"; then - echo "Patch branch '${patch_branch}' already exists; checking its state." - git fetch origin "+refs/heads/${patch_branch}:refs/remotes/origin/${patch_branch}" >/dev/null - git switch --detach "origin/${patch_branch}" - - branch_version="$(read_version)" - if [ "${branch_version}" != "${base_version}" ] && [ "${branch_version}" != "${next_version}" ]; then - echo "Patch branch '${patch_branch}' has VersionPrefix '${branch_version}', expected '${base_version}' (unprepared) or '${next_version}' (already prepared)." >&2 - echo "Fix the branch before preparing another patch." >&2 - exit 1 - fi - - # The base tag must be an ancestor of the patch branch. - if ! git merge-base --is-ancestor "${base_tag}" "origin/${patch_branch}"; then - echo "Base tag '${base_tag}' is not an ancestor of '${patch_branch}'. Refusing to continue." >&2 - exit 1 - fi + echo "Patch branch '${patch_branch}' already exists; checking its state." + git fetch origin "+refs/heads/${patch_branch}:refs/remotes/origin/${patch_branch}" >/dev/null + git switch --detach "origin/${patch_branch}" + + branch_version="$(read_version)" + if [ "${branch_version}" != "${base_version}" ] && [ "${branch_version}" != "${next_version}" ]; then + echo "Patch branch '${patch_branch}' has VersionPrefix '${branch_version}', expected '${base_version}' (unprepared) or '${next_version}' (already prepared)." >&2 + echo "Fix the branch before preparing another patch." >&2 + exit 1 + fi + + # The base tag must be an ancestor of the patch branch. + if ! git merge-base --is-ancestor "${base_tag}" "origin/${patch_branch}"; then + echo "Base tag '${base_tag}' is not an ancestor of '${patch_branch}'. Refusing to continue." >&2 + exit 1 + fi else - echo "Creating patch branch '${patch_branch}' from '${base_tag}'." - git branch "${patch_branch}" "${base_tag}" - git push origin "refs/heads/${patch_branch}" + echo "Creating patch branch '${patch_branch}' from '${base_tag}'." + git branch "${patch_branch}" "${base_tag}" + git push origin "refs/heads/${patch_branch}" fi # Ensure prepare branch exists with version bump. if remote_ref_exists "refs/heads/${prepare_branch}"; then - echo "Prepare branch '${prepare_branch}' already exists." + echo "Prepare branch '${prepare_branch}' already exists." else - git fetch origin "+refs/heads/${patch_branch}:refs/remotes/origin/${patch_branch}" >/dev/null - git switch --detach "origin/${patch_branch}" - git switch -c "${prepare_branch}" + git fetch origin "+refs/heads/${patch_branch}:refs/remotes/origin/${patch_branch}" >/dev/null + git switch --detach "origin/${patch_branch}" + git switch -c "${prepare_branch}" - write_version "${next_version}" + write_version "${next_version}" - if [ -z "$(git status --porcelain)" ]; then - echo "No version change was needed; ${patch_branch} is already at ${next_version}." >&2 - exit 1 - fi + if [ -z "$(git status --porcelain)" ]; then + echo "No version change was needed; ${patch_branch} is already at ${next_version}." >&2 + exit 1 + fi - git add Directory.Build.props - git commit -m "chore: prepare ${next_tag}" - git push origin "HEAD:refs/heads/${prepare_branch}" + git add Directory.Build.props + git commit -m "chore: prepare ${next_tag}" + git push origin "HEAD:refs/heads/${prepare_branch}" fi # Ensure PR exists. existing_pr="$(gh pr list --base "${patch_branch}" --head "${prepare_branch}" --state open --json number --jq '.[0].number // empty')" if [ -z "${existing_pr}" ]; then - body=$(cat <&2 - exit 1 - fi + local version="$1" + if [[ ! "$version" =~ $SEMVER_REGEX ]]; then + echo "Version must be plain SemVer X.Y.Z. Received: '$version'." >&2 + exit 1 + fi } # Echo the X, Y, Z components of a SemVer separated by spaces. semver_parts() { - local version="$1" - assert_semver "$version" - echo "${version//./ }" + local version="$1" + assert_semver "$version" + echo "${version//./ }" } # Compare two plain SemVers. Echoes -1, 0, or 1 (left vs right). semver_compare() { - local left="$1" - local right="$2" - local l_parts r_parts - read -r -a l_parts <<< "$(semver_parts "$left")" - read -r -a r_parts <<< "$(semver_parts "$right")" - for i in 0 1 2; do - if (( l_parts[i] < r_parts[i] )); then echo -1; return; fi - if (( l_parts[i] > r_parts[i] )); then echo 1; return; fi - done - echo 0 + local left="$1" + local right="$2" + local l_parts r_parts + read -r -a l_parts <<<"$(semver_parts "$left")" + read -r -a r_parts <<<"$(semver_parts "$right")" + for i in 0 1 2; do + if ((l_parts[i] < r_parts[i])); then + echo -1 + return + fi + if ((l_parts[i] > r_parts[i])); then + echo 1 + return + fi + done + echo 0 } # Read the value from Directory.Build.props. read_version() { - local props - props="$(props_path)" - local count - count="$(grep -c '' "$props" || true)" - if [ "$count" != "1" ]; then - echo "Expected exactly one in $props, found $count." >&2 - exit 1 - fi - local version - version="$(sed -n 's|.*\([^<]*\).*|\1|p' "$props" | head -n 1 | tr -d '[:space:]')" - assert_semver "$version" - echo "$version" + local props + props="$(props_path)" + local count + count="$(grep -c '' "$props" || true)" + if [ "$count" != "1" ]; then + echo "Expected exactly one in $props, found $count." >&2 + exit 1 + fi + local version + version="$(sed -n 's|.*\([^<]*\).*|\1|p' "$props" | head -n 1 | tr -d '[:space:]')" + assert_semver "$version" + echo "$version" } # Replace the value in Directory.Build.props. write_version() { - local new_version="$1" - assert_semver "$new_version" - local props - props="$(props_path)" - # Portable sed (works on GNU sed and BSD sed when the pattern has no slashes other than the delimiter). - sed -i.bak "s|[^<]*|${new_version}|" "$props" - rm -f "${props}.bak" + local new_version="$1" + assert_semver "$new_version" + local props + props="$(props_path)" + # Portable sed (works on GNU sed and BSD sed when the pattern has no slashes other than the delimiter). + sed -i.bak "s|[^<]*|${new_version}|" "$props" + rm -f "${props}.bak" } # Echo the latest stable (non-draft, non-prerelease) release tag, or empty if none. # Requires GH_TOKEN to be set for `gh`. latest_release_tag() { - local line_filter="${1:-}" # Optional X.Y filter, e.g. "1.9". - local excluded_tag="${2:-}" # Optional tag to omit when finding a rerun's predecessor. - local tags - if ! tags="$(gh release list --limit 200 --json 'tagName,isDraft,isPrerelease' \ - --jq '.[] | select(.isDraft == false and .isPrerelease == false) | .tagName | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))')"; then - echo "Failed to list GitHub Releases. Is GH_TOKEN set with the right permissions?" >&2 - exit 1 - fi - if [ -n "$line_filter" ]; then - if [[ ! "$line_filter" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then - echo "Line filter must look like X.Y. Received: '$line_filter'." >&2 - exit 1 - fi - tags="$(echo "$tags" | grep -E "^v${line_filter//./\\.}\\.[0-9]+$" || true)" + local line_filter="${1:-}" # Optional X.Y filter, e.g. "1.9". + local excluded_tag="${2:-}" # Optional tag to omit when finding a rerun's predecessor. + local tags + if ! tags="$(gh release list --limit 200 --json 'tagName,isDraft,isPrerelease' \ + --jq '.[] | select(.isDraft == false and .isPrerelease == false) | .tagName | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))')"; then + echo "Failed to list GitHub Releases. Is GH_TOKEN set with the right permissions?" >&2 + exit 1 + fi + if [ -n "$line_filter" ]; then + if [[ ! "$line_filter" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "Line filter must look like X.Y. Received: '$line_filter'." >&2 + exit 1 fi - if [ -n "$excluded_tag" ]; then - tags="$(echo "$tags" | grep -Fxv "$excluded_tag" || true)" - fi - # Sort tags by version and take the highest. - echo "$tags" | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1 | sed 's/^/v/' | sed 's/^v$//' + tags="$(echo "$tags" | grep -E "^v${line_filter//./\\.}\\.[0-9]+$" || true)" + fi + if [ -n "$excluded_tag" ]; then + tags="$(echo "$tags" | grep -Fxv "$excluded_tag" || true)" + fi + # Sort tags by version and take the highest. + echo "$tags" | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1 | sed 's/^/v/' | sed 's/^v$//' } # Return success only when a remote ref exists. Do not treat a failed lookup as absence. remote_ref_exists() { - local ref="$1" - local status - if git ls-remote --exit-code origin "$ref" >/dev/null; then - return 0 - else - status=$? - fi - if [ "$status" = "2" ]; then - return 1 - fi - echo "Failed to inspect remote ref '${ref}' (git exit ${status})." >&2 - exit "$status" + local ref="$1" + local status + if git ls-remote --exit-code origin "$ref" >/dev/null; then + return 0 + else + status=$? + fi + if [ "$status" = "2" ]; then + return 1 + fi + echo "Failed to inspect remote ref '${ref}' (git exit ${status})." >&2 + exit "$status" } # Write a key=value pair to GITHUB_OUTPUT (or stdout when not in CI). gh_output() { - local name="$1" - local value="$2" - if [ -n "${GITHUB_OUTPUT:-}" ]; then - echo "${name}=${value}" >> "$GITHUB_OUTPUT" - else - echo "${name}=${value}" - fi + local name="$1" + local value="$2" + if [ -n "${GITHUB_OUTPUT:-}" ]; then + echo "${name}=${value}" >>"$GITHUB_OUTPUT" + else + echo "${name}=${value}" + fi } # Configure git for release-bot commits. configure_release_bot_git() { - git config user.name "OpenGameBuilder Release Bot" - git config user.email "release-bot@users.noreply.github.com" + git config user.name "OpenGameBuilder Release Bot" + git config user.email "release-bot@users.noreply.github.com" } diff --git a/scripts/render-edge.sh b/scripts/render-edge.sh index 0d1a2e3..89eac86 100644 --- a/scripts/render-edge.sh +++ b/scripts/render-edge.sh @@ -10,9 +10,12 @@ fi profile="$1" case "$profile" in - shared) environments=(production staging) ;; - staging|production) environments=("$profile") ;; - *) echo "Unknown edge profile '$profile'; expected shared, staging, or production." >&2; exit 1 ;; +shared) environments=(production staging) ;; +staging | production) environments=("$profile") ;; +*) + echo "Unknown edge profile '$profile'; expected shared, staging, or production." >&2 + exit 1 + ;; esac repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -33,12 +36,12 @@ done # Retain relative mounts so the result can be transferred to a different host # and applied with that host's permanent edge directory as its project directory. -printf '# ogb-edge-profile: %s\n' "$profile" > "$temporary_dir/compose.yml" -"${compose[@]}" config --no-path-resolution >> "$temporary_dir/compose.yml" -printf '# ogb-edge-profile: %s\n' "$profile" > "$temporary_dir/Caddyfile" +printf '# ogb-edge-profile: %s\n' "$profile" >"$temporary_dir/compose.yml" +"${compose[@]}" config --no-path-resolution >>"$temporary_dir/compose.yml" +printf '# ogb-edge-profile: %s\n' "$profile" >"$temporary_dir/Caddyfile" for environment in "${environments[@]}"; do - printf '\n' >> "$temporary_dir/Caddyfile" - cat "$source_dir/Caddyfile.$environment" >> "$temporary_dir/Caddyfile" + printf '\n' >>"$temporary_dir/Caddyfile" + cat "$source_dir/Caddyfile.$environment" >>"$temporary_dir/Caddyfile" done mv -- "$temporary_dir/compose.yml" "$candidate_dir/compose.yml" diff --git a/scripts/resolve-edge-profile.sh b/scripts/resolve-edge-profile.sh index fc526e5..6d83924 100644 --- a/scripts/resolve-edge-profile.sh +++ b/scripts/resolve-edge-profile.sh @@ -10,11 +10,11 @@ fi environment="$1" profile="$2" case "$environment:$profile" in - staging:staging|production:production|staging:shared|production:shared) ;; - *) - echo "EDGE_PROFILE must be explicitly set to '$environment' or 'shared' for a staging or production deployment environment." >&2 - exit 1 - ;; +staging:staging | production:production | staging:shared | production:shared) ;; +*) + echo "EDGE_PROFILE must be explicitly set to '$environment' or 'shared' for a staging or production deployment environment." >&2 + exit 1 + ;; esac check_production=false 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/scripts/validate-release.sh b/scripts/validate-release.sh index 30024f9..c56a5fc 100644 --- a/scripts/validate-release.sh +++ b/scripts/validate-release.sh @@ -24,7 +24,7 @@ source_ref="${SOURCE_REF#refs/heads/}" source_ref="${source_ref#origin/}" version="$(read_version)" -read -r major minor patch <<< "$(semver_parts "$version")" +read -r major minor patch <<<"$(semver_parts "$version")" tag="v${version}" source_sha="$(git rev-parse HEAD)" @@ -33,13 +33,13 @@ git fetch --force --tags --quiet origin # If a tag with this version already exists, it must point at the same commit (idempotent re-runs). tag_exists=false if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then - tag_exists=true - existing_sha="$(git rev-list -n 1 "${tag}")" - if [ "${existing_sha}" != "${source_sha}" ]; then - echo "Tag ${tag} already exists at ${existing_sha}, but ${source_ref} is at ${source_sha}." >&2 - exit 1 - fi - echo "Tag ${tag} already exists at the expected SHA. Continuing idempotently." + tag_exists=true + existing_sha="$(git rev-list -n 1 "${tag}")" + if [ "${existing_sha}" != "${source_sha}" ]; then + echo "Tag ${tag} already exists at ${existing_sha}, but ${source_ref} is at ${source_sha}." >&2 + exit 1 + fi + echo "Tag ${tag} already exists at the expected SHA. Continuing idempotently." fi latest_tag="$(latest_release_tag)" @@ -49,69 +49,69 @@ next_main_version="" previous_tag="" if [ "${patch}" = "0" ]; then - kind="standard" - - if [ "${source_ref}" != "main" ]; then - echo "Standard releases must be published from main. Received '${source_ref}'." >&2 - exit 1 - fi - - if [ "${latest_tag}" = "${tag}" ]; then - if [ "${tag_exists}" != true ]; then - echo "GitHub Release ${tag} exists, but its tag is missing." >&2 - exit 1 - fi - previous_tag="$(latest_release_tag "" "${tag}")" - elif [ -n "${latest_tag}" ]; then - if [ "$(semver_compare "${version}" "${latest_version}")" != "1" ]; then - echo "Standard release '${version}' must be greater than latest stable release '${latest_version}'." >&2 - exit 1 - fi - previous_tag="${latest_tag}" - fi + kind="standard" - next_main_version="${major}.$((minor + 1)).0" -else - kind="patch" - - expected_branch="patch/v${version}" - if [ "${source_ref}" != "${expected_branch}" ]; then - echo "Patch releases must be published from ${expected_branch}. Received '${source_ref}'." >&2 - exit 1 - fi + if [ "${source_ref}" != "main" ]; then + echo "Standard releases must be published from main. Received '${source_ref}'." >&2 + exit 1 + fi - line="${major}.${minor}" - line_latest_tag="$(latest_release_tag "${line}")" - if [ "${line_latest_tag}" = "${tag}" ]; then - if [ "${tag_exists}" != true ]; then - echo "GitHub Release ${tag} exists, but its tag is missing." >&2 - exit 1 - fi - previous_tag="$(latest_release_tag "${line}" "${tag}")" - else - previous_tag="${line_latest_tag}" + if [ "${latest_tag}" = "${tag}" ]; then + if [ "${tag_exists}" != true ]; then + echo "GitHub Release ${tag} exists, but its tag is missing." >&2 + exit 1 fi - if [ -z "${previous_tag}" ]; then - echo "Could not find a prior stable release in line ${line}. Publish a standard release first." >&2 - exit 1 + previous_tag="$(latest_release_tag "" "${tag}")" + elif [ -n "${latest_tag}" ]; then + if [ "$(semver_compare "${version}" "${latest_version}")" != "1" ]; then + echo "Standard release '${version}' must be greater than latest stable release '${latest_version}'." >&2 + exit 1 fi + previous_tag="${latest_tag}" + fi - line_latest_version="${previous_tag#v}" - read -r _ _ line_latest_patch <<< "$(semver_parts "${line_latest_version}")" - expected_patch=$((line_latest_patch + 1)) - if [ "${patch}" != "${expected_patch}" ]; then - echo "Patch release '${version}' must be the next patch after '${line_latest_version}' (expected patch ${expected_patch})." >&2 - exit 1 + next_main_version="${major}.$((minor + 1)).0" +else + kind="patch" + + expected_branch="patch/v${version}" + if [ "${source_ref}" != "${expected_branch}" ]; then + echo "Patch releases must be published from ${expected_branch}. Received '${source_ref}'." >&2 + exit 1 + fi + + line="${major}.${minor}" + line_latest_tag="$(latest_release_tag "${line}")" + if [ "${line_latest_tag}" = "${tag}" ]; then + if [ "${tag_exists}" != true ]; then + echo "GitHub Release ${tag} exists, but its tag is missing." >&2 + exit 1 fi - - # Only patch the latest deployed release line. - if [ -n "${latest_tag}" ] && [ "${latest_tag}" != "${tag}" ]; then - read -r latest_major latest_minor _ <<< "$(semver_parts "${latest_version}")" - if [ "${latest_major}.${latest_minor}" != "${line}" ]; then - echo "Patch releases must patch the latest deployed line. Latest stable is '${latest_tag}', this patch is for '${line}'." >&2 - exit 1 - fi + previous_tag="$(latest_release_tag "${line}" "${tag}")" + else + previous_tag="${line_latest_tag}" + fi + if [ -z "${previous_tag}" ]; then + echo "Could not find a prior stable release in line ${line}. Publish a standard release first." >&2 + exit 1 + fi + + line_latest_version="${previous_tag#v}" + read -r _ _ line_latest_patch <<<"$(semver_parts "${line_latest_version}")" + expected_patch=$((line_latest_patch + 1)) + if [ "${patch}" != "${expected_patch}" ]; then + echo "Patch release '${version}' must be the next patch after '${line_latest_version}' (expected patch ${expected_patch})." >&2 + exit 1 + fi + + # Only patch the latest deployed release line. + if [ -n "${latest_tag}" ] && [ "${latest_tag}" != "${tag}" ]; then + read -r latest_major latest_minor _ <<<"$(semver_parts "${latest_version}")" + if [ "${latest_major}.${latest_minor}" != "${line}" ]; then + echo "Patch releases must patch the latest deployed line. Latest stable is '${latest_tag}', this patch is for '${line}'." >&2 + exit 1 fi + fi fi 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/verify-edge-profile.sh b/scripts/verify-edge-profile.sh index 3c49e2d..1af3abb 100644 --- a/scripts/verify-edge-profile.sh +++ b/scripts/verify-edge-profile.sh @@ -8,7 +8,7 @@ if [[ "$expected_profile" != shared && "$expected_profile" != staging && "$expec echo "Expected edge profile shared staging or production." >&2 exit 1 fi -if (( $# != 2 )); then +if (($# != 2)); then echo "Usage: verify-edge-profile.sh " >&2 exit 1 fi diff --git a/scripts/verify-web-publish.sh b/scripts/verify-web-publish.sh index 104b1d7..ff621ea 100644 --- a/scripts/verify-web-publish.sh +++ b/scripts/verify-web-publish.sh @@ -26,7 +26,7 @@ fi boot_scripts=("$web_root"/_framework/dotnet.*.js) if [ "${#boot_scripts[@]}" -eq 0 ] || - ! grep -q '"applicationEnvironment": "Production"' "${boot_scripts[@]}"; then + ! grep -q '"applicationEnvironment": "Production"' "${boot_scripts[@]}"; then echo 'Published WASM bootstrap does not select Production.' >&2 exit 1 fi 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/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/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.Api/Properties/launchSettings.json b/src/OpenGameBuilder.Api/Properties/launchSettings.json index 36ee2ae..69f15ef 100644 --- a/src/OpenGameBuilder.Api/Properties/launchSettings.json +++ b/src/OpenGameBuilder.Api/Properties/launchSettings.json @@ -12,4 +12,4 @@ } }, "$schema": "https://json.schemastore.org/launchsettings.json" -} \ No newline at end of file +} diff --git a/src/OpenGameBuilder.Api/appsettings.Development.json b/src/OpenGameBuilder.Api/appsettings.Development.json index 8c086b0..006504d 100644 --- a/src/OpenGameBuilder.Api/appsettings.Development.json +++ b/src/OpenGameBuilder.Api/appsettings.Development.json @@ -6,9 +6,6 @@ } }, "Cors": { - "AllowedOrigins": [ - "http://localhost:5001", - "https://localhost:7001" - ] + "AllowedOrigins": ["http://localhost:5001", "https://localhost:7001"] } } 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/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.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 diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index d0c2f47..ce2f552 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,6 +2,7 @@ + true true false diff --git a/tests/OpenGameBuilder.Api.Client.Tests/packages.lock.json b/tests/OpenGameBuilder.Api.Client.Tests/packages.lock.json new file mode 100644 index 0000000..324e89b --- /dev/null +++ b/tests/OpenGameBuilder.Api.Client.Tests/packages.lock.json @@ -0,0 +1,302 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.10.1, )", + "resolved": "18.10.1", + "contentHash": "SxCFvJE/2ltUQgCgv6+Ixagohw4sZc9hM7Wid2foaNM7paBscktoHLoRDz91fW8wbHG87Rg9Ku59/8PXumqUrA==", + "dependencies": { + "Microsoft.CodeCoverage": "18.10.1", + "Microsoft.TestPlatform.TestHost": "18.10.1" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "kzLFyBYUnoidpGOXvpNOfIo8C92gVgVR6X8ncHwhcrDYugiOut5rrhDkr0L3cWHd7J2pKXf6LgaeXoBBDlx1ag==" + }, + "xunit.v3": { + "type": "Direct", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "KpDYf6jFjYfOchuRJOAXPOpwN03hLE1AF4kWzCUSnhr8JZMiGH4BT1YidCcm6BE1H4E90GyJzuKoCwXbg9PXmA==", + "dependencies": { + "xunit.v3.mtp-v2": "[4.0.1]" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.10.1", + "contentHash": "tV7tCyp/t+DuwQugLkCeUN7NBFFB4fEGRPr0gkMQ2nQgAvQQjmGlZZi1dsLzbyoe/A0mbnKybmkIh8qhT9p4Zw==" + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "8xaGcvS/qZ1otoxPQCEJkNva389CVL/plNcvIETZhQTETYdRkYDPEYhUMoAGONo4FU45ufdfE0j29AfWVVj0wA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dAgIf1TOr8KLs+aBRIbXUZBjHoSH4rDG8+XkX/Q6AZwkQdMA0+yPDKTHsieeXdZDfpOpZVHzOnuAb5Z2nX3KsA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "lXyK2O5GoYvfxW8eCFcD16JFbcoSTM1sJkAM0UHS1jZyl9NYMW64Tqm6OQFT0IDBjZi+xHt95/Zg+nxZhGFhZg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "amx0O/R4dReqKlC3T740+CHpX8D8tAIV0f5kAPfFi4XK9jYo9BSepGt9/rWfvvCoSMaZ1+XvnqPp/Vv2fmTdAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.12", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.12" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "fMp91qlRlMOuHfMyp7LuAnOS+MOeWTF1obJmyFcs4nZCXu6pr+QB1AbFJcl2qpBltBrqF/Y7+Rn9IuNcSuctig==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "6I46fTPfgYkrjRYfRXbho9WOvOelTnNjWuZws/hzGHDASH1LEJeA4VKK9k3wJvido8o7jJSB5WkMTonX7HM1bA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.10.1", + "contentHash": "o+PuLDRVp1iwiVAPBADZItXgm9Ck23XtPhE4kZtZVi0uYnMiiz1Dx9Cjh75kJ+ijfqnORL3VOTOaQVKi0f/ixg==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.10.1", + "contentHash": "KYz2omqkoz9E2mWa4fdgT+Ycie1wi5968EmyJlusjlp87RxxtA2FvDm8tcBEYIDzLnkWGnsKjE4d13Z7/rxkQA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.10.1" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "2.4.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0", + "Microsoft.Testing.Platform": "2.4.0", + "Microsoft.Testing.Platform.MSBuild": "2.4.0", + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.inproc.console": "[4.0.1]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "dependencies": { + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "dependencies": { + "xunit.analyzers": "2.1.0", + "xunit.v3.assert": "[4.0.1]", + "xunit.v3.core.mtp-v2": "[4.0.1]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "dependencies": { + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.common": "[4.0.1]" + } + }, + "opengamebuilder.api.client": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Configuration": "[10.0.12, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.12, )", + "Microsoft.Extensions.Http": "[10.0.12, )", + "Microsoft.Extensions.Options": "[10.0.12, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.12, )", + "OpenGameBuilder.Api.Contracts": "[0.11.0, )" + } + }, + "opengamebuilder.api.contracts": { + "type": "Project" + }, + "Microsoft.Extensions.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "e3IPP32CRNL031VZJAUTlCTG0YN7WFh4mN3fsSHTDQCJB+3+f0jGycv4fXk3rrftaY3B85XrQaj7sRthrOsavg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw==" + }, + "Microsoft.Extensions.Http": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "x/fnoUGgmIUjSgLR1p6Mpi/vXc1Xe0xDPJnggPp23I6vwM/kY4nxyOjo8i6G5KPVw+esJM+rJsqnHbdjasvCBw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Diagnostics": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Options": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rqpu4qj5WE9x1IHGXSIgHBKi7IUlQaHyp4aXCYIanG2OghlUMFZpZTgExaXwcvmLAJHsxKQWMPpc7D2WIbCVtA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Binder": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + } + } + } +} \ No newline at end of file diff --git a/tests/OpenGameBuilder.Api.Tests/packages.lock.json b/tests/OpenGameBuilder.Api.Tests/packages.lock.json new file mode 100644 index 0000000..6b93d49 --- /dev/null +++ b/tests/OpenGameBuilder.Api.Tests/packages.lock.json @@ -0,0 +1,774 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.AspNetCore.Mvc.Testing": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "OtjgnL8RRLQYaiuS3KzbBbyQ6am1ZrX3dvBdiuXghxL+LQinmT2vMumytveYPbnXLknZSk/aEDKENI6uf7NoNg==", + "dependencies": { + "Microsoft.AspNetCore.TestHost": "10.0.12", + "Microsoft.Extensions.DependencyModel": "10.0.12", + "Microsoft.Extensions.Hosting": "10.0.12" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.10.1, )", + "resolved": "18.10.1", + "contentHash": "SxCFvJE/2ltUQgCgv6+Ixagohw4sZc9hM7Wid2foaNM7paBscktoHLoRDz91fW8wbHG87Rg9Ku59/8PXumqUrA==", + "dependencies": { + "Microsoft.CodeCoverage": "18.10.1", + "Microsoft.TestPlatform.TestHost": "18.10.1" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "kzLFyBYUnoidpGOXvpNOfIo8C92gVgVR6X8ncHwhcrDYugiOut5rrhDkr0L3cWHd7J2pKXf6LgaeXoBBDlx1ag==" + }, + "xunit.v3": { + "type": "Direct", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "KpDYf6jFjYfOchuRJOAXPOpwN03hLE1AF4kWzCUSnhr8JZMiGH4BT1YidCcm6BE1H4E90GyJzuKoCwXbg9PXmA==", + "dependencies": { + "xunit.v3.mtp-v2": "[4.0.1]" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.TestHost": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "bTHgbcjSrLxlqSasYRDU9BcJ3hDGoqIjtDY0soLNrndOWi3kxtOyddwUMaNZgtoI2Uf6y7ZQs2EuVj0c20B7mA==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.10.1", + "contentHash": "tV7tCyp/t+DuwQugLkCeUN7NBFFB4fEGRPr0gkMQ2nQgAvQQjmGlZZi1dsLzbyoe/A0mbnKybmkIh8qhT9p4Zw==" + }, + "Microsoft.Extensions.AmbientMetadata.Application": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "Sa4xD2ab5yNRJYt4piS05C9b5Gpd1G3TZpvVJFgHyqLYdRThUNLcLHpREI4yjkmElCjHXoPcF+SHUim9cZ8cvQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.12", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.12" + } + }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "Sh3ujDRJpCMNY4x+43ljCROt4qBOoWfsL8l816RPYZS1S+Tw277r5002rbB15Sh6hFcCT8mGYBO7NtpfSYrQ0Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.ObjectPool": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "8xaGcvS/qZ1otoxPQCEJkNva389CVL/plNcvIETZhQTETYdRkYDPEYhUMoAGONo4FU45ufdfE0j29AfWVVj0wA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dAgIf1TOr8KLs+aBRIbXUZBjHoSH4rDG8+XkX/Q6AZwkQdMA0+yPDKTHsieeXdZDfpOpZVHzOnuAb5Z2nX3KsA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "hWWJzJpmg161rYJ2H6vxqCuFfh6zL2Tp9emOrfu1M4gsmW5BOlW8GldthyFwm2L+beW+0axmkMKRw3HyO1qceQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "OIFhLxKAdvSrZuecX177ZkN4AcAB6OLlbS8y0MQYL68512aMOZI8cJLb8OeuYe2X+gu5uMqbbCxhzAmSp+Q2ow==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "PNPS7yYH9U2M0dExn0tMkQFVtE53xTlCs/dgTfsWTTFyCF6qwGT3VrTgm2nnnkSRlSmvM9/J+n9xCpyzd9xTpQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Physical": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "H4u2ZeIhjmRQvbHj7S7U4MK1kXb0khx3UtOWcir32VHczdFyIVF0lvHew/VVHmCF7Uk0QseAUaO7HyFKOCSiKA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "2OtyTIt0/tkChr3inq9Y0c34xogxXW//68gvPzIWgKwyVR4x82RSGVSZfV+ZgG13zrKBzqjaI23wpUCfKpY2yQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Json": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Physical": "10.0.12" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "lXyK2O5GoYvfxW8eCFcD16JFbcoSTM1sJkAM0UHS1jZyl9NYMW64Tqm6OQFT0IDBjZi+xHt95/Zg+nxZhGFhZg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.DependencyInjection.AutoActivation": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "AJ2lFnzzJkN2ZRvfPBKQ5k3EJFkhmsm1tEhWzK58KsxU+WK3GVOh/5WHUhQ56TWjnxGZCGgr0+U1QUuJ4rJVuA==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "rDPQVTxh/zMTDF7wHlRqL/jjZbwjAP6315ydBxj51pZW7qkAwGwjoSiMutxYI91xmOE3ZXjAGHbYRqzyJV7Urw==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "amx0O/R4dReqKlC3T740+CHpX8D8tAIV0f5kAPfFi4XK9jYo9BSepGt9/rWfvvCoSMaZ1+XvnqPp/Vv2fmTdAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.12", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.12" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "fMp91qlRlMOuHfMyp7LuAnOS+MOeWTF1obJmyFcs4nZCXu6pr+QB1AbFJcl2qpBltBrqF/Y7+Rn9IuNcSuctig==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "/6C1XjuRYRXst3QUoFnQdL0Q2ZihDlxr8PQ4jei8MyjVRZEGFg+le6N90d/CdpdgIbDz/h8JIKY1psACYDhPRQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Features": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "GaHsj8gISJ4BtRI4iK4meCil+LAYmv3NWuPKg6d29sw9cNsEN+1N8UMQXA/JhUXdrGTy16+B+YtjY3bco/8jMA==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "iQseV6HixWzEYQH4i++84ToIroXnM5jC1TJF4EqBz3lWNG1HBTMYDLkRb6Wm3wk5V6WqSfl7NukO/LYsloGyfg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "3cY+wo2+Qe65BVwShhr2fFqZv5Ns73h3kX4mpW3WvSgfT3AWCk0hKArKW4TF7Dg3mvbJdA6RhWPW7FR+PS5/1w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "45cV+UeI0chPAIXrx9Wv7K4PJXuuIR2pnOur5MOAo6kOFclBHSLLDrvGAxMfWRMtUvpv/kQZkxlgaUN56jwzSQ==" + }, + "Microsoft.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dzPj59oALFLu9TA4YUrfxqXEcbJVen/D2kOoitgM1lXBeSvPwPR8rOrrV0NKilCV3K0l4o+TLJJ3a1wmTu40Uw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Binder": "10.0.12", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.12", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.12", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.12", + "Microsoft.Extensions.Configuration.Json": "10.0.12", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.12", + "Microsoft.Extensions.DependencyInjection": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Diagnostics": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Physical": "10.0.12", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging.Configuration": "10.0.12", + "Microsoft.Extensions.Logging.Console": "10.0.12", + "Microsoft.Extensions.Logging.Debug": "10.0.12", + "Microsoft.Extensions.Logging.EventLog": "10.0.12", + "Microsoft.Extensions.Logging.EventSource": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Http.Diagnostics": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "yt9uQJSwh3hJag5NSDGEyWFbPKHaYem0w17TlSgv533NP8Kn56c5si870grJWGSL7aQyeUqxfKvDZxZpFaQUhw==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.12", + "Microsoft.Extensions.Telemetry": "10.10.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "6I46fTPfgYkrjRYfRXbho9WOvOelTnNjWuZws/hzGHDASH1LEJeA4VKK9k3wJvido8o7jJSB5WkMTonX7HM1bA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "CLHWfKQZWFE6fSzRXfjd4UlGveckkfPtwx/p5yF/o3hEkMSzGsl7MSxNAkI19hjw0rprBqrQC09Dwy6Lh6kweA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Binder": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "nHvaR8gO0CdEfG324KdUsP3Hqgdf1licZb+UlNh5pFYw7+o8QYTmiMgFmwayE/LPSpaqND/Qn3xHa1cI9b8x0w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging.Configuration": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dEvvABq9Qgfu/SgTncXDjCshcJmgO97bLOnMAuqBDSonlk4+VXlKJMHPak+S78TlOxJ47+gY7vPgVukJsr69gg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "Qn0FWPMiQzM4GQEEXxrJbh3vp3I9wrAgpMcsVH36+6FTRGXaqtwHDgf/2030FUJ4OuNSpOeIRh+9kCZGDDsfGg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "System.Diagnostics.EventLog": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "nKpYoiUdBu8fdLwDxj/fjjGn//7/VwRLzujtMq99/pExrswMXOz+80hbB98QxrD8qPt9oWlFpEVrN9OBKOP7RQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "opBULzn9F7ZOL8EoFuqM2QjI6x9AG04DFNdnU2ky/kI4aUzbiXEfuy9jvCuu3D/ieccuDHTlalyC62esPD8wGg==" + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" + }, + "Microsoft.Extensions.Resilience": { + "type": "Transitive", + "resolved": "10.10.0", + "contentHash": "aYIiuCSqsIqpaqya2JchArkaWibMhiXTNj4+wkjv0s7A/Uy1dvc4MsCPTOtvOU3ffz0+vYRHi7GHcXqynZfdbQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics": "10.0.12", + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.10.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.12", + "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==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Binder": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Features": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "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.Logging.Configuration": "10.0.12", + "Microsoft.Extensions.ObjectPool": "10.0.12", + "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.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.ObjectPool": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "2.12.0", + "contentHash": "0xB/be+f6qYOhvQPUrVsLrau+fkowYv6gt9RIkajMhwT7sZnqtcA797yW3bS5kgSje+AQW4xZgyLnW641YFioQ==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.10.1", + "contentHash": "o+PuLDRVp1iwiVAPBADZItXgm9Ck23XtPhE4kZtZVi0uYnMiiz1Dx9Cjh75kJ+ijfqnORL3VOTOaQVKi0f/ixg==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.10.1", + "contentHash": "KYz2omqkoz9E2mWa4fdgT+Ycie1wi5968EmyJlusjlp87RxxtA2FvDm8tcBEYIDzLnkWGnsKjE4d13Z7/rxkQA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.10.1" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.19.0", + "contentHash": "tH3Y3ABOCcUXbnz/jntnfEXcSxEy+GzS2niNqwJPIcNdAYUpP09QHLaAwcJExeS+ibfmlcSkmIfWlKhe7yVjIA==", + "dependencies": { + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "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": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "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": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Polly.Core": "8.4.2" + } + }, + "Polly.RateLimiting": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", + "dependencies": { + "Polly.Core": "8.4.2", + "System.Threading.RateLimiting": "8.0.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "sOZM+VyRj1pg/ItlHsvrSbjws6oGqZveTzTNBGZ9ziwRDyKsOF3ub6vhvOdxotCIqtlnyHAIwP5sLNvgiZC+pQ==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "2.4.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0", + "Microsoft.Testing.Platform": "2.4.0", + "Microsoft.Testing.Platform.MSBuild": "2.4.0", + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.inproc.console": "[4.0.1]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "dependencies": { + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "dependencies": { + "xunit.analyzers": "2.1.0", + "xunit.v3.assert": "[4.0.1]", + "xunit.v3.core.mtp-v2": "[4.0.1]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "dependencies": { + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.common": "[4.0.1]" + } + }, + "opengamebuilder.api": { + "type": "Project", + "dependencies": { + "Microsoft.AspNetCore.OpenApi": "[10.0.12, )", + "OpenGameBuilder.Api.Contracts": "[0.11.0, )", + "OpenGameBuilder.ServiceDefaults": "[0.11.0, )", + "Scalar.AspNetCore": "[2.17.6, )" + } + }, + "opengamebuilder.api.client": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Configuration": "[10.0.12, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.12, )", + "Microsoft.Extensions.Http": "[10.0.12, )", + "Microsoft.Extensions.Options": "[10.0.12, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.12, )", + "OpenGameBuilder.Api.Contracts": "[0.11.0, )" + } + }, + "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.AspNetCore.OpenApi": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "7RnQ/UKn+kK3JDLmTpW+vVqNErWthGPAVAy1aIiBzv4ucQ6g9NFvOJUuT0PHcpvcJxGVxctMxj6t+NESWMrgHQ==", + "dependencies": { + "Microsoft.OpenApi": "[2.12.0, 3.0.0)" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "e3IPP32CRNL031VZJAUTlCTG0YN7WFh4mN3fsSHTDQCJB+3+f0jGycv4fXk3rrftaY3B85XrQaj7sRthrOsavg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "ZlIxIJtWmrph0Ikj0svSaTkZ+d+ZYpTAhazpf1b3aoFRmlNnNWwkcvqnqT9yKHjKvfBRthvUrkq11OxfItOhpg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Http": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "x/fnoUGgmIUjSgLR1p6Mpi/vXc1Xe0xDPJnggPp23I6vwM/kY4nxyOjo8i6G5KPVw+esJM+rJsqnHbdjasvCBw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Diagnostics": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "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.ObjectPool": "10.0.12", + "Microsoft.Extensions.Resilience": "10.10.0" + } + }, + "Microsoft.Extensions.Options": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rqpu4qj5WE9x1IHGXSIgHBKi7IUlQaHyp4aXCYIanG2OghlUMFZpZTgExaXwcvmLAJHsxKQWMPpc7D2WIbCVtA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Binder": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.ServiceDiscovery": { + "type": "CentralTransitive", + "requested": "[10.10.0, )", + "resolved": "10.10.0", + "contentHash": "YdTl+XNxLiy8OYCFsYuSKkTGE9VyBaJlL+hlN+g9fSLwn77z37mkkdsjMnw3JhUZLd5nHotl9yQMI4s57FX1Dw==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.12", + "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": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "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": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "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)" + } + }, + "Scalar.AspNetCore": { + "type": "CentralTransitive", + "requested": "[2.17.6, )", + "resolved": "2.17.6", + "contentHash": "Qho7zxnjr5620jueYl40XlPNDJdn871MPe352AfYE2zvUkneMjOR5U9Cj+4f1Na7ODSSp5DUL0WM3MTdoKOazA==" + } + } + } +} \ No newline at end of file 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 ce3de52..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)" @@ -6,45 +8,62 @@ 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' +cat >"$test_root/bin/docker" <<'EOF' #!/usr/bin/env bash 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" -fail() { echo "FAIL: $*" >&2; exit 1; } +fail() { + echo "FAIL: $*" >&2 + exit 1 +} run_app() { bash "$repo_root/scripts/deploy-app.sh" "$1" "$app_dir" "${2:-}"; } make_release() { local id="$1" sha="$2" source incoming web_sha source="$test_root/source-$id" incoming="$app_dir/incoming/$id" mkdir -p "$source" "$incoming" - printf '\n' "$id" > "$source/index.html" - printf 'stale compressed index\n' > "$source/index.html.br" - printf 'stale compressed index\n' > "$source/index.html.gz" - printf 'asset %s\n' "$id" > "$source/asset-$id.txt" + printf '\n' "$id" >"$source/index.html" + printf 'stale compressed index\n' >"$source/index.html.br" + printf 'stale compressed index\n' >"$source/index.html.gz" + printf 'asset %s\n' "$id" >"$source/asset-$id.txt" tar -czf "$incoming/web-release.tar.gz" -C "$source" . web_sha="$(sha256sum "$incoming/web-release.tar.gz" | cut -d ' ' -f 1)" printf 'RELEASE_ID=%s\nSOURCE_SHA=%s\nAPI_IMAGE=ghcr.io/example/api@sha256:%s\nWEB_SHA256=%s\n' \ - "$id" "$sha" "$(printf 'a%.0s' {1..64})" "$web_sha" > "$incoming/release-manifest.txt" - printf 'name: ogb-staging\nservices:\n api:\n image: ${API_IMAGE}\n' > "$incoming/compose.yml" + "$id" "$sha" "$(printf 'a%.0s' {1..64})" "$web_sha" >"$incoming/release-manifest.txt" + printf 'name: ogb-staging\nservices:\n api:\n image: ${API_IMAGE}\n' >"$incoming/compose.yml" } legacy_sha="$(printf '1%.0s' {1..40})" printf 'SOURCE_SHA=%s\nAPI_IMAGE=ghcr.io/example/api@sha256:%s\nWEB_SHA256=%s\n' \ - "$legacy_sha" "$(printf 'b%.0s' {1..64})" "$(printf 'c%.0s' {1..64})" > "$app_dir/release-manifest.txt" -printf 'API_IMAGE=ghcr.io/example/api@sha256:%s\n' "$(printf 'b%.0s' {1..64})" > "$app_dir/.env" -printf 'name: ogb-staging\nservices:\n api:\n image: ${API_IMAGE}\n' > "$app_dir/compose.yml" -printf 'legacy page\n' > "$app_dir/web/index.html" -printf 'old compressed index\n' > "$app_dir/web/index.html.br" -printf 'old compressed index\n' > "$app_dir/web/index.html.gz" -printf 'legacy asset\n' > "$app_dir/web/old-asset.txt" + "$legacy_sha" "$(printf 'b%.0s' {1..64})" "$(printf 'c%.0s' {1..64})" >"$app_dir/release-manifest.txt" +printf 'API_IMAGE=ghcr.io/example/api@sha256:%s\n' "$(printf 'b%.0s' {1..64})" >"$app_dir/.env" +printf 'name: ogb-staging\nservices:\n api:\n image: ${API_IMAGE}\n' >"$app_dir/compose.yml" +printf 'legacy page\n' >"$app_dir/web/index.html" +printf 'old compressed index\n' >"$app_dir/web/index.html.br" +printf 'old compressed index\n' >"$app_dir/web/index.html.gz" +printf 'legacy asset\n' >"$app_dir/web/old-asset.txt" sha_a="$(printf '2%.0s' {1..40})" make_release 101-1-222222222222 "$sha_a" @@ -76,7 +95,164 @@ grep -Fq 'legacy page' "$app_dir/web/index.html" || fail 'failed activation chan echo 'PASS failed API update recovers the previous pair' make_release 103-1-444444444444 "$(printf '4%.0s' {1..40})" -printf 'corruption' >> "$app_dir/incoming/103-1-444444444444/web-release.tar.gz" +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' diff --git a/tests/deploy-edge/run.sh b/tests/deploy-edge/run.sh index 6935222..7f8d5ac 100644 --- a/tests/deploy-edge/run.sh +++ b/tests/deploy-edge/run.sh @@ -9,7 +9,7 @@ export EDGE_DIR="$test_root/active" export DOCKER_CALLS="$test_root/docker-calls" export PATH="$test_root/bin:$PATH" -cat > "$test_root/bin/docker" <<'EOF' +cat >"$test_root/bin/docker" <<'EOF' #!/usr/bin/env bash printf '%s\n' "$*" >> "$DOCKER_CALLS" case "$*" in @@ -28,15 +28,18 @@ esac EOF chmod +x "$test_root/bin/docker" -fail() { echo "FAIL: $*" >&2; exit 1; } +fail() { + echo "FAIL: $*" >&2 + exit 1 +} write_profile() { local default_config='example.com { respond "ok" }' - printf '# ogb-edge-profile: %s\n%s\n' "$2" "${3:-$default_config}" > "$1" + printf '# ogb-edge-profile: %s\n%s\n' "$2" "${3:-$default_config}" >"$1" } reset_fixture() { rm -f "$DOCKER_CALLS" "$EDGE_DIR/compose.yml" "$EDGE_DIR/Caddyfile" rm -rf "$test_root/staging" "$test_root/production" - printf 'name: ogb-edge\nservices:\n caddy:\n image: caddy:2\n' > "$EDGE_DIR/compose.yml" + printf 'name: ogb-edge\nservices:\n caddy:\n image: caddy:2\n' >"$EDGE_DIR/compose.yml" write_profile "$EDGE_DIR/Caddyfile" shared 'old.example.com { respond "old" }' cp "$EDGE_DIR/compose.yml" "$test_root/candidate/compose.yml" write_profile "$test_root/candidate/Caddyfile" shared 'new.example.com { respond "new" }' @@ -52,7 +55,7 @@ assert_rejected_without_mutation() { shift cp "$EDGE_DIR/compose.yml" "$test_root/expected-compose.yml" cp "$EDGE_DIR/Caddyfile" "$test_root/expected-Caddyfile" - if "$@" > "$test_root/rejection.log" 2>&1; then fail 'unsafe edge operation was accepted'; fi + if "$@" >"$test_root/rejection.log" 2>&1; then fail 'unsafe edge operation was accepted'; fi grep -Fq "$expected_error" "$test_root/rejection.log" || fail "expected error: $expected_error" [[ ! -s "$DOCKER_CALLS" ]] || fail 'rejected profile operation called Docker' [[ ! -e "$test_root/staging" && ! -e "$test_root/production" ]] || fail 'rejected profile operation created application directories' @@ -75,7 +78,7 @@ done echo 'PASS incomplete candidates do not touch Docker or application directories' reset_fixture -printf 'unmarked.example.com { respond "old" }\n' > "$test_root/candidate/Caddyfile" +printf 'unmarked.example.com { respond "old" }\n' >"$test_root/candidate/Caddyfile" assert_rejected_without_mutation 'Candidate Caddyfile requires an edge profile marker' run_apply for marker in \ '# ogb-edge-profile: invalid' \ @@ -83,7 +86,7 @@ for marker in \ '# ogb-edge-profile:shared' \ $'# another comment\n# ogb-edge-profile: shared' \ $'# ogb-edge-profile: shared\n# ogb-edge-profile: shared'; do - printf '%s\nexample.com { respond "ok" }\n' "$marker" > "$test_root/candidate/Caddyfile" + printf '%s\nexample.com { respond "ok" }\n' "$marker" >"$test_root/candidate/Caddyfile" assert_rejected_without_mutation 'Invalid edge profile marker' run_apply done echo 'PASS missing malformed misplaced and duplicate candidate markers are rejected' @@ -128,7 +131,7 @@ grep -Fxq '# ogb-edge-profile: shared' "$EDGE_DIR/Caddyfile" || fail 'approved p echo 'PASS changing an installed profile requires explicit production-authorized migration' reset_fixture -printf 'legacy.example.com { respond "old" }\n' > "$EDGE_DIR/Caddyfile" +printf 'legacy.example.com { respond "old" }\n' >"$EDGE_DIR/Caddyfile" write_profile "$test_root/candidate/Caddyfile" staging assert_rejected_without_mutation 'Unmarked legacy edge can only adopt the shared profile through production' run_apply staging true write_profile "$test_root/candidate/Caddyfile" production @@ -143,7 +146,7 @@ for marker in \ $'# another comment\n# ogb-edge-profile: shared' \ $'# ogb-edge-profile: shared\n# ogb-edge-profile: production'; do reset_fixture - printf '%s\nexample.com { respond "ok" }\n' "$marker" > "$EDGE_DIR/Caddyfile" + printf '%s\nexample.com { respond "ok" }\n' "$marker" >"$EDGE_DIR/Caddyfile" assert_rejected_without_mutation 'Invalid edge profile marker' run_apply production true done echo 'PASS malformed installed profiles cannot be overridden' @@ -187,7 +190,7 @@ done echo 'PASS application preflight checks exact marked profile without Docker or mutations' reset_fixture -printf 'legacy.example.com { respond "old" }\n' > "$EDGE_DIR/Caddyfile" +printf 'legacy.example.com { respond "old" }\n' >"$EDGE_DIR/Caddyfile" run_verify shared >/dev/null assert_rejected_without_mutation 'Unmarked legacy edge is only valid for the shared profile' run_verify staging assert_rejected_without_mutation 'Unmarked legacy edge is only valid for the shared profile' run_verify production @@ -199,13 +202,13 @@ for marker in \ $'# another comment\n# ogb-edge-profile: shared' \ $'# ogb-edge-profile: shared\n# ogb-edge-profile: shared'; do reset_fixture - printf '%s\nexample.com { respond "ok" }\n' "$marker" > "$EDGE_DIR/Caddyfile" + printf '%s\nexample.com { respond "ok" }\n' "$marker" >"$EDGE_DIR/Caddyfile" assert_rejected_without_mutation 'Invalid edge profile marker' run_verify shared done for missing_file in compose.yml Caddyfile; do reset_fixture rm "$EDGE_DIR/$missing_file" - if run_verify shared > "$test_root/rejection.log" 2>&1; then fail 'incomplete edge installation passed preflight'; fi + if run_verify shared >"$test_root/rejection.log" 2>&1; then fail 'incomplete edge installation passed preflight'; fi grep -Fq 'Edge installation requires compose.yml and Caddyfile' "$test_root/rejection.log" || fail 'preflight did not report incomplete installation' [[ ! -s "$DOCKER_CALLS" ]] || fail 'preflight called Docker for incomplete installation' done @@ -263,14 +266,14 @@ assert_not_called 'up -d' echo 'PASS failed reload restores previous configuration' reset_fixture -printf 'name: ogb-edge\nservices:\n caddy:\n image: caddy:2.1\n' > "$test_root/candidate/compose.yml" +printf 'name: ogb-edge\nservices:\n caddy:\n image: caddy:2.1\n' >"$test_root/candidate/compose.yml" run_apply >/dev/null assert_called 'up -d' assert_called 'exec -T caddy caddy reload' echo 'PASS explicit Compose change updates the edge service' reset_fixture -printf 'name: ogb-edge\nservices:\n caddy:\n image: caddy:2.1\n' > "$test_root/candidate/compose.yml" +printf 'name: ogb-edge\nservices:\n caddy:\n image: caddy:2.1\n' >"$test_root/candidate/compose.yml" export FAIL_UP=1 if run_apply >/dev/null 2>&1; then fail 'failed Compose update was reported as success'; fi grep -Fxq ' image: caddy:2' "$EDGE_DIR/compose.yml" || fail 'failed Compose update did not restore active file' diff --git a/tests/deploy-smoke/smoke.mjs b/tests/deploy-smoke/smoke.mjs index 1f3c475..574144f 100644 --- a/tests/deploy-smoke/smoke.mjs +++ b/tests/deploy-smoke/smoke.mjs @@ -1,13 +1,25 @@ -import assert from 'node:assert/strict'; -import { chromium } from 'playwright'; +import assert from "node:assert/strict"; +import { chromium } from "playwright"; -const baseUrl = process.env.SMOKE_TEST_BASE_URL?.replace(/\/$/, ''); +const baseUrl = process.env.SMOKE_TEST_BASE_URL?.replace(/\/$/, ""); const expectedRevision = process.env.EXPECTED_SOURCE_SHA; const expectedReleaseId = process.env.EXPECTED_RELEASE_ID; -assert.match(baseUrl ?? '', /^https:\/\//, 'A public HTTPS smoke URL is required'); -assert.match(expectedRevision ?? '', /^[0-9a-f]{40}$/, 'Expected source revision is required'); -assert.match(expectedReleaseId ?? '', /^[a-zA-Z0-9][a-zA-Z0-9.-]{0,100}$/, 'Expected release ID is required'); +assert.match( + baseUrl ?? "", + /^https:\/\//, + "A public HTTPS smoke URL is required", +); +assert.match( + expectedRevision ?? "", + /^[0-9a-f]{40}$/, + "Expected source revision is required", +); +assert.match( + expectedReleaseId ?? "", + /^[a-zA-Z0-9][a-zA-Z0-9.-]{0,100}$/, + "Expected release ID is required", +); const origin = new URL(baseUrl).origin; const expectedPage = `${origin}/releases/${expectedReleaseId}/index.html`; @@ -18,36 +30,59 @@ try { const context = await browser.newContext(); const page = await context.newPage(); const pageErrors = []; - page.on('pageerror', error => pageErrors.push(error.message)); + page.on("pageerror", (error) => pageErrors.push(error.message)); try { const aboutResponsePromise = page.waitForResponse( - response => response.url() === `${origin}/api/about`, + (response) => response.url() === `${origin}/api/about`, { timeout: 30000 }, ); aboutResponsePromise.catch(() => {}); - await page.goto(`${baseUrl}/`, { waitUntil: 'domcontentloaded', timeout: 30000 }); + await page.goto(`${baseUrl}/`, { + waitUntil: "domcontentloaded", + timeout: 30000, + }); await page.waitForURL(expectedPage, { timeout: 30000 }); const aboutResponse = await aboutResponsePromise; - assert.equal(aboutResponse.status(), 200, 'The page must successfully call /api/about'); + assert.equal( + aboutResponse.status(), + 200, + "The page must successfully call /api/about", + ); const about = await aboutResponse.json(); - assert.equal(about.sourceRevision, expectedRevision, 'The running API must match the requested source revision'); - assert.equal(await page.locator('base').getAttribute('href'), `/releases/${expectedReleaseId}/`); + assert.equal( + about.sourceRevision, + expectedRevision, + "The running API must match the requested source revision", + ); + assert.equal( + await page.locator("base").getAttribute("href"), + `/releases/${expectedReleaseId}/`, + ); const title = `${about.applicationName} ${about.version}`; await page.waitForFunction( - expected => document.querySelector('#app h1')?.textContent?.includes(expected), + (expected) => + document.querySelector("#app h1")?.textContent?.includes(expected), title, { timeout: 30000 }, ); - assert.deepEqual(pageErrors, [], 'The frontend must start without page errors'); - console.log(`PASS ${expectedReleaseId}: frontend rendered ${title} using API ${expectedRevision}`); + assert.deepEqual( + pageErrors, + [], + "The frontend must start without page errors", + ); + console.log( + `PASS ${expectedReleaseId}: frontend rendered ${title} using API ${expectedRevision}`, + ); process.exitCode = 0; lastError = undefined; break; } catch (error) { lastError = error; console.error(`Smoke attempt ${attempt}/6 failed: ${error.message}`); - if (pageErrors.length > 0) console.error(`Browser errors: ${pageErrors.join('; ')}`); - if (attempt < 6) await new Promise(resolve => setTimeout(resolve, 5000)); + if (pageErrors.length > 0) + console.error(`Browser errors: ${pageErrors.join("; ")}`); + if (attempt < 6) + await new Promise((resolve) => setTimeout(resolve, 5000)); } finally { await context.close(); } diff --git a/tests/deploy-topology/run.sh b/tests/deploy-topology/run.sh index 592bd21..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)" @@ -7,7 +9,10 @@ trap 'rm -rf -- "$test_root"' EXIT resolver="$repo_root/scripts/resolve-edge-profile.sh" renderer="$repo_root/scripts/render-edge.sh" -fail() { echo "FAIL: $*" >&2; exit 1; } +fail() { + echo "FAIL: $*" >&2 + exit 1 +} assert_contains() { grep -Fq -- "$2" "$1" || fail "$1 is missing $2"; } assert_absent() { if grep -Fq -- "$2" "$1"; then fail "$1 unexpectedly contains $2"; fi; } assert_rejected() { if "$@" >/dev/null 2>&1; then fail "unexpectedly accepted: $*"; fi; } @@ -36,7 +41,7 @@ echo 'PASS edge topology is explicit and must match the deployment environment' # checked independently of shell quoting. mkdir -p "$test_root/bin" export CURL_CALLS="$test_root/curl-calls" -cat > "$test_root/bin/curl" <<'EOF' +cat >"$test_root/bin/curl" <<'EOF' #!/usr/bin/env bash printf '%s\n' "$@" >> "$CURL_CALLS" printf '%s' "${CURL_RESPONSE:-}" @@ -61,20 +66,20 @@ for environment in production staging; do export CURL_RESPONSE="ok $environment" CURL_EXIT_CODE=0 rm -f "$CURL_CALLS" bash "$health_checker" "$environment" "https://$hostname/" >/dev/null - mapfile -t curl_arguments < "$CURL_CALLS" + mapfile -t curl_arguments <"$CURL_CALLS" resolve_count=0 noproxy_count=0 for ((index = 0; index < ${#curl_arguments[@]}; index++)); do case "${curl_arguments[$index]}" in - --noproxy) - ((noproxy_count += 1)) - [[ "${curl_arguments[$((index + 1))]:-}" == '*' ]] || fail 'health probe can use an external proxy' - ;; - --resolve) - ((resolve_count += 1)) - [[ "${curl_arguments[$((index + 1))]:-}" == "$hostname:443:127.0.0.1" ]] || fail 'health probe does not target the deployed host' - ;; - --location|--location-trusted|-L|--insecure|-k) fail 'health probe follows redirects or bypasses TLS validation' ;; + --noproxy) + ((noproxy_count += 1)) + [[ "${curl_arguments[$((index + 1))]:-}" == '*' ]] || fail 'health probe can use an external proxy' + ;; + --resolve) + ((resolve_count += 1)) + [[ "${curl_arguments[$((index + 1))]:-}" == "$hostname:443:127.0.0.1" ]] || fail 'health probe does not target the deployed host' + ;; + --location | --location-trusted | -L | --insecure | -k) fail 'health probe follows redirects or bypasses TLS validation' ;; esac done [[ "$resolve_count" == 1 ]] || fail 'health probe must specify exactly one host-local resolution' @@ -102,7 +107,7 @@ awk ' /- name: Check selected edge routes on the deployed host$/ { selected = 1; next } selected && /- name:/ { exit } selected { print } -' "$repo_root/.github/workflows/cd-edge.yml" > "$test_root/edge-health-step" +' "$repo_root/.github/workflows/cd-edge.yml" >"$test_root/edge-health-step" assert_contains "$test_root/edge-health-step" 'EDGE_ENVIRONMENTS: ${{ steps.target.outputs.environments }}' assert_contains "$test_root/edge-health-step" 'for environment in $EDGE_ENVIRONMENTS; do' assert_contains "$test_root/edge-health-step" 'ssh deployment bash -s -- "$environment" "$url" < scripts/check-edge-health.sh' @@ -113,7 +118,7 @@ for job in authorize-profile-change apply; do $0 == " " job ":" { selected = 1; next } selected && /^ [a-zA-Z0-9_-]+:/ { exit } selected { print } - ' "$repo_root/.github/workflows/cd-edge.yml" > "$test_root/$job-job" + ' "$repo_root/.github/workflows/cd-edge.yml" >"$test_root/$job-job" done assert_contains "$test_root/authorize-profile-change-job" 'if: ${{ inputs.allow-profile-change }}' assert_contains "$test_root/authorize-profile-change-job" 'environment: production' diff --git a/tests/release-scripts/run.sh b/tests/release-scripts/run.sh index 1ff0775..0be9e49 100644 --- a/tests/release-scripts/run.sh +++ b/tests/release-scripts/run.sh @@ -6,15 +6,18 @@ source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" real_git="$(command -v git)" test_root="$(mktemp -d -t ogb-release-tests.XXXXXX)" cleanup() { - case "${test_root}" in - */ogb-release-tests.*) rm -rf -- "${test_root}" ;; - *) echo "Refusing to remove unexpected test directory: ${test_root}" >&2; exit 1 ;; - esac + case "${test_root}" in + */ogb-release-tests.*) rm -rf -- "${test_root}" ;; + *) + echo "Refusing to remove unexpected test directory: ${test_root}" >&2 + exit 1 + ;; + esac } trap cleanup EXIT mkdir -p "${test_root}/mock-bin" -cat > "${test_root}/mock-bin/git" <<'EOF' +cat >"${test_root}/mock-bin/git" <<'EOF' #!/usr/bin/env bash if [ "${1:-}" = push ]; then printf 'git %s\n' "$*" >> "$MOCK_LOG" @@ -26,7 +29,7 @@ if [ "${1:-}" = ls-remote ] && [ "${MOCK_REMOTE_LOOKUP_FAIL:-0}" = 1 ]; then fi exec "$REAL_GIT" "$@" EOF -cat > "${test_root}/mock-bin/gh" <<'EOF' +cat >"${test_root}/mock-bin/gh" <<'EOF' #!/usr/bin/env bash printf 'gh %s\n' "$*" >> "$MOCK_LOG" case "${1:-} ${2:-}" in @@ -57,77 +60,89 @@ EOF chmod +x "${test_root}/mock-bin/git" "${test_root}/mock-bin/gh" export REAL_GIT="${real_git}" PATH="${test_root}/mock-bin:${PATH}" -fail() { echo "FAIL: $*" >&2; exit 1; } -assert_status() { [ "${status}" = "$1" ] || { cat "$output" >&2; fail "Expected exit $1, got ${status}"; }; } -assert_contains() { grep -Fq -- "$1" "$2" || { cat "$2" >&2; fail "Missing '$1' in $2"; }; } -assert_not_contains() { if grep -Fq -- "$1" "$2"; then cat "$2" >&2; fail "Unexpected '$1' in $2"; fi; } +fail() { + echo "FAIL: $*" >&2 + exit 1 +} +assert_status() { [ "${status}" = "$1" ] || { + cat "$output" >&2 + fail "Expected exit $1, got ${status}" +}; } +assert_contains() { grep -Fq -- "$1" "$2" || { + cat "$2" >&2 + fail "Missing '$1' in $2" +}; } +assert_not_contains() { if grep -Fq -- "$1" "$2"; then + cat "$2" >&2 + fail "Unexpected '$1' in $2" +fi; } assert_no_push() { assert_not_contains 'git push ' "$MOCK_LOG"; } new_fixture() { - local name="$1" version="$2" - fixture="${test_root}/${name}" - repo="${fixture}/work" - origin="${fixture}/origin.git" - mkdir -p "$fixture" - "$REAL_GIT" init -q -b main "$repo" - "$REAL_GIT" init -q --bare "$origin" - "$REAL_GIT" -C "$repo" config user.name 'Release Script Test' - "$REAL_GIT" -C "$repo" config user.email 'release-test@example.invalid' - "$REAL_GIT" -C "$repo" config core.autocrlf false - "$REAL_GIT" -C "$repo" remote add origin "$origin" - write_props "$version" - "$REAL_GIT" -C "$repo" add Directory.Build.props - "$REAL_GIT" -C "$repo" commit -qm base - base_sha="$("$REAL_GIT" -C "$repo" rev-parse HEAD)" - remote_ref refs/heads/main "$base_sha" - MOCK_LOG="${fixture}/operations.log" - MOCK_RELEASES_FILE="${fixture}/releases.txt" - output="${fixture}/output.txt" - gh_output_file="${fixture}/github-output.txt" - : > "$MOCK_LOG" - : > "$MOCK_RELEASES_FILE" - : > "$gh_output_file" - export MOCK_LOG MOCK_RELEASES_FILE - unset MOCK_RELEASE_LIST_FAIL MOCK_PR_LIST_FAIL MOCK_PR_CREATE_FAIL MOCK_PR_NUMBER \ - MOCK_PUSH_EXIT MOCK_REMOTE_LOOKUP_FAIL || true + local name="$1" version="$2" + fixture="${test_root}/${name}" + repo="${fixture}/work" + origin="${fixture}/origin.git" + mkdir -p "$fixture" + "$REAL_GIT" init -q -b main "$repo" + "$REAL_GIT" init -q --bare "$origin" + "$REAL_GIT" -C "$repo" config user.name 'Release Script Test' + "$REAL_GIT" -C "$repo" config user.email 'release-test@example.invalid' + "$REAL_GIT" -C "$repo" config core.autocrlf false + "$REAL_GIT" -C "$repo" remote add origin "$origin" + write_props "$version" + "$REAL_GIT" -C "$repo" add Directory.Build.props + "$REAL_GIT" -C "$repo" commit -qm base + base_sha="$("$REAL_GIT" -C "$repo" rev-parse HEAD)" + remote_ref refs/heads/main "$base_sha" + MOCK_LOG="${fixture}/operations.log" + MOCK_RELEASES_FILE="${fixture}/releases.txt" + output="${fixture}/output.txt" + gh_output_file="${fixture}/github-output.txt" + : >"$MOCK_LOG" + : >"$MOCK_RELEASES_FILE" + : >"$gh_output_file" + export MOCK_LOG MOCK_RELEASES_FILE + unset MOCK_RELEASE_LIST_FAIL MOCK_PR_LIST_FAIL MOCK_PR_CREATE_FAIL MOCK_PR_NUMBER \ + MOCK_PUSH_EXIT MOCK_REMOTE_LOOKUP_FAIL || true } write_props() { - local version="$1" extra="${2:-}" - printf '\n \n %s\n %s\n \n\n' \ - "$version" "$extra" > "$repo/Directory.Build.props" + local version="$1" extra="${2:-}" + printf '\n \n %s\n %s\n \n\n' \ + "$version" "$extra" >"$repo/Directory.Build.props" } remote_ref() { - "$REAL_GIT" --git-dir="$origin" fetch -q "$repo" "$2" - "$REAL_GIT" --git-dir="$origin" update-ref "$1" "$2" + "$REAL_GIT" --git-dir="$origin" fetch -q "$repo" "$2" + "$REAL_GIT" --git-dir="$origin" update-ref "$1" "$2" } tag_at() { - "$REAL_GIT" -C "$repo" tag "$1" "$2" - remote_ref "refs/tags/$1" "$2" + "$REAL_GIT" -C "$repo" tag "$1" "$2" + remote_ref "refs/tags/$1" "$2" } commit_props() { - write_props "$1" "${2:-}" - "$REAL_GIT" -C "$repo" add Directory.Build.props - "$REAL_GIT" -C "$repo" commit -qm "$1" + write_props "$1" "${2:-}" + "$REAL_GIT" -C "$repo" add Directory.Build.props + "$REAL_GIT" -C "$repo" commit -qm "$1" } patch_branch() { - "$REAL_GIT" -C "$repo" switch -q -c "patch/v$1" - commit_props "$1" "${2:-}" - patch_sha="$("$REAL_GIT" -C "$repo" rev-parse HEAD)" - remote_ref "refs/heads/patch/v$1" "$patch_sha" + "$REAL_GIT" -C "$repo" switch -q -c "patch/v$1" + commit_props "$1" "${2:-}" + patch_sha="$("$REAL_GIT" -C "$repo" rev-parse HEAD)" + remote_ref "refs/heads/patch/v$1" "$patch_sha" } -releases() { printf '%s\n' "$@" > "$MOCK_RELEASES_FILE"; } +releases() { printf '%s\n' "$@" >"$MOCK_RELEASES_FILE"; } run_script() { - if (cd "$repo" && "$@") > "$output" 2>&1; then status=0; else status=$?; fi + if (cd "$repo" && "$@") >"$output" 2>&1; then status=0; else status=$?; fi } validate() { run_script env SOURCE_REF="$1" GITHUB_OUTPUT="$gh_output_file" bash "$source_root/scripts/validate-release.sh"; } post_standard() { - run_script env KIND=standard VERSION="$1" TAG="v$1" NEXT_MAIN_VERSION="$2" \ - bash "$source_root/scripts/post-release.sh" + run_script env KIND=standard VERSION="$1" TAG="v$1" NEXT_MAIN_VERSION="$2" \ + bash "$source_root/scripts/post-release.sh" } post_patch() { - run_script env KIND=patch VERSION="$1" TAG="v$1" bash "$source_root/scripts/post-release.sh" + run_script env KIND=patch VERSION="$1" TAG="v$1" bash "$source_root/scripts/post-release.sh" } pass() { echo "PASS: $1"; } @@ -170,7 +185,7 @@ pass 'next patch release' tag_at v1.9.1 "$patch_sha" releases v1.9.0 v1.9.1 -: > "$gh_output_file" +: >"$gh_output_file" validate patch/v1.9.1 assert_status 0 assert_contains 'previous_tag=v1.9.0' "$gh_output_file" @@ -220,7 +235,8 @@ assert_contains 'latest deployed line' "$output" pass 'old release line rejected' new_fixture github_failure 1.10.0 -MOCK_RELEASE_LIST_FAIL=1; export MOCK_RELEASE_LIST_FAIL +MOCK_RELEASE_LIST_FAIL=1 +export MOCK_RELEASE_LIST_FAIL validate main [ "$status" != 0 ] || fail 'Failed GitHub release lookup was accepted' assert_contains 'Failed to list GitHub Releases' "$output" @@ -239,7 +255,8 @@ new_fixture standard_followup_rerun 1.9.0 commit_props 1.10.0 bump_sha="$("$REAL_GIT" -C "$repo" rev-parse HEAD)" remote_ref refs/heads/chore/bump-version-to-1.10.0 "$bump_sha" -MOCK_PR_NUMBER=42; export MOCK_PR_NUMBER +MOCK_PR_NUMBER=42 +export MOCK_PR_NUMBER post_standard 1.9.0 1.10.0 assert_status 0 assert_contains 'Version bump PR already exists: #42' "$output" @@ -248,7 +265,8 @@ assert_not_contains 'gh pr create' "$MOCK_LOG" pass 'existing follow-up PR rerun' new_fixture pr_list_failure 1.9.0 -MOCK_PR_LIST_FAIL=1; export MOCK_PR_LIST_FAIL +MOCK_PR_LIST_FAIL=1 +export MOCK_PR_LIST_FAIL post_standard 1.9.0 1.10.0 [ "$status" != 0 ] || fail 'Failed PR lookup was accepted' assert_contains 'mock GitHub PR lookup failed' "$output" @@ -256,14 +274,16 @@ assert_not_contains 'gh pr create' "$MOCK_LOG" pass 'failed GitHub PR lookup' new_fixture pr_create_failure 1.9.0 -MOCK_PR_CREATE_FAIL=1; export MOCK_PR_CREATE_FAIL +MOCK_PR_CREATE_FAIL=1 +export MOCK_PR_CREATE_FAIL post_standard 1.9.0 1.10.0 [ "$status" != 0 ] || fail 'Failed PR creation was accepted' assert_contains 'mock GitHub PR creation failed' "$output" pass 'failed GitHub PR creation' new_fixture remote_lookup_failure 1.9.0 -MOCK_REMOTE_LOOKUP_FAIL=1; export MOCK_REMOTE_LOOKUP_FAIL +MOCK_REMOTE_LOOKUP_FAIL=1 +export MOCK_REMOTE_LOOKUP_FAIL post_standard 1.9.0 1.10.0 [ "$status" != 0 ] || fail 'Failed remote branch lookup was accepted' assert_contains 'Failed to inspect remote ref' "$output" @@ -312,7 +332,8 @@ commit_props 1.9.1 prepare_sha="$("$REAL_GIT" -C "$repo" rev-parse HEAD)" remote_ref refs/heads/chore/prepare-v1.9.1 "$prepare_sha" releases v1.9.0 -MOCK_PR_NUMBER=43; export MOCK_PR_NUMBER +MOCK_PR_NUMBER=43 +export MOCK_PR_NUMBER run_script bash "$source_root/scripts/prepare-patch.sh" assert_status 0 assert_contains 'Prepare patch PR already exists: #43' "$output" diff --git a/tests/supply-chain/run.sh b/tests/supply-chain/run.sh index 28080a1..a1cc219 100644 --- a/tests/supply-chain/run.sh +++ b/tests/supply-chain/run.sh @@ -4,7 +4,10 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$repo_root" -fail() { echo "FAIL: $*" >&2; exit 1; } +fail() { + echo "FAIL: $*" >&2 + exit 1 +} action_count=0 version_comment_pattern='#[[:space:]]+v[0-9]' @@ -20,17 +23,19 @@ 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" ((base_count += 1)) -done < src/OpenGameBuilder.Api/Dockerfile +done 0)) || fail 'no Dockerfile base images were checked' grep -Eq '^[[:space:]]+image:[[:space:]]+[^[:space:]@]+@sha256:[0-9a-f]{64}$' deploy/edge/compose.yml || 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' @@ -52,6 +57,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 ' @@ -62,7 +77,12 @@ mapfile -t api_image_patterns < <(awk ' for dependency in dotnet/sdk dotnet/aspnet; do matched=false for pattern in "${api_image_patterns[@]}"; do - if [[ "$dependency" == $pattern ]]; then matched=true; break; fi + # Dependabot patterns intentionally match globs, not literal strings. + # shellcheck disable=SC2053 + if [[ "$dependency" == $pattern ]]; then + matched=true + break + fi done [[ "$matched" == true ]] || fail "Dependabot API image group does not match ${dependency}" done