Skip to content

Commit bf3f395

Browse files
authored
Use GitHub app credentials for auto-dependabot (#508)
Actions performed with `GITHUB_TOKEN` may not trigger follow-up workflow runs, which can prevent merge queue CI (merge_group) from starting and can leave auto-merge “stuck” without merging. Using a GitHub App token avoids this suppression and restores reliable merge-queue processing.
2 parents 0436390 + de30890 commit bf3f395

11 files changed

Lines changed: 345 additions & 115 deletions

File tree

.github/cookiecutter-migrate.template.py

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -85,66 +85,80 @@ def apply_patch(patch_content: str) -> None:
8585
subprocess.run(["patch", "-p1"], input=patch_content.encode(), check=True)
8686

8787

88-
def replace_file_contents_atomically( # noqa; DOC501
89-
filepath: str | Path,
90-
old: str,
91-
new: str,
92-
count: SupportsIndex = -1,
93-
*,
94-
content: str | None = None,
88+
def replace_file_atomically( # noqa; DOC501, DOC503
89+
filepath: str | Path, new_content: str
9590
) -> None:
96-
"""Replace a file atomically with new content.
91+
"""Replace a file atomically with the given content.
92+
93+
The replacement is done atomically by writing to a temporary file in the
94+
same directory and then moving it to the target location.
9795
9896
Args:
9997
filepath: The path to the file to replace.
100-
old: The string to replace.
101-
new: The string to replace it with.
102-
count: The maximum number of occurrences to replace. If negative, all occurrences are
103-
replaced.
104-
content: The content to replace. If not provided, the file is read from disk.
105-
106-
The replacement is done atomically by writing to a temporary file and
107-
then moving it to the target location.
98+
new_content: The content to write to the file.
10899
"""
109100
if isinstance(filepath, str):
110101
filepath = Path(filepath)
111102

112-
if content is None:
113-
content = filepath.read_text(encoding="utf-8")
114-
115-
content = content.replace(old, new, count)
116-
117-
# Create temporary file in the same directory to ensure atomic move
118103
tmp_dir = filepath.parent
104+
tmp_dir.mkdir(parents=True, exist_ok=True)
119105

120106
# pylint: disable-next=consider-using-with
121107
tmp = tempfile.NamedTemporaryFile(mode="w", dir=tmp_dir, delete=False)
122108

123109
try:
124-
# Copy original file permissions
125-
st = os.stat(filepath)
126-
127-
# Write the new content
128-
tmp.write(content)
110+
st = None
111+
try:
112+
st = os.stat(filepath)
113+
except FileNotFoundError:
114+
st = None
129115

130-
# Ensure all data is written to disk
116+
tmp.write(new_content)
131117
tmp.flush()
132118
os.fsync(tmp.fileno())
133119
tmp.close()
134120

135-
# Copy original file permissions to the new file
136-
os.chmod(tmp.name, st.st_mode)
121+
if st is not None:
122+
os.chmod(tmp.name, st.st_mode)
137123

138-
# Perform atomic replace
139-
os.rename(tmp.name, filepath)
124+
os.replace(tmp.name, filepath)
140125

141126
except BaseException:
142-
# Clean up the temporary file in case of errors
143127
tmp.close()
144128
os.unlink(tmp.name)
145129
raise
146130

147131

132+
def replace_file_contents_atomically( # noqa; DOC501
133+
filepath: str | Path,
134+
old: str,
135+
new: str,
136+
count: SupportsIndex = -1,
137+
*,
138+
content: str | None = None,
139+
) -> None:
140+
"""Replace a file atomically with new content.
141+
142+
The replacement is done atomically by writing to a temporary file and
143+
then moving it to the target location.
144+
145+
Args:
146+
filepath: The path to the file to replace.
147+
old: The string to replace.
148+
new: The string to replace it with.
149+
count: The maximum number of occurrences to replace. If negative, all occurrences are
150+
replaced.
151+
content: The content to replace. If not provided, the file is read from disk.
152+
"""
153+
if isinstance(filepath, str):
154+
filepath = Path(filepath)
155+
156+
if content is None:
157+
content = filepath.read_text(encoding="utf-8")
158+
159+
replace_file_atomically(filepath, content.replace(old, new, count))
160+
161+
148162
def calculate_file_sha256_skip_lines(filepath: Path, skip_lines: int) -> str | None:
149163
"""Calculate SHA256 of file contents excluding the first N lines.
150164

.github/workflows/auto-dependabot.yaml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
name: Auto-merge Dependabot PR
22

33
on:
4-
pull_request:
4+
# XXX: !!! SECURITY WARNING !!!
5+
# pull_request_target has write access to the repo, and can read secrets. We
6+
# need to audit any external actions executed in this workflow and make sure no
7+
# checked out code is run (not even installing dependencies, as installing
8+
# dependencies usually can execute pre/post-install scripts). We should also
9+
# only use hashes to pick the action to execute (instead of tags or branches).
10+
# For more details read:
11+
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
12+
pull_request_target:
513

614
permissions:
7-
contents: write
15+
contents: read
816
pull-requests: write
917

1018
jobs:
1119
auto-merge:
20+
name: Auto-merge Dependabot PR
1221
if: github.actor == 'dependabot[bot]'
1322
runs-on: ubuntu-slim
1423
steps:
24+
- name: Generate GitHub App token
25+
id: app-token
26+
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
27+
with:
28+
app-id: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_ID }}
29+
private-key: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_PRIVATE_KEY }}
30+
1531
- name: Auto-merge Dependabot PR
1632
uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2
1733
with:
18-
github-token: ${{ secrets.GITHUB_TOKEN }}
34+
github-token: ${{ steps.app-token.outputs.token }}
1935
dependency-type: 'all'
2036
auto-merge: 'true'
2137
merge-method: 'merge'

RELEASE_NOTES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
This release migrates lightweight GitHub Actions workflow jobs to use the new cost-effective `ubuntu-slim` runner.
66
It also updates cookiecutter pyproject license metadata to SPDX expressions to avoid setuptools deprecation warnings.
7+
The auto-dependabot workflow now uses a GitHub App installation token instead of `GITHUB_TOKEN` to fix merge queue and auto-merge failures.
78

89
## Upgrading
910

@@ -46,3 +47,10 @@ But you might still need to adapt your code:
4647

4748
- Switched `project.license` to SPDX expressions and added `project.license-files`.
4849
This removes deprecated setuptools license metadata and avoids build warnings.
50+
51+
- Fixed auto-dependabot workflow failing to trigger merge queue CI or complete
52+
auto-merge. The workflow now uses a GitHub App installation token (via
53+
`actions/create-github-app-token`) instead of `GITHUB_TOKEN`, which was
54+
suppressing subsequent workflow runs by design. Workflow permissions have been
55+
reduced to the minimum needed for the workflow (`contents: read` and
56+
`pull-requests: write`).

0 commit comments

Comments
 (0)