@@ -2990,6 +2990,80 @@ jobs:
29902990 fs.writeFileSync(".ubiquityos.issue.md", markdown);
29912991 NODE
29922992
2993+ - name : Map GitHub App installation token
2994+ if : steps.parse.outputs.allowed == 'true' && steps.parse.outputs.privileged == 'true'
2995+ id : app_token_map
2996+ env :
2997+ GH_TOKEN : ${{ inputs.authToken }}
2998+ REPO : ${{ steps.parse.outputs.repo }}
2999+ EXPECTED_INSTALLATION_ID : ${{ steps.parse.outputs.installation_id }}
3000+ run : |
3001+ set -euo pipefail
3002+
3003+ repo="${REPO:?Missing REPO}"
3004+ gh api "repos/$repo" > "$RUNNER_TEMP/ubq_repo_from_token.json"
3005+
3006+ # Preferred mapping source for installation tokens.
3007+ if gh api "installation" > "$RUNNER_TEMP/ubq_installation_from_token.json"; then
3008+ echo "Resolved installation metadata via /installation."
3009+ else
3010+ echo "Could not read /installation with current token; falling back to webhook installation id."
3011+ printf '{}' > "$RUNNER_TEMP/ubq_installation_from_token.json"
3012+ fi
3013+
3014+ node <<'NODE'
3015+ const fs = require("node:fs");
3016+
3017+ const expectedRaw = String(process.env.EXPECTED_INSTALLATION_ID || "").trim();
3018+ const expectedInstallationId = Number.parseInt(expectedRaw, 10);
3019+ const expectedId = Number.isFinite(expectedInstallationId) && expectedInstallationId > 0 ? expectedInstallationId : null;
3020+
3021+ const readJson = (path, fallback) => {
3022+ try {
3023+ return JSON.parse(fs.readFileSync(path, "utf8"));
3024+ } catch {
3025+ return fallback;
3026+ }
3027+ };
3028+
3029+ const repoInfo = readJson(`${process.env.RUNNER_TEMP}/ubq_repo_from_token.json`, {});
3030+ const installationInfo = readJson(`${process.env.RUNNER_TEMP}/ubq_installation_from_token.json`, {});
3031+
3032+ const mappedInstallationId = typeof installationInfo.id === "number" && Number.isFinite(installationInfo.id) ? installationInfo.id : expectedId;
3033+ if (expectedId && mappedInstallationId && expectedId !== mappedInstallationId) {
3034+ console.error(`GitHub App installation mismatch: expected ${expectedId}, mapped ${mappedInstallationId}.`);
3035+ process.exit(1);
3036+ }
3037+
3038+ const tokenMap = {
3039+ tokenType: "github_app_installation",
3040+ installationId: mappedInstallationId,
3041+ expectedInstallationId: expectedId,
3042+ appSlug: typeof installationInfo.app_slug === "string" ? installationInfo.app_slug : "",
3043+ accountLogin: typeof installationInfo?.account?.login === "string" ? installationInfo.account.login : "",
3044+ repository: typeof repoInfo.full_name === "string" ? repoInfo.full_name : "",
3045+ };
3046+
3047+ const outputLines = [];
3048+ const setOutput = (k, v) => outputLines.push(`${k}<<EOF\n${String(v)}\nEOF\n`);
3049+
3050+ setOutput("map_json", JSON.stringify(tokenMap));
3051+ setOutput("installation_id", tokenMap.installationId ? String(tokenMap.installationId) : "");
3052+ setOutput("app_slug", tokenMap.appSlug);
3053+ setOutput("account_login", tokenMap.accountLogin);
3054+ setOutput("repo", tokenMap.repository);
3055+
3056+ fs.appendFileSync(process.env.GITHUB_OUTPUT, outputLines.join(""));
3057+
3058+ const printable = {
3059+ installationId: tokenMap.installationId,
3060+ appSlug: tokenMap.appSlug || null,
3061+ accountLogin: tokenMap.accountLogin || null,
3062+ repository: tokenMap.repository || null,
3063+ };
3064+ process.stdout.write(`GitHub App token mapping: ${JSON.stringify(printable)}\n`);
3065+ NODE
3066+
29933067 - name : Fetch marketplace plugin registry (best-effort)
29943068 if : steps.parse.outputs.allowed == 'true' && steps.parse.outputs.privileged == 'true'
29953069 continue-on-error : true
@@ -3034,6 +3108,7 @@ jobs:
30343108 CONVERSATION_KEY : ${{ steps.parse.outputs.conversation_key }}
30353109 ENVIRONMENT : ${{ steps.parse.outputs.environment }}
30363110 CONFIG_PATH_CANDIDATES_JSON : ${{ steps.parse.outputs.config_path_candidates }}
3111+ APP_TOKEN_MAP_JSON : ${{ steps.app_token_map.outputs.map_json }}
30373112 run : |
30383113 node <<'NODE'
30393114 const fs = require("node:fs");
@@ -3045,12 +3120,27 @@ jobs:
30453120 const conversationKey = String(process.env.CONVERSATION_KEY || "").trim();
30463121 const environment = String(process.env.ENVIRONMENT || "").trim();
30473122 const configPathCandidatesJson = String(process.env.CONFIG_PATH_CANDIDATES_JSON || "").trim();
3123+ const appTokenMapJson = String(process.env.APP_TOKEN_MAP_JSON || "").trim();
30483124 let configPathCandidates = [];
3125+ let appTokenMap = null;
30493126 try {
30503127 configPathCandidates = configPathCandidatesJson ? JSON.parse(configPathCandidatesJson) : [];
30513128 } catch {
30523129 configPathCandidates = [];
30533130 }
3131+ try {
3132+ appTokenMap = appTokenMapJson ? JSON.parse(appTokenMapJson) : null;
3133+ } catch {
3134+ appTokenMap = null;
3135+ }
3136+
3137+ const authMappingLine =
3138+ privileged && appTokenMap && typeof appTokenMap === "object"
3139+ ? [
3140+ "- GH token mapping (resolved from this run token):",
3141+ ` installation_id=${appTokenMap.installationId || "unknown"}, app_slug=${appTokenMap.appSlug || "unknown"}, account=${appTokenMap.accountLogin || "unknown"}, repo_access=${appTokenMap.repository || "unknown"}.`,
3142+ ].join("\n")
3143+ : null;
30543144
30553145 const contextLines = [
30563146 "- The full issue/PR thread (including collaborators' comments) is available in `.ubiquityos.issue.md`.",
@@ -3062,13 +3152,14 @@ jobs:
30623152 ? `- Config path candidates (edit the active one): ${configPathCandidates.join(", ")}`
30633153 : null,
30643154 "- If present, the marketplace plugin registry is in `.ubiquityos.marketplace.json` (best-effort; may be empty if access is unavailable).",
3155+ authMappingLine,
30653156 ].filter(Boolean);
30663157
30673158 const operationsLines = privileged
30683159 ? [
30693160 "GitHub operations (privileged):",
30703161 "- The invoker is an OWNER/MEMBER/COLLABORATOR; you may use the GitHub CLI `gh` for GitHub operations (issues/PRs/labels/comments).",
3071- "- Authentication: `GH_TOKEN` is set to a GitHub App installation token. `gh auth status` may display `actions-user` (expected for non-user tokens). Do not run `gh auth login`. If you need a sanity check, run: `gh api repos/<owner>/<repo> -q .full_name `.",
3162+ "- Authentication: `GH_TOKEN` is a GitHub App installation token. Treat the GH token mapping in this prompt as source of truth. `gh auth status` may display `actions-user` (expected for non-user tokens). Do not run `gh auth login`.",
30723163 "- Marketplace auth (optional): if you need to create/update repos under the marketplace org, `UOS_MARKETPLACE_GH_TOKEN` may be set and `UOS_MARKETPLACE_ORG` is provided.",
30733164 ' - For marketplace ops, prefix commands like: `GH_TOKEN=\"$UOS_MARKETPLACE_GH_TOKEN\" gh ... --repo \"$UOS_MARKETPLACE_ORG/<repo>\"`.',
30743165 "- Sandbox: outbound network for tool commands is enabled in this run.",
0 commit comments