diff --git a/scripts/action/fetch-state.sh b/scripts/action/fetch-state.sh index c0385b2..cf16a1e 100755 --- a/scripts/action/fetch-state.sh +++ b/scripts/action/fetch-state.sh @@ -32,7 +32,14 @@ trusted='[.artifacts[]? | sort_by(.created_at) | last' selected="" expires="" rejected=0 page=1 while [ "$page" -le 10 ]; do - listing="$(api "repos/$REPOSITORY/actions/artifacts?name=$ARTIFACT_NAME&per_page=100&page=$page" 2>/dev/null || true)" + if ! listing="$(api "repos/$REPOSITORY/actions/artifacts?name=$ARTIFACT_NAME&per_page=100&page=$page" 2>/dev/null)"; then + # A denied listing and an empty one are the same silence otherwise, and the + # denial is permanent: the run publishes fine, since uploading needs no + # permission, so a repository can look like it is always cold and never + # learn why. + echo "::warning::Could not list artifacts in $REPOSITORY. Add 'actions: read' to the job's permissions to reuse previous analyses; deriving from the base for now." + exit 0 + fi [ -n "$listing" ] || break returned="$(jq -r '.artifacts | length' <<< "$listing" 2>/dev/null || echo 0)" [ "${returned:-0}" -gt 0 ] || break diff --git a/tests/test_action_state.py b/tests/test_action_state.py index 748f234..e5060fe 100644 --- a/tests/test_action_state.py +++ b/tests/test_action_state.py @@ -671,3 +671,36 @@ def test_it_uses_state_produced_by_this_repository(self) -> None: self.assertEqual(result.returncode, 0, result.stderr) self.assertTrue((self.root / "out").exists()) + + +class UnreadableArtifactsTests(unittest.TestCase): + """Listing needs actions: read; uploading does not. Without it a repository + publishes on every run and reads on none, which looks like a permanent miss.""" + + def test_it_says_why_when_the_listing_is_denied(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + bin_dir = root / "bin" + bin_dir.mkdir() + gh = bin_dir / "gh" + gh.write_text("#!/bin/sh\nexit 1\n", encoding="utf-8") # denied + gh.chmod(0o755) + + result = subprocess.run( + [str(ROOT / "scripts" / "action" / "fetch-state.sh")], + env={ + "PATH": f"{bin_dir}:{os.environ['PATH']}", + "RUNNER_TEMP": str(root), + "REPOSITORY": "owner/repo", + "GH_HOST": "https://github.com", + "ARTIFACT_NAME": "codeboarding-base-cfg-sha", + "DEST": str(root / "out"), + }, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("actions: read", result.stdout) + self.assertFalse((root / "out").exists())