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
7 changes: 4 additions & 3 deletions .github/workflows/linting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ name: Linting and MyPy (Pelican)
on:
push:
paths:
- '**.py'
- '**/linting.yml'
- '**/pylintrc'
- 'pelican/**.py'
- 'pelican/pyproject.toml'
- 'pelican/pylintrc'
- '.github/workflows/linting.yml'
workflow_dispatch:

permissions:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/pelican-action-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ env.SOURCE }}
persist-credentials: false
# Credentials must persist: the pelican action's publish step runs
# `git push` against this working tree to commit the built site.
- name: Ignore the action checkout
run: |
echo "self/" >> .git/info/exclude
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/stash-action-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
run: |
cd stash/restore
python3 test_get_stash.py
python3 test_download_stash.py

- name: Test Save
uses: ./stash/save
Expand Down
92 changes: 92 additions & 0 deletions stash/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#

[build-system]
requires = ["setuptools>=82.0.1"]
build-backend = "setuptools.build_meta"

[project]
name = "apache-stash-action"
version = "0.1.0"
description = "GitHub Action for stashing and restoring build caches via workflow artifacts."
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.10"
authors = [
{ name = "Apache Software Foundation", email = "dev@infra.apache.org" },
]
keywords = ["github-action", "cache", "artifacts", "apache"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Build Tools",
]
# The stash action's Python code is stdlib-only at runtime; the external
# dependencies are the `gh` and `jq` CLIs, which are installed on the
# GitHub Actions runner and checked for at action start-up.
dependencies = []

# PEP 735 dependency groups. `dev` holds everything a contributor needs
# to lint and test the stash helpers locally. pytest is listed so the
# existing unittest-based tests can be discovered and run via pytest if
# desired, alongside `python3 test_*.py`.
[dependency-groups]
dev = [
"ruff>=0.6",
"mypy>=1.10",
"pytest>=8",
]

[project.urls]
Homepage = "https://github.com/apache/infrastructure-actions"
Source = "https://github.com/apache/infrastructure-actions"
Issues = "https://github.com/apache/infrastructure-actions/issues"

[tool.setuptools]
py-modules = []

[tool.uv]
required-version = ">=0.5.0"

[tool.hatch.envs.default]
installer = "uv"
dependency-groups = ["dev"]

[tool.hatch.envs.default.scripts]
lint = "ruff check ."
fmt = "ruff format ."
test = "pytest"

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
ignore = ["E501"]

[tool.ruff.format]
quote-style = "double"
38 changes: 22 additions & 16 deletions stash/restore/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ inputs:
If true, only the current branch will be searched for the stash.
If false, the base branch(PRs)/default branch will be searched as well.
default: "false"
retry-count:
description: >
Number of attempts for downloading the stash artifact. When `gh run download`
exits with code 1 (the transient failure mode observed for artifact downloads),
the download will be retried until it succeeds or this many attempts have been
made. Other exit codes are not retried.
default: "3"
fail-on-download:
description: >
If true, the action will fail when a stash artifact was found but could not be
downloaded (after exhausting `retry-count` attempts). If false (the default),
a failed download is reported only via the `stash-hit` output and the step
itself succeeds.
default: "false"
outputs:
stash-hit:
description: >
Expand Down Expand Up @@ -139,31 +153,23 @@ runs:
)

- name: Download Stash
shell: bash
shell: python3 {0}
if: steps.check-stash.outputs.stash_found != 'false'
id: download
env:
PYTHONPATH: "${{ github.action_path }}"
GH_TOKEN: "${{ inputs.token }}"
STASH_NAME: "${{ steps.check-stash.outputs.stash_name }}"
STASH_RUN_ID: "${{ steps.check-stash.outputs.stash_run_id }}"
REPO: "${{ github.repository }}"
STASH_DIR: "${{ steps.mung.outputs.stash_path }}"
INPUTS_CLEAN: ${{ inputs.clean }}
CLEAN: "${{ inputs.clean }}"
RETRY_COUNT: "${{ inputs.retry-count }}"
FAIL_ON_DOWNLOAD: "${{ inputs.fail-on-download }}"
run: |
# Catch errors in the download with || to avoid the whole workflow failing
# when the download times out
if [[ "${INPUTS_CLEAN}" == "true" ]]; then
if [[ -d "$STASH_DIR" ]]; then
echo "Removing existing stash directory: $STASH_DIR"
rm -rf "$STASH_DIR"
fi
fi
gh run download "$STASH_RUN_ID" \
--name "$STASH_NAME" \
--dir "$STASH_DIR" \
-R "$REPO" || download="failed" && download="success"

echo "download=$download" >> "$GITHUB_OUTPUT"
import os, sys
import download_stash
sys.exit(download_stash.download_stash(os.environ))

- name: Set stash-hit Output
id: output
Expand Down
102 changes: 102 additions & 0 deletions stash/restore/download_stash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Copyright (c) The stash contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Download a stash artifact with retries.

Required env vars:
STASH_RUN_ID - workflow run ID the artifact was produced by
STASH_NAME - artifact name
STASH_DIR - destination directory
REPO - owner/name of the repository
RETRY_COUNT - max download attempts (retries on gh exit code 1)
FAIL_ON_DOWNLOAD - "true" to exit 1 on download failure, else "false"
CLEAN - "true" to remove STASH_DIR before downloading
GITHUB_OUTPUT - file to write the `download` output to
"""

import os
import shutil
import subprocess
import sys
from collections.abc import Callable, Mapping


def run_gh_download(run_id: str, name: str, dest: str, repo: str) -> int:
"""Invoke ``gh run download`` and return its exit code."""
return subprocess.run(
[
"gh", "run", "download", run_id,
"--name", name,
"--dir", dest,
"-R", repo,
],
check=False,
).returncode


def download_stash(
env: Mapping[str, str],
run_download: Callable[[str, str, str, str], int] = run_gh_download,
) -> int:
"""Run the clean/retry/fail-on-download logic.

Returns the desired process exit code (0 for success or tolerated
failure, 1 when the download failed and ``FAIL_ON_DOWNLOAD`` is
``"true"``). The ``run_download`` hook exists so tests can stub out
the real ``gh`` call.
"""
stash_run_id = env["STASH_RUN_ID"]
stash_name = env["STASH_NAME"]
stash_dir = env["STASH_DIR"]
repo = env["REPO"]
retry_count = int(env.get("RETRY_COUNT", "1"))
fail_on_download = env.get("FAIL_ON_DOWNLOAD", "false").lower() == "true"
clean = env.get("CLEAN", "false").lower() == "true"
github_output = env["GITHUB_OUTPUT"]

if clean and os.path.isdir(stash_dir):
print(f"Removing existing stash directory: {stash_dir}")
shutil.rmtree(stash_dir, ignore_errors=True)

download = "failed"
for attempt in range(1, retry_count + 1):
print(f"Downloading stash (attempt {attempt} of {retry_count})...", flush=True)
rc = run_download(stash_run_id, stash_name, stash_dir, repo)
if rc == 0:
download = "success"
break
if rc != 1:
print(
f"::warning ::gh run download failed with exit code {rc}; "
"not retrying."
)
break
print(
f"::warning ::gh run download failed with exit code 1 on "
f"attempt {attempt}."
)

with open(github_output, "a", encoding="utf-8") as f:
f.write(f"download={download}\n")

if download != "success" and fail_on_download:
print(
f"::error ::Stash artifact download failed after {retry_count} "
"attempt(s) and fail-on-download is true."
)
return 1
return 0


if __name__ == "__main__":
sys.exit(download_stash(os.environ))
Loading
Loading