From d7ba226342357194d78da1fd8479da5632320c64 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Tue, 12 May 2026 00:44:08 +0200 Subject: [PATCH] fix(ci): resolve smoke test race condition and add explicit CodeQL workflow The Validate and Release workflows trigger concurrently on release-commit pushes to main. Smoke tests that naively query the latest release tag get a 404 because release-binaries.yml hasn't uploaded the platform assets yet. Fix the race by iterating recent releases and selecting the first one that has the expected asset uploaded. Also add an explicit CodeQL workflow to replace GitHub's default setup, which silently skipped PR #279 and dropped the Scorecard SAST score to 9/10. Signed-off-by: Rhuan Barreto --- .../agent-memory/archgate-developer/MEMORY.md | 2 + .github/workflows/codeql.yml | 52 +++++++++++++++++++ .github/workflows/scorecard.yml | 2 +- .github/workflows/smoke-test-linux.yml | 22 +++++++- .github/workflows/smoke-test-windows.yml | 25 +++++++-- 5 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/codeql.yml 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"