Skip to content

t1k:jenkins-unity

FieldValue
Modulet1k-devops
Version1.4.1
Effortmedium
Tools—

Keywords: android build, auto-detect, basejob, build job, cheat flags, debug flags, define symbols, develop release creative, discord thread, jenkins, new game, onboarding, project detection, three jobs, unity, unityandroid

/t1k:jenkins-unity
<git-ssh-url> [--project <ProjectName>]

Jenkins Unity Onboarding — three jobs per new game

Section titled “Jenkins Unity Onboarding — three jobs per new game”

Given a new Unity game repo, create <ProjectName>-Develop, <ProjectName>-Release, and <ProjectName>-Creative in Jenkins folder UnityAndroid on https://jenkins.the1studio.org, each cloned from UnityAndroid/BaseJob. This skill is a caller, not a fork: every server round-trip goes through ~/.claude/skills/t1k-jenkins-job/scripts/jenkins-job.cjs (t1k:jenkins-job, protected: true — read it, cite it, never edit it). The delta this skill owns is the Unity-specific per-job parameter matrix, the fixed three-job shape, the Creative define append (;CREATIVE;THEONE_HIDE_FPS), define-symbol append, an independent second-opinion verifier (scripts/verify-params-independent.cjs) that shares no code with the engine script, and — since v0.2.0 — Unity-project auto-detection (scripts/detect-unity-project.cjs) that pre-fills Step 1 when run inside a Unity checkout.

The full per-job contract lives in references/param-matrix.md — read it before generating anything. BaseJob’s parameter shape (including the trap parameter) is in references/basejob-anatomy.md. Auto-detection’s algorithm, edge cases, and the two-mode contract (detected vs. fallback define symbols) are in references/project-detection.md.

Two credential sets, same server, different env-var names for the same account:

ToolEnv vars
jenkins-job.cjs (this skill’s engine — every call below)JENKINS_URL, JENKINS_USER, JENKINS_TOKEN
Jenkins MCP registration (t1k-mcp-management → scripts/install-jenkins.sh)JENKINS_URL, JENKINS_USERNAME, JENKINS_API_TOKEN

Exporting the MCP’s names and running jenkins-job.cjs (or vice versa) fails with a 401 that reads exactly like a bad token — it is a name mismatch, not a bad credential. Credentials come from env only, never as argv, never echoed to the user.

The Jenkins MCP Server plugin has no job-creation tool — it is read + trigger only (verified against the plugin’s full tool list). Creation always goes through POST /createItem via jenkins-job.cjs create. The MCP is useful only after creation (triggerBuild/getBuild/ getBuildLog), and its tools load only at session start — a session that did not start with the MCP registered cannot use them mid-session. Treat MCP verification as optional, never a gate.

S=~/.claude/skills/t1k-jenkins-job/scripts/jenkins-job.cjs, V=~/.claude/skills/t1k-jenkins-unity/scripts/verify-params-independent.cjs (this skill’s own independent verifier), and D=~/.claude/skills/t1k-jenkins-unity/scripts/detect-unity-project.cjs (the auto-detection script) below — all three absolute. A relative scripts/...cjs resolves against whatever the shell’s cwd happens to be at run time — which for $D specifically is guaranteed not to be this skill’s own directory, since Step 0 deliberately sets cwd to the user’s Unity project. Every node $D … / node $V … / node $S … call below MUST use the variable, never a bare relative path, or it fails MODULE_NOT_FOUND.

Step 0 — Unity project auto-detection (accelerator, never a requirement)

Section titled “Step 0 — Unity project auto-detection (accelerator, never a requirement)”

Run node $D detect (cwd = the directory the user is invoking from, or an explicit --cwd/--project-dir). Full algorithm, edge cases, and the two-mode define-symbols contract: references/project-detection.md.

statusAction
not-a-unity-projectFall back to Step 1 entirely, asking for everything. This is the default outcome outside a Unity checkout — never a failure.
multiple-candidatesAskUserQuestion which of the listed candidates — never silently pick the first — then re-run detect --project-dir <chosen>.
okProceed below. Show the user EVERY detected field (projectName, repoUrl, unityVersion, branch, defineSymbolsAndroid, and any warnings) and fold them into Step 1’s AskUserQuestion as proposed, editable defaults — never silent auto-fill. The jobs are not cheaply reversible; propose-then-confirm always.

