mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-24 15:01:37 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c6f251f8a |
No files matched your search
@@ -26,7 +26,9 @@ jobs:
|
||||
# "view || create" could race and make duplicate drafts. The draft is tied to
|
||||
# the pushed tag (the tag must already exist on GitHub, then tag_name is set
|
||||
# explicitly), so scripts/publish-release.sh and `gh release upload` find it
|
||||
# by tag. It is created with the REST API so the id comes straight back: the
|
||||
# by tag. Editing a draft can make GitHub show an untagged-* name again; the
|
||||
# upload step below recovers that exact draft if tag lookup fails. It is
|
||||
# created with the REST API so the id comes straight back: the
|
||||
# release list can lag a fresh draft (v0.4.2's first run found nothing and
|
||||
# PATCHed releases/ with an empty id). tests/test_release_workflow.py runs
|
||||
# this step against tests/fakegh.
|
||||
@@ -40,8 +42,13 @@ jobs:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
# A rerun finds the draft an earlier attempt made.
|
||||
id=$(gh api --paginate "repos/$GH_REPO/releases?per_page=100" --jq ".[] | select(.tag_name == \"$tag\") | .id" | head -n 1)
|
||||
# A rerun finds the draft even if GitHub temporarily shows untagged-*.
|
||||
id=$(gh api --paginate "repos/$GH_REPO/releases?per_page=100" \
|
||||
--jq ".[] | select(.tag_name == \"$tag\" or (.draft and (.tag_name | startswith(\"untagged-\")) and .name == \"Frame Control ${tag#v}\")) | .id")
|
||||
if [ -n "$id" ] && ! [[ "$id" =~ ^[0-9]+$ ]]; then
|
||||
echo "expected one release for $tag; found: $id" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$id" ]; then
|
||||
# Given a missing tag, GitHub would create it on the default branch.
|
||||
gh api "repos/$GH_REPO/git/ref/tags/$tag" >/dev/null \
|
||||
@@ -135,8 +142,25 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
gh release upload "${GITHUB_REF_NAME}" ${{ matrix.files }} --clobber
|
||||
# A release-notes edit can make GitHub expose a draft as untagged-*.
|
||||
# Keep the normal upload path; recover only that lookup failure.
|
||||
if result=$(gh release upload "${GITHUB_REF_NAME}" ${{ matrix.files }} --clobber 2>&1); then
|
||||
printf '%s\n' "$result"
|
||||
exit 0
|
||||
fi
|
||||
printf '%s\n' "$result" >&2
|
||||
case "$result" in *"release not found"*) ;; *) exit 1 ;; esac
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
ids=$(gh api --paginate "repos/$GH_REPO/releases?per_page=100" \
|
||||
--jq ".[] | select(.draft and .name == \"Frame Control ${tag#v}\" and (.tag_name == \"$tag\" or (.tag_name | startswith(\"untagged-\")))) | .id")
|
||||
if ! [[ "$ids" =~ ^[0-9]+$ ]]; then
|
||||
echo "expected one matching draft for $tag; found: ${ids:-none}" >&2
|
||||
exit 1
|
||||
fi
|
||||
gh api -X PATCH "repos/$GH_REPO/releases/$ids" -f tag_name="$tag" >/dev/null
|
||||
gh release upload "$tag" ${{ matrix.files }} --clobber
|
||||
# Tagged builds are attached to the release above; only other builds need artifacts.
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Recover a tagged release upload when GitHub temporarily displays its draft
|
||||
as `untagged-…` after an edit, without creating a duplicate draft or
|
||||
silently retrying unrelated upload failures.
|
||||
|
||||
- Import standalone APK/OBB backups from XAPK/APKS/APKM archives through the
|
||||
existing preview and game-data setup flow. Unsafe archives, split sets and
|
||||
Android/data restores are refused with guidance. This does not establish RE4
|
||||
|
||||
+4
-1
@@ -14,7 +14,10 @@ count. So a build reaches people only when you publish it, after testing it.
|
||||
|
||||
`.github/workflows/release.yml` builds macOS, Windows and Linux, and
|
||||
attaches everything to a **draft** release for that tag. Nobody is
|
||||
offered a draft.
|
||||
offered a draft. Editing a draft while installers are uploading can make
|
||||
GitHub temporarily show its tag as `untagged-…`. The workflow finds that
|
||||
draft by its exact version title, restores the tag association and retries
|
||||
the affected upload; it refuses ambiguous matches.
|
||||
|
||||
2. Download the draft's installers and test them. An installed copy of the
|
||||
previous version won't offer the draft, so install it directly.
|
||||
|
||||
+15
-2
@@ -3,8 +3,8 @@
|
||||
tests/test_release_workflow.py. Serves the releases in $FAKEGH_RELEASES (a JSON list,
|
||||
drafts included) and the tags in $FAKEGH_TAGS (space-separated); an upload's body is
|
||||
written to $FAKEGH_UPLOAD. Every call is appended to $FAKEGH_LOG as a JSON line.
|
||||
Anything else fails, so the script under test can't fall back to `gh release ...`
|
||||
(GraphQL) unnoticed. POST .../releases (release.yml's draft step) answers as release
|
||||
Anything else fails; `gh release upload` works only when a release-workflow test
|
||||
explicitly sets FAKEGH_ALLOW_RELEASE_UPLOAD=1. POST .../releases (release.yml's draft step) answers as release
|
||||
$FAKEGH_NEW_ID (default 9001) without adding it to the listing, like GitHub's list
|
||||
lagging a fresh draft. With jq installed, --jq filters a response the way gh does
|
||||
(raw strings, compact JSON)."""
|
||||
@@ -17,6 +17,13 @@ import sys
|
||||
args = sys.argv[1:]
|
||||
with open(os.environ["FAKEGH_LOG"], "a") as f:
|
||||
f.write(json.dumps(args) + "\n")
|
||||
if args[:2] == ["release", "upload"] and os.environ.get("FAKEGH_ALLOW_RELEASE_UPLOAD") == "1":
|
||||
releases = json.load(open(os.environ["FAKEGH_RELEASES"]))
|
||||
if os.environ.get("FAKEGH_UPLOAD_ERROR"):
|
||||
sys.exit(os.environ["FAKEGH_UPLOAD_ERROR"])
|
||||
if not any(r.get("tag_name") == args[2] for r in releases):
|
||||
sys.exit("release not found")
|
||||
sys.exit(0)
|
||||
if not args or args[0] != "api":
|
||||
sys.exit("fakegh: only `gh api` is supported: %r" % args)
|
||||
|
||||
@@ -80,6 +87,12 @@ elif method == "PATCH" and endpoint.startswith(repo + "releases/"):
|
||||
rid = endpoint[len(repo + "releases/"):]
|
||||
if not rid.isdigit(): # what GitHub said to v0.4.2's PATCH of releases/ (an empty id)
|
||||
sys.exit("gh: Not Found (HTTP 404)")
|
||||
if os.environ.get("FAKEGH_ALLOW_RELEASE_UPLOAD") == "1":
|
||||
for item in releases:
|
||||
if str(item["id"]) == rid:
|
||||
item.update(fields)
|
||||
with open(os.environ["FAKEGH_RELEASES"], "w") as f:
|
||||
json.dump(releases, f)
|
||||
page = "https://github.com/saphid/frame-control/releases/tag/%s" % fields.get("tag_name")
|
||||
answer({"id": int(rid), "tag_name": fields.get("tag_name"), "html_url": page},
|
||||
"published %s at %s" % (fields.get("tag_name"), page))
|
||||
|
||||
@@ -21,12 +21,13 @@ ROOT = Path(__file__).resolve().parent.parent
|
||||
WORKFLOW = ROOT / ".github" / "workflows" / "release.yml"
|
||||
FAKEGH = ROOT / "tests" / "fakegh"
|
||||
STEP = "- name: Create the draft release"
|
||||
UPLOAD_STEP = "- name: Upload to the release"
|
||||
|
||||
|
||||
def step_script():
|
||||
def step_script(step=STEP):
|
||||
"""The step's `run: |` block, dedented."""
|
||||
lines = WORKFLOW.read_text().splitlines()
|
||||
i = next(n for n, line in enumerate(lines) if line.strip() == STEP)
|
||||
i = next(n for n, line in enumerate(lines) if line.strip() == step)
|
||||
while lines[i].strip() != "run: |":
|
||||
i += 1
|
||||
indent = len(lines[i]) - len(lines[i].lstrip())
|
||||
@@ -87,6 +88,19 @@ class DraftStep(unittest.TestCase):
|
||||
[patch] = self.patches(log)
|
||||
self.assertEqual(patch[3], "repos/saphid/frame-control/releases/42")
|
||||
|
||||
def test_a_rerun_reuses_the_same_untagged_draft(self):
|
||||
p, log = self.run_step([release(42, "untagged-abc")])
|
||||
self.assertEqual(p.returncode, 0, p.stderr + p.stdout)
|
||||
self.assertEqual(self.posts(log), [])
|
||||
self.assertEqual(self.patches(log)[0][3], "repos/saphid/frame-control/releases/42")
|
||||
|
||||
def test_ambiguous_drafts_do_not_create_or_patch(self):
|
||||
p, log = self.run_step([release(42, "untagged-abc"), release(43, "untagged-def")])
|
||||
self.assertNotEqual(p.returncode, 0)
|
||||
self.assertIn("expected one release", p.stderr)
|
||||
self.assertEqual(self.posts(log), [])
|
||||
self.assertEqual(self.patches(log), [])
|
||||
|
||||
def test_other_releases_alone_mean_a_new_draft(self):
|
||||
p, log = self.run_step([release(7, "v9.8.6", "Frame Control 9.8.6", draft=False),
|
||||
release(8, "v9.8.70", "Frame Control 9.8.70")])
|
||||
@@ -102,5 +116,55 @@ class DraftStep(unittest.TestCase):
|
||||
self.assertEqual(self.patches(log), [])
|
||||
|
||||
|
||||
@unittest.skipUnless(os.name != "nt" and shutil.which("bash") and shutil.which("jq"),
|
||||
"needs a POSIX bash and jq; Actions runs this step under bash")
|
||||
class UploadStep(unittest.TestCase):
|
||||
def run_step(self, releases, error=None):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
script = step_script(UPLOAD_STEP).replace("${{ matrix.files }}", "artifact.exe artifact.zip")
|
||||
(d / "step.sh").write_text(script)
|
||||
(d / "releases.json").write_text(json.dumps(releases))
|
||||
(d / "artifact.exe").write_bytes(b"installer")
|
||||
(d / "artifact.zip").write_bytes(b"portable")
|
||||
env = dict(os.environ, PATH="%s:%s" % (FAKEGH, os.environ["PATH"]),
|
||||
FAKEGH_RELEASES=str(d / "releases.json"), FAKEGH_TAGS="v9.8.7",
|
||||
FAKEGH_LOG=str(d / "log"), FAKEGH_ALLOW_RELEASE_UPLOAD="1",
|
||||
GITHUB_REF_NAME="v9.8.7", GH_REPO="saphid/frame-control")
|
||||
if error:
|
||||
env["FAKEGH_UPLOAD_ERROR"] = error
|
||||
p = subprocess.run(["bash", "--noprofile", "--norc", "-eo", "pipefail", str(d / "step.sh")],
|
||||
cwd=d, env=env, capture_output=True, text=True, timeout=30)
|
||||
log = [json.loads(line) for line in (d / "log").read_text().splitlines()]
|
||||
return p, log
|
||||
|
||||
def test_tagged_draft_uses_normal_upload(self):
|
||||
p, log = self.run_step([release(42, "v9.8.7")])
|
||||
self.assertEqual(p.returncode, 0, p.stderr)
|
||||
self.assertEqual(len([c for c in log if c[:2] == ["release", "upload"]]), 1)
|
||||
self.assertFalse(any(c[:3] == ["api", "-X", "PATCH"] for c in log))
|
||||
|
||||
def test_untagged_draft_is_reassociated_then_uploaded(self):
|
||||
p, log = self.run_step([release(42, "untagged-abc")])
|
||||
self.assertEqual(p.returncode, 0, p.stderr)
|
||||
self.assertEqual(len([c for c in log if c[:2] == ["release", "upload"]]), 2)
|
||||
patches = [c for c in log if c[:3] == ["api", "-X", "PATCH"]]
|
||||
self.assertEqual(len(patches), 1)
|
||||
self.assertIn("repos/saphid/frame-control/releases/42", patches[0])
|
||||
self.assertIn("tag_name=v9.8.7", patches[0])
|
||||
|
||||
def test_ambiguous_untagged_drafts_are_not_changed(self):
|
||||
p, log = self.run_step([release(42, "untagged-abc"), release(43, "untagged-def")])
|
||||
self.assertNotEqual(p.returncode, 0)
|
||||
self.assertIn("expected one matching draft", p.stderr)
|
||||
self.assertFalse(any(c[:3] == ["api", "-X", "PATCH"] for c in log))
|
||||
|
||||
def test_other_upload_failures_do_not_patch_draft(self):
|
||||
p, log = self.run_step([release(42, "v9.8.7")], error="network unavailable")
|
||||
self.assertNotEqual(p.returncode, 0)
|
||||
self.assertIn("network unavailable", p.stderr)
|
||||
self.assertFalse(any(c[:3] == ["api", "-X", "PATCH"] for c in log))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user