Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 53 additions & 64 deletions .github/workflows/merge-gate.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
# Merge Gate -- single-authority orchestrator that aggregates ALL required
# PR-time checks into one verdict. Branch protection requires only this
# check; this workflow verifies all underlying checks via the Checks API.
# check; this workflow polls the Checks API for all underlying checks.
#
# Why this file exists:
# GitHub's required-status-checks model is name-based, not workflow-based.
# We tier our CI: ci.yml runs Tier 1 on `pull_request` and emits
# `Build & Test (Linux)`, while ci-integration-pr-stub.yml stubs the four
# Tier 2 checks via `pull_request_target`. That asymmetry means required
# checks depend on TWO independent webhook deliveries succeeding. If the
# `pull_request` event is dropped (transient, observed on PR #856), 4/5
# stubs go green and the 5th hangs in "Expected -- Waiting" indefinitely.
# Our CI is tiered: ci.yml emits 'Build & Test (Linux)' and
# ci-integration-pr-stub.yml emits four stubs that hold positions for
# merge-queue jobs in ci-integration.yml. Without this gate, branch
# protection had to require all 5 checks individually -- adding or
# renaming a check meant a ruleset edit. With this gate, branch
# protection requires only 'Merge Gate / gate' and the gate aggregates.
# Tide / bors pattern.
#
# This workflow collapses N separately-required checks into a single
# `Merge Gate / gate` check that:
# - dispatches via two redundant triggers (pull_request +
# pull_request_target) so a single dropped delivery is recoverable;
# - polls the Checks API for ALL underlying required checks;
# - exits red if any check fails, never appears, or never completes;
# - is the SOLE required check, decoupling branch protection from
# workflow topology (Tide / bors pattern).
# Why a single trigger (not dual pull_request + pull_request_target):
# We tried dual-trigger redundancy in PR #865 to harden against rare
# dropped 'pull_request' webhook deliveries (observed once on PR #856).
# It backfired: 'concurrency: cancel-in-progress' produced TWO check-runs
# per SHA -- one SUCCESS and one CANCELLED -- which poisons branch
# protection's status-check rollup ('CANCELLED' counts as failure ->
# PR BLOCKED). No GitHub Actions primitive cleanly de-duplicates checks
# across event channels. World-class OSS projects (k8s, rust, deno,
# next.js) accept this and use a single trigger plus manual recovery.
#
# Security:
# `pull_request_target` is used here for redundancy ONLY. This workflow
# never checks out PR code under that trigger, never interpolates PR data
# into `run:`, and has read-only token permissions. The classic
# pull_request_target+checkout(head) exploit is impossible by construction.
# See ci-integration-pr-stub.yml for the same security model.
# Recovery if a 'pull_request' webhook is dropped:
# - Push an empty commit: git commit --allow-empty -m 'retrigger' && git push
# - Or trigger manually: gh workflow run merge-gate.yml -f pr_number=NNN
# - Or close + reopen the PR.

name: Merge Gate

Expand All @@ -36,18 +36,18 @@ on:
- 'docs/**'
- '.gitignore'
- 'LICENSE'
pull_request_target:
branches: [ main ]
types: [ opened, synchronize, reopened, ready_for_review ]
paths-ignore:
- 'docs/**'
- '.gitignore'
- 'LICENSE'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to re-run the gate against'
required: true
type: string

# Single in-flight gate per PR. If both pull_request and pull_request_target
# fire (the happy redundant case), the second one cancels the first.
# Dedup pushes to the same PR: cancel any older in-flight gate run on
# the same PR head. Now safe -- only one trigger channel, so cancellations
# only happen on rapid push-after-push, not on cross-event collisions.
concurrency:
group: merge-gate-${{ github.event.pull_request.number || github.ref }}
group: merge-gate-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}
cancel-in-progress: true

permissions:
Expand All @@ -61,48 +61,35 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 35
steps:
# On pull_request we can safely checkout PR head: the runner has no
# secrets and a read-only token. Under pull_request_target we MUST NOT
# checkout PR head -- we fetch from the base branch via API instead.
- name: Checkout PR head (pull_request only)
if: github.event_name == 'pull_request'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
sparse-checkout: |
.github/scripts/ci