Surface every warnings entry verbatim — see Gotchas #12/#15 for the two that matter most (Unity-tool-name vs. editor version, and branch representativeness).

Step 1 — input gathering (one batched AskUserQuestion, 4 questions, + a conditional keystore round)

Section titled “Step 1 — input gathering (one batched AskUserQuestion, 4 questions, + a conditional keystore round)”
  1. Git repo URL, SSH form (git@github.com:The1Studio/<Project>.git). Skip if given as an argument, or pre-fill from Step 0’s repoUrl and ask the user to confirm.
  2. <ProjectName> — propose derived from the repo name (strip .git, strip a leading Unity prefix) or from Step 0’s detected projectName when available, ask the user to confirm or correct. It becomes both the job-name prefix and PARAM_BUILD_FILE_NAME — a wrong value is wrong in two places.
  3. Three Discord thread IDs, one per job, explicitly labelled Develop / Release / Creative.
  4. Keep or strip the GitHubPushTrigger. BaseJob carries one; every sampled real job kept it. Kept, every push to a tracked branch starts a build. State that consequence in the question.

Conditional 5th round — keystore. AskUserQuestion takes at most 4 questions per call, so the keystore goes in a second call, made only when the project ships its own signing key: check the detected project dir for a *.keystore file. Found one → ask for PARAM_KEYSTORE_NAME (propose the discovered filename with its extension), PARAM_KEYSTORE_ALIAS_NAME, and the two passwords. Found none → inherit BaseJob’s shared studio key and say so in the report, so an inherited signing key is always a stated decision rather than a silent default. Never guess a password and never carry one over from a sibling job. Contract: param-matrix.md § “Keystore parameters are PER-PROJECT”.

Inherited verbatim from t1k:jenkins-job § “Never inherit a thread ID”. Discord thread creation is out of scope — this skill never calls the Discord API. It asks for three existing IDs and never reuses a value because it appeared in BaseJob, a sibling job, a previous run, or earlier in the conversation. If the user does not have the IDs yet, stop and wait — do not create jobs with a placeholder intending to fix it later; a job posting to the wrong thread is worse than a job that doesn’t exist, and creation is not cheaply reversible (see § “Failure handling — rollback is update, never delete” below). The three IDs must be pairwise distinct — if the user supplies a duplicate, ask again.

Step 2 — pre-flight probes (all read-only; report every result in one table before generating anything)

Section titled “Step 2 — pre-flight probes (all read-only; report every result in one table before generating anything)”
#ProbeCommandFailure handling
1Credentials presentassert JENKINS_URL/JENKINS_USER/JENKINS_TOKEN non-emptyHard stop. Never echo the values.
2Name collisionnode $S list UnityAndroid --grep <ProjectName>Compare case-insensitively — UnityAndroid holds 222+ jobs and Jenkins names are case-sensitive, so Foo-Develop/foo-Develop can coexist. Any hit → stop and ask.
3Branch existencegit ls-remote --heads <url> develop 'release*' creativeReport which of the three are missing — Release matches the job’s wildcard specifier */release*, so any release-<ver> branch counts as present. Warning, not a stop — a branch may be created later; createItem accepts any specifier, and a missing branch surfaces only as a confusing checkout error on first build.
4BaseJob reachablenode $S get-config UnityAndroid BaseJob --out <tmp>/basejob.xmlHard stop on non-200. This file is Step 3’s input regardless of mode (boolean flags, keystore, scriptPath always come from here); it is the define-symbol source ONLY in fallback mode — in detected mode, Step 0’s defineSymbolsAndroid is the source instead (see project-detection.md).
5Independent read-backnode $V --config <tmp>/basejob.xml --self-testHard stop. Confirms the fetched template is the one this skill’s contract assumes, before anything is derived from it.

This phase must NOT create/update/POST anything, and must NOT create a scratch/test job “to check permissions” — Job/Delete is unproven for this account (a doDelete GET probe returned 405, which gates on HTTP method before permission and settles nothing); permission is proven by creating the first job the user actually wants (Step 4), not by a throwaway.

Step 3 — generate three job configs, offline

Section titled “Step 3 — generate three job configs, offline”

