diff --git a/.github/workflows/kc-check-imports.yml b/.github/workflows/kc-check-imports.yml new file mode 100644 index 0000000000..2896c4e9b3 --- /dev/null +++ b/.github/workflows/kc-check-imports.yml @@ -0,0 +1,128 @@ +name: kc-check-imports + +on: + pull_request: + branches: + - "ga/**" + - "lts/**" + workflow_dispatch: + inputs: + baseline: + description: "Baseline ref (tag or SHA) to validate against" + required: false + default: "v2026.4.20" + type: string + range: + description: "Range or candidates (e.g. origin/ga/1.0..HEAD)" + required: false + default: "" + type: string + +permissions: + contents: read + pull-requests: write + +jobs: + check-imports: + runs-on: ubuntu-latest + steps: + - name: Checkout repo (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch baseline tag + run: | + set -euo pipefail + baseline="${{ inputs.baseline || 'v2026.4.20' }}" + if ! git rev-parse "$baseline" >/dev/null 2>&1; then + echo "Baseline ref '$baseline' not found locally; fetching tags from origin." + git fetch origin --tags --force + fi + if ! git rev-parse "$baseline" >/dev/null 2>&1; then + echo "ERROR: baseline ref '$baseline' not found after fetch. Push the tag to origin or pass --baseline=." + exit 1 + fi + + - name: Checkout kissclaw-tools + uses: actions/checkout@v4 + with: + repository: MachineWisdomAI/kissclaw-tools + path: .kissclaw-tools + token: ${{ secrets.KISSCLAW_TOOLS_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install kissclaw-tools dependencies + working-directory: .kissclaw-tools + run: | + if [ -f pnpm-lock.yaml ]; then + pnpm install --frozen-lockfile + elif [ -f package.json ]; then + pnpm install + else + echo "kissclaw-tools has no package.json yet; installing typescript locally for module resolution." + npm install --no-save typescript + fi + + - name: Install repo dependencies (for TS module resolution) + run: pnpm install --frozen-lockfile + + - name: Resolve range + id: range + run: | + set -euo pipefail + if [ -n "${{ inputs.range }}" ]; then + range="${{ inputs.range }}" + elif [ "${{ github.event_name }}" = "pull_request" ]; then + base_ref="origin/${{ github.base_ref }}" + git fetch origin "${{ github.base_ref }}" --depth=200 + range="${base_ref}..HEAD" + else + range="origin/ga/1.0..HEAD" + fi + echo "range=$range" >> "$GITHUB_OUTPUT" + + - name: kc-check-imports — per-candidate + id: candidates + run: | + set -euo pipefail + baseline="${{ inputs.baseline || 'v2026.4.20' }}" + range="${{ steps.range.outputs.range }}" + echo "Running kc-check-imports --candidates $range against baseline $baseline" + node .kissclaw-tools/kc-check-imports.mjs \ + --baseline "$baseline" \ + --candidates "$range" \ + --repo "$PWD" \ + > kc-check-imports-candidates.json + cat kc-check-imports-candidates.json + continue-on-error: false + + - name: kc-check-imports — final-tree + if: always() + run: | + set -euo pipefail + baseline="${{ inputs.baseline || 'v2026.4.20' }}" + range="${{ steps.range.outputs.range }}" + echo "Running kc-check-imports --final-tree $range against baseline $baseline" + node .kissclaw-tools/kc-check-imports.mjs \ + --baseline "$baseline" \ + --final-tree "$range" \ + --repo "$PWD" \ + > kc-check-imports-final-tree.json + cat kc-check-imports-final-tree.json + + - name: Upload reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: kc-check-imports-reports + path: | + kc-check-imports-candidates.json + kc-check-imports-final-tree.json diff --git a/.github/workflows/kissclaw-release.yml b/.github/workflows/kissclaw-release.yml index bbecd4a9cc..ca66c9e388 100644 --- a/.github/workflows/kissclaw-release.yml +++ b/.github/workflows/kissclaw-release.yml @@ -10,23 +10,41 @@ on: required: true type: string +# `contents: write` is required for `gh release upload`. The default-branch +# defense-in-depth check below prevents this write capability from being abused +# to publish builds from `main` (the upstream mirror). permissions: - contents: read + contents: write jobs: build: runs-on: ubuntu-latest steps: + - name: Resolve build ref + id: ref + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "release" ]; then + ref="${{ github.event.release.tag_name }}" + elif [ -n "${{ inputs.ref }}" ]; then + ref="${{ inputs.ref }}" + else + ref="${{ github.ref }}" + fi + echo "ref=$ref" >> "$GITHUB_OUTPUT" + - name: Checkout release ref uses: actions/checkout@v4 with: - ref: ${{ inputs.ref || github.ref }} + ref: ${{ steps.ref.outputs.ref }} + fetch-depth: 0 - name: Verify checkout integrity run: | set -euo pipefail - intended_ref="${{ inputs.ref || github.ref }}" - # Annotated tags need dereferencing to get the commit SHA. + intended_ref="${{ steps.ref.outputs.ref }}" + # Annotated tags need ^{commit} dereferencing; rev-list -n 1 handles both + # annotated tags and branches consistently. intended_sha=$(git rev-list -n 1 "$intended_ref") actual_sha=$(git rev-parse HEAD) if [ "$intended_sha" != "$actual_sha" ]; then @@ -34,6 +52,8 @@ jobs: exit 1 fi default_branch=$(gh api "repos/${{ github.repository }}" --jq .default_branch) + # actions/checkout@v4 only fetches the requested ref by default, so origin/$default_branch + # may not exist locally. Ask GitHub for the SHA directly. default_sha=$(gh api "repos/${{ github.repository }}/git/refs/heads/$default_branch" --jq .object.sha) if [ "$actual_sha" = "$default_sha" ]; then echo "ERROR: HEAD equals default-branch HEAD ($default_sha). Release builds must be from a tag or release branch, not the upstream mirror." @@ -58,38 +78,63 @@ jobs: run: pnpm build - name: Package tarball + id: pack run: | set -euo pipefail version=$(node -p "require('./package.json').version") name=$(node -p "require('./package.json').name") + # pnpm pack writes -.tgz. We renormalize defensively to + # "kissclaw-.tgz" so the release asset name is deterministic + # regardless of the package.json name field. Guarded so we don't mv onto + # ourselves when the names already agree. pnpm pack - tarball="${name}-${version}.tgz" - mv *.tgz "kissclaw-${version}.tgz" - tarball="kissclaw-${version}.tgz" - sha256sum "$tarball" > "${tarball}.sha256" - echo "TARBALL=$tarball" >> "$GITHUB_ENV" - echo "VERSION=$version" >> "$GITHUB_ENV" + src="${name}-${version}.tgz" + dst="kissclaw-${version}.tgz" + if [ ! -f "$src" ]; then + src=$(ls *.tgz | head -n 1) + fi + if [ "$src" != "$dst" ]; then + mv "$src" "$dst" + fi + sha256sum "$dst" > "${dst}.sha256" + { + echo "tarball=$dst" + echo "version=$version" + } >> "$GITHUB_OUTPUT" - name: Generate release metadata run: | set -euo pipefail + built_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) + sha=$(git rev-parse HEAD) cat > "kissclaw-release-metadata.json" <