diff --git a/.claude/agent-memory/archgate-developer/MEMORY.md b/.claude/agent-memory/archgate-developer/MEMORY.md index dea0285..aebc2a4 100644 --- a/.claude/agent-memory/archgate-developer/MEMORY.md +++ b/.claude/agent-memory/archgate-developer/MEMORY.md @@ -52,6 +52,8 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv - **Inquirer prompts leave cursor at wrong column on Windows — always reset with `cursorTo`** — After an `inquirer.prompt()` call finishes (especially checkbox with long wrapped answer lines), the cursor is left at the end of the rendered answer text. On Windows terminals, `\n` moves down but does NOT reset to column 0, so all subsequent output starts at the wrong horizontal offset. Fix: call `cursorTo(process.stdout, 0)` from `node:readline` after each prompt returns, guarded by `if (process.stdout.isTTY)`. Applied in `src/helpers/editor-detect.ts` for `promptEditorSelection()` and `promptSingleEditorSelection()`. The `upgrade.ts` command already uses the same `clearLine`/`cursorTo` pattern for download progress cleanup. - **`inquirer` must be lazy-loaded via dynamic `import()` — never statically imported at module level** — `inquirer` costs ~200ms to parse. Static `import inquirer from "inquirer"` at module level forces every CLI invocation to pay this cost — even `--help`, `--version`, and non-interactive commands that never prompt. Always use `const { default: inquirer } = await import("inquirer")` at the call site, inside the action handler or function that actually needs it. Applied in `src/commands/adr/create.ts`, `src/commands/init.ts`, `src/helpers/editor-detect.ts`, `src/helpers/login-flow.ts`. Same principle applies to any heavy dependency used only in specific code paths (e.g., Sentry and PostHog SDKs are already lazy-loaded in `sentry.ts` and `telemetry.ts`). - **Telemetry/Sentry init should be started eagerly but awaited lazily** — In `src/cli.ts`, `initSentry()` + `initTelemetry()` are started before command registration (so they run concurrently with setup) but only awaited in the `preAction` hook (right before the first telemetry event). This defers ~150ms of SDK parsing + git subprocess cost for `--help`/`--version` which never trigger `preAction`. The `preAction` hook is `async` to support this `await`. +- **Smoke test install-script steps must find a release with uploaded assets, not just the latest tag** — On release-commit pushes to main, the Validate and Release workflows trigger concurrently. The Release workflow creates the tag/release before `release-binaries.yml` uploads platform binaries. Smoke tests that naively use `gh release view --json tagName` get the just-created release and 404 on the binary download. Fix: iterate `gh release list --limit 5` and check each release's assets for the expected artifact (`archgate-win32-x64.zip` / `archgate-linux-x64.tar.gz`) before selecting the version. Applied in `smoke-test-windows.yml` and `smoke-test-linux.yml`. +- **GitHub CodeQL "default setup" can silently skip PRs — use an explicit workflow** — The repository-level "default setup" for CodeQL does not guarantee analysis on every PR. PR #279 (Renovate deps update) had zero CodeQL analyses, dropping the Scorecard SAST score from 10 to 9. Fix: add an explicit `.github/workflows/codeql.yml` that runs on `push: [main]`, `pull_request: [main]`, and a weekly schedule. After merging, disable the "default setup" in repository Settings > Code security > Code scanning to avoid duplicate analyses. The explicit workflow gives Scorecard a detectable `github/codeql-action` reference and guarantees coverage. - **`GITHUB_TOKEN`-authored pushes do NOT trigger downstream workflows — release.yml MUST use the GH App token** — When an Actions workflow pushes commits or opens PRs using `${{ github.token }}` / `secrets.GITHUB_TOKEN`, GitHub intentionally suppresses the resulting `push` / `pull_request` events to prevent recursion. Symptom on release PRs: the head SHA has no `pull_request`-event check runs, so `Validate Code` / `Lint, Test & Check` / `DCO Sign-off Check` are missing from the PR rollup and branch protection treats the PR as missing required checks. PR [#131](https://github.com/archgate/cli/pull/131) papered over this by manually `gh workflow run` + posting commit statuses, but `workflow_dispatch` runs land on `head_branch: release` with `pull_requests: []` — they are not associated with the PR ref, so `Lint, Test & Check` stayed orphaned and the bug recurred on PR [#251](https://github.com/archgate/cli/pull/251). Root-cause fix: in `release.yml` the `pull-request` job MUST generate a GitHub App installation token via `actions/create-github-app-token` (using `secrets.GH_APP_APP_ID` / `secrets.GH_APP_PRIVATE_KEY`) and pass it to BOTH `actions/checkout` and `simple-release-action`. App-token-authored pushes DO trigger `pull_request` events naturally. Apply the same pattern to any future workflow that pushes to a branch whose downstream CI must run. ## Validation Pipeline diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d3e55bc --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,52 @@ +name: CodeQL + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + # Weekly Monday 06:30 UTC (offset from Scorecard at 06:15) + - cron: "30 6 * * 1" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + ARCHGATE_TELEMETRY: "0" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + security-events: write + contents: read + + strategy: + fail-fast: false + matrix: + language: + - javascript-typescript + - actions + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + with: + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f8db4ea..87ff6c7 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -40,6 +40,6 @@ jobs: publish_results: true - name: Upload SARIF to GitHub Security tab - uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: sarif_file: results.sarif diff --git a/.github/workflows/smoke-test-linux.yml b/.github/workflows/smoke-test-linux.yml index 8fcd50d..8491056 100644 --- a/.github/workflows/smoke-test-linux.yml +++ b/.github/workflows/smoke-test-linux.yml @@ -72,11 +72,29 @@ jobs: GH_TOKEN: ${{ github.token }} run: | if [ -z "$ARCHGATE_VERSION" ]; then - ARCHGATE_VERSION="$(gh release view --json tagName --jq .tagName 2>/dev/null || true)" - if [ -z "$ARCHGATE_VERSION" ]; then + # Find the newest release that already has the Linux asset uploaded. + # On release-commit pushes the Validate and Release workflows start + # concurrently, so the latest tag may exist before its binaries are + # uploaded by release-binaries.yml — hitting a 404. + asset="archgate-linux-x64.tar.gz" + tags="$(gh release list --limit 5 --json tagName --jq '.[].tagName' 2>/dev/null || true)" + if [ -z "$tags" ]; then echo "::warning::No releases found, skipping install.sh smoke test" exit 0 fi + found="" + for tag in $tags; do + assets="$(gh release view "$tag" --json assets --jq '.assets[].name' 2>/dev/null || true)" + if echo "$assets" | grep -qF "$asset"; then + ARCHGATE_VERSION="$tag" + found=1 + break + fi + done + if [ -z "$found" ]; then + echo "::warning::No release with $asset found, skipping install.sh smoke test" + exit 0 + fi export ARCHGATE_VERSION fi echo "Testing install.sh with version $ARCHGATE_VERSION" diff --git a/.github/workflows/smoke-test-windows.yml b/.github/workflows/smoke-test-windows.yml index 37ea6a8..04a0d11 100644 --- a/.github/workflows/smoke-test-windows.yml +++ b/.github/workflows/smoke-test-windows.yml @@ -114,12 +114,31 @@ jobs: GH_TOKEN: ${{ github.token }} run: | if (-not $env:ARCHGATE_VERSION) { - $release = gh release view --json tagName --jq .tagName 2>$null - if ($LASTEXITCODE -ne 0 -or -not $release) { + # Find the newest release that already has the Windows asset uploaded. + # On release-commit pushes the Validate and Release workflows start + # concurrently, so the latest tag may exist before its binaries are + # uploaded by release-binaries.yml — hitting a 404. + $asset = "archgate-win32-x64.zip" + $tags = gh release list --limit 5 --json tagName --jq '.[].tagName' 2>$null + if ($LASTEXITCODE -ne 0 -or -not $tags) { Write-Host "::warning::No releases found, skipping install.ps1 smoke test" exit 0 } - $env:ARCHGATE_VERSION = $release + $found = $false + foreach ($tag in $tags -split "`n") { + $tag = $tag.Trim() + if (-not $tag) { continue } + $assets = gh release view $tag --json assets --jq '.assets[].name' 2>$null + if ($assets -match [regex]::Escape($asset)) { + $env:ARCHGATE_VERSION = $tag + $found = $true + break + } + } + if (-not $found) { + Write-Host "::warning::No release with $asset found, skipping install.ps1 smoke test" + exit 0 + } } Write-Host "Testing install.ps1 with version $env:ARCHGATE_VERSION"