autonomous-sdlc-harness 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -0
- package/dist/commands/init.js +72 -9
- package/dist/commands/init.js.map +1 -1
- package/dist/core/writer.js +3 -2
- package/dist/core/writer.js.map +1 -1
- package/dist/doctor/checks.js +255 -12
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +60 -2
- package/dist/generators/githubWorkflows.js.map +1 -1
- package/dist/remote/githubActions.js +61 -3
- package/dist/remote/githubActions.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/context/conventions.md +1 -1
- package/templates/claude/context/layer.md +1 -1
- package/templates/github/workflows/harness-control.yml +63 -17
- package/templates/github/workflows/harness-run.yml +144 -5
- package/templates/github/workflows/harness-trigger.yml +4 -1
- package/templates/scripts/autonomous-watcher.sh +133 -28
- package/templates/scripts/lib/harness-run-lib.sh +59 -27
- package/templates/scripts/remote-run.sh +872 -122
- package/templates/state-dir/clarifications/README.md +1 -1
package/dist/doctor/checks.js
CHANGED
|
@@ -108,7 +108,7 @@ import { FORCED_SIGNAL_ID } from '../detect/signals.js';
|
|
|
108
108
|
import { CLAUDE_MD_PATH, SETUP_PENDING_CLOSE, SETUP_PENDING_OPEN, SKELETON_GUIDANCE_MARKER, TASK_OFFER_PATH, UNFILLED_STUB_MARKER, } from '../generators/claudeContext.js';
|
|
109
109
|
import { caseLabelMatches, isGlobPattern, PRE_PUSH_HOOK, readProtectedCaseLabel, resolveGithooksDir, resolveProtectedBranches, } from '../generators/githooks.js';
|
|
110
110
|
import { PUSH_CMD_KEY, PUSH_DESTINATION_PLACEHOLDER, PUSH_URL_KEY, pushEnvCandidates, } from '../generators/notifications.js';
|
|
111
|
-
import { IN_FLIGHT_RUNS_NOTE, pinnedCliCommand, upgradeWorkflowsCommand } from '../generators/githubWorkflows.js';
|
|
111
|
+
import { IN_FLIGHT_RUNS_NOTE, pinnedCliCommand, unparseableControlRoute, upgradeWorkflowsCommand } from '../generators/githubWorkflows.js';
|
|
112
112
|
import { DOCS_SEARCH_SERVER_SCRIPT_NAME, outerLoopScriptsDir } from '../generators/outerLoopScripts.js';
|
|
113
113
|
import { bashScriptRule, entryWord, isUnderDirectory, namesBrowserTool, normalizedRoot, pluginRootDirectories, pluginRootEntries, pluginRootEntryTarget, pluginRootHelpers, PLUGIN_ROOT_ENTRIES_FLAG, PROFILE_PATH, renderProfile, TEMPLATE_PATH as PROFILE_TEMPLATE_PATH, } from '../generators/permissionProfile.js';
|
|
114
114
|
import { ENABLED_PLUGINS_KEY, MARKETPLACE_ENTRY_SHAPE, MARKETPLACE_FLAG, MARKETPLACES_KEY, MARKETPLACE_NAME, marketplaceEntryDefect, PLUGIN_KEY, SETTINGS_PATH, SLUG_SHAPE, } from '../generators/projectSettings.js';
|
|
@@ -118,7 +118,7 @@ import { selectedStateDirs } from '../generators/stateDir.js';
|
|
|
118
118
|
import { machineConfigDir } from '../machine/paths.js';
|
|
119
119
|
import { installedPluginsPath, knownMarketplacesPath, pluginInstallRoot, pluginRuntimeRoot, pluginScriptsDir, } from '../machine/plugins.js';
|
|
120
120
|
import { inspect, readRegistry, registryPath } from '../machine/registry.js';
|
|
121
|
-
import { API_KEY_SECRET, CLI_VERSION_VARIABLE, DEFAULT_GH_CLI, COMMAND_HANDLE, COMMAND_VERBS, DEFAULT_TRIGGER_LABEL, GH_CLI_VARIABLE, ghCli, GIT_TOKEN_SECRET, LEGACY_TRIGGER_LABEL, OAUTH_TOKEN_SECRET, PR_CREATE_SETTING, PR_CREATE_SETTING_PATH, PUSH_URL_SECRET, REMOTE_STOP_VARIABLE, renderedCliVersions, runGh, RUNNER_VARIABLE, TRIGGER_ALLOWED_BOTS_VARIABLE, TRIGGER_LABEL_VARIABLE, triggerFallbackLabel, WORKFLOW_CONTROL_FILE, WORKFLOW_CONTROL_PATH, WORKFLOW_RESUME_FILE, WORKFLOW_RESUME_PATH, WORKFLOW_RUN_FILE, WORKFLOW_RUN_PATH, WORKFLOW_TRIGGER_FILE, WORKFLOW_TRIGGER_PATH, } from '../remote/githubActions.js';
|
|
121
|
+
import { API_KEY_SECRET, carriesRunActors, CLI_VERSION_VARIABLE, DEFAULT_GH_CLI, COMMAND_HANDLE, COMMAND_VERBS, DEFAULT_TRIGGER_LABEL, effectiveRunActors, GH_CLI_VARIABLE, ghCli, GIT_TOKEN_SECRET, LEGACY_TRIGGER_LABEL, OAUTH_TOKEN_SECRET, PR_CREATE_SETTING, PR_CREATE_SETTING_PATH, PUSH_URL_SECRET, REMOTE_STOP_VARIABLE, renderedCliVersions, runActorAdmitted, runGh, RUN_ACTORS_EVERY_WRITER, RUN_ACTORS_VARIABLE, RUNNER_VARIABLE, TRIGGER_ALLOWED_BOTS_VARIABLE, TRIGGER_LABEL_VARIABLE, triggerFallbackLabel, WORKFLOW_CONTROL_FILE, WORKFLOW_CONTROL_PATH, WORKFLOW_RESUME_FILE, WORKFLOW_RESUME_PATH, WORKFLOW_RUN_FILE, WORKFLOW_RUN_PATH, WORKFLOW_TRIGGER_FILE, WORKFLOW_TRIGGER_PATH, } from '../remote/githubActions.js';
|
|
122
122
|
import { modelFilesPresent } from '../retrieval/models.js';
|
|
123
123
|
import { launcherSearchPath, parseSelfCheck, PYTHON_DATABASE_URL_VARIABLE, PYTHON_FETCH_MODELS_SUB_COMMAND, PYTHON_RETRIEVAL_COMMAND, PYTHON_SELF_CHECK_SUB_COMMAND, pythonDatabaseUrl, SELF_CHECK_INDEX_NOT_ATTEMPTED, } from '../retrieval/pythonBackend.js';
|
|
124
124
|
import { retrievalCliEntry, retrievalModelCacheDir, retrievalRuntimeDir, retrievalRuntimeState, } from '../retrieval/runtime.js';
|
|
@@ -156,6 +156,11 @@ const REMOTE_RUN_SCRIPT = 'remote-run.sh';
|
|
|
156
156
|
* YAML file spells this endpoint.
|
|
157
157
|
*/
|
|
158
158
|
const ARTIFACT_RETENTION_ENDPOINT = 'repos/{owner}/{repo}/actions/permissions/artifact-and-log-retention';
|
|
159
|
+
/**
|
|
160
|
+
* The `gh api` path {@link REMOTE_GITHUB_CHECK} reads the repository's workflow listing from, to find
|
|
161
|
+
* a harness workflow GitHub lists by its path. Local for {@link ARTIFACT_RETENTION_ENDPOINT}'s reason.
|
|
162
|
+
*/
|
|
163
|
+
const WORKFLOWS_ENDPOINT = 'repos/{owner}/{repo}/actions/workflows?per_page=100';
|
|
159
164
|
/**
|
|
160
165
|
* The `gh api` path {@link REMOTE_GITHUB_CHECK} reads the *Allow GitHub Actions to create and approve
|
|
161
166
|
* pull requests* setting from — `can_approve_pull_request_reviews`. Local for
|
|
@@ -163,6 +168,19 @@ const ARTIFACT_RETENTION_ENDPOINT = 'repos/{owner}/{repo}/actions/permissions/ar
|
|
|
163
168
|
* usually can.
|
|
164
169
|
*/
|
|
165
170
|
const PR_SETTING_ENDPOINT = 'repos/{owner}/{repo}/actions/permissions/workflow';
|
|
171
|
+
/**
|
|
172
|
+
* The `gh api` path {@link REMOTE_GITHUB_CHECK} reads the repository's owner from, to grade an unset
|
|
173
|
+
* {@link RUN_ACTORS_VARIABLE}. Local for {@link ARTIFACT_RETENTION_ENDPOINT}'s reason.
|
|
174
|
+
*/
|
|
175
|
+
const REPOSITORY_ENDPOINT = 'repos/{owner}/{repo}';
|
|
176
|
+
/**
|
|
177
|
+
* The `gh api` path {@link REMOTE_GITHUB_CHECK} reads the repository's collaborators from, to name
|
|
178
|
+
* writers {@link RUN_ACTORS_VARIABLE} does not admit. Local for {@link ARTIFACT_RETENTION_ENDPOINT}'s
|
|
179
|
+
* reason. Listing collaborators needs push access.
|
|
180
|
+
*/
|
|
181
|
+
const COLLABORATORS_ENDPOINT = 'repos/{owner}/{repo}/collaborators?per_page=100';
|
|
182
|
+
/** A full {@link COLLABORATORS_ENDPOINT} page: at this many entries more collaborators may exist. */
|
|
183
|
+
const COLLABORATORS_PAGE_SIZE = 100;
|
|
166
184
|
/** Below this many days of artifact retention {@link REMOTE_GITHUB_CHECK} warns; argued there. */
|
|
167
185
|
const ARTIFACT_RETENTION_WARN_DAYS = 30;
|
|
168
186
|
/** The permission lists a generated profile carries, in the order the template writes them. */
|
|
@@ -1865,7 +1883,11 @@ const DAEMON_PATH_CHECK = {
|
|
|
1865
1883
|
*
|
|
1866
1884
|
* **Every finding is reported, and the grade is the worst of them.** Two `fail`s: no
|
|
1867
1885
|
* `harness-run.yml`, because no remote run can be dispatched; and no `gh`, because the watcher
|
|
1868
|
-
* dispatches through it.
|
|
1886
|
+
* dispatches through it. Five `warn`s: a `harness-run.yml` whose text does not read
|
|
1887
|
+
* {@link RUN_ACTORS_VARIABLE} ({@link carriesRunActors}), remedied by {@link upgradeWorkflowsCommand} —
|
|
1888
|
+
* local evidence, because the workflow is what GitHub runs and only its text says whether the list
|
|
1889
|
+
* reaches the gate and the scripts; listed beside the pin warning when both fire, and an unreadable
|
|
1890
|
+
* file adds nothing to the pin note. A `harness-run.yml` pinned (`remote/githubActions.ts` →
|
|
1869
1891
|
* {@link renderedCliVersions}) to a version other than this CLI's — never a `fail`, because the job
|
|
1870
1892
|
* installs its pin and the adopter may stay on it deliberately; the remedy is
|
|
1871
1893
|
* `generators/githubWorkflows.ts` → {@link upgradeWorkflowsCommand}, the alternative `doctor` at the
|
|
@@ -1954,6 +1976,9 @@ const REMOTE_EXECUTION_CHECK = {
|
|
|
1954
1976
|
else if (pins !== undefined) {
|
|
1955
1977
|
pinnedHere = true;
|
|
1956
1978
|
}
|
|
1979
|
+
if (text !== undefined && !carriesRunActors(text)) {
|
|
1980
|
+
warnings.push(`\`${WORKFLOW_RUN_PATH}\` was written before \`${RUN_ACTORS_VARIABLE}\`: its run job launches for any writer's dispatch or re-run, and its collect job passes the scripts no list, so a review round's authors are held to an unset list (the repository owner alone, or nobody in an organisation-owned repository) once the scripts are re-rendered; \`${upgradeWorkflowsCommand(version)}\` re-renders it after a .bak (docs/remote-execution.md, section 7, Upgrading)`);
|
|
1981
|
+
}
|
|
1957
1982
|
if (!branchUsable) {
|
|
1958
1983
|
notes.push(`whether GitHub's default branch carries ${WORKFLOW_RUN_PATH} is not graded, because defaultBranch is not a branch name (see the config check)`);
|
|
1959
1984
|
}
|
|
@@ -2026,6 +2051,32 @@ function retentionDaysOf(stdout) {
|
|
|
2026
2051
|
const days = parsed.days;
|
|
2027
2052
|
return typeof days === 'number' && Number.isInteger(days) && days > 0 ? days : undefined;
|
|
2028
2053
|
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Each `workflows[]` entry's `name`, keyed by its `path`, of an Actions workflow listing — entries
|
|
2056
|
+
* lacking a string `name` or `path` are skipped — with the listing's `total_count` (the entry count when
|
|
2057
|
+
* absent) and the number of entries listed, or `undefined` when there is no `workflows` array.
|
|
2058
|
+
*/
|
|
2059
|
+
function workflowNamesByPathOf(stdout) {
|
|
2060
|
+
let parsed;
|
|
2061
|
+
try {
|
|
2062
|
+
parsed = JSON.parse(stdout);
|
|
2063
|
+
}
|
|
2064
|
+
catch {
|
|
2065
|
+
return undefined;
|
|
2066
|
+
}
|
|
2067
|
+
if (!isJsonObject(parsed))
|
|
2068
|
+
return undefined;
|
|
2069
|
+
const { workflows, total_count: totalCount } = parsed;
|
|
2070
|
+
if (!Array.isArray(workflows))
|
|
2071
|
+
return undefined;
|
|
2072
|
+
const names = new Map();
|
|
2073
|
+
for (const item of workflows) {
|
|
2074
|
+
if (isJsonObject(item) && typeof item.name === 'string' && typeof item.path === 'string')
|
|
2075
|
+
names.set(item.path, item.name);
|
|
2076
|
+
}
|
|
2077
|
+
const total = typeof totalCount === 'number' && Number.isInteger(totalCount) ? totalCount : workflows.length;
|
|
2078
|
+
return { names, total, listed: workflows.length };
|
|
2079
|
+
}
|
|
2029
2080
|
/** The boolean `can_approve_pull_request_reviews` of a workflow-permissions answer, or `undefined` for any other shape. */
|
|
2030
2081
|
function prApprovalSettingOf(stdout) {
|
|
2031
2082
|
let parsed;
|
|
@@ -2040,6 +2091,49 @@ function prApprovalSettingOf(stdout) {
|
|
|
2040
2091
|
const allowed = parsed.can_approve_pull_request_reviews;
|
|
2041
2092
|
return typeof allowed === 'boolean' ? allowed : undefined;
|
|
2042
2093
|
}
|
|
2094
|
+
/** The string `.owner.login` and `.owner.type` of a repository answer, or `undefined` for any other shape. */
|
|
2095
|
+
function repositoryOwnerOf(stdout) {
|
|
2096
|
+
let parsed;
|
|
2097
|
+
try {
|
|
2098
|
+
parsed = JSON.parse(stdout);
|
|
2099
|
+
}
|
|
2100
|
+
catch {
|
|
2101
|
+
return undefined;
|
|
2102
|
+
}
|
|
2103
|
+
if (!isJsonObject(parsed))
|
|
2104
|
+
return undefined;
|
|
2105
|
+
const owner = parsed.owner;
|
|
2106
|
+
if (!isJsonObject(owner))
|
|
2107
|
+
return undefined;
|
|
2108
|
+
const { login, type } = owner;
|
|
2109
|
+
return typeof login === 'string' && typeof type === 'string' ? { login, type } : undefined;
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Each `login` whose `permissions.push` is `true` — write, maintain and admin all carry it — of a
|
|
2113
|
+
* collaborators answer, with the array's length, or `undefined` for any other shape.
|
|
2114
|
+
*/
|
|
2115
|
+
function writersOf(stdout) {
|
|
2116
|
+
let parsed;
|
|
2117
|
+
try {
|
|
2118
|
+
parsed = JSON.parse(stdout);
|
|
2119
|
+
}
|
|
2120
|
+
catch {
|
|
2121
|
+
return undefined;
|
|
2122
|
+
}
|
|
2123
|
+
if (!Array.isArray(parsed))
|
|
2124
|
+
return undefined;
|
|
2125
|
+
const logins = [];
|
|
2126
|
+
for (const item of parsed) {
|
|
2127
|
+
if (!isJsonObject(item) || typeof item.login !== 'string')
|
|
2128
|
+
return undefined;
|
|
2129
|
+
const permissions = item.permissions;
|
|
2130
|
+
if (!isJsonObject(permissions))
|
|
2131
|
+
return undefined;
|
|
2132
|
+
if (permissions.push === true)
|
|
2133
|
+
logins.push(item.login);
|
|
2134
|
+
}
|
|
2135
|
+
return { logins, count: parsed.length };
|
|
2136
|
+
}
|
|
2043
2137
|
/**
|
|
2044
2138
|
* What GitHub says about the remote setup — asked only under {@link CheckContext.probeGithub}.
|
|
2045
2139
|
*
|
|
@@ -2049,9 +2143,14 @@ function prApprovalSettingOf(stdout) {
|
|
|
2049
2143
|
*
|
|
2050
2144
|
* Grades, worst wins and every finding is reported:
|
|
2051
2145
|
* - `fail` — `gh` does not spawn or `gh auth status` refuses (nothing further is asked); GitHub does
|
|
2052
|
-
* not know `harness-run.yml`; neither credential secret is set
|
|
2146
|
+
* not know `harness-run.yml`; neither credential secret is set; a harness workflow GitHub lists by
|
|
2147
|
+
* its path, which is how it lists a file it could not parse — judged from the workflow listing
|
|
2148
|
+
* ({@link WORKFLOWS_ENDPOINT}) because `gh workflow view` answers for an unparsed file too, as Gate 12
|
|
2149
|
+
* round 7 observed. `harness-trigger.yml` and `harness-control.yml` are judged only when
|
|
2150
|
+
* {@link forgeTriggerApplies}.
|
|
2053
2151
|
* - `warn` — `HARNESS_PUSH_URL` absent; `harness-resume.yml` unknown to GitHub; `HARNESS_REMOTE_STOP`
|
|
2054
|
-
* set;
|
|
2152
|
+
* set; the workflow listing unread, or not reaching a judged harness workflow because the repository
|
|
2153
|
+
* has more workflows than one page lists, so whether GitHub could parse it cannot be told; artifact retention below {@link ARTIFACT_RETENTION_WARN_DAYS} days; when
|
|
2055
2154
|
* {@link forgeTriggerApplies}, `harness-trigger.yml` or `harness-control.yml` unknown to GitHub, no
|
|
2056
2155
|
* label by the effective trigger name, or the pull-request setting off with no `HARNESS_GIT_TOKEN`
|
|
2057
2156
|
* secret; and any call that timed out, could not reach GitHub, or answered in a shape not
|
|
@@ -2072,9 +2171,25 @@ function prApprovalSettingOf(stdout) {
|
|
|
2072
2171
|
* request with that token instead.
|
|
2073
2172
|
* - all three trigger answers positive — both workflows known and the label present — is confirmed on
|
|
2074
2173
|
* every outcome that reaches the trigger reads, `fail` and `warn` included, so an unrelated finding
|
|
2075
|
-
* never hides it; any answer not positive is already among the warnings
|
|
2174
|
+
* never hides it; any answer not positive is already among the warnings, and a forge workflow
|
|
2175
|
+
* GitHub lists by its path withholds it, being among the failures. An outcome returned before
|
|
2076
2176
|
* those reads — `gh` not runnable, no usable login, or no readable answer to the login probe — asks
|
|
2077
2177
|
* GitHub nothing about the trigger.
|
|
2178
|
+
* - who may start, steer, answer and review a run is {@link effectiveRunActors} of
|
|
2179
|
+
* `HARNESS_RUN_ACTORS`, read only when the variable listing was. An unset list adds a read of the
|
|
2180
|
+
* repository's owner ({@link REPOSITORY_ENDPOINT}); any answer but a readable owner — refused, timed
|
|
2181
|
+
* out, unreachable or an unread shape — is a *cannot tell* warning, and the list then grades as
|
|
2182
|
+
* admitting nobody with no second warning. `*`, a list of logins, and an unset list in a user-owned
|
|
2183
|
+
* repository are each a note naming who acts; an unset list whose owner is not a user is a `warn`
|
|
2184
|
+
* with the `gh variable set` remedy. `*` while `CLAUDE_CODE_OAUTH_TOKEN` is set is a `warn` whether
|
|
2185
|
+
* or not `ANTHROPIC_API_KEY` is also set: the token is one person's subscription, readable by every
|
|
2186
|
+
* writer.
|
|
2187
|
+
* - a list of logins or an owner-only list while `CLAUDE_CODE_OAUTH_TOKEN` is set adds a read of the
|
|
2188
|
+
* collaborators ({@link COLLABORATORS_ENDPOINT}). A writer the list does not admit is a `warn` naming
|
|
2189
|
+
* them: the list stops their spending through the harness, but a writer can still read the secret
|
|
2190
|
+
* through an edited workflow run from a branch. A full first page with no such writer is a *cannot
|
|
2191
|
+
* tell* warning, as are a timed-out, unreachable or unread answer; a refused read is a note on the
|
|
2192
|
+
* retention read's terms, since listing collaborators needs push access.
|
|
2078
2193
|
*
|
|
2079
2194
|
* **Why 30 days.** A parked run waits on a human answer and a usage-paused one on a reset, and the
|
|
2080
2195
|
* `harness-state` bundle is the only remote copy of either; once the repository's retention expires
|
|
@@ -2152,16 +2267,55 @@ const REMOTE_GITHUB_CHECK = {
|
|
|
2152
2267
|
if (resume.answer.kind === 'refused') {
|
|
2153
2268
|
warnings.push(`GitHub does not know ${WORKFLOW_RESUME_FILE} (${resume.call}: ${resume.answer.why}), so usage auto-resume is unavailable: push ${WORKFLOW_RESUME_PATH} to the repository's default branch`);
|
|
2154
2269
|
}
|
|
2270
|
+
const listing = ask(['api', WORKFLOWS_ENDPOINT]);
|
|
2271
|
+
if (listing.answer === undefined)
|
|
2272
|
+
return fail(noSpawn);
|
|
2273
|
+
const unparsed = new Set();
|
|
2274
|
+
const parsable = 'whether GitHub could parse the harness workflows';
|
|
2275
|
+
if (listing.answer.kind !== 'answered') {
|
|
2276
|
+
warnings.push(cannotTell(listing.call, listing.answer.why, parsable));
|
|
2277
|
+
}
|
|
2278
|
+
else {
|
|
2279
|
+
const listingRead = workflowNamesByPathOf(listing.answer.stdout);
|
|
2280
|
+
if (listingRead === undefined) {
|
|
2281
|
+
warnings.push(`cannot tell ${parsable}: ${listing.call} answered in a shape this check does not read`);
|
|
2282
|
+
}
|
|
2283
|
+
else {
|
|
2284
|
+
const version = ownManifestString('version');
|
|
2285
|
+
const judged = forgeTriggerApplies(ctx.config)
|
|
2286
|
+
? [WORKFLOW_RUN_PATH, WORKFLOW_RESUME_PATH, WORKFLOW_TRIGGER_PATH, WORKFLOW_CONTROL_PATH]
|
|
2287
|
+
: [WORKFLOW_RUN_PATH, WORKFLOW_RESUME_PATH];
|
|
2288
|
+
const unseen = [];
|
|
2289
|
+
for (const path of judged) {
|
|
2290
|
+
const name = listingRead.names.get(path);
|
|
2291
|
+
if (name === undefined) {
|
|
2292
|
+
if (listingRead.total > listingRead.listed)
|
|
2293
|
+
unseen.push(path);
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
if (name !== path)
|
|
2297
|
+
continue;
|
|
2298
|
+
unparsed.add(path);
|
|
2299
|
+
const route = path === WORKFLOW_CONTROL_PATH
|
|
2300
|
+
? unparseableControlRoute(version)
|
|
2301
|
+
: `Check ${path} with actionlint, or re-render it with \`${pinnedCliCommand(version)} init --force\`, which keeps a .bak; then commit it and push it to the repository's default branch.`;
|
|
2302
|
+
failures.push(`GitHub lists ${path} by its path rather than its name, which it does for a workflow file it cannot parse, so that workflow runs for no event: ${route}`);
|
|
2303
|
+
}
|
|
2304
|
+
if (unseen.length > 0) {
|
|
2305
|
+
warnings.push(`cannot tell whether GitHub could parse ${nameList(unseen)}: ${listing.call} listed ${listingRead.listed} of the repository's ${listingRead.total} workflows, and not ${unseen.length === 1 ? 'that one' : 'those'}`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2155
2309
|
const variables = ask(['variable', 'list', '--json', 'name,value']);
|
|
2156
2310
|
if (variables.answer === undefined)
|
|
2157
2311
|
return fail(noSpawn);
|
|
2158
2312
|
const variableValues = variables.answer.kind === 'answered' ? ghJsonEntries(variables.answer.stdout) : undefined;
|
|
2159
2313
|
let runner;
|
|
2160
2314
|
if (variables.answer.kind !== 'answered') {
|
|
2161
|
-
warnings.push(cannotTell(variables.call, variables.answer.why, `the ${RUNNER_VARIABLE} and ${
|
|
2315
|
+
warnings.push(cannotTell(variables.call, variables.answer.why, `the ${RUNNER_VARIABLE}, ${REMOTE_STOP_VARIABLE} and ${RUN_ACTORS_VARIABLE} variables`));
|
|
2162
2316
|
}
|
|
2163
2317
|
else if (variableValues === undefined) {
|
|
2164
|
-
warnings.push(`cannot tell the ${RUNNER_VARIABLE} and ${
|
|
2318
|
+
warnings.push(`cannot tell the ${RUNNER_VARIABLE}, ${REMOTE_STOP_VARIABLE} and ${RUN_ACTORS_VARIABLE} variables: ${variables.call} answered in a shape this check does not read`);
|
|
2165
2319
|
}
|
|
2166
2320
|
else {
|
|
2167
2321
|
const label = variableValues.get(RUNNER_VARIABLE)?.trim() ?? '';
|
|
@@ -2189,6 +2343,73 @@ const REMOTE_GITHUB_CHECK = {
|
|
|
2189
2343
|
warnings.push(`the repository keeps artifacts for ${retentionDays} days, so a remote run parked or paused longer than that loses its state bundle: raise it under Settings → Actions → General → Artifact and log retention`);
|
|
2190
2344
|
}
|
|
2191
2345
|
}
|
|
2346
|
+
// Undefined only when the variables listing was unread, whose warning above already names the list.
|
|
2347
|
+
let runActors;
|
|
2348
|
+
if (variableValues !== undefined) {
|
|
2349
|
+
const listed = variableValues.get(RUN_ACTORS_VARIABLE);
|
|
2350
|
+
const unset = effectiveRunActors(listed, undefined).kind === 'nobody';
|
|
2351
|
+
let owner;
|
|
2352
|
+
if (unset) {
|
|
2353
|
+
const repository = ask(['api', REPOSITORY_ENDPOINT]);
|
|
2354
|
+
if (repository.answer === undefined)
|
|
2355
|
+
return fail(noSpawn);
|
|
2356
|
+
const whoOwns = 'who owns the repository, so who may act on a run';
|
|
2357
|
+
if (repository.answer.kind !== 'answered') {
|
|
2358
|
+
warnings.push(cannotTell(repository.call, repository.answer.why, whoOwns));
|
|
2359
|
+
}
|
|
2360
|
+
else {
|
|
2361
|
+
owner = repositoryOwnerOf(repository.answer.stdout);
|
|
2362
|
+
if (owner === undefined)
|
|
2363
|
+
warnings.push(`cannot tell ${whoOwns}: ${repository.call} answered in a shape this check does not read`);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
runActors = effectiveRunActors(listed, owner);
|
|
2367
|
+
const acts = 'start, steer, answer and review a run';
|
|
2368
|
+
if (runActors.kind === 'every-writer') {
|
|
2369
|
+
notes.push(`${RUN_ACTORS_VARIABLE} is \`${RUN_ACTORS_EVERY_WRITER}\`, so every collaborator with write access may ${acts}`);
|
|
2370
|
+
}
|
|
2371
|
+
else if (runActors.kind === 'listed') {
|
|
2372
|
+
notes.push(`${RUN_ACTORS_VARIABLE} admits ${nameList(runActors.logins)}, so only ${runActors.logins.length === 1 ? 'that login' : 'those logins'} may ${acts}`);
|
|
2373
|
+
}
|
|
2374
|
+
else if (runActors.kind === 'owner') {
|
|
2375
|
+
notes.push(`${RUN_ACTORS_VARIABLE} is unset, so only the repository owner, ${runActors.login}, may ${acts}`);
|
|
2376
|
+
}
|
|
2377
|
+
else if (runActors.owner !== undefined) {
|
|
2378
|
+
const { login, type } = runActors.owner;
|
|
2379
|
+
warnings.push(`${RUN_ACTORS_VARIABLE} is unset and ${login} is ${/^[aeiou]/i.test(type) ? 'an' : 'a'} ${type}, not a user, so no person may start, steer, answer or review a run: set it with \`gh variable set ${RUN_ACTORS_VARIABLE} --body <login,login>\`, or \`--body '${RUN_ACTORS_EVERY_WRITER}'\` for every writer`);
|
|
2380
|
+
}
|
|
2381
|
+
if (runActors.kind === 'every-writer' && secretNames?.has(OAUTH_TOKEN_SECRET) === true) {
|
|
2382
|
+
warnings.push(`${RUN_ACTORS_VARIABLE} is \`${RUN_ACTORS_EVERY_WRITER}\` while ${OAUTH_TOKEN_SECRET} is set, so every writer's runs spend one person's subscription, which its terms do not let them share: name the people in ${RUN_ACTORS_VARIABLE}, or use a Claude API organisation's key in ${API_KEY_SECRET} (docs/remote-execution.md, section 9)`);
|
|
2383
|
+
}
|
|
2384
|
+
if ((runActors.kind === 'listed' || runActors.kind === 'owner') && secretNames?.has(OAUTH_TOKEN_SECRET) === true) {
|
|
2385
|
+
const admitted = runActors;
|
|
2386
|
+
const collaborators = ask(['api', COLLABORATORS_ENDPOINT]);
|
|
2387
|
+
if (collaborators.answer === undefined)
|
|
2388
|
+
return fail(noSpawn);
|
|
2389
|
+
const beyond = `whether any writer is beyond ${RUN_ACTORS_VARIABLE}`;
|
|
2390
|
+
if (collaborators.answer.kind === 'unknown') {
|
|
2391
|
+
warnings.push(cannotTell(collaborators.call, collaborators.answer.why, beyond));
|
|
2392
|
+
}
|
|
2393
|
+
else if (collaborators.answer.kind === 'refused') {
|
|
2394
|
+
notes.push(`writers beyond ${RUN_ACTORS_VARIABLE} not checked: ${collaborators.call} needs push access (${collaborators.answer.why})`);
|
|
2395
|
+
}
|
|
2396
|
+
else {
|
|
2397
|
+
const writers = writersOf(collaborators.answer.stdout);
|
|
2398
|
+
if (writers === undefined) {
|
|
2399
|
+
warnings.push(`cannot tell ${beyond}: ${collaborators.call} answered in a shape this check does not read`);
|
|
2400
|
+
}
|
|
2401
|
+
else {
|
|
2402
|
+
const extra = writers.logins.filter((login) => !runActorAdmitted(admitted, login));
|
|
2403
|
+
if (extra.length > 0) {
|
|
2404
|
+
warnings.push(`${OAUTH_TOKEN_SECRET} is set and ${nameList(extra)} can write to the repository without being on ${RUN_ACTORS_VARIABLE}: the list stops them spending the subscription through the harness, but any writer can still read the secret by running an edited workflow from a branch; on a private repository a push ruleset on the workflow paths and the scripts they run closes that, and on a public one only withholding write access does (docs/remote-execution.md, section 9)`);
|
|
2405
|
+
}
|
|
2406
|
+
else if (writers.count === COLLABORATORS_PAGE_SIZE) {
|
|
2407
|
+
warnings.push(`cannot tell ${beyond}: ${collaborators.call} returned a full page of ${COLLABORATORS_PAGE_SIZE} collaborators with no writer beyond the list, and more may exist`);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2192
2413
|
let triggerKnown = '';
|
|
2193
2414
|
if (forgeTriggerApplies(ctx.config)) {
|
|
2194
2415
|
const trigger = ask(['workflow', 'view', WORKFLOW_TRIGGER_FILE]);
|
|
@@ -2250,7 +2471,8 @@ const REMOTE_GITHUB_CHECK = {
|
|
|
2250
2471
|
notes.push(`${TRIGGER_ALLOWED_BOTS_VARIABLE} admits ${nameList(bots)}, each of which can start a run without a permission check`);
|
|
2251
2472
|
}
|
|
2252
2473
|
}
|
|
2253
|
-
|
|
2474
|
+
const forgeParsed = !unparsed.has(WORKFLOW_TRIGGER_PATH) && !unparsed.has(WORKFLOW_CONTROL_PATH);
|
|
2475
|
+
if (trigger.answer.kind === 'answered' && control.answer.kind === 'answered' && labelFound && forgeParsed) {
|
|
2254
2476
|
triggerKnown = `; GitHub knows ${WORKFLOW_TRIGGER_FILE} and ${WORKFLOW_CONTROL_FILE}, and the label \`${labelName}\` exists`;
|
|
2255
2477
|
}
|
|
2256
2478
|
const prSetting = ask(['api', PR_SETTING_ENDPOINT]);
|
|
@@ -2307,7 +2529,12 @@ const REMOTE_GITHUB_CHECK = {
|
|
|
2307
2529
|
* workflow not carried by `origin/<defaultBranch>`, because GitHub runs an `issues` or
|
|
2308
2530
|
* `issue_comment` workflow only from its default branch — that last with
|
|
2309
2531
|
* {@link REMOTE_EXECUTION_CHECK}'s push remedy and its two *not graded* notes, on the same reasoning.
|
|
2310
|
-
* Each warn names every file it is about, so two absent files are one line naming both.
|
|
2532
|
+
* Each warn names every file it is about, so two absent files are one line naming both. A fourth,
|
|
2533
|
+
* joined to whichever of those fires or standing alone: a present forge workflow whose text does not
|
|
2534
|
+
* read {@link RUN_ACTORS_VARIABLE} ({@link carriesRunActors}), named in one warning with `init --force`
|
|
2535
|
+
* — these two carry no pin and move with the scripts, not with {@link upgradeWorkflowsCommand}. It is
|
|
2536
|
+
* local evidence because the workflows are what GitHub runs and only their text says whether the list
|
|
2537
|
+
* reaches the scripts; a read that throws is skipped.
|
|
2311
2538
|
*
|
|
2312
2539
|
* A value outside {@link FORGE_KINDS} is the config check's `fail`, and is not graded here.
|
|
2313
2540
|
*/
|
|
@@ -2348,9 +2575,22 @@ const FORGE_CHECK = {
|
|
|
2348
2575
|
[WORKFLOW_CONTROL_PATH]: 'comments and reviews start nothing',
|
|
2349
2576
|
};
|
|
2350
2577
|
const consequence = (paths) => paths.map((path) => startsNothing[path]).join(', and ');
|
|
2578
|
+
const listless = forgeTriggerApplies(ctx.config)
|
|
2579
|
+
? presentWorkflows.filter((path) => {
|
|
2580
|
+
try {
|
|
2581
|
+
return !carriesRunActors(readFileSync(join(root, ...path.split('/')), 'utf8'));
|
|
2582
|
+
}
|
|
2583
|
+
catch {
|
|
2584
|
+
return false;
|
|
2585
|
+
}
|
|
2586
|
+
})
|
|
2587
|
+
: [];
|
|
2588
|
+
const listlessWarning = listless.length === 0
|
|
2589
|
+
? ''
|
|
2590
|
+
: `${nameList(listless)} ${listless.length === 1 ? 'was' : 'were'} written before \`${RUN_ACTORS_VARIABLE}\` and ${listless.length === 1 ? 'passes' : 'pass'} the scripts no list: with scripts written at the same time every writer may still start and command a run, and with the scripts re-rendered every start and command is held to an unset list — the repository owner alone, or nobody in an organisation-owned repository; \`${CLI} init --force\` replaces both workflows and the scripts after a .bak (docs/remote-execution.md, section 7, Upgrading)`;
|
|
2351
2591
|
const absent = forgeWorkflows.filter((path) => !presentWorkflows.includes(path));
|
|
2352
2592
|
if (absent.length > 0) {
|
|
2353
|
-
return warn(`${on}, but ${nameList(absent)} ${isAre(absent)} absent, so ${consequence(absent)}: re-run \`${CLI} init\`, which writes ${absent.length === 1 ? 'it' : 'them'} create-if-absent`);
|
|
2593
|
+
return warn(`${on}, but ${nameList(absent)} ${isAre(absent)} absent, so ${consequence(absent)}: re-run \`${CLI} init\`, which writes ${absent.length === 1 ? 'it' : 'them'} create-if-absent${listless.length > 0 ? `; ${listlessWarning}` : ''}`);
|
|
2354
2594
|
}
|
|
2355
2595
|
const branch = ctx.config.defaultBranch;
|
|
2356
2596
|
let carried;
|
|
@@ -2363,10 +2603,13 @@ const FORGE_CHECK = {
|
|
|
2363
2603
|
else {
|
|
2364
2604
|
const uncarried = forgeWorkflows.filter((path) => !pathAtRef(root, `origin/${branch}`, path));
|
|
2365
2605
|
if (uncarried.length > 0) {
|
|
2366
|
-
return warn(`${on}, but origin/${branch} does not carry ${nameList(uncarried)}, as this checkout last fetched it, and GitHub runs an issues or issue_comment workflow only from its default branch, so ${consequence(uncarried)} yet: commit ${uncarried.length === 1 ? 'it' : 'them'}, then run \`${WORKFLOW_SCOPE_COMMAND}\`, then \`${defaultBranchPushCommand(branch)}\`. ${WORKFLOW_SCOPE_REASON} ${defaultBranchPushReason(branch)}`);
|
|
2606
|
+
return warn(`${on}, but origin/${branch} does not carry ${nameList(uncarried)}, as this checkout last fetched it, and GitHub runs an issues or issue_comment workflow only from its default branch, so ${consequence(uncarried)} yet: commit ${uncarried.length === 1 ? 'it' : 'them'}, then run \`${WORKFLOW_SCOPE_COMMAND}\`, then \`${defaultBranchPushCommand(branch)}\`. ${WORKFLOW_SCOPE_REASON} ${defaultBranchPushReason(branch)}${listless.length > 0 ? ` ${listlessWarning}` : ''}`);
|
|
2367
2607
|
}
|
|
2368
2608
|
carried = ` and origin/${branch} carries them`;
|
|
2369
2609
|
}
|
|
2610
|
+
if (listless.length > 0) {
|
|
2611
|
+
return warn(`${on}: ${WORKFLOW_TRIGGER_PATH} and ${WORKFLOW_CONTROL_PATH} are present${carried}; ${listlessWarning}`);
|
|
2612
|
+
}
|
|
2370
2613
|
const asked = ctx.probeGithub
|
|
2371
2614
|
? `the ${REMOTE_GITHUB_CHECK.id} check above reports what GitHub says`
|
|
2372
2615
|
: `\`${CLI} doctor --check-github\` asks GitHub`;
|