|
| 1 | +# ============================================================================= |
| 2 | +# Drift-check composite action |
| 3 | +# ============================================================================= |
| 4 | +# |
| 5 | +# Reusable action invoked by: |
| 6 | +# - the meta-repo's own drift-check.yml workflow (mode: all) |
| 7 | +# - tool-repo validate.yml workflows (mode: self) — wired up in Session D |
| 8 | +# |
| 9 | +# What it does: |
| 10 | +# 1. Checks out THIS action's repo (TMHSDigital/Developer-Tools-Directory), |
| 11 | +# which is where the drift_check Python package lives. Tool repos that |
| 12 | +# consume this action only need to reference it by tag; they do NOT |
| 13 | +# need to vendor the checker. |
| 14 | +# 2. Sets up Python and installs runtime deps (drift_check is stdlib-only |
| 15 | +# today, but pip install -r requirements.txt runs anyway in case it |
| 16 | +# grows deps in Phase 3). |
| 17 | +# 3. Runs the checker in `self` mode (caller's checkout) or `all` mode |
| 18 | +# (every active repo in registry.json via sparse-checkout). |
| 19 | +# 4. Optionally upserts the sticky drift issue (meta-repo only). |
| 20 | +# 5. Writes a step summary and exposes counts as outputs so the calling |
| 21 | +# workflow can branch on them. |
| 22 | +# |
| 23 | +# Pinning rule: tool repos MUST consume this action via @v1.7 (or a SHA), |
| 24 | +# never @main. The drift checker's release pipeline maintains the v1.7 tag |
| 25 | +# pointing at the latest 1.7.x. Q9 of the design doc explains why @main is |
| 26 | +# forbidden for tool-repo callers (would break every tool-repo PR on |
| 27 | +# unrelated meta-repo changes). |
| 28 | +# ============================================================================= |
| 29 | + |
| 30 | +name: 'Agent-file drift check' |
| 31 | +description: 'Run the drift checker in self or all mode and optionally upsert the sticky issue.' |
| 32 | +author: 'TMHSDigital' |
| 33 | + |
| 34 | +inputs: |
| 35 | + mode: |
| 36 | + description: '"self" to check only the calling repo (uses GITHUB_WORKSPACE); "all" to check every active registry.json entry via sparse-checkout.' |
| 37 | + required: true |
| 38 | + default: 'self' |
| 39 | + format: |
| 40 | + description: 'Output format: markdown | json | gh-summary. Defaults to gh-summary so step summary is populated.' |
| 41 | + required: false |
| 42 | + default: 'gh-summary' |
| 43 | + github-token: |
| 44 | + description: 'GitHub token. Required for mode=all (sparse-checkout) and update-sticky-issue. Optional for mode=self.' |
| 45 | + required: false |
| 46 | + default: '' |
| 47 | + update-sticky-issue: |
| 48 | + description: 'true to upsert the sticky drift report issue. Only set true in the meta-repo workflow.' |
| 49 | + required: false |
| 50 | + default: 'false' |
| 51 | + python-version: |
| 52 | + description: 'Python interpreter for the checker.' |
| 53 | + required: false |
| 54 | + default: '3.11' |
| 55 | + meta-repo-ref: |
| 56 | + description: 'git ref of TMHSDigital/Developer-Tools-Directory to use for the checker code. Defaults to v1.7 (latest 1.7.x).' |
| 57 | + required: false |
| 58 | + default: 'v1.7' |
| 59 | + caller-path: |
| 60 | + description: 'When mode=self, path inside GITHUB_WORKSPACE that points at the caller checkout. Defaults to "." (root).' |
| 61 | + required: false |
| 62 | + default: '.' |
| 63 | + |
| 64 | +outputs: |
| 65 | + exit-code: |
| 66 | + description: 'drift checker exit code: 0=clean, 1=drift, 2=tool error' |
| 67 | + value: ${{ steps.run.outputs.exit-code }} |
| 68 | + error-count: |
| 69 | + description: 'number of error-severity findings' |
| 70 | + value: ${{ steps.run.outputs.error-count }} |
| 71 | + warning-count: |
| 72 | + description: 'number of warning-severity findings' |
| 73 | + value: ${{ steps.run.outputs.warning-count }} |
| 74 | + sticky-issue-action: |
| 75 | + description: 'one of: created, updated, reopened, closed, no_op, skipped' |
| 76 | + value: ${{ steps.sticky.outputs.action }} |
| 77 | + |
| 78 | +runs: |
| 79 | + using: 'composite' |
| 80 | + steps: |
| 81 | + # The drift checker code lives in this repo. Whether the caller is the |
| 82 | + # meta-repo itself or a tool repo, we always need the checker code at |
| 83 | + # a known path. Use a dedicated subdirectory so we don't clobber the |
| 84 | + # caller's own checkout (which lives at GITHUB_WORKSPACE). |
| 85 | + - name: Checkout drift checker |
| 86 | + uses: actions/checkout@v5 |
| 87 | + with: |
| 88 | + repository: TMHSDigital/Developer-Tools-Directory |
| 89 | + ref: ${{ inputs.meta-repo-ref }} |
| 90 | + path: .drift-checker |
| 91 | + |
| 92 | + - name: Set up Python |
| 93 | + uses: actions/setup-python@v5 |
| 94 | + with: |
| 95 | + python-version: ${{ inputs.python-version }} |
| 96 | + |
| 97 | + - name: Install dependencies (best-effort) |
| 98 | + shell: bash |
| 99 | + working-directory: .drift-checker |
| 100 | + run: | |
| 101 | + if [ -f requirements.txt ]; then |
| 102 | + pip install -r requirements.txt |
| 103 | + else |
| 104 | + echo "no requirements.txt — drift_check is stdlib-only, skipping" |
| 105 | + fi |
| 106 | +
|
| 107 | + - name: Resolve meta-commit |
| 108 | + id: meta |
| 109 | + shell: bash |
| 110 | + working-directory: .drift-checker |
| 111 | + run: | |
| 112 | + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" |
| 113 | + echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" |
| 114 | +
|
| 115 | + - name: Run drift checker |
| 116 | + id: run |
| 117 | + shell: bash |
| 118 | + working-directory: .drift-checker |
| 119 | + env: |
| 120 | + DRIFT_CHECK_TOKEN: ${{ inputs.github-token }} |
| 121 | + run: | |
| 122 | + set +e |
| 123 | + EXTRA_ARGS=() |
| 124 | + if [ "${{ inputs.mode }}" = "all" ]; then |
| 125 | + EXTRA_ARGS+=(--all) |
| 126 | + elif [ "${{ inputs.mode }}" = "self" ]; then |
| 127 | + EXTRA_ARGS+=(--local "$GITHUB_WORKSPACE/${{ inputs.caller-path }}") |
| 128 | + else |
| 129 | + echo "::error::invalid mode: ${{ inputs.mode }} (expected 'self' or 'all')" |
| 130 | + exit 2 |
| 131 | + fi |
| 132 | +
|
| 133 | + # Always emit JSON to a side-channel file so we can extract counts |
| 134 | + # for outputs, then write the requested format separately. |
| 135 | + python scripts/drift_check/cli.py \ |
| 136 | + "${EXTRA_ARGS[@]}" \ |
| 137 | + --format json \ |
| 138 | + --output /tmp/drift-findings.json \ |
| 139 | + --meta-commit "${{ steps.meta.outputs.sha }}" |
| 140 | + FINDINGS_RC=$? |
| 141 | +
|
| 142 | + # Extract counts (default to 0 if jq missing or file absent). |
| 143 | + ERR=0; WARN=0 |
| 144 | + if command -v jq >/dev/null 2>&1 && [ -f /tmp/drift-findings.json ]; then |
| 145 | + ERR=$(jq -r '.summary.errors // 0' /tmp/drift-findings.json) |
| 146 | + WARN=$(jq -r '.summary.warnings // 0' /tmp/drift-findings.json) |
| 147 | + fi |
| 148 | + echo "exit-code=$FINDINGS_RC" >> "$GITHUB_OUTPUT" |
| 149 | + echo "error-count=$ERR" >> "$GITHUB_OUTPUT" |
| 150 | + echo "warning-count=$WARN" >> "$GITHUB_OUTPUT" |
| 151 | +
|
| 152 | + # Now render the user-requested format (which writes to step summary |
| 153 | + # automatically when format=gh-summary). |
| 154 | + if [ "${{ inputs.format }}" != "json" ]; then |
| 155 | + python scripts/drift_check/cli.py \ |
| 156 | + "${EXTRA_ARGS[@]}" \ |
| 157 | + --format "${{ inputs.format }}" \ |
| 158 | + --meta-commit "${{ steps.meta.outputs.sha }}" |
| 159 | + fi |
| 160 | +
|
| 161 | + # Action exit code is decoupled from drift-checker exit code; we |
| 162 | + # surface drift via outputs so the calling workflow chooses how |
| 163 | + # to react. Composite action exits 0 unless the checker hit a |
| 164 | + # tool error (rc=2). |
| 165 | + if [ "$FINDINGS_RC" = "2" ]; then |
| 166 | + exit 2 |
| 167 | + fi |
| 168 | + exit 0 |
| 169 | +
|
| 170 | + - name: Upsert sticky issue |
| 171 | + id: sticky |
| 172 | + if: ${{ inputs.update-sticky-issue == 'true' }} |
| 173 | + shell: bash |
| 174 | + working-directory: .drift-checker |
| 175 | + env: |
| 176 | + DRIFT_CHECK_TOKEN: ${{ inputs.github-token }} |
| 177 | + GH_TOKEN: ${{ inputs.github-token }} |
| 178 | + run: | |
| 179 | + set +e |
| 180 | + if [ -z "${{ inputs.github-token }}" ]; then |
| 181 | + echo "::warning::update-sticky-issue=true but github-token is empty; skipping" |
| 182 | + echo "action=skipped" >> "$GITHUB_OUTPUT" |
| 183 | + exit 0 |
| 184 | + fi |
| 185 | + EXTRA_ARGS=() |
| 186 | + if [ "${{ inputs.mode }}" = "all" ]; then |
| 187 | + EXTRA_ARGS+=(--all) |
| 188 | + else |
| 189 | + EXTRA_ARGS+=(--local "$GITHUB_WORKSPACE/${{ inputs.caller-path }}") |
| 190 | + fi |
| 191 | + OUT=$(python scripts/drift_check/cli.py \ |
| 192 | + "${EXTRA_ARGS[@]}" \ |
| 193 | + --format json \ |
| 194 | + --output /dev/null \ |
| 195 | + --update-sticky-issue \ |
| 196 | + --meta-commit "${{ steps.meta.outputs.sha }}" 2>&1) |
| 197 | + RC=$? |
| 198 | + echo "$OUT" |
| 199 | + # Parse the "Sticky issue: <action>" line emitted by cli.py. |
| 200 | + ACTION=$(echo "$OUT" | grep -oE 'Sticky issue: (created|updated|reopened|closed|no_op)' | head -1 | awk '{print $3}') |
| 201 | + echo "action=${ACTION:-unknown}" >> "$GITHUB_OUTPUT" |
| 202 | + if [ "$RC" = "2" ]; then |
| 203 | + exit 2 |
| 204 | + fi |
| 205 | + exit 0 |
0 commit comments