- name: Fetch script from base (pull_request_target only)
if: github.event_name == 'pull_request_target'
- name: Resolve PR head SHA
id: sha
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p .github/scripts/ci
# Self-bootstrap: if the script does not yet exist on base (i.e. this
# is the PR adding the script), degrade to a no-op that passes. Once
# the script lands on main, this branch becomes a real gate.
status=$(curl -fsSL -o .github/scripts/ci/merge_gate_wait.sh \
--retry 5 --retry-delay 3 --retry-connrefused --max-time 30 \
-w '%{http_code}' \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github.raw" \
"https://api.github.com/repos/${{ github.repository }}/contents/.github/scripts/ci/merge_gate_wait.sh?ref=${GITHUB_BASE_REF}" \
|| echo "404")
if [ "$status" = "404" ] || [ ! -s .github/scripts/ci/merge_gate_wait.sh ]; then
echo "::warning::merge_gate_wait.sh not found on base ref '${GITHUB_BASE_REF}' yet -- self-bootstrap pass."
cat > .github/scripts/ci/merge_gate_wait.sh <<'BOOTSTRAP'
#!/usr/bin/env bash
echo "[merge-gate] self-bootstrap pass: script not yet on base"
exit 0
BOOTSTRAP
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
sha=$(gh api "repos/${{ github.repository }}/pulls/${{ inputs.pr_number }}" --jq '.head.sha')
else
sha="${{ github.event.pull_request.head.sha }}"
fi
chmod +x .github/scripts/ci/merge_gate_wait.sh
if [ -z "$sha" ]; then
echo "::error::Could not resolve PR head SHA"
exit 1
fi
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "[merge-gate] resolved head SHA: $sha"

- name: Checkout PR head
uses: actions/checkout@v4
with:
ref: ${{ steps.sha.outputs.sha }}
sparse-checkout: |
.github/scripts/ci

- name: Wait for all required checks
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.event.pull_request.head.sha }}
SHA: ${{ steps.sha.outputs.sha }}
# All PR-time checks the gate aggregates. Keep this in sync with
# the underlying workflows: ci.yml emits Build & Test (Linux),
# ci-integration-pr-stub.yml emits the other four.
Expand All @@ -114,3 +101,5 @@ jobs:
run: |
chmod +x .github/scripts/ci/merge_gate_wait.sh
.github/scripts/ci/merge_gate_wait.sh


22 changes: 18 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.9.2] - 2026-04-23

### Added

- New `enterprise/governance-guide.md` documentation page: flagship governance reference for CISO / VPE / Platform Tech Lead audiences, covering enforcement points, bypass contract, failure semantics, air-gapped operation, rollout playbook, and known gaps. Trims duplicated content in `governance.md`, `apm-policy.md`, and `integrations/github-rulesets.md`. Adds `templates/apm-policy-starter.yml`. (#851)
- `apm install` now supports Azure DevOps AAD bearer-token auth via `az account get-access-token`, with PAT-first fallback for orgs that disable PAT creation. Closes #852 (#856)
- New CI safety net: `merge-gate.yml` orchestrator turns dropped `pull_request` webhook deliveries into clear red checks instead of stuck `Expected -- Waiting for status to be reported`. Triggers on both `pull_request` and `pull_request_target` for redundancy. (#865) (PR follow-up to #856 CI flake)
- `merge-gate.yml` now aggregates ALL PR-time required checks (`Build & Test (Linux)` + 4 stubs from `ci-integration-pr-stub.yml`) into a single `Merge Gate / gate` verdict. Branch protection requires only this single check, decoupling the ruleset from CI workflow topology (Tide / bors pattern).
- `apm install` supports Azure DevOps AAD bearer-token auth via `az account get-access-token`, with PAT-first fallback for orgs that disable PAT creation. Closes #852 (#856)
- New `enterprise/governance-guide.md`: flagship governance reference for CISO / VPE / Platform Tech Lead audiences; trims duplication across `governance.md`, `apm-policy.md`, `integrations/github-rulesets.md`; adds `templates/apm-policy-starter.yml`. (#851)
- Enterprise docs IA refactor: hub page + merged team guides, deduped governance content. (#858)
- Landing page rewritten around the three-pillar spine. (#855)
- First-package tutorial rewritten end-to-end; fixes `.apm/` anatomy hallucinations. (#866)

### Changed

- gh-aw workflows now use `imports:` for shared APM context instead of the deprecated `dependencies:` field. (#864)
- CI: `merge-gate.yml` orchestrator turns dropped `pull_request` webhook deliveries into clear red checks instead of stuck `Expected -- Waiting for status to be reported`. (#865)
- CI: `Merge Gate / gate` aggregates all PR-time required checks (`Build & Test (Linux)` + 4 stubs) into a single verdict; branch protection requires only this one check, decoupling the ruleset from CI workflow topology (Tide / bors pattern). (#867, #868)
- CI: `merge-gate.yml` simplified to a single `pull_request` trigger with `workflow_dispatch` for manual recovery; the dual-trigger redundancy attempt was poisoning the branch-protection rollup with `CANCELLED` check-runs. (#868)

### Fixed

- `apm install` surfaces the custom port in clone / `ls-remote` error messages for generic git hosts. (#804)

## [0.9.1] - 2026-04-22

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "apm-cli"
version = "0.9.1"
version = "0.9.2"
description = "MCP configuration tool"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
Loading