From 8058033b44839212ba26c529e266499fa9887a77 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 7 Oct 2026 01:07:05 +1100 Subject: [PATCH 1/2] chore: prepare license and CI for public contributions --- .github/workflows/deploy-cloudflare.yml | 2 +- .github/workflows/test.yml | 33 +++++++++---------------- .github/workflows/weekly-social.yml | 2 +- LICENSE | 21 ++++++++++++++++ 4 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 LICENSE diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml index 238e6b09..1e80cc14 100644 --- a/.github/workflows/deploy-cloudflare.yml +++ b/.github/workflows/deploy-cloudflare.yml @@ -50,7 +50,7 @@ jobs: with: ref: ${{ github.event.workflow_run.head_sha || github.sha }} - - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@main + - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@575a348edd78a65c6b28104a86d6a3401301c913 with: install-args: '' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f27348a..da20c692 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,42 +18,33 @@ concurrency: group: test-${{ github.ref }} cancel-in-progress: true -# Every job runs on Harlan's workstation. A pull request from a fork must never -# reach it, because that would run untrusted code on the host. Each job -# therefore runs only for a push or for a pull request from a branch in this -# repository. A maintainer pushes a fork's change to a branch here to test it. -# -# Every job skipped still concludes the run as `success`. Deploy to Cloudflare -# guards on the triggering event and repository for that reason. +# Fork pull requests run on GitHub-hosted runners. Same-repository branches +# and main pushes use the workstation pool. Deploy requires a main push. jobs: harness-proof: - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: [self-hosted, linux, x64, harlan-desktop-ci] + runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && fromJSON('["ubuntu-latest"]') || fromJSON('["self-hosted", "linux", "x64", "harlan-desktop-ci"]') }} timeout-minutes: 15 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@main + - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@575a348edd78a65c6b28104a86d6a3401301c913 - run: | pnpm --filter @skilld/skill-harness-worker types:check pnpm --filter @skilld/skill-harness-worker typecheck pnpm --filter @skilld/skill-harness-worker test:run harlan-desktop: - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository - uses: harlan-zw/harlan-nuxt/.github/workflows/harlan-desktop-site-ci.yml@main + uses: harlan-zw/harlan-nuxt/.github/workflows/harlan-desktop-site-ci.yml@575a348edd78a65c6b28104a86d6a3401301c913 with: - # Ephemeral containers on the workstation. Safe here because the - # repository is private; a self-hosted runner on a public repository would - # let a fork pull request run code on that host. - runs-on: '["self-hosted", "linux", "x64", "harlan-desktop-ci"]' + runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && '["ubuntu-latest"]' || '["self-hosted", "linux", "x64", "harlan-desktop-ci"]' }} # Each check runs through `pnpm pipeline`, so an unchanged task restores # from the pool's shared cache volume instead of re-running. The graph # lives in pnpm-workspace.yaml: vitest still gets its `.nuxt/tsconfig.json` # because every pipeline runs `dev:prepare` as a cacheable task. - task-cache: true + task-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + prepare-command: pnpm dev:prepare timeout-minutes: 30 # The only gate between a merge and production. `Deploy to Cloudflare` fires on @@ -76,19 +67,19 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@main + - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@575a348edd78a65c6b28104a86d6a3401301c913 - run: pnpm cli:grammar build: # Main builds in the deploy workflow. Keep the build check on pull requests. - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository - runs-on: [self-hosted, linux, x64, harlan-desktop-ci] + if: github.event_name == 'pull_request' + runs-on: ${{ github.event.pull_request.head.repo.full_name != github.repository && fromJSON('["ubuntu-latest"]') || fromJSON('["self-hosted", "linux", "x64", "harlan-desktop-ci"]') }} timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@main + - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@575a348edd78a65c6b28104a86d6a3401301c913 # The desktop runner sends SIGHUP after 45 seconds without output. # Nitro can bundle longer than that, so the wrapper emits a heartbeat. - run: pnpm exec tsx scripts/run-with-heartbeat.ts pnpm build diff --git a/.github/workflows/weekly-social.yml b/.github/workflows/weekly-social.yml index ef6fa55e..d977a1f1 100644 --- a/.github/workflows/weekly-social.yml +++ b/.github/workflows/weekly-social.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@main + - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@575a348edd78a65c6b28104a86d6a3401301c913 - name: Prepare post and verify destination id: prepare run: pnpm exec tsx scripts/weekly-social.ts prepare ${{ matrix.destination }} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..00a85130 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Harlan Wilton + +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. From 5afb2fdc0655a455262d62bd6d8f102301e8dcfd Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 7 Oct 2026 01:14:30 +1100 Subject: [PATCH 2/2] fix(ci): move public repository jobs to hosted runners --- .github/actionlint.yaml | 6 ---- .github/workflows/deploy-cloudflare.yml | 2 +- .github/workflows/test.yml | 30 ++++++---------- .github/workflows/weekly-social.yml | 2 +- docs/runbooks/public-repository.md | 48 +++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 27 deletions(-) delete mode 100644 .github/actionlint.yaml create mode 100644 docs/runbooks/public-repository.md diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml deleted file mode 100644 index 45295eaf..00000000 --- a/.github/actionlint.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Declares the workstation pool's labels so actionlint stops reporting them as -# unknown. Harlan's desktop GitHub runner, in harlan-zw/harlan-nuxt infra/github-runner. -self-hosted-runner: - labels: - - harlan-desktop-ci - - harlan-desktop-deploy diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml index 1e80cc14..fc2334c3 100644 --- a/.github/workflows/deploy-cloudflare.yml +++ b/.github/workflows/deploy-cloudflare.yml @@ -43,7 +43,7 @@ jobs: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main') - runs-on: [self-hosted, linux, x64, harlan-desktop-deploy] + runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da20c692..1da07183 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,12 +18,12 @@ concurrency: group: test-${{ github.ref }} cancel-in-progress: true -# Fork pull requests run on GitHub-hosted runners. Same-repository branches -# and main pushes use the workstation pool. Deploy requires a main push. +# All checks run on GitHub-hosted runners. Forks can change workflow files, +# so a workflow condition cannot protect a registered workstation runner. jobs: harness-proof: - runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && fromJSON('["ubuntu-latest"]') || fromJSON('["self-hosted", "linux", "x64", "harlan-desktop-ci"]') }} + runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -35,15 +35,13 @@ jobs: pnpm --filter @skilld/skill-harness-worker typecheck pnpm --filter @skilld/skill-harness-worker test:run - harlan-desktop: + site: uses: harlan-zw/harlan-nuxt/.github/workflows/harlan-desktop-site-ci.yml@575a348edd78a65c6b28104a86d6a3401301c913 with: - runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && '["ubuntu-latest"]' || '["self-hosted", "linux", "x64", "harlan-desktop-ci"]' }} - # Each check runs through `pnpm pipeline`, so an unchanged task restores - # from the pool's shared cache volume instead of re-running. The graph - # lives in pnpm-workspace.yaml: vitest still gets its `.nuxt/tsconfig.json` - # because every pipeline runs `dev:prepare` as a cacheable task. - task-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: '["ubuntu-latest"]' + # Hosted runners have no workstation task cache. Prepare Nuxt before + # typecheck and tests through the reusable workflow's direct commands. + task-cache: false prepare-command: pnpm dev:prepare timeout-minutes: 30 @@ -57,11 +55,7 @@ jobs: # `Deploy to Cloudflare` by hand. published-cli-grammar: if: github.event_name == 'push' - runs-on: [self-hosted, linux, x64, harlan-desktop-ci] - # The desktop pool re-downloads the Node tarball in every ephemeral - # container, and a saturated shared uplink has measured 9.5 minutes for - # that one step. The check itself takes seconds; give the setup the same - # 30-minute budget as every other job on this pool. + runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -73,15 +67,13 @@ jobs: build: # Main builds in the deploy workflow. Keep the build check on pull requests. if: github.event_name == 'pull_request' - runs-on: ${{ github.event.pull_request.head.repo.full_name != github.repository && fromJSON('["ubuntu-latest"]') || fromJSON('["self-hosted", "linux", "x64", "harlan-desktop-ci"]') }} + runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: harlan-zw/harlan-nuxt/.github/actions/harlan-desktop-setup@575a348edd78a65c6b28104a86d6a3401301c913 - # The desktop runner sends SIGHUP after 45 seconds without output. - # Nitro can bundle longer than that, so the wrapper emits a heartbeat. - - run: pnpm exec tsx scripts/run-with-heartbeat.ts pnpm build + - run: pnpm build env: NODE_OPTIONS: --max-old-space-size=8192 diff --git a/.github/workflows/weekly-social.yml b/.github/workflows/weekly-social.yml index d977a1f1..3970cf33 100644 --- a/.github/workflows/weekly-social.yml +++ b/.github/workflows/weekly-social.yml @@ -16,7 +16,7 @@ permissions: jobs: weekly: - runs-on: [self-hosted, linux, x64, harlan-desktop-ci] + runs-on: ubuntu-latest timeout-minutes: 15 strategy: fail-fast: false diff --git a/docs/runbooks/public-repository.md b/docs/runbooks/public-repository.md new file mode 100644 index 00000000..dd786bab --- /dev/null +++ b/docs/runbooks/public-repository.md @@ -0,0 +1,48 @@ +# Public repository preparation + +Complete these steps before changing repository visibility. + +## Remove self-hosted access + +A fork can change workflow files and request a self-hosted runner. +Workflow conditions do not protect registered runners. +[GitHub documents this risk](https://docs.github.com/en/actions/reference/security/secure-use#hardening-for-self-hosted-runners). + +1. Merge the workflow changes that use GitHub-hosted runners. +2. Confirm the main push passes Test and Deploy to Cloudflare. +3. In the runner infrastructure repository, remove this repository from both pool configurations. + Remove its `harlan-desktop-ci` and `harlan-desktop-deploy` entries from `runners.conf` and `hogwild-runners.conf`. +4. Apply both pool configurations using the runner infrastructure's installation procedure. + Let active jobs finish before removing their registrations. +5. Confirm the supervisor no longer creates runners for this repository. +6. Confirm GitHub reports zero self-hosted runners for the repository. + +Run the last check from this repository: + +```sh +gh api repos/skilld-dev/skilld.dev/actions/runners --jq '.runners | length' +``` + +The result must be `0`. +Keep organization runner groups unavailable to public repositories unless they restrict trusted workflows and refs. +Changing `runs-on` alone leaves a registered runner available to a modified fork workflow. + +## Review published records + +Scan tracked files, all available Git refs, retained Actions logs, artifacts, issues, and pull request discussions. +Review internal work notes and backups for deliberate publication. +Deleting a file from main leaves earlier versions in Git history. + +If a scan finds a real credential, rotate it before removing its public copies. +Review image attachments separately. A text scan cannot inspect their pixels. + +[GitHub makes historical Actions logs public when visibility changes](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/setting-repository-visibility). +Secret masking does not guarantee that every credential stays hidden. + +## Change visibility + +After the checks pass, the repository owner can change visibility in GitHub settings. +Confirm branch protection, required checks, fork workflow approvals, and secret protection after the change. +Use the current `site/lint`, `site/typecheck`, `site/test`, `build`, and `harness-proof` job names when setting required checks. +Exercise a fork pull request and confirm that every job uses GitHub-hosted runners. +Confirm that its Test completion does not start a production deployment.