Skip to content

platform's GitHub configuration creates overnightworks/figure #312

Closed

Description

@FlexOr2

platform's GitHub configuration creates overnightworks/figure

Caller: overnightworks/hopin#629, card line 4, ruled A by the operator on 09.11.2026. Its outline puts "figure's repository" right after P0, and FG1a (bootstrap, container rules, figure check) waits on it. The operator, in chat to hopin's head: „also ich hab a entschieden! aber figure wäre der richtige name und nciht character?“

Blocked by #307. Today every plan of the root fails its listing preconditions, because agents is personas on GitHub. This slice is built after #307 has landed and been applied, and its fixtures name personas.

Ruled

  • hopin#629, ruled sentence 11: "The figure package lives in a new repository named figure, which every app takes (card line 4, A)."

  • hopin#629, card line 4, option A ("Eigenes Repo figure"), its contract word for word:

    A new repository overnightworks/figure, created through platform's GitHub configuration, ships framework-free TypeScript ESM with three and @pixiv/three-vrm as its dependencies. It owns the fleet's VRM 1.0 profile, its check command figure check (npm overnightworks-figure), and drawing and driving a figure in any browser. It is released as a tarball on an immutable release through quality's release gate and pinned by URL, like appkit. Its first callers are hopin's agent tile, presentator's co-presenter and the CI of personas (today's overnightworks/agents). The accept field is figure: {url, sha256}, and every name the plan writes as character becomes figure. Source: hopin#629 comments 6741826135 and 6741910486; the name from personas' head after the operator's doubt about character, 09.11.2026.

  • Names (hal docs/naming.md, by git show origin/main only): the repository is the stem figure, and the Sonar project key is overnightworks_figure.

  • Every fleet repository's GitHub settings and the organisation rulesets live as code in platform #10, line 3, ruled B on 06.11.2026: "Every managed repository carries lifecycle destroy = false and archive_on_destroy = true … a removed key is forgotten, never destroyed". figure falls under it from its first apply.

  • docs/github-settings.md, Platform on k3s and Flux: from an empty PC to every app live, with hal archived #1 to Bump actions/checkout from 6.1.0 to 7.0.1 #6. The slice makes none of them false and changes nothing they require. The apply goes through Add the repository skeleton and the fleet's quality gate #3 to Bump astral-sh/setup-uv from 10.0.1 to 10.3.3 #5.

What it builds

Change 1, the create (in src/opentofu/github/repositories.tf):

  1. A created map beside managed and unmanaged. It maps figure to its description, and nothing else is in it. A new local declared holds managed plus the keys of created.
  2. Both resources (github_repository.managed and github_repository_vulnerability_alerts.managed) iterate over declared. Their addresses stay …managed["figure"].
  3. The import blocks stay over local.managed, unchanged. A typo in managed still fails the import and creates nothing, and every existing repository still recovers from a lost state by import.
  4. Six attributes are set only for a key of created. Every other key gets null, and ignore_changes keeps them out of every later plan:
    • description, from the map;
    • visibility = "public";
    • has_issues = true;
    • auto_init = true;
    • has_wiki = true and has_projects = true, which every fleet repository carries as GitHub's default; the provider sends false for an unset one, so leaving them out would make figure the one repository without them.
  5. One new precondition on output.managed_repositories: a name in created that the listing holds (archived, fork or not) fails the plan, naming it. The message tells you to move it to managed. This keeps a name from being created twice. It also stops a plan whose name was created by hand before the apply, instead of failing halfway through the apply on GitHub's 422.
  6. undeclared_repositories subtracts declared instead of managed.
  7. The output's value becomes sort(tolist(local.declared)).
  8. The import comment "so no repository is created" now says that a name in managed is never created and only a name in created is.

Size: one production file, about 25 changed production lines. Tests: one new run in tests/repository_listing.tftest.hcl and a new tests/repository_creation.tftest.hcl with one run (below).

