From 560bb5744104a081d047ffada35adb8d531d4316 Mon Sep 17 00:00:00 2001 From: Jon Bogaty Date: Mon, 24 Aug 2026 11:30:10 -0500 Subject: [PATCH 1/2] ci: automate trusted maintenance releases --- .github/workflows/automerge.yml | 20 +++++---- .github/workflows/ci.yml | 41 +++++++++++++++++-- context7.json | 36 ++++++++++++++++ .../tests/contract/context7-contract.test.ts | 36 ++++++++++++++++ .../tests/contract/workflows-contract.test.ts | 15 +++++-- 5 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 context7.json create mode 100644 packages/declarative-hex-worlds/tests/contract/context7-contract.test.ts diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 5d6e25b..7d5ede6 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -1,4 +1,4 @@ -name: Automerge +name: Automerge trusted maintenance on: pull_request: @@ -8,19 +8,25 @@ permissions: contents: read jobs: - dependabot: - name: Dependabot Auto-merge + trusted-maintenance: + name: Trusted maintenance auto-merge if: >- - github.actor == 'dependabot[bot]' && - github.event.pull_request.user.login == 'dependabot[bot]' && - github.event.pull_request.head.repo.full_name == github.repository + github.event.pull_request.head.repo.full_name == github.repository && + ( + startsWith(github.event.pull_request.head.ref, 'release-please--') || + ( + github.actor == 'dependabot[bot]' && + github.event.pull_request.user.login == 'dependabot[bot]' && + contains(github.event.pull_request.head.ref, '-non-major-') + ) + ) runs-on: ubuntu-latest permissions: contents: write pull-requests: write env: PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} steps: - name: Enable auto-merge run: gh pr merge "$PR_URL" --auto --merge diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec9bc2c..3de66ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,31 @@ env: # pointing at a local models/ dir. jobs: + policy: + name: CI policy + runs-on: ubuntu-latest + outputs: + run_ci: ${{ steps.classify.outputs.run_ci }} + steps: + - id: classify + name: Classify trusted maintenance pull requests + env: + EVENT_NAME: ${{ github.event_name }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + REPOSITORY: ${{ github.repository }} + run: | + run_ci=true + if [ "$EVENT_NAME" = "pull_request" ] && [ "$PR_HEAD_REPOSITORY" = "$REPOSITORY" ]; then + if [[ "$PR_HEAD_REF" == release-please--* ]]; then + run_ci=false + elif [ "$PR_AUTHOR" = "dependabot[bot]" ] && [[ "$PR_HEAD_REF" == *-non-major-* ]]; then + run_ci=false + fi + fi + echo "run_ci=$run_ci" >> "$GITHUB_OUTPUT" + # Every job installs with `pnpm install --frozen-lockfile` against pnpm's # native content-addressed store cache (setup-node cache:'pnpm'). The # frozen lockfile guarantees identical resolved bytes across jobs, and the @@ -52,6 +77,8 @@ jobs: # "leverage each action's own caching" approach; no custom pack/restore. check: name: ${{ matrix.task }} + needs: [policy] + if: needs.policy.outputs.run_ci == 'true' runs-on: ubuntu-latest strategy: fail-fast: false @@ -87,7 +114,8 @@ jobs: # Sourcey is the sole production renderer. This safe pull_request job builds # from the checked-out change but never deploys or receives credentials. name: Sourcey Docs Build - needs: [check] + needs: [policy, check] + if: always() && needs.policy.outputs.run_ci == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -123,7 +151,8 @@ jobs: # actually composes into a working game (per RFC 0001 §D-test-topology). A green # library but red examples means the public surface or a binding regressed. name: Examples (consumer e2e) - needs: [check] + needs: [policy, check] + if: always() && needs.policy.outputs.run_ci == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -171,6 +200,8 @@ jobs: coverage: name: Coverage + needs: [policy] + if: needs.policy.outputs.run_ci == 'true' runs-on: ubuntu-latest # Library scripts (test:coverage, coverage:merge:enforce), src/cli, and the # coverage/ output are all package-level — run this job's steps from the @@ -241,7 +272,8 @@ jobs: dependency-review: name: Dependency Review - if: github.event_name == 'pull_request' + needs: [policy] + if: needs.policy.outputs.run_ci == 'true' && github.event_name == 'pull_request' runs-on: ubuntu-latest permissions: contents: read @@ -259,7 +291,8 @@ jobs: semgrep: name: Semgrep SAST - needs: check + needs: [policy, check] + if: always() && needs.policy.outputs.run_ci == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..b2b0251 --- /dev/null +++ b/context7.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "projectTitle": "Declarative Hex Worlds", + "description": "A Koota-first, declarative 2.5D hex-gameboard runtime with recipes, scenarios, simulation, rendering, and KayKit asset support.", + "branch": "main", + "folders": [ + "docs", + "packages/declarative-hex-worlds/docs", + "packages/declarative-hex-worlds/src", + "examples" + ], + "excludeFolders": [ + ".git", + "node_modules", + "dist", + "coverage", + "references", + "models", + "tests", + "docs/dist" + ], + "excludeFiles": [ + "CHANGELOG.md" + ], + "rules": [ + "Treat docs/ as the canonical user and agent documentation site; it is built with Sourcey.", + "Use the public runtime, recipe, scenario, simulation, and blueprint APIs instead of mutating Koota relation stores directly.", + "Keep FREE asset manifests publishable and fetch raw KayKit assets through bootstrap; never publish local references or EXTRA binaries.", + "Preserve footprint occupancy through the public placement and actor helpers, and validate serializable plans before runtime-specific checks." + ], + "previousVersions": [ + { + "tag": "declarative-hex-worlds@1.2.3" + } + ] +} diff --git a/packages/declarative-hex-worlds/tests/contract/context7-contract.test.ts b/packages/declarative-hex-worlds/tests/contract/context7-contract.test.ts new file mode 100644 index 0000000..6b782b1 --- /dev/null +++ b/packages/declarative-hex-worlds/tests/contract/context7-contract.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { findWorkspaceRoot } from '../setup/workspace-root'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +interface Context7Config { + $schema?: string; + projectTitle?: string; + branch?: string; + folders?: string[]; + excludeFolders?: string[]; + rules?: string[]; +} + +const workspaceRoot = findWorkspaceRoot(import.meta.dirname); +const config = JSON.parse( + readFileSync(resolve(workspaceRoot, 'context7.json'), 'utf8') +) as Context7Config; + +describe('Context7 owner configuration', () => { + it('pins the official schema and the canonical branch', () => { + expect(config.$schema).toBe('https://context7.com/schema/context7.json'); + expect(config.branch).toBe('main'); + expect(config.projectTitle).toBe('Declarative Hex Worlds'); + }); + + it('indexes canonical documentation and public API source without local assets', () => { + expect(config.folders).toEqual( + expect.arrayContaining(['docs', 'packages/declarative-hex-worlds/docs', 'packages/declarative-hex-worlds/src']) + ); + expect(config.excludeFolders).toEqual( + expect.arrayContaining(['node_modules', 'references', 'models', 'tests', 'docs/dist']) + ); + expect(config.rules?.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/declarative-hex-worlds/tests/contract/workflows-contract.test.ts b/packages/declarative-hex-worlds/tests/contract/workflows-contract.test.ts index 01e0310..347bee0 100644 --- a/packages/declarative-hex-worlds/tests/contract/workflows-contract.test.ts +++ b/packages/declarative-hex-worlds/tests/contract/workflows-contract.test.ts @@ -25,6 +25,7 @@ const files = { benchmarks: '.github/workflows/benchmarks.yml', cd: '.github/workflows/cd.yml', ci: '.github/workflows/ci.yml', + context7: 'context7.json', dependabot: '.github/dependabot.yml', release: '.github/workflows/release.yml', releasePleaseConfig: 'release-please-config.json', @@ -111,6 +112,12 @@ describe('workflow contract', () => { ['actions/upload-pages-artifact'], // dep-review job ['fail-on-severity: high'], + // Trusted release-please and non-major Dependabot PRs are immutable + // encapsulations of already-vetted changes. Keep the classifier explicit + // so normal contributor, major, and security-update PRs still run CI. + ['name: CI policy'], + ['release-please--*'], + ["*-non-major-*"], ])('includes %s', (snippet) => { expect(read(files.ci)).toContain(snippet); }); @@ -286,17 +293,17 @@ describe('workflow contract', () => { ["github.actor == 'dependabot[bot]'"], ["github.event.pull_request.user.login == 'dependabot[bot]'"], ['github.event.pull_request.head.repo.full_name == github.repository'], + ["startsWith(github.event.pull_request.head.ref, 'release-please--')"], + ["contains(github.event.pull_request.head.ref, '-non-major-')"], + ['GH_TOKEN: $' + '{{ secrets.CI_GITHUB_TOKEN }}'], ['gh pr merge "$PR_URL" --auto --merge'], ])('includes %s', (snippet) => { expect(automergeContent).toContain(snippet); }); it.each([ - ['Release Please Auto-merge'], - ['release-please:'], - ["startsWith(github.head_ref, 'release-please--')"], ["github.event.pull_request.user.type == 'Bot'"], - ])('excludes %s so release PRs stay a maintainer checkpoint', (snippet) => { + ])('excludes unsafe broad bot matching', (snippet) => { expect(automergeContent).not.toContain(snippet); }); From 24ec809352c6c18668952763b1e492c997e4e583 Mon Sep 17 00:00:00 2001 From: Jon Bogaty Date: Mon, 24 Aug 2026 11:33:19 -0500 Subject: [PATCH 2/2] ci: scope CI policy permission --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3de66ae..9765a25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,6 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true -permissions: - contents: read - env: NODE_VERSION: '22' @@ -46,6 +43,8 @@ jobs: policy: name: CI policy runs-on: ubuntu-latest + permissions: + contents: read outputs: run_ci: ${{ steps.classify.outputs.run_ci }} steps: