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
80 changes: 47 additions & 33 deletions .github/cookiecutter-migrate.template.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,66 +85,80 @@ def apply_patch(patch_content: str) -> None:
subprocess.run(["patch", "-p1"], input=patch_content.encode(), check=True)


def replace_file_contents_atomically( # noqa; DOC501
filepath: str | Path,
old: str,
new: str,
count: SupportsIndex = -1,
*,
content: str | None = None,
def replace_file_atomically( # noqa; DOC501, DOC503
filepath: str | Path, new_content: str
) -> None:
"""Replace a file atomically with new content.
"""Replace a file atomically with the given content.

The replacement is done atomically by writing to a temporary file in the
same directory and then moving it to the target location.

Args:
filepath: The path to the file to replace.
old: The string to replace.
new: The string to replace it with.
count: The maximum number of occurrences to replace. If negative, all occurrences are
replaced.
content: The content to replace. If not provided, the file is read from disk.

The replacement is done atomically by writing to a temporary file and
then moving it to the target location.
new_content: The content to write to the file.
"""
if isinstance(filepath, str):
filepath = Path(filepath)

if content is None:
content = filepath.read_text(encoding="utf-8")

content = content.replace(old, new, count)

# Create temporary file in the same directory to ensure atomic move
tmp_dir = filepath.parent
tmp_dir.mkdir(parents=True, exist_ok=True)

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

try:
# Copy original file permissions
st = os.stat(filepath)

# Write the new content
tmp.write(content)
st = None
try:
st = os.stat(filepath)
except FileNotFoundError:
st = None

# Ensure all data is written to disk
tmp.write(new_content)
tmp.flush()
os.fsync(tmp.fileno())
tmp.close()

# Copy original file permissions to the new file
os.chmod(tmp.name, st.st_mode)
if st is not None:
os.chmod(tmp.name, st.st_mode)

# Perform atomic replace
os.rename(tmp.name, filepath)
os.replace(tmp.name, filepath)

except BaseException:
# Clean up the temporary file in case of errors
tmp.close()
os.unlink(tmp.name)
raise


def replace_file_contents_atomically( # noqa; DOC501
filepath: str | Path,
old: str,
new: str,
count: SupportsIndex = -1,
*,
content: str | None = None,
) -> None:
"""Replace a file atomically with new content.

The replacement is done atomically by writing to a temporary file and
then moving it to the target location.

Args:
filepath: The path to the file to replace.
old: The string to replace.
new: The string to replace it with.
count: The maximum number of occurrences to replace. If negative, all occurrences are
replaced.
content: The content to replace. If not provided, the file is read from disk.
"""
if isinstance(filepath, str):
filepath = Path(filepath)

if content is None:
content = filepath.read_text(encoding="utf-8")

replace_file_atomically(filepath, content.replace(old, new, count))


def calculate_file_sha256_skip_lines(filepath: Path, skip_lines: int) -> str | None:
"""Calculate SHA256 of file contents excluding the first N lines.

Expand Down
22 changes: 19 additions & 3 deletions .github/workflows/auto-dependabot.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
name: Auto-merge Dependabot PR

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

permissions:
contents: write
contents: read
pull-requests: write

jobs:
auto-merge:
name: Auto-merge Dependabot PR
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-slim
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_ID }}
private-key: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_PRIVATE_KEY }}

- name: Auto-merge Dependabot PR
uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ steps.app-token.outputs.token }}
dependency-type: 'all'
auto-merge: 'true'
merge-method: 'merge'
Expand Down
8 changes: 8 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

## Upgrading

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

- Switched `project.license` to SPDX expressions and added `project.license-files`.
This removes deprecated setuptools license metadata and avoids build warnings.

- Fixed auto-dependabot workflow failing to trigger merge queue CI or complete
auto-merge. The workflow now uses a GitHub App installation token (via
`actions/create-github-app-token`) instead of `GITHUB_TOKEN`, which was
suppressing subsequent workflow runs by design. Workflow permissions have been
reduced to the minimum needed for the workflow (`contents: read` and
`pull-requests: write`).
Loading