From 176f6ec1ead714b46cd40e50db1070d566883858 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Tue, 21 Jul 2026 14:29:14 -0700 Subject: [PATCH] fix: Ask whether or not to write github/lab action file, or none --- README.md | 7 +- examples/openwiki-update.gitlab-ci.yml | 6 +- examples/openwiki-update.yml | 6 +- src/code-mode.ts | 95 +++++++++++++++++++++++--- src/commands.ts | 2 +- src/credentials.tsx | 85 ++++++++++++++++++++++- test/code-mode.test.ts | 40 ++++++++++- 7 files changed, 219 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 1ccc96d0..f895c90e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,10 @@ Bare `openwiki --init` and `openwiki --update` run in code mode. Use `openwiki personal --init` or `openwiki personal --update` for the local personal brain wiki. -Then to ensure your documentation stays up-to-date, add the CI workflow for your Git provider to automatically open a PR or merge request with documentation updates: +During interactive code-mode setup, OpenWiki asks whether to create a GitHub +Actions workflow, a GitLab CI/CD pipeline, or no CI file. The generated workflow +automatically opens a pull request or merge request with documentation updates. +You can also add one of the examples manually: - GitHub Actions: copy [openwiki-update.yml](./examples/openwiki-update.yml) into `.github/workflows/openwiki-update.yml`. - GitLab CI: copy [openwiki-update.gitlab-ci.yml](./examples/openwiki-update.gitlab-ci.yml) into `.gitlab-ci.yml` or include it from your existing GitLab pipeline. @@ -171,7 +174,7 @@ Bare `openwiki` runs in code mode for the current repository. It creates initial Bare `openwiki --init` and `openwiki --update` default to code mode and operate on repository documentation. Use the `personal` positional mode or `--mode personal` to initialize or update the local personal brain wiki. -On each `code` run, `openwiki` maintains both an `AGENTS.md` and a `CLAUDE.md` at the repository root, adding prompting that instructs your coding agent to reference the wiki when searching for context. Each file is created if it does not already exist. If a file is present, OpenWiki only rewrites its own `` block and leaves the rest of your content untouched (appending the block the first time). The scheduled GitHub Actions workflow includes these files, along with the workflow itself, in the documentation pull request. +On each `code` run, `openwiki` maintains both an `AGENTS.md` and a `CLAUDE.md` at the repository root, adding prompting that instructs your coding agent to reference the wiki when searching for context. Each file is created if it does not already exist. If a file is present, OpenWiki only rewrites its own `` block and leaves the rest of your content untouched (appending the block the first time). If you opt into GitHub Actions or GitLab CI/CD during setup, the generated CI file includes these agent files in its documentation update. Repository-specific wiki instructions are stored separately in `openwiki/INSTRUCTIONS.md`. This file is a shared, user-authored brief for the diff --git a/examples/openwiki-update.gitlab-ci.yml b/examples/openwiki-update.gitlab-ci.yml index c265f759..c48149ba 100644 --- a/examples/openwiki-update.gitlab-ci.yml +++ b/examples/openwiki-update.gitlab-ci.yml @@ -6,19 +6,19 @@ openwiki_update: - if: '$CI_PIPELINE_SOURCE == "web"' before_script: - apt-get update && apt-get install -y git curl - - npm install --global openwiki + - npm install --global openwiki@0.2.2 - git config user.name "${GITLAB_USER_NAME:-OpenWiki Bot}" - git config user.email "${GITLAB_USER_EMAIL:-openwiki@example.com}" script: - openwiki code --update --print - | - if git diff --quiet -- openwiki AGENTS.md CLAUDE.md .github/workflows/openwiki-update.yml; then + if git diff --quiet -- openwiki AGENTS.md CLAUDE.md .gitlab-ci.yml; then echo "OpenWiki is already up to date." exit 0 fi - export OPENWIKI_BRANCH="openwiki/update-${CI_PIPELINE_ID}" - git checkout -b "$OPENWIKI_BRANCH" - - git add openwiki AGENTS.md CLAUDE.md .github/workflows/openwiki-update.yml + - git add openwiki AGENTS.md CLAUDE.md .gitlab-ci.yml - git commit -m "docs: update OpenWiki" - git push "https://oauth2:${OPENWIKI_GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" "$OPENWIKI_BRANCH" - | diff --git a/examples/openwiki-update.yml b/examples/openwiki-update.yml index 37fdeaf1..79b56033 100644 --- a/examples/openwiki-update.yml +++ b/examples/openwiki-update.yml @@ -15,17 +15,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: true - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" - name: Install OpenWiki - run: npm install --global openwiki + run: npm install --global openwiki@0.2.2 - name: Run OpenWiki # --update also handles the first run for code docs when env vars are set. diff --git a/src/code-mode.ts b/src/code-mode.ts index f3447c5e..728ac49e 100644 --- a/src/code-mode.ts +++ b/src/code-mode.ts @@ -1,35 +1,56 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; +import { OPENWIKI_VERSION } from "./constants.js"; import { isFileNotFoundError } from "./fs-errors.js"; const OPENWIKI_AGENTS_SNIPPET_START = ""; const OPENWIKI_AGENTS_SNIPPET_END = ""; const DEFAULT_CODE_MODE_CRON = "0 8 * * *"; +export type CodeModeAutomation = "github" | "gitlab" | "none"; + // Root agent-instruction files OpenWiki keeps pointed at the generated wiki. // Each is created when missing and refreshed in place when already present. const CODE_MODE_AGENT_FILES = ["AGENTS.md", "CLAUDE.md"]; export async function ensureCodeModeRepoSetup( cwd: string, + automation: CodeModeAutomation = "none", cronExpression = DEFAULT_CODE_MODE_CRON, ): Promise { - await writeCodeModeWorkflow(cwd, cronExpression); + if (automation === "github") { + await writeGitHubActionsWorkflow(cwd, cronExpression); + } else if (automation === "gitlab") { + await writeGitLabCiWorkflow(cwd); + } + await writeCodeModeAgentSnippets(cwd); } -async function writeCodeModeWorkflow( +async function writeGitHubActionsWorkflow( cwd: string, cronExpression: string, ): Promise { - const workflowPath = path.join( + const workflowPath = resolveRepoPath( cwd, ".github", "workflows", "openwiki-update.yml", ); await mkdir(path.dirname(workflowPath), { recursive: true }); - await writeFile(workflowPath, createCodeModeWorkflow(cronExpression), "utf8"); + await writeFile( + workflowPath, + createGitHubActionsWorkflow(cronExpression), + "utf8", + ); +} + +async function writeGitLabCiWorkflow(cwd: string): Promise { + await writeFile( + resolveRepoPath(cwd, ".gitlab-ci.yml"), + createGitLabCiWorkflow(), + "utf8", + ); } async function writeCodeModeAgentSnippets(cwd: string): Promise { @@ -37,11 +58,25 @@ async function writeCodeModeAgentSnippets(cwd: string): Promise { await Promise.all( CODE_MODE_AGENT_FILES.map((fileName) => - writeCodeModeAgentSnippet(path.join(cwd, fileName), snippet), + writeCodeModeAgentSnippet(resolveRepoPath(cwd, fileName), snippet), ), ); } +function resolveRepoPath(cwd: string, ...segments: string[]): string { + const repoRoot = path.resolve(cwd); + const targetPath = path.resolve(repoRoot, ...segments); + + if ( + targetPath !== repoRoot && + !targetPath.startsWith(`${repoRoot}${path.sep}`) + ) { + throw new Error(`Refusing to write outside repository root: ${targetPath}`); + } + + return targetPath; +} + async function writeCodeModeAgentSnippet( agentsPath: string, snippet: string, @@ -66,7 +101,7 @@ async function writeCodeModeAgentSnippet( await writeFile(agentsPath, nextContent, "utf8"); } -function createCodeModeWorkflow(cronExpression: string): string { +function createGitHubActionsWorkflow(cronExpression: string): string { return `name: OpenWiki Update on: @@ -83,15 +118,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" - name: Install OpenWiki - run: npm install --global openwiki + run: npm install --global openwiki@${OPENWIKI_VERSION} - name: Run OpenWiki run: openwiki code --update --print @@ -121,6 +156,46 @@ jobs: `; } +function createGitLabCiWorkflow(): string { + return `openwiki_update: + image: node:22 + stage: deploy + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + - if: '$CI_PIPELINE_SOURCE == "web"' + before_script: + - apt-get update && apt-get install -y git curl + - npm install --global openwiki@${OPENWIKI_VERSION} + - git config user.name "\${GITLAB_USER_NAME:-OpenWiki Bot}" + - git config user.email "\${GITLAB_USER_EMAIL:-openwiki@example.com}" + script: + - openwiki code --update --print + - | + if git diff --quiet -- openwiki AGENTS.md CLAUDE.md .gitlab-ci.yml; then + echo "OpenWiki is already up to date." + exit 0 + fi + - export OPENWIKI_BRANCH="openwiki/update-\${CI_PIPELINE_ID}" + - git checkout -b "$OPENWIKI_BRANCH" + - git add openwiki AGENTS.md CLAUDE.md .gitlab-ci.yml + - git commit -m "docs: update OpenWiki" + - git push "https://oauth2:\${OPENWIKI_GITLAB_TOKEN}@\${CI_SERVER_HOST}/\${CI_PROJECT_PATH}.git" "$OPENWIKI_BRANCH" + - | + curl --fail --request POST \\ + --header "PRIVATE-TOKEN: \${OPENWIKI_GITLAB_TOKEN}" \\ + --form "source_branch=\${OPENWIKI_BRANCH}" \\ + --form "target_branch=\${CI_DEFAULT_BRANCH}" \\ + --form "title=docs: update OpenWiki" \\ + --form "description=Automated OpenWiki documentation update generated by the scheduled GitLab pipeline." \\ + "\${CI_API_V4_URL}/projects/\${CI_PROJECT_ID}/merge_requests" + variables: + OPENWIKI_PROVIDER: openrouter + OPENWIKI_MODEL_ID: z-ai/glm-5.2 + LANGCHAIN_PROJECT: openwiki + LANGCHAIN_TRACING_V2: "true" +`; +} + function createCodeModeAgentsSnippet(): string { return `${OPENWIKI_AGENTS_SNIPPET_START} @@ -128,7 +203,7 @@ function createCodeModeAgentsSnippet(): string { This repository uses OpenWiki for recurring code documentation. Start with \`openwiki/quickstart.md\`, then follow its links to architecture, workflows, domain concepts, operations, integrations, testing guidance, and source maps. -The scheduled OpenWiki GitHub Actions workflow refreshes the repository wiki. Do not hand-edit generated OpenWiki pages unless explicitly asked; prefer updating source code/docs and letting OpenWiki regenerate. +The scheduled OpenWiki CI workflow refreshes the repository wiki when configured. Do not hand-edit generated OpenWiki pages unless explicitly asked; prefer updating source code/docs and letting OpenWiki regenerate. ${OPENWIKI_AGENTS_SNIPPET_END}`; } diff --git a/src/commands.ts b/src/commands.ts index eaf84c4e..60b8b115 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -658,7 +658,7 @@ export const helpContent: HelpContent = { { label: "openwiki code", description: - "Run OpenWiki for the current repository, writing docs under repo openwiki/ and using GitHub Actions for recurrence.", + "Run OpenWiki for the current repository, writing docs under repo openwiki/ with optional GitHub Actions or GitLab CI/CD recurrence.", }, { label: "openwiki personal", diff --git a/src/credentials.tsx b/src/credentials.tsx index 81e5431c..24d138b7 100644 --- a/src/credentials.tsx +++ b/src/credentials.tsx @@ -52,6 +52,10 @@ import { import type { AuthProviderId } from "./auth/types.js"; import type { OpenWikiRunMode } from "./commands.js"; import type { ConnectorId } from "./connectors/types.js"; +import { + ensureCodeModeRepoSetup, + type CodeModeAutomation, +} from "./code-mode.js"; import { getConnectorConfigPath } from "./openwiki-home.js"; import { getSavedEnvValue, @@ -113,6 +117,7 @@ type InitSetupProps = { type PromptStep = | "api-key" | "base-url" + | "code-automation" | "code-repo-confirm" | "code-repo-path" | "final" @@ -377,6 +382,14 @@ const SOURCE_CONTINUE_OPTIONS = [ ] as const; const FINAL_OPTIONS = ["Run ingestion now", "Run later"] as const; const CODE_REPO_OPTIONS = ["Confirm and continue", "Edit path"] as const; +const CODE_AUTOMATION_OPTIONS = [ + { id: "github", label: "GitHub Actions" }, + { id: "gitlab", label: "GitLab CI/CD pipeline" }, + { id: "none", label: "Do not create a CI file" }, +] as const satisfies readonly { + id: CodeModeAutomation; + label: string; +}[]; export function needsCredentialSetup( modelIdOverride: string | null = null, @@ -709,6 +722,8 @@ export function InitSetup({ useState(0); const [templateSelectionIndex, setTemplateSelectionIndex] = useState(0); const [cronModeSelectionIndex, setCronModeSelectionIndex] = useState(0); + const [codeAutomationSelectionIndex, setCodeAutomationSelectionIndex] = + useState(0); const [powerModeSelectionIndex, setPowerModeSelectionIndex] = useState(0); const [cronFieldSelectionIndex, setCronFieldSelectionIndex] = useState(0); const [cronReplaceCurrentField, setCronReplaceCurrentField] = useState(true); @@ -1072,6 +1087,9 @@ export function InitSetup({ setCodeRepoSelectionIndex(0); setInput(""); break; + case "code-automation": + setInput(""); + break; case "code-repo-path": setCodeRepoPathInput(codeRepoRoot); break; @@ -1241,6 +1259,19 @@ export function InitSetup({ return; } + if (step === "code-automation") { + handleMenuInput(key, () => + setCodeAutomationSelectionIndex((index) => + moveSelectionIndex( + index, + key.upArrow ? -1 : 1, + CODE_AUTOMATION_OPTIONS.length, + ), + ), + ); + return; + } + if (step === "source-menu") { handleMenuInput(key, () => setSourceSelectionIndex((index) => @@ -1927,7 +1958,8 @@ export function InitSetup({ setInput(""); if (isCodeMode(nextConfig)) { - setStep("final"); + setCodeAutomationSelectionIndex(0); + setStep("code-automation"); return; } @@ -2146,6 +2178,11 @@ export function InitSetup({ return; } + if (step === "code-automation") { + setStep("final"); + return; + } + if (step === "final") { const runIngestionNow = FINAL_OPTIONS[finalSelectionIndex] === "Run ingestion now"; @@ -2153,7 +2190,21 @@ export function InitSetup({ ...onboardingConfig, completedAt: new Date().toISOString(), }; + + if (selectedMode === "code") { + const automation = + CODE_AUTOMATION_OPTIONS[codeAutomationSelectionIndex]?.id ?? "none"; + + try { + await ensureCodeModeRepoSetup(codeRepoRoot, automation); + } catch (setupError) { + setError(getErrorMessage(setupError)); + return; + } + } + await saveConfigForCurrentMode(nextConfig); + onComplete({ mode: selectedMode, modelId: @@ -2302,7 +2353,8 @@ export function InitSetup({ return; } - setStep("final"); + setCodeAutomationSelectionIndex(0); + setStep("code-automation"); } async function completeSetup(options: CompleteSetupOptions) { @@ -2935,6 +2987,7 @@ export function InitSetup({ {step ? ( + Set up automatic OpenWiki updates? + + Choose a CI provider, or continue without creating a CI file. + + + {CODE_AUTOMATION_OPTIONS.map((option, index) => ( + + {" "} + {option.label} + + ))} + + + GitHub writes .github/workflows/openwiki-update.yml; GitLab writes + .gitlab-ci.yml. + + Use up/down arrows, then press Enter. + + ); + } + if (step === "code-repo-confirm") { return ( diff --git a/test/code-mode.test.ts b/test/code-mode.test.ts index 11df0650..cda6c94e 100644 --- a/test/code-mode.test.ts +++ b/test/code-mode.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, test } from "vitest"; import { ensureCodeModeRepoSetup } from "../src/code-mode.ts"; +import { OPENWIKI_VERSION } from "../src/constants.ts"; const SNIPPET_START = ""; const SNIPPET_END = ""; @@ -100,11 +101,24 @@ Trailing notes that must survive. }); describe("ensureCodeModeRepoSetup workflow", () => { - test("generated PR includes agent files and the workflow in add-paths", async () => { + test("does not create a CI file without an explicit choice", async () => { const repo = await createTempRepo(); await ensureCodeModeRepoSetup(repo); + expect( + await readIfPresent( + path.join(repo, ".github", "workflows", "openwiki-update.yml"), + ), + ).toBeNull(); + expect(await readIfPresent(path.join(repo, ".gitlab-ci.yml"))).toBeNull(); + }); + + test("GitHub Actions includes agent files and the workflow in add-paths", async () => { + const repo = await createTempRepo(); + + await ensureCodeModeRepoSetup(repo, "github"); + const workflow = await readIfPresent( path.join(repo, ".github", "workflows", "openwiki-update.yml"), ); @@ -118,5 +132,29 @@ describe("ensureCodeModeRepoSetup workflow", () => { ]) { expect(workflow).toContain(managedPath); } + expect(workflow).toContain("actions/checkout@11bd7190"); + expect(workflow).toContain( + `npm install --global openwiki@${OPENWIKI_VERSION}`, + ); + }); + + test("GitLab CI uses the standard filename and GitLab-managed paths", async () => { + const repo = await createTempRepo(); + + await ensureCodeModeRepoSetup(repo, "gitlab"); + + const pipeline = await readIfPresent(path.join(repo, ".gitlab-ci.yml")); + expect(pipeline).not.toBeNull(); + expect(pipeline).toContain("openwiki_update:"); + expect(pipeline).toContain("$CI_PIPELINE_SOURCE"); + expect(pipeline).toContain( + "git add openwiki AGENTS.md CLAUDE.md .gitlab-ci.yml", + ); + expect(pipeline).not.toContain(".github/workflows/openwiki-update.yml"); + expect( + await readIfPresent( + path.join(repo, ".github", "workflows", "openwiki-update.yml"), + ), + ).toBeNull(); }); });