Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions .github/workflows/fleet-pat-health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,27 @@ concurrency:
cancel-in-progress: false

env:
# Space-separated list of owner/repo pairs to check.
# Add new fleet repos here when they receive GH_RELEASE_PAT.
FLEET_REPOS: "izzywdev/FuzeX izzywdev/FuzeFinance izzywdev/FuzeExecutive"
# Space-separated list of `owner/repo`, or `owner/repo=workflow.yml` where the release job
# is not in release.yml. Add new fleet repos here when they receive GH_RELEASE_PAT.
#
# These are exactly the repos whose OWN workflows reference `secrets.GH_RELEASE_PAT` to bump
# a prod tag β€” derived from the fleet-wide survey in FuzeSDLC's provision_secrets.py, not
# hand-listed. FuzeCall, FuzeDeploy and FuzeMerchandize were provisioned the credential on
# 2026-08-26 and are added here in the same change: a repo that holds the PAT but is absent
# from this list has NO expiry detection at all, which is the gap this workflow exists to
# close and the one it had for six of its nine repos.
#
# FuzeFront and FuzeSDLC deliberately excluded: both hold GH_RELEASE_PAT, but neither uses
# it to cut a release β€” FuzeFront for this health check itself, FuzeSDLC for fleet secret
# provisioning. Listing them would check a release workflow that does not exist.
FLEET_REPOS: >-
izzywdev/FuzeX
izzywdev/FuzeFinance
izzywdev/FuzeExecutive
izzywdev/FuzePlan
izzywdev/FuzeCall
izzywdev/FuzeDeploy
izzywdev/FuzeMerchandize=build-and-push.yml
WORKFLOW_FILE: "release.yml"
CRED_STEP_NAME: "Require the release credential"

Expand Down Expand Up @@ -79,13 +97,23 @@ jobs:
CRED_FAILURES=""
OTHER_FAILURES=""

for REPO in $FLEET_REPOS; do
for ENTRY in $FLEET_REPOS; do
# Each entry is `owner/repo`, or `owner/repo=workflow.yml` when the repo's
# release job does not live in release.yml. FuzeMerchandize is the reason this
# exists: it references GH_RELEASE_PAT from build-and-push.yml, and querying a
# workflow file a repo does not have returns the same "could not query" error as
# a genuinely broken credential β€” a false alarm that trains people to ignore the
# alert this whole workflow exists to raise.
REPO="${ENTRY%%=*}"
WF="${ENTRY#*=}"
[ "$WF" = "$ENTRY" ] && WF="$WORKFLOW_FILE"

echo ""
echo "==> ${REPO}"
echo "==> ${REPO} (${WF})"

