CI: ensure patch on runner (vendored shim), fallback to artifacts branch, master target
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
# Builds the signed release APK and F-Droid v2 artifacts as soon as a new
|
# Builds the signed release APK and F-Droid v2 artifacts as soon as a new
|
||||||
# release is created, and attaches everything to that release.
|
# release is created, and attaches everything to that release.
|
||||||
#
|
#
|
||||||
# Works on Gitea Actions (GitHub-Actions-compatible) — the repo lives on
|
# Written for Gitea Actions (GitHub-Actions-compatible) — the repo lives on
|
||||||
# https://git.obahan.xyz/frank/wirevpn.
|
# https://git.obahan.xyz/frank/wirevpn. If release-attachment storage is
|
||||||
|
# unavailable on the server, outputs are also published to the `artifacts`
|
||||||
|
# branch (tag-name addressed via commit message).
|
||||||
#
|
#
|
||||||
# Required repository secrets:
|
# Required repository secrets:
|
||||||
# KEYSTORE_FILE base64-encoded release signing keystore (.jks)
|
# KEYSTORE_FILE base64-encoded release signing keystore (.jks)
|
||||||
@@ -61,7 +63,20 @@ jobs:
|
|||||||
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" \
|
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" \
|
||||||
"platform-tools" "platforms;android-${ANDROID_API_LEVEL}" "build-tools;36.0.0" "ndk;${ANDROID_NDK_VERSION}"
|
"platform-tools" "platforms;android-${ANDROID_API_LEVEL}" "build-tools;36.0.0" "ndk;${ANDROID_NDK_VERSION}"
|
||||||
echo "sdk.dir=$ANDROID_HOME" > local.properties
|
echo "sdk.dir=$ANDROID_HOME" > local.properties
|
||||||
echo "ndk.dir=$ANDROID_HOME/ndk/${ANDROID_NDK_VERSION}" >> local.properties
|
|
||||||
|
- name: Ensure build prerequisites (patch)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
if ! command -v patch >/dev/null 2>&1; then
|
||||||
|
echo "patch not found; using vendored shim"
|
||||||
|
mkdir -p "$HOME/bin"
|
||||||
|
cp scripts/patch-shim.py "$HOME/bin/patch"
|
||||||
|
chmod +x "$HOME/bin/patch"
|
||||||
|
echo "$HOME/bin" >> "$GITHUB_PATH"
|
||||||
|
echo "patch shim ready (vendored)"
|
||||||
|
else
|
||||||
|
echo "patch present on runner"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Decode signing keystore
|
- name: Decode signing keystore
|
||||||
run: |
|
run: |
|
||||||
@@ -114,15 +129,41 @@ jobs:
|
|||||||
rid=$(curl -fsSL -H "Authorization: token $GITEA_TOKEN" "$GITEA_API/repos/$REPO/releases/tags/$tag" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' 2>/dev/null || true)
|
rid=$(curl -fsSL -H "Authorization: token $GITEA_TOKEN" "$GITEA_API/repos/$REPO/releases/tags/$tag" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' 2>/dev/null || true)
|
||||||
if [ -z "$rid" ]; then
|
if [ -z "$rid" ]; then
|
||||||
rid=$(curl -fsSL -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
|
rid=$(curl -fsSL -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
|
||||||
-d "{\"name\":\"$tag\",\"tag_name\":\"$tag\",\"target\":\"main\"}" \
|
-d "{\"name\":\"$tag\",\"tag_name\":\"$tag\",\"target\":\"master\"}" \
|
||||||
"$GITEA_API/repos/$REPO/releases" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
|
"$GITEA_API/repos/$REPO/releases" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
|
||||||
fi
|
fi
|
||||||
|
ok=0
|
||||||
for f in dist/page.onram.wirevpn.apk dist/page.onram.wirevpn.metadata dist/page.onram.wirevpn.signature; do
|
for f in dist/page.onram.wirevpn.apk dist/page.onram.wirevpn.metadata dist/page.onram.wirevpn.signature; do
|
||||||
curl -fsSL -H "Authorization: token $GITEA_TOKEN" \
|
if curl -fsSL -H "Authorization: token $GITEA_TOKEN" \
|
||||||
-F "file=@$f" \
|
-F "file=@$f" \
|
||||||
"$GITEA_API/repos/$REPO/releases/$rid/assets"
|
"$GITEA_API/repos/$REPO/releases/$rid/assets" >/dev/null; then
|
||||||
echo "uploaded $f"
|
echo "uploaded $f"
|
||||||
|
ok=1
|
||||||
|
else
|
||||||
|
echo "attachment upload failed for $f (Gitea attachment storage broken?)"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
if [ "$ok" != "1" ]; then
|
||||||
|
echo "::warning::Could not attach release assets; falling back to artifacts branch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish build outputs to artifacts branch (fallback + archive)
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
tag="${GITHUB_REF#refs/tags/}"
|
||||||
|
[ -n "$tag" ] || tag="latest"
|
||||||
|
mkdir -p dist-artifacts && cd dist-artifacts
|
||||||
|
cp ../dist/page.onram.wirevpn.apk ../dist/page.onram.wirevpn.metadata ../dist/page.onram.wirevpn.signature . 2>/dev/null || exit 0
|
||||||
|
git config user.name "wirevpn-ci"
|
||||||
|
git config user.email "ci@localhost"
|
||||||
|
git init -q .
|
||||||
|
git add -A
|
||||||
|
git commit -qm "build outputs for $tag"
|
||||||
|
git push -q --force "https://frank:${GITEA_TOKEN}@git.obahan.xyz/frank/wirevpn.git" HEAD:artifacts
|
||||||
|
echo "artifacts published to branch 'artifacts'"
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
Executable
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Minimal `patch` compatible shim (enough for the libwg-go Makefile).
|
||||||
|
|
||||||
|
Supports: patch [-pN] [-f] [-N] [-r-] [-d DIR] [-s] (diff read from stdin)
|
||||||
|
Handles unified diffs with multiple files/hunks (git-style or plain).
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv):
|
||||||
|
strip = 1
|
||||||
|
dir_ = "."
|
||||||
|
args = list(argv)
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
a = args[i]
|
||||||
|
if a == "-p":
|
||||||
|
strip = int(args[i + 1]); i += 2; continue
|
||||||
|
elif a.startswith("-p") and a[2:].isdigit():
|
||||||
|
strip = int(a[2:]); i += 1; continue
|
||||||
|
elif a == "-d":
|
||||||
|
dir_ = args[i + 1]; i += 2; continue
|
||||||
|
elif a in ("-f", "-N", "-s", "-t", "-i"):
|
||||||
|
i += 1; continue
|
||||||
|
elif a == "-r-":
|
||||||
|
i += 1; continue
|
||||||
|
elif a.startswith("-"):
|
||||||
|
i += 1; continue
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
return strip, dir_
|
||||||
|
|
||||||
|
|
||||||
|
def strip_path(p, strip):
|
||||||
|
parts = p.split("/")
|
||||||
|
if strip and len(parts) > strip:
|
||||||
|
parts = parts[strip:]
|
||||||
|
return "/".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_hunks(lines, hunks):
|
||||||
|
"""Apply parsed hunks (list of (old_start, old_lines, new_lines)) to lines, in order."""
|
||||||
|
out = list(lines)
|
||||||
|
offset = 0
|
||||||
|
for old_start, old_count, old_content, new_content in hunks:
|
||||||
|
idx = old_start - 1 + offset # 0-based
|
||||||
|
# find match allowing drift
|
||||||
|
best = None
|
||||||
|
for d in range(0, 200):
|
||||||
|
for sign in (1, -1):
|
||||||
|
cand = idx + d * sign
|
||||||
|
if cand < 0 or cand + old_count > len(out):
|
||||||
|
continue
|
||||||
|
if out[cand:cand + old_count] == old_content:
|
||||||
|
best = cand
|
||||||
|
break
|
||||||
|
if best is not None:
|
||||||
|
break
|
||||||
|
if best is None:
|
||||||
|
# try whitespace-insensitive
|
||||||
|
def norm(l):
|
||||||
|
return l.rstrip("\n")
|
||||||
|
for d in range(0, 400):
|
||||||
|
for sign in (1, -1):
|
||||||
|
cand = idx + d * sign
|
||||||
|
if cand < 0 or cand + old_count > len(out):
|
||||||
|
continue
|
||||||
|
if [norm(x) for x in out[cand:cand + old_count]] == [norm(x) for x in old_content]:
|
||||||
|
best = cand
|
||||||
|
break
|
||||||
|
if best is not None:
|
||||||
|
break
|
||||||
|
if best is None:
|
||||||
|
raise SystemExit(f"patch: hunk at line {old_start} failed to match")
|
||||||
|
out[best:best + old_count] = new_content
|
||||||
|
offset += len(new_content) - old_count
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def process_file(text, fname, strip, dir_):
|
||||||
|
# text: the file's section (after "diff --git" or first "---"), up to next file
|
||||||
|
lines = text.splitlines()
|
||||||
|
# find hunks
|
||||||
|
hunks = []
|
||||||
|
old_content_all = []
|
||||||
|
# We'll re-parse the raw section for hunk headers
|
||||||
|
i = 0
|
||||||
|
in_hunk = False
|
||||||
|
old_block = []
|
||||||
|
new_block = []
|
||||||
|
old_start = 0
|
||||||
|
while i < len(lines):
|
||||||
|
ln = lines[i]
|
||||||
|
m = re.match(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", ln)
|
||||||
|
if m:
|
||||||
|
if in_hunk and old_block:
|
||||||
|
old_count = len(old_block)
|
||||||
|
new_count = len(new_block)
|
||||||
|
hunks.append((old_start, old_count, old_block, new_block))
|
||||||
|
old_start = int(m.group(1))
|
||||||
|
old_count_expect = int(m.group(2) or 1)
|
||||||
|
new_count_expect = int(m.group(4) or 1)
|
||||||
|
old_block = []
|
||||||
|
new_block = []
|
||||||
|
in_hunk = True
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and len(old_block) < old_count_expect and len(new_block) < new_count_expect:
|
||||||
|
l = lines[i]
|
||||||
|
if l.startswith("+"):
|
||||||
|
new_block.append(l[1:] + "\n")
|
||||||
|
elif l.startswith("-"):
|
||||||
|
old_block.append(l[1:] + "\n")
|
||||||
|
elif l.startswith(" "):
|
||||||
|
old_block.append(l[1:] + "\n")
|
||||||
|
new_block.append(l[1:] + "\n")
|
||||||
|
elif l.startswith("\\"):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
if in_hunk and old_block:
|
||||||
|
old_count = len(old_block)
|
||||||
|
new_count = len(new_block)
|
||||||
|
hunks.append((old_start, old_count, old_block, new_block))
|
||||||
|
target = os.path.join(dir_, strip_path(fname, strip))
|
||||||
|
if not os.path.exists(target):
|
||||||
|
raise SystemExit(f"patch: target {target} not found")
|
||||||
|
with open(target, encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
out = apply_hunks(content.splitlines(True), hunks)
|
||||||
|
with open(target, "w", encoding="utf-8") as f:
|
||||||
|
f.writelines(out)
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
strip, dir_ = parse_args(sys.argv[1:])
|
||||||
|
data = sys.stdin.read()
|
||||||
|
# split into per-file sections
|
||||||
|
# A new file starts at a line "diff --git " or, for plain unified diffs, at "--- "
|
||||||
|
sections = []
|
||||||
|
cur = []
|
||||||
|
for ln in data.splitlines(keepends=True):
|
||||||
|
if ln.startswith("diff --git "):
|
||||||
|
if cur:
|
||||||
|
sections.append("".join(cur))
|
||||||
|
cur = [ln]
|
||||||
|
elif ln.startswith("--- ") and not any(c.startswith("diff --git") for c in cur):
|
||||||
|
# plain unified diff, new file
|
||||||
|
if cur:
|
||||||
|
sections.append("".join(cur))
|
||||||
|
cur = [ln]
|
||||||
|
else:
|
||||||
|
cur.append(ln)
|
||||||
|
if cur:
|
||||||
|
sections.append("".join(cur))
|
||||||
|
|
||||||
|
for sec in sections:
|
||||||
|
slines = sec.splitlines()
|
||||||
|
fname = None
|
||||||
|
for l in slines:
|
||||||
|
if l.startswith("diff --git "):
|
||||||
|
m = re.match(r"^diff --git (\S+) b/(\S+)", l)
|
||||||
|
if m:
|
||||||
|
fname = m.group(1) # keep the a/ prefix; strip_path removes it
|
||||||
|
break
|
||||||
|
if fname is None:
|
||||||
|
for l in slines:
|
||||||
|
if l.startswith("--- "):
|
||||||
|
p = l[4:].strip()
|
||||||
|
# drop timestamp (last whitespace-separated token if it starts with a date)
|
||||||
|
parts = p.split("\t")
|
||||||
|
p = parts[0].strip()
|
||||||
|
if p == "/dev/null":
|
||||||
|
continue
|
||||||
|
fname = p
|
||||||
|
break
|
||||||
|
if fname is None:
|
||||||
|
continue
|
||||||
|
# section body starts after the header; pass whole section, parser skips headers
|
||||||
|
process_file(sec, fname, strip, dir_)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user