t1k:jenkins-unity
| Field | Value |
|---|---|
| Module | t1k-devops |
| Version | 1.4.1 |
| Effort | medium |
| 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
How to invoke
Section titled “How to invoke”/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:
| Tool | Env 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.
status | Action |
|---|---|
not-a-unity-project | Fall back to Step 1 entirely, asking for everything. This is the default outcome outside a Unity checkout — never a failure. |
multiple-candidates | AskUserQuestion which of the listed candidates — never silently pick the first — then re-run detect --project-dir <chosen>. |
ok | Proceed 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)”- Git repo URL, SSH form (
git@github.com:The1Studio/<Project>.git). Skip if given as an argument, or pre-fill from Step 0’srepoUrland ask the user to confirm. <ProjectName>— propose derived from the repo name (strip.git, strip a leadingUnityprefix) or from Step 0’s detectedprojectNamewhen available, ask the user to confirm or correct. It becomes both the job-name prefix andPARAM_BUILD_FILE_NAME— a wrong value is wrong in two places.- Three Discord thread IDs, one per job, explicitly labelled Develop / Release / Creative.
- 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”.
Never inherit a thread ID
Section titled “Never inherit a thread ID”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)”| # | Probe | Command | Failure handling |
|---|---|---|---|
| 1 | Credentials present | assert JENKINS_URL/JENKINS_USER/JENKINS_TOKEN non-empty | Hard stop. Never echo the values. |
| 2 | Name collision | node $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. |
| 3 | Branch existence | git ls-remote --heads <url> develop 'release*' creative | Report 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. |
| 4 | BaseJob reachable | node $S get-config UnityAndroid BaseJob --out <tmp>/basejob.xml | Hard 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). |
| 5 | Independent read-back | node $V --config <tmp>/basejob.xml --self-test | Hard 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>", thennode $D release-append --list "<stripped>", which appendsPRODUCTION(hides the FPS overlay and debug UI; takes Adjust + AppsFlyer out of sandbox mode —com.gdk.3rd/Analytics/*Tracker.csgate on#if THEONE_MMP_DEBUG && !PRODUCTION). Like the Creative symbols it is in noProjectSettings.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 thestrippedarray 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. PrintappendedSymbolstoo. - Creative — the same
strip-debug-flagsoutput, thennode $D creative-append --list "<stripped>", which appends both Creative symbols —CREATIVE(strips ad UI, force-unlocks content) andTHEONE_HIDE_FPS(suppresses the on-screen FPS counter, which a playable ad must never show;UITemplate’sFps.csandUITemplateVContainer.csgate on#if PRODUCTION || THEONE_HIDE_FPS). Idempotent per symbol on the;-split list (never a substring match), so a project whose Android list already containsCREATIVEgetsTHEONE_HIDE_FPStopped up rather thanCREATIVEdouble-appended. Print theappendedSymbolsarray — 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:
node $V --config <tmp>/develop.xml --expect PARAM_DISCORD_THREAD_ID=<id-develop>→ green.$VreportsPARAM_SHOULD_RESET_JENKINS_PARAMS=falseon all three generated XMLs — the canary.diff <(xmllint --format $B) <(xmllint --format <tmp>/develop.xml)differs only in the intended elements (the--setparameters,<url>, the branch<name>, and the triggers block if--strip-triggerswas used). Read every hunk; do not count them.- The Release
gencall’s own stdout log (log.join('\n'), printed bygenfor every mutation it applied) contains no line at all forPARAM_SHOULD_BUILD_DEVELOPMENT,PARAM_SHOULD_OPTIMIZE_BUILD_SIZE, orPARAM_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--setand no--setat all produce byte-identical XML (confirmed withcmp), so inspectingrelease.xmlcannot 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--setwas 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.
-
Create exactly one job:
node $S create UnityAndroid <ProjectName>-Develop --from <tmp>/develop.xml.-Developis the smoke target because it’s a job the user actually wants — never a scratch job.createItemreturns 200 with an empty body on success; that is not failure, but a 200 also does not prove the config landed — always verify. -
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 answernode $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
$Valone.jenkins-job.cjs verifyhas no--expect-branchflag 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’sverifyfor branch; always pass--expect-triggersto 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. -
Print both tables and the job URL, then
AskUserQuestion: proceed with-Release/-Creative, re-check something, correct viaupdate, 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.
Reporting
Section titled “Reporting”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-runfirst, then for real, then re-verify with both parsers. - Wrong job name → cannot be removed by this skill (
Job/Deleteunproven). Report plainly and ask a Jenkins admin. Never delete-and-recreate, never leave a duplicate silently. - Never
--seta keystore parameter to fix anything — a value POSTed into aPasswordParameterDefinitionis stored verbatim and readable by anyone withJob/Read. BaseJob’s encrypted defaults clone safely within this instance; keep them untouched.
Gotchas
Section titled “Gotchas”PARAM_DISCORD_THREAD_IDhas 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’sfindParamBlockhandles this — verify with an independent parser anyway, because read and write sharing one broken locator produces a self-consistent lie.--seton a define-symbol list is a whole-value replace, not an append. The;CREATIVE;THEONE_HIDE_FPSappend must read BaseJob’s live current value first, then pass the full concatenated string.- Keystore params are PER-PROJECT, and Jenkins DOES encrypt a password POSTed via
config.xml. Two corrections. (a) BaseJob’sthe1_googleplay.keystore/theonestudiois the shared-template key — a title with its own*.keystoremust 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 prefixAAAAQ→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. Job/Deleteis UNPROVEN for this account. AdoDeleteGET 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.UnityAndroidholds 222+ jobs. Collision-check case-insensitively; Jenkins names are case-sensitive, soXandxcan coexist.- The CSRF crumb must travel with its session cookie —
jenkins-job.cjshandles it; a hand-rolledcurlneeds one cookie jar (-c/-b) across both requests. createItemreturns 200 with an empty body on success, and a 200 does not prove the config landed. Always verify.- 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. - 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.
- 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. GithubProjectProperty’s<projectUrl>is not corrected bygen. 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.- Detected Unity version ≠ Jenkins tool name — ColorFit’s real job stores
Unity 6000.3.13f1with a literal prefix. Confirm before--seting it (project-detection.md). 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.- 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.
- 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. PRODUCTIONon Release is an invariant. It lives in noProjectSettings.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 withdetect-unity-project.cjs release-append, never by hand. Seeparam-matrix.md§ “The Release define append”.- Verify role-append isolation across the three jobs, not just within one.
PRODUCTIONmust be on Release and on neither sibling;CREATIVE/THEONE_HIDE_FPSon 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 inUnityAndroid(Arrow3D-Develop carriesCREATIVE).
Related
Section titled “Related”~/.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 symbolsstrip-debug-flagsremoves; 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.