Change 2, the fold, is an item of its own (#313), cut together with this one and blocked by it, since a pull request lands one item and the fold can only follow the apply. It is dispatched the moment this item's apply has run, and no other apply of the root runs between this apply and the fold's landing, since every plan fails on the new precondition until then. What it does:

  • figure moves from created into managed, and the listing fixtures gain it.
  • created, declared, the four conditional attributes, the precondition and the creation test are deleted. The tree goes back to import-only, and the comment goes back to "so no repository is created".
  • The mechanism stays recoverable from git, and the next new repository brings it back from this item's first commit. An empty created would be an extension point with no caller.
  • The plan after the fold must exit 0. The import blocks then name figure, which is already in the state, so they are a no-op. The vulnerability alerts' import id is the name while the state holds the numeric id; the 15 existing repositories plan empty the same way since the apply of 08.11.2026.

Alternatives weighed:

  • (a) A separate created set, which import and listing checks skip, folded into managed later.
    • Chosen, in the form above.
    • Costs: about 25 lines that live for a few hours, and a second small pull request.
    • Leaves behind: nothing after the fold.
    • Keeps every guarantee the brief names. Between the apply and the fold, every plan of the root fails on the new precondition, which forces the fold.
  • (b) One set, with the import skipped for names the listing does not hold.
    • Rejected. A typo in managed would silently create a repository, which is exactly the safeguard the import gives today.
    • It also needs unmanageable_names dropped, and an import for_each that depends on a data source. Nobody has verified that OpenTofu supports that dependency, and tofu test cannot exercise import blocks.
  • (c) An agent runs gh repo create by hand; the slice only adds figure to managed.
    • The cheapest path: one pull request, no mechanism.
    • It does not meet the ruled sentence "created through platform's GitHub configuration", so it needs the operator's new word.
    • It also leaves a window in which the repository has the organisation's defaults for new repositories: secret scanning, push protection and Dependabot alerts are off for new repositories in this organisation, and merge commits are allowed. In that window, a hand step that no review saw sets visibility, issues and the first commit.
  • (d) Keep an empty created after the fold as a standing create path.
    • Rejected under the code policy: there is no current caller.
    • The operator may rule a standing path if creations keep coming. platform-secrets (06.11) and figure (09.11) came in one week.

Attributes on create, and what provider 6.13.0 sends. ignore_changes does not apply to a create; the configured values are sent. Sources: github/resource_github_repository.go resourceGithubRepositoryObject (587–652), resourceGithubRepositoryCreate (654–786), resourceGithubRepositoryUpdate, calculateVisibility (523–537); go-github v88 createRepoRequest (github/repos.go 508–600), whose pointer fields send an explicit false.

Attribute Declared What 6.13.0 sends Why
visibility "public" POST visibility: public; without it, calculateVisibility also falls back to public The ruling says "pinned by URL, like appkit"; appkit is public (gh api repos/overnightworks/appkit). A release asset of a private repository cannot be fetched by URL without a token, which hopin's build, presentator and personas' CI would then need. Secret scanning and push protection are free only on a public repository (#34).
has_issues true POST has_issues; the schema has no default, so without it false is sent and issues are off FG1a and the rest of the outline are issues in figure; every fleet repository has issues on.
auto_init true POST auto_init: true, so GitHub writes a first commit (README) on main FG1a needs a default branch to open a pull request against. Whether main: PR only would refuse a first direct push into an empty repository is not verified; the first commit avoids the question.
default_branch not set Create refuses anything but main; Update never sends it on a new resource GitHub names the first branch main.
description from created POST description; without it, "" It is sent only at the create; afterwards it stays as set on GitHub (ignore_changes), so figure's head may change it.
allow_squash_merge true, merge commit and rebase false, delete_branch_on_merge true as for every repository POST, then the same in the PATCH that Create ends with Same as the fleet, from the first moment.
security_and_analysis (secret scanning, push protection) the existing dynamic block (figure is not in listed_private) Not in the POST; the PATCH after the create carries it, since HasChange("security_and_analysis") holds on a new resource The organisation does not enable it for new repositories (secret_scanning_enabled_for_new_repositories: false).
Dependabot alerts github_repository_vulnerability_alerts.managed["figure"], enabled = true PUT /repos/…/vulnerability-alerts, and the state id is the repository's numeric id The organisation's default for new repositories is off.
has_wiki, has_projects, has_discussions, has_downloads not set false (no schema default) Off on figure, while older repositories carry GitHub's default on; nothing in the fleet uses them. The head may add them as true before the build if the fleet should look the same.
archive_on_destroy, destroy = false as for every repository Written into the state by the apply #10 line 3. A created object carries destroy = false in its state from the first apply, unlike an import (GH-6).
topics not set none hal docs/naming.md: topics are never set by hand, and are not set at all until edge#30.

What reaches figure without this slice:

  • Rulesets. main: PR only and release tags are immutable target ~ALL.
  • Immutable releases. The organisation enforces them for all repositories: orgs/overnightworks/settings/immutable-releases answers enforced_repositories: all, and appkit, speech-runtime, quality and personas report enabled: true, enforced_by_owner: true.
  • quality_gate. It is required with the default false, so quality / verdict required does not reach figure until quality's merge command sets true.
  • Quality's onboarding stays outside platform: the property value, role (library, as appkit), the Sonar project overnightworks_figure, CodeQL through quality's codeql.yml (which Every fleet repository's GitHub settings and the organisation rulesets live as code in platform #10's G4 rule will then require), and the release gate.

Proof

  • CI. mise run github:test in CI's opentofu job is green (format, init against the lock, validate, the mocked tests, the delete-refusing test).
    • New listing run: a_repository_to_create_that_the_listing_holds_fails.
    • New creation run: a_repository_to_create_is_public_with_issues_and_a_first_commit. It targets github_repository.managed["figure"] and github_repository_vulnerability_alerts.managed["figure"] with a full listing, and asserts visibility == "public", has_issues, auto_init, the description equal to created's, both scanning statuses enabled and the alerts enabled.
  • Plan before the pull request (builder). The builder runs scripts/apply_github_settings.sh on the lane's commit. Like platform's GitHub configuration manages personas, the repository agents became #307, it is a plan with the operator's gh login and writes no state. Its summary goes into the journal: exactly 2 to add (github_repository.managed["figure"], github_repository_vulnerability_alerts.managed["figure"]), 0 to change, 0 to destroy, no import and no forget, and the output change. Anything else stops the lane.
  • Apply after the landing (agent). An agent runs scripts/apply_github_settings.sh --apply from a clean origin/main; the plan and apply output go into the journal. Then scripts/apply_github_settings.sh must exit 1 on exactly the new precondition, naming figure.
  • Read-back with gh api (GET only), recorded in the journal:
    • repos/overnightworks/figure: visibility, has_issues, default_branch, the merge settings, the description, security_and_analysis;
    • repos/overnightworks/figure/vulnerability-alerts: 204;
    • repos/overnightworks/figure/commits?sha=main: one commit;
    • repos/overnightworks/figure/rules/branches/main: deletion, non_fast_forward, pull_request;
    • repos/overnightworks/figure/rulesets: both ~ALL rulesets;
    • repos/overnightworks/figure/immutable-releases;
    • repos/overnightworks/figure/properties/values: quality_gate false.
  • Fold, the fold item's proof. Its pull request is green in CI. Its pre-PR plan and the plan after its landing both exit 0.
  • Pre-check (09.11.2026). A scratch copy of origin/main, with platform's GitHub configuration manages personas, the repository agents became #307's rename and change 1 applied, passed all 15 mocked runs under tofu test, the two new runs included. So targeting the one for_each instance that has no import block works beside the import blocks of the other instances. tofu fmt asked only for alignment.

Trace (main e375b80)

Risk

Material: an outward-facing, public creation that nothing deletes. The repository name is taken for good, and it is public from its first commit. The lane gets an Opus review at high effort and a cross-provider final gate. The first review drives this matrix:

# Sentence Cell Driven by
1 The plan creates exactly figure's two resources and changes nothing else the builder's pre-PR plan summary: 2 add, 0 change, 0 destroy, no import or forget builder, reviewer reads the journal
2 A typo in managed still cannot create a repository the diff leaves both import for_each on local.managed; the existing run a_managed_repository_missing_from_the_listing_fails still passes reviewer, CI
3 A name in created that the listing holds fails the plan the new mocked run; the live plan after the apply exits 1 naming figure CI, agent
4 Created public, issues on, first commit on main, the description, squash-only, delete-branch-on-merge the creation run; the gh api read-back CI, agent
5 Secret scanning, push protection and Dependabot alerts on from the create the creation run; read-back of security_and_analysis and vulnerability-alerts 204 CI, agent
6 The organisation rulesets and immutable releases reach figure, and quality / verdict does not until quality_gate=true read-back of rules/branches/main, rulesets, immutable-releases, properties/values agent
7 Nothing deletes it: no delete is ever planned, destroy = false is in its state, and a removal is a forget the delete-refusing test (CI); the reviewer reads the state's figure entry after the apply for skip_destroy/destroy = false (names and flags only) CI, agent
8 The apply's own output preconditions hold (the listing is read at plan time, not again at apply) --apply exits 0 agent
9 After the fold, a plan is empty and figure recovers by import like every other name the plan after the fold exits 0; a lost-state import of figure is not driven, since it is the same import block that 15 names pass agent; row 9's second half not driven, reason named

Rollback

A created repository is never deleted (#10 line 3). The login has no delete_repo scope, and the apply refuses every delete (GH-5).

  • Before the apply: reverting change 1 changes nothing on GitHub.
  • After the apply: a revert removes figure from created. The plan forgets both resources, because the apply wrote destroy = false. The repository stays on GitHub, and undeclared_repositories keeps every plan red until figure is in managed or in unmanaged with a reason.
  • Archiving or deleting the repository is the operator's own hand step, on his word.
  • What stays in any case: the name, the first commit, and anything pushed since.

Done when

version = 1
now = "Drafted on 09.11.2026 from hopin#629 line 4 (ruled A): figure is created through platform's GitHub configuration. Blocked by #307, since every plan of the root fails until agents is personas in managed."
next = "Once #307 has landed and been applied, claim and build change 1 (created map, precondition, two mocked runs) with the builder's pre-PR plan; an agent applies after the landing; then the fold item is dispatched."
done_when = "Change 1 is landed with a pre-PR plan of exactly figure's two creates; an agent applied it from a clean origin/main and the gh api read-back matches; the fold item is dispatched; #10's Safeguards sentence is corrected."
scope = ["src/opentofu/github/repositories.tf", "src/opentofu/github/tests/repository_listing.tftest.hcl", "src/opentofu/github/tests/repository_creation.tftest.hcl"]

Activity

  1. added theissue type on Nov 9, 2026
  2. FlexOr2 commented on Nov 9, 2026

    @FlexOr2
    MemberAuthor

    Landed in #332 as 869856f. The head's plan from origin/main at 869856f named exactly github_repository.managed["figure"] and github_repository_vulnerability_alerts.managed["figure"] (2 to add, 0 to change, 0 to destroy); --apply completed with 2 added; read-back: overnightworks/figure public squash=true merge=false rebase=false delete_branch=true archived=false, vulnerability alerts 204. The fold #313 is dispatched; #10's Safeguards sentence is corrected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions