gowalk-cicd 1.0.95 → 1.0.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +9 -2
- package/README.md +10 -3
- package/action/.daemux-version +1 -1
- package/action/action.yml +9 -13
- package/action/scripts/autoupdate_check.sh +8 -65
- package/action/scripts/autoupdate_stage.py +62 -0
- package/action/scripts/test_autoupdate_stage.py +107 -0
- package/android-action/.daemux-version +1 -1
- package/android-action/flutter-setup/action.yml +26 -0
- package/android-action/scripts/flutter_archive_cache.py +47 -0
- package/android-action/scripts/flutter_download.py +12 -9
- package/android-action/scripts/flutter_manifest.py +9 -4
- package/android-action/scripts/test_flutter_archive_cache.py +102 -0
- package/android-action/scripts/test_google_bootstrap_wiring.py +13 -4
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -155,7 +155,7 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
155
155
|
Unknown code or origins refuse; SDK setup, non-cache children and later steps retain their
|
|
156
156
|
proxy requirement. Never export this scope through `GITHUB_ENV` or add cache domains to
|
|
157
157
|
`NO_PROXY`. The preloader snapshot under `RUNNER_TEMP` also covers official post/save after
|
|
158
|
-
consumer autoupdate changes the action directory. The installer delivers this development
|
|
158
|
+
older consumer autoupdate changes the action directory. The installer delivers this development
|
|
159
159
|
tooling for native iOS repositories too; it does not require an Android app.
|
|
160
160
|
- Flutter manifest/archive GET recovery is scoped to the two pinned setup invocations through `BASH_ENV`.
|
|
161
161
|
It retains the account proxy and allows four attempts within 60 seconds for manifests or 20 minutes
|
|
@@ -165,6 +165,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
165
165
|
`flutter_download_failed` emits fixed transport codes and retained-byte counts, never curl stderr.
|
|
166
166
|
Archive progress emits retained byte counts every minute and safe results after each attempt;
|
|
167
167
|
the transfer deadline leaves room for a failure receipt before a 30-minute validation job ends.
|
|
168
|
+
With SDK caching enabled, failed archive transfers preserve public bytes in a job-owned,
|
|
169
|
+
digest-scoped directory. Separate optional cache restore/save steps retain them between runs;
|
|
170
|
+
they never expose partial bytes as an installed SDK. Successful or corrupt completed archives
|
|
171
|
+
are removed from that partial cache. Full SDK cache identities and provider transport stay unchanged.
|
|
168
172
|
- Web Release selects bundled renderer resources with `--no-web-resources-cdn` inside
|
|
169
173
|
the existing proxied build command. This is not a font or integration offline guarantee;
|
|
170
174
|
preserve app-authored font declarations, bootstrap configuration and other build inputs.
|
|
@@ -309,7 +313,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
309
313
|
from `${{ github.action_path }}` after that step. The plugin self-update
|
|
310
314
|
(`autoupdate_check.sh`) runs right after the snapshot and preserves its own source candidate,
|
|
311
315
|
before the version/signing/archive steps, so a red deploy still receives the
|
|
312
|
-
release that fixes it
|
|
316
|
+
release that fixes it. `autoupdate_stage.py` installs and preserves that update in one
|
|
317
|
+
task-owned detached worktree, then removes it. The active build's HEAD, index, dirty files
|
|
318
|
+
and action manifests stay unchanged: GitHub reparses local composites for post hooks using
|
|
319
|
+
cached step IDs, so a script snapshot alone cannot protect a changed manifest's step count.
|
|
313
320
|
`test_action_step_order.py` pins both. A new script reference in
|
|
314
321
|
`action/action.yml` therefore uses `"$SWIFT_APP_ACTION/scripts/..."`.
|
|
315
322
|
- Delivery dependencies may remain after a green job; each is a typed
|
package/README.md
CHANGED
|
@@ -321,6 +321,11 @@ and `retained_bytes`; `attempt_finished` adds `attempt`, `curl_exit`, `http_stat
|
|
|
321
321
|
and `proxy_status`. In-progress notices arrive each minute; elapsed time belongs to
|
|
322
322
|
the current attempt. Archives have a 20-minute total transfer budget so a failure
|
|
323
323
|
receipt precedes a 30-minute validation job's deadline. Progress is not a success receipt.
|
|
324
|
+
When SDK caching is enabled, an exhausted transfer preserves its partial public archive for
|
|
325
|
+
the next run under a separate release-specific cache key. Resumption still uses the selected
|
|
326
|
+
official manifest digest and the configured provider proxy; only a verified complete archive
|
|
327
|
+
can reach SDK extraction. Partial-cache restore uses the existing one-minute cache segment
|
|
328
|
+
bound. Optional cache operations do not extend the transfer deadline or turn a failed installation green.
|
|
324
329
|
|
|
325
330
|
Add a field by bumping the schema version; never change the meaning of an
|
|
326
331
|
existing one.
|
|
@@ -335,8 +340,10 @@ into its current task branch and land it through the normal checked PR, includin
|
|
|
335
340
|
when later deploy steps fail. CI never pushes to the default branch or rebases its
|
|
336
341
|
dirty build checkout; a failed preservation is a job failure. The run that
|
|
337
342
|
performed the update keeps executing the version it started with: the action
|
|
338
|
-
snapshots
|
|
339
|
-
|
|
343
|
+
snapshots its scripts to `$RUNNER_TEMP`. Updates install and publish from a separate,
|
|
344
|
+
owned detached worktree that is removed afterward. The active checkout's action
|
|
345
|
+
manifests remain unchanged too: GitHub reloads local composites for post hooks using
|
|
346
|
+
previously cached step IDs, so changing their step count can crash cleanup.
|
|
340
347
|
|
|
341
348
|
### Google Play upload retry
|
|
342
349
|
|
|
@@ -1011,7 +1018,7 @@ The vendored action ships with a per-run autoupdate check. On every
|
|
|
1011
1018
|
push to your default branch, the action queries npm for the latest
|
|
1012
1019
|
`gowalk-cicd`, compares against the local marker at
|
|
1013
1020
|
`.github/actions/swift-app/.daemux-version`, and if newer, re-vendors
|
|
1014
|
-
the package via `npx --yes` and preserves
|
|
1021
|
+
the package via `npx --yes` in a temporary detached worktree and preserves its action files
|
|
1015
1022
|
on a verified task branch (under both `.github/actions/swift-app/` and
|
|
1016
1023
|
`.github/actions/android-app/`).
|
|
1017
1024
|
`.github/workflows/deploy.yml` is NEVER auto-committed — see
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.97
|
package/action/action.yml
CHANGED
|
@@ -179,7 +179,7 @@ inputs:
|
|
|
179
179
|
description: >
|
|
180
180
|
Per-run check for a newer gowalk-cicd on npm. When 'true'
|
|
181
181
|
(default), the action queries npm on default-branch push runs and
|
|
182
|
-
|
|
182
|
+
installs the package in a detached worktree via `npx --yes` if its version is
|
|
183
183
|
newer than the marker at .github/actions/swift-app/.daemux-version.
|
|
184
184
|
The refreshed action files (NOT deploy.yml, which GITHUB_TOKEN
|
|
185
185
|
cannot push) are retained on a verified task branch for the app session's
|
|
@@ -190,14 +190,9 @@ inputs:
|
|
|
190
190
|
runs:
|
|
191
191
|
using: composite
|
|
192
192
|
steps:
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
# local-action consumers — while this run is still executing the
|
|
197
|
-
# action.yml the runner loaded at job start. Reading scripts from the
|
|
198
|
-
# refreshed tree under the old step definitions is a mid-run version
|
|
199
|
-
# mix; one copy taken before anything mutates keeps the whole run on
|
|
200
|
-
# one version of the plugin.
|
|
193
|
+
# Keep every script and prompt on the version this invocation started with.
|
|
194
|
+
# Auto-update installs and publishes from a detached worktree; it never changes
|
|
195
|
+
# action manifests that the runner may reparse using already-cached step IDs.
|
|
201
196
|
- name: Snapshot this action for the whole run
|
|
202
197
|
shell: bash
|
|
203
198
|
run: |
|
|
@@ -233,6 +228,8 @@ runs:
|
|
|
233
228
|
mkdir -p "$RUNNER_TEMP/swift-app-bot-scripts"
|
|
234
229
|
cp "$SWIFT_APP_ACTION/scripts/autoupdate_check.sh" \
|
|
235
230
|
"$RUNNER_TEMP/swift-app-bot-scripts/"
|
|
231
|
+
cp "$SWIFT_APP_ACTION/scripts/autoupdate_stage.py" \
|
|
232
|
+
"$RUNNER_TEMP/swift-app-bot-scripts/"
|
|
236
233
|
cp "$SWIFT_APP_ACTION/scripts/commit_bot_changes.sh" \
|
|
237
234
|
"$RUNNER_TEMP/swift-app-bot-scripts/"
|
|
238
235
|
cp "$SWIFT_APP_ACTION/scripts/source_maintenance.py" \
|
|
@@ -243,8 +240,8 @@ runs:
|
|
|
243
240
|
- name: Check for app-ci update
|
|
244
241
|
# Default branch + push event only. `auto-update` is on by default;
|
|
245
242
|
# consumers can pin via `false`. Runs from $BOT_SCRIPTS_DIR
|
|
246
|
-
# (RUNNER_TEMP)
|
|
247
|
-
#
|
|
243
|
+
# (RUNNER_TEMP); installation and candidate preservation run in an isolated
|
|
244
|
+
# checkout so current action manifests and scripts remain unchanged.
|
|
248
245
|
if: ${{ inputs.auto-update == 'true'
|
|
249
246
|
&& github.event_name == 'push'
|
|
250
247
|
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
@@ -521,8 +518,7 @@ runs:
|
|
|
521
518
|
# this action; this script is a no-op when nothing is staged, so a
|
|
522
519
|
# PR / feature-branch run that somehow reaches this step does no
|
|
523
520
|
# harm — but we still gate on default-branch push for clarity.
|
|
524
|
-
# Runs from $BOT_SCRIPTS_DIR
|
|
525
|
-
# ${action_path} earlier in this run and must not swap scripts under us.
|
|
521
|
+
# Runs from the same immutable $BOT_SCRIPTS_DIR as the update candidate.
|
|
526
522
|
if: ${{ (inputs.archive == 'true' || inputs.auto-update == 'true')
|
|
527
523
|
&& github.event_name == 'push'
|
|
528
524
|
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Checks npm for newer gowalk-cicd
|
|
3
|
-
#
|
|
2
|
+
# Checks npm for newer gowalk-cicd and preserves an isolated update candidate.
|
|
3
|
+
# Exits 0 on no-op or package-install failure; unconfirmed source preservation fails.
|
|
4
4
|
#
|
|
5
5
|
# DESIGN DECISION (intentional, not a bug):
|
|
6
6
|
# This script runs `npx --yes gowalk-cicd@<latest>` on every default-
|
|
@@ -53,66 +53,9 @@ if [[ "$current" == "$latest" ]]; then
|
|
|
53
53
|
exit 0
|
|
54
54
|
fi
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
git config user.name >/dev/null 2>&1 || git config user.name "github-actions[bot]"
|
|
64
|
-
git config user.email >/dev/null 2>&1 || \
|
|
65
|
-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
66
|
-
|
|
67
|
-
# NOTE: We deliberately do NOT stage .github/workflows/deploy.yml here.
|
|
68
|
-
# GITHUB_TOKEN cannot push workflow-file changes regardless of
|
|
69
|
-
# `permissions: contents: write` (GH security policy: prevents CI from
|
|
70
|
-
# modifying its own triggers). Consumer-side deploy.yml updates require
|
|
71
|
-
# manual `npx --yes gowalk-cicd` by the user. Document this in
|
|
72
|
-
# the action README and changelog when shipping a deploy.yml schema change.
|
|
73
|
-
|
|
74
|
-
# Stage everything under the vendored action dir EXCEPT Python bytecode
|
|
75
|
-
# caches. The action's own scripts run during this CI job and Python
|
|
76
|
-
# generates `__pycache__/*.pyc` files inside scripts/ as a side effect of
|
|
77
|
-
# importing them. Those caches are build artifacts of *this* run, not part
|
|
78
|
-
# of the action distribution -- sweeping them into the autoupdate bot
|
|
79
|
-
# commit is benign but noisy (every clean run emits a "refresh
|
|
80
|
-
# __pycache__" diff).
|
|
81
|
-
#
|
|
82
|
-
# DESIGN DECISION: use `git add -A` (stages additions, modifications, AND
|
|
83
|
-
# deletions) instead of the previous `git ls-files -mo` flow. `ls-files
|
|
84
|
-
# -mo` enumerates modified + other (untracked) paths but NOT deletions:
|
|
85
|
-
# when an autoupdate cycle removes a script (e.g. `prepare_signing.py`
|
|
86
|
-
# gets renamed to `prepare_signing_v2.py` in a new release), the old
|
|
87
|
-
# file would stay tracked in the consumer repo forever -- the bot
|
|
88
|
-
# commit would add the new file but never remove the orphan. `git add
|
|
89
|
-
# -A` handles all three change kinds in one call. We then unstage any
|
|
90
|
-
# pycache paths that the broad `-A` swept in.
|
|
91
|
-
#
|
|
92
|
-
# Force flag (`-f`) is implicit in `git add -A` for explicitly listed
|
|
93
|
-
# pathspecs that match .gitignore patterns? No -- `-A` still honors
|
|
94
|
-
# .gitignore for untracked files. To preserve the prior semantics
|
|
95
|
-
# (consumer's .gitignore must not silently drop autoupdate-managed
|
|
96
|
-
# files like `.daemux-version`), we pass `--force` explicitly.
|
|
97
|
-
#
|
|
98
|
-
# BSD xargs (default on macos-15 runners) does NOT support
|
|
99
|
-
# `-r`/`--no-run-if-empty`, so we capture pycache paths into a variable
|
|
100
|
-
# and skip the xargs call when empty -- portable across BSD and GNU.
|
|
101
|
-
git add -A --force -- \
|
|
102
|
-
.github/actions/swift-app/ \
|
|
103
|
-
.github/actions/android-app/ 2>/dev/null || true
|
|
104
|
-
|
|
105
|
-
# Unstage Python bytecode that the broad `-A` swept in. `git diff
|
|
106
|
-
# --cached --name-only` lists every staged path; we filter to pycache
|
|
107
|
-
# entries and `git reset HEAD --` to drop them from the index. Working
|
|
108
|
-
# tree is left untouched (the .pyc files remain on disk; only their
|
|
109
|
-
# index entries are removed).
|
|
110
|
-
pycache="$(git diff --cached --name-only -- \
|
|
111
|
-
.github/actions/swift-app/ .github/actions/android-app/ \
|
|
112
|
-
| grep -E '(/__pycache__/|\.pyc$)' || true)"
|
|
113
|
-
if [[ -n "$pycache" ]]; then
|
|
114
|
-
printf '%s\n' "$pycache" \
|
|
115
|
-
| tr '\n' '\0' \
|
|
116
|
-
| xargs -0 git reset HEAD -- 2>/dev/null || true
|
|
117
|
-
fi
|
|
118
|
-
echo "autoupdate: staged refreshed iOS + Android actions (deploy.yml not staged; __pycache__ excluded)"
|
|
56
|
+
# The runner reparses local action.yml files for post hooks and reuses the step IDs
|
|
57
|
+
# it cached before execution. Updating these files in place can change their step
|
|
58
|
+
# count and crash cleanup. Publish the update candidate from a detached worktree;
|
|
59
|
+
# the current job keeps its exact manifests, scripts, index and uncommitted source.
|
|
60
|
+
echo "autoupdate: $PKG $current -> $latest; preparing an isolated source candidate"
|
|
61
|
+
exec python3 "$(dirname "$0")/autoupdate_stage.py" "$latest"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Prepare CI updates away from action manifests the current runner may reload."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def git(root: Path, *args: str) -> str:
|
|
14
|
+
result = subprocess.run(["git", *args], cwd=root, capture_output=True,
|
|
15
|
+
text=True, check=False, timeout=60)
|
|
16
|
+
if result.returncode:
|
|
17
|
+
raise RuntimeError("autoupdate_worktree_git_failed")
|
|
18
|
+
return result.stdout.strip()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def stage(root: Path) -> None:
|
|
22
|
+
git(root, "add", "-A", "--force", "--", ".github/actions/swift-app/", ".github/actions/android-app/")
|
|
23
|
+
names = git(root, "diff", "--cached", "--name-only", "-z").split("\0")
|
|
24
|
+
bytecode = [name for name in names if "/__pycache__/" in name or name.endswith((".pyc", ".pyo"))]
|
|
25
|
+
if bytecode:
|
|
26
|
+
git(root, "reset", "--quiet", "HEAD", "--", *bytecode)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def update(version: str) -> None:
|
|
30
|
+
if not re.fullmatch(r"\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?", version):
|
|
31
|
+
raise RuntimeError("autoupdate_version_invalid")
|
|
32
|
+
root = Path.cwd()
|
|
33
|
+
base = git(root, "rev-parse", "HEAD")
|
|
34
|
+
parent = Path(tempfile.mkdtemp(prefix="app-ci-update-", dir=os.environ["RUNNER_TEMP"]))
|
|
35
|
+
worktree = parent / "checkout"
|
|
36
|
+
added = False
|
|
37
|
+
try:
|
|
38
|
+
git(root, "worktree", "add", "--detach", str(worktree), base)
|
|
39
|
+
added = True
|
|
40
|
+
result = subprocess.run(["npx", "--yes", "gowalk-cicd@" + version], cwd=worktree,
|
|
41
|
+
check=False, timeout=180)
|
|
42
|
+
if result.returncode:
|
|
43
|
+
print("::warning::autoupdate: pinned package installation failed; keeping the current build")
|
|
44
|
+
return
|
|
45
|
+
stage(worktree)
|
|
46
|
+
script = Path(__file__).with_name("commit_bot_changes.sh")
|
|
47
|
+
result = subprocess.run(["bash", str(script)], cwd=worktree, check=False, timeout=180)
|
|
48
|
+
if result.returncode:
|
|
49
|
+
raise RuntimeError("autoupdate_source_preservation_failed")
|
|
50
|
+
finally:
|
|
51
|
+
if added:
|
|
52
|
+
git(root, "worktree", "remove", "--force", str(worktree))
|
|
53
|
+
shutil.rmtree(parent)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
try:
|
|
58
|
+
update(sys.argv[1])
|
|
59
|
+
except (RuntimeError, OSError, subprocess.SubprocessError) as exc:
|
|
60
|
+
code = str(exc) if isinstance(exc, RuntimeError) else "autoupdate_operation_unverified"
|
|
61
|
+
print("::error::" + code, file=sys.stderr)
|
|
62
|
+
raise SystemExit(1) from None
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Exercise real isolated installs and protected-remote publication without changing a live action."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import textwrap
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from source_maintenance_fixture import MaintenanceFixture
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AutoupdateTests(MaintenanceFixture):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
super().setUp()
|
|
14
|
+
self.remote_base = self.base
|
|
15
|
+
self.action = self.repo / ".github/actions/android-app/flutter-setup/action.yml"
|
|
16
|
+
self.marker = self.repo / ".github/actions/swift-app/.daemux-version"
|
|
17
|
+
self.action.parent.mkdir(parents=True)
|
|
18
|
+
self.marker.parent.mkdir(parents=True)
|
|
19
|
+
self.action.write_text("runs:\n using: composite\n steps:\n - run: old\n shell: bash\n")
|
|
20
|
+
self.marker.write_text("1.2.3")
|
|
21
|
+
self.git("add", ".github")
|
|
22
|
+
self.git("commit", "-m", "Existing action")
|
|
23
|
+
self.base = self.git("rev-parse", "HEAD").strip()
|
|
24
|
+
self.env["GITHUB_SHA"] = self.base
|
|
25
|
+
config = self.root / "checkout-auth.config"
|
|
26
|
+
self.git("config", "--file", str(config), "fixture.token", "worktree-allowed")
|
|
27
|
+
self.git("config", "includeIf.gitdir:" + str((self.repo / ".git").resolve()) + "/worktrees/*.path",
|
|
28
|
+
str(config))
|
|
29
|
+
self.scratch = self.root / "runner-temp"
|
|
30
|
+
self.scratch.mkdir()
|
|
31
|
+
self.env["RUNNER_TEMP"] = str(self.scratch)
|
|
32
|
+
binary = self.root / "bin"
|
|
33
|
+
binary.mkdir()
|
|
34
|
+
(binary / "npm").write_text("#!/bin/sh\nprintf '1.2.4\\n'\n")
|
|
35
|
+
(binary / "npm").chmod(0o755)
|
|
36
|
+
self.installer = binary / "npx"
|
|
37
|
+
self.installer.write_text("#!/bin/sh\nset -eu\n"
|
|
38
|
+
"[ \"$1\" = --yes ] && [ \"$2\" = gowalk-cicd@1.2.4 ]\n"
|
|
39
|
+
"[ \"$(git config --get fixture.token)\" = worktree-allowed ]\n"
|
|
40
|
+
"printf '1.2.4' > .github/actions/swift-app/.daemux-version\n"
|
|
41
|
+
"printf ' - run: added\\n shell: bash\\n' >> .github/actions/android-app/flutter-setup/action.yml\n"
|
|
42
|
+
"mkdir -p .github/workflows .github/actions/swift-app/scripts/__pycache__\n"
|
|
43
|
+
"printf 'never publish workflow' > .github/workflows/deploy.yml\n"
|
|
44
|
+
"printf 'never publish bytecode' > .github/actions/swift-app/scripts/__pycache__/test.pyc\n"
|
|
45
|
+
"exit ${INSTALL_EXIT:-0}\n")
|
|
46
|
+
self.installer.chmod(0o755)
|
|
47
|
+
self.env["PATH"] = str(binary) + ":" + self.env["PATH"]
|
|
48
|
+
self.script = Path(__file__).with_name("autoupdate_check.sh")
|
|
49
|
+
|
|
50
|
+
def assert_current_build_preserved(self, original, staged):
|
|
51
|
+
self.assertEqual(self.action.read_bytes(), original)
|
|
52
|
+
self.assertEqual(self.marker.read_text(), "1.2.3")
|
|
53
|
+
self.assertEqual(self.git("diff", "--cached"), staged)
|
|
54
|
+
self.assertEqual(self.git("rev-parse", "HEAD").strip(), self.base)
|
|
55
|
+
self.assertEqual(self.git("ls-remote", "origin", "refs/heads/main").split()[0], self.remote_base)
|
|
56
|
+
self.assertEqual(len(self.git("worktree", "list", "--porcelain").split("worktree ")) - 1, 1)
|
|
57
|
+
self.assertEqual(list(self.scratch.iterdir()), [])
|
|
58
|
+
|
|
59
|
+
def test_growing_composite_and_replay_preserve_current_build_and_publish_exact_update(self):
|
|
60
|
+
self.stage()
|
|
61
|
+
original, staged = self.action.read_bytes(), self.git("diff", "--cached")
|
|
62
|
+
first = self.receipt(self.run_script())
|
|
63
|
+
self.assert_current_build_preserved(original, staged)
|
|
64
|
+
updated = self.git("show", first["head_sha"] + ":.github/actions/android-app/flutter-setup/action.yml")
|
|
65
|
+
self.assertEqual(updated.count("- run:"), 2)
|
|
66
|
+
self.assertEqual(original.decode().count("- run:"), 1)
|
|
67
|
+
paths = self.git("diff", "--name-only", self.base, first["head_sha"]).splitlines()
|
|
68
|
+
self.assertEqual(paths, [".github/actions/android-app/flutter-setup/action.yml",
|
|
69
|
+
".github/actions/swift-app/.daemux-version"])
|
|
70
|
+
self.assertEqual(self.receipt(self.run_script()), first)
|
|
71
|
+
self.assert_current_build_preserved(original, staged)
|
|
72
|
+
|
|
73
|
+
def test_failed_partial_install_keeps_current_action_and_cleans_owned_worktree(self):
|
|
74
|
+
self.stage()
|
|
75
|
+
original, staged = self.action.read_bytes(), self.git("diff", "--cached")
|
|
76
|
+
self.env["INSTALL_EXIT"] = "1"
|
|
77
|
+
result = self.run_script()
|
|
78
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
79
|
+
self.assertIn("pinned package installation failed", result.stdout)
|
|
80
|
+
self.assertNotIn("source_maintenance_pending", result.stdout)
|
|
81
|
+
self.assert_current_build_preserved(original, staged)
|
|
82
|
+
self.assertEqual(len(self.git("ls-remote", "--heads", "origin").splitlines()), 1)
|
|
83
|
+
|
|
84
|
+
def test_unconfirmed_preservation_is_not_a_success(self):
|
|
85
|
+
self.stage()
|
|
86
|
+
original, staged = self.action.read_bytes(), self.git("diff", "--cached")
|
|
87
|
+
self.hook.write_text("#!/bin/sh\nexit 1\n")
|
|
88
|
+
result = self.run_script()
|
|
89
|
+
self.assertNotEqual(result.returncode, 0)
|
|
90
|
+
self.assertIn("autoupdate_source_preservation_failed", result.stderr)
|
|
91
|
+
self.assert_current_build_preserved(original, staged)
|
|
92
|
+
|
|
93
|
+
def test_actual_action_snapshot_can_execute_the_update_helper(self):
|
|
94
|
+
action = Path(__file__).resolve().parents[1]
|
|
95
|
+
source = (action / "action.yml").read_text()
|
|
96
|
+
block = re.search(r" - name: Stage bot-side scripts to runner temp\n(.*?)(?=\n - name:)",
|
|
97
|
+
source, re.S)[1]
|
|
98
|
+
script = textwrap.dedent(block.split(" run: |\n", 1)[1])
|
|
99
|
+
self.env.update({"SWIFT_APP_ACTION": str(action), "GITHUB_ENV": str(self.root / "environment")})
|
|
100
|
+
subprocess.run(["bash", "-e", "-c", script], env=self.env, cwd=self.repo, check=True,
|
|
101
|
+
text=True, capture_output=True, timeout=10)
|
|
102
|
+
self.script = self.scratch / "swift-app-bot-scripts/autoupdate_check.sh"
|
|
103
|
+
self.assertEqual(self.receipt(self.run_script())["base_sha"], self.base)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
unittest.main()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.97
|
|
@@ -138,6 +138,18 @@ runs:
|
|
|
138
138
|
with:
|
|
139
139
|
path: ${{ steps.flutter-action.outputs.PUB-CACHE-PATH }}
|
|
140
140
|
key: ${{ steps.flutter-action.outputs.PUB-CACHE-KEY }}-${{ hashFiles('**/pubspec.lock') }}
|
|
141
|
+
- name: Restore interrupted Flutter archive
|
|
142
|
+
if: ${{ inputs.cache == 'true' && inputs.dry-run != 'true' && steps.cache-flutter.outputs.cache-hit != 'true' }}
|
|
143
|
+
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25
|
|
144
|
+
continue-on-error: true
|
|
145
|
+
env:
|
|
146
|
+
NODE_OPTIONS: --require "${{ steps.scope.outputs.preloader }}"
|
|
147
|
+
SEGMENT_DOWNLOAD_TIMEOUT_MINS: '1'
|
|
148
|
+
with:
|
|
149
|
+
path: ${{ runner.temp }}/gowalk-flutter-archives
|
|
150
|
+
key: >-
|
|
151
|
+
flutter-archive-v1-${{ steps.flutter-action.outputs.CACHE-KEY }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
152
|
+
restore-keys: flutter-archive-v1-${{ steps.flutter-action.outputs.CACHE-KEY }}-
|
|
141
153
|
- name: Install or reuse the resolved Flutter SDK
|
|
142
154
|
id: install
|
|
143
155
|
if: ${{ inputs.dry-run != 'true' }}
|
|
@@ -145,6 +157,8 @@ runs:
|
|
|
145
157
|
env:
|
|
146
158
|
BASH_ENV: ${{ steps.transport.outputs.manifest_env }}
|
|
147
159
|
GOWALK_FLUTTER_MANIFEST_HELPER: ${{ steps.transport.outputs.manifest_helper }}
|
|
160
|
+
GOWALK_FLUTTER_ARCHIVE_CACHE: >-
|
|
161
|
+
${{ inputs.cache == 'true' && format('{0}/gowalk-flutter-archives', runner.temp) || '' }}
|
|
148
162
|
with:
|
|
149
163
|
channel: ${{ steps.flutter-action.outputs.CHANNEL }}
|
|
150
164
|
flutter-version: ${{ steps.flutter-action.outputs.VERSION }}
|
|
@@ -158,6 +172,18 @@ runs:
|
|
|
158
172
|
pub-cache-path: ${{ steps.flutter-action.outputs.PUB-CACHE-PATH }}
|
|
159
173
|
dry-run: 'false'
|
|
160
174
|
git-source: ${{ inputs.git-source }}
|
|
175
|
+
- name: Preserve interrupted Flutter archive
|
|
176
|
+
if: >-
|
|
177
|
+
${{ !cancelled() && inputs.cache == 'true' && steps.install.outcome == 'failure'
|
|
178
|
+
&& steps.scope.outcome == 'success' }}
|
|
179
|
+
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25
|
|
180
|
+
continue-on-error: true
|
|
181
|
+
env:
|
|
182
|
+
NODE_OPTIONS: --require "${{ steps.scope.outputs.preloader }}"
|
|
183
|
+
with:
|
|
184
|
+
path: ${{ runner.temp }}/gowalk-flutter-archives
|
|
185
|
+
key: >-
|
|
186
|
+
flutter-archive-v1-${{ steps.flutter-action.outputs.CACHE-KEY }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
161
187
|
# A later app failure must not discard a successfully installed, expensive SDK.
|
|
162
188
|
- name: Save installed Flutter SDK
|
|
163
189
|
if: >-
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Keep only public SDK archive bytes in the job's optional, digest-scoped download cache."""
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import flutter_download
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cached_directory() -> Path:
|
|
11
|
+
root = Path(os.environ["GOWALK_FLUTTER_ARCHIVE_CACHE"])
|
|
12
|
+
temporary = Path(os.environ["RUNNER_TEMP"]).resolve(strict=True)
|
|
13
|
+
if root.is_symlink() or root.parent.resolve(strict=True) != temporary:
|
|
14
|
+
raise ValueError("flutter_archive_cache_scope_invalid")
|
|
15
|
+
root.mkdir(mode=0o700, exist_ok=True)
|
|
16
|
+
directory = root / flutter_download.archive_digest()
|
|
17
|
+
if directory.is_symlink():
|
|
18
|
+
raise ValueError("flutter_archive_cache_scope_invalid")
|
|
19
|
+
directory.mkdir(mode=0o700, exist_ok=True)
|
|
20
|
+
destination = directory / "download"
|
|
21
|
+
if destination.is_symlink() or (destination.exists() and not destination.is_file()):
|
|
22
|
+
raise ValueError("flutter_archive_cache_scope_invalid")
|
|
23
|
+
if destination.exists() and destination.stat().st_size > flutter_download.MAX_ARCHIVE_BYTES:
|
|
24
|
+
raise ValueError("flutter_archive_size_exceeded")
|
|
25
|
+
return directory
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@contextmanager
|
|
29
|
+
def workspace(kind: str):
|
|
30
|
+
if kind != "archive" or not os.environ.get("GOWALK_FLUTTER_ARCHIVE_CACHE"):
|
|
31
|
+
with tempfile.TemporaryDirectory(prefix="flutter-manifest-", dir=os.environ["RUNNER_TEMP"]) as scratch:
|
|
32
|
+
yield Path(scratch)
|
|
33
|
+
return
|
|
34
|
+
directory = cached_directory()
|
|
35
|
+
try:
|
|
36
|
+
yield directory
|
|
37
|
+
except ValueError as error:
|
|
38
|
+
if str(error) == "flutter_archive_checksum_mismatch":
|
|
39
|
+
(directory / "download").unlink(missing_ok=True)
|
|
40
|
+
raise
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def reuse_complete(destination: Path, kind: str) -> bool:
|
|
44
|
+
if kind != "archive" or not destination.stat().st_size or not flutter_download.archive_matches(destination):
|
|
45
|
+
return False
|
|
46
|
+
flutter_download.publish(destination, kind)
|
|
47
|
+
return True
|
|
@@ -63,18 +63,21 @@ def progress(destination: Path, started: float, event: str, **fields) -> None:
|
|
|
63
63
|
file=sys.stderr, flush=True)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def archive_matches(destination: Path) -> bool:
|
|
67
|
+
if destination.stat().st_size > MAX_ARCHIVE_BYTES:
|
|
68
|
+
raise ValueError("flutter_archive_size_exceeded")
|
|
69
|
+
digest = hashlib.sha256()
|
|
70
|
+
with destination.open("rb") as source:
|
|
71
|
+
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
72
|
+
digest.update(chunk)
|
|
73
|
+
return digest.hexdigest() == archive_digest()
|
|
74
|
+
|
|
75
|
+
|
|
66
76
|
def publish(destination: Path, kind: str) -> None:
|
|
67
77
|
if kind == "manifest":
|
|
68
78
|
json.loads(destination.read_bytes())
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
raise ValueError("flutter_archive_size_exceeded")
|
|
72
|
-
digest = hashlib.sha256()
|
|
73
|
-
with destination.open("rb") as source:
|
|
74
|
-
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
75
|
-
digest.update(chunk)
|
|
76
|
-
if digest.hexdigest() != archive_digest():
|
|
77
|
-
raise ValueError("flutter_archive_checksum_mismatch")
|
|
79
|
+
elif not archive_matches(destination):
|
|
80
|
+
raise ValueError("flutter_archive_checksum_mismatch")
|
|
78
81
|
with destination.open("rb") as source:
|
|
79
82
|
shutil.copyfileobj(source, sys.stdout.buffer, length=1024 * 1024)
|
|
80
83
|
|
|
@@ -5,11 +5,11 @@ import os
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import re
|
|
7
7
|
import sys
|
|
8
|
-
import tempfile
|
|
9
8
|
import time
|
|
10
9
|
from urllib.parse import urlsplit
|
|
11
10
|
|
|
12
11
|
import flutter_download
|
|
12
|
+
import flutter_archive_cache
|
|
13
13
|
|
|
14
14
|
RETRY_CODES = {5, 6, 7, 18, 28, 35, 52, 55, 56, 92, 95}
|
|
15
15
|
RETRY_HTTP = {408, 429, 500, 502, 503, 504}
|
|
@@ -24,8 +24,10 @@ def download(arguments: list[str], directory: Path) -> int:
|
|
|
24
24
|
child_env = {**os.environ, "NO_PROXY": "", "no_proxy": ""}
|
|
25
25
|
deadline, code, http, proxy_http = time.monotonic() + (60 if kind == "manifest" else 1200), 1, 0, 0
|
|
26
26
|
destination = directory / "download"
|
|
27
|
-
destination.touch(mode=0o600, exist_ok=
|
|
27
|
+
destination.touch(mode=0o600, exist_ok=True)
|
|
28
28
|
destination.chmod(0o600)
|
|
29
|
+
if flutter_archive_cache.reuse_complete(destination, kind):
|
|
30
|
+
return 0
|
|
29
31
|
attempted = 0
|
|
30
32
|
for attempt in range(1, 5):
|
|
31
33
|
remaining = deadline - time.monotonic()
|
|
@@ -74,8 +76,11 @@ def request_kind(arguments):
|
|
|
74
76
|
|
|
75
77
|
def main() -> int:
|
|
76
78
|
try:
|
|
77
|
-
with
|
|
78
|
-
|
|
79
|
+
with flutter_archive_cache.workspace(request_kind(sys.argv[1:])) as scratch:
|
|
80
|
+
result = download(sys.argv[1:], scratch)
|
|
81
|
+
if result == 0:
|
|
82
|
+
(scratch / "download").unlink(missing_ok=True)
|
|
83
|
+
return result
|
|
79
84
|
except (ValueError, OSError, KeyError) as error:
|
|
80
85
|
safe = {"flutter_archive_digest_required", "flutter_archive_size_exceeded", "flutter_archive_checksum_mismatch"}
|
|
81
86
|
reason = str(error) if str(error) in safe else "flutter_manifest_invalid_request_or_response"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""A later job resumes retained public SDK bytes without bypassing digest or account checks."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
from flutter_manifest_fixture import tls_proxy
|
|
13
|
+
import test_flutter_archive as archive_fixture
|
|
14
|
+
|
|
15
|
+
ARCHIVE, ROOT = archive_fixture.ARCHIVE, archive_fixture.ROOT
|
|
16
|
+
DIGEST = hashlib.sha256(ARCHIVE).hexdigest()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def invoke(root, server, cert, **kwargs):
|
|
20
|
+
with patch.dict(os.environ, {"GOWALK_FLUTTER_ARCHIVE_CACHE": str(root / "gowalk-flutter-archives")}):
|
|
21
|
+
return archive_fixture.ArchiveRecoveryTests().invoke(root, server, cert, **kwargs)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ArchiveCacheTests(unittest.TestCase):
|
|
25
|
+
def test_later_job_resumes_after_the_previous_job_exhausted_its_retries(self):
|
|
26
|
+
with tempfile.TemporaryDirectory() as first, tempfile.TemporaryDirectory() as second:
|
|
27
|
+
root, resumed = Path(first), Path(second)
|
|
28
|
+
with tls_proxy(root, ["partial", "range_partial", "range_partial", "range_partial"],
|
|
29
|
+
ARCHIVE) as (server, cert):
|
|
30
|
+
result = invoke(root, server, cert)
|
|
31
|
+
self.assertEqual((result.returncode, result.stdout), (18, b""))
|
|
32
|
+
partial = root / "gowalk-flutter-archives" / DIGEST / "download"
|
|
33
|
+
self.assertEqual(partial.read_bytes(), ARCHIVE[:124])
|
|
34
|
+
shutil.copytree(root / "gowalk-flutter-archives", resumed / "gowalk-flutter-archives")
|
|
35
|
+
with tls_proxy(resumed, ["range"], ARCHIVE) as (server, cert):
|
|
36
|
+
result = invoke(resumed, server, cert)
|
|
37
|
+
self.assertEqual((result.returncode, result.stdout), (0, ARCHIVE))
|
|
38
|
+
self.assertEqual(server.ranges, ["bytes=124-"])
|
|
39
|
+
self.assertFalse((resumed / "gowalk-flutter-archives" / DIGEST / "download").exists())
|
|
40
|
+
|
|
41
|
+
def test_complete_cached_archive_is_verified_without_another_archive_request(self):
|
|
42
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
43
|
+
root = Path(directory)
|
|
44
|
+
partial = root / "gowalk-flutter-archives" / DIGEST / "download"
|
|
45
|
+
partial.parent.mkdir(parents=True)
|
|
46
|
+
partial.write_bytes(ARCHIVE)
|
|
47
|
+
with tls_proxy(root, [], ARCHIVE) as (server, cert):
|
|
48
|
+
result = invoke(root, server, cert)
|
|
49
|
+
self.assertEqual((result.returncode, result.stdout, server.connects), (0, ARCHIVE, 0))
|
|
50
|
+
|
|
51
|
+
def test_complete_cached_bytes_cannot_bypass_a_missing_proxy(self):
|
|
52
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
53
|
+
root = Path(directory)
|
|
54
|
+
partial = root / "gowalk-flutter-archives" / DIGEST / "download"
|
|
55
|
+
partial.parent.mkdir(parents=True)
|
|
56
|
+
partial.write_bytes(ARCHIVE)
|
|
57
|
+
env = {**os.environ, "RUNNER_TEMP": str(root), "GOWALK_FLUTTER_ARCHIVE_SHA256": DIGEST,
|
|
58
|
+
"GOWALK_FLUTTER_ARCHIVE_CACHE": str(root / "gowalk-flutter-archives"),
|
|
59
|
+
"HTTPS_PROXY": "", "https_proxy": ""}
|
|
60
|
+
url = "https://provider.invalid/flutter_infra_release/releases/stable/sdk.tar.xz"
|
|
61
|
+
result = subprocess.run([sys.executable, str(ROOT / "flutter_manifest.py"),
|
|
62
|
+
"--connect-timeout", "15", "--retry", "5", url],
|
|
63
|
+
env=env, capture_output=True, timeout=10)
|
|
64
|
+
self.assertEqual((result.returncode, result.stdout), (1, b""))
|
|
65
|
+
self.assertEqual(partial.read_bytes(), ARCHIVE)
|
|
66
|
+
|
|
67
|
+
def test_changed_digest_cannot_reuse_another_release_partial(self):
|
|
68
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
69
|
+
root = Path(directory)
|
|
70
|
+
foreign = root / "gowalk-flutter-archives" / ("a" * 64) / "download"
|
|
71
|
+
foreign.parent.mkdir(parents=True)
|
|
72
|
+
foreign.write_bytes(b"unrelated release")
|
|
73
|
+
with tls_proxy(root, ["ok"], ARCHIVE) as (server, cert):
|
|
74
|
+
result = invoke(root, server, cert)
|
|
75
|
+
self.assertEqual((result.returncode, result.stdout), (0, ARCHIVE))
|
|
76
|
+
self.assertEqual(server.ranges, [""])
|
|
77
|
+
self.assertEqual(foreign.read_bytes(), b"unrelated release")
|
|
78
|
+
|
|
79
|
+
def test_corrupted_restored_partial_fails_verification_and_is_not_cached_again(self):
|
|
80
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
81
|
+
root = Path(directory)
|
|
82
|
+
partial = root / "gowalk-flutter-archives" / DIGEST / "download"
|
|
83
|
+
partial.parent.mkdir(parents=True)
|
|
84
|
+
partial.write_bytes(b"wrong bytes")
|
|
85
|
+
with tls_proxy(root, ["range"], ARCHIVE) as (server, cert):
|
|
86
|
+
result = invoke(root, server, cert)
|
|
87
|
+
self.assertEqual((result.returncode, result.stdout), (1, b""))
|
|
88
|
+
self.assertEqual(server.ranges, ["bytes=11-"])
|
|
89
|
+
self.assertFalse(partial.exists())
|
|
90
|
+
|
|
91
|
+
def test_symlinked_cache_does_not_read_or_write_another_directory(self):
|
|
92
|
+
with tempfile.TemporaryDirectory() as directory, tempfile.TemporaryDirectory() as outside:
|
|
93
|
+
root = Path(directory)
|
|
94
|
+
(root / "gowalk-flutter-archives").symlink_to(outside, target_is_directory=True)
|
|
95
|
+
with tls_proxy(root, [], ARCHIVE) as (server, cert):
|
|
96
|
+
result = invoke(root, server, cert)
|
|
97
|
+
self.assertEqual((result.returncode, result.stdout, server.connects), (1, b"", 0))
|
|
98
|
+
self.assertEqual(list(Path(outside).iterdir()), [])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
unittest.main()
|
|
@@ -24,7 +24,11 @@ class BootstrapWiringTests(unittest.TestCase):
|
|
|
24
24
|
self.assertNotIn("continue-on-error", step)
|
|
25
25
|
self.assertEqual(checked, 3)
|
|
26
26
|
setup = yaml.safe_load((ROOT / "android-action/flutter-setup/action.yml").read_text())
|
|
27
|
-
|
|
27
|
+
steps = setup["runs"]["steps"]
|
|
28
|
+
by_id = {step.get("id"): step for step in steps}
|
|
29
|
+
guard, scope, resolve, sdk_cache, pub_cache, install = (
|
|
30
|
+
by_id[name] for name in ("transport", "scope", "flutter-action", "cache-flutter", "cache-pub", "install"))
|
|
31
|
+
save = next(step for step in steps if step["name"] == "Save installed Flutter SDK")
|
|
28
32
|
self.assertIn('play_store_proxy.py" --bootstrap-env', guard["run"])
|
|
29
33
|
self.assertNotIn("continue-on-error", guard)
|
|
30
34
|
self.assertTrue(scope["continue-on-error"])
|
|
@@ -32,9 +36,13 @@ class BootstrapWiringTests(unittest.TestCase):
|
|
|
32
36
|
self.assertEqual(resolve["with"]["dry-run"], "true")
|
|
33
37
|
for step in (resolve, install):
|
|
34
38
|
self.assertEqual(step["uses"], "subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2")
|
|
35
|
-
|
|
39
|
+
expected_env = {
|
|
36
40
|
"BASH_ENV": "${{ steps.transport.outputs.manifest_env }}",
|
|
37
|
-
"GOWALK_FLUTTER_MANIFEST_HELPER": "${{ steps.transport.outputs.manifest_helper }}"}
|
|
41
|
+
"GOWALK_FLUTTER_MANIFEST_HELPER": "${{ steps.transport.outputs.manifest_helper }}"}
|
|
42
|
+
if step is install:
|
|
43
|
+
expected_env["GOWALK_FLUTTER_ARCHIVE_CACHE"] = (
|
|
44
|
+
"${{ inputs.cache == 'true' && format('{0}/gowalk-flutter-archives', runner.temp) || '' }}")
|
|
45
|
+
self.assertEqual(step["env"], expected_env)
|
|
38
46
|
self.assertEqual(step["with"]["cache"], "false")
|
|
39
47
|
self.assertEqual(step["with"]["pub-cache"], "false")
|
|
40
48
|
self.assertNotIn("continue-on-error", step)
|
|
@@ -66,7 +74,8 @@ class BootstrapWiringTests(unittest.TestCase):
|
|
|
66
74
|
self.assertEqual(restore["uses"],
|
|
67
75
|
"actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25")
|
|
68
76
|
install = next(step for step in steps if step.get("id") == "install")
|
|
69
|
-
save = steps[
|
|
77
|
+
save = next(step for step in steps if step["name"] == "Save installed Flutter SDK")
|
|
78
|
+
self.assertGreater(steps.index(save), steps.index(install))
|
|
70
79
|
self.assertEqual(save["uses"], "actions/cache/save@caa296126883cff596d87d8935842f9db880ef25")
|
|
71
80
|
self.assertEqual(save["with"], restore["with"])
|
|
72
81
|
self.assertTrue(save["continue-on-error"])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.97
|