Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion .github/workflows/action-pins.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Single source of truth for third-party action pins. Dependabot only scans
# `.github/workflows` and a root `action.yml`, so pins used elsewhere — the
# composite actions, `ci:generate-workflow` — are listed here to keep them
# updated. `mise run ci:check-action-pins` verifies every reference agrees.
# updated. `mise run ci:check-action-pins` verifies every reference agrees, and
# `mise run ci:fix-action-pins` copies catalog pins onto consumers.
name: Action pins

# Never runs. The trigger is a branch that is never created, and `!always()` is
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions .mise/tasks/ci/check-action-pins
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ def main() -> int:
for problem in problems:
print(f"error: {problem}", file=sys.stderr)
print(
f"\nUpdate {CATALOG.as_posix()} and the references above so every action "
"resolves to one commit, then run `mise run ci:generate-workflow`.",
f"\nRun `mise run ci:fix-action-pins` to copy catalog pins onto consumers. "
f"Unpinned uses and unused catalog entries still require an edit of "
f"{CATALOG.as_posix()}.",
file=sys.stderr,
)
return 1
Expand Down
34 changes: 34 additions & 0 deletions .mise/tasks/ci/commit-hygiene-fixes
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# [MISE] description="Commit tracked files left dirty by mise run fix."
set -euo pipefail
cd "${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../../.." && pwd)}"

committed=false
git update-index -q --refresh
if ! git diff --quiet || ! git diff --cached --quiet; then
git add --update

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the lockfile exclusion when committing fixes

When mise rewrites any tracked root mise*.lock file during a Dependabot hygiene run, this unrestricted dirty-tree check stages and commits it, even though the workflow's existing generated-file check deliberately excludes those files at lines 289-290. This turns tolerated, runner-generated lockfile changes into bot-authored dependency-PR changes and can trigger an unnecessary full CI dispatch; apply the same path exclusion when deciding what to commit and when staging it.

Useful? React with 👍 / 👎.

git -c user.name="github-actions[bot]" \
-c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit --message "chore: apply hygiene fixes"
committed=true
fi

if [[ -n ${GITHUB_OUTPUT:-} ]]; then
echo "committed=${committed}" >> "${GITHUB_OUTPUT}"
fi
if [[ ${committed} == true ]]; then
echo "committed=true"
else
echo "Tree already clean."
fi

if [[ -z ${GH_TOKEN:-} || -z ${HEAD_REF:-} ]]; then
exit 0
fi
if [[ ${committed} != true && $(git log -1 --pretty=%s) != "chore: apply hygiene fixes" ]]; then
exit 0
Comment on lines +30 to +35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Clean reruns redispatch unchanged CI

When the workflow is rerun after a previous invocation pushed chore: apply hygiene fixes, the clean tree still bypasses this early return based solely on the tip commit message. The script then pushes an unchanged branch and starts another complete CI workflow for the same commit, wasting CI capacity and potentially producing concurrent runs for one SHA.

Fix in Cursor Cloud Agents

fi
git push
# GITHUB_TOKEN pushes do not start new workflow runs, so dispatch CI on the
# updated branch after the push.
gh workflow run CI --ref "${HEAD_REF}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-run the healed commit in pull-request context

When hygiene pushes a generated-fix commit, the push deliberately creates no pull_request run, and this replacement dispatch uses HEAD_REF; gh workflow run --help defines --ref as the “Branch or tag name which contains the version of the workflow file,” so the resulting run checks the raw Dependabot branch and has no pull-request merge ref or payload. The earlier PR run remains attached to the pre-fix commit, meaning the final required checks never test the healed commit merged with the current base branch. Trigger the follow-up in a way that preserves pull-request merge-context coverage.

Useful? React with 👍 / 👎.

23 changes: 23 additions & 0 deletions .mise/tasks/ci/fix-action-pins
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
# [MISE] description="Copy catalog action pins onto every consumer."
# [MISE] shell="python"

import pathlib
import sys


ROOT = pathlib.Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))

from ci.action_pins import fix_pins # noqa: E402