gen makes no network call. Full command block and per-job verification table: references/param-matrix.md. Sourcing PARAM_UNITY_SCRIPTING_DEFINE_SYMBOLS differs by mode (same reference doc, “DETECTED” vs “ORIGINAL FALLBACK” tables) — detected mode replaces BaseJob’s list with the project’s own, filtered per job:

  • Develop — Step 0’s defineSymbolsAndroid, unfiltered (debug/cheat flags kept on purpose).
  • Release — node $D strip-debug-flags --list "<detected>", then node $D release-append --list "<stripped>", which appends PRODUCTION (hides the FPS overlay and debug UI; takes Adjust + AppsFlyer out of sandbox mode — com.gdk.3rd/Analytics/*Tracker.cs gate on #if THEONE_MMP_DEBUG && !PRODUCTION). Like the Creative symbols it is in no ProjectSettings.asset, so detection cannot surface it and omitting it fails silently — the build succeeds having compiled the wrong path. It is an invariant: never conditional, never asked about. Print the stripped array even when empty (“stripped: none”) — a silent filter is a filter nobody re-checks, and the denylist (references/debug-flag-denylist.json) is a seed list that will go stale; the printed report is the user’s one chance to catch a flag it doesn’t know about. Print appendedSymbols too.
  • Creative — the same strip-debug-flags output, then node $D creative-append --list "<stripped>", which appends both Creative symbols — CREATIVE (strips ad UI, force-unlocks content) and THEONE_HIDE_FPS (suppresses the on-screen FPS counter, which a playable ad must never show; UITemplate’s Fps.cs and UITemplateVContainer.cs gate on #if PRODUCTION || THEONE_HIDE_FPS). Idempotent per symbol on the ;-split list (never a substring match), so a project whose Android list already contains CREATIVE gets THEONE_HIDE_FPS topped up rather than CREATIVE double-appended. Print the appendedSymbols array — a partial append is the interesting case, and a silent one reads identically to a full one.

Fallback mode (references/param-matrix.md § “ORIGINAL FALLBACK”) differs only in the input list: Develop inherits BaseJob’s value, while Release and Creative run BaseJob’s live value through the same release-append / creative-append step. The role appends apply in both modes — they are never inherited, because BaseJob carries none of the three symbols.

BLOCKING GATE — prove no cross-parameter write, before Step 4 makes any live call

Section titled “BLOCKING GATE — prove no cross-parameter write, before Step 4 makes any live call”

Governed by rules/workflow-gates.md. Override: none.

PARAM_DISCORD_THREAD_ID has no <defaultValue> element at all (see basejob-anatomy.md). A locator that walks to “the next <defaultValue>” writes the thread ID into the following parameter, PARAM_SHOULD_RESET_JENKINS_PARAMS, and destroys its false — silently, and it verifies green whenever read and write share the locator.

Pass conditions — all four, evidence not assertion:

  1. node $V --config <tmp>/develop.xml --expect PARAM_DISCORD_THREAD_ID=<id-develop> → green.
  2. $V reports PARAM_SHOULD_RESET_JENKINS_PARAMS=false on all three generated XMLs — the canary.
  3. diff <(xmllint --format $B) <(xmllint --format <tmp>/develop.xml) differs only in the intended elements (the --set parameters, <url>, the branch <name>, and the triggers block if --strip-triggers was used). Read every hunk; do not count them.
  4. The Release gen call’s own stdout log (log.join('\n'), printed by gen for every mutation it applied) contains no line at all for PARAM_SHOULD_BUILD_DEVELOPMENT, PARAM_SHOULD_OPTIMIZE_BUILD_SIZE, or PARAM_SHOULD_BUILD_APP_BUNDLE. Check the log, not the resulting XML — when BaseJob’s own default already equals the value you’d otherwise --set, an explicit same-value --set and no --set at all produce byte-identical XML (confirmed with cmp), so inspecting release.xml cannot tell inherit-by-omission apart from a same-value override. The log can: it lists exactly the parameters that were --set, so an absent entry is the only real proof no --set was passed for these three.

On failure: stop. Create nothing. Report which condition failed with the offending diff hunk via AskUserQuestion — never patch around it silently. If jenkins-job.cjs’s locator is genuinely broken, that is a theonekit-core maintainer fix (the script is protected: true), and this work waits.

Step 4 — preview-first create, then the rest

Section titled “Step 4 — preview-first create, then the rest”

Per rules/preview-first-batch.md. Entry condition: the gate above passed.

  1. Create exactly one job: node $S create UnityAndroid <ProjectName>-Develop --from <tmp>/develop.xml. -Develop is the smoke target because it’s a job the user actually wants — never a scratch job. createItem returns 200 with an empty body on success; that is not failure, but a 200 also does not prove the config landed — always verify.

  2. Verify it twice, with two different parsers, and NOT the same assertions:

    Terminal window
    node $S verify UnityAndroid <ProjectName>-Develop \
    --expect PARAM_DISCORD_THREAD_ID=<id-develop> --expect PARAM_BUILD_FILE_NAME=<ProjectName> \
    --expect PARAM_SHOULD_BUILD_DEVELOPMENT=true --expect PARAM_SHOULD_OPTIMIZE_BUILD_SIZE=false \
    --expect PARAM_SHOULD_BUILD_APP_BUNDLE=false \
    --expect-repo "<url>" --expect-triggers <yes|no> # the user's Step 1 Q4 answer
    node $V --fetch UnityAndroid <ProjectName>-Develop \
    --expect PARAM_DISCORD_THREAD_ID=<id-develop> --expect PARAM_BUILD_FILE_NAME=<ProjectName> \
    --expect PARAM_SHOULD_RESET_JENKINS_PARAMS=false \
    --expect-repo "<url>" --expect-branch '*/develop'

    Both must be green. Branch is checked by $V alone. jenkins-job.cjs verify has no --expect-branch flag at all — passing one is accepted by its generic argument parser and then silently never read, so it would print “all expectations met” whether the branch is right or wrong. Never rely on the engine’s verify for branch; always pass --expect-triggers to it instead, since that IS one it actually checks — matching the user’s keep-or-strip answer from Step 1 Q4, so a silently-broken --strip-triggers (its regex missing and no-opping, reported as "(already empty)") is caught here rather than going unnoticed.

  3. Print both tables and the job URL, then AskUserQuestion: proceed with -Release/-Creative, re-check something, correct via update, or stop. A clean smoke is not auto-permission — ask even when everything is green.

Then create the remaining two, and run both verifiers against all three jobs in one final table using the per-job assertions in references/param-matrix.md — engine verify with --expect-triggers <yes|no> on every job (the trigger decision, verified), $V with --expect-branch on every job (branch, verified — the engine cannot). Define-symbol checks are always string equality, never substring.

Optional, never a gate: trigger a build via the Jenkins MCP if it is registered this session (triggerBuild/getBuild/getBuildLog) to confirm the job actually builds.

State explicitly: the three job names + URLs; every parameter left at BaseJob’s default that is arguably per-project (PARAM_DISCORD_WEBHOOK_URL, PARAM_UNITY_TOOL_NAME, PARAM_BUILD_VERSION, the keystore set); the trigger decision and its consequence; any branch reported missing by probe 3. Silence about an inherited value reads as a decision nobody made.

Failure handling — rollback is update, never delete

Section titled “Failure handling — rollback is update, never delete”
  • Wrong parameter value → node $S update UnityAndroid <name> --set PARAM=value --dry-run first, then for real, then re-verify with both parsers.
  • Wrong job name → cannot be removed by this skill (Job/Delete unproven). Report plainly and ask a Jenkins admin. Never delete-and-recreate, never leave a duplicate silently.
  • Never --set a keystore parameter to fix anything — a value POSTed into a PasswordParameterDefinition is stored verbatim and readable by anyone with Job/Read. BaseJob’s encrypted defaults clone safely within this instance; keep them untouched.
  1. PARAM_DISCORD_THREAD_ID has NO <defaultValue> element in BaseJob. Jenkins omits the element entirely for an empty default. A “find the name, take the next <defaultValue>” regex writes into the next parameter (PARAM_SHOULD_RESET_JENKINS_PARAMS) and silently destroys it. jenkins-job.cjs’s findParamBlock handles this — verify with an independent parser anyway, because read and write sharing one broken locator produces a self-consistent lie.
  2. --set on a define-symbol list is a whole-value replace, not an append. The ;CREATIVE;THEONE_HIDE_FPS append must read BaseJob’s live current value first, then pass the full concatenated string.
  3. Keystore params are PER-PROJECT, and Jenkins DOES encrypt a password POSTed via config.xml. Two corrections. (a) BaseJob’s the1_googleplay.keystore / theonestudio is the shared-template key — a title with its own *.keystore must set all four explicitly, or it signs with the wrong key and nothing notices until the store rejects the upload. (b) The old “stored verbatim, re-encrypt through the UI” claim is false: verified 2026-08-26, three jobs written with a plaintext password each read back a distinct fresh {AQAA…} ciphertext, length prefix AAAAQ→AAAAg. No UI re-save needed. Keep the plaintext out of shell history, and remember you cannot read the value back — verification proves the ciphertext changed, not that it is correct.
  4. Job/Delete is UNPROVEN for this account. A doDelete GET probe returned 405, which gates on method before permission and settles nothing. Never create a scratch/test job to try something out — smoke-test on a job the user actually wants.
  5. UnityAndroid holds 222+ jobs. Collision-check case-insensitively; Jenkins names are case-sensitive, so X and x can coexist.
  6. The CSRF crumb must travel with its session cookie — jenkins-job.cjs handles it; a hand-rolled curl needs one cookie jar (-c/-b) across both requests.
  7. createItem returns 200 with an empty body on success, and a 200 does not prove the config landed. Always verify.
  8. Real jobs have drifted from BaseJob. Sampled branch specifiers: Arrow3D’s three jobs all use master; ColorFit uses */develop, origin/release, */creative. Define-symbol lists differ per project too. Read sibling jobs for XML structure, never for values.
  9. Credential env-var names differ between tools for the same credential — see § Setup. The mismatch presents as a 401 that looks like a bad token.
  10. The Jenkins MCP cannot create jobs — read + trigger only; creation is always createItem. Its tools load only at session start, so post-create MCP verification is optional, never a gate.
  11. GithubProjectProperty’s <projectUrl> is not corrected by gen. A cloned job’s cosmetic “GitHub project” link still points at BaseJob’s own repo even though the actual git checkout remote (<userRemoteConfigs>) is set correctly. Cosmetic only — builds check out the right repo — but note it if the user asks why the job’s GitHub link looks wrong.
  12. Detected Unity version ≠ Jenkins tool name — ColorFit’s real job stores Unity 6000.3.13f1 with a literal prefix. Confirm before --seting it (project-detection.md).
  13. ProjectSettings.asset’s define-symbols keys can contain spaces (Nintendo Switch:, Windows Store Apps:) — Android:’s value needs a key-anchored scan, not a block regex.
  14. The debug/cheat denylist is a seed list and WILL go stale. Always print the strip report per job, including “stripped: none” — the user’s one chance to catch a flag it doesn’t know.
  15. A checked-out branch’s symbols are not guaranteed representative of any job — verified live on Arrow3D, whose checkout (releaseios-1.0.5) is none of develop/release/creative. Print the branch alongside the symbols.
  16. PRODUCTION on Release is an invariant. It lives in no ProjectSettings.asset — it exists to make the CI build differ from a local one — so detection can never surface it and a missing append fails silently: the build succeeds, compiling the wrong path. Sampled evidence: 3/3 Release jobs carry it, 0/6 others do, BaseJob does not. Compute it with detect-unity-project.cjs release-append, never by hand. See param-matrix.md § “The Release define append”.
  17. Verify role-append isolation across the three jobs, not just within one. PRODUCTION must be on Release and on neither sibling; CREATIVE/THEONE_HIDE_FPS on Creative and on neither sibling. A per-job check passes happily when one list was pasted into two jobs — real drift of exactly this shape exists in UnityAndroid (Arrow3D-Develop carries CREATIVE).
  • ~/.claude/skills/t1k-jenkins-job/SKILL.md — the engine this skill calls; read its “Never inherit a thread ID” and “Batch rules” sections in full. protected: true.
  • references/param-matrix.md — the full per-job contract, generation commands, Creative-append algorithm, and verification assertion table.
  • references/basejob-anatomy.md — every BaseJob parameter, in document order, with the trap and the canary called out.
  • references/project-detection.md — auto-detection algorithm, edge cases, and the detected-vs- fallback define-symbols contract.
  • references/debug-flag-denylist.json — the SSOT for which symbols strip-debug-flags removes; extend it here, in one edit, not in prose or code.
  • rules/preview-first-batch.md — the Step 4 smoke-one-then-ask gate.
  • rules/workflow-gates.md — the <HARD-GATE> contract both gates above cite.