# Fetch the latest completed run for this repo's release workflow.
RUN_JSON=$(gh api \
"repos/${REPO}/actions/workflows/${WORKFLOW_FILE}/runs?per_page=1&status=completed" \
"repos/${REPO}/actions/workflows/${WF}/runs?per_page=1&status=completed" \
--jq '.workflow_runs[0] | {id: .id, conclusion: .conclusion, created_at: .created_at, html_url: .html_url}' \
2>&1) || {
echo "::warning::Could not query ${REPO} β€” PAT may lack repo scope or workflow not found"
Expand Down
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,15 @@ _site_test/

# Claude Code personal/session settings (not committed)
.claude/settings.local.json

# Local dev/test database file. `backend/scripts/init-db.js` (npm run db:init)
# writes this whenever the run is not pointed at Postgres, so anyone who
# reproduces a CI failure locally generates one. A database binary must never
# be committed: it churns on every run and can carry real data.
backend/database.sqlite

# Jest coverage output. `npm run test:coverage` (backend-tests.yml's "Generate
# test coverage" step, and anyone reproducing it locally) writes ~6.6M of
# generated HTML + lcov per workspace. CI ships it to Codecov from the runner;
# it is never a source artifact. Unanchored so it matches any workspace.
coverage/
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,50 @@ export async function up(knex: Knex): Promise<void> {
// with a clear cause instead of a mysterious later FK violation.
const root = await knex('organizations').where({ id: ROOT_ORG_ID }).first()
if (!root) {
throw new Error(
`organizations.${ROOT_ORG_ID} (the platform root org) does not exist yet β€” ` +
'backend/src migration 015_seed_root_platform_organization must run before this one.'
// 2026-08-26: this used to throw UNCONDITIONALLY. The reasoning above is
// right for the case it describes β€” backfilling to a still-absent id would
// violate apps_organization_id_foreign, and a clear error beats a mystery
// FK stack trace. But it fired even when there was NOTHING TO BACKFILL,
// and a migration that throws is a boot crashloop, not a warning: this
// service's tree must stay runnable on its own (see
// tests/migrations.idempotency.integration.test.ts, which runs exactly
// this tree against a bare schema and asserts a clean no-op). Since the
// root org is seeded by a DIFFERENT deployable's tree, any environment
// where applications-service migrates first hit this β€” the same shape as
// the 2026-08-16 P1 that migration 022 in backend/src was fixed for.
//
// So the throw is narrowed to the case that genuinely warrants it.
//
// What closes the gap is NOT a re-run of this migration: knex records it
// as applied and never executes it again, so "self-heals on the next boot"
// would be false here. It is the SIBLING migration against the same shared
// table β€” backend/src's 026_apps_organization_id_not_null, in the tree that
// also owns 015 and therefore always has the root org by the time it runs.
// That sibling sets the DEFAULT and NOT NULL, exactly as its own header
// describes: "whichever service's migrations happen to run first against a
// given database does the real work, and the other is a no-op".
//
// The invariant the query-side half depends on still holds in this branch:
// service.ts's list() may drop its `organization_id IS NULL` arm only once
// no row can be null, and we skip precisely when there are ZERO org-less
// rows β€” so there is nothing that could silently disappear.
const orphans = await knex('apps').whereNull('organization_id').count({ n: '*' }).first()
const orphanCount = Number(orphans?.n ?? 0)
if (orphanCount > 0) {
throw new Error(
`organizations.${ROOT_ORG_ID} (the platform root org) does not exist yet, and ` +
`${orphanCount} org-less app(s) need backfilling to it β€” ` +
'backend/src migration 015_seed_root_platform_organization must run before this one.'
)
}
// eslint-disable-next-line no-console
console.log(
`[011] platform root org ${ROOT_ORG_ID} not seeded yet and no org-less apps to backfill β€” ` +
"skipping; backend/src's sibling migration 026 applies DEFAULT + NOT NULL once 015 has " +
'seeded it. Neither is set here: the DEFAULT would point at a non-existent org and ' +
'reintroduce the very FK hazard this guard exists to prevent.'
)
return
}

const backfilled = await knex('apps')
Expand Down
19 changes: 18 additions & 1 deletion backend/tests/apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,22 @@ describe('Apps Registration Routes', () => {
})

// An org NEITHER caller belongs to, for the BOLA-exclusion assertion.
// The row must EXIST: `apps.organization_id` carries the FK
// `apps_organization_id_foreign`, so referencing an org that was never
// inserted aborts this beforeAll and fails every test in the block. What
// makes it a "does not belong" org is the absence of an
// organization_memberships row below, not the absence of the org itself.
otherOrgId = uuidv4()
await db('organizations').insert({
id: otherOrgId,
name: 'Visibility Parity Other Org',
slug: `visibility-parity-other-org-${otherOrgId.slice(0, 8)}`,
owner_id: ADMIN_USER_ID,
type: 'organization',
settings: JSON.stringify({}),
metadata: JSON.stringify({}),
is_active: true,
})

// Case 1: 'organization' visibility, owned by an org the admin belongs to.
ownOrgAppId = uuidv4()
Expand Down Expand Up @@ -710,7 +725,9 @@ describe('Apps Registration Routes', () => {
.where('organization_id', scopedOrgId)
.del()
await db('apps').whereIn('id', [ownOrgAppId, otherOrgAppId]).del()
await db('organizations').where('id', scopedOrgId).del()
// Both orgs, and only after the apps that reference them are gone β€”
// apps.organization_id has no ON DELETE, so the reverse order fails.
await db('organizations').whereIn('id', [scopedOrgId, otherOrgId]).del()
})

it("shows an 'organization'-visibility app to a member of that org", async () => {
Expand Down
38 changes: 37 additions & 1 deletion backend/tests/rootOrgAbsentGuards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,50 @@ describe('#750 β€” nothing inserts a reference to an unverified root organizatio
})

describe('migration 022 (root-membership backfill + personal-org reclassify)', () => {
it('skips the backfill when the root organization does not exist', async () => {
it('2026-08-26 AMENDMENT (#680): ADOPTS a platform org under a different id and backfills against ITS id', async () => {
// This assertion was inverted until #680 (73c30aae): it required a SKIP
// for this fixture. That was the contract which crashlooped
// fuzefront-backend and fuzefront-security on 2026-08-16 β€” migration
// 015's "adopt a pre-existing platform org rather than creating a
// second one" branch can leave a prod DB whose real platform-root org
// has an id other than ROOT_ORG_ID and NO ROOT_ORG_ID row at all (the
// 2026-07-29 rebuild: 92f2020b-…, slug `fuzefront`). Hardcoding
// ROOT_ORG_ID then made every INSERT violate
// organization_memberships_organization_id_foreign on every boot.
//
// 022 now resolves the root org exactly as ensureRootPortal() does β€”
// prefer ROOT_ORG_ID, else the oldest type='platform' row β€” so a
// divergent platform org IS the root org, and the backfill must run
// against ITS id. #680 changed the migration and left this guard
// asserting the old behaviour, which is why Backend Tests went red at
// 05:15Z on 2026-08-26.
const { knex, raws } = makeKnex({
users: [{ id: PLATFORM_REGISTRAR_ID }],
organizations: [{ id: 'legacy-platform-id', slug: 'legacy', type: 'platform' }],
})

await migration022.up(knex)

const inserts = membershipInserts(raws)
expect(inserts).toHaveLength(1)
// The crux: bound to the ADOPTED org, never to the hardcoded constant.
// This is the exact regression #680 fixed.
expect(inserts[0].bindings).toContain('legacy-platform-id')
expect(inserts[0].bindings).not.toContain(ROOT_ORG_ID)
})

it('skips the backfill when NO platform org exists at all, even with users present', async () => {
// The genuine "root organization does not exist" case post-#680, and the
// coverage the inverted assertion above was standing in for. Users are
// present deliberately: the empty-fixture test below has none, so it
// cannot distinguish "skipped the backfill" from "had nobody to backfill".
const { knex, raws } = makeKnex({
users: [{ id: PLATFORM_REGISTRAR_ID }],
organizations: [{ id: 'some-org', slug: 'some-org', type: 'organization' }],
})

await migration022.up(knex)

expect(membershipInserts(raws)).toHaveLength(0)
})

Expand Down
Loading
Loading