def main() -> int:
changed = fix_pins(ROOT)
for path in changed:
print(f"updated {path.relative_to(ROOT).as_posix()}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
43 changes: 36 additions & 7 deletions .mise/tasks/ci/generate-workflow
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,18 @@ def setup(
zig: bool = False,
gradle: bool = True,
save_toolchains: bool = False,
checkout: bool = True,
) -> list[str]:
lines = [
f" - uses: {CHECKOUT}",
" with:",
" persist-credentials: false",
" - uses: ./.github/actions/setup-ci-deps",
]
lines: list[str] = []
if checkout:
lines.extend(
[
f" - uses: {CHECKOUT}",
" with:",
" persist-credentials: false",
]
)
lines.append(" - uses: ./.github/actions/setup-ci-deps")
if zig:
lines.append(" id: setup")
lines.append(" with:")
Expand Down Expand Up @@ -227,6 +232,10 @@ def render(source: dict[str, object], presets: dict[str, object]) -> str:
" - synchronize",
" - reopened",
" - ready_for_review",
# GITHUB_TOKEN pushes from Dependabot hygiene healing do not start
# pull_request workflows, so that job dispatches CI on the updated
# branch after it commits.
" workflow_dispatch:",
"",
"permissions:",
" contents: read",
Expand All @@ -240,10 +249,23 @@ def render(source: dict[str, object], presets: dict[str, object]) -> str:
" name: hygiene",
" runs-on: ubuntu-latest",
" timeout-minutes: 30",
" permissions:",
" contents: write",
" actions: write",
Comment on lines +252 to +254

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use an event that can push Dependabot fixes

When DEPENDABOT_PR is true, this job was triggered by a Dependabot pull_request; GitHub treats these runs like fork-originated workflows and gives them a read-only GITHUB_TOKEN, despite the requested write scopes (GitHub documentation). Consequently, commit-hygiene-fixes can create its local commit, but git push fails with a permission error, so neither the fix nor the follow-up dispatch reaches the branch.

Useful? React with 👍 / 👎.

Comment on lines +252 to +254

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Isolate write permissions from ordinary pull requests

When a non-Dependabot same-repository pull request runs hygiene, this job-level block still grants every PR-controlled step—including ./.github/actions/setup-ci-deps and the mise tasks—contents: write and actions: write; the later DEPENDABOT_PR conditions restrict only checkout and commit steps, not token availability. Because actions can access github.token even when it is not passed explicitly, code introduced by such a PR can mutate repository contents or workflow runs before review. Keep the general hygiene job read-only and move the privileged healing path into a separately constrained job or event.

Useful? React with 👍 / 👎.

SCCACHE_ENVIRONMENT,
*gate(),
" env:",
" DEPENDABOT_PR: ${{ github.event_name == 'pull_request' && github.actor == 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository }}",
" steps:",
*setup(gradle=False),
f" - uses: {CHECKOUT}",
" if: env.DEPENDABOT_PR != 'true'",
" with:",
" persist-credentials: false",
f" - uses: {CHECKOUT}",
" if: env.DEPENDABOT_PR == 'true'",
" with:",
" ref: ${{ github.head_ref }}",
*setup(gradle=False, checkout=False),
" - run: mise run ci:generate-workflow --check",
" - run: mise run ci:generate-devcontainer-tools --check",
" - run: mise run ci:test-release-tools",
Expand All @@ -254,7 +276,14 @@ def render(source: dict[str, object], presets: dict[str, object]) -> str:
# `mise run fix` provides the project-scoped formatter tools that the
# dprint wrappers expect; calling hk directly leaves them uninstalled.
" - run: mise run fix",
" - id: apply-fix",
" if: env.DEPENDABOT_PR == 'true'",
" env:",
" GH_TOKEN: ${{ github.token }}",
" HEAD_REF: ${{ github.head_ref }}",
" run: mise run ci:commit-hygiene-fixes",
Comment on lines +283 to +288

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move pin healing before generated checks

When Dependabot updates .github/workflows/action-pins.yml, the generator immediately expects .github/workflows/ci.yml to contain the new catalog references, but mise run ci:generate-workflow --check runs at line 269 before mise run fix, which is the first command that invokes the new action-pin fixer. The hygiene job therefore exits on the stale generated workflow and never reaches this commit step, defeating the healing flow for every action-pin update.

Useful? React with 👍 / 👎.

" - name: Check generated and formatted files",
" if: steps.apply-fix.outputs.committed != 'true'",
" run: |",
" git update-index -q --refresh",
" git diff --exit-code -- . ':(exclude)mise*.lock'",
Expand Down
37 changes: 36 additions & 1 deletion ci/action_pins.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Read and verify the third-party GitHub Actions pins catalog."""
"""Read, verify, and rewrite the third-party GitHub Actions pins catalog."""

from __future__ import annotations

Expand Down Expand Up @@ -106,3 +106,38 @@ def check_pins(root: pathlib.Path) -> list[str]:
f"{CATALOG.as_posix()}: {action} is no longer used; remove the pin"
)
return problems


# The SHA and version comment; indent, `- `, and `uses:` stay as they were.
_PIN_REF = re.compile(r"@[0-9a-f]{40}\s+#\s*\S+")


def _apply_pin(line: str, pin: Pin) -> str:
return _PIN_REF.sub(f"@{pin.sha} # {pin.version}", line, count=1)


def fix_pins(root: pathlib.Path) -> list[pathlib.Path]:
"""Rewrite consumer pins that disagree with the catalog.

Unpinned uses, actions missing from the catalog, and unused catalog
entries stay as they are. Those still fail ``check_pins``.
"""
pins = catalog(root)
changed: list[pathlib.Path] = []
for path in consumers(root):
original = path.read_text()
rewritten: list[str] = []
dirty = False
for line in original.splitlines(keepends=True):
pinned = PINNED.match(line)
if pinned:
current = Pin(**pinned.groupdict())
expected = pins.get(current.action)
if expected is not None and current != expected:
line = _apply_pin(line, expected)
dirty = True
rewritten.append(line)
if dirty:
path.write_text("".join(rewritten), newline="\n")
changed.append(path)
return changed
Loading
Loading