diff --git a/.github/workflows/build-fork.yml b/.github/workflows/build-fork.yml deleted file mode 100644 index 5520cf746b..0000000000 --- a/.github/workflows/build-fork.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Publish Fork Image to GHCR - -on: - push: - branches: [main] - tags: - - "v*" - workflow_dispatch: - -# Least-privilege default: read-only at the top level; the build job that pushes to -# GHCR grants packages: write itself (Scorecard TokenPermissions). -permissions: - contents: read - -env: - IMAGE_NAME: ghcr.io/kang-heewon/omniroute - -jobs: - build: - name: Build and Push Fork Image - if: github.repository == 'kang-heewon/OmniRoute' - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v6 - with: - images: ${{ env.IMAGE_NAME }} - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=sha,prefix=sha- - type=ref,event=tag - labels: | - org.opencontainers.image.title=omniroute - org.opencontainers.image.description=Unified AI proxy/router — fork image - org.opencontainers.image.url=https://github.com/kang-heewon/OmniRoute - org.opencontainers.image.source=https://github.com/kang-heewon/OmniRoute - org.opencontainers.image.licenses=MIT - - - name: Build and push - uses: docker/build-push-action@v7 - with: - context: . - target: runner-base - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/build-rinseaid-image.yml b/.github/workflows/build-rinseaid-image.yml deleted file mode 100644 index e601976245..0000000000 --- a/.github/workflows/build-rinseaid-image.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build Rinseaid OmniRoute image - -on: - push: - branches: [build-k3-reasoning-image] - paths: - - Dockerfile - - package-lock.json - - package.json - - open-sse/** - - scripts/build/** - - .github/workflows/build-rinseaid-image.yml - workflow_dispatch: - -permissions: - contents: read - packages: write - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: docker/setup-buildx-action@v3 - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - uses: docker/build-push-action@v6 - with: - context: . - target: runner-base - platforms: linux/amd64 - push: true - tags: ghcr.io/rinseaid/omniroute:k3-reasoning-${{ github.sha }} diff --git a/changelog.d/maintenance/8967-remove-fork-publish-workflows.md b/changelog.d/maintenance/8967-remove-fork-publish-workflows.md new file mode 100644 index 0000000000..d07d91980a --- /dev/null +++ b/changelog.d/maintenance/8967-remove-fork-publish-workflows.md @@ -0,0 +1 @@ +- **chore(ci):** removed two fork-owned image-publish workflows that had ridden into the repo as unrelated extra files in on-topic PRs — `build-fork.yml` (`ghcr.io/kang-heewon`, job-level guard, so it instantiated a skipped run on every push to main and every tag) and `build-rinseaid-image.yml` (`ghcr.io/rinseaid`, no guard, never fired). Neither could authenticate against this repository's token; a new policy guard now fails CI on any workflow targeting a foreign registry namespace ([#8967](https://github.com/diegosouzapw/OmniRoute/pull/8967)) diff --git a/tests/unit/workflows-no-foreign-fork-publishers.test.ts b/tests/unit/workflows-no-foreign-fork-publishers.test.ts new file mode 100644 index 0000000000..6b3835c279 --- /dev/null +++ b/tests/unit/workflows-no-foreign-fork-publishers.test.ts @@ -0,0 +1,93 @@ +/** + * Policy guard: no workflow in this repository may publish to — or be gated on — a + * DIFFERENT repository's namespace. + * + * Twice now a contributor's own fork CI has ridden into the canonical repo as an + * unrelated extra file in an otherwise on-topic PR: + * + * .github/workflows/build-fork.yml added by #1528 (scope: SSE translator) + * env: IMAGE_NAME: ghcr.io/kang-heewon/omniroute + * if: github.repository == 'kang-heewon/OmniRoute' + * → the guard sits on the JOB, not the workflow, so GitHub instantiated a run on + * every push to main and every v* tag and skipped the job: 100+ runs, zero + * runner cost, and a permanently noisy check board on every release. + * + * .github/workflows/build-rinseaid-image.yml added by #8729 (scope: SSE reasoning) + * tags: ghcr.io/rinseaid/omniroute:... + * → no repository guard at all; it simply never fires because its trigger branch + * (`build-k3-reasoning-image`) does not exist here. 0 runs. + * + * Neither can succeed: this repo's GITHUB_TOKEN cannot write to another owner's GHCR + * namespace. So the cost is not a breach, it is dead configuration that review keeps + * waving through because the PR it arrives in is about something else entirely. + * + * This test is the cheap check that review is not: it reads the workflow directory and + * fails on any foreign owner, so the next accidental inclusion is caught by CI instead + * of surviving until someone wonders why a release board shows a skipped fork job. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const workflowDir = path.join(repoRoot, ".github/workflows"); + +/** The only owner whose namespaces this repository may publish to or gate on. */ +const OWNER = "diegosouzapw"; + +function workflowFiles(): string[] { + return fs + .readdirSync(workflowDir) + .filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")) + .map((f) => path.join(workflowDir, f)); +} + +test("no workflow publishes to another owner's container registry", () => { + const offenders: string[] = []; + + for (const file of workflowFiles()) { + const text = fs.readFileSync(file, "utf-8"); + // ghcr.io//... and index.docker.io//... — the owner is the segment + // right after the registry host. + for (const m of text.matchAll(/\b(?:ghcr\.io|(?:index\.)?docker\.io)\/([A-Za-z0-9_.-]+)/g)) { + const owner = m[1]; + if (owner.toLowerCase() !== OWNER) { + offenders.push(`${path.basename(file)} → ${m[0]}`); + } + } + } + + assert.deepEqual( + offenders, + [], + `workflow(s) target a registry namespace that is not ${OWNER}'s:\n ${offenders.join("\n ")}\n` + + `A fork's publish workflow does not belong in the canonical repository — it cannot ` + + `authenticate anyway, and it pollutes every release check board.` + ); +}); + +test("no workflow job is gated on a different repository", () => { + const offenders: string[] = []; + + for (const file of workflowFiles()) { + const text = fs.readFileSync(file, "utf-8"); + // `if: github.repository == 'owner/name'` — a guard naming someone else's repo means + // the workflow was written for a fork. + for (const m of text.matchAll(/github\.repository\s*[=!]=\s*['"]([^'"]+)['"]/g)) { + const [owner] = m[1].split("/"); + if (owner.toLowerCase() !== OWNER) { + offenders.push(`${path.basename(file)} → ${m[0]}`); + } + } + } + + assert.deepEqual( + offenders, + [], + `workflow(s) gate on a foreign repository:\n ${offenders.join("\n ")}\n` + + `Note the failure mode: a job-level guard still instantiates a run on every ` + + `matching trigger, so the workflow shows up as a skipped check forever.` + ); +});