Compare commits

...
Author SHA1 Message Date
Jeppe B de780d857c Add POS card payment VAT visual previews 2026-07-27 18:05:53 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fd31609cb3 Bump ruby/setup-ruby from 1.319.0 to 1.321.0 (#226)
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.319.0
to 1.321.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/setup-ruby/releases">ruby/setup-ruby's
releases</a>.</em></p>
<blockquote>
<h2>v1.321.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Add jruby-10.1.1.0 by <a
href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a>
in <a
href="https://redirect.github.com/ruby/setup-ruby/pull/932">ruby/setup-ruby#932</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/setup-ruby/compare/v1.320.0...v1.321.0">https://github.com/ruby/setup-ruby/compare/v1.320.0...v1.321.0</a></p>
<h2>v1.320.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update CRuby releases on Windows by <a
href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a>
in <a
href="https://redirect.github.com/ruby/setup-ruby/pull/931">ruby/setup-ruby#931</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/setup-ruby/compare/v1.319.0...v1.320.0">https://github.com/ruby/setup-ruby/compare/v1.319.0...v1.320.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ruby/setup-ruby/commit/95ef2b042f9d7a56d8268cba8559e2842e2ad01b"><code>95ef2b0</code></a>
Add jruby-10.1.1.0</li>
<li><a
href="https://github.com/ruby/setup-ruby/commit/a30dfa457ad68707b8b910ac3a244714b61c0626"><code>a30dfa4</code></a>
Update CRuby releases on Windows</li>
<li>See full diff in <a
href="https://github.com/ruby/setup-ruby/compare/003a5c4d8d6321bd302e38f6f0ec593f77f06600...95ef2b042f9d7a56d8268cba8559e2842e2ad01b">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.319.0&new-version=1.321.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 11:38:49 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 009519ee62 Bump actions/download-artifact from 4.3.0 to 8.0.1 (#227)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 4.3.0 to 8.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Support for CJK characters in the artifact name by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/471">actions/download-artifact#471</a></li>
<li>Add a regression test for artifact name + content-type mismatches by
<a href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
in <a
href="https://redirect.github.com/actions/download-artifact/pull/472">actions/download-artifact#472</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v8...v8.0.1">https://github.com/actions/download-artifact/compare/v8...v8.0.1</a></p>
<h2>v8.0.0</h2>
<h2>v8 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/download-artifact@v8 has been migrated to an ESM module. This
should be transparent to the caller but forks might need to make
significant changes.</p>
</blockquote>
<blockquote>
<p>[!IMPORTANT]
Hash mismatches will now error by default. Users can override this
behavior with a setting change (see below).</p>
</blockquote>
<h3>Direct downloads</h3>
<p>To support direct uploads in <code>actions/upload-artifact</code>,
the action will no longer attempt to unzip all downloaded files.
Instead, the action checks the <code>Content-Type</code> header ahead of
unzipping and skips non-zipped files. Callers wishing to download a
zipped file as-is can also set the new <code>skip-decompress</code>
parameter to <code>true</code>.</p>
<h3>Enforced checks (breaking)</h3>
<p>A previous release introduced digest checks on the download. If a
download hash didn't match the expected hash from the server, the action
would log a warning. Callers can now configure the behavior on mismatch
with the <code>digest-mismatch</code> parameter. To be secure by
default, we are now defaulting the behavior to <code>error</code> which
will fail the workflow run.</p>
<h3>ESM</h3>
<p>To support new versions of the @actions/* packages, we've upgraded
the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Don't attempt to un-zip non-zipped downloads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/460">actions/download-artifact#460</a></li>
<li>Add a setting to specify what to do on hash mismatch and default it
to <code>error</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/461">actions/download-artifact#461</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v7...v8.0.0">https://github.com/actions/download-artifact/compare/v7...v8.0.0</a></p>
<h2>v7.0.0</h2>
<h2>v7 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/download-artifact@v7 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v6 had preliminary
support for Node 24, however this action was by default still running on
Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</a></li>
<li>Download Artifact Node24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li>
<li>fix: update <code>@​actions/artifact</code> to fix Node.js 24
punycode deprecation by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/451">actions/download-artifact#451</a></li>
<li>prepare release v7.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/452">actions/download-artifact#452</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/download-artifact/commit/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"><code>3e5f45b</code></a>
Add regression tests for CJK characters (<a
href="https://redirect.github.com/actions/download-artifact/issues/471">#471</a>)</li>
<li><a
href="https://github.com/actions/download-artifact/commit/e6d03f67377d4412c7aa56a8e2e4988e6ec479dd"><code>e6d03f6</code></a>
Add a regression test for artifact name + content-type mismatches (<a
href="https://redirect.github.com/actions/download-artifact/issues/472">#472</a>)</li>
<li><a
href="https://github.com/actions/download-artifact/commit/70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3"><code>70fc10c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/461">#461</a>
from actions/danwkennedy/digest-mismatch-behavior</li>
<li><a
href="https://github.com/actions/download-artifact/commit/f258da9a506b755b84a09a531814700b86ccfc62"><code>f258da9</code></a>
Add change docs</li>
<li><a
href="https://github.com/actions/download-artifact/commit/ccc058e5fbb0bb2352213eaec3491e117cbc4a5c"><code>ccc058e</code></a>
Fix linting issues</li>
<li><a
href="https://github.com/actions/download-artifact/commit/bd7976ba57ecea96e6f3df575eb922d11a12a9fd"><code>bd7976b</code></a>
Add a setting to specify what to do on hash mismatch and default it to
<code>error</code></li>
<li><a
href="https://github.com/actions/download-artifact/commit/ac21fcf45e0aaee541c0f7030558bdad38d77d6c"><code>ac21fcf</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/460">#460</a>
from actions/danwkennedy/download-no-unzip</li>
<li><a
href="https://github.com/actions/download-artifact/commit/15999bff51058bc7c19b50ebbba518eaef7c26c0"><code>15999bf</code></a>
Add note about package bumps</li>
<li><a
href="https://github.com/actions/download-artifact/commit/974686ed5098c7f9c9289ec946b9058e496a2561"><code>974686e</code></a>
Bump the version to <code>v8</code> and add release notes</li>
<li><a
href="https://github.com/actions/download-artifact/commit/fbe48b1d2756394be4cd4358ed3bc1343b330e75"><code>fbe48b1</code></a>
Update test names to make it clearer what they do</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/download-artifact/compare/d3f86a106a0bac45b974a628896c90dbdf5c8093...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=4.3.0&new-version=8.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 11:15:19 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b6f9b5a3a4 Bump actions/setup-node from 5 to 7 (#228)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5
to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-node/releases">actions/setup-node's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Add cache-primary-key and cache-matched-key as outputs by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1577">actions/setup-node#1577</a></li>
<li>Migrate to ESM and upgrade dependencies by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1574">actions/setup-node#1574</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Remove dummy NODE_AUTH_TOKEN export by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1558">actions/setup-node#1558</a></li>
<li>Only use <code>mirrorToken</code> in <code>getManifest</code> if
it's provided by <a
href="https://github.com/deiga"><code>@​deiga</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
</ul>
<h3>Documentation updates:</h3>
<ul>
<li>Add documentation for publishing to npm with Trusted Publisher
(OIDC) by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li>docs: Update restore-only cache documentation by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1550">actions/setup-node#1550</a></li>
<li>docs: Update caching recommendations to mitigate cache poisoning
risks by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1567">actions/setup-node#1567</a></li>
</ul>
<h3>Dependency update:</h3>
<ul>
<li>Upgrade <code>@​actions/cache</code> to 5.1.0, log cache write
denied by <a
href="https://github.com/jasongin"><code>@​jasongin</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li><a href="https://github.com/deiga"><code>@​deiga</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
<li><a href="https://github.com/jasongin"><code>@​jasongin</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v7.0.0">https://github.com/actions/setup-node/compare/v6...v7.0.0</a></p>
<h2>v6.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update <code>@​actions/cache</code> to 5.1.0 and add security
overrides for undici and fast-xml-parser by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1579">actions/setup-node#1579</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0">https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0</a></p>
<h2>v6.4.0</h2>
<h2>What's Changed</h2>
<h3>Dependency updates:</h3>
<ul>
<li>Upgrade <a
href="https://github.com/actions"><code>@​actions</code></a>
dependencies by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
<li>Update Node.js versions in versions.yml and bump package to v6.4.0
by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1533">actions/setup-node#1533</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v6.4.0">https://github.com/actions/setup-node/compare/v6...v6.4.0</a></p>
<h2>v6.3.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Support parsing <code>devEngines</code> field by <a
href="https://github.com/susnux"><code>@​susnux</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1283">actions/setup-node#1283</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-node/commit/820762786026740c76f36085b0efc47a31fe5020"><code>8207627</code></a>
Migrate to ESM and upgrade dependencies (<a
href="https://redirect.github.com/actions/setup-node/issues/1574">#1574</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/04be95cf3511ea51ebf9f224ddfb99cc7ab87cd4"><code>04be95c</code></a>
Add cache-primary-key and cache-matched-key as outputs (<a
href="https://redirect.github.com/actions/setup-node/issues/1577">#1577</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/7c2c68d20d402ed6a201ada70a81341941093140"><code>7c2c68d</code></a>
docs: Update caching recommendations to mitigate cache poisoning risks
(<a
href="https://redirect.github.com/actions/setup-node/issues/1567">#1567</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/6a61c0375d66246de94630495909f12cf8dac84d"><code>6a61c03</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/setup-node/issues/1569">#1569</a>
from jasongin/update-actions-cache-5.1.0</li>
<li><a
href="https://github.com/actions/setup-node/commit/30eb73b41ded577900c1ebf968ef95cdf8f7434f"><code>30eb73b</code></a>
Resolve high-severity audit issues</li>
<li><a
href="https://github.com/actions/setup-node/commit/4e1a87a501d0302f99e30e2748568adcb388d09f"><code>4e1a87a</code></a>
Update dist</li>
<li><a
href="https://github.com/actions/setup-node/commit/360237f0c01778d0c17291f75c56d6feae4f7574"><code>360237f</code></a>
Strict equality</li>
<li><a
href="https://github.com/actions/setup-node/commit/4f8aac5beb2f0854bc79651567a18c67eb0b9de3"><code>4f8aac5</code></a>
Bump <code>@​actions/cache</code> to 5.1.0, log cache write denied</li>
<li><a
href="https://github.com/actions/setup-node/commit/f4a67bbeca970f103397d3d2b9462cf787cd2980"><code>f4a67bb</code></a>
Only use <code>mirrorToken</code> in <code>getManifest</code> if it's
provided (<a
href="https://redirect.github.com/actions/setup-node/issues/1548">#1548</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/0355742c943ddb13ca8a6b700f824231caa91e75"><code>0355742</code></a>
Remove dummy NODE_AUTH_TOKEN export (<a
href="https://redirect.github.com/actions/setup-node/issues/1558">#1558</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-node/compare/v5...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=5&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 12:55:41 +02:00
Jeppe B 7387a2b56e fix(i18n): unblock Truckwash iOS release catalog checks (#225)
Fix the location-permission source catalog nesting and regenerate all active v2 locale files so the App Store release matrix resolves the new compatibility aliases correctly.
2026-07-23 20:22:47 +02:00
Jeppe B 9ff103d4d0 fix(ios): respect status area and persist location consent (#224)
Keep the iOS status bar outside the Capacitor web view and replace startup geolocation watching with silent permission checks plus an explicit location action.

Verified by full unit, App Store readiness, Qodana, production build, Capacitor sync, and Playwright mobile suites.
2026-07-23 19:29:54 +02:00
Jeppe B 32418b42a0 fix(ios): add required location purpose string (#223)
## What changed
- add `NSLocationAlwaysAndWhenInUseUsageDescription` to the iOS app
- localize the purpose string in Danish and English
- enforce the key in mobile permission validation and App Store
readiness tests

## Why
App Store Connect accepted builds 1 and 2 but emitted `ITMS-90683`,
stating that the final app references APIs requiring this purpose
string. Shipping a corrected binary avoids submitting a candidate with a
known Apple delivery warning.

## Validation
- `node scripts/mobile/check-permissions.mjs`
- `node scripts/mobile/validate-app-store.mjs --strict`
- `vitest run tests/unit/app-store-product-readiness.spec.js` (5 tests)
- Prettier check for changed JS files
- `git diff --check`
2026-07-23 16:36:35 +00:00
Jeppe B bf2208e77b fix(ios): use valid Danish TestFlight locale (#222)
Use Apple's supported `da` beta locale and cover the localization/distribution flow with a regression test.

The first signed upload already processed version 1.0.0 build 1 successfully; this fixes the post-processing localization failure before the controlled retry.
2026-07-23 16:50:37 +02:00
Jeppe B 5702d45bc6 fix(ios): harden App Store release automation (#221)
## Summary

- replace the unsupported top-level App Store version collection with
Apple's app-scoped version endpoint
- add tested release-policy and availability readback for exact
version/build, `AFTER_APPROVAL`, Denmark only, no preorder, and no
automatic future territories
- strengthen the stable `App Store Readiness` check and align
Fastlane/candidate handoff with the approved 1.0.0 release policy

## Task contract

`truckwash-ios-release-20260723` — R4 (`ci-policy`, `release-policy`,
`credential-handling`, `branch-protection-or-rules`,
`mobile-store-submission`). The user explicitly approved implementation,
protected-master delivery, and the App Store release path.

## Changed files

- App Store Connect client and dependency-free Node tests
- App Store readiness and candidate workflows
- Fastlane candidate release configuration
- Apple App Store release runbook

## Verification

- `node --test tests/node/app-store-connect.test.mjs` — 10 passed
- `node scripts/mobile/validate-app-store.mjs --strict` — passed
- `node scripts/mobile/check-permissions.mjs` — passed
- App Store product-readiness Vitest — 5 passed
- ESLint on changed Node files — passed
- workflow YAML parsing — passed
- `git diff --check` — passed
- local Fastlane validation unavailable because Ruby/Bundler is not
installed on this host; `App Store Readiness` runs it on GitHub

## Release target

- iOS App Store
- bundle `io.truckwash.app`
- version `1.0.0`
- App Store Connect app `6792777794`
- Denmark only
- automatic release after approval
- no preorder or phased release for 1.0.0

The repository App Store automation switch remains disabled until this
change is merged and credential health is reverified.
2026-07-23 12:59:17 +00:00
Jeppe BandJeppe Bundgaard 42352b4c2d fix(release): isolate post-deploy bookkeeping (#220)
Scopes rollback to actual public or credentialed live-gate failure.
Release Manager recording becomes non-blocking post-deploy observability
and records only the API check because the mandatory Playwright gate
already verifies the exact full static inventory. This avoids the
duplicate synchronous static verification that exceeded the API proxy
timeout and rolled back a verified release.\n\nVerification:\n- git diff
--check\n- release.yml parsed with the checked-in YAML dependency\n-
credentialed role gate without secrets: 2 skipped, exit 0

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-23 06:18:01 +02:00
Jeppe BandJeppe Bundgaard 729416e5ef fix(release): skip role smoke without credentials (#219)
Makes the credentialed live Playwright gate conditional on its secrets
being configured. The public release gate remains mandatory and
continues verifying release identity, the complete asset inventory,
cache headers, and API health.\n\nVerification:\n- git diff --check\n-
PLAYWRIGHT_BASE_URL=https://truckwash.io Playwright @role-live gate: 2
skipped, exit 0

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-23 04:53:51 +02:00
Jeppe BandJeppe Bundgaard 41b3be926a test(release): allow full live asset inventory (#218)
## Summary
- give the complete public asset inventory gate a five-minute test
budget
- keep the API and rendered guest-flow smoke tests on the existing
default timeout

## Rationale
The production manifest contains the full release inventory.
Sequentially downloading and hashing it takes longer than the global
60-second Playwright timeout from GitHub-hosted runners, causing false
failures even though release identity, API, and guest-flow checks pass.

## Verification
- ESLint on the changed spec
- Playwright live configuration test discovery

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-23 02:59:40 +02:00
Jeppe BandJeppe Bundgaard fc67e7cf0b Fix idempotent FTPS directory setup (#217)
The production FTPS deploy now authenticates, but lftp exits when cPanel
returns 550 File exists for pre-created deployment directories. Use lftp
mkdir -p -f for the archive and activation directories so retries remain
idempotent while subsequent upload operations still surface real access
failures.\n\nVerification:\n- vitest tests/unit/cpanel-deploy.spec.js
(26 passed)\n- eslint scripts/release/cpanel-deploy-lib.mjs
tests/unit/cpanel-deploy.spec.js\n- git diff --check

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-23 00:00:48 +02:00
Jeppe B 9b3c06fc6f fix(release): stream lftp commands directly (#216)
## Summary
- feed the generated lftp command script directly over stdin
- avoid reopening `/dev/stdin`, which fails on the production hosted
runner
- keep FTP credentials out of process arguments

## Verification
- `vitest run tests/unit/cpanel-deploy.spec.js` (26/26)
- ESLint on changed files
- `git diff --check`

Supersedes the failed production release run 29948809036.
2026-07-22 21:24:04 +02:00
Jeppe B f0e3c4812b Require fresh passkeys for passwordless account deletion (#215)
## Summary
- Complete the frontend contract for hardened backend account deletion
(#319).
- For passwordless accounts, request a fresh deletion-specific WebAuthn
challenge and submit its serialized assertion.
- Reuse the existing passkey assertion serializer instead of duplicating
WebAuthn conversion logic.
- Accept the durable `manual_review` backend state while presenting the
existing safe failure copy.

## Verification
- Account deletion unit tests: 9/9.
- Focused ESLint passed for all four changed files.
- Node syntax checks and `git diff --check` passed.

Backend rollout flags remain default-off; this UI is inert until #319
schema checks and explicit API enablement are completed.
2026-07-22 20:05:37 +02:00
Jeppe B 4c7d8c6f2e Consolidate verified CI and Fastlane upgrades (#214)
## Summary
- Consolidate the intended changes from #194–#199 onto current master.
- Upgrade checkout 7.0.1, upload-artifact 7.0.1, setup-android 4.0.1,
setup-java 5.6.0, github-script 9.0.0, and Fastlane 2.237.0.
- Pin every upgraded workflow action to its verified immutable commit
SHA.
- Exclude the abandoned dependency-aware test-graph ancestor entirely.

## Verification
- All five action families matched live upstream tag commits
(`github-script` uses the peeled annotated-tag commit).
- Workflow YAML parse passed.
- AI workflow generated-output check passed.
- 22 focused mobile/Playwright workflow unit tests passed.
- `git diff --check` passed.
- Ruby/Bundler is unavailable locally; Linux/macOS Fastlane resolution
remains a required CI gate.
2026-07-22 19:43:57 +02:00
Jeppe B 74dd8e3691 Preserve redacted FTPS failure diagnostics (#213)
## Summary
- Preserve lftp stdout/stderr when the process exits non-zero.
- Surface bounded, whitespace-normalized diagnostics through the deploy
error.
- Redact FTPS host, username, password, path, URL userinfo, and encoded
secret forms.

## Verification
- `vitest run tests/unit/cpanel-deploy.spec.js` (25/25)
- `node --check scripts/release/cpanel-deploy-lib.mjs`
- `git diff --check`

This is the prerequisite diagnostic repair for failed Frontend Release
run 29854900889. Production was not switched during that failure.
2026-07-22 19:23:04 +02:00
Jeppe B 7782d93fe9 Gate mobile releases behind explicit phased rollout (#212)
Require explicit mobile-v* tags or manual dispatch, gate exact tested master SHAs, and default Google Play production submissions to an initial 1% in-progress rollout.
2026-07-22 18:51:40 +02:00
Jeppe BandJeppe Bundgaard fd26b0ee81 Package frontend releases without host zip tools (#211)
## Summary

- remove the release packager's undeclared dependency on host `zip` and
`unzip` executables
- create and round-trip validate ZIP artifacts in Node with explicit
paths, permissions, timestamps, CRC checks, and resource limits
- preserve the existing archive filename, checksum, inventory, and
top-level `dist/` contract

## Root cause

After the prebuilt-dist integrity repair passed on master, Frontend
Release reached packaging and failed with `spawn zip ENOENT` on the
self-hosted runner. The workflow never installed or checked either
archive executable.

## Verification

- focused release/deployment tests: 57/57 passed
- packager tests: 9/9 passed, including empty `PATH`, cross-timezone
determinism, exact archive entries, permissions, and oversized
sparse-file rejection
- real production build: 735 files packaged successfully with an empty
`PATH`
- repeated real packaging produced byte-identical archives
- Info-ZIP test/list/checksum validation passed
- extraction under `umask 077`: every directory is `0755`; all 735 files
extracted
- extracted inventory exactly matches the source inventory
- ESLint and Prettier passed

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-21 16:43:19 +00:00
Jeppe B a01902356d Harden hosted releases and mobile store gates (#205) 2026-07-21 18:17:47 +02:00
Jeppe BandJeppe Bundgaard 0692cb3aea Preserve prebuilt dist during release gate (#210)
## Summary

- serve the release workflow's already-built `dist` from the production
Playwright gate
- preserve the existing auto-build behavior for standalone local
production tests
- keep the pre/post `dist` inventory guard strict and unchanged

## Root cause

The release workflow built and fingerprinted `dist`, but Playwright then
launched `preview:prod`, which ran a second Vite build. Timestamped
build metadata changed hashed chunks and caused the integrity comparison
to fail after all 26 production browser tests had passed.

## Verification

- production Playwright gate: 26/26 passed
- pre/post inventory: 735 files, zero changes
- release package validation: 735 files passed
- ESLint passed
- Prettier passed
- release package unit tests: 7/7 passed
- workflow YAML parsed successfully
- no test files changed

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-21 15:23:41 +00:00
Jeppe B de3f067372 Run frontend tests concurrently on GitHub-hosted runners (#209)
Parallelize quality and Playwright jobs while preserving the existing test suite and required CI contracts.
2026-07-21 16:03:57 +02:00
Jeppe B a0c11e4bb7 Fix POS release-blocking customer rules and completion (#208)
Resolve direct-order product reconciliation context, preserve valid selections when restricted products are activated, remove the redundant certificate request, and add focused regression coverage.
2026-07-21 15:42:50 +02:00
Jeppe B 71e7fac555 Fix literal translation coverage (#206)
Replace finite dynamic view translations with explicit literal-key mappings so the i18n integrity gate can verify locale coverage without changing rendered copy or tests.
2026-07-20 23:54:04 +02:00
Jeppe BandJeppe Bundgaard 6dc27a355f Fix privacy fallbacks and mobile safe-area test (#204)
Restore a green `master` baseline before processing the Dependabot
queue.

This PR:
- replaces the accidental Danish privacy-policy fallback in German,
Norwegian, and Swedish catalogs with an explicit English fallback;
- keeps the generated locale catalogs synchronized and preserves the
glossary de-duplication from #203;
- updates the mobile safe-area E2E setup from `/login` to public
`/guest/home`, which renders the header geometry the test asserts.

Verification on the final rebased tree:
- i18n compile/check and all catalog audits
- focused Vitest: 5/5
- Chromium mobile Playwright: 13/13
- Prettier, ESLint, encoding, and `git diff --check`

WebKit remains covered by GitHub CI because the local host lacks its
required runtime libraries.

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-20 19:28:32 +00:00
Jeppe B eeec725f08 Restore complete privacy-policy locale coverage (#203)
## Summary
- add the privacy-policy compatibility source to German, Norwegian, and
Swedish so every active locale has the same runtime key set
- reuse existing English and German glossary tokens so the raw v2
catalog keeps its word-deduplication invariant
- regenerate the affected runtime locale catalogs

## Verification
- `playwright test tests/e2e/i18n-v2-integrity.spec.ts
--project=chromium-mobile` (12 passed)
- `vitest run tests/unit/app-store-product-readiness.spec.js` (5 passed)
- i18n source compile check and all three catalog audits
- Prettier, encoding, and `git diff --check`
2026-07-20 20:51:32 +02:00
Jeppe B 97df3193e3 Complete App Store artwork and signed release automation (#200)
Align generated artwork with the published Truck Wash storefront, add strict iPhone and iPad App Store screenshots, and complete signed iOS release automation.
2026-07-20 18:19:47 +00:00
Jeppe B ed2d67934c Unblock app icon validation in CI (#202)
Format the app icon background test and avoid per-pixel assertion overhead so the full unit suite remains bounded.
2026-07-20 19:20:26 +02:00
Jeppe B 1a959c2ce8 Activate cPanel releases with a pinned account runner (#193)
Replace unsafe legacy Fileman symlink activation with an authenticated, root-owned account runner and crash-safe pointer reconciliation.
2026-07-20 18:20:03 +02:00
Jeppe B 88eda43560 Automate signed iOS App Store releases (#192)
## What changed

- adds production iOS identity, localized storefront metadata, native
privacy declarations, App Store-safe artwork, and account-deletion UX
- mirrors the live Danish Google Play title, short description, and long
description in the App Store metadata source
- generates Android launcher/store icons from the opaque iOS marketing
master so both platforms use the same white background
- adds guarded GitHub Actions workflows for storefront readiness,
credential health, signed TestFlight uploads, and App Store candidate
preparation
- adds pinned Fastlane configuration with a committed dependency lock,
release manifest tooling, and an operational App Store runbook
- preserves the upstream iOS safe-area implementation while retaining
opaque App Store icon assets

## Why

The repository previously supported development-signed device bundles
but had no production App Store identity, reproducible storefront source
of truth, or protected signed-release pipeline. Apple also requires
in-app account deletion for apps that support account creation. The
Android icon master was transparent, which rendered as black on dark
store/device surfaces.

## Impact

Automation remains fail-closed behind
`APP_STORE_AUTOMATION_ENABLED=false`. No build can upload to TestFlight
or change App Store metadata until the switch is deliberately enabled
after merge and the remaining release gates are satisfied.

## Validation

- focused App Store, iOS icon, and cross-platform icon-background tests
pass
- every generated Android store/launcher icon is opaque with pure-white
corners; iOS marketing artwork is checked the same way
- Android icon drift check passes for all 19 generated files
- production Vite build and the broader focused release checks completed
successfully
- storefront metadata is valid; only the two expected screenshot-set
warnings remain
- App Store Readiness is green at head `4445fecc`
- Apple Distribution certificate and App Store profile were
independently verified for `HP3FJ4GVL7.io.truckwash.app`
- live App Store Connect API authentication succeeded for app
`6792777794`
- App Store record, free Denmark-only availability, and automatic
`Internal QA` TestFlight group are configured
- EU trader status, Content Rights, 4+ age rating, and the published App
Privacy label are completed in App Store Connect
- iPhone and iPad accessibility declarations are configured honestly as
pre-release drafts

## Remaining external gates

- reviewed iPhone and iPad screenshot sets are still required
- an App Review login must be supplied without creating or exposing
customer credentials
- the first signed TestFlight candidate must run after merge and
deliberate automation enablement
2026-07-20 17:59:43 +02:00
Jeppe B 133e53cfa6 Fix iPhone mobile header safe-area spacing
Add iOS safe-area viewport support and inset-aware mobile header/spacer
sizing so controls clear the notch or Dynamic Island. Desktop and tablet
headers remain unchanged. Includes focused unit and mobile browser
regression coverage.
2026-07-20 16:16:56 +02:00
Jeppe B c69f4f7fc7 Run cPanel recovery and deploy from stable runner (#189)
Use the stable self-hosted runner for cPanel API calls and install lftp job-locally without sudo.
2026-07-20 15:54:45 +02:00
Jeppe B 10f993e686 Support direct cPanel UAPI responses (#188) 2026-07-20 15:47:52 +02:00
Jeppe B a02ddfbfbc Normalize cPanel success results (#187) 2026-07-20 15:41:15 +02:00
Jeppe B 7a85c93631 Use Android artwork for iOS app icon
Replace the iOS AppIcon variants with the exact Android launcher artwork
from `public/favicons/web-app-manifest-512x512.png`, so the physical
iPhone home-screen icon matches Android. Verified all declared icon
dimensions and focused iOS icon test.
2026-07-20 15:41:02 +02:00
Jeppe B a3d11cfca3 Use relative account home in cPanel audit (#186) 2026-07-20 15:36:14 +02:00
Jeppe B 74279fc443 Address cPanel deployment review findings (#183) 2026-07-20 15:30:54 +02:00
Jeppe B 7e3a72961f Add complete iOS app icon asset set (#184)
Add standard iPhone, iPad, and marketing AppIcon variants with asset regression coverage.
2026-07-20 15:23:39 +02:00
Jeppe B ca2179352c Fix customer booking product restrictions and responsive layout (#182)
## What changed

- load customer product rules for the booking customer and keep
selection fail-closed while they resolve
- prevent restricted products, add-ons, pickup, and wash certificates
from remaining selected or being submitted
- improve product-grid containment, selected add-on styling, unavailable
messaging, and tablet/mobile rendering
- preserve booking details when the API rejects stale restricted items
- add unit and Playwright coverage for loading, retry, exact
restrictions, automatic products, layout, and stale backend recovery

## Why

The customer booking flow could use stale or unrelated POS customer-rule
state, expose restricted items, and lose useful form state after a
server rejection. The desktop product area also overflowed and switched
to an inconsistent compact layout after selection.

## Validation

- desktop booking Playwright suite: 12 passed
- mobile booking Playwright suite: 4 passed, 8 expected desktop-only
skips
- relevant unit tests: 35 passed
- ESLint and i18n checks
- production Vite build

## Related backend PR

The coordinated API PR enforces the same rule at the write boundary.
2026-07-20 14:41:47 +02:00
Jeppe B 8ddac065c6 Guard iOS debug signing with live CORS (#181)
Verify the stable API and exact Capacitor iOS CORS contract before signing device-debug IPAs, with regression coverage and troubleshooting guidance.
2026-07-20 14:37:17 +02:00
Jeppe B a930bd35a5 Add atomic cPanel frontend deployment (#178)
Build and archive the tested frontend, upload it through dedicated FTPS credentials, and atomically activate it through cPanel after CI succeeds.
2026-07-20 13:53:49 +02:00
Jeppe B 6b8f3ff806 Avoid Xcode version broken pipe in iOS debug workflow (#180)
Capture the full Xcode version output before selecting its first line so Xcode 26 cannot abort on a closed pipe.
2026-07-20 12:37:42 +02:00
Jeppe B 3323f392e3 Add signed iOS device debug workflow (#179)
Add a protected development-signing workflow, isolated debug app identity, Linux USB device tooling, documentation, and focused validation coverage.
2026-07-20 12:07:10 +02:00
Jeppe B f8f2b80641 Align restricted POS add-on controls and registration label (#177)
Align restricted desktop and mobile POS controls, scope the Danish primary registration label to POS, and preserve generic registration wording across shared surfaces and other locales.
2026-07-20 10:57:17 +02:00
Jeppe B 1729443cc3 Resolve frontend Qodana critical and high findings (#176)
Resolve recommended-profile Critical and High findings, update vulnerable dependencies, restore invoice queue E2E authentication setup, and clear the remaining frontend Qodana findings.
2026-07-17 06:22:51 +02:00
Jeppe B 6b5ac8a8b3 Document frontend branch protection activation (#175)
## Purpose

After-activation canary for repository ruleset `19051697`.

## Evidence before activation

- preparation PR #172 established stable `Required CI`
- strict-current-base remediation PR #174 passed `Required CI` and
Qodana
- merged master run `29501928124` completed with 43 successful jobs and
one intentional skip
- effective master rules were audited after activation

## Canary acceptance

- merge is blocked while `Required CI` is pending
- the exact `Required CI` check comes from GitHub Actions integration
`15368`
- only squash merge is accepted
- the branch is deleted automatically after merge
2026-07-16 20:45:00 +02:00
Jeppe B b12aca2757 Fix customer-rule i18n coverage gate (#174)
* fix: make customer rule translations statically auditable

* test: scope draft filter coverage to desktop layouts
2026-07-16 15:22:35 +02:00
Jeppe B 39cc3a780a Configure advisory Qodana analysis (#173) 2026-07-16 14:21:02 +02:00
Jeppe B 18302723e8 Prepare frontend master branch protection (#172)
Add stable Required CI, repair E2E ownership, make Qodana optional-state handling explicit, and document the desired protection policy.
2026-07-16 13:12:57 +02:00
Jeppe B 20c383d22d Format customer rule tests 2026-07-16 12:11:06 +02:00
Jeppe B 77a0c4e018 Add customer rule product restrictions 2026-07-16 11:51:42 +02:00
Jeppe B 9a5c8b193d style: format admin order filter test 2026-07-16 11:50:41 +02:00
Jeppe B 0562ba8ab0 feat: add admin order filters 2026-07-16 11:45:08 +02:00
Jeppe Bundgaard 906e860c86 test: distinguish order table requests 2026-07-16 01:29:02 +02:00
Jeppe B 516e1fb58d Expose invoicing flag badge translations 2026-07-15 22:47:11 +02:00
Jeppe B 7da1307cf6 Restore i18n locale compatibility keys 2026-07-15 21:37:21 +02:00
Jeppe B ea4893d815 Ignore warm prefetches in month selection E2E 2026-07-15 20:50:02 +02:00
Jeppe B f2e0079b59 Stabilize invoicing period E2E scenarios 2026-07-15 20:26:49 +02:00
Jeppe B 47d8baa496 Format frontend test files 2026-07-15 19:42:27 +02:00
Jeppe B 6d96d64811 Improve invoicing period tree and POS restrictions 2026-07-15 17:04:51 +02:00
Jeppe B 92fb998e5a Fix customer additional-service restrictions (#171) 2026-07-15 15:08:01 +02:00
Jeppe B bed6030e2b Stabilize invoicing period date fixtures 2026-07-15 02:45:02 +02:00
Jeppe B 328a85bad1 Review object tree dynamic i18n helper
Add the invoicing period object tree translation wrapper to the reviewed non-literal call list; it mirrors the existing flag-list helper pattern.
2026-07-15 01:30:18 +02:00
Jeppe B 394375e429 Fix user profile two-factor test id
Keep the profile two-factor wrapper test id stable and move spacing tokens to class so the mobile profile security E2E can find the status element.
2026-07-15 00:17:34 +02:00
Jeppe B ca744adfd9 Link security status filter i18n label
Reuse the canonical system status label for the security incident status filter so the v2 duplicate raw phrase gate stays green.
2026-07-14 23:35:53 +02:00
Jeppe B 5e42f55570 Fix invoicing period object-tree flag handling
Keep expanded period rows on InvoicingPeriodObjectTree while rendering order-scoped flags in a compact expanded flag list. Update the invoicing-period E2E fixtures and source contract for the object-tree path.
2026-07-14 22:39:33 +02:00
Jeppe B 021da1a074 Fix invoicing period expanded order details (#170) 2026-07-14 21:02:22 +02:00
Jeppe B 0e55f45e91 Fix invoicing queue-state unit spec 2026-07-14 16:05:04 +02:00
Jeppe B 9749837506 Format frontend test files 2026-07-14 15:55:17 +02:00
Jeppe B 4fbb0b98c3 Improve superuser invoicing and system status views 2026-07-14 15:41:05 +02:00
Jeppe B 0edf8afcaf Regenerate Danish i18n runtime 2026-07-14 13:52:23 +02:00
gold-dev e2ee54578c Feat: 202607 - truckwash.dk V15_sent slide 8 2026-07-14 12:28:56 +03:00
gold-dev b35e4484de Merge remote-tracking branch 'origin/master' 2026-07-14 12:15:00 +03:00
gold-dev c8368612eb Feat: 202607 - truckwash.dk V15_sent slide 6, 7 2026-07-14 12:14:47 +03:00
Jeppe B 26266e724b Stabilize customer booking and i18n checks 2026-07-14 10:34:29 +02:00
Jeppe B 6ccfea9eb4 Limit security nav assertion to desktop layout 2026-07-14 02:15:44 +02:00
Jeppe B d4349c20a7 Format subuser management spec 2026-07-14 00:59:12 +02:00
Jeppe Bundgaard 63f1cc2450 Fix custom subuser Dognvask updates 2026-07-13 22:30:18 +02:00
Jeppe Bundgaard acec2a694a Add loading screen for session confirmation and enhance vehicle management labels 2026-07-13 22:11:51 +02:00
Jeppe B f15d7224e0 Make failover wiring source check formatter tolerant 2026-07-13 21:50:28 +02:00
Jeppe B 236f552a22 Stabilize subuser QR auth smoke assertion 2026-07-13 20:57:30 +02:00
Jeppe B 321b6f2c64 Format auth smoke session bootstrap test 2026-07-13 20:36:03 +02:00
Jeppe B 218ead9a31 Fix subuser QR session bootstrap 2026-07-13 20:33:48 +02:00
Jeppe B 60b6e82c88 Dedupe cron worker target i18n key 2026-07-13 20:32:03 +02:00
Jeppe B bbe4ad938a Add cron worker controls and align superuser specs 2026-07-13 19:47:08 +02:00
Jeppe B 9e27380a10 Fix i18n v2 catalog integrity drift
Fill missing backup compatibility keys for Danish and Norwegian and centralize repeated shared/support labels through existing i18n links.
2026-07-13 17:59:51 +02:00
Jeppe Bundgaard baf83fd079 Restore gateway smoke helpers after route spec cleanup
Keep the gateway health-card smoke test runnable after removing the stale replication-management helper block.
2026-07-13 15:02:15 +00:00
Jeppe Bundgaard dfe3163692 Align frontend smoke tests with current route permissions
Update self-serve mocked customer permissions for the guarded wash routes and replace removed replication-management E2E coverage with dashboard redirect assertions.
2026-07-13 16:41:08 +02:00
Jeppe Bundgaard 4452bd4088 Update system status route contract test 2026-07-13 15:57:42 +02:00
Jeppe Bundgaard 6f93b840a9 Relax superuser module wiring assertions 2026-07-13 15:45:59 +02:00
Jeppe Bundgaard 443f50c144 Enhance subuser management contact and action flows 2026-07-13 15:11:49 +02:00
Jeppe Bundgaard 5a7f902d01 Enhance subuser action menu with new sections and labels for improved user experience 2026-07-13 10:42:13 +02:00
Jeppe Bundgaard 02cc82cee1 Enhance pagination filter dropdown styling and update subuser management tests 2026-07-13 10:27:05 +02:00
Jeppe Bundgaard fa9b05958d Enhance subuser management functionality and improve UI responsiveness 2026-07-13 10:25:20 +02:00
Jeppe Bundgaard 6fb2c9e7df Update self-serve permissions and enhance UI components for customer interactions 2026-07-13 10:21:18 +02:00
Jeppe Bundgaard 54e59b4b3e Add system status displays for Minio and Redis, and enhance backup configuration 2026-07-13 10:07:56 +02:00
Jeppe B 8a0ea6cae5 Fix cron status translation literals 2026-07-13 08:48:45 +02:00
Jeppe B 1bbd816e83 Fix session bootstrap heading assertion
Target the system status heading in the delayed session bootstrap test so Playwright strict mode does not match both the navigation link and page heading.
2026-07-09 16:17:33 +02:00
Jeppe B 4e56191b7e Fix cron due i18n link target
Add locale glossary entries for the cron due label and link the source alias through terms.glossary.due so generated runtime links resolve across active locales.
2026-07-09 14:51:17 +02:00
Jeppe B 4404fb49d3 Fix cron i18n duplicate aliases
Link repeated cron labels through existing cron/shared i18n aliases and regenerate global-v2.json so i18n integrity checks pass.
2026-07-09 13:00:25 +02:00
Jeppe B fd4db442e4 Add Playwright component test quarantine flow 2026-07-09 11:29:44 +02:00
Jeppe B 416f392108 Classify superuser cron Playwright spec 2026-07-09 11:26:59 +02:00
Jeppe B dbe9f6b0b2 Redirect QR signups to kundeoprettelse
Redirect legacy QR signup routes for new customers and drivers to the shared /kundeoprettelse customer creation page.

Validated with focused eslint and QR Playwright coverage for mobile and desktop.
2026-07-09 11:19:30 +02:00
Jeppe B 18f06f7a63 Format POS visual e2e spec 2026-07-09 11:19:23 +02:00
Jeppe B fb75ddc3e9 Format superuser cron e2e spec 2026-07-09 11:15:29 +02:00
Jeppe B 1326a40033 Add superuser cron operations UI 2026-07-09 11:12:02 +02:00
Jeppe Bundgaard 85be8d2ce7 Fix Android launcher icon assets
Regenerate Android launcher icons from the blue striped source asset.
Add a repeatable generator and README documentation for the icon workflow.
2026-07-09 10:52:56 +02:00
Jeppe Bundgaard b5fb8c8cac Fix frontend master CI runner failures 2026-07-09 10:50:11 +02:00
Jeppe Bundgaard 71b7ec4987 Fix invoicing period department 75 table overflow 2026-07-08 20:38:39 +02:00
Jeppe Bundgaard ad110d46d9 Fix POS order table primary registration display 2026-07-08 19:23:20 +02:00
Jeppe B dabe0c13ef Format mobile POS addon quantity update 2026-07-08 18:52:23 +02:00
Jeppe B a5dbfb5cba Fix mobile POS additional item product selection 2026-07-08 18:48:44 +02:00
Jeppe B 75c4700ac6 Fix mobile POS addon quantity updates 2026-07-08 18:47:08 +02:00
Jeppe B 19ce60fd0a Fix orders table formatting and unit expectation 2026-07-08 17:02:59 +02:00
Jeppe Bundgaard d26933b722 Enhance orders table with compact text cells and add test for font size consistency 2026-07-08 16:57:13 +02:00
Jeppe B 30dfe2c0c0 Stabilize frontend E2E selectors 2026-07-08 16:53:52 +02:00
Jeppe Bundgaard fa55bcd5f3 Add date cell to orders table and update styling for better visibility 2026-07-08 16:01:27 +02:00
Jeppe B fbc1dc9406 Update view i18n dynamic key allowlist 2026-07-08 15:54:41 +02:00
Jeppe B 4a4b82c8b6 Format invoice bulk actions changes 2026-07-08 15:52:06 +02:00
Jeppe Bundgaard 042e477252 Add invoice collection bulk actions and table header preferences functionality 2026-07-08 15:43:14 +02:00
Jeppe B d1d7b72441 Format duplicate-date E2E assertion 2026-07-08 14:56:29 +02:00
Jeppe B 48d8d7f527 Stabilize mobile superuser E2E assertions 2026-07-08 14:54:17 +02:00
Jeppe B 73a71e5262 Sync i18n v2 generated runtime files 2026-07-08 14:01:10 +02:00
Jeppe B cab779e403 Align Danish vehicle action unit expectation 2026-07-08 13:54:09 +02:00
Jeppe Bundgaard e51e874264 Merge remote-tracking branch 'origin/master' 2026-07-08 13:52:15 +02:00
Jeppe Bundgaard e399c9f974 Add bulk action preview and apply functionality for invoice collections 2026-07-08 13:51:54 +02:00
Jeppe B acd46e5f2a Fix i18n locale integrity coverage 2026-07-08 13:43:37 +02:00
Jeppe B 59af8453f3 Fix i18n word dedupe casing variants 2026-07-08 13:04:41 +02:00
Jeppe Bundgaard d03f4d9aae Add subusers management features and enhance department navigation 2026-07-08 12:27:45 +02:00
Jeppe B 4cc2c3310c Fix frontend test gate expectations 2026-07-07 22:16:25 +02:00
Jeppe Bundgaard 1123b34e10 Enhance UI components and improve date selection functionality 2026-07-07 20:52:14 +02:00
Jeppe Bundgaard 8c12fce187 Add customer pricing labels and update date selection components 2026-07-07 18:16:45 +02:00
Jeppe Bundgaard a82176c855 Merge limited backoffice role template actions 2026-07-07 03:27:39 +02:00
Jeppe Bundgaard 7180bbd6d4 Add limited backoffice role template actions 2026-07-07 03:23:32 +02:00
Copilotandcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> b9b4539764 ci: run automated tests on main and dev pushes (#165)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-07 01:56:19 +02:00
Jeppe BandJeppe Bundgaard 96ccc01d08 Add limited backoffice employee migration (#164)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-07 00:14:05 +02:00
Jeppe Bundgaard 2108dd6c94 Send Selvvask import period dates
Include selected invoice-period dates when importing XLVask usage from the Selvvask period view.
2026-07-06 23:49:36 +02:00
Jeppe BandJeppe Bundgaard ca2003f2d6 Allow hosted runners for manual frontend tests (#161)
* Allow hosted runners for manual frontend tests

* Fix hosted full E2E i18n failures

* Stabilize release assignment E2E selection

* Gate full E2E on focused regressions

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 22:38:29 +02:00
Jeppe BandJeppe Bundgaard acbbac588f [codex] disable invoice downloads until invoices exist (#158)
* disable invoice downloads until invoices exist

* enable github-hosted frontend ci runners

* Update POS invoice download visual expectations

* Fix full E2E i18n and self-serve coverage

* Review dynamic view i18n keys

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 22:35:58 +02:00
Jeppe B f75ff97042 Merge pull request #139 from copenhagentruckwash/codex/backoffice-title-department-selector
[codex] Move backoffice department selector into title
2026-07-06 22:35:13 +02:00
Jeppe Bundgaard 4aa7af9716 Merge remote-tracking branch 'origin/master' into codex/backoffice-title-department-selector
# Conflicts:
#	.github/workflows/tests.yml
2026-07-06 22:34:43 +02:00
Jeppe BandJeppe Bundgaard d2682da3cc [codex] Improve admin notifications page UI (#145)
* Improve admin notifications page UI

* Stabilize notifications page E2E bootstrap wait

* Fix PR Playwright diff head ref

* Guard MyWashStart timers after test teardown

* Stabilize self-serve wash E2E timing

* Refresh edge gateway fixture runtime state

* Run Playwright E2E on GitHub-hosted runners

* Run all frontend CI on GitHub-hosted runners

* Increase hosted full E2E parallelism

* Stabilize full E2E validation

* Preserve superuser gateway navigation label

* Add targeted Playwright dispatch workflow

* Integrate targeted Playwright dispatch into tests workflow

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 22:33:40 +02:00
Jeppe Bundgaard aa1d2ab623 Merge remote-tracking branch 'origin/master' into codex/backoffice-title-department-selector
# Conflicts:
#	.github/workflows/tests.yml
#	src/i18n/generated/de-v2.json
#	src/i18n/generated/global-v2.json
#	src/i18n/generated/no-v2.json
#	src/i18n/generated/sv-v2.json
#	src/i18n/source/de/phrases/compat/departments/index.json
#	src/i18n/source/de/phrases/compat/objects/index.json
#	src/i18n/source/de/phrases/compat/vehicles/index.json
#	src/i18n/source/global/shared/superuser_dashboard/index.json
#	src/i18n/source/no/phrases/compat/departments/index.json
#	src/i18n/source/no/phrases/compat/objects/index.json
#	src/i18n/source/no/phrases/compat/vehicles/index.json
#	src/i18n/source/sv/phrases/compat/objects/index.json
#	src/i18n/source/sv/phrases/compat/vehicles/index.json
#	tests/e2e/self-serve-wash.spec.js
#	tests/unit/playwright-full-workflow.spec.js
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 22:33:27 +02:00
Jeppe Bundgaard 929333d264 Add focused E2E dispatch scope 2026-07-06 22:21:30 +02:00
Jeppe B d2cbf823d8 Fix action wheel menu placement stability
Stabilizes the action settings wheel dropdown placement so the menu does not flip above/below after opening, and adds regression coverage plus hosted full-matrix workflow support.
2026-07-06 22:15:46 +02:00
Jeppe Bundgaard ba580e43e6 Align backoffice selector with title 2026-07-06 21:34:44 +02:00
Jeppe Bundgaard 3c49cec213 Review dynamic i18n view keys 2026-07-06 20:47:51 +02:00
Jeppe B c237fce38d Fix null product query parameters
Fixes product detail requests so null optional query params are omitted while final_price=false is preserved.
2026-07-06 20:14:45 +02:00
Jeppe Bundgaard 8acecc61cd Fix hosted full E2E catalog failures 2026-07-06 20:12:12 +02:00
Jeppe B 63149b7597 Merge pull request #162 from copenhagentruckwash/codex/daily-report-product-targets-ui
[codex] Add daily report product target controls
2026-07-06 19:36:03 +02:00
Jeppe Bundgaard a331eeadcb Speed up hosted full E2E dispatch 2026-07-06 19:27:37 +02:00
Jeppe Bundgaard 33e109b131 Fix rebased limited backoffice home handler 2026-07-06 19:27:37 +02:00
Jeppe Bundgaard 68b4328c72 Allow manual test runs on GitHub-hosted runners 2026-07-06 19:27:35 +02:00
Jeppe Bundgaard dd8a0a1952 Move backoffice department selector into title 2026-07-06 19:27:05 +02:00
Jeppe Bundgaard b58c1eb9ec Add daily report product target controls 2026-07-06 19:10:50 +02:00
Jeppe B 27994d6ac6 Merge pull request #159 from copenhagentruckwash/codex/superuser-user-overview-hub
[codex] Finish superuser user overview hub
2026-07-06 18:57:04 +02:00
Jeppe Bundgaard 9d6978e9c7 Merge remote-tracking branch 'origin/master' into codex/superuser-user-overview-hub
# Conflicts:
#	.github/workflows/tests.yml
2026-07-06 18:38:40 +02:00
Jeppe Bundgaard 62ae4bc890 [codex] Move PR E2E to hosted runners 2026-07-06 18:33:44 +02:00
Jeppe B c2ce6a859b Merge pull request #150 from copenhagentruckwash/codex/customer-product-fixed-price-overrides
Add customer product fixed price UI
2026-07-06 18:15:31 +02:00
Jeppe BandJeppe Bundgaard aa5e0a5507 Add EAN to customer registration flows (#147)
* Add EAN to economic customer registration

* Focus EAN PR Playwright mapping

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 18:12:05 +02:00
Jeppe Bundgaard 6c1c1def78 Add limited backoffice employee QR login links 2026-07-06 17:32:58 +02:00
Jeppe Bundgaard 1a232183a8 Merge master into fixed price override branch 2026-07-06 17:28:58 +02:00
Jeppe Bundgaard cfa7d5be69 Merge remote-tracking branch 'origin/master' into codex/superuser-user-overview-hub
# Conflicts:
#	.github/workflows/tests.yml
#	scripts/playwright-pr-mapping.mjs
#	src/views/dashboards/userDashboard/wash/MyWashStart.vue
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 17:12:45 +02:00
Jeppe B bb375eb149 Merge pull request #144 from copenhagentruckwash/codex/fix-invoicing-period-selfwash
Fix invoicing period Selvvask counts
2026-07-06 17:08:06 +02:00
Jeppe B 7e5e6ce680 Enforce only tankcleaning POS product rules (#151) 2026-07-06 17:07:03 +02:00
Jeppe Bundgaard 1032f8b4f1 Show limited backoffice employees in backoffice 2026-07-06 16:59:46 +02:00
Jeppe B 744a56c8b2 Merge PR #157: Cleanup superuser dashboard tabs
Cleanup superuser dashboard tabs
2026-07-06 16:59:27 +02:00
Jeppe Bundgaard e58372a190 Merge remote-tracking branch 'origin/master' into codex/pr157-merge-resolve-692380
# Conflicts:
#	scripts/playwright-pr-mapping.mjs
#	src/i18n/generated/da-v2.json
#	src/i18n/generated/de-v2.json
#	src/i18n/generated/en-v2.json
#	src/i18n/generated/no-v2.json
#	src/i18n/generated/sv-v2.json
#	src/i18n/source/da/phrases/index.json
#	src/i18n/source/de/phrases/index.json
#	src/i18n/source/en/phrases/index.json
#	src/i18n/source/no/phrases/index.json
#	src/i18n/source/sv/phrases/index.json
#	src/views/dashboards/userDashboard/wash/MyWashStart.vue
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 16:59:00 +02:00
Jeppe Bundgaard 19d44b8666 Add limited backoffice user ID translations 2026-07-06 16:53:41 +02:00
Jeppe Bundgaard 92a52d6194 Gate subuser customer navigation and actions 2026-07-06 16:48:24 +02:00
Jeppe BandJeppe Bundgaard db1c0e25c0 Move limited backoffice shortcut into header (#140)
* Move limited backoffice shortcut into header

* Use shared Playwright port locks in CI

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 16:47:33 +02:00
Jeppe Bundgaard 04860a7304 Deduplicate limited backoffice price products 2026-07-06 16:34:10 +02:00
Jeppe B d8d67301ed Add autosave for limited backoffice prices (#143) 2026-07-06 16:26:46 +02:00
Jeppe Bundgaard fa1e915b9c Merge origin/master into Selvvask period fix 2026-07-06 16:07:05 +02:00
Jeppe BandJeppe Bundgaard da86b69b2c Warn before invoicing multiple months together (#155)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 16:02:10 +02:00
Jeppe B 44e3167278 Build superuser department overview page (#156)
Build the superuser department overview page and align PR changed-E2E selection with the PR head ref.
2026-07-06 16:01:26 +02:00
Jeppe B 5cc22f14bc Fix date-only selection handling
Use local date-only parsing and formatting across date inputs and filters so clicked dates persist exactly.\n\nVerified with local format, lint, i18n, build, full unit tests, PR E2E, focused date E2E, broad smoke reruns, and GitHub PR checks.
2026-07-06 15:52:19 +02:00
Jeppe B 8519af5cbb Merge pull request #141 from copenhagentruckwash/codex/custom-pricing-only-departments
Add custom-only department pricing toggle
2026-07-06 15:31:41 +02:00
Jeppe BandJeppe Bundgaard 3fc4be8335 Update limited backoffice employee form (#146)
* Update limited backoffice employee form

* Map limited backoffice changes to focused E2E

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 15:31:16 +02:00
Jeppe Bundgaard 5a5cc0bed3 Guard MyWashStart timers after teardown 2026-07-06 15:02:57 +02:00
Jeppe Bundgaard 1a9e7503ef Stabilize PR Playwright runner load 2026-07-06 15:01:01 +02:00
Jeppe Bundgaard d84e9e3406 Map superuser dashboard changes to E2E 2026-07-06 14:56:18 +02:00
Jeppe Bundgaard c41954993c Fix PR E2E changed diff head 2026-07-06 14:54:41 +02:00
Jeppe Bundgaard a7c8fc3bff Reduce PR E2E runner pressure 2026-07-06 14:52:30 +02:00
Jeppe Bundgaard 36c1f10de8 Reduce PR E2E runner pressure 2026-07-06 14:47:03 +02:00
Jeppe Bundgaard 6002f97fb6 Finish superuser user overview hub 2026-07-06 14:42:23 +02:00
Jeppe Bundgaard 4378dad2b9 Stabilize PR E2E runner checks 2026-07-06 14:39:06 +02:00
Jeppe BandJeppe Bundgaard 278298ecd2 Submit error reports without screenshots (#153)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 14:28:24 +02:00
Jeppe Bundgaard b8959cb7bb Cleanup superuser dashboard tabs 2026-07-06 14:07:36 +02:00
Jeppe BandJeppe Bundgaard dec90bbbbf Block restricted mobile additional items (#148)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 14:07:04 +02:00
Jeppe BandJeppe Bundgaard a4b9d2f04d Add superuser vehicle creation flow (#152)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 13:59:40 +02:00
Jeppe Bundgaard ee41366c08 Bound changed PR E2E selection 2026-07-06 13:57:06 +02:00
Jeppe Bundgaard 3f42b77d13 Stabilize invoice distribution smoke session setup 2026-07-06 13:48:48 +02:00
Jeppe Bundgaard e22f78a449 Stabilize unit timer cleanup 2026-07-06 13:06:41 +02:00
Jeppe Bundgaard f2b66ff19f Stabilize recent wash refresh timers in tests 2026-07-06 13:05:01 +02:00
Jeppe Bundgaard 2fd0893773 Add customer product fixed price UI 2026-07-06 12:52:31 +02:00
Jeppe Bundgaard 0e9292d6d9 Merge remote-tracking branch 'origin/master' into codex/custom-pricing-only-departments
# Conflicts:
#	scripts/playwright-pr-mapping.mjs
2026-07-06 12:48:54 +02:00
Jeppe Bundgaard 69250ada66 Stabilize self-serve restored lane smoke test 2026-07-06 12:44:26 +02:00
Jeppe BandJeppe Bundgaard 22f8b5f203 Improve superuser role permissions page (#149)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 12:43:03 +02:00
Jeppe B 987acc5b0f Add role permission help modal (#142)
Adds the Rolle help icon and grouped permissions modal for limited backoffice employees, plus i18n and Playwright coverage.
2026-07-06 12:25:01 +02:00
Jeppe Bundgaard ce844137a0 Harden POS order detail E2E wait 2026-07-06 12:18:20 +02:00
Jeppe Bundgaard 7f3a8c07e2 Stabilize edge gateway installer smoke tests 2026-07-06 11:47:58 +02:00
Jeppe Bundgaard bd14d58f08 Stabilize self-serve wash smoke assertion 2026-07-06 11:03:22 +02:00
Jeppe Bundgaard b8494cd1d9 Merge remote-tracking branch 'origin/master' into codex/custom-pricing-only-departments
# Conflicts:
#	tests/unit/playwright-pr-mapping.spec.js
2026-07-06 10:36:57 +02:00
Jeppe Bundgaard 1a1c3db8e5 Fix invoicing period selfwash counts 2026-07-06 10:33:48 +02:00
Jeppe Bundgaard 49772c1334 Trigger frontend PR checks 2026-07-06 10:30:43 +02:00
Jeppe Bundgaard 02ddb99000 Add custom-only department pricing toggle 2026-07-06 10:21:06 +02:00
Jeppe BandJeppe Bundgaard cf1509e34f Fix POS step metadata persistence (#138)
* Fix POS step metadata persistence

* Preserve registration save payload formatting

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 10:20:13 +02:00
Jeppe BandJeppe Bundgaard ce2e671768 Fix department notification SMS table refresh
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 09:57:30 +02:00
Jeppe BandJeppe Bundgaard cf2add29dd Default vehicle subscriptions to false (#136)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 09:21:31 +02:00
Jeppe Bundgaard 8d2bd958ab Stabilize invoicing action wheel E2E 2026-07-05 17:54:26 +02:00
Jeppe Bundgaard fcdd8ecbfa Serve Font Awesome fixtures in E2E tests 2026-07-05 16:34:58 +02:00
Jeppe Bundgaard 58beb0d5c5 Stabilize POS step duplicate warning unit test 2026-07-05 16:12:24 +02:00
Jeppe Bundgaard 2b1e33b89a Reduce full Playwright runner pressure 2026-07-05 15:59:06 +02:00
Jeppe Bundgaard 02d77847da Stabilize frontend Playwright CI runner 2026-07-05 15:13:21 +02:00
Jeppe Bundgaard ad999b7e71 Fix Android SDK setup for mobile artifacts 2026-07-02 16:00:11 +02:00
Jeppe Bundgaard 06540f5342 Add signed Android artifact automation 2026-07-02 14:48:47 +02:00
Jeppe Bundgaard f1a34abe87 Retry E2E container dependency installs 2026-07-02 13:08:48 +02:00
Jeppe Bundgaard 18c3248113 Stabilize POS duplicate visual test date 2026-07-02 12:55:24 +02:00
Jeppe Bundgaard aacc39810b Refactor product data handling and improve error management in superuser products layout 2026-07-02 11:48:15 +02:00
Jeppe Bundgaard 24f19b8798 Show product name on superuser product page 2026-07-02 11:31:59 +02:00
Jeppe Bundgaard ff17f4f740 Refine limited backoffice navigation 2026-07-02 11:24:28 +02:00
Jeppe Bundgaard 2db4497385 Add mobile store artifact automation 2026-07-02 11:11:52 +02:00
Jeppe Bundgaard 94036a31e5 Fix limited backoffice i18n integrity 2026-07-01 17:42:11 +02:00
Jeppe Bundgaard 866baf9f70 Classify limited backoffice E2E slice 2026-07-01 17:15:08 +02:00
Jeppe Bundgaard 90cdc55574 Add limited backoffice functionality with employee management and department pricing 2026-07-01 16:37:25 +02:00
Jeppe Bundgaard 2f562e82a0 Fix subuser auth E2E regressions 2026-07-01 16:20:27 +02:00
Jeppe Bundgaard e6adc3876e Classify customer subuser E2E slice titles 2026-07-01 15:43:00 +02:00
Jeppe Bundgaard 475a7504a5 Refactor subuser permissions and enhance artifact management 2026-07-01 13:18:21 +02:00
Jeppe Bundgaard 7c04622019 Stabilize POS duplicate warning visual snapshot 2026-07-01 12:53:02 +02:00
Jeppe Bundgaard 5609b28e5a Reserve Playwright CI ports 2026-07-01 10:44:17 +02:00
Jeppe Bundgaard 2dc2a49758 Switch edge gateway frontend to HTTPS polling 2026-07-01 09:40:47 +02:00
Jeppe Bundgaard e304c66f93 Reduce Firefox full E2E worker pressure 2026-06-30 17:13:44 +02:00
Jeppe Bundgaard db0caf1bea Update self-serve relay tests for local transport 2026-06-30 17:10:05 +02:00
Jeppe Bundgaard 8ddb87ddca Refactor relay status getters and setters to use type aliases for improved clarity 2026-06-30 16:22:35 +02:00
Jeppe Bundgaard 730ddb1c96 Add outbox replay handling with configurable limits and timeouts 2026-06-30 16:15:57 +02:00
Jeppe Bundgaard 48f428a4f0 Extend self-serve hardware batch wait 2026-06-30 15:22:29 +02:00
Jeppe Bundgaard 806db63e9e Avoid duplicate self-serve gate errors 2026-06-30 13:32:50 +02:00
Jeppe Bundgaard c2a30c46c3 Handle Cloud Shelly fallback in self-serve controls 2026-06-30 12:07:43 +02:00
Jeppe Bundgaard 1ce08a7f32 Stabilize POS action menu vehicle assertion 2026-06-30 12:03:11 +02:00
Jeppe Bundgaard 4486d26e50 Update admin POS E2E expectations 2026-06-30 10:57:07 +02:00
Jeppe Bundgaard e49a3b46e6 Update superuser E2E expectations 2026-06-30 10:19:14 +02:00
Jeppe Bundgaard 7bfb341551 Enhance stopping wash functionality with local storage management and restoration logic 2026-06-30 10:16:09 +02:00
Jeppe Bundgaard 05e1dae033 Relax self-serve studio graph spread assertion 2026-06-30 09:41:25 +02:00
Jeppe Bundgaard 7f98454965 Add batch processing for lane hardware commands and status retrieval 2026-06-30 09:30:38 +02:00
Jeppe Bundgaard c6ec485303 Stabilize desktop POS E2E assertions 2026-06-30 09:30:28 +02:00
Jeppe Bundgaard d6d3e3b0e9 Stabilize POS mobile note popup test 2026-06-30 09:09:01 +02:00
Jeppe Bundgaard 91b6d6a9dc Fix night wash E2E booking label expectation 2026-06-30 08:27:55 +02:00
Jeppe Bundgaard b2f6165ae3 Update self-serve lane checks to include availability and self-serve status validation 2026-06-29 15:45:59 +02:00
Jeppe Bundgaard 4ab49794de Add null safety for selectedGrantCustomerNumber in session handling 2026-06-29 15:38:48 +02:00
Jeppe Bundgaard fe69642a64 Add machine wash configuration and controls to self-serve module 2026-06-29 15:15:26 +02:00
Jeppe Bundgaard 65829f5e23 Refactor authentication handling and permission checks in self-serve routes 2026-06-29 14:53:55 +02:00
Jeppe Bundgaard 8713bf8646 Enhance self-serve functionality by adding lane availability checks and updating response data 2026-06-29 14:25:43 +02:00
Jeppe Bundgaard 3ec0e1f992 Fix typo in invoice transfer monitor test for failed transfers 2026-06-29 12:26:30 +02:00
Jeppe Bundgaard 74151c65d0 Update smoke tests to support localization for workfeed and system status pages 2026-06-29 12:08:03 +02:00
Jeppe Bundgaard e16777f95c Improve regex patterns for invoice period flags and collection visibility checks 2026-06-29 11:52:36 +02:00
Jeppe Bundgaard 8e9e66f846 Add endpoint and functionality to test Slack internal department goal progress webhook 2026-06-29 11:32:43 +02:00
Jeppe Bundgaard 0e9db3ee20 Refactor i18n message handling to use runtime messages and improve linked message resolution 2026-06-29 11:20:34 +02:00
Jeppe Bundgaard 220013c4f0 Refactor input value handling and improve validation in ObjectsGlobal component 2026-06-29 11:08:47 +02:00
Jeppe Bundgaard d23d5da165 Refactor input value handling and improve validation in ObjectsGlobal component 2026-06-29 08:27:54 +02:00
Jeppe B d8a2789760 Merge pull request #135 from copenhagentruckwash/codex/limit-attachment-and-order-item-loads
[codex] Limit attachment GETs and dedupe order item loads
2026-06-15 18:42:19 +02:00
Jeppe Bundgaard 53315bbb56 Stabilize parallel E2E slices 2026-06-15 16:17:45 +02:00
Jeppe Bundgaard c88f5442ab Stabilize edge gateway cancellation E2E mock 2026-06-15 12:39:20 +02:00
Jeppe Bundgaard 7f347b6c90 Group full E2E slices by browser device and role 2026-06-15 12:19:15 +02:00
Jeppe Bundgaard e607ac8232 Limit attachment GETs and dedupe order item loads 2026-06-15 11:55:30 +02:00
Jeppe B 79b4441ffc Merge pull request #134 from copenhagentruckwash/fix/pos-scanner-live-preview-fps
Keep POS scanner camera preview live
2026-06-15 11:26:08 +02:00
Jeppe Bundgaard 2594cd475f Keep POS scanner camera preview live 2026-06-15 11:06:19 +02:00
Jeppe B 74b808ada0 Merge pull request #133 from copenhagentruckwash/fix/frontend-e2e-pos-camera-live-feed
Fix frontend E2E browser env and POS camera preview
2026-06-15 10:07:47 +02:00
Jeppe Bundgaard 4fdbd22879 Stabilize remaining full e2e slices 2026-06-15 01:13:46 +02:00
Jeppe Bundgaard 0392672a2d Stabilize Firefox desktop and tablet e2e slices 2026-06-14 23:54:28 +02:00
Jeppe Bundgaard 3d229a2de2 Stabilize WebKit desktop POS visuals 2026-06-14 22:33:05 +02:00
Jeppe Bundgaard 7e090ab405 Stabilize admin mobile e2e slices 2026-06-14 21:29:22 +02:00
Jeppe Bundgaard b8c3758714 Separate PR and manual test concurrency 2026-06-14 20:37:05 +02:00
Jeppe Bundgaard 298fc0e04b Target Docker-capable e2e runners 2026-06-14 20:26:59 +02:00
Jeppe Bundgaard 1c1bffaee4 Install Node before e2e port probe 2026-06-14 19:49:02 +02:00
Jeppe Bundgaard 14506e545a Stabilize frontend e2e CI runners 2026-06-14 19:40:24 +02:00
Jeppe Bundgaard 89dff75416 Fix frontend e2e browser env and POS camera preview 2026-06-14 18:18:14 +02:00
Jeppe B 8a790582f4 Merge pull request #132 from copenhagentruckwash/codex/optimize-scanner-lpr-frontend
[codex] optimize scanner LPR frontend
2026-06-12 22:08:42 +02:00
Jeppe Bundgaard e6aff8f512 optimize scanner lpr frontend 2026-06-12 21:42:36 +02:00
Jeppe B 1c277d1944 Merge pull request #131 from copenhagentruckwash/fix/pwa-selfserve-speed
[codex] Improve PWA request queueing
2026-06-12 13:28:53 +02:00
Jeppe Bundgaard 26cc166282 Stop mobile camera scan queue growth 2026-06-12 13:09:34 +02:00
Jeppe Bundgaard 30e878979c Reduce POS request queue blocking 2026-06-12 12:27:05 +02:00
Jeppe Bundgaard 0f335e0984 Fix self-serve hardware request queueing 2026-06-12 11:48:51 +02:00
Jeppe B 4083c05b72 Merge pull request #130 from copenhagentruckwash/fix-issues-on-webkit-browsers
Fix WebKit async UI timing
2026-06-12 11:48:46 +02:00
Jeppe B 7b4d24a212 Fix WebKit async UI timing 2026-06-12 11:32:44 +02:00
Jeppe Bundgaard 02c31c8bb0 Fix i18n catalog compatibility 2026-06-11 21:45:48 +02:00
Jeppe Bundgaard 6c0278c6e9 Enhance local data reset dialog functionality and tests 2026-06-11 21:26:12 +02:00
Jeppe Bundgaard 41e0320ce1 Add local data reset dialog and enhance footer navigation with long press functionality 2026-06-11 21:05:36 +02:00
Jeppe Bundgaard d97b02bace Add indexedDB management and local data reset confirmation dialog 2026-06-11 21:02:26 +02:00
Jeppe Bundgaard 11bbe953f3 Add version display in viewport for mobile users 2026-06-11 19:25:58 +02:00
Jeppe Bundgaard ecdd895a5a Refactor notification display logic in SelfServeTasksStep component 2026-06-11 18:49:17 +02:00
Jeppe B 07bb815754 Merge pull request #129 from copenhagentruckwash/test/self-serve-logic-condition-coverage
Add self-serve condition evaluation coverage
2026-06-11 18:48:31 +02:00
Jeppe Bundgaard 2a64fcdf19 Stabilize self-serve mobile e2e checks 2026-06-11 18:21:35 +02:00
Jeppe Bundgaard f8b7bda74a Guard self-serve active wash restore after unmount 2026-06-11 18:04:42 +02:00
Jeppe Bundgaard 5a0fc5f36b Add self-serve condition evaluation coverage 2026-06-11 17:57:10 +02:00
Jeppe B 9e378b32ac Merge pull request #128 from copenhagentruckwash/fix/mobile-gps-department-selection
Fix mobile GPS department auto-selection
2026-06-11 17:53:38 +02:00
Jeppe Bundgaard 968587d847 Fix mobile GPS department auto-selection 2026-06-11 17:32:28 +02:00
Jeppe B fa809ccd0b Merge pull request #126 from copenhagentruckwash/fix/self-serve-path-editor-case-limit
Fix self-serve path editor case limit
2026-06-11 16:57:35 +02:00
Jeppe Bundgaard a8800a3be1 Avoid duplicate PR branch push workflows 2026-06-11 16:37:51 +02:00
Jeppe Bundgaard 1f23b58d1e Isolate frontend E2E workflow ports 2026-06-11 16:36:36 +02:00
Jeppe B c6b266b0de Merge pull request #127 from copenhagentruckwash/codex/slack-test-webhook
Add Slack test webhook button
2026-06-11 16:31:09 +02:00
Jeppe Bundgaard 3eba32c235 Avoid setup-node cache restore timeouts 2026-06-11 16:06:02 +02:00
Jeppe B ec76ae426e Merge pull request #125 from copenhagentruckwash/fix/mobile-pos-product-27-note
Fix mobile POS product 27 completion
2026-06-11 15:39:35 +02:00
Jeppe Bundgaard e35d59c652 Use frontend runners for CI 2026-06-11 15:24:12 +02:00
Jeppe Bundgaard 73fc89557b Add Slack test webhook button 2026-06-11 15:06:43 +02:00
Jeppe Bundgaard be98715045 Fix self-serve path editor case limit 2026-06-11 14:46:23 +02:00
Jeppe Bundgaard 4392b7915f Fix mobile POS product 27 completion 2026-06-11 14:34:53 +02:00
Jeppe B b1fb92549c Merge pull request #124 from copenhagentruckwash/fix/full-e2e-tests-20260611
Fix full e2e regressions
2026-06-11 14:22:32 +02:00
Jeppe Bundgaard 9bba666f3a Harden mobile e2e route readiness waits 2026-06-11 13:51:39 +02:00
Jeppe Bundgaard dcf0fd61ba Avoid sudo in Playwright browser fallback 2026-06-11 13:24:22 +02:00
Jeppe Bundgaard 3d02146911 Stabilize full e2e CI matrix 2026-06-11 13:13:06 +02:00
Jeppe Bundgaard 10bc822233 Fix full e2e regressions 2026-06-11 10:13:58 +02:00
Jeppe B baac1a243a Merge pull request #123 from copenhagentruckwash/fix/self-serve-machine-tasks-after-start
Keep machine tasks visible after start summary refresh
2026-06-10 22:11:00 +02:00
Jeppe Bundgaard 7c233d7c03 Keep machine tasks visible after start summary refresh 2026-06-10 20:53:02 +02:00
Jeppe B 7c19dbddf0 Merge pull request #122 from copenhagentruckwash/fix/self-serve-start-wash-type
Honor wash type in self-serve start command
2026-06-10 20:15:58 +02:00
Jeppe Bundgaard f4b248573a Honor wash type in self-serve start command 2026-06-10 19:18:24 +02:00
Jeppe B 14e454e9d9 Merge pull request #121 from copenhagentruckwash/fix-github-runner-test-failures-8jso54
Robust Playwright browser install, e2e/unit test reliability fixes, and session bootstrap improvements
2026-06-10 16:08:11 +02:00
Jeppe B ebab7d8804 Merge pull request #120 from copenhagentruckwash/fix-github-runner-test-failures
Stabilize flaky Playwright e2e tests: ensure session bootstrap, add test hooks and increase timeouts
2026-06-10 16:07:53 +02:00
Jeppe B 78a3fb1879 test: stabilize reference autocomplete unit cleanup 2026-06-10 16:06:36 +02:00
Jeppe B d42b58c4fe test: harden flaky chromium smoke specs 2026-06-10 15:27:14 +02:00
Jeppe B 724ad8e7a1 ci: stabilize Playwright runner installs and smoke tests 2026-06-10 02:57:02 +02:00
Jeppe B 2fe50729a5 ci: fall back for unsupported Playwright deps install 2026-06-10 00:52:14 +02:00
Jeppe B eb4eee1aef test: stabilize github runner e2e checks 2026-06-09 23:02:28 +02:00
Jeppe Bundgaard db972e2923 test: refresh invoicing flag counts from scoped flags 2026-06-09 16:03:23 +02:00
Jeppe Bundgaard 00817925e4 Add path case answer mapping and matching logic for improved path verification 2026-06-09 15:56:59 +02:00
Jeppe Bundgaard 347ab3c811 ci: allow e2e jobs on linux runner pool 2026-06-09 15:53:58 +02:00
Jeppe Bundgaard a3d05b15db test: stabilize edge gateway route readiness 2026-06-09 15:46:48 +02:00
Jeppe Bundgaard 9093d0d601 test: isolate request queue shortcut listeners 2026-06-09 15:20:15 +02:00
Jeppe Bundgaard 8c57b98627 Fix i18n CI runtime menu checks 2026-06-09 15:14:22 +02:00
Jeppe Bundgaard 4b99751c0e test: reset POS department route in unit setup 2026-06-09 15:13:06 +02:00
Jeppe Bundgaard 66f4331b4e Refactor machine allowance logic in self-serve studio and remove unused controls 2026-06-09 14:52:20 +02:00
Jeppe Bundgaard e1a824d565 Fix path editor answer row layout 2026-06-09 14:18:15 +02:00
Jeppe Bundgaard 883a88a551 ci: split playwright pr e2e matrix 2026-06-09 14:07:25 +02:00
Jeppe Bundgaard a328bc3767 Filter path editor to machine start cases 2026-06-09 13:57:33 +02:00
Jeppe Bundgaard 255447f788 Hide placeholder departments in admin navigation 2026-06-09 13:34:08 +02:00
Jeppe Bundgaard b3d57ca717 test: cover collected invoice move checks 2026-06-09 13:24:23 +02:00
Jeppe Bundgaard 45d21c4372 fix: restore self-serve lane step hook 2026-06-09 13:19:22 +02:00
Jeppe Bundgaard 7f7b5ae082 Add Swedish invoice collection move translation 2026-06-09 13:17:55 +02:00
Jeppe Bundgaard 028d8f1fc8 Add move to customer functionality for invoice collections in multiple languages 2026-06-09 13:16:26 +02:00
Jeppe Bundgaard 8d01ca1363 Implement move collected invoice to customer feature with API integration and tests 2026-06-09 13:16:05 +02:00
Jeppe Bundgaard a36403d279 Refactor self-serve fetch logic to use refs for state management; implement abort controller for active requests 2026-06-09 13:07:21 +02:00
Jeppe Bundgaard a78ad69c0c Add selfserve dynamic image size selector 2026-06-09 12:46:49 +02:00
Jeppe Bundgaard 22c856afb6 test: stabilize superuser firefox e2e 2026-06-09 12:34:11 +02:00
Jeppe Bundgaard 71a073ebf9 Add hidden frontend maintenance refresh 2026-06-09 12:32:58 +02:00
Jeppe Bundgaard f709cac95e Test wash type service sync 2026-06-09 12:13:35 +02:00
Jeppe Bundgaard 27867e48df test: update my wash start unit stubs 2026-06-09 11:50:21 +02:00
Jeppe Bundgaard f1a7e1dfcd test: stabilize POS draft full slice 2026-06-09 11:29:13 +02:00
Jeppe Bundgaard 31e9566f2d Refactor API route handling in tests to use centralized path patterns; enhance error handling in self-serve components 2026-06-09 10:52:20 +02:00
Jeppe Bundgaard f7f749e230 Fix PWA install scope 2026-06-08 20:56:32 +02:00
Jeppe Bundgaard f5b7da7b0a test: skip WebKit mobile invoice monitor header 2026-06-08 20:42:30 +02:00
Jeppe Bundgaard 2311188b13 test: align self-serve stop failure e2e 2026-06-08 20:25:01 +02:00
Jeppe Bundgaard 25637b52d9 test: update self-serve completion contract 2026-06-08 20:20:46 +02:00
Jeppe Bundgaard fddcc86cd6 fix: preserve self-serve stop failure banner 2026-06-08 20:17:44 +02:00
Jeppe Bundgaard c1b2be7d70 Enhance task ID handling in updateLaneAllowedServices; improve error handling in wash process 2026-06-08 19:56:02 +02:00
Jeppe Bundgaard 0a7b3a36aa test: stabilize release and department e2e mocks 2026-06-08 19:51:17 +02:00
Jeppe Bundgaard 57f102de71 test: skip desktop-only superuser specs on compact projects 2026-06-08 19:47:54 +02:00
Jeppe Bundgaard 6b22887a90 Remove guidance text for unavailable machine wash in self-serve flow 2026-06-08 18:58:33 +02:00
Jeppe Bundgaard 3b3f212ceb ci: use polling for Playwright Vite server 2026-06-08 18:54:25 +02:00
Jeppe Bundgaard 1a11e31836 ci: raise Playwright file descriptor limit 2026-06-08 18:46:56 +02:00
Jeppe Bundgaard d4b69c3990 test: align release runtime inspector privileges 2026-06-08 18:38:49 +02:00
Jeppe Bundgaard 4cbccd533a fix: stabilize POS route and action menu tests 2026-06-08 18:09:51 +02:00
Jeppe Bundgaard a9bc3ab0fc Fix POS booking match recursion 2026-06-08 17:29:06 +02:00
Jeppe Bundgaard d9f929df04 fix: restore Slack i18n compatibility 2026-06-08 17:25:56 +02:00
Jeppe Bundgaard bc154a4e6d Keep release wait after auto sync gateway timeout 2026-06-08 17:12:34 +02:00
Jeppe Bundgaard 7d48e40e05 Enhance release workflow with current commit check and enable concurrency cancellation 2026-06-08 17:02:42 +02:00
Jeppe Bundgaard 47af253bf8 Enhance self-serve logic to ignore stale answer sync responses and implement mutation request tracking 2026-06-08 16:53:03 +02:00
Jeppe Bundgaard e126dfccf9 Enhance accessibility and linting configuration
- Add tabindex attributes to input fields for better keyboard navigation
- Introduce ESLint configuration for improved code quality and consistency
- Update package.json to include linting scripts and dependencies
- Add tests for desktop tab navigation in POS flow
2026-06-08 16:35:56 +02:00
Jeppe Bundgaard be5191825d fix: distinguish Slack config permission errors 2026-06-08 16:33:52 +02:00
Jeppe Bundgaard 68cf9c4c06 Continue release wait after auto sync timeout 2026-06-08 16:25:24 +02:00
Jeppe Bundgaard e46b7ae58d Request release auto sync before artifact wait 2026-06-08 16:18:45 +02:00
Jeppe Bundgaard 3d81fb8dd7 Fix release verification for external build ids 2026-06-08 15:59:39 +02:00
Jeppe Bundgaard ff983dca60 Trigger frontend release 2026-06-08 15:48:21 +02:00
Jeppe Bundgaard fb0fceb6c4 Add booking count permission checks and handle API response failures gracefully 2026-06-08 15:26:57 +02:00
Jeppe Bundgaard 9591afb9a2 Add booking count permission checks and handle API response failures gracefully 2026-06-08 13:41:57 +02:00
Jeppe Bundgaard 0afde8ac8e Add support for webmanifest files in .htaccess and nginx configuration 2026-06-08 13:11:21 +02:00
Jeppe Bundgaard 4a83486b30 Update Danish translations for various UI elements and API keys 2026-06-08 12:51:10 +02:00
Jeppe Bundgaard a3ceac1636 Add Slack configuration module with customer registration webhook settings 2026-06-08 12:43:02 +02:00
Jeppe Bundgaard 8666d17a42 Add superuser new customer email notification settings 2026-06-08 12:19:32 +02:00
gold-dev 96d13b2400 Feat: Produktivitet overview 2026-06-08 11:32:30 +03:00
gold-dev 12fc1f80cc Feat: Rename ”Charts” til ”Grafer” 2026-06-08 10:50:37 +03:00
Jeppe B 2dfe4e9b37 Merge pull request #119 from copenhagentruckwash/fix-github-runner-test-failures
Fix self-serve wash progress actions, guided-wash emits and guest fallback UI; install Playwright deps for CI
2026-06-08 09:26:26 +02:00
Jeppe B 46c634e5a7 Fix self-serve wash test regressions 2026-06-08 09:13:24 +02:00
Jeppe Bundgaard 880680d7f8 Add reusable ErrorBanner component and integrate into MyWashStart 2026-06-07 22:07:56 +02:00
Jeppe Bundgaard 7d5ed772c2 Avoid empty MyWash lane service sync 2026-06-04 07:40:33 +02:00
Jeppe Bundgaard 83fbcdeaa8 Refresh MyWash eligibility before service sync 2026-06-04 07:31:41 +02:00
Jeppe Bundgaard 55a761d3a5 Publish MyWash tasks before hydration 2026-06-04 07:27:12 +02:00
Jeppe Bundgaard abe8e7750f Preserve MyWash machine services during sync 2026-06-04 07:21:07 +02:00
Jeppe Bundgaard aeef75b831 Clear stale MyWash summary after preview 2026-06-04 07:13:00 +02:00
Jeppe Bundgaard d6356d0244 Stop MyWash eligibility refresh after completion 2026-06-04 06:35:26 +02:00
Jeppe Bundgaard bbf2aeb95f Keep MyWash completion visible after stop 2026-06-04 06:05:41 +02:00
Jeppe Bundgaard 3a7d78ae05 Serve release-prefixed index shell 2026-06-04 04:08:33 +02:00
Jeppe Bundgaard 2a2f752692 Refresh recent completion marker during startup 2026-06-04 03:51:43 +02:00
Jeppe Bundgaard 8883bf8a07 Seed recently completed wash state before startup awaits 2026-06-04 03:44:29 +02:00
Jeppe Bundgaard 6ec80e2796 Recover recently completed self-serve screen 2026-06-04 03:29:55 +02:00
Jeppe Bundgaard 849a7b77cf Adopt resolved self-serve vehicle type 2026-06-04 03:16:59 +02:00
Jeppe Bundgaard 36ccd0c381 Keep completed wash fallback visible 2026-06-04 03:01:09 +02:00
Jeppe Bundgaard 58e7c8d6c9 Keep completed wash after lane refresh 2026-06-04 02:44:48 +02:00
Jeppe Bundgaard d04fc8cc0f Keep completed self-serve wash visible 2026-06-04 02:29:16 +02:00
Jeppe Bundgaard 339a1b1452 Recover completed wash after stop timeout 2026-06-04 02:15:15 +02:00
Jeppe Bundgaard 2d2d63d09e Merge remote-tracking branch 'origin/master' 2026-06-04 02:02:26 +02:00
Jeppe B 8241f7fd2f Treat unoccupied stop invoice errors as completed washes 2026-06-04 02:01:35 +02:00
Jeppe Bundgaard 985571af87 Treat unoccupied stop invoice errors as completed washes 2026-06-04 01:55:35 +02:00
Jeppe Bundgaard 82e1e4a466 Ignore preview-only active wash reservations 2026-06-04 01:21:25 +02:00
Jeppe Bundgaard 9a7f7bbb78 Prevent preview-only self-serve sessions from auto-restoring 2026-06-04 01:08:04 +02:00
Jeppe Bundgaard 359f29b75e Keep self-serve preview questions visible during summary load 2026-06-04 00:45:20 +02:00
Jeppe B ceced7cbaa Show MyWash completion when Buefy hides step panel 2026-06-04 00:21:33 +02:00
Jeppe Bundgaard 6b5cd00b10 Show completed wash outside step panels 2026-06-04 00:10:36 +02:00
Jeppe Bundgaard 02eae47d34 Refactor toggle-task event emission in SelfServeTaskList component 2026-06-04 00:00:28 +02:00
Jeppe Bundgaard 3f01d38676 Enhance self-serve image loading with preload and error handling 2026-06-03 23:53:54 +02:00
Jeppe Bundgaard 390a29ef55 Merge remote-tracking branch 'origin/master' 2026-06-03 23:34:05 +02:00
Jeppe Bundgaard b7f5786747 Add vehicle type selection logic for departments without product allow-list 2026-06-03 23:33:54 +02:00
Jeppe B 814c535757 Use relative release manifest asset paths 2026-06-03 23:13:26 +02:00
Jeppe Bundgaard ed0e27e4fd Use relative release manifest asset paths 2026-06-03 23:07:17 +02:00
Jeppe Bundgaard 659df480d7 Merge remote-tracking branch 'origin/master' 2026-06-03 23:04:43 +02:00
Jeppe B d0c45f6180 Fix release frontend origin routing 2026-06-03 22:45:32 +02:00
Jeppe Bundgaard 5a20461bd4 Fix release frontend origin routing 2026-06-03 22:40:49 +02:00
Jeppe Bundgaard 849f52fb8e Refactor error message display and update test descriptions for clarity 2026-06-03 22:16:27 +02:00
Jeppe Bundgaard da0c699b88 Add self-serve session handling and lane selection normalization 2026-06-03 22:07:47 +02:00
Jeppe Bundgaard af5514c982 Implement dynamic base path handling for frontend assets and update routing logic 2026-06-03 22:00:18 +02:00
Jeppe Bundgaard cc689e80a9 Use release base for frontend HTML assets 2026-06-03 21:45:25 +02:00
Jeppe Bundgaard 3b51374678 Add dynamic release frontend base path handling and update static asset references 2026-06-03 21:44:27 +02:00
Jeppe Bundgaard f07569db98 Allow active self-serve lane selection 2026-06-03 21:32:58 +02:00
Jeppe Bundgaard c5fcb90683 Include emitted chunks in release manifest 2026-06-03 21:12:08 +02:00
Jeppe Bundgaard 270f87dee1 Split release smoke static and app targets 2026-06-03 21:03:03 +02:00
Jeppe Bundgaard 765b8f3b35 Fix self-serve entry regressions 2026-06-03 20:51:06 +02:00
Jeppe Bundgaard 3ff695899c Restore self-serve task phase from active sessions 2026-06-03 20:32:40 +02:00
Jeppe Bundgaard be35baa838 Refactor self-serve registration field for improved layout and accessibility 2026-06-03 20:06:31 +02:00
Jeppe B 470d4d7314 Normalize release bootstrap cleanup 2026-06-03 19:38:54 +02:00
Jeppe B 0976608a25 Refresh release bootstrap asset hash 2026-06-03 19:34:48 +02:00
Jeppe B e7f4dfa07e Trigger frontend release after asset mismatch 2026-06-03 19:23:01 +02:00
Jeppe B 2c68f335c6 Trigger frontend release for self-serve vehicle fix 2026-06-03 18:08:25 +02:00
Jeppe Bundgaard cf29589e29 Fix self-serve vehicle step progression 2026-06-03 17:51:46 +02:00
Jeppe Bundgaard 05cabd8edc Add margin and padding adjustments to self-serve task checkbox 2026-06-02 20:09:18 +02:00
Jeppe Bundgaard 2f4ecba51a Merge remote-tracking branch 'origin/master' 2026-06-02 20:04:11 +02:00
Jeppe Bundgaard 74a7c9f23f Refactor self-serve task list to improve task title handling and remove unnecessary description checks 2026-06-02 20:03:55 +02:00
Jeppe B b075a717f7 Cover hidden customer number self-serve flow 2026-06-02 19:59:30 +02:00
Jeppe B a3dcb888d3 Cover self-serve task title presentation 2026-06-02 19:59:12 +02:00
Jeppe B 02706af508 Allow QR self-serve vehicle step without customer field 2026-06-02 19:58:50 +02:00
Jeppe B 1f2c007776 Tighten self-serve task presentation 2026-06-02 19:58:28 +02:00
Jeppe Bundgaard 42dfdfef13 Add mocks for department daily report components and update Vitest configuration for SCSS 2026-06-02 17:54:37 +02:00
Jeppe B b40bf0b9e1 Merge pull request #110 from copenhagentruckwash/create-playwright-test-suite-for-mywashstart
Add mocked Playwright tests for MyWashStart flow and improve answer-sync & department fallback
2026-06-02 13:52:00 +02:00
Jeppe B cc15a9c468 Merge branch 'master' into create-playwright-test-suite-for-mywashstart 2026-06-02 13:51:52 +02:00
Jeppe B 5d8b1eecb4 Merge pull request #111 from copenhagentruckwash/create-playwright-test-suite-for-mywashstart-6chtnh
Add mocked Playwright tests for MyWashStart flow and improve answer-sync & department fallback
2026-06-02 13:50:32 +02:00
Jeppe B 093d82d762 Merge branch 'master' into create-playwright-test-suite-for-mywashstart-6chtnh 2026-06-02 13:50:24 +02:00
Jeppe B 686577c209 Merge pull request #114 from copenhagentruckwash/update-mywashstart.vue-with-timeout-handling
Fix delayed active wash restore cleanup
2026-06-02 13:48:47 +02:00
Jeppe B c2b333a78b Merge branch 'master' into update-mywashstart.vue-with-timeout-handling 2026-06-02 13:48:33 +02:00
Jeppe B cef8b8bf0f Merge pull request #115 from copenhagentruckwash/update-userdashboard-to-inspect-lane-readiness
Guard wash entry self-serve CTA
2026-06-02 13:47:39 +02:00
Jeppe B 62a46710ba Merge branch 'master' into update-userdashboard-to-inspect-lane-readiness 2026-06-02 13:47:31 +02:00
Jeppe B a3e6feb07e Merge pull request #113 from copenhagentruckwash/refactor-self-serve-data-loading-with-fetch-scheduler
Debounce self-serve wash data fetches
2026-06-02 13:43:40 +02:00
Jeppe B 2ad093d4f1 Merge pull request #112 from copenhagentruckwash/update-mywashstart.vue-for-answer-sync-state
Block wash question flow on failed answer sync
2026-06-02 13:43:22 +02:00
Jeppe B 6017d576fc Merge pull request #116 from copenhagentruckwash/update-wash-department-handling-and-ui
Handle self-serve wash when geolocation is unavailable
2026-06-02 13:42:51 +02:00
Jeppe B 08ac38dc87 Merge pull request #117 from copenhagentruckwash/add-and-extend-unit-and-e2e-tests
Add production-ready tests for user self-serve wash start (unit + mocked e2e)
2026-06-02 13:42:36 +02:00
Jeppe B 3960daa556 Add production wash start test matrix 2026-06-02 13:42:22 +02:00
Jeppe B 395cbd87dd Handle self-serve wash without geolocation 2026-06-02 13:42:16 +02:00
Jeppe B 23429c047f Guard wash entry self-serve CTA 2026-06-02 13:42:09 +02:00
Jeppe B af68748c16 Fix active wash restore timeout cleanup 2026-06-02 13:42:02 +02:00
Jeppe B 18ab007f87 Debounce self-serve wash data fetches 2026-06-02 13:41:55 +02:00
Jeppe B 63942045be Block wash question flow on failed answer sync 2026-06-02 13:41:51 +02:00
Jeppe B a4cdc6e4f3 Add mocked MyWashStart flow coverage 2026-06-02 13:41:44 +02:00
Jeppe B 07c611bdbe Add mocked MyWashStart flow coverage 2026-06-02 13:41:32 +02:00
Jeppe Bundgaard 5170139a32 Switch CI to self-hosted runners
Updated all GitHub Actions workflows to use self-hosted runners instead of `ubuntu-latest`. This change ensures better control over the CI environment and aligns with internal infrastructure requirements.
2026-06-02 10:36:18 +02:00
Jeppe Bundgaard 90952a68b7 Refactor SubusersTable to use ActionSettingsWheelButton for row actions 2026-06-02 09:23:49 +02:00
Jeppe Bundgaard 1cc14d7b41 Merge remote-tracking branch 'origin/master' 2026-06-02 08:36:42 +02:00
Jeppe B aa4d6576f7 Merge pull request #108 from copenhagentruckwash/fix-page-crash-issues
Fix wash start recovery from API failures
2026-06-02 08:35:28 +02:00
Jeppe B 2a46984f9c Merge pull request #109 from copenhagentruckwash/fix-playwright-exit-code-error
Stabilize release sidebar E2E coverage
2026-06-02 08:34:47 +02:00
Jeppe Bundgaard 4a9c721c55 Remove debug message for beta frontend in build output 2026-06-02 08:12:50 +02:00
Jeppe B 7be9995c87 Stabilize release sidebar e2e coverage 2026-06-02 08:10:51 +02:00
Jeppe B 85529de3f8 Fix wash start recovery from API failures 2026-06-02 08:07:03 +02:00
Jeppe B 114ca9d956 Merge pull request #107 from copenhagentruckwash/investigate-and-fix-failing-tests
Resolve failing frontend CI: fix merge conflicts, restore edge cache & tests
2026-06-02 07:15:41 +02:00
Jeppe B 29f858685a Fix self-serve wash smoke restore fixture 2026-06-02 07:15:26 +02:00
Jeppe B 4112faf682 Fix edge gateway broker config e2e 2026-06-02 02:38:37 +02:00
Jeppe B a13b73c414 Harden edge gateway service parsing 2026-06-02 02:06:32 +02:00
Jeppe B 667e93af8f Resolve failing frontend checks 2026-06-02 01:13:28 +02:00
Jeppe B 54a888026e Merge pull request #104 from copenhagentruckwash/propose-fix-for-price-tampering-vulnerability
Fix trusted booking prices in POS hydration
2026-06-02 00:48:56 +02:00
Jeppe B a0fb02baa1 Merge pull request #106 from copenhagentruckwash/fix-booking-delete-access-permission
Fix booking delete action visibility
2026-06-02 00:47:38 +02:00
Jeppe B c445b932db Fix booking delete action visibility 2026-06-02 00:47:19 +02:00
copilot-swe-agent[bot] ffccb1683b Merge origin/master and resolve POS pricing conflict 2026-06-01 22:44:56 +00:00
Jeppe B 0d590fc9d6 Merge pull request #105 from copenhagentruckwash/fix-postinstall-script-vulnerability
Remove unsafe postinstall hook
2026-06-02 00:40:46 +02:00
Jeppe B 1d4910f947 Remove unsafe postinstall hook 2026-06-02 00:40:34 +02:00
Jeppe B 7e457ed961 Fix trusted booking prices in POS hydration 2026-06-02 00:39:55 +02:00
Jeppe B 34722c6985 Merge pull request #103 from copenhagentruckwash/fix-vulnerability-in-pos-order-pricing
Fix POS booking price hydration
2026-06-02 00:39:37 +02:00
Jeppe B c5a13b50ab Fix POS booking price hydration 2026-06-02 00:39:15 +02:00
Jeppe B 387bd50281 Merge pull request #102 from copenhagentruckwash/fix-unsandboxed-blob-attachment-previews
Sanitize and sandbox order attachment previews to prevent XSS
2026-06-02 00:38:15 +02:00
Jeppe B 81016fd898 Fix unsafe order attachment previews 2026-06-02 00:38:03 +02:00
Jeppe B 39fc1612ac Merge pull request #101 from copenhagentruckwash/fix-duplicate-stripe-invoice-submission-issue
Prevent duplicate Stripe invoice queue submissions
2026-06-02 00:30:18 +02:00
Jeppe B 7ab12a3fff Prevent duplicate Stripe invoice queue submissions 2026-06-02 00:29:53 +02:00
Jeppe B b4d869c469 Merge pull request #100 from copenhagentruckwash/fix-autosave-to-prevent-infinite-retry-loop
Fix autosave failed-save retry loop
2026-06-02 00:28:52 +02:00
Jeppe B 799b667828 Fix autosave failed-save retry loop 2026-06-02 00:28:39 +02:00
Jeppe B ae3513edf3 Merge pull request #99 from copenhagentruckwash/fix-repeated-booking-prefetch-flooding-api
Avoid duplicate booking prefetch by honoring exact-plate cache
2026-06-02 00:27:37 +02:00
Jeppe B 6db1861b78 Fix repeated booking prefetch requests 2026-06-02 00:27:25 +02:00
Jeppe B a8e839ba18 Merge pull request #98 from copenhagentruckwash/fix-mobile-pos-orders-completion-issue
Fix mobile POS order completion
2026-06-02 00:27:07 +02:00
Jeppe B b224cf67d1 Fix mobile POS order completion 2026-06-02 00:26:53 +02:00
Jeppe B 222237ab34 Merge pull request #96 from copenhagentruckwash/fix-edge-gateway-localstorage-vulnerability
Harden edge gateway workspace cache
2026-06-02 00:16:07 +02:00
copilot-swe-agent[bot] 433cfe521e Planning merge conflict resolution 2026-06-01 22:14:58 +00:00
Jeppe B 1e4a776fa5 Merge pull request #97 from copenhagentruckwash/fix-auto-selection-of-customer-suggestions
Fix stale POS customer suggestion auto-selection
2026-06-02 00:13:59 +02:00
Jeppe B e88a424a7d Fix stale POS customer suggestion auto-selection 2026-06-02 00:13:46 +02:00
Jeppe B 9450c7902d Harden edge gateway workspace cache 2026-06-02 00:08:20 +02:00
Jeppe B 75b5883afe Merge pull request #95 from copenhagentruckwash/fix-stripe-email-invoice-payment-misclassification
Fix Stripe email invoice paid validation
2026-06-02 00:07:49 +02:00
Jeppe B c26f0184d9 Fix Stripe email invoice paid validation 2026-06-02 00:07:35 +02:00
Jeppe B cb74b4bbe7 Merge pull request #93 from copenhagentruckwash/propose-fix-for-cached-gateway-details-leak
Scope edge gateway cache to session and prevent auth-blind cached fallbacks
2026-06-01 23:48:26 +02:00
Jeppe B 326eca656f Fix edge gateway cache authorization fallback 2026-06-01 23:48:17 +02:00
Jeppe B f0a5058b68 Merge pull request #92 from copenhagentruckwash/propose-fix-for-self-hosted-ci-vulnerability
Use GitHub-hosted runners for tests workflow
2026-06-01 23:46:15 +02:00
copilot-swe-agent[bot] d39cd36995 Merge remote-tracking branch 'origin/master' into propose-fix-for-self-hosted-ci-vulnerability
# Conflicts:
#	.github/workflows/tests.yml
2026-06-01 21:43:04 +00:00
Jeppe B 0548018b5c Use GitHub-hosted runners for tests workflow 2026-06-01 23:40:30 +02:00
Jeppe B 699e10856e Merge pull request #91 from copenhagentruckwash/propose-fix-for-self-hosted-runner-vulnerability
Fix CI runner exposure for pull requests
2026-06-01 23:40:09 +02:00
Jeppe B 36b399cb2f Fix CI runner exposure for pull requests 2026-06-01 23:40:00 +02:00
Jeppe B f613d58727 Merge pull request #90 from copenhagentruckwash/propose-fix-for-product-catalog-vulnerability
Restrict vehicle type product edits to superusers
2026-06-01 23:35:14 +02:00
Jeppe B feccfada77 Restrict vehicle type product edits to superusers 2026-06-01 23:35:04 +02:00
Jeppe B 4127e66114 Merge pull request #89 from copenhagentruckwash/fix-unvalidated-attachment-download-links
Validate self-serve attachment download links
2026-06-01 23:34:45 +02:00
Jeppe B b2a9b59399 Validate self-serve attachment download links 2026-06-01 23:34:34 +02:00
Jeppe B 6ffed9c2f2 Merge pull request #88 from copenhagentruckwash/fix-lane-toggle-guard-permission-issue
Fix lane toggle permission guards
2026-06-01 23:34:14 +02:00
Jeppe B 53e193d8d1 Fix lane toggle permission guards 2026-06-01 23:34:03 +02:00
Jeppe B 7835691bc9 Merge pull request #87 from copenhagentruckwash/fix-websocket-token-exposure-vulnerability
Secure edge gateway WebSocket session tokens
2026-06-01 23:33:44 +02:00
Jeppe B 91715541ec Secure edge gateway websocket sessions 2026-06-01 23:33:34 +02:00
Jeppe B e3f9e99129 Merge pull request #86 from copenhagentruckwash/fix-stored-xss-in-relay-options
Fix select option HTML escaping
2026-06-01 23:33:20 +02:00
Jeppe B 4634cbad89 Fix select option HTML escaping 2026-06-01 23:33:10 +02:00
Jeppe B 5db2a36da0 Merge pull request #85 from copenhagentruckwash/propose-fix-for-relay-failure-issue
Rollback wash start when relay activation fails
2026-06-01 22:53:44 +02:00
Jeppe B cd7b273da4 Rollback wash start when relay activation fails 2026-06-01 22:53:34 +02:00
Jeppe B cbbad1a6e2 Merge pull request #84 from copenhagentruckwash/fix-pos-url-order-reassignment-vulnerability
Prevent implicit POS order department changes
2026-06-01 22:44:24 +02:00
Jeppe B 9661d4efa7 Prevent implicit POS order department changes 2026-06-01 22:44:14 +02:00
Jeppe B 31762c23c5 Merge pull request #83 from copenhagentruckwash/fix-unescaped-order-delete-reasons
Fix protected order delete reason escaping
2026-06-01 22:43:55 +02:00
Jeppe B c71a6b4d14 Fix protected order delete reason escaping 2026-06-01 22:43:45 +02:00
Jeppe B e8ddb33f21 Merge pull request #82 from copenhagentruckwash/fix-vulnerability-in-edge-broker-config
Secure edge gateway broker configuration
2026-06-01 22:28:28 +02:00
Jeppe B ea368d19bb Secure edge gateway broker configuration 2026-06-01 22:28:18 +02:00
Jeppe B 7ab50b6290 Merge pull request #81 from copenhagentruckwash/fix-unredacted-broker-diagnostics-exposure
Redact edge gateway diagnostic secrets
2026-06-01 22:27:33 +02:00
Jeppe B fd7cfbd3c3 Redact edge gateway diagnostics 2026-06-01 22:27:19 +02:00
Jeppe B 90725af10a Merge pull request #80 from copenhagentruckwash/fix-ungated-destructive-actions-in-ui
Gate self-serve studio gateway actions and require confirmation for destructive operations
2026-06-01 22:26:58 +02:00
Jeppe B d63a62a921 Gate self-serve gateway actions 2026-06-01 22:26:44 +02:00
Jeppe B 4bc5481941 Merge pull request #79 from copenhagentruckwash/propose-fix-for-deferred-start-relay-issue
Require successful machine relay activation before marking wash in progress
2026-06-01 22:15:16 +02:00
Jeppe B 9feede3b0d Require machine relay activation before wash progress 2026-06-01 22:15:02 +02:00
Jeppe B 5aad9eda8d Merge pull request #78 from copenhagentruckwash/fix-raw-edge-log-context-exposure
Redact edge gateway log context secrets
2026-06-01 22:13:42 +02:00
Jeppe B 91b15faba6 Redact edge gateway log context secrets 2026-06-01 22:13:32 +02:00
Jeppe B 98c1c61cbd Merge pull request #77 from copenhagentruckwash/fix-active-wash-endpoint-information-leak
Scope active-wash restore to authenticated customer (my-active-wash)
2026-06-01 22:06:25 +02:00
Jeppe B 4463a5326a Fix scoped active wash restoration 2026-06-01 22:06:14 +02:00
Jeppe B bdb23513a4 Merge pull request #76 from copenhagentruckwash/propose-fix-for-security-vulnerability
Harden self-serve vehicle condition mutations
2026-06-01 22:05:37 +02:00
Jeppe B ee40f16232 Harden self-serve vehicle condition mutations 2026-06-01 22:05:27 +02:00
Jeppe B a76944b00c Merge pull request #75 from copenhagentruckwash/fix-sessionstorage-vulnerability-in-invoicing-period
Secure privileged session caches
2026-06-01 21:52:53 +02:00
Jeppe B cf074ff893 Secure privileged session caches 2026-06-01 21:52:42 +02:00
Jeppe B 712eb19db5 Merge pull request #74 from copenhagentruckwash/propose-fix-for-release-manager-token-leak
Restrict release manager control API origins
2026-06-01 21:52:20 +02:00
Jeppe B de9bc9b170 Restrict release manager control API origins 2026-06-01 21:52:09 +02:00
Jeppe B 95e2404307 Merge pull request #73 from copenhagentruckwash/fix-unredacted-deployment-failures-exposure
Redact release deployment failure diagnostics
2026-06-01 21:27:42 +02:00
Jeppe B 2c01feb3be Redact release deployment failure diagnostics 2026-06-01 21:27:28 +02:00
Jeppe B f416e0d462 Merge pull request #72 from copenhagentruckwash/fix-telemetry-that-leaks-passwords
Redact serialized request bodies and omit failed-request bodies when capture is disabled
2026-06-01 21:26:45 +02:00
Jeppe B 7f1308c8ce Redact release timeline failed request bodies 2026-06-01 21:26:32 +02:00
Jeppe B fd8d45e437 Merge pull request #71 from copenhagentruckwash/fix-vulnerabilities-in-release-runtime-urls
Harden release runtime URL trust boundaries
2026-06-01 21:25:51 +02:00
copilot-swe-agent[bot] 20390342d5 Merge origin/master and resolve release timeline spec conflict 2026-06-01 19:21:12 +00:00
Jeppe B c47bf229f0 Harden release runtime URL trust boundaries 2026-06-01 21:14:39 +02:00
Jeppe B 3616e197e1 Merge pull request #70 from copenhagentruckwash/propose-fix-for-isolated-stack-vulnerability
Prevent active isolated stack reuse during release deploys
2026-06-01 21:12:26 +02:00
Jeppe B 3d8544de21 Prevent isolated stack deployment reuse 2026-06-01 21:11:17 +02:00
Jeppe B 504abb7a15 Merge pull request #69 from copenhagentruckwash/fix-release-subject-autocomplete-exposure
Guard release assignment subject search
2026-06-01 21:11:02 +02:00
Jeppe B 37021533b9 Guard release assignment search by manage permission 2026-06-01 21:10:51 +02:00
Jeppe B 59eab84f03 Merge pull request #68 from copenhagentruckwash/fix-cross-origin-framing-vulnerability-in-nginx
Add anti-framing headers to Coolify Nginx frontend
2026-06-01 21:10:26 +02:00
Jeppe B e67033711c Add anti-framing headers to Coolify Nginx config 2026-06-01 21:10:14 +02:00
Jeppe B 85fd00f020 Merge pull request #67 from copenhagentruckwash/fix-metadata-exposure-for-low-privileged-users
Restrict release runtime metadata inspector
2026-06-01 21:09:54 +02:00
Jeppe B 4f064a1625 Restrict release runtime metadata inspector 2026-06-01 21:09:42 +02:00
Jeppe B 3bf7ada226 Merge pull request #66 from copenhagentruckwash/fix-tls-verification-in-dev-proxy
Secure Vite API proxy TLS by default
2026-06-01 21:09:15 +02:00
Jeppe B 3b90e17a1e Secure Vite API proxy TLS by default 2026-06-01 21:09:05 +02:00
Jeppe B c1fb5b72ee Merge pull request #65 from copenhagentruckwash/fix-privilege-escalation-in-daily-report
Guard department lane toggles by wash-lane permission
2026-06-01 21:08:43 +02:00
Jeppe B 29da26cc08 Guard department lane toggles by permission 2026-06-01 21:08:32 +02:00
Jeppe B 807fc09b1e Merge pull request #64 from copenhagentruckwash/fix-dgnvask-toggle-issue
Fix Døgnvask toggle state with configuration warnings
2026-06-01 21:08:03 +02:00
Jeppe B 528e4994db Fix Dognvask warning toggle state 2026-06-01 21:07:53 +02:00
Jeppe B c339122f4f Merge pull request #63 from copenhagentruckwash/fix-release-gate-binding-to-build
Bind frontend release gate to build id
2026-06-01 21:07:33 +02:00
Jeppe B feeef46354 Bind frontend release gate to build id 2026-06-01 21:07:23 +02:00
Jeppe B 066bd14554 Merge pull request #62 from copenhagentruckwash/fix-active-wash-refresh-vulnerability
Harden active-wash refresh ownership check
2026-06-01 21:06:53 +02:00
Jeppe B bffaa2ab88 Fix active wash refresh ownership check 2026-06-01 21:06:39 +02:00
Jeppe B 31495ea67b Merge pull request #61 from copenhagentruckwash/fix-ftp-deploy-password-exposure
Secure lftp release upload credentials
2026-06-01 21:02:13 +02:00
Jeppe B 8f2533f1b2 Secure lftp release upload credentials 2026-06-01 21:02:01 +02:00
Jeppe B 13b0742deb Merge pull request #60 from copenhagentruckwash/fix-cropping-issue-in-path-editor
Fix self-serve path editor answer switch layout
2026-06-01 20:45:04 +02:00
Jeppe B e2b2d50e5e Fix self-serve path editor answer switch layout 2026-06-01 20:19:35 +02:00
Jeppe B 55d61e37f3 Merge pull request #59 from copenhagentruckwash/fix-e2e-tests-in-i18n-v2-catalog
Fix i18n v2 catalog integrity and split Release Manager source fragments
2026-06-01 19:45:57 +02:00
Jeppe B fa19f5a704 Fix i18n v2 catalog integrity 2026-06-01 19:44:50 +02:00
Jeppe Bundgaard b995031255 Enable "Selv vask" button in MobileFooter.vue 2026-06-01 19:43:57 +02:00
Jeppe Bundgaard d096444ca0 Remove unused lane unavailable warning message from SelfServeLaneStep.vue 2026-06-01 19:41:16 +02:00
Jeppe B 10358d4f53 Merge pull request #58 from copenhagentruckwash/fix-self-serve-path-editor-for-all-departments
Fix self-serve path editor vehicle scope
2026-06-01 18:32:11 +02:00
Jeppe B 78f41b8380 Fix self-serve path editor vehicle scope 2026-06-01 18:31:53 +02:00
Jeppe B 90d4bc7c04 Merge pull request #57 from copenhagentruckwash/fix-bugs-in-start-wash-flow
Harden customer wash start flow
2026-06-01 18:01:29 +02:00
Jeppe B b59c17f120 Harden customer wash start flow 2026-06-01 17:54:42 +02:00
Jeppe Bundgaard c279e7fd38 Merge remote-tracking branch 'origin/master' 2026-06-01 17:23:06 +02:00
Jeppe Bundgaard 8a4fcb3e62 Remove unused return messages and cleanup peer dependency flags in package-lock.json 2026-06-01 17:22:33 +02:00
Jeppe B 680ef7088b Merge pull request #56 from copenhagentruckwash/investigate-playwright-exit-code-1-issue
Stabilize POS email notification E2E setup
2026-06-01 17:20:54 +02:00
Jeppe B 83935af973 Stabilize POS email notification E2E setup 2026-06-01 16:45:40 +02:00
Jeppe B 37973f8fcb Add x-api-coverage happy path flag to listOrders operation 2026-06-01 13:31:48 +02:00
Jeppe B acf19c281a Add email notification actions for booking confirmations and wash certificates 2026-06-01 13:24:29 +02:00
Jeppe B 683fce32da Fix POS step 1 customer lookup loop 2026-06-01 12:41:08 +02:00
Jeppe B 4c9ba7c30d Add unit tests for Playwright full-slice ownership 2026-06-01 12:28:42 +02:00
Jeppe B ca1f3ab477 Merge pull request #55 from copenhagentruckwash/codex/frontend-release-gate-coolify-20260530
Fix frontend release gate for Coolify deploys
2026-05-31 10:22:29 +02:00
Jeppe B 2ab09076f5 Stabilize edge terminal smoke navigation 2026-05-30 21:51:07 +02:00
Jeppe B 97e17fadd4 Respect mobile self-serve studio layout in e2e 2026-05-30 20:33:27 +02:00
Jeppe B ef780e5542 Keep completed self-serve wash step visible 2026-05-30 19:44:52 +02:00
Jeppe B 931c31c7be Stabilize self-serve question loading layout 2026-05-30 18:37:37 +02:00
Jeppe B 25f2be875c Wait for self-serve completion command response 2026-05-30 17:45:06 +02:00
Jeppe B ff70326665 Stabilize frontend PR smoke gates 2026-05-30 16:58:04 +02:00
Jeppe B 52ed49211a Fix frontend release gate for Coolify deploys 2026-05-30 15:39:03 +02:00
Jeppe B 0565fc2a7c Merge pull request #54 from copenhagentruckwash/codex/master-tests-pass-frontend-20260528
[codex] Keep user vehicle actions in wheel menu
2026-05-29 16:33:25 +02:00
Jeppe B d30273cda8 Map user vehicle changes to PR Playwright coverage 2026-05-29 14:46:52 +02:00
Jeppe B b893e69b54 Keep user vehicle actions in wheel menu 2026-05-28 23:35:14 +02:00
Jeppe B eabb12ef97 Merge pull request #53 from copenhagentruckwash/fix-issues-and-verify-with-tests
Format DepartmentSelfServeStudio.vue and update vehicle actions test to reflect wheel menu behavior
2026-05-28 19:52:53 +02:00
Jeppe Bundgaard 225741cf7b Refactor unit tests and clean up Vue component formatting
- Update `self-serve-studio-managed-inspector.spec.js` to improve node kind checks.
- Tidy up and standardize `SessionUser.request` calls in `vehiclesTable.vue`.
- Format code consistently in tables, conditions, and task specs for clarity and maintenance.
2026-05-28 19:47:02 +02:00
Jeppe B dad42da6d9 Keep vehicle actions in wheel menu 2026-05-28 19:01:27 +02:00
Jeppe Bundgaard 0fa2284e89 Add connectivity issue handling logic and tests
- Add `ConnectivityIssue.vue` component for displaying server connection issues.
- Implement polling logic with retry intervals to check server health.
- Update i18n with new connectivity issue messages.
- Add unit tests to cover connectivity failure and recovery scenarios.
- Update test suite for breadth of coverage, including router contract and timer cleanup.
2026-05-28 18:11:21 +02:00
Jeppe Bundgaard cc241e5e19 Add subuser support and require notes for specific products 2026-05-28 16:36:42 +02:00
Jeppe Bundgaard c5a536fbea Add subuser support and require notes for specific products 2026-05-28 16:12:41 +02:00
Jeppe Bundgaard 3a7fef11ef Implement subuser password policy validation and update related components 2026-05-27 19:16:26 +02:00
Jeppe Bundgaard 625757fafc Fix Danish translations and enhance mobile department auto-selection logic 2026-05-27 17:34:51 +02:00
Jeppe Bundgaard e1a8af5a72 Extend i18n with new self-serve prompts and lane availability messages, and add E2E test for lane confirmation logic. 2026-05-27 13:25:31 +02:00
Jeppe Bundgaard c437c89cfa Add release management enhancements, runtime-aware frontend updates, and extensive test coverage
- Implement release update detection, asset preloading, and frontend version management.
- Add unit and E2E tests for release update workflows, widget behavior, and failure scenarios.
- Introduce new services for handling release ping, error reporting, and update installation workflows.
- Extend i18n for release-related components and error report localization.
- Add `ReleaseFrontendVersionBadge.vue` and related styles to display frontend update statuses.
2026-05-27 13:23:22 +02:00
Jeppe Bundgaard 184ef5670c Enhance session management by normalizing session payload and resetting session state on logout. Update login redirect logic and improve runtime API URL handling for selected release channels. 2026-05-26 17:28:21 +02:00
Jeppe Bundgaard 8ab8915429 Add release management components and update routing logic. Introduce keyboard shortcuts, enhance release data grid, and improve asset handling in Nginx configuration. 2026-05-26 14:14:13 +02:00
Jeppe B a952dd2c61 Merge pull request #52
Add endpoint mode options and enhance runtime refresh handling
2026-05-21 14:48:44 +02:00
Jeppe Bundgaard ebcd52a019 Add endpoint mode options, release channel rollback handling, and runtime refresh improvements
- Implement `endpoint_mode` (manual/auto) and related configuration in i18n across multiple locales.
- Add new utility methods for channel rollback and runtime confirmation workflows.
- Update unit and E2E tests to cover runtime refresh behavior, channel switching mechanics, and release references.
- Enhance `SessionUser` and related services to improve error handling during release runtime refresh.
- Add data-test attributes for frontend components to support improved test coverage.
2026-05-21 14:47:46 +02:00
Jeppe Bundgaard b2ddf0db5c Add beta frontend debug display in build info 2026-05-21 11:45:20 +02:00
Jeppe Bundgaard eaf4b965d5 Add runtime support for service sets, release management enhancements, and new unit tests
- Extend `releaseTimeline.js` and related modules for runtime-aware service sets and URLs.
- Update the release session builder to include `service_set` handling.
- Introduce release service definitions, status tones, and enhanced runtime display text.
- Add new dataset mode options, preview rows, and warnings for release configurations in `ConfigurationReleaseManager.vue`.
- Implement extensive utility methods for data service handling, target context, and deployment endpoints.
- Add `vite-api-proxy.spec.js` and extend `release-bootstrap.spec.js` to ensure coverage for new functionality.
2026-05-21 11:41:50 +02:00
Jeppe Bundgaard 6ba1c754a1 Serve frontend releases under channel prefixes 2026-05-20 19:27:01 +02:00
Jeppe Bundgaard 468613ab68 Add Coolify frontend release Docker image 2026-05-20 18:53:17 +02:00
Jeppe Bundgaard 261d84bd01 Route selected release runtime through gateway 2026-05-20 18:32:21 +02:00
Jeppe Bundgaard ea8db593c5 Add release headers service, runtime-aware API header handling, and i18n updates for release configuration
- Implement `releaseHeaders.js` to manage release-related HTTP headers.
- Update requests to include runtime-based release headers.
- Extend i18n phrases across multiple locales for release runtime and configuration labels.
- Enhance unit tests for header building and API availability error handling.
2026-05-20 17:52:54 +02:00
Jeppe Bundgaard 130cc2fc10 Add release runtime bootstrap implementation with unit and E2E tests
- Create `releaseBootstrap.js` service to handle runtime configuration and remote frontend loading.
- Integrate runtime-aware API URL resolution across components and services.
- Update i18n phrases to clarify new domains for `ssl_domain_message`.
- Enhance unit and E2E tests to validate runtime-based frontend loading and fallback behavior.
- Refactor session-related API calls and incorporate runtime-configured base URLs.
2026-05-20 11:26:21 +02:00
Jeppe Bundgaard 3715da8fd3 Add release channel selector components, subject autocomplete, and related tests
- Implement components for `ReleaseChannelSelector`, including panel and sidebar variants.
- Add `ReleaseAssignmentSubjectAutocomplete` for user, subuser, and customer search functionality.
- Update i18n phrases for channel selector and subject assignment.
- Enhance unit and E2E tests for release channel availability, switching, and runtime configuration.
- Modify modules for channel-related state handling and runtime refresh mechanics.
2026-05-20 10:31:49 +02:00
Jeppe Bundgaard 100e477a95 Add "Delete" functionality for release service sets in isolated stacks
- Implement deletion of inactive isolated stacks via API and UI.
- Normalize build pack handling, defaulting frontend to "static".
- Update E2E tests to verify deletion and adjusted UI behavior.
- Refactor related release manager methods and validation logic.
2026-05-19 16:55:20 +02:00
Jeppe Bundgaard 1132c8c256 Merge remote-tracking branch 'origin/master' 2026-05-19 14:47:01 +02:00
Jeppe Bundgaard 42237fcc70 Add unit tests for release manager i18n validation and implement ReleaseReplayInspector Vue component for timeline inspection and replay functionality. 2026-05-19 14:46:42 +02:00
gold-dev ae8b3b6b64 Feat: Hvidovre only one toggler 2026-05-19 14:26:23 +03:00
Jeppe B 8378491b0b Merge pull request #51
Fix chromium mobile runner flakes
2026-05-19 13:12:11 +02:00
Jeppe Bundgaard 20274cf8eb Update i18n phrases for ssl_domain fields to clarify load balancer routing details and add new methods for release service sets and bundles in SuperUser Release Manager. 2026-05-19 13:11:17 +02:00
Jeppe Bundgaard d25726fafd Add unit and E2E tests for release channel availability and switched-notice functionality. Configure runtime mocks and implement Vue components for channel-specific handling. 2026-05-19 12:05:25 +02:00
Jeppe Bundgaard 4f1973d048 Add SuperUser Release Manager view with channel management, deployments, replay, and integrations 2026-05-19 10:04:00 +02:00
Jeppe Bundgaard d69c474655 Add multilingual glossary and phrases for i18n, including terms, attributes, and compatibility across da, de, en, no, and sv locales. 2026-05-19 08:03:39 +02:00
Jeppe Bundgaard 56078307da Update vehicles table action visibility and adjust E2E test timeouts and MinIO configuration
- Change `displayActionsDirectly` prop to `false` in vehicles table for compact mode consistency.
- Refactor E2E tests with reusable `PAGE_READY_TIMEOUT` constant to prevent flaky behavior.
- Enhance MinIO configuration in replication management and system status tests to include additional environment variables and form fields.
2026-05-18 15:07:25 +02:00
Jeppe Bundgaard e6291cdb8a Add response handling for MinIO provisioning in replication E2E test 2026-05-18 13:52:01 +02:00
Jeppe Bundgaard bf0b2152c5 Enhance replication management to support MinIO alongside database and Redis, update UI labels, and improve status reporting 2026-05-18 13:48:16 +02:00
Jeppe Bundgaard 9771723992 Fix chromium mobile runner flakes 2026-05-18 12:28:23 +02:00
Jeppe Bundgaard 156e50a864 Merge remote-tracking branch 'origin/master' 2026-05-18 11:01:12 +02:00
Jeppe Bundgaard 5081fe0776 Add Playwright PR runner and Vitest unit test fast runner scripts; integrate with test commands and update specs/tests for PR-based selection. 2026-05-18 10:59:56 +02:00
Jeppe Bundgaard 656e43d449 Add SuperUser replication management view, including UI for database and Redis hosts, replication status, provisioning, and Compose template generation 2026-05-18 09:55:41 +02:00
Jeppe Bundgaard d135a3f0af Add database and Redis replication support to System Status Dashboard, including UI updates, new routes, localization, and unit/E2E tests. 2026-05-17 15:32:45 +02:00
gold-dev 7bd880497f Feat: Disable click redirect, setup checkbox counts 2026-05-13 22:15:28 +03:00
Jeppe Bundgaard d148c0d6b9 Enhance ActionSettingsWheelButton functionality with fixed positioning logic, ResizeObserver integration, and dropdown layout syncing; refactor unit tests for improved mock structure. 2026-05-13 17:36:15 +02:00
Jeppe Bundgaard 75bab7b425 Update UI styles and logic for ActionSettingsWheelButton, invoicing period components, and desktop section layout, adding flyout positioning logic and syncing. 2026-05-13 16:47:31 +02:00
Jeppe Bundgaard 8dfaebba00 Add unit test for user vehicle actions visibility and enhancements for department 75 loading UI, default department handling, and translations. 2026-05-13 15:35:21 +02:00
Jeppe Bundgaard f7f01dcd2f Add E2E and unit tests for invoicing period flag handling, enhance duplicate groups filtering by date, and implement Excel export for department 75 distribution 2026-05-13 13:58:00 +02:00
Jeppe Bundgaard fba85a72c1 Refactor pagination components to use provide and usePaginatedList, optimize fast link handling with queuing and concurrency limits, and update XLVask content generation logic and caching. 2026-05-12 15:03:03 +02:00
Jeppe Bundgaard c657ec57ad Refactor DatePeriodSelector and invoicing period tests: adjust visibility condition for mobile update button, remove redundant test skips, and enhance logic for date shortcut handling across layouts. 2026-05-12 05:38:50 +02:00
Jeppe Bundgaard f24df440d0 Enhance ActionSettingsWheelButton and invoicing period tests with improved structure and visibility checks 2026-05-12 04:52:05 +02:00
Jeppe Bundgaard b73f83a4ab Clean up debug logs in invoicing statistics, add isDateLoading state for period paging, update date change handling with refresh logic, and enhance locale translations and tests. 2026-05-12 04:03:36 +02:00
Jeppe Bundgaard 2700ff3526 Add caching, component updates, and unit tests for invoicing period and XLVask functionality 2026-05-12 03:41:40 +02:00
Jeppe Bundgaard a261f2f3bd Add unit tests and components for invoicing period flag handling and self-wash view rendering 2026-05-11 21:40:51 +02:00
Jeppe Bundgaard 81c4ba0800 Refine safety seal normalization, enhance booking completion flow, and update E2E tests for improved validation and popup handling. 2026-05-11 04:37:34 +02:00
Jeppe Bundgaard 31e72af9bf Warm smoke route chunks before Playwright runs 2026-05-11 03:47:05 +02:00
Jeppe Bundgaard 10a52521d5 Fix invoicing period invoice action 2026-05-11 03:35:36 +02:00
Jeppe Bundgaard c9b34ba36c Fix frontend smoke test races 2026-05-11 03:15:56 +02:00
Jeppe Bundgaard 87ad8f9d31 Merge remote-tracking branch 'origin/master' 2026-05-11 01:57:06 +02:00
Jeppe Bundgaard a754cfc7bb Add delay support for booking completion flow, refine safety seal handling, and enhance popup submission logic with better action management and validation. Update related E2E tests. 2026-05-11 01:56:15 +02:00
gold-dev 29bc0e548b Feat: Header & Daily report block switcher 2026-05-07 16:39:11 +03:00
Jeppe Bundgaard 378598b161 Restrict unassigned department visibility test to desktop, refactor test selectors, and simplify department loading logic. 2026-05-07 14:12:17 +02:00
Jeppe Bundgaard e8c1dda3cc Add E2E test for unassigned department visibility in the overview list. Refactor department sorting and UI logic with priority-based filtering for improved accessibility. 2026-05-07 13:54:53 +02:00
Jeppe Bundgaard fabc4ad54a Refactor department access logic to use canAccessAssignedDepartment. Update related components, tests, and UI for consistency. Add tests for unassigned department handling and align E2E layouts. 2026-05-07 13:50:12 +02:00
Jeppe Bundgaard b89ae2bb9e Sort departments by priority order in UI and API logic. Add support for "order_priority" and normalize fields consistently throughout components and tests. 2026-05-07 13:22:07 +02:00
Jeppe Bundgaard fb162eef6c Add archived department filtering feature for superusers, including E2E tests, UI updates, and OpenAPI enhancements. Extend department table with archived toggle switch and update translations. 2026-05-07 12:54:31 +02:00
Jeppe Bundgaard dc2840e023 Merge remote-tracking branch 'origin/master' 2026-05-07 10:43:02 +02:00
Jeppe Bundgaard fe6d9e8d16 Add E2E tests for department branding management and refine supporting API mocks. Implement branding creation, editing, assignment, and clearing workflows. Extend DepartmentProfile with branding UI and logic updates. 2026-05-07 10:42:45 +02:00
Jeppe Bundgaard 7ab7d83fb5 Align invoicing period specs with formatted date contract 2026-05-07 10:15:49 +02:00
gold-dev 028dad2183 Feat: Time ago to local time hours 2026-05-06 17:08:57 +03:00
gold-dev 6b0fe60958 Feat: Change texts 2026-05-06 16:49:06 +03:00
Jeppe Bundgaard a7789cb631 Merge remote-tracking branch 'origin/master' 2026-05-06 14:15:01 +02:00
Jeppe Bundgaard da65876a64 Add responsive tests for "/qr/new-customer" layout, extend API with split_by_month logic, and update E2E/admin tests with session mocking and department sync fixes. Normalize POS department handling, improve invoicing period split logic, and enhance receipt export safety. 2026-05-06 14:14:34 +02:00
Jeppe Bundgaard f245161bfa Align stale unit specs with current self-serve and invoices views 2026-05-06 09:08:23 +02:00
Jeppe Bundgaard bdffdca420 Handle "STOP" command failures by interpreting "lane not occupied" as a safe-to-clear state. Refactor error handling, add clearActiveWashState, and extend unit/E2E tests with this scenario. 2026-04-30 14:52:40 +02:00
Jeppe Bundgaard 5d1199a133 Update unit test to include lane click event trigger 2026-04-29 17:43:29 +02:00
Jeppe Bundgaard 4752b3d096 Add self-serve feature logic, including lane filtering, normalization, and OpenAPI updates. Enhance Self-Serve Studio with progress indicators, UI controls, path projection streaming, and abort handling. Update E2E and unit tests for enhanced self-serve behaviors. 2026-04-29 17:27:53 +02:00
Jeppe Bundgaard fc9c3700a1 Add Danish self-serve labels and guided wash flow steps. Update UI with guided step actions and bottom control integration. Fix translations and refine E2E/unit test coverage for self-serve flow behavior. 2026-04-29 16:01:34 +02:00
Jeppe Bundgaard a15d6841c5 Add task attachment management to Self-Serve Studio. Extend graph, path outcomes, and flow logic. Update E2E/unit tests with attachment creation, upload, download, deletion flows, and related validations. 2026-04-29 14:41:29 +02:00
Jeppe Bundgaard 9641f67e3a Extend Self-Serve Studio with "action" nodes, supporting creation, focus, and editing workflows. Add unit tests, normalize forms, and enhance virtual binding/gateway logic. Update E2E tests and node visualization settings. 2026-04-29 10:56:40 +02:00
Jeppe Bundgaard de2fa57c63 Add audit issue navigation and task-button normalization to Self-Serve Studio. Implement node ID resolution, audit visualization logic, and reusable task button helpers. Extend E2E and unit tests. 2026-04-29 09:51:09 +02:00
Jeppe Bundgaard a658fe7863 Resume active self-serve washes for authenticated customers across devices. Update OpenAPI, UI components, and extend E2E/unit tests. 2026-04-28 18:00:09 +02:00
Jeppe Bundgaard 503fb84f10 Extend Self-Serve Studio with deferred relay logic and Bookinger-style loading states. Update OpenAPI, UI components, and related E2E/unit tests. 2026-04-28 17:09:50 +02:00
Jeppe Bundgaard 11bb8f824d Extend Self-Serve Studio with vehicle type management, mutation options, and enhanced simulation/debug tools. Update OpenAPI, UI components, and related E2E/unit tests. 2026-04-28 16:54:27 +02:00
Jeppe Bundgaard 8615cc6678 Introduce task-service logic and relay coverage to Self-Serve Studio. Add service normalization, binding service visualizations, and task-service coverage mapping. Extend E2E tests for tasks and bindings. 2026-04-28 15:42:06 +02:00
Jeppe Bundgaard 65e4c27f45 Enhance Self-Serve Studio with dynamic filters, scoped layouts, and optimized auto-layout logic. Update filtering behavior, node visibility, and edge handling. Extend E2E tests for vehicle-specific scenarios. 2026-04-28 15:01:30 +02:00
Jeppe Bundgaard f9d8b87768 Add new table and enhance studio layout logic
Introduce `department_selfserve_studio_layouts` table for department-specific layouts and implement advanced auto-layout functionality in the DepartmentSelfServeStudio module. Added custom node definitions, updated styling, and integrated new logics for sorting and visualizing nodes in the Vue Flow interface.
2026-04-28 14:21:26 +02:00
Jeppe Bundgaard 586a0cc86d Add fullscreen mode to Self-Serve Studio with improved canvas controls and responsive layout adjustments. Extend related E2E tests. 2026-04-28 13:46:47 +02:00
Jeppe Bundgaard e9cbb40559 Add all-in-one self-serve studio with flow editor, simulator, versioning, and enhanced E2E/OpenAPI support. Extend legacy edge gateway workflows. 2026-04-28 13:36:16 +02:00
Jeppe Bundgaard f2c3962749 Improve error handling for lane service updates and add comprehensive tests for edge gateway failures. Refactor route handling and enhance lane/session binding logic. 2026-04-28 12:29:56 +02:00
Jeppe Bundgaard e63e113628 Load EdgeGateway components asynchronously in SuperUserDashboard self-serve view to optimize performance. 2026-04-28 11:37:35 +02:00
Jeppe Bundgaard 4b02de7358 Add self-serve Edge Agents management with navigation, routes, and tabs. Update legacy redirects and extend unit and E2E tests. 2026-04-28 11:32:23 +02:00
Jeppe Bundgaard 61aab76e63 Add self-serve sessions management with filtering, force-stop functionality, and enhanced E2E test coverage. Extend edge gateway module configuration tests. 2026-04-28 10:03:36 +02:00
Jeppe Bundgaard e26e37d9d7 Add enhanced admin access checks and error handling for draft transaction customers and self-serve processes. Extend edge gateway diagnostics, refine terminal readiness detection, and improve live session test coverage. 2026-04-28 09:01:52 +02:00
Jeppe Bundgaard d411da2a12 Enhance edge gateway live sessions, improve WebSocket and error diagnostics, optimize lane relay testing, and extend terminal and command handling features. 2026-04-27 17:39:24 +02:00
Jeppe Bundgaard 15b3c726dc Add relay test modes (local/cloud), enhance lane and self-serve relay testing, and refine UI/UX for department hardware workspace. 2026-04-27 16:25:18 +02:00
Jeppe Bundgaard ee47c801ea Enhance edge gateway live session handling, extend telemetry updates, refine department lane bindings, and replace legacy invoice components. 2026-04-27 14:14:52 +02:00
Jeppe B d0993d2963 Merge pull request #50 from copenhagentruckwash/codex/github-actions-green-20260424-local
Stabilize frontend unit test environments
2026-04-24 21:09:18 +02:00
2625 changed files with 483579 additions and 149534 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(find /c/Users/2jepp/WebstormProjects/pleno-vue -type f \\\\\\(-name *.ts -o -name *.js \\\\\\) -not -path */node_modules* -not -path */dist/* -not -path */.git/* -not -path */dev-dist/*)",
"Bash(find /c/Users/2jepp/WebstormProjects/pleno-vue/src -type f \\\\\\(-name *.ts -o -name *.js -o -name *.vue \\\\\\))",
"Bash(find /c/Users/2jepp/WebstormProjects/pleno-vue/app -type f \\\\\\(-name *.ts -o -name *.js -o -name *.java -o -name *.kt \\\\\\))",
"Bash(grep -r \"getAutomaticFlags\\\\|AutomaticFlags\\\\|automaticFlags\\\\|Automatic.*Flags\" /c/Users/2jepp/WebstormProjects/pleno-vue --include=*.ts --include=*.js --include=*.java --include=*.kt --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=dev-dist --exclude-dir=output --exclude-dir=.git)",
"Bash(xargs grep:*)"
]
}
}
+17
View File
@@ -0,0 +1,17 @@
.github
.idea
.vscode
coverage
dev-dist
dist
node_modules
node_modules.*
output
playwright-report
test-results
.ai
.ai-workflow
.claude
.codex
.gradle
gradle
+15
View File
@@ -0,0 +1,15 @@
* text=auto eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary
*.pdf binary
*.zip binary
*.webm binary
+58
View File
@@ -0,0 +1,58 @@
# Default branch protection
The intended repository ruleset is stored in
[`rulesets/protect-default-branch.json`](rulesets/protect-default-branch.json).
It targets the configured default branch and requires pull requests, the strict
`Required CI` check from GitHub Actions, resolved review conversations,
squash-only merges, and linear history. Branch deletion and force pushes are
blocked. Qodana remains advisory and is not part of the required gate.
The ruleset's `RepositoryRole` actor ID `5` is GitHub's built-in Administrator
role. Its `pull_request` bypass mode permits an administrator to bypass rules
only while merging an existing pull request; it does not permit a direct push.
## Repository settings
Keep squash merge enabled and disable merge commits and rebase merge. Enable
auto-merge, the update-branch option, and automatic deletion of merged head
branches. Keep the Actions token read-only and do not allow Actions to approve
pull-request reviews.
## Activation and verification
1. Confirm a pull request and a `master` push each produce exactly one
successful `Required CI` check from GitHub Actions integration `15368`.
2. For the initial ruleset POST, override the committed JSON's `enforcement`
value to `disabled`, then compare GitHub's normalized API response with this
file.
3. PUT the exact committed JSON to the inspected ruleset to activate it.
4. Open a canary pull request and confirm that pending or failing CI, unresolved
conversations, and an out-of-date branch block merging; only squash merge is
available.
5. After merging, confirm the head branch is deleted and the post-merge full
E2E, frontend release, and mobile release guards still run.
If validation exposes a blocker, disable the ruleset rather than deleting it so
its configuration and history remain available.
## Activation record
Repository ruleset `19051697` was activated on 2026-07-16 after preparation
PR #172 established `Required CI`, remediation PR #174 passed the strict
current-base gate, and merged master run `29501928124` completed with all 43
executed jobs successful. This documentation update is the after-activation
canary for the normal protected pull-request path.
## Normal publishing flow
Create a scoped feature branch, open a pull request to `master`, wait for
`Required CI`, update the branch if `master` advanced, resolve every review
conversation, and squash-merge. For waits expected to exceed 90 seconds, use
the workspace `scripts/ci-watch.sh` helper instead of repeatedly polling GitHub.
## Break glass
For an incident, an administrator must still open a pull request. Document the
incident and why the normal gate cannot complete, then use the PR-only bypass
when merging. Monitor all post-merge workflows and open a follow-up pull request
for any validation or remediation deferred during the incident.
+21
View File
@@ -0,0 +1,21 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
time: "07:00"
timezone: Europe/Copenhagen
open-pull-requests-limit: 5
labels: [dependencies, ci]
- package-ecosystem: bundler
directory: /
schedule:
interval: weekly
day: monday
time: "07:15"
timezone: Europe/Copenhagen
open-pull-requests-limit: 3
labels: [dependencies, ios]
@@ -0,0 +1,53 @@
{
"name": "Protect default branch",
"target": "branch",
"enforcement": "active",
"bypass_actors": [
{
"actor_id": 5,
"actor_type": "RepositoryRole",
"bypass_mode": "pull_request"
}
],
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH"],
"exclude": []
}
},
"rules": [
{
"type": "deletion"
},
{
"type": "non_fast_forward"
},
{
"type": "required_linear_history"
},
{
"type": "pull_request",
"parameters": {
"allowed_merge_methods": ["squash"],
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_approving_review_count": 0,
"required_review_thread_resolution": true
}
},
{
"type": "required_status_checks",
"parameters": {
"do_not_enforce_on_create": false,
"required_status_checks": [
{
"context": "Required CI",
"integration_id": 15368
}
],
"strict_required_status_checks_policy": true
}
}
]
}
+88
View File
@@ -0,0 +1,88 @@
name: App Store Readiness
on:
pull_request:
push:
branches: [master]
paths:
- "fastlane/**"
- "ios/**"
- "scripts/mobile/**"
- "tests/node/app-store-connect.test.mjs"
- ".github/workflows/app-store-readiness.yml"
- "Gemfile*"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: app-store-readiness-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
validate:
name: App Store Readiness
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Setup Ruby
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.3"
- name: Resolve the pinned Fastlane dependency graph
run: bundle lock
- name: Check the committed Fastlane dependency lock
id: fastlane-lock
continue-on-error: true
run: test -z "$(git status --porcelain -- Gemfile.lock)"
- name: Preserve a generated lock for review
if: steps.fastlane-lock.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: generated-fastlane-lock
path: Gemfile.lock
if-no-files-found: error
retention-days: 1
- name: Require a current committed Fastlane dependency lock
if: steps.fastlane-lock.outcome == 'failure'
run: |
echo 'Gemfile.lock is missing or stale. Download generated-fastlane-lock and commit it.' >&2
exit 1
- name: Install the pinned Fastlane dependency graph
run: bundle install --jobs 4 --retry 3
- name: Validate strict App Store metadata and candidate assets
run: node scripts/mobile/validate-app-store.mjs --strict
- name: Validate native mobile permissions
run: node scripts/mobile/check-permissions.mjs
- name: Test App Store Connect automation
run: node --test tests/node/app-store-connect.test.mjs
- name: Validate Fastlane configuration
run: bundle exec fastlane lanes
- name: Validate JavaScript syntax
run: |
node --check scripts/mobile/validate-app-store.mjs
node --check scripts/mobile/app-store-connect.mjs
node --check scripts/mobile/create-ios-release-manifest.mjs
node --check tests/node/app-store-connect.test.mjs
node scripts/mobile/app-store-connect.mjs self-test-jwt
-28
View File
@@ -1,28 +0,0 @@
name: Qodana Configuration Upload
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
upload-qodana-config:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Run Qodana Configuration Uploader
env:
QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }}
run: |
docker run --rm \
-v $(pwd):/workspace \
-w /workspace \
-e QODANA_CONFIGURATIONS_TOKEN=$QODANA_CONFIGURATIONS_TOKEN \
jetbrains/qodana-configuration-uploader:latest \
--global-configs-file qodana-global-configurations.yaml \
--qodana-host https://qodana.cloud
+55 -17
View File
@@ -1,28 +1,66 @@
name: Qodana Configuration Upload
name: Qodana
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
workflow_dispatch:
pull_request:
branches: [master, beta, canary, internal]
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [master, beta, canary, internal]
permissions:
contents: read
checks: write
pull-requests: write
concurrency:
group: qodana-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref }}
cancel-in-progress: true
jobs:
upload-qodana-config:
runs-on: ubuntu-latest
qodana:
name: Qodana
if: >-
github.event_name != 'pull_request' ||
(
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login != 'dependabot[bot]'
)
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@v5
# v5.0.1
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Run Qodana Configuration Uploader
- name: Require Qodana project token
shell: bash
env:
QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }}
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
run: |
docker run --rm \
-v $(pwd):/workspace \
-w /workspace \
-e QODANA_CONFIGURATIONS_TOKEN=$QODANA_CONFIGURATIONS_TOKEN \
jetbrains/qodana-configuration-uploader:latest \
--global-configs-file qodana-global-configurations.yaml \
--qodana-host https://qodana.cloud
set -euo pipefail
if [[ -z "${QODANA_TOKEN:-}" ]]; then
echo "::error::QODANA_TOKEN is not configured for this repository."
exit 1
fi
- name: Qodana
# v2026.1.3
uses: JetBrains/qodana-action@4861e015da555e86a72b862892aba6c2b93e6891
with:
use-caches: true
cache-default-branch-only: true
upload-result: false
use-annotations: true
pr-mode: ${{ github.event_name == 'pull_request' }}
post-pr-comment: true
github-token: ${{ github.token }}
push-fixes: none
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
@@ -0,0 +1,190 @@
name: iOS App Store Candidate
on:
push:
tags: ["ios-v*"]
permissions:
contents: read
actions: read
concurrency:
group: ios-app-store-candidate
cancel-in-progress: false
jobs:
resolve:
name: Resolve exact tested build
runs-on: ubuntu-24.04
timeout-minutes: 15
outputs:
enabled: ${{ steps.resolve.outputs.enabled }}
source_sha: ${{ steps.resolve.outputs.source_sha }}
version: ${{ steps.resolve.outputs.version }}
build_number: ${{ steps.manifest.outputs.build_number }}
app_store_build_id: ${{ steps.manifest.outputs.app_store_build_id }}
steps:
- name: Checkout tagged source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Validate protected tag and release version
id: resolve
shell: bash
env:
AUTOMATION_ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
run: |
set -euo pipefail
[[ "$GITHUB_REF_NAME" =~ ^ios-v([0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo "Tag must be ios-vX.Y.Z." >&2; exit 1; }
version="${BASH_REMATCH[1]}"
source_sha="$(git rev-parse HEAD)"
manifest_version="$(node -p "JSON.parse(require('fs').readFileSync('ios/release.json')).marketingVersion")"
[[ "$version" == "$manifest_version" ]] || { echo "Tag version $version does not match ios/release.json $manifest_version." >&2; exit 1; }
git show-ref --verify --quiet refs/remotes/origin/master || { echo "origin/master was not included in the full checkout." >&2; exit 1; }
git merge-base --is-ancestor "$source_sha" origin/master || { echo "Tagged commit is not reachable from master." >&2; exit 1; }
enabled=false
[[ "$AUTOMATION_ENABLED" == true ]] && enabled=true
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
if [[ "$enabled" != true ]]; then
echo "### Candidate promotion safely disabled" >> "$GITHUB_STEP_SUMMARY"
echo 'No App Store environment or credentials were accessed. Enable only after the signed canary.' >> "$GITHUB_STEP_SUMMARY"
fi
- name: Download exact TestFlight release manifest
if: steps.resolve.outputs.enabled == 'true'
id: manifest
shell: bash
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ steps.resolve.outputs.source_sha }}
EXPECTED_VERSION: ${{ steps.resolve.outputs.version }}
run: |
set -euo pipefail
artifact_name="ios-release-manifest-$SOURCE_SHA"
response="$RUNNER_TEMP/ios-artifacts.json"
curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts?name=$artifact_name&per_page=100" > "$response"
artifact_id="$(jq -r --arg sha "$SOURCE_SHA" '[.artifacts[] | select(.expired == false) | select(.workflow_run.head_sha == $sha)] | sort_by(.created_at) | last | .id // empty' "$response")"
[[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "No successful TestFlight release manifest exists for $SOURCE_SHA." >&2; exit 1; }
mkdir -p output/candidate
curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" -o "$RUNNER_TEMP/manifest.zip"
unzip -q "$RUNNER_TEMP/manifest.zip" -d output/candidate
MANIFEST=output/candidate/ios-release-manifest.json node <<'NODE'
const fs = require("node:fs");
const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST, "utf8"));
const checks = {
schema: manifest.schemaVersion === 1,
repository: manifest.repository === process.env.GITHUB_REPOSITORY,
source: manifest.sourceSha === process.env.SOURCE_SHA,
version: manifest.marketingVersion === process.env.EXPECTED_VERSION,
bundle: manifest.bundleId === "io.truckwash.app",
build: /^[1-9][0-9]*$/.test(manifest.buildNumber),
appStoreBuild: typeof manifest.appStoreBuildId === "string" && manifest.appStoreBuildId.length > 0,
};
const failed = Object.entries(checks).filter(([, ok]) => !ok).map(([name]) => name);
if (failed.length) throw new Error(`Invalid iOS release manifest: ${failed.join(", ")}`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `build_number=${manifest.buildNumber}\napp_store_build_id=${manifest.appStoreBuildId}\n`);
NODE
promote:
name: Sync and verify App Store candidate
needs: resolve
if: needs.resolve.outputs.enabled == 'true'
runs-on: macos-15
timeout-minutes: 60
environment: app-store-candidate
env:
IOS_SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }}
IOS_MARKETING_VERSION: ${{ needs.resolve.outputs.version }}
IOS_BUILD_NUMBER: ${{ needs.resolve.outputs.build_number }}
EXPECTED_APP_STORE_BUILD_ID: ${{ needs.resolve.outputs.app_store_build_id }}
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
steps:
- name: Checkout exact candidate source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.IOS_SOURCE_SHA }}
persist-credentials: false
- name: Setup Ruby and pinned Fastlane
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Validate complete candidate storefront
run: node scripts/mobile/validate-app-store.mjs --strict
- name: Verify public storefront URLs
shell: bash
run: |
set -euo pipefail
for file in support_url privacy_url marketing_url; do
url="$(tr -d '\r\n' < "fastlane/metadata/da-DK/$file.txt")"
curl --fail --silent --show-error --location --connect-timeout 10 --max-time 30 --output /dev/null "$url"
done
- name: Verify exact processed TestFlight build
run: node scripts/mobile/app-store-connect.mjs verify-candidate
- name: Sync metadata and screenshots without App Review submission
run: bundle exec fastlane ios prepare_candidate
- name: Configure automatic release after approval
run: node scripts/mobile/app-store-connect.mjs configure-release-policy
- name: Read back exact App Store candidate
id: readback
run: node scripts/mobile/app-store-connect.mjs verify-store-version
- name: Verify Denmark-only availability and no preorder
id: availability
run: node scripts/mobile/app-store-connect.mjs verify-availability
- name: Write candidate handoff
env:
APP_STORE_STATE: ${{ steps.readback.outputs.app_store_state }}
APP_STORE_VERSION_ID: ${{ steps.readback.outputs.app_store_version_id }}
RELEASE_TYPE: ${{ steps.readback.outputs.release_type }}
AVAILABLE_TERRITORIES: ${{ steps.availability.outputs.available_territories }}
run: |
echo "### iOS $IOS_MARKETING_VERSION candidate prepared" >> "$GITHUB_STEP_SUMMARY"
echo "- Source: \`$IOS_SOURCE_SHA\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Exact tested build: \`$IOS_BUILD_NUMBER\` (\`$EXPECTED_APP_STORE_BUILD_ID\`)" >> "$GITHUB_STEP_SUMMARY"
echo "- App Store state: \`$APP_STORE_STATE\`" >> "$GITHUB_STEP_SUMMARY"
echo "- App Store version ID: \`$APP_STORE_VERSION_ID\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Release policy: \`$RELEASE_TYPE\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Availability: \`$AVAILABLE_TERRITORIES\` only; preorder disabled" >> "$GITHUB_STEP_SUMMARY"
echo "- [Open the app in App Store Connect](https://appstoreconnect.apple.com/apps/$APP_STORE_CONNECT_APP_ID/appstore)" >> "$GITHUB_STEP_SUMMARY"
echo "- App Review submission remains manual; Apple will release automatically after approval." >> "$GITHUB_STEP_SUMMARY"
disabled:
name: Promotion disabled
needs: resolve
if: needs.resolve.outputs.enabled != 'true'
runs-on: ubuntu-24.04
steps:
- run: echo "App Store candidate promotion is disabled; no environment or credentials were accessed."
+117
View File
@@ -0,0 +1,117 @@
name: iOS Credential Health
on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ios-credential-health
cancel-in-progress: true
jobs:
gate:
runs-on: ubuntu-24.04
outputs:
enabled: ${{ steps.gate.outputs.enabled }}
steps:
- id: gate
env:
ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
run: |
enabled=false
[[ "$ENABLED" == true ]] && enabled=true
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
if [[ "$enabled" != true ]]; then
echo "App Store automation is disabled; credential health did not access its environment." >> "$GITHUB_STEP_SUMMARY"
fi
validate:
needs: gate
if: needs.gate.outputs.enabled == 'true'
runs-on: macos-15
timeout-minutes: 15
environment: app-store-signing
env:
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Validate API key and app access
run: node scripts/mobile/app-store-connect.mjs verify-credentials
- name: Validate certificate and profile identity and expiry
shell: bash
env:
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
run: |
set -euo pipefail
cert_p12="$RUNNER_TEMP/distribution.p12"
cert_pem="$RUNNER_TEMP/distribution.pem"
cert_der="$RUNNER_TEMP/distribution.der"
profile="$RUNNER_TEMP/distribution.mobileprovision"
profile_plist="$RUNNER_TEMP/distribution-profile.plist"
keychain="$RUNNER_TEMP/credential-health.keychain-db"
keychain_password="$(openssl rand -hex 24)"
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64.replace(/\\s/g,''),'base64'))" "$cert_p12"
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_APP_STORE_PROFILE_BASE64.replace(/\\s/g,''),'base64'))" "$profile"
chmod 600 "$cert_p12" "$profile"
security create-keychain -p "$keychain_password" "$keychain"
security unlock-keychain -p "$keychain_password" "$keychain"
security import "$cert_p12" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain"
security list-keychains -d user -s "$keychain"
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null
security find-identity -v -p codesigning "$keychain" | grep -q 'Apple Distribution' || {
echo "Distribution P12 does not contain a usable private signing identity." >&2
exit 1
}
openssl pkcs12 -in "$cert_p12" -clcerts -nokeys -passin env:IOS_DISTRIBUTION_CERTIFICATE_PASSWORD -out "$cert_pem"
openssl x509 -in "$cert_pem" -noout -subject -issuer -dates
openssl x509 -in "$cert_pem" -checkend 2592000 -noout || { echo "Distribution certificate expires within 30 days." >&2; exit 1; }
openssl x509 -in "$cert_pem" -outform DER -out "$cert_der"
security cms -D -i "$profile" > "$profile_plist"
CERT_DER="$cert_der" PROFILE_PLIST="$profile_plist" python3 <<'PY'
import datetime, hashlib, os, plistlib, sys
with open(os.environ["PROFILE_PLIST"], "rb") as handle: profile = plistlib.load(handle)
with open(os.environ["CERT_DER"], "rb") as handle: cert_sha = hashlib.sha1(handle.read()).hexdigest().upper()
expiration = profile.get("ExpirationDate")
if expiration and expiration.tzinfo is None: expiration = expiration.replace(tzinfo=datetime.timezone.utc)
warning = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30)
ent = profile.get("Entitlements", {})
checks = {
"team": os.environ["APPLE_TEAM_ID"] in profile.get("TeamIdentifier", []),
"bundle": ent.get("application-identifier") == f'{os.environ["APPLE_TEAM_ID"]}.{os.environ["IOS_BUNDLE_ID"]}',
"distribution": ent.get("get-task-allow") is False and not profile.get("ProvisionedDevices"),
"profile expiry beyond 30 days": expiration is not None and expiration > warning,
"certificate belongs to profile": cert_sha in {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])},
}
failed = [name for name, ok in checks.items() if not ok]
if failed:
print("Credential health failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
sys.exit(1)
print(f"Provisioning profile is healthy through {expiration.isoformat()}.")
PY
- name: Clean temporary credential files
if: always()
run: |
security delete-keychain "$RUNNER_TEMP/credential-health.keychain-db" 2>/dev/null || true
rm -f "$RUNNER_TEMP"/distribution.{p12,pem,der,mobileprovision} "$RUNNER_TEMP/distribution-profile.plist"
+624
View File
@@ -0,0 +1,624 @@
name: iOS Device Debug IPA
on:
workflow_dispatch:
inputs:
source_ref:
description: Same-repository branch, tag, or commit to build
required: true
default: master
type: string
expected_sha:
description: Full 40-character SHA that source_ref must resolve to
required: true
type: string
confirmation:
description: Type SIGN IOS DEBUG IPA
required: true
type: string
permissions:
contents: read
concurrency:
group: ios-device-debug-${{ github.run_id }}
cancel-in-progress: false
jobs:
resolve:
name: Resolve and verify source
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
source_sha: ${{ steps.resolve.outputs.source_sha }}
steps:
- name: Validate dispatch confirmation
shell: bash
env:
CONFIRMATION: ${{ inputs.confirmation }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
SOURCE_REF: ${{ inputs.source_ref }}
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "$WORKFLOW_REF" != "refs/heads/master" ]]; then
echo "The signing workflow must be dispatched from the master workflow ref" >&2
exit 1
fi
if [[ "$CONFIRMATION" != "SIGN IOS DEBUG IPA" ]]; then
echo "confirmation must exactly match SIGN IOS DEBUG IPA" >&2
exit 1
fi
if [[ ! "$EXPECTED_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "expected_sha must be a full 40-character commit SHA" >&2
exit 1
fi
if [[ -z "$SOURCE_REF" || "$SOURCE_REF" =~ [[:space:]] ]]; then
echo "source_ref must be non-empty and contain no whitespace" >&2
exit 1
fi
if [[ "$SOURCE_REF" == refs/pull/* || "$SOURCE_REF" == pull/* ]]; then
echo "Pull-request refs are not eligible for device-debug signing" >&2
exit 1
fi
- name: Checkout same-repository history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve immutable commit
id: resolve
shell: bash
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
SOURCE_REF: ${{ inputs.source_ref }}
run: |
set -euo pipefail
expected_sha="${EXPECTED_SHA,,}"
if [[ "$SOURCE_REF" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
candidate="$SOURCE_REF"
elif [[ "$SOURCE_REF" == refs/heads/* ]]; then
candidate="refs/remotes/origin/${SOURCE_REF#refs/heads/}"
elif [[ "$SOURCE_REF" == refs/tags/* ]]; then
candidate="$SOURCE_REF"
elif git show-ref --verify --quiet "refs/remotes/origin/$SOURCE_REF"; then
candidate="refs/remotes/origin/$SOURCE_REF"
elif git show-ref --verify --quiet "refs/tags/$SOURCE_REF"; then
candidate="refs/tags/$SOURCE_REF"
else
echo "source_ref does not identify a same-repository branch, tag, or fetched commit" >&2
exit 1
fi
source_sha="$(git rev-parse --verify "${candidate}^{commit}" 2>/dev/null || true)"
source_sha="${source_sha,,}"
if [[ ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "source_ref could not be resolved to a commit" >&2
exit 1
fi
if [[ "$source_sha" != "$expected_sha" ]]; then
echo "source_ref resolved to a SHA different from expected_sha" >&2
exit 1
fi
reachable=false
while IFS= read -r repository_ref; do
if git merge-base --is-ancestor "$source_sha" "$repository_ref" 2>/dev/null; then
reachable=true
break
fi
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin refs/tags)
if [[ "$reachable" != true ]]; then
echo "The requested commit is not reachable from a same-repository branch or tag" >&2
exit 1
fi
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "Resolved source_ref to $source_sha" >> "$GITHUB_STEP_SUMMARY"
build:
name: Build development-signed IPA
needs: resolve
runs-on: macos-26
timeout-minutes: 90
environment:
name: mobile-device-debug
env:
IOS_PROJECT_PATH: ios/App/App.xcodeproj
IOS_SCHEME: App
IOS_DEBUG_BUNDLE_ID: ${{ vars.IOS_DEBUG_BUNDLE_ID || 'io.truckwash.app.debug' }}
IOS_DEBUG_API_URL: ${{ vars.IOS_DEBUG_API_URL || 'https://api-v2.truckwash.io/master/api' }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
MOBILE_VERSION_NAME: 0.0.${{ github.run_number }}
RESOLVED_SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }}
steps:
- name: Checkout resolved source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.resolve.outputs.source_sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify runner and resolve build number
shell: bash
run: |
set -euo pipefail
xcode_version_output="$(xcodebuild -version)"
IFS= read -r xcode_version <<< "$xcode_version_output"
xcode_major="$(awk '{split($2, version, "."); print version[1]}' <<< "$xcode_version")"
if [[ ! "$xcode_major" =~ ^[0-9]+$ ]] || (( xcode_major < 26 )); then
echo "Xcode 26 or newer is required; found $xcode_version" >&2
exit 1
fi
build_number="$((10#$GITHUB_RUN_NUMBER * 100 + 10#$GITHUB_RUN_ATTEMPT))"
if [[ ! "$build_number" =~ ^[1-9][0-9]{0,17}$ ]]; then
echo "Derived build number is outside Apple's supported integer format" >&2
exit 1
fi
echo "MOBILE_VERSION_CODE=$build_number" >> "$GITHUB_ENV"
echo "XCODE_VERSION=$xcode_version" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Verify stable API and Capacitor iOS CORS
shell: bash
run: |
set -euo pipefail
api_base="${IOS_DEBUG_API_URL%/}"
curl --fail --silent --show-error --location \
--connect-timeout 10 --max-time 20 \
--header 'Accept: application/json' \
--output /dev/null \
"$api_base/ping"
cors_headers="$RUNNER_TEMP/ios-debug-cors-headers.txt"
cors_body="$RUNNER_TEMP/ios-debug-cors-body.txt"
cors_status="$(curl --silent --show-error \
--connect-timeout 10 --max-time 20 \
--request OPTIONS \
--header 'Origin: capacitor://localhost' \
--header 'Access-Control-Request-Method: POST' \
--header 'Access-Control-Request-Headers: authorization,content-type' \
--dump-header "$cors_headers" \
--output "$cors_body" \
--write-out '%{http_code}' \
"$api_base/ping")"
if [[ ! "$cors_status" =~ ^2[0-9][0-9]$ ]]; then
echo "Stable API rejected the Capacitor iOS CORS preflight with HTTP $cors_status" >&2
sed -n '1,20p' "$cors_body" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-origin:[[:space:]]*capacitor://localhost[[:space:]]*$' "$cors_headers"; then
echo "Stable API did not allow the exact capacitor://localhost origin" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-credentials:[[:space:]]*true[[:space:]]*$' "$cors_headers"; then
echo "Stable API did not allow credentialed Capacitor requests" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-methods:.*[[:space:],]POST([[:space:],]|$)' "$cors_headers"; then
echo "Stable API did not allow POST from the Capacitor origin" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Authorization([[:space:],]|$)' "$cors_headers"; then
echo "Stable API did not allow the Authorization header from the Capacitor origin" >&2
exit 1
fi
if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Content-Type([[:space:],]|$)' "$cors_headers"; then
echo "Stable API did not allow the Content-Type header from the Capacitor origin" >&2
exit 1
fi
- name: Build stable production web payload
env:
RELEASE_COMMIT_SHA: ${{ env.RESOLVED_SOURCE_SHA }}
VITE_API_URL: ${{ env.IOS_DEBUG_API_URL }}
VITE_RELEASE_MANAGER_CONTROL_API_URL: ${{ env.IOS_DEBUG_API_URL }}
VITE_RELEASE_PUBLIC_GATEWAY_API_URL: https://api-v2.truckwash.io
run: |
npm run build
node -e "const manifest = require('./dist/release-manifest.json'); if (manifest.commit_sha !== process.env.RESOLVED_SOURCE_SHA) { throw new Error('Web release manifest source SHA mismatch'); }"
- name: Sync and validate iOS shell
shell: bash
run: |
set -euo pipefail
npx cap sync ios
npm run mobile:permissions:check
if grep -q 'isa = PBXShellScriptBuildPhase;' "$IOS_PROJECT_PATH/project.pbxproj"; then
echo "Unexpected Xcode shell-script build phase detected" >&2
exit 1
fi
xcodebuild -resolvePackageDependencies -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME"
- name: Validate native Debug and Release settings
shell: bash
run: |
set -euo pipefail
debug_settings="$RUNNER_TEMP/ios-debug-build-settings.txt"
release_settings="$RUNNER_TEMP/ios-release-build-settings.txt"
xcodebuild -showBuildSettings \
-project "$IOS_PROJECT_PATH" \
-scheme "$IOS_SCHEME" \
-configuration Debug \
CODE_SIGNING_ALLOWED=NO > "$debug_settings"
xcodebuild -showBuildSettings \
-project "$IOS_PROJECT_PATH" \
-scheme "$IOS_SCHEME" \
-configuration Release \
CODE_SIGNING_ALLOWED=NO > "$release_settings"
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app\.debug$' "$debug_settings"
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash Debug$' "$debug_settings"
grep -Eq '^[[:space:]]*PRODUCT_NAME = TruckWashDebug$' "$debug_settings"
grep -Eq '^[[:space:]]*CAPACITOR_DEBUG = true$' "$debug_settings"
grep -Eq '^[[:space:]]*DEBUG_INFORMATION_FORMAT = dwarf-with-dsym$' "$debug_settings"
grep -Eq '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = 15\.0$' "$debug_settings"
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app$' "$release_settings"
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash$' "$release_settings"
grep -Eq '^[[:space:]]*PRODUCT_NAME = App$' "$release_settings"
- name: Install and validate Apple development signing assets
shell: bash
env:
IOS_DEBUG_CERTIFICATE_BASE64: ${{ secrets.IOS_DEBUG_CERTIFICATE_BASE64 }}
IOS_DEBUG_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DEBUG_CERTIFICATE_PASSWORD }}
IOS_DEBUG_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_DEBUG_PROVISION_PROFILE_BASE64 }}
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
run: |
set -euo pipefail
node scripts/mobile/check-ios-debug-signing-env.mjs
certificate_path="$RUNNER_TEMP/ios-debug-development.p12"
profile_path="$RUNNER_TEMP/ios-debug-development.mobileprovision"
profile_plist="$RUNNER_TEMP/ios-debug-development-profile.plist"
keychain_path="$RUNNER_TEMP/ios-debug-signing.keychain-db"
keychain_password="$(openssl rand -base64 48 | tr -d '\n')"
echo "::add-mask::$keychain_password"
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_CERTIFICATE_BASE64.replace(/\\s/g, ''), 'base64'))" "$certificate_path"
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_PROVISION_PROFILE_BASE64.replace(/\\s/g, ''), 'base64'))" "$profile_path"
chmod 600 "$certificate_path" "$profile_path"
security cms -D -i "$profile_path" > "$profile_plist"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" \
-P "$IOS_DEBUG_CERTIFICATE_PASSWORD" \
-A \
-t cert \
-f pkcs12 \
-k "$keychain_path"
security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"')
security set-key-partition-list \
-S apple-tool:,apple: \
-s \
-k "$keychain_password" \
"$keychain_path"
signing_identity_sha="$(security find-identity -v -p codesigning "$keychain_path" | awk '/Apple Development/ {print $2; exit}')"
if [[ ! "$signing_identity_sha" =~ ^[0-9A-Fa-f]{40}$ ]]; then
echo "The PKCS#12 file does not contain a valid Apple Development signing identity" >&2
exit 1
fi
IOS_SIGNING_IDENTITY_SHA="$signing_identity_sha" PROFILE_PLIST="$profile_plist" python3 <<'PY'
import datetime
import hashlib
import os
import plistlib
import re
import sys
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
profile = plistlib.load(handle)
team_id = os.environ["APPLE_TEAM_ID"]
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
entitlements = profile.get("Entitlements", {})
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
provisioned = set(profile.get("ProvisionedDevices", []))
expiration = profile.get("ExpirationDate")
now = datetime.datetime.now(datetime.timezone.utc)
if expiration and expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
checks = {
"profile team identifier": team_id in profile.get("TeamIdentifier", []),
"application identifier": entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
"entitlement team identifier": entitlements.get("com.apple.developer.team-identifier") == team_id,
"development entitlement": entitlements.get("get-task-allow") is True,
"profile expiration": expiration is not None and expiration > now,
"registered devices": bool(allowed) and allowed <= provisioned,
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
"developer certificate": bool(profile.get("DeveloperCertificates")),
"profile UUID": isinstance(profile.get("UUID"), str) and re.fullmatch(r"[0-9A-Fa-f-]{36}", profile["UUID"]) is not None,
"safe profile name": isinstance(profile.get("Name"), str) and not any(char in profile["Name"] for char in "\r\n"),
}
identity_sha = os.environ["IOS_SIGNING_IDENTITY_SHA"].upper()
certificate_hashes = {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])}
checks["certificate belongs to profile"] = identity_sha in certificate_hashes
failures = [label for label, passed in checks.items() if not passed]
if failures:
print("Development provisioning profile validation failed:", file=sys.stderr)
for failure in failures:
print(f"- {failure}", file=sys.stderr)
sys.exit(1)
PY
profile_uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$profile_plist")"
profile_name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$profile_plist")"
profile_expiration="$(PROFILE_PLIST="$profile_plist" python3 - <<'PY'
import datetime
import os
import plistlib
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
expiration = plistlib.load(handle)["ExpirationDate"]
if expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
print(expiration.astimezone(datetime.timezone.utc).isoformat().replace("+00:00", "Z"))
PY
)"
profile_install_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
profile_install_path="$profile_install_dir/$profile_uuid.mobileprovision"
mkdir -p "$profile_install_dir"
cp "$profile_path" "$profile_install_path"
echo "IOS_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "IOS_PROFILE_INSTALL_PATH=$profile_install_path" >> "$GITHUB_ENV"
echo "IOS_PROFILE_NAME=$profile_name" >> "$GITHUB_ENV"
echo "IOS_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV"
echo "IOS_PROFILE_EXPIRATION=$profile_expiration" >> "$GITHUB_ENV"
- name: Archive Debug app with Apple Development signing
shell: bash
run: |
set -euo pipefail
archive_path="$RUNNER_TEMP/TruckWashDebug.xcarchive"
xcodebuild \
-project "$IOS_PROJECT_PATH" \
-scheme "$IOS_SCHEME" \
-configuration Debug \
-destination "generic/platform=iOS" \
-archivePath "$archive_path" \
archive \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Development" \
PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME" \
PRODUCT_BUNDLE_IDENTIFIER="$IOS_DEBUG_BUNDLE_ID" \
MARKETING_VERSION="$MOBILE_VERSION_NAME" \
CURRENT_PROJECT_VERSION="$MOBILE_VERSION_CODE" \
DEBUG_INFORMATION_FORMAT="dwarf-with-dsym" \
ONLY_ACTIVE_ARCH=NO
echo "IOS_ARCHIVE_PATH=$archive_path" >> "$GITHUB_ENV"
- name: Export development IPA
shell: bash
run: |
set -euo pipefail
export_options="$RUNNER_TEMP/ios-debug-ExportOptions.plist"
export_path="$RUNNER_TEMP/ios-debug-export"
EXPORT_OPTIONS="$export_options" python3 <<'PY'
import os
import plistlib
options = {
"method": "development",
"signingStyle": "manual",
"teamID": os.environ["APPLE_TEAM_ID"],
"provisioningProfiles": {
os.environ["IOS_DEBUG_BUNDLE_ID"]: os.environ["IOS_PROFILE_NAME"],
},
"stripSwiftSymbols": True,
"manageAppVersionAndBuildNumber": False,
}
with open(os.environ["EXPORT_OPTIONS"], "wb") as handle:
plistlib.dump(options, handle)
PY
xcodebuild \
-exportArchive \
-archivePath "$IOS_ARCHIVE_PATH" \
-exportPath "$export_path" \
-exportOptionsPlist "$export_options"
shopt -s nullglob
ipa_files=("$export_path"/*.ipa)
if [[ ${#ipa_files[@]} -ne 1 ]]; then
echo "Expected exactly one exported IPA; found ${#ipa_files[@]}" >&2
exit 1
fi
echo "IOS_EXPORTED_IPA=${ipa_files[0]}" >> "$GITHUB_ENV"
- name: Validate exported IPA and embedded signature
shell: bash
env:
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
run: |
set -euo pipefail
inspect_dir="$RUNNER_TEMP/ios-debug-inspect"
mkdir -p "$inspect_dir"
unzip -q "$IOS_EXPORTED_IPA" -d "$inspect_dir"
shopt -s nullglob
app_bundles=("$inspect_dir"/Payload/*.app)
if [[ ${#app_bundles[@]} -ne 1 ]]; then
echo "Expected exactly one Payload app; found ${#app_bundles[@]}" >&2
exit 1
fi
app_path="${app_bundles[0]}"
app_info="$app_path/Info.plist"
embedded_profile="$RUNNER_TEMP/ios-debug-embedded-profile.plist"
signature_entitlements="$RUNNER_TEMP/ios-debug-signature-entitlements.plist"
signature_details="$RUNNER_TEMP/ios-debug-signature-details.txt"
security cms -D -i "$app_path/embedded.mobileprovision" > "$embedded_profile"
codesign --verify --deep --strict "$app_path"
codesign -d --entitlements :- "$app_path" > "$signature_entitlements"
codesign -dvv "$app_path" > /dev/null 2> "$signature_details"
grep -Fq "TeamIdentifier=$APPLE_TEAM_ID" "$signature_details"
grep -Eq '^Authority=Apple Development:' "$signature_details"
executable_name="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app_info")"
architectures="$(lipo -archs "$app_path/$executable_name")"
if [[ " $architectures " != *" arm64 "* ]]; then
echo "Exported executable does not contain arm64" >&2
exit 1
fi
APP_INFO="$app_info" PROFILE_PLIST="$embedded_profile" SIGNATURE_ENTITLEMENTS="$signature_entitlements" python3 <<'PY'
import datetime
import os
import plistlib
import sys
def load(path):
with open(path, "rb") as handle:
return plistlib.load(handle)
info = load(os.environ["APP_INFO"])
profile = load(os.environ["PROFILE_PLIST"])
signature = load(os.environ["SIGNATURE_ENTITLEMENTS"])
profile_entitlements = profile.get("Entitlements", {})
team_id = os.environ["APPLE_TEAM_ID"]
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
provisioned = set(profile.get("ProvisionedDevices", []))
expiration = profile.get("ExpirationDate")
now = datetime.datetime.now(datetime.timezone.utc)
if expiration and expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
checks = {
"bundle identifier": info.get("CFBundleIdentifier") == bundle_id,
"display name": info.get("CFBundleDisplayName") == "Truck Wash Debug",
"debug executable": info.get("CFBundleExecutable") == "TruckWashDebug",
"marketing version": info.get("CFBundleShortVersionString") == os.environ["MOBILE_VERSION_NAME"],
"build number": info.get("CFBundleVersion") == os.environ["MOBILE_VERSION_CODE"],
"minimum iOS": info.get("MinimumOSVersion") == "15.0",
"profile UUID": profile.get("UUID") == os.environ["IOS_PROFILE_UUID"],
"profile team": team_id in profile.get("TeamIdentifier", []),
"profile application identifier": profile_entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
"development profile": profile_entitlements.get("get-task-allow") is True,
"signature application identifier": signature.get("application-identifier") == f"{team_id}.{bundle_id}",
"signature team identifier": signature.get("com.apple.developer.team-identifier") == team_id,
"debuggable signature": signature.get("get-task-allow") is True,
"profile expiration": expiration is not None and expiration > now,
"registered devices": bool(allowed) and allowed <= provisioned,
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
}
failures = [label for label, passed in checks.items() if not passed]
if failures:
print("Exported development IPA validation failed:", file=sys.stderr)
for failure in failures:
print(f"- {failure}", file=sys.stderr)
sys.exit(1)
PY
- name: Assemble debug artifact
shell: bash
env:
SOURCE_REF: ${{ inputs.source_ref }}
run: |
set -euo pipefail
short_sha="${RESOLVED_SOURCE_SHA:0:12}"
artifact_name="truck-wash-debug-${MOBILE_VERSION_NAME}-${short_sha}"
artifact_dir="$RUNNER_TEMP/device-debug-artifact"
ipa_filename="$artifact_name.ipa"
dsym_filename="$artifact_name.dSYM.zip"
mkdir -p "$artifact_dir"
cp "$IOS_EXPORTED_IPA" "$artifact_dir/$ipa_filename"
shopt -s nullglob
dsym_bundles=("$IOS_ARCHIVE_PATH"/dSYMs/*.dSYM)
if [[ ${#dsym_bundles[@]} -eq 0 ]]; then
echo "The Debug archive did not contain any dSYM bundles" >&2
exit 1
fi
ditto -c -k --sequesterRsrc --keepParent "$IOS_ARCHIVE_PATH/dSYMs" "$artifact_dir/$dsym_filename"
capacitor_version="$(node -p "require('./node_modules/@capacitor/core/package.json').version")"
BUILT_AT_UTC="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
CAPACITOR_VERSION="$capacitor_version" \
DSYM_FILENAME="$dsym_filename" \
IPA_FILENAME="$ipa_filename" \
MANIFEST_PATH="$artifact_dir/manifest.json" \
python3 <<'PY'
import json
import os
manifest = {
"schema_version": 1,
"repository": os.environ["GITHUB_REPOSITORY"],
"source_ref": os.environ["SOURCE_REF"],
"source_sha": os.environ["RESOLVED_SOURCE_SHA"],
"workflow_run": int(os.environ["GITHUB_RUN_NUMBER"]),
"workflow_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]),
"built_at_utc": os.environ["BUILT_AT_UTC"],
"api_url": os.environ["IOS_DEBUG_API_URL"],
"release_manager_control_api_url": os.environ["IOS_DEBUG_API_URL"],
"bundle_id": os.environ["IOS_DEBUG_BUNDLE_ID"],
"display_name": "Truck Wash Debug",
"executable_name": "TruckWashDebug",
"version": os.environ["MOBILE_VERSION_NAME"],
"build": os.environ["MOBILE_VERSION_CODE"],
"minimum_ios": "15.0",
"capacitor_version": os.environ["CAPACITOR_VERSION"],
"xcode_version": os.environ["XCODE_VERSION"],
"signing_method": "development",
"profile_expiration_utc": os.environ["IOS_PROFILE_EXPIRATION"],
"ipa_filename": os.environ["IPA_FILENAME"],
"dsym_filename": os.environ["DSYM_FILENAME"],
}
with open(os.environ["MANIFEST_PATH"], "w", encoding="utf-8") as handle:
json.dump(manifest, handle, indent=2, sort_keys=True)
handle.write("\n")
PY
(
cd "$artifact_dir"
shasum -a 256 "$ipa_filename" "$dsym_filename" manifest.json > SHA256SUMS
)
echo "IOS_DEBUG_ARTIFACT_DIR=$artifact_dir" >> "$GITHUB_ENV"
echo "IOS_DEBUG_ARTIFACT_NAME=$artifact_name" >> "$GITHUB_ENV"
- name: Upload device-debug artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.IOS_DEBUG_ARTIFACT_NAME }}
path: ${{ env.IOS_DEBUG_ARTIFACT_DIR }}
if-no-files-found: error
retention-days: 7
- name: Clean up Apple signing assets
if: always()
shell: bash
run: |
if [[ -n "${IOS_KEYCHAIN_PATH:-}" ]]; then
security delete-keychain "$IOS_KEYCHAIN_PATH" || true
else
security delete-keychain "$RUNNER_TEMP/ios-debug-signing.keychain-db" || true
fi
if [[ -n "${IOS_PROFILE_INSTALL_PATH:-}" ]]; then
rm -f "$IOS_PROFILE_INSTALL_PATH"
fi
rm -f \
"$RUNNER_TEMP/ios-debug-development.p12" \
"$RUNNER_TEMP/ios-debug-development.mobileprovision" \
"$RUNNER_TEMP/ios-debug-development-profile.plist" \
"$RUNNER_TEMP/ios-debug-embedded-profile.plist" \
"$RUNNER_TEMP/ios-debug-signature-entitlements.plist" \
"$RUNNER_TEMP/ios-debug-signature-details.txt"
+431
View File
@@ -0,0 +1,431 @@
name: iOS Internal TestFlight
on:
workflow_run:
workflows: [Frontend Release]
types: [completed]
branches: [master]
workflow_dispatch:
inputs:
source_sha:
description: Full master commit SHA with a verified Frontend Release proof
required: true
type: string
confirmation:
description: Type UPLOAD IOS INTERNAL BUILD
required: true
type: string
permissions:
contents: read
actions: read
concurrency:
group: ios-internal-testflight
cancel-in-progress: false
jobs:
prepare:
name: Resolve verified release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'workflow_run' &&
github.event.workflow_run.head_branch == 'master' &&
github.event.workflow_run.head_repository.full_name == github.repository)
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
source_sha: ${{ steps.resolve.outputs.source_sha }}
enabled: ${{ steps.resolve.outputs.enabled }}
current: ${{ steps.resolve.outputs.current }}
steps:
- name: Checkout repository history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve immutable source and rollout gate
id: resolve
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
WORKFLOW_SOURCE_SHA: ${{ github.event.workflow_run.head_sha || '' }}
INPUT_SOURCE_SHA: ${{ inputs.source_sha || '' }}
CONFIRMATION: ${{ inputs.confirmation || '' }}
AUTOMATION_ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
run: |
set -euo pipefail
source_sha="$WORKFLOW_SOURCE_SHA"
if [[ "$EVENT_NAME" == workflow_dispatch ]]; then
[[ "$GITHUB_REF" == refs/heads/master ]] || { echo "Dispatch this workflow from master." >&2; exit 1; }
[[ "$CONFIRMATION" == "UPLOAD IOS INTERNAL BUILD" ]] || { echo "Invalid confirmation." >&2; exit 1; }
source_sha="${INPUT_SOURCE_SHA,,}"
fi
[[ "$source_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "A full lowercase source SHA is required." >&2; exit 1; }
git show-ref --verify --quiet refs/remotes/origin/master || { echo "origin/master was not included in the full checkout." >&2; exit 1; }
git cat-file -e "${source_sha}^{commit}"
git merge-base --is-ancestor "$source_sha" origin/master || { echo "Source is not reachable from master." >&2; exit 1; }
current=false
[[ "$(git rev-parse origin/master)" == "$source_sha" ]] && current=true
enabled=false
[[ "$AUTOMATION_ENABLED" == true ]] && enabled=true
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "current=$current" >> "$GITHUB_OUTPUT"
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
if [[ "$enabled" != true ]]; then
echo "### iOS automation is safely disabled" >> "$GITHUB_STEP_SUMMARY"
echo 'Set repository variable `APP_STORE_AUTOMATION_ENABLED=true` only after the signing/API credential canary passes.' >> "$GITHUB_STEP_SUMMARY"
elif [[ "$current" != true ]]; then
echo "### Stale release skipped" >> "$GITHUB_STEP_SUMMARY"
echo "The verified SHA is no longer current master." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Require green WebKit mobile tests before App Store upload
if: steps.resolve.outputs.enabled == 'true' && steps.resolve.outputs.current == 'true'
run: node scripts/mobile/verify-store-test-gate.mjs --platform apple
env:
GH_TOKEN: ${{ github.token }}
STORE_SOURCE_SHA: ${{ steps.resolve.outputs.source_sha }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
deliver:
name: Sign, upload, process, and distribute
needs: prepare
if: needs.prepare.outputs.enabled == 'true' && needs.prepare.outputs.current == 'true'
runs-on: macos-15
timeout-minutes: 120
environment: app-store-signing
env:
DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer
IOS_SOURCE_SHA: ${{ needs.prepare.outputs.source_sha }}
IOS_PROJECT_PATH: ${{ vars.IOS_PROJECT || 'ios/App/App.xcodeproj' }}
IOS_SCHEME: ${{ vars.IOS_SCHEME || 'App' }}
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
TESTFLIGHT_INTERNAL_GROUP_ID: ${{ vars.TESTFLIGHT_INTERNAL_GROUP_ID }}
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
steps:
- name: Checkout verified source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.IOS_SOURCE_SHA }}
fetch-depth: 1
persist-credentials: false
- name: Download and verify frontend release proof
shell: bash
env:
GH_TOKEN: ${{ github.token }}
TRIGGERING_RELEASE_RUN_ID: ${{ github.event.workflow_run.id || '' }}
run: |
set -euo pipefail
artifact_name="frontend-release-proof-$IOS_SOURCE_SHA"
response="$RUNNER_TEMP/proof-artifacts.json"
curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts?name=$artifact_name&per_page=100" > "$response"
artifact_id="$(jq -r --arg run "$TRIGGERING_RELEASE_RUN_ID" '
[.artifacts[] | select(.expired == false) | select(($run == "") or ((.workflow_run.id|tostring) == $run))] |
sort_by(.created_at) | last | .id // empty' "$response")"
[[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "No verified Frontend Release proof found for $IOS_SOURCE_SHA." >&2; exit 1; }
mkdir -p output/frontend-release-proof
curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" -o "$RUNNER_TEMP/proof.zip"
unzip -q "$RUNNER_TEMP/proof.zip" -d output/frontend-release-proof
PROOF_PATH=output/frontend-release-proof/frontend-release-proof.json node <<'NODE'
const fs = require("node:fs");
const proof = JSON.parse(fs.readFileSync(process.env.PROOF_PATH, "utf8"));
const checks = {
schema: proof.schemaVersion === 1,
repository: proof.repository === process.env.GITHUB_REPOSITORY,
source: proof.sourceSha === process.env.IOS_SOURCE_SHA,
publicGate: proof.livePublicGate === "passed",
credentialedGate: proof.liveCredentialedGate === "passed",
managerGate: proof.releaseManagerGate === "passed",
serverVersion: proof.serverVersionUpdated === true,
};
const failures = Object.entries(checks).filter(([, passed]) => !passed).map(([label]) => label);
if (failures.length) throw new Error(`Invalid frontend release proof: ${failures.join(", ")}`);
NODE
- name: Verify Xcode 26 and iOS 26 SDK
shell: bash
run: |
set -euo pipefail
[[ -x "$DEVELOPER_DIR/usr/bin/xcodebuild" ]] || { echo "Xcode 26.3 is not installed at $DEVELOPER_DIR." >&2; exit 1; }
xcode_version="$(xcodebuild -version | sed -n '1p')"
sdk_version="$(xcrun --sdk iphoneos --show-sdk-version)"
[[ "$xcode_version" =~ ^Xcode\ 26\. ]] || { echo "Xcode 26.x required; found $xcode_version." >&2; exit 1; }
[[ "$sdk_version" =~ ^26\. ]] || { echo "iPhoneOS 26 SDK required; found $sdk_version." >&2; exit 1; }
echo "XCODE_VERSION=$xcode_version" >> "$GITHUB_ENV"
echo "IOS_SDK_VERSION=$sdk_version" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Setup Ruby and pinned Fastlane
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Install web dependencies
run: npm ci --legacy-peer-deps
- name: Validate storefront and resolve version
id: version
shell: bash
run: |
set -euo pipefail
node scripts/mobile/validate-app-store.mjs
version="$(node -p "JSON.parse(require('fs').readFileSync('ios/release.json')).marketingVersion")"
bundle="$(node -p "JSON.parse(require('fs').readFileSync('ios/release.json')).bundleId")"
[[ "$bundle" == "$IOS_BUNDLE_ID" ]]
echo "IOS_MARKETING_VERSION=$version" >> "$GITHUB_ENV"
echo "MOBILE_VERSION_NAME=$version" >> "$GITHUB_ENV"
- name: Resolve build number from App Store Connect
id: app-store
run: node scripts/mobile/app-store-connect.mjs next-build-number
- name: Export resolved build number
env:
BUILD_NUMBER: ${{ steps.app-store.outputs.build_number }}
run: |
[[ "$BUILD_NUMBER" =~ ^[1-9][0-9]*$ ]]
echo "IOS_BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
echo "MOBILE_VERSION_CODE=$BUILD_NUMBER" >> "$GITHUB_ENV"
- name: Validate complete Apple environment
env:
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
UPLOAD_IOS_TO_APP_STORE: "true"
run: node scripts/mobile/check-store-upload-env.mjs --ios
- name: Build and sync production iOS shell
run: |
npm run build
npx cap sync ios
npm run mobile:permissions:check
xcodebuild -resolvePackageDependencies -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME"
- name: Validate native release settings
shell: bash
run: |
set -euo pipefail
settings="$RUNNER_TEMP/ios-release-build-settings.txt"
xcodebuild -showBuildSettings -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME" -configuration Release CODE_SIGNING_ALLOWED=NO > "$settings"
grep -Eq "^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = ${IOS_BUNDLE_ID//./\.}$" "$settings"
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash$' "$settings"
grep -Eq '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = 15\.0$' "$settings"
if grep -q 'isa = PBXShellScriptBuildPhase;' "$IOS_PROJECT_PATH/project.pbxproj"; then
echo "Unexpected Xcode shell-script build phase detected." >&2
exit 1
fi
- name: Install and validate Apple distribution signing assets
shell: bash
env:
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
run: |
set -euo pipefail
certificate_path="$RUNNER_TEMP/apple-distribution.p12"
profile_path="$RUNNER_TEMP/app-store.mobileprovision"
profile_plist="$RUNNER_TEMP/app-store-profile.plist"
keychain_path="$RUNNER_TEMP/app-store-signing.keychain-db"
keychain_password="$(openssl rand -base64 48 | tr -d '\n')"
echo "::add-mask::$keychain_password"
echo "IOS_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
node -e "const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64.replace(/\\s/g,''),'base64'))" "$certificate_path"
node -e "const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_APP_STORE_PROFILE_BASE64.replace(/\\s/g,''),'base64'))" "$profile_path"
chmod 600 "$certificate_path" "$profile_path"
security cms -D -i "$profile_path" > "$profile_plist"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path"
security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"')
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain_path"
identity_sha="$(security find-identity -v -p codesigning "$keychain_path" | awk '/Apple Distribution/ {print $2; exit}')"
[[ "$identity_sha" =~ ^[0-9A-Fa-f]{40}$ ]] || { echo "P12 lacks an Apple Distribution identity." >&2; exit 1; }
IOS_SIGNING_IDENTITY_SHA="$identity_sha" PROFILE_PLIST="$profile_plist" python3 <<'PY'
import datetime, hashlib, os, plistlib, re, sys
with open(os.environ["PROFILE_PLIST"], "rb") as handle: profile = plistlib.load(handle)
entitlements = profile.get("Entitlements", {})
expiration = profile.get("ExpirationDate")
if expiration and expiration.tzinfo is None: expiration = expiration.replace(tzinfo=datetime.timezone.utc)
team = os.environ["APPLE_TEAM_ID"]
bundle = os.environ["IOS_BUNDLE_ID"]
hashes = {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])}
checks = {
"team": team in profile.get("TeamIdentifier", []),
"application identifier": entitlements.get("application-identifier") == f"{team}.{bundle}",
"team entitlement": entitlements.get("com.apple.developer.team-identifier") == team,
"distribution entitlement": entitlements.get("get-task-allow") is False,
"App Store profile has no devices": not profile.get("ProvisionedDevices"),
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
"expiration": expiration is not None and expiration > datetime.datetime.now(datetime.timezone.utc),
"certificate belongs to profile": os.environ["IOS_SIGNING_IDENTITY_SHA"].upper() in hashes,
"safe profile name": isinstance(profile.get("Name"), str) and not re.search(r"[\r\n]", profile["Name"]),
}
failed = [name for name, passed in checks.items() if not passed]
if failed:
print("Distribution signing validation failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
sys.exit(1)
PY
profile_uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$profile_plist")"
profile_name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$profile_plist")"
profile_install="$HOME/Library/MobileDevice/Provisioning Profiles/$profile_uuid.mobileprovision"
mkdir -p "$(dirname "$profile_install")"
echo "IOS_PROFILE_INSTALL_PATH=$profile_install" >> "$GITHUB_ENV"
cp "$profile_path" "$profile_install"
echo "IOS_PROFILE_NAME=$profile_name" >> "$GITHUB_ENV"
echo "IOS_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV"
- name: Archive and export App Store IPA
shell: bash
run: |
set -euo pipefail
archive="$RUNNER_TEMP/TruckWash.xcarchive"
export_dir="$RUNNER_TEMP/ios-export"
export_options="$RUNNER_TEMP/ExportOptions.plist"
xcodebuild -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME" -configuration Release \
-destination 'generic/platform=iOS' -archivePath "$archive" archive \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY='Apple Distribution' \
PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME" MARKETING_VERSION="$IOS_MARKETING_VERSION" \
CURRENT_PROJECT_VERSION="$IOS_BUILD_NUMBER" DEBUG_INFORMATION_FORMAT='dwarf-with-dsym'
EXPORT_OPTIONS="$export_options" python3 <<'PY'
import os, plistlib
options = {"method":"app-store-connect","signingStyle":"manual","teamID":os.environ["APPLE_TEAM_ID"],"provisioningProfiles":{os.environ["IOS_BUNDLE_ID"]:os.environ["IOS_PROFILE_NAME"]},"stripSwiftSymbols":True,"manageAppVersionAndBuildNumber":False}
with open(os.environ["EXPORT_OPTIONS"], "wb") as handle: plistlib.dump(options, handle)
PY
xcodebuild -exportArchive -archivePath "$archive" -exportPath "$export_dir" -exportOptionsPlist "$export_options"
shopt -s nullglob
ipa_files=("$export_dir"/*.ipa)
[[ ${#ipa_files[@]} -eq 1 ]] || { echo "Expected one IPA; found ${#ipa_files[@]}." >&2; exit 1; }
echo "IOS_ARCHIVE_PATH=$archive" >> "$GITHUB_ENV"
echo "IOS_IPA_PATH=${ipa_files[0]}" >> "$GITHUB_ENV"
- name: Inspect signed IPA
shell: bash
run: |
set -euo pipefail
inspect="$RUNNER_TEMP/ios-inspect"
unzip -q "$IOS_IPA_PATH" -d "$inspect"
shopt -s nullglob
apps=("$inspect"/Payload/*.app)
[[ ${#apps[@]} -eq 1 ]] || { echo "Expected one Payload app." >&2; exit 1; }
app="${apps[0]}"
codesign --verify --deep --strict "$app"
codesign -d --entitlements :- "$app" > "$RUNNER_TEMP/entitlements.plist"
security cms -D -i "$app/embedded.mobileprovision" > "$RUNNER_TEMP/embedded-profile.plist"
[[ -f "$app/PrivacyInfo.xcprivacy" ]]
[[ -f "$app/da.lproj/InfoPlist.strings" ]]
[[ -f "$app/en.lproj/InfoPlist.strings" ]]
APP_PATH="$app" python3 <<'PY'
import os, plistlib, sys
app = os.environ["APP_PATH"]
with open(f"{app}/Info.plist", "rb") as handle: info = plistlib.load(handle)
with open(os.path.join(os.environ["RUNNER_TEMP"], "entitlements.plist"), "rb") as handle: ent = plistlib.load(handle)
with open(os.path.join(os.environ["RUNNER_TEMP"], "embedded-profile.plist"), "rb") as handle: profile = plistlib.load(handle)
checks = {
"bundle": info.get("CFBundleIdentifier") == os.environ["IOS_BUNDLE_ID"],
"version": info.get("CFBundleShortVersionString") == os.environ["IOS_MARKETING_VERSION"],
"build": info.get("CFBundleVersion") == os.environ["IOS_BUILD_NUMBER"],
"minimum iOS": info.get("MinimumOSVersion") == "15.0",
"profile": profile.get("UUID") == os.environ["IOS_PROFILE_UUID"],
"non-debug signature": ent.get("get-task-allow") is not True,
"signature application id": ent.get("application-identifier") == f'{os.environ["APPLE_TEAM_ID"]}.{os.environ["IOS_BUNDLE_ID"]}',
}
failed = [name for name, passed in checks.items() if not passed]
if failed:
print("IPA validation failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
sys.exit(1)
PY
- name: Recheck live master before upload
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
live_master_sha="$(curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/master" | jq -r '.object.sha')"
[[ "$live_master_sha" == "$IOS_SOURCE_SHA" ]] || {
echo "master advanced while the signed build was queued; refusing upload." >&2
exit 1
}
- name: Upload and wait for App Store processing
env:
TESTFLIGHT_WHAT_TO_TEST: Automatisk intern build fra verificeret master ${{ env.IOS_SOURCE_SHA }}.
run: bundle exec fastlane ios upload_internal
- name: Assign exact processed build to Internal QA
id: distribute
env:
TESTFLIGHT_WHAT_TO_TEST: Automatisk intern build fra verificeret master ${{ env.IOS_SOURCE_SHA }}.
run: node scripts/mobile/app-store-connect.mjs wait-and-distribute
- name: Assemble signed release evidence
shell: bash
env:
APP_STORE_BUILD_ID: ${{ steps.distribute.outputs.app_store_build_id }}
run: |
set -euo pipefail
artifact="output/ios-release"
mkdir -p "$artifact"
cp "$IOS_IPA_PATH" "$artifact/TruckWash-$IOS_MARKETING_VERSION-$IOS_BUILD_NUMBER.ipa"
shopt -s nullglob
dsyms=("$IOS_ARCHIVE_PATH"/dSYMs/*.dSYM)
[[ ${#dsyms[@]} -gt 0 ]] || { echo "Release archive contains no dSYM bundles." >&2; exit 1; }
ditto -c -k --sequesterRsrc --keepParent "$IOS_ARCHIVE_PATH/dSYMs" "$artifact/TruckWash-$IOS_MARKETING_VERSION-$IOS_BUILD_NUMBER.dSYM.zip"
node scripts/mobile/create-ios-release-manifest.mjs
(cd "$artifact" && shasum -a 256 -- * > SHA256SUMS)
- name: Upload signed IPA
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: truck-wash-ios-${{ env.IOS_SOURCE_SHA }}
path: output/ios-release/*.ipa
if-no-files-found: error
retention-days: 30
- name: Upload release manifest, dSYM, and checksums
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ios-release-manifest-${{ env.IOS_SOURCE_SHA }}
path: |
output/ios-release/ios-release-manifest.json
output/ios-release/*.dSYM.zip
output/ios-release/SHA256SUMS
if-no-files-found: error
retention-days: 90
- name: Clean up Apple signing material
if: always()
shell: bash
run: |
if [[ -n "${IOS_KEYCHAIN_PATH:-}" ]]; then security delete-keychain "$IOS_KEYCHAIN_PATH" || true; fi
if [[ -n "${IOS_PROFILE_INSTALL_PATH:-}" ]]; then rm -f "$IOS_PROFILE_INSTALL_PATH"; fi
rm -f "$RUNNER_TEMP/apple-distribution.p12" "$RUNNER_TEMP/app-store.mobileprovision" "$RUNNER_TEMP/app-store-profile.plist"
disabled:
name: Automation disabled
needs: prepare
if: needs.prepare.outputs.enabled != 'true'
runs-on: ubuntu-24.04
steps:
- run: echo "App Store automation is disabled; no signing environment or secrets were accessed."
+264
View File
@@ -0,0 +1,264 @@
name: Android Store Artifacts
on:
workflow_dispatch:
inputs:
version_name:
description: Store version name, for example 1.4.0
required: false
type: string
version_code:
description: Store build number/version code
required: false
type: string
upload_android_to_play:
description: Upload the signed Android App Bundle to Google Play
required: false
type: boolean
default: true
android_track:
description: Google Play track for manual dispatches
required: false
type: choice
default: production
options:
- production
- beta
- alpha
- internal
android_release_status:
description: Google Play release status for manual dispatches
required: false
type: choice
default: inProgress
options:
- inProgress
- draft
- halted
push:
tags:
- "mobile-v*"
permissions:
contents: read
actions: read
concurrency:
group: android-store-artifacts-${{ github.ref_name || github.run_id }}
cancel-in-progress: true
jobs:
android:
name: Android AAB and Play upload
runs-on: ubuntu-24.04
environment: mobile-store-production
timeout-minutes: 60
env:
ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME || 'io.truckwash.twa' }}
ANDROID_AAB_PATH: ${{ vars.ANDROID_AAB_PATH || 'android/app/build/outputs/bundle/release/app-release.aab' }}
ANDROID_SIGNING_IDENTITY_REF: github-environment:mobile-store-production/android-keystore
PLAY_STORE_TRACK: ${{ inputs.android_track || vars.PLAY_STORE_TRACK || 'production' }}
PLAY_STORE_RELEASE_STATUS: ${{ inputs.android_release_status || 'inProgress' }}
PLAY_STORE_USER_FRACTION: "0.01"
UPLOAD_ANDROID_TO_PLAY: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_android_to_play }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Guard current master release
id: release-guard
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
EXPECTED_SHA: ${{ github.sha }}
RELEASE_BRANCH: ${{ github.ref_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
UPLOAD_TO_PLAY: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_android_to_play }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
current=true
latest_sha="$(curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/$DEFAULT_BRANCH" | jq -r '.object.sha // empty')"
if [[ ! "$latest_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "Could not resolve origin/$DEFAULT_BRANCH." >&2
exit 1
fi
if [[ "$latest_sha" != "$EXPECTED_SHA" && "$UPLOAD_TO_PLAY" == "true" ]]; then
current=false
echo "Skipping stale mobile upload for $EXPECTED_SHA; origin/$DEFAULT_BRANCH is $latest_sha."
elif [[ "$latest_sha" != "$EXPECTED_SHA" ]]; then
echo "Allowing artifact-only build for $EVENT_NAME on $RELEASE_BRANCH; store upload remains disabled."
else
echo "Mobile upload commit is current for $DEFAULT_BRANCH."
fi
echo "current=$current" >> "$GITHUB_OUTPUT"
echo "source_sha=$latest_sha" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
if: steps.release-guard.outputs.current == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Require green Chromium mobile tests before Play upload
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
run: node scripts/mobile/verify-store-test-gate.mjs --platform android
env:
GH_TOKEN: ${{ github.token }}
STORE_SOURCE_SHA: ${{ steps.release-guard.outputs.source_sha }}
TEST_WORKFLOW_RUN_ID: ""
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
- name: Setup Java
if: steps.release-guard.outputs.current == 'true'
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: temurin
java-version: 21
- name: Setup Android SDK
if: steps.release-guard.outputs.current == 'true'
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
- name: Install Android SDK packages
if: steps.release-guard.outputs.current == 'true'
shell: bash
run: |
set -euo pipefail
yes | sdkmanager --licenses >/dev/null || true
sdkmanager "platforms;android-36" "build-tools;36.0.0"
- name: Resolve mobile version
if: steps.release-guard.outputs.current == 'true'
shell: bash
env:
INPUT_VERSION_NAME: ${{ inputs.version_name || '' }}
INPUT_VERSION_CODE: ${{ inputs.version_code || '' }}
run: |
set -euo pipefail
version_name="$INPUT_VERSION_NAME"
if [[ -z "$version_name" && "$GITHUB_REF_NAME" == mobile-v* ]]; then
version_name="${GITHUB_REF_NAME#mobile-v}"
fi
if [[ -z "$version_name" ]]; then
version_name="0.0.${GITHUB_RUN_NUMBER}"
fi
version_code="${INPUT_VERSION_CODE:-$GITHUB_RUN_NUMBER}"
echo "MOBILE_VERSION_NAME=$version_name" >> "$GITHUB_ENV"
echo "MOBILE_VERSION_CODE=$version_code" >> "$GITHUB_ENV"
- name: Check Android store environment
if: steps.release-guard.outputs.current == 'true'
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
run: node scripts/mobile/check-store-upload-env.mjs --android
- name: Install dependencies
if: steps.release-guard.outputs.current == 'true'
run: npm ci --legacy-peer-deps
- name: Decode Android signing key
if: steps.release-guard.outputs.current == 'true'
shell: bash
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
keystore_path="$RUNNER_TEMP/android-release.keystore"
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.ANDROID_KEYSTORE_BASE64, 'base64'))" "$keystore_path"
echo "ANDROID_KEYSTORE_FILE=$keystore_path" >> "$GITHUB_ENV"
echo "ANDROID_KEYSTORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD" >> "$GITHUB_ENV"
echo "ANDROID_KEY_ALIAS=$ANDROID_KEY_ALIAS" >> "$GITHUB_ENV"
echo "ANDROID_KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> "$GITHUB_ENV"
- name: Build and sync Android shell
if: steps.release-guard.outputs.current == 'true'
run: |
npm run mobile:android:sync
npm run mobile:permissions:check
npm run mobile:android:signing:check
- name: Build signed Android App Bundle
if: steps.release-guard.outputs.current == 'true'
working-directory: android
run: ./gradlew --no-daemon bundleRelease
- name: Verify Android App Bundle signature
if: steps.release-guard.outputs.current == 'true'
run: jarsigner -verify -certs -verbose "$ANDROID_AAB_PATH" >/dev/null
- name: Record immutable Android artifact proof
if: steps.release-guard.outputs.current == 'true'
id: artifact-proof
shell: bash
run: |
set -euo pipefail
artifact_sha256="$(sha256sum "$ANDROID_AAB_PATH" | awk '{print $1}')"
[[ "$artifact_sha256" =~ ^[0-9a-f]{64}$ ]]
echo "ANDROID_AAB_SHA256=$artifact_sha256" >> "$GITHUB_ENV"
echo "sha256=$artifact_sha256" >> "$GITHUB_OUTPUT"
echo "Android AAB SHA-256: \`$artifact_sha256\`" >> "$GITHUB_STEP_SUMMARY"
- name: Upload Android artifact
if: steps.release-guard.outputs.current == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: truck-wash-android-${{ env.MOBILE_VERSION_NAME }}-${{ github.sha }}
path: ${{ env.ANDROID_AAB_PATH }}
if-no-files-found: error
retention-days: 14
- name: Recheck live master before Play upload
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
EXPECTED_SHA: ${{ steps.release-guard.outputs.source_sha }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
live_master_sha="$(curl --fail --silent --show-error --location \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/$DEFAULT_BRANCH" | jq -r '.object.sha // empty')"
[[ "$live_master_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "Could not resolve origin/$DEFAULT_BRANCH." >&2; exit 1; }
[[ "$live_master_sha" == "$EXPECTED_SHA" ]] || {
echo "$DEFAULT_BRANCH advanced while the Android bundle was building; refusing Play upload." >&2
exit 1
}
- name: Upload Android App Bundle to Google Play
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
id: play-upload
env:
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
run: npm run mobile:android:play-upload
- name: Record Google Play submission proof
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
env:
ARTIFACT_SHA256: ${{ steps.artifact-proof.outputs.sha256 }}
PLAY_EDIT_ID: ${{ steps.play-upload.outputs.play_edit_id }}
PLAY_VERSION_CODE: ${{ steps.play-upload.outputs.version_code }}
run: |
set -euo pipefail
test -n "$ARTIFACT_SHA256"
test -n "$PLAY_EDIT_ID"
test -n "$PLAY_VERSION_CODE"
printf 'Google Play submission proof: platform=android applicationId=%s version=%s buildNumber=%s artifactSha256=%s signingIdentityRef=%s storeSubmissionId=%s status=%s fraction=%s\n' \
"$ANDROID_PACKAGE_NAME" "$MOBILE_VERSION_NAME" "$PLAY_VERSION_CODE" \
"$ARTIFACT_SHA256" "$ANDROID_SIGNING_IDENTITY_REF" "$PLAY_EDIT_ID" \
"$PLAY_STORE_RELEASE_STATUS" "$PLAY_STORE_USER_FRACTION" >> "$GITHUB_STEP_SUMMARY"
+394
View File
@@ -0,0 +1,394 @@
name: Frontend Release
on:
workflow_run:
workflows:
- Automated Tests
types:
- completed
branches:
- master
permissions:
contents: read
actions: read
concurrency:
group: frontend-production
cancel-in-progress: false
jobs:
build-release:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'master' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-24.04
env:
RELEASE_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
RELEASE_EXPECTED_COMMIT: ${{ github.event.workflow_run.head_sha }}
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
outputs:
current: ${{ steps.branch-head.outputs.current }}
build_id: ${{ steps.package.outputs.build_id }}
artifact_name: ${{ steps.package-names.outputs.artifact_name }}
archive_name: ${{ steps.package.outputs.archive_name }}
checksum_name: ${{ steps.package-names.outputs.checksum_name }}
inventory_name: ${{ steps.package-names.outputs.inventory_name }}
release_id: ${{ steps.package.outputs.release_id }}
steps:
- name: Check release commit is current
id: branch-head
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ github.token }}
script: |
const { data: branch } = await github.rest.repos.getBranch({
owner: context.repo.owner,
repo: context.repo.repo,
branch: "master",
});
const expected = process.env.RELEASE_EXPECTED_COMMIT;
const current = branch.commit.sha === expected;
core.setOutput("current", String(current));
core.info(
current
? `Release commit ${expected} is current for master.`
: `Skipping stale release for ${expected}; origin/master is ${branch.commit.sha}.`,
);
- name: Checkout tested commit
if: steps.branch-head.outputs.current == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ env.RELEASE_COMMIT_SHA }}
- name: Setup Node.js
if: steps.branch-head.outputs.current == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Install dependencies
if: steps.branch-head.outputs.current == 'true'
run: npm ci --legacy-peer-deps
- name: Check AI workflow sync
if: steps.branch-head.outputs.current == 'true'
run: node scripts/sync-ai-workflow.mjs --check
- name: Source and i18n checks
if: steps.branch-head.outputs.current == 'true'
run: |
npm run text:check-encoding
npm run i18n:v2:source-check
- name: Build release artifact
if: steps.branch-head.outputs.current == 'true'
run: npm run build
- name: Record pre-gate dist inventory
if: steps.branch-head.outputs.current == 'true'
run: |
inventory="$RUNNER_TEMP/dist-before-production-gate.txt"
while IFS= read -r -d '' file; do
relative_path="${file#dist/}"
printf '%s\t%s\t%s\n' \
"$(sha256sum "$file" | awk '{print $1}')" \
"$(stat --format='%s' "$file")" \
"$relative_path"
done < <(find dist -type f -print0 | LC_ALL=C sort -z) > "$inventory"
- name: Install Playwright Chromium
if: steps.branch-head.outputs.current == 'true'
run: node scripts/install-playwright-browsers.mjs chromium
- name: Production Playwright gate
if: steps.branch-head.outputs.current == 'true'
run: npm run test:e2e:prod
env:
PLAYWRIGHT_PROD_PREBUILT: "1"
PLAYWRIGHT_PROD_WEBKIT: "0"
- name: Confirm production gate did not mutate dist
if: steps.branch-head.outputs.current == 'true'
run: |
inventory="$RUNNER_TEMP/dist-after-production-gate.txt"
while IFS= read -r -d '' file; do
relative_path="${file#dist/}"
printf '%s\t%s\t%s\n' \
"$(sha256sum "$file" | awk '{print $1}')" \
"$(stat --format='%s' "$file")" \
"$relative_path"
done < <(find dist -type f -print0 | LC_ALL=C sort -z) > "$inventory"
cmp "$RUNNER_TEMP/dist-before-production-gate.txt" "$inventory"
- name: Package and validate release
if: steps.branch-head.outputs.current == 'true'
id: package
run: node scripts/release/package-dist.mjs
env:
RELEASE_OUTPUT_DIR: release-artifacts
- name: Resolve package metadata
if: steps.branch-head.outputs.current == 'true'
id: package-names
env:
BUILD_ID: ${{ steps.package.outputs.build_id }}
CHECKSUM_PATH: ${{ steps.package.outputs.checksum_path }}
INVENTORY_PATH: ${{ steps.package.outputs.inventory_path }}
run: |
echo "artifact_name=frontend-release-$BUILD_ID" >> "$GITHUB_OUTPUT"
echo "checksum_name=$(basename -- "$CHECKSUM_PATH")" >> "$GITHUB_OUTPUT"
echo "inventory_name=$(basename -- "$INVENTORY_PATH")" >> "$GITHUB_OUTPUT"
- name: Upload release package
if: steps.branch-head.outputs.current == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.package-names.outputs.artifact_name }}
path: |
${{ steps.package.outputs.archive_path }}
${{ steps.package.outputs.checksum_path }}
${{ steps.package.outputs.inventory_path }}
if-no-files-found: error
retention-days: 14
deploy-frontend-production:
needs: build-release
if: needs.build-release.outputs.current == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 90
environment:
name: frontend-production
url: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
env:
RELEASE_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
PLAYWRIGHT_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
PLAYWRIGHT_RELEASE_STATIC_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
PLAYWRIGHT_RELEASE_API_PING_PATHS: /master/api/ping
RELEASE_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
RELEASE_EXPECTED_COMMIT: ${{ github.event.workflow_run.head_sha }}
RELEASE_BUILD_ID: ${{ needs.build-release.outputs.build_id }}
RELEASE_EXPECTED_BUILD_ID: ${{ needs.build-release.outputs.build_id }}
RELEASE_ID: ${{ needs.build-release.outputs.release_id }}
RELEASE_STRICT_BUILD_ID: "true"
RELEASE_REQUIRE_CACHE_HEADERS: "true"
RELEASE_WAIT_INITIAL_SECONDS: 0
RELEASE_WAIT_TIMEOUT_SECONDS: 300
RELEASE_POLL_INTERVAL_SECONDS: 5
steps:
- name: Checkout tested commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ env.RELEASE_COMMIT_SHA }}
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Install secure FTP client without system changes
run: |
if command -v lftp >/dev/null 2>&1; then
exit 0
fi
package_root="$RUNNER_TEMP/lftp-package"
mkdir -p "$package_root"
(
cd "$package_root"
apt-get download lftp
dpkg-deb --extract ./lftp_*.deb root
)
echo "$package_root/root/usr/bin" >> "$GITHUB_PATH"
- name: Install Playwright Chromium
run: node scripts/install-playwright-browsers.mjs chromium
- name: Download validated release package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ needs.build-release.outputs.artifact_name }}
path: release-artifacts
- name: Resolve downloaded release package
env:
ARCHIVE_NAME: ${{ needs.build-release.outputs.archive_name }}
CHECKSUM_NAME: ${{ needs.build-release.outputs.checksum_name }}
INVENTORY_NAME: ${{ needs.build-release.outputs.inventory_name }}
run: |
[[ -n "$ARCHIVE_NAME" && "$ARCHIVE_NAME" == "$(basename -- "$ARCHIVE_NAME")" ]]
[[ -n "$CHECKSUM_NAME" && "$CHECKSUM_NAME" == "$(basename -- "$CHECKSUM_NAME")" ]]
[[ -n "$INVENTORY_NAME" && "$INVENTORY_NAME" == "$(basename -- "$INVENTORY_NAME")" ]]
archive_path="$GITHUB_WORKSPACE/release-artifacts/$ARCHIVE_NAME"
checksum_path="$GITHUB_WORKSPACE/release-artifacts/$CHECKSUM_NAME"
inventory_path="$GITHUB_WORKSPACE/release-artifacts/$INVENTORY_NAME"
[[ -f "$archive_path" && -f "$checksum_path" && -f "$inventory_path" ]]
echo "RELEASE_ARCHIVE_PATH=$archive_path" >> "$GITHUB_ENV"
echo "RELEASE_ARCHIVE_SHA256_PATH=$checksum_path" >> "$GITHUB_ENV"
echo "RELEASE_INVENTORY_PATH=$inventory_path" >> "$GITHUB_ENV"
- name: Check release commit is still current
id: branch-head
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ github.token }}
script: |
const { data: branch } = await github.rest.repos.getBranch({
owner: context.repo.owner,
repo: context.repo.repo,
branch: "master",
});
const expected = process.env.RELEASE_EXPECTED_COMMIT;
const current = branch.commit.sha === expected;
core.setOutput("current", String(current));
core.info(
current
? `Release commit ${expected} is current immediately before activation.`
: `Skipping stale release for ${expected}; origin/master is ${branch.commit.sha}.`,
);
- name: Deploy atomically and verify cPanel release
if: steps.branch-head.outputs.current == 'true'
id: deploy
timeout-minutes: 15
run: node scripts/release/deploy-cpanel.mjs
env:
NODE_OPTIONS: --use-system-ca
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
RELEASE_GITHUB_REPOSITORY: ${{ github.repository }}
RELEASE_GITHUB_TOKEN: ${{ github.token }}
- name: Public live Playwright gate
if: steps.branch-head.outputs.current == 'true'
id: public_live
timeout-minutes: 10
run: npm run test:e2e:live:public
env:
NODE_OPTIONS: --use-system-ca
- name: Credentialed live Playwright gate (when configured)
if: steps.branch-head.outputs.current == 'true'
id: credentialed_live
timeout-minutes: 15
run: npm run test:e2e:live:roles
env:
NODE_OPTIONS: --use-system-ca
PLAYWRIGHT_USER_CUSTOMER_NUMBER: ${{ secrets.PLAYWRIGHT_USER_CUSTOMER_NUMBER }}
PLAYWRIGHT_USER_PASSWORD: ${{ secrets.PLAYWRIGHT_USER_PASSWORD }}
PLAYWRIGHT_USER_OTP_SECRET: ${{ secrets.PLAYWRIGHT_USER_OTP_SECRET }}
PLAYWRIGHT_OPERATOR_USER_ID: ${{ secrets.PLAYWRIGHT_OPERATOR_USER_ID }}
PLAYWRIGHT_OPERATOR_PASSWORD: ${{ secrets.PLAYWRIGHT_OPERATOR_PASSWORD }}
PLAYWRIGHT_DEPARTMENT_ID: ${{ secrets.PLAYWRIGHT_DEPARTMENT_ID }}
- name: Roll back after live verification failure
if: >-
failure() && steps.branch-head.outputs.current == 'true' &&
steps.deploy.outcome == 'success' &&
(steps.public_live.outcome == 'failure' || steps.credentialed_live.outcome == 'failure')
timeout-minutes: 10
run: node scripts/release/deploy-cpanel.mjs --rollback
env:
NODE_OPTIONS: --use-system-ca
RELEASE_ROLLBACK_TARGET: ${{ steps.deploy.outputs.rollback_target }}
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
- name: Record Release Manager gate
if: steps.branch-head.outputs.current == 'true'
continue-on-error: true
timeout-minutes: 5
run: |
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
release_gate_build_id="${RELEASE_VERIFIED_BUILD_ID:-$RELEASE_EXPECTED_BUILD_ID}"
curl --fail --show-error --silent \
-X POST "$RELEASE_MANAGER_GATE_URL" \
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"master\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$release_gate_build_id\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":false,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"api_gateway\"]}"
env:
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
RELEASE_REPOSITORY: ${{ github.repository }}
RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Update server version after verification
if: steps.branch-head.outputs.current == 'true'
run: npm run release:update-server-version
env:
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
RELEASE_VERSION: ${{ github.event.workflow_run.head_sha }}
- name: Create verified frontend release proof
if: steps.branch-head.outputs.current == 'true'
shell: bash
run: |
set -euo pipefail
proof_dir="output/frontend-release-proof"
mkdir -p "$proof_dir"
PROOF_PATH="$proof_dir/frontend-release-proof.json" node <<'NODE'
const { writeFileSync } = require("node:fs");
const required = (name) => {
if (!process.env[name]) throw new Error(`Missing ${name}`);
return process.env[name];
};
const proof = {
schemaVersion: 1,
repository: required("GITHUB_REPOSITORY"),
sourceSha: required("RELEASE_COMMIT_SHA").toLowerCase(),
testedWorkflowRunId: required("TESTED_WORKFLOW_RUN_ID"),
frontendReleaseRunId: required("GITHUB_RUN_ID"),
frontendReleaseRunAttempt: required("GITHUB_RUN_ATTEMPT"),
buildId: required("RELEASE_BUILD_ID"),
livePublicGate: "passed",
liveCredentialedGate: "passed",
releaseManagerGate: "passed",
serverVersionUpdated: true,
completedAt: new Date().toISOString(),
};
writeFileSync(process.env.PROOF_PATH, `${JSON.stringify(proof, null, 2)}\n`, { mode: 0o600 });
NODE
env:
TESTED_WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
- name: Publish verified frontend release proof
if: steps.branch-head.outputs.current == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: frontend-release-proof-${{ env.RELEASE_COMMIT_SHA }}
path: output/frontend-release-proof/frontend-release-proof.json
if-no-files-found: error
retention-days: 30
- name: Upload Playwright report
if: failure() && steps.branch-head.outputs.current == 'true'
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: frontend-release-playwright-${{ env.RELEASE_BUILD_ID }}
path: output/playwright
if-no-files-found: ignore
retention-days: 14
+728 -66
View File
@@ -3,7 +3,44 @@ name: Automated Tests
on:
pull_request:
push:
branches:
- main
- master
- dev
workflow_dispatch:
inputs:
mode:
description: "What to run for a manual dispatch."
required: true
type: choice
default: full
options:
- full
- targeted
- targeted-then-full
target_specs:
description: "Comma- or newline-separated Playwright spec paths under tests/e2e."
required: false
type: string
default: "tests/e2e/superuser-department-overview.spec.js"
target_projects:
description: "JSON array of Playwright projects for targeted mode."
required: false
type: string
default: '["chromium-desktop","chromium-mobile","chromium-tablet","webkit-mobile","webkit-desktop"]'
target_grep:
description: "Optional Playwright grep pattern for targeted mode."
required: false
type: string
default: ""
runner:
description: "Runner pool for this manually dispatched test run"
required: false
default: "self-hosted"
type: choice
options:
- self-hosted
- github-hosted
schedule:
- cron: "0 2 * * *"
@@ -11,22 +48,44 @@ permissions:
contents: read
concurrency:
group: frontend-tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: frontend-tests-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.event_name == 'push' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
# Repository variables used as CI runner and credit controls:
# - FRONTEND_CI_STANDARD_RUNNER: JSON runs-on value for format/build/unit jobs.
# - FRONTEND_CI_E2E_RUNNER: JSON runs-on value for Playwright jobs.
# - FRONTEND_CI_PR_E2E_MAX_PARALLEL: numeric Playwright PR job parallelism.
# - FRONTEND_CI_FULL_E2E_MAX_PARALLEL: numeric full-suite job parallelism.
# - FRONTEND_CI_FULL_E2E_CONCURRENT_MAX_PARALLEL: full-suite parallelism while PR E2E runs beside it.
# GitHub-hosted target: ["ubuntu-24.04"], with PR parallelism 10, concurrent full parallelism 26,
# and standalone scheduled full parallelism 36. This keeps the workflow peak at 36 hosted jobs.
jobs:
format-tests:
# Match the labels exposed by the Coolify-managed GitHub runner.
runs-on: [self-hosted, Linux, X64, default]
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
timeout-minutes: 15
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
if [[ -n "$foreign_entry" ]]; then
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
rm -rf "$trash" 2>/dev/null || true
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
mkdir -p "$GITHUB_WORKSPACE"
fi
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Check AI workflow sync
run: node scripts/sync-ai-workflow.mjs --check
@@ -37,119 +96,722 @@ jobs:
- name: Check frontend test formatting
run: npm run format:tests:check
build-and-unit:
needs: format-tests
runs-on: [self-hosted, Linux, X64, default]
quality-checks:
name: Quality-${{ matrix.check }}
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
timeout-minutes: 30
strategy:
fail-fast: false
max-parallel: 5
matrix:
check: [lint, i18n, build, unit-fast, unit-serial]
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
if [[ -n "$foreign_entry" ]]; then
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
rm -rf "$trash" 2>/dev/null || true
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
mkdir -p "$GITHUB_WORKSPACE"
fi
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Build sanity check
run: npm run build
- name: Unit tests
run: npm run test:unit
- name: Run quality check
shell: bash
env:
VITEST_BATCH_SIZE: 5
MATRIX_CHECK: ${{ matrix.check }}
run: |
set -euo pipefail
case "$MATRIX_CHECK" in
lint)
npm run lint
;;
i18n)
npm run i18n:v2:check
;;
build)
npm run build
;;
unit-fast)
npm run text:check-encoding
npm run test:unit:fast
;;
unit-serial)
VITEST_BATCH_SIZE=5 npm run test:unit:serial
;;
*)
echo "Unsupported quality check: $MATRIX_CHECK" >&2
exit 1
;;
esac
e2e-smoke:
if: github.event_name != 'schedule'
build-and-unit:
if: ${{ always() }}
name: Build and unit summary
needs: [format-tests, quality-checks]
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
timeout-minutes: 5
steps:
- name: Verify quality jobs succeeded
shell: bash
env:
FORMAT_TESTS_RESULT: ${{ needs.format-tests.result }}
QUALITY_CHECKS_RESULT: ${{ needs.quality-checks.result }}
run: |
set -euo pipefail
failed=0
for required_job in FORMAT_TESTS_RESULT QUALITY_CHECKS_RESULT; do
result="${!required_job:-missing}"
if [[ "$result" != "success" ]]; then
echo "${required_job}=${result}" >&2
failed=1
fi
done
exit "$failed"
e2e-targeted:
if: >
github.event_name == 'workflow_dispatch' &&
(inputs.mode == 'targeted' || inputs.mode == 'targeted-then-full')
needs: build-and-unit
runs-on: [self-hosted, Linux, X64, default]
name: E2E-targeted-${{ matrix.project }}
# Use GitHub-hosted runners to avoid self-hosted desktop contention and sleep/power events.
runs-on: ubuntu-24.04
timeout-minutes: 35
strategy:
fail-fast: false
matrix:
project: [chromium-desktop, chromium-mobile]
project: ${{ fromJSON(inputs.target_projects || '["chromium-desktop"]') }}
env:
MATRIX_PROJECT: ${{ matrix.project }}
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-targeted-${{ matrix.project }}
PLAYWRIGHT_REPORTER_MODE: line-html
PLAYWRIGHT_WORKERS: 1
PLAYWRIGHT_VIDEO_MODE: on-first-retry
TARGET_GREP: ${{ inputs.target_grep }}
TARGET_SPECS: ${{ inputs.target_specs }}
RUN_ID: ${{ github.run_id }}
steps:
- name: Normalize workspace permissions
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
if [[ -n "$foreign_entry" ]]; then
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
rm -rf "$trash" 2>/dev/null || true
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
mkdir -p "$GITHUB_WORKSPACE"
fi
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Run targeted Playwright specs in container
shell: bash
run: |
set -euo pipefail
case "$MATRIX_PROJECT" in
chromium-mobile) project_offset=1 ;;
chromium-desktop) project_offset=2 ;;
chromium-tablet) project_offset=3 ;;
webkit-mobile) project_offset=31 ;;
webkit-desktop) project_offset=32 ;;
webkit-tablet) project_offset=33 ;;
firefox-mobile) project_offset=61 ;;
firefox-desktop) project_offset=62 ;;
firefox-tablet) project_offset=63 ;;
*) echo "Unsupported Playwright project: $MATRIX_PROJECT" >&2; exit 1 ;;
esac
port_seed=$((20000 + (RUN_ID % 20000) + project_offset))
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
mkdir -p "$lock_root"
chmod 1777 "$lock_root" 2>/dev/null || true
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
playwright_port_lock=""
playwright_dev_port=""
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
lock_dir="${lock_root}/${candidate}.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
continue
fi
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
rmdir "$lock_dir" || true
continue
fi
playwright_port_lock="$lock_dir"
playwright_dev_port="$candidate"
break
done
if [[ -z "$playwright_dev_port" ]]; then
echo "Unable to find a free Playwright dev-server port." >&2
exit 1
fi
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
if docker info >/dev/null 2>&1; then
docker_cmd=(docker)
elif sudo -n docker info >/dev/null 2>&1; then
docker_cmd=(sudo docker)
else
echo "Docker is not available to the runner user, and sudo docker is not available." >&2
exit 1
fi
mkdir -p output/playwright
scripts/ci/runner-diagnostics.sh "before targeted Playwright ${MATRIX_PROJECT}" -- "${docker_cmd[@]}"
SYSTEMD_INHIBIT_REASON="Frontend targeted Playwright ${MATRIX_PROJECT}" \
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
--volume "$PWD:/source:ro" \
--volume "$PWD/output/playwright:/work/output/playwright" \
--workdir /work \
--env HOME=/tmp \
--env CI="${CI:-}" \
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
--env TARGET_GREP="$TARGET_GREP" \
--env TARGET_SPECS="$TARGET_SPECS" \
mcr.microsoft.com/playwright:v1.58.2-noble \
bash -lc '
set -euo pipefail
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
git config --global --add safe.directory /work
install_dependencies() {
local attempt
for attempt in 1 2 3; do
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
return 0
fi
if [[ "$attempt" == "3" ]]; then
return 1
fi
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
sleep 20
done
}
install_dependencies
ulimit -n 16384 || true
mapfile -t spec_args < <(printf "%s\n" "$TARGET_SPECS" | tr "," "\n" | sed "s/^[[:space:]]*//;s/[[:space:]]*$//;/^$/d")
if [[ "${#spec_args[@]}" -eq 0 && -z "${TARGET_GREP:-}" ]]; then
echo "Provide at least one spec path or grep pattern." >&2
exit 1
fi
for spec_path in "${spec_args[@]}"; do
if [[ "$spec_path" == /* || "$spec_path" == *".."* || "$spec_path" != tests/e2e/* ]]; then
echo "Targeted spec must stay under tests/e2e: $spec_path" >&2
exit 1
fi
if [[ ! -f "$spec_path" ]]; then
echo "Targeted spec does not exist: $spec_path" >&2
exit 1
fi
done
args=("${spec_args[@]}")
if [[ -n "${TARGET_GREP:-}" ]]; then
args+=(--grep "$TARGET_GREP")
fi
args+=(--project="$MATRIX_PROJECT")
npx playwright test "${args[@]}"
'
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run Playwright smoke tests (${{ matrix.project }})
run: npx playwright test --grep @smoke --project="${{ matrix.project }}"
- name: Runner diagnostics after Playwright failure
if: failure() || cancelled()
continue-on-error: true
run: scripts/ci/runner-diagnostics.sh "after targeted Playwright ${{ matrix.project }}"
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
if: failure() || cancelled()
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-smoke-${{ matrix.project }}
name: playwright-report-targeted-${{ matrix.project }}
path: |
output/playwright/report
output/playwright/test-results
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-*
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}
if-no-files-found: ignore
retention-days: 7
retention-days: 1
e2e-full:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch
needs: build-and-unit
name: E2E-full-${{ matrix.role }}-${{ matrix.browser_label }}-${{ matrix.device }}
runs-on: [self-hosted, Linux, X64, default]
e2e-pr:
if: >
always() &&
github.event_name != 'schedule' &&
needs.build-and-unit.result == 'success' &&
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
(
github.event_name != 'workflow_dispatch' ||
inputs.mode == 'full' ||
needs.e2e-targeted.result == 'success'
)
needs: [build-and-unit, e2e-targeted]
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
runs-on: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
timeout-minutes: 45
strategy:
fail-fast: false
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_PR_E2E_MAX_PARALLEL || '2') }}
matrix:
role: [customer, subuser, admin, superuser]
browser: [chromium, firefox, webkit]
device: [mobile, tablet, desktop]
suite: [changed-1-of-2, changed-2-of-2, smoke, pr, ct]
project: [chromium-desktop, chromium-mobile]
env:
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }}
PLAYWRIGHT_REPORTER_MODE: line-html
PLAYWRIGHT_WORKERS: 1
PLAYWRIGHT_VIDEO_MODE: on-first-retry
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
if [[ -n "$foreign_entry" ]]; then
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
rm -rf "$trash" 2>/dev/null || true
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
mkdir -p "$GITHUB_WORKSPACE"
fi
fi
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Resolve Playwright diff refs
id: playwright-diff
shell: bash
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
EVENT_NAME: ${{ github.event_name }}
HEAD_SHA: ${{ github.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
zero_sha="0000000000000000000000000000000000000000"
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
base_ref="$PR_BASE_SHA"
head_ref="$PR_HEAD_SHA"
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
base_ref="origin/$DEFAULT_BRANCH"
head_ref="$HEAD_SHA"
else
base_ref="$PUSH_BEFORE_SHA"
head_ref="$HEAD_SHA"
fi
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_HEAD_SHA" ]]; then
head_ref="$PR_HEAD_SHA"
else
head_ref="$HEAD_SHA"
fi
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
echo "head=$head_ref" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 22
- name: Run Playwright PR suite in container
shell: bash
env:
DIFF_BASE_REF: ${{ steps.playwright-diff.outputs.base }}
DIFF_HEAD_REF: ${{ steps.playwright-diff.outputs.head }}
MATRIX_SUITE: ${{ matrix.suite }}
MATRIX_PROJECT: ${{ matrix.project }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
case "$MATRIX_SUITE" in
changed-1-of-2) suite_offset=0 ;;
changed-2-of-2) suite_offset=10 ;;
smoke) suite_offset=20 ;;
pr) suite_offset=30 ;;
ct) suite_offset=40 ;;
*) echo "Unsupported Playwright PR suite: $MATRIX_SUITE" >&2; exit 1 ;;
esac
case "$MATRIX_PROJECT" in
chromium-desktop) project_offset=1 ;;
chromium-mobile) project_offset=2 ;;
*) echo "Unsupported Playwright PR project: $MATRIX_PROJECT" >&2; exit 1 ;;
esac
port_seed=$((21000 + (RUN_ID % 20000) + suite_offset + project_offset))
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
mkdir -p "$lock_root"
chmod 1777 "$lock_root" 2>/dev/null || true
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
playwright_port_lock=""
playwright_dev_port=""
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
lock_dir="${lock_root}/${candidate}.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
continue
fi
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
rmdir "$lock_dir" || true
continue
fi
playwright_port_lock="$lock_dir"
playwright_dev_port="$candidate"
break
done
if [[ -z "$playwright_dev_port" ]]; then
echo "Unable to find a free Playwright dev-server port." >&2
exit 1
fi
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
if docker info >/dev/null 2>&1; then
docker_cmd=(docker)
elif sudo -n docker info >/dev/null 2>&1; then
docker_cmd=(sudo docker)
else
echo "Docker is not available to the runner user, and sudo docker is not available." >&2
exit 1
fi
mkdir -p output/playwright
scripts/ci/runner-diagnostics.sh "before Playwright PR ${MATRIX_SUITE}/${MATRIX_PROJECT}" -- "${docker_cmd[@]}"
SYSTEMD_INHIBIT_REASON="Frontend Playwright PR ${MATRIX_SUITE}/${MATRIX_PROJECT}" \
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
--volume "$PWD:/source:ro" \
--volume "$PWD/output/playwright:/work/output/playwright" \
--workdir /work \
--env HOME=/tmp \
--env CI="${CI:-}" \
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
--env MATRIX_SUITE="$MATRIX_SUITE" \
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
--env DIFF_BASE_REF="$DIFF_BASE_REF" \
--env DIFF_HEAD_REF="$DIFF_HEAD_REF" \
mcr.microsoft.com/playwright:v1.58.2-noble \
bash -lc '
set -euo pipefail
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
git config --global --add safe.directory /work
install_dependencies() {
local attempt
for attempt in 1 2 3; do
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
return 0
fi
if [[ "$attempt" == "3" ]]; then
return 1
fi
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
sleep 20
done
}
install_dependencies
ulimit -n 16384 || true
case "$MATRIX_SUITE" in
ct)
npm run test:ct -- --project="$MATRIX_PROJECT"
;;
smoke)
npx playwright test --grep @smoke --project="$MATRIX_PROJECT"
;;
pr)
npm run test:e2e:pr -- --core-only --project="$MATRIX_PROJECT"
;;
changed-1-of-2)
npm run test:e2e:pr -- --changed-only --project="$MATRIX_PROJECT" --base="$DIFF_BASE_REF" --head="$DIFF_HEAD_REF" -- --shard=1/2 --pass-with-no-tests
;;
changed-2-of-2)
npm run test:e2e:pr -- --changed-only --project="$MATRIX_PROJECT" --base="$DIFF_BASE_REF" --head="$DIFF_HEAD_REF" -- --shard=2/2 --pass-with-no-tests
;;
*)
echo "Unsupported Playwright PR suite: $MATRIX_SUITE" >&2
exit 1
;;
esac
'
- name: Runner diagnostics after Playwright failure
if: failure() || cancelled()
continue-on-error: true
run: scripts/ci/runner-diagnostics.sh "after Playwright PR ${{ matrix.suite }}/${{ matrix.project }}"
- name: Upload Playwright report
if: failure() || cancelled()
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-pr-${{ matrix.suite }}-${{ matrix.project }}
path: |
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-*
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}
if-no-files-found: ignore
retention-days: 1
required-ci:
if: ${{ always() && (github.event_name == 'pull_request' || github.event_name == 'push') }}
name: Required CI
needs: [format-tests, build-and-unit, e2e-pr]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify required jobs succeeded
shell: bash
env:
FORMAT_TESTS_RESULT: ${{ needs.format-tests.result }}
BUILD_AND_UNIT_RESULT: ${{ needs.build-and-unit.result }}
E2E_PR_RESULT: ${{ needs.e2e-pr.result }}
run: |
set -euo pipefail
failed=0
for required_job in FORMAT_TESTS_RESULT BUILD_AND_UNIT_RESULT E2E_PR_RESULT; do
result="${!required_job:-missing}"
if [[ "$result" != "success" ]]; then
echo "${required_job}=${result}" >&2
failed=1
fi
done
exit "$failed"
e2e-full:
if: >
always() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch) &&
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
needs.build-and-unit.result == 'success' &&
(
github.event_name != 'workflow_dispatch' ||
inputs.mode == 'full' ||
needs.e2e-targeted.result == 'success'
)
needs: [build-and-unit, e2e-targeted]
name: E2E-full-${{ matrix.browser_label }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
runs-on: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
timeout-minutes: 60
strategy:
fail-fast: false
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_FULL_E2E_MAX_PARALLEL || '36') > 0 && github.event_name == 'schedule' && fromJSON(vars.FRONTEND_CI_FULL_E2E_MAX_PARALLEL || '36') || fromJSON(vars.FRONTEND_CI_FULL_E2E_CONCURRENT_MAX_PARALLEL || '26') }}
matrix:
device: [mobile, desktop, tablet]
role: [superuser, admin, customer, subuser]
shard_index: [1, 2]
browser: [chromium, webkit, firefox]
exclude:
- role: customer
shard_index: 2
- role: subuser
shard_index: 2
include:
- browser: chromium
browser_label: Chromium
browser_install: chromium
- browser: firefox
browser_label: Firefox
browser_install: firefox
- browser: webkit
browser_label: WebKit
browser_install: webkit
- browser: firefox
browser_label: Firefox
browser_install: firefox
env:
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-full-${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
PLAYWRIGHT_REPORTER_MODE: line-html
PLAYWRIGHT_WORKERS: 1
PLAYWRIGHT_VIDEO_MODE: off
steps:
- name: Repair self-hosted workspace permissions
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
shell: bash
run: |
if [[ -d "$GITHUB_WORKSPACE" ]]; then
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
if [[ -n "$foreign_entry" ]]; then
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
rm -rf "$trash" 2>/dev/null || true
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
mkdir -p "$GITHUB_WORKSPACE"
fi
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Run full Playwright slice in container
shell: bash
env:
MATRIX_ROLE: ${{ matrix.role }}
MATRIX_BROWSER: ${{ matrix.browser }}
MATRIX_DEVICE: ${{ matrix.device }}
MATRIX_SHARD_INDEX: ${{ matrix.shard_index }}
MATRIX_SHARD_TOTAL: ${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
case "$MATRIX_ROLE" in
superuser) role_offset=0 ;;
admin) role_offset=100 ;;
customer) role_offset=200 ;;
subuser) role_offset=300 ;;
*) echo "Unsupported Playwright role: $MATRIX_ROLE" >&2; exit 1 ;;
esac
case "$MATRIX_BROWSER" in
chromium) browser_offset=0 ;;
webkit) browser_offset=30 ;;
firefox) browser_offset=60 ;;
*) echo "Unsupported Playwright browser: $MATRIX_BROWSER" >&2; exit 1 ;;
esac
case "$MATRIX_DEVICE" in
mobile) device_offset=1 ;;
desktop) device_offset=2 ;;
tablet) device_offset=3 ;;
*) echo "Unsupported Playwright device: $MATRIX_DEVICE" >&2; exit 1 ;;
esac
case "$MATRIX_SHARD_INDEX" in
1) shard_offset=0 ;;
2) shard_offset=400 ;;
*) echo "Unsupported Playwright shard index: $MATRIX_SHARD_INDEX" >&2; exit 1 ;;
esac
port_seed=$((22000 + (RUN_ID % 20000) + role_offset + browser_offset + device_offset + shard_offset))
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
mkdir -p "$lock_root"
chmod 1777 "$lock_root" 2>/dev/null || true
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
playwright_port_lock=""
playwright_dev_port=""
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
lock_dir="${lock_root}/${candidate}.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
continue
fi
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
rmdir "$lock_dir" || true
continue
fi
playwright_port_lock="$lock_dir"
playwright_dev_port="$candidate"
break
done
if [[ -z "$playwright_dev_port" ]]; then
echo "Unable to find a free Playwright dev-server port." >&2
exit 1
fi
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
if docker info >/dev/null 2>&1; then
docker_cmd=(docker)
elif sudo -n docker info >/dev/null 2>&1; then
docker_cmd=(sudo docker)
else
echo "Docker is not available to the runner user, and sudo docker is not available." >&2
exit 1
fi
mkdir -p output/playwright
scripts/ci/runner-diagnostics.sh "before Playwright full ${MATRIX_BROWSER}/${MATRIX_DEVICE}/${MATRIX_ROLE}/shard-${MATRIX_SHARD_INDEX}-of-${MATRIX_SHARD_TOTAL}" -- "${docker_cmd[@]}"
SYSTEMD_INHIBIT_REASON="Frontend Playwright full ${MATRIX_BROWSER}/${MATRIX_DEVICE}/${MATRIX_ROLE}/shard-${MATRIX_SHARD_INDEX}-of-${MATRIX_SHARD_TOTAL}" \
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
--volume "$PWD:/source:ro" \
--volume "$PWD/output/playwright:/work/output/playwright" \
--workdir /work \
--env HOME=/tmp \
--env CI="${CI:-}" \
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
--env MATRIX_ROLE="$MATRIX_ROLE" \
--env MATRIX_BROWSER="$MATRIX_BROWSER" \
--env MATRIX_DEVICE="$MATRIX_DEVICE" \
--env MATRIX_SHARD_INDEX="$MATRIX_SHARD_INDEX" \
--env MATRIX_SHARD_TOTAL="$MATRIX_SHARD_TOTAL" \
mcr.microsoft.com/playwright:v1.58.2-noble \
bash -lc '
set -euo pipefail
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
git config --global --add safe.directory /work
install_dependencies() {
local attempt
for attempt in 1 2 3; do
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
return 0
fi
if [[ "$attempt" == "3" ]]; then
return 1
fi
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
sleep 20
done
}
install_dependencies
ulimit -n 16384 || true
npm run test:e2e:full:slice -- --role="$MATRIX_ROLE" --project="$MATRIX_BROWSER-$MATRIX_DEVICE" --shard="$MATRIX_SHARD_INDEX/$MATRIX_SHARD_TOTAL"
'
- name: Install Playwright browsers
run: npx playwright install --with-deps ${{ matrix.browser_install }}
- name: Run full Playwright slice
run: npm run test:e2e:full:slice -- --role="${{ matrix.role }}" --project="${{ matrix.browser }}-${{ matrix.device }}"
- name: Runner diagnostics after Playwright failure
if: failure() || cancelled()
continue-on-error: true
run: scripts/ci/runner-diagnostics.sh "after Playwright full ${{ matrix.browser }}/${{ matrix.device }}/${{ matrix.role }}/shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}"
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
if: failure() || cancelled()
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}
name: playwright-report-full-${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
path: |
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/report
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/test-results
output/playwright/test-lists/${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}.txt
output/playwright/test-lists/${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}.txt
output/playwright/test-lists/${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}.txt
if-no-files-found: ignore
retention-days: 14
retention-days: 1
full-e2e-summary:
if: >
always() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch) &&
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted')
name: Full E2E summary
needs: [e2e-full]
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify full E2E succeeded
shell: bash
env:
FULL_E2E_RESULT: ${{ needs.e2e-full.result }}
run: |
set -euo pipefail
if [[ "$FULL_E2E_RESULT" != "success" ]]; then
echo "E2E_FULL_RESULT=${FULL_E2E_RESULT:-missing}" >&2
exit 1
fi
+10
View File
@@ -8,12 +8,22 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
node_modules.codex-permission-blocked-*
.DS_Store
dist
dist-ssr
coverage
*.local
dev-dist
.playwright-cli/
# Mobile build and signing outputs
/app/build/
/app-release*.aab
/app-release*.apk
/app-release*.idsig
*.keystore
*.jks
/cypress/videos/
/cypress/screenshots/
View File
View File
+1
View File
@@ -0,0 +1 @@
3.0
View File
+4
View File
@@ -0,0 +1,4 @@
{
"version": 1,
"setupCompletedAt": "2026-06-07T11:37:04.444Z"
}
+26
View File
@@ -0,0 +1,26 @@
FROM node:24-alpine AS build
WORKDIR /app
RUN apk add --no-cache git
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY . .
ARG SOURCE_COMMIT
ARG RELEASE_COMMIT_SHA
ARG COMMIT_SHA
ARG GITHUB_SHA
ENV SOURCE_COMMIT=$SOURCE_COMMIT
ENV RELEASE_COMMIT_SHA=$RELEASE_COMMIT_SHA
ENV COMMIT_SHA=$COMMIT_SHA
ENV GITHUB_SHA=$GITHUB_SHA
RUN npm run build
FROM nginx:1.27-alpine
COPY nginx.coolify-frontend.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
+4
View File
@@ -0,0 +1,4 @@
source "https://rubygems.org"
ruby ">= 3.2", "< 3.5"
gem "fastlane", "2.237.0"
+248
View File
@@ -0,0 +1,248 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.8)
abbrev (0.1.2)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1271.0)
aws-sdk-core (3.254.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.130.0)
aws-sdk-core (~> 3, >= 3.254.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.228.0)
aws-sdk-core (~> 3, >= 3.254.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.1.2)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (1.6.0)
logger
faraday (1.10.6)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.4)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.1)
fastlane (2.237.0)
CFPropertyList (>= 2.3, < 5.0.0)
abbrev (~> 0.1)
addressable (>= 2.9.0, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0)
base64 (~> 0.2)
benchmark (>= 0.1.0)
bundler (>= 2.4.0, < 5.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 2.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.1.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.3.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.10.3, < 4)
logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_json (~> 1.12)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3)
naturally (~> 2.2)
nkf (~> 0.2)
optparse (>= 0.1.1, < 1.0.0)
ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.105.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0)
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.28.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-playcustomapp_v1 (0.18.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.65.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.9.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (2.2.2)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.7.0)
google-cloud-storage (1.62.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (>= 0.18, < 2)
google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6)
googleauth (~> 1.9)
mini_mime (~> 1.0)
google-logging-utils (0.2.0)
googleauth (1.17.1)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 4.0)
os (>= 0.9, < 2.0)
pstore (~> 0.1)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.21.1)
jwt (3.2.0)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.21.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.3.0)
optparse (0.8.1)
os (1.1.4)
ostruct (0.6.3)
plist (3.7.2)
pstore (0.2.1)
public_suffix (7.0.5)
rake (13.4.2)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.8.0)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.22.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 4.0)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.28.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
base64
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
nkf
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
fastlane (= 2.237.0)
RUBY VERSION
ruby 3.3.12p206
BUNDLED WITH
2.5.22
+105 -7
View File
@@ -16,12 +16,52 @@ See [Vite Configuration Reference](https://vite.dev/config/).
npm install
```
## Contributing Changes
Create a scoped feature branch, push it, and open a pull request targeting
`master`. Do not push directly to `master`. Merge only after the `Required CI`
check succeeds, all review conversations are resolved, and the branch is up to
date. Use squash merge so `master` retains linear history.
See [`.github/BRANCH_PROTECTION.md`](.github/BRANCH_PROTECTION.md) for the
repository policy, rollout checks, and emergency bypass procedure.
### Compile and Hot-Reload for Development
```sh
npm run dev
```
By default, the Vite dev server proxies `/api/*` to the remote stable API at
`https://api-v2.truckwash.io/master/api`. This lets the Vue app run locally
without a local PHP API container.
To develop against a local PHP API instead:
```powershell
$env:VITE_API_PROXY_TARGET="http://localhost"; npm run dev
```
To use another remote API route:
```powershell
$env:VITE_API_PROXY_BASE_PATH="/canary/api"; npm run dev
```
TLS certificate validation is enabled for proxied HTTPS APIs by default. If you
are using a trusted local HTTPS API with a self-signed certificate, you can opt
out explicitly:
```powershell
$env:VITE_API_PROXY_TARGET="https://local-api.test"; $env:VITE_API_PROXY_SECURE="false"; npm run dev
```
For compatible local gateways that expect the `/api` prefix to be preserved:
```powershell
$env:VITE_API_PROXY_TARGET="http://localhost"; $env:VITE_API_PROXY_STRIP_PREFIX="false"; npm run dev
```
### Compile and Minify for Production
```sh
@@ -66,16 +106,22 @@ Artifacts and summaries:
## Playwright Full E2E
Run the permanent grouped full-suite entrypoint with a hard max of 5 total workers across:
Run the permanent grouped full-suite entrypoint with a hard max of 5 total workers across the browser-engine groups:
- `chromium-(desktop|tablet|mobile)`
- `firefox-(desktop|tablet|mobile)`
- `webkit-(desktop|tablet|mobile)`
- Chromium
- WebKit
- Firefox
```sh
npm run test:e2e:ci
```
The full CI matrix is ordered by browser engine, then device class, then user role:
- browsers: `chromium`, `webkit`, `firefox`
- devices: `mobile`, `desktop`, `tablet`
- roles: `superuser`, `admin`, `customer`, `subuser`
Run a single full-suite slice for one role and one Playwright project:
```sh
@@ -87,7 +133,7 @@ Default worker allocation:
```sh
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=2
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=1
```
Optional overrides:
@@ -96,18 +142,70 @@ Optional overrides:
PLAYWRIGHT_PARALLEL_BASE_PORT=5191
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=2
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=1
```
The runner fails fast if the combined worker count exceeds 5.
GitHub Actions keeps the full browser/device/role matrix stable by capping full-suite
matrix parallelism at 2 jobs, running each full slice with `PLAYWRIGHT_WORKERS=1`,
wrapping Docker Playwright runs with `systemd-inhibit` when available, and setting
`PLAYWRIGHT_VIDEO_MODE=off` for the full matrix. The E2E network harness serves
Font Awesome from local fixtures so WebKit page loads and visual snapshots do not
depend on CDN/TLS availability. Traces and screenshots are still retained on
failure.
Artifacts and summaries:
- `output/playwright/ci-parallel-report/index.html`
- `output/playwright/ci-parallel-chromium/report/index.html`
- `output/playwright/ci-parallel-firefox/report/index.html`
- `output/playwright/ci-parallel-webkit/report/index.html`
- `output/playwright/test-lists/<role>-<project>.txt`
- `output/playwright/test-lists/<project>-<role>.txt`
- `output/playwright/test-lists/<role>-<project>.txt` (legacy compatibility copy)
## Android App Icon
The Play Store Android package is built from the Capacitor project in `android/`.
The legacy Bubblewrap/TWA project at the repository root is not used by
`npm run mobile:android:bundle`.
The native launcher and store icons use the opaque iOS marketing icon as their
shared master so Android and iOS keep the same white background:
```text
ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png
```
Regenerate the checked-in launcher assets after changing that source image:
```sh
npm run mobile:android:icons
```
Check that the generated Android launcher assets are current:
```sh
npm run mobile:android:icons:check
```
`npm run mobile:android:sync` runs the icon generator before building and syncing
the Capacitor Android project. The generator updates `android/app/src/main/res`
launcher assets, `public/icons/icon-192x192.png`, `public/icons/icon-512x512.png`,
and `store_icon.png`.
## Mobile Store Releases
Signed Android and iOS store artifacts are built through the GitHub Actions
`Mobile Store Artifacts` workflow. By default, current `master` after green
`Automated Tests` uploads Android to Google Play production and uploads iOS to
App Store Connect.
See `docs/mobile-artifacts.md` for workflow triggers, required secrets, and
local mobile checks. See `docs/app-store-release.md` for App Store Connect
release preparation and review notes. For a separate development-signed IPA
that can be installed on an approved iPhone from Ubuntu over USB, see
`docs/ios-device-debug.md`.
## Bubblewrap (TWA) Build and Install
BIN
View File
Binary file not shown.
+101
View File
@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
+2
View File
@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep
+77
View File
@@ -0,0 +1,77 @@
apply plugin: 'com.android.application'
def mobileEnv = { String name, String defaultValue = null ->
return System.getenv(name) ?: (project.hasProperty(name) ? project.property(name).toString() : defaultValue)
}
def releaseStoreFile = mobileEnv('ANDROID_KEYSTORE_FILE')
def hasReleaseSigning = releaseStoreFile &&
mobileEnv('ANDROID_KEYSTORE_PASSWORD') &&
mobileEnv('ANDROID_KEY_ALIAS') &&
mobileEnv('ANDROID_KEY_PASSWORD')
android {
namespace = "io.truckwash.twa"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "io.truckwash.twa"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(mobileEnv('MOBILE_VERSION_CODE', '1'))
versionName mobileEnv('MOBILE_VERSION_NAME', '1.0')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
signingConfigs {
release {
if (hasReleaseSigning) {
storeFile file(releaseStoreFile)
storePassword mobileEnv('ANDROID_KEYSTORE_PASSWORD')
keyAlias mobileEnv('ANDROID_KEY_ALIAS')
keyPassword mobileEnv('ANDROID_KEY_PASSWORD')
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (hasReleaseSigning) {
signingConfig signingConfigs.release
}
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
+19
View File
@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-geolocation')
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,26 @@
package io.truckwash.twa;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("io.truckwash.twa", appContext.getPackageName());
}
}
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
@@ -0,0 +1,5 @@
package io.truckwash.twa;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Truck Wash</string>
<string name="title_activity_main">Truck Wash</string>
<string name="package_name">io.truckwash.twa</string>
<string name="custom_url_scheme">io.truckwash.twa</string>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -0,0 +1,18 @@
package io.truckwash.twa;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
+29
View File
@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
+6
View File
@@ -0,0 +1,6 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-geolocation'
project(':capacitor-geolocation').projectDir = new File('../node_modules/@capacitor/geolocation/android')
+22
View File
@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+5
View File
@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'
+16
View File
@@ -0,0 +1,16 @@
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -56,9 +56,9 @@ android {
defaultConfig {
applicationId "io.truckwash.twa"
minSdkVersion 21
targetSdkVersion 35
versionCode 6
versionName "6"
targetSdkVersion 36
versionCode 7
versionName "7"
// The name for the application
resValue "string", "appName", twaManifest.name
@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Automatically generated file. DO NOT MODIFY -->
<!-- Value from default config. -->
<bool name="enableNotification">true</bool>
<!-- Value from default config. -->
<bool name="enableSiteSettingsShortcut">true</bool>
<!-- Value from default config. -->
<color name="backgroundColor">#0787BB</color>
<!-- Value from default config. -->
<color name="colorPrimary">#063651</color>
<!-- Value from default config. -->
<color name="colorPrimaryDark">#000000</color>
<!-- Value from default config. -->
<color name="navigationColor">#000000</color>
<!-- Value from default config. -->
<color name="navigationColorDark">#000000</color>
<!-- Value from default config. -->
<color name="navigationDividerColor">#000000</color>
<!-- Value from default config. -->
<color name="navigationDividerColorDark">#000000</color>
<!-- Value from default config. -->
<integer name="splashScreenFadeOutDuration">300</integer>
<!-- Value from default config. -->
<string name="appName" translatable="false">Truck Wash</string>
<!-- Value from default config. -->
<string name="fallbackType" translatable="false">customtabs</string>
<!-- Value from default config. -->
<string name="fullScopeUrl" translatable="false">https://truckwash.io/</string>
<!-- Value from default config. -->
<string name="generatorApp" translatable="false">bubblewrap-cli</string>
<!-- Value from default config. -->
<string name="hostName" translatable="false">truckwash.io</string>
<!-- Value from default config. -->
<string name="launchHandlerClientMode" translatable="false">navigate-existing</string>
<!-- Value from default config. -->
<string name="launchUrl" translatable="false">https://truckwash.io/</string>
<!-- Value from default config. -->
<string name="launcherName" translatable="false">Truck Wash</string>
<!-- Value from default config. -->
<string name="orientation" translatable="false">portrait</string>
<!-- Value from default config. -->
<string name="providerAuthority" translatable="false">io.truckwash.twa.fileprovider</string>
<!-- Value from default config. -->
<string name="webManifestUrl" translatable="false">https://truckwash.io/manifest.webmanifest</string>
</resources>
@@ -1,55 +0,0 @@
-keep class androidx.core.app.CoreComponentFactory { <init>(); }
-keep class androidx.core.content.FileProvider { <init>(); }
-keep class androidx.profileinstaller.ProfileInstallReceiver { <init>(); }
-keep class androidx.startup.InitializationProvider { <init>(); }
-keep class com.google.android.gms.common.api.GoogleApiActivity { <init>(); }
-keep class com.google.androidbrowserhelper.locationdelegation.PermissionRequestActivity { <init>(); }
-keep class com.google.androidbrowserhelper.trusted.FocusActivity { <init>(); }
-keep class com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity { <init>(); }
-keep class com.google.androidbrowserhelper.trusted.NotificationPermissionRequestActivity { <init>(); }
-keep class com.google.androidbrowserhelper.trusted.WebViewFallbackActivity { <init>(); }
-keep class io.truckwash.twa.Application { <init>(); }
-keep class io.truckwash.twa.DelegationService { <init>(); }
-keep class io.truckwash.twa.LauncherActivity { <init>(); }
-keep class android.widget.Space { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.app.AlertController$RecycleListView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.view.menu.ActionMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.view.menu.ExpandedMenuView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.view.menu.ListMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionBarContainer { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionBarContextView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionBarOverlayLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActionMenuView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ActivityChooserView$InnerLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.AlertDialogLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ButtonBarLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ContentFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.DialogTitle { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.FitWindowsFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.FitWindowsLinearLayout { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.SearchView$SearchAutoComplete { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.Toolbar { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.appcompat.widget.ViewStubCompat { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.browser.browseractions.BrowserActionsFallbackMenuView { <init>(android.content.Context, android.util.AttributeSet); }
-keep class androidx.core.widget.NestedScrollView { <init>(android.content.Context, android.util.AttributeSet); }
@@ -1,2 +0,0 @@
#- File Locator -
listingFile=../../../../outputs/apk/release/output-metadata.json
@@ -1,2 +0,0 @@
appMetadataVersion=1.1
androidGradlePluginVersion=8.9.1
@@ -1,18 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "BUNDLE",
"kind": "RegularFile"
},
"applicationId": "io.truckwash.twa",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"outputFile": "../../../../outputs/bundle/release/app-release.aab"
}
],
"elementType": "File"
}
@@ -1,2 +0,0 @@
#- File Locator -
listingFile=../../../bundle_ide_model/release/produceReleaseBundleIdeListingFile/output-metadata.json
@@ -1,222 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Google Inc. All Rights Reserved.
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.
-->
<!--
The "package" attribute is rewritten by the Gradle build with the value of applicationId.
It is still required here, as it is used to derive paths, for instance when referring
to an Activity by ".MyActivity" instead of the full name. If more Activities are added to the
application, the package attribute will need to reflect the correct path in order to use
the abbreviated format.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.truckwash.twa"
android:versionCode="6"
android:versionName="6" >
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="35" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
<permission
android:name="io.truckwash.twa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
android:protectionLevel="signature" />
<uses-permission android:name="io.truckwash.twa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" />
<application
android:name="io.truckwash.twa.Application"
android:allowBackup="true"
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/appName"
android:manageSpaceActivity="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
android:supportsRtl="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar" >
<meta-data
android:name="asset_statements"
android:resource="@string/assetStatements" />
<meta-data
android:name="web_manifest_url"
android:value="@string/webManifestUrl" />
<meta-data
android:name="twa_generator"
android:value="@string/generatorApp" />
<activity android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity" >
<meta-data
android:name="android.support.customtabs.trusted.MANAGE_SPACE_URL"
android:value="@string/launchUrl" />
</activity>
<activity
android:name="io.truckwash.twa.LauncherActivity"
android:alwaysRetainTaskState="true"
android:exported="true"
android:label="@string/launcherName" >
<meta-data
android:name="android.support.customtabs.trusted.DEFAULT_URL"
android:value="@string/launchUrl" />
<meta-data
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
android:resource="@color/colorPrimary" />
<meta-data
android:name="android.support.customtabs.trusted.LAUNCH_HANDLER_CLIENT_MODE"
android:value="@string/launchHandlerClientMode" />
<meta-data
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR_DARK"
android:resource="@color/colorPrimaryDark" />
<meta-data
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
android:resource="@color/navigationColor" />
<meta-data
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR_DARK"
android:resource="@color/navigationColorDark" />
<meta-data
android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR"
android:resource="@color/navigationDividerColor" />
<meta-data
android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR_DARK"
android:resource="@color/navigationDividerColorDark" />
<meta-data
android:name="android.support.customtabs.trusted.SPLASH_IMAGE_DRAWABLE"
android:resource="@drawable/splash" />
<meta-data
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_BACKGROUND_COLOR"
android:resource="@color/backgroundColor" />
<meta-data
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_FADE_OUT_DURATION"
android:value="@integer/splashScreenFadeOutDuration" />
<meta-data
android:name="android.support.customtabs.trusted.FILE_PROVIDER_AUTHORITY"
android:value="@string/providerAuthority" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<meta-data
android:name="android.support.customtabs.trusted.FALLBACK_STRATEGY"
android:value="@string/fallbackType" />
<meta-data
android:name="android.support.customtabs.trusted.SCREEN_ORIENTATION"
android:value="@string/orientation" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true" >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="@string/hostName"
android:scheme="https" />
</intent-filter>
</activity>
<activity android:name="com.google.androidbrowserhelper.trusted.FocusActivity" />
<activity
android:name="com.google.androidbrowserhelper.trusted.WebViewFallbackActivity"
android:configChanges="orientation|screenSize" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/providerAuthority"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<service
android:name="io.truckwash.twa.DelegationService"
android:enabled="@bool/enableNotification"
android:exported="@bool/enableNotification" >
<meta-data
android:name="android.support.customtabs.trusted.SMALL_ICON"
android:resource="@drawable/ic_notification_icon" />
<intent-filter>
<action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<activity android:name="com.google.androidbrowserhelper.trusted.NotificationPermissionRequestActivity" />
<activity android:name="com.google.androidbrowserhelper.locationdelegation.PermissionRequestActivity" />
<activity
android:name="com.google.android.gms.common.api.GoogleApiActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="io.truckwash.twa.androidx-startup"
android:exported="false" >
<meta-data
android:name="androidx.emoji2.text.EmojiCompatInitializer"
android:value="androidx.startup" />
<meta-data
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
android:value="androidx.startup" />
<meta-data
android:name="androidx.profileinstaller.ProfileInstallerInitializer"
android:value="androidx.startup" />
</provider>
<receiver
android:name="androidx.profileinstaller.ProfileInstallReceiver"
android:directBootAware="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.DUMP" >
<intent-filter>
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE" />
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SKIP_FILE" />
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.SAVE_PROFILE" />
</intent-filter>
<intent-filter>
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
</intent-filter>
</receiver>
</application>
</manifest>
@@ -1,873 +0,0 @@
Landroidx/activity/a;
Landroidx/activity/ComponentActivity$4;
Landroidx/lifecycle/j;
Landroidx/lifecycle/k;
HSPLandroidx/activity/ComponentActivity$4;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
Landroidx/activity/b;
Landroidx/core/app/b;
Landroidx/lifecycle/l;
Landroidx/lifecycle/E;
Landroidx/lifecycle/f;
LI/d;
Landroidx/activity/c;
HSPLandroidx/activity/c;-><init>(Z)V
HSPLandroidx/activity/c;->a()V
PLandroidx/activity/c;->b(Landroidx/activity/a;)V
HSPLandroidx/activity/c;->c(Z)V
Landroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;
PLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->d()V
HSPLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
Landroidx/activity/OnBackPressedDispatcher;
HSPLandroidx/activity/OnBackPressedDispatcher;->a(Landroidx/activity/c;)Landroidx/activity/a;
Lc/a;
Lc/d;
Lc/e;
Lc/f;
Lc/g;
Lc/h;
Lc/i;
HSPLc/i;-><clinit>()V
Ld/a;
HSPLd/a;-><init>(II)V
Le/a;
Lg/b;
Lh/a;
HSPLh/a;-><init>(Landroid/content/Context;)V
HSPLh/a;->a(Landroid/content/Context;)Lh/a;
HSPLh/a;->b()I
HSPLh/a;->c()I
HSPLh/a;->d()Z
Lh/b;
HSPLh/b;-><clinit>()V
HSPLh/b;-><init>(Landroid/content/Context;)V
Li/a;
Ls/b;
HSPLi/a;-><init>(Landroid/content/Context;IIIILjava/lang/CharSequence;)V
Landroidx/appcompat/view/menu/a;
Landroidx/appcompat/view/menu/h;
HSPLandroidx/appcompat/view/menu/a;-><init>(Landroid/content/Context;II)V
HSPLandroidx/appcompat/view/menu/a;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
PLandroidx/appcompat/view/menu/a;->a(Landroidx/appcompat/view/menu/d;Z)V
HSPLandroidx/appcompat/view/menu/a;->f(Landroidx/appcompat/view/menu/h$a;)V
HSPLandroidx/appcompat/view/menu/a;->j(Z)V
Landroidx/appcompat/view/menu/d$a;
Landroidx/appcompat/view/menu/d$b;
Landroidx/appcompat/view/menu/d;
Ls/a;
HSPLandroidx/appcompat/view/menu/d;-><clinit>()V
HSPLandroidx/appcompat/view/menu/d;-><init>(Landroid/content/Context;)V
HSPLandroidx/appcompat/view/menu/d;->b(Landroidx/appcompat/view/menu/h;Landroid/content/Context;)V
PLandroidx/appcompat/view/menu/d;->close()V
PLandroidx/appcompat/view/menu/d;->d(Z)V
HSPLandroidx/appcompat/view/menu/d;->h(Z)V
HSPLandroidx/appcompat/view/menu/d;->q()V
HSPLandroidx/appcompat/view/menu/d;->r()Ljava/util/ArrayList;
HSPLandroidx/appcompat/view/menu/d;->v()Ljava/util/ArrayList;
HSPLandroidx/appcompat/view/menu/d;->A()Ljava/util/ArrayList;
HSPLandroidx/appcompat/view/menu/d;->hasVisibleItems()Z
HSPLandroidx/appcompat/view/menu/d;->H(Z)V
HSPLandroidx/appcompat/view/menu/d;->M(Landroidx/appcompat/view/menu/d$a;)V
HSPLandroidx/appcompat/view/menu/d;->setQwertyMode(Z)V
HSPLandroidx/appcompat/view/menu/d;->U(Z)V
HSPLandroidx/appcompat/view/menu/d;->size()I
HSPLandroidx/appcompat/view/menu/d;->V()V
HSPLandroidx/appcompat/view/menu/d;->W()V
Landroidx/appcompat/view/menu/h$a;
Landroidx/appcompat/view/menu/i;
Landroidx/appcompat/widget/a$a;
HSPLandroidx/appcompat/widget/a$a;-><init>(Landroidx/appcompat/widget/a;)V
Landroidx/appcompat/widget/a;
HSPLandroidx/appcompat/widget/a;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
Landroidx/appcompat/widget/b;
HSPLandroidx/appcompat/widget/b;-><init>(Landroidx/appcompat/widget/ActionBarContainer;)V
HSPLandroidx/appcompat/widget/b;->draw(Landroid/graphics/Canvas;)V
HSPLandroidx/appcompat/widget/b;->getOpacity()I
HSPLandroidx/appcompat/widget/b;->getOutline(Landroid/graphics/Outline;)V
Landroidx/appcompat/widget/ActionBarContainer;
HSPLandroidx/appcompat/widget/ActionBarContainer;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/ActionBarContainer;->drawableStateChanged()V
HSPLandroidx/appcompat/widget/ActionBarContainer;->jumpDrawablesToCurrentState()V
HSPLandroidx/appcompat/widget/ActionBarContainer;->onFinishInflate()V
HSPLandroidx/appcompat/widget/ActionBarContainer;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/ActionBarContainer;->onMeasure(II)V
HSPLandroidx/appcompat/widget/ActionBarContainer;->setTabContainer(Landroidx/appcompat/widget/G;)V
PLandroidx/appcompat/widget/ActionBarContainer;->verifyDrawable(Landroid/graphics/drawable/Drawable;)Z
Landroidx/appcompat/widget/ActionBarContextView;
HSPLandroidx/appcompat/widget/ActionBarContextView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/ActionBarContextView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
PLandroidx/appcompat/widget/ActionBarContextView;->onDetachedFromWindow()V
Landroidx/appcompat/widget/ActionBarOverlayLayout$a;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$a;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
Landroidx/appcompat/widget/ActionBarOverlayLayout$b;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$b;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
Landroidx/appcompat/widget/ActionBarOverlayLayout$c;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$c;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
Landroidx/appcompat/widget/ActionBarOverlayLayout$d;
Landroidx/appcompat/widget/ActionBarOverlayLayout$e;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$e;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
Landroidx/appcompat/widget/ActionBarOverlayLayout;
Landroidx/core/view/o;
Landroidx/core/view/p;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;-><clinit>()V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->h(Landroid/view/View;Landroid/graphics/Rect;ZZZZ)Z
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->k(Landroid/util/AttributeSet;)Landroidx/appcompat/widget/ActionBarOverlayLayout$e;
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->l(Landroid/view/View;)Landroidx/appcompat/widget/u;
PLandroidx/appcompat/widget/ActionBarOverlayLayout;->m()V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->n(Landroid/content/Context;)V
PLandroidx/appcompat/widget/ActionBarOverlayLayout;->onDetachedFromWindow()V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onMeasure(II)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onStartNestedScroll(Landroid/view/View;Landroid/view/View;I)Z
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->f(Landroid/view/View;Landroid/view/View;II)Z
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onWindowVisibilityChanged(I)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->q()V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setActionBarVisibilityCallback(Landroidx/appcompat/widget/ActionBarOverlayLayout$d;)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setHasNonEmbeddedTabs(Z)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setWindowCallback(Landroid/view/Window$Callback;)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setWindowTitle(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->shouldDelayChildPressedState()Z
Landroidx/appcompat/widget/c$d$a;
Landroidx/appcompat/widget/y;
HSPLandroidx/appcompat/widget/c$d$a;-><init>(Landroidx/appcompat/widget/c$d;Landroid/view/View;Landroidx/appcompat/widget/c;)V
Landroidx/appcompat/widget/c$d;
Landroidx/appcompat/widget/l;
Landroidx/appcompat/widget/ActionMenuView$a;
HSPLandroidx/appcompat/widget/c$d;-><init>(Landroidx/appcompat/widget/c;Landroid/content/Context;)V
Landroidx/appcompat/widget/c$f;
HSPLandroidx/appcompat/widget/c$f;-><init>(Landroidx/appcompat/widget/c;)V
Landroidx/appcompat/widget/c;
HSPLandroidx/appcompat/widget/c;-><init>(Landroid/content/Context;)V
PLandroidx/appcompat/widget/c;->w()Z
HSPLandroidx/appcompat/widget/c;->h()Z
PLandroidx/appcompat/widget/c;->z()Z
PLandroidx/appcompat/widget/c;->A()Z
HSPLandroidx/appcompat/widget/c;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
PLandroidx/appcompat/widget/c;->a(Landroidx/appcompat/view/menu/d;Z)V
HSPLandroidx/appcompat/widget/c;->D(Z)V
HSPLandroidx/appcompat/widget/c;->E(Landroidx/appcompat/widget/ActionMenuView;)V
HSPLandroidx/appcompat/widget/c;->j(Z)V
Landroidx/appcompat/widget/ActionMenuView$e;
Landroidx/appcompat/widget/ActionMenuView;
Landroidx/appcompat/widget/z;
HSPLandroidx/appcompat/widget/ActionMenuView;-><init>(Landroid/content/Context;)V
HSPLandroidx/appcompat/widget/ActionMenuView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
PLandroidx/appcompat/widget/ActionMenuView;->w()V
HSPLandroidx/appcompat/widget/ActionMenuView;->C(Landroidx/appcompat/view/menu/d;)V
PLandroidx/appcompat/widget/ActionMenuView;->onDetachedFromWindow()V
HSPLandroidx/appcompat/widget/ActionMenuView;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/ActionMenuView;->onMeasure(II)V
HSPLandroidx/appcompat/widget/ActionMenuView;->G()Landroidx/appcompat/view/menu/d;
HSPLandroidx/appcompat/widget/ActionMenuView;->H(Landroidx/appcompat/view/menu/h$a;Landroidx/appcompat/view/menu/d$a;)V
HSPLandroidx/appcompat/widget/ActionMenuView;->setOnMenuItemClickListener(Landroidx/appcompat/widget/ActionMenuView$e;)V
HSPLandroidx/appcompat/widget/ActionMenuView;->setOverflowReserved(Z)V
HSPLandroidx/appcompat/widget/ActionMenuView;->setPopupTheme(I)V
HSPLandroidx/appcompat/widget/ActionMenuView;->setPresenter(Landroidx/appcompat/widget/c;)V
Landroidx/appcompat/widget/e;
HSPLandroidx/appcompat/widget/e;-><init>(Landroid/view/View;)V
HSPLandroidx/appcompat/widget/e;->b()V
HSPLandroidx/appcompat/widget/e;->e(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/e;->k()Z
Landroidx/appcompat/widget/f$a;
Landroidx/appcompat/widget/D$f;
HSPLandroidx/appcompat/widget/f$a;-><init>()V
HSPLandroidx/appcompat/widget/f$a;->f([II)Z
HSPLandroidx/appcompat/widget/f$a;->a(Landroidx/appcompat/widget/D;Landroid/content/Context;I)Landroid/graphics/drawable/Drawable;
HSPLandroidx/appcompat/widget/f$a;->b(Landroid/content/Context;I)Landroid/content/res/ColorStateList;
HSPLandroidx/appcompat/widget/f$a;->d(Landroid/content/Context;ILandroid/graphics/drawable/Drawable;)Z
HSPLandroidx/appcompat/widget/f$a;->c(Landroid/content/Context;ILandroid/graphics/drawable/Drawable;)Z
Landroidx/appcompat/widget/f;
HSPLandroidx/appcompat/widget/f;-><clinit>()V
HSPLandroidx/appcompat/widget/f;-><init>()V
HSPLandroidx/appcompat/widget/f;->a()Landroid/graphics/PorterDuff$Mode;
HSPLandroidx/appcompat/widget/f;->b()Landroidx/appcompat/widget/f;
HSPLandroidx/appcompat/widget/f;->e(Landroid/content/Context;I)Landroid/content/res/ColorStateList;
HSPLandroidx/appcompat/widget/f;->f()V
Landroidx/appcompat/widget/g;
HSPLandroidx/appcompat/widget/g;-><init>(Landroid/widget/EditText;)V
HSPLandroidx/appcompat/widget/g;->a(Landroid/text/method/KeyListener;)Landroid/text/method/KeyListener;
HSPLandroidx/appcompat/widget/g;->c(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/g;->e(Z)V
Landroidx/appcompat/widget/h;
HSPLandroidx/appcompat/widget/h;-><init>(Landroid/widget/TextView;)V
HSPLandroidx/appcompat/widget/h;->a([Landroid/text/InputFilter;)[Landroid/text/InputFilter;
HSPLandroidx/appcompat/widget/h;->b(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/h;->d(Z)V
Landroidx/appcompat/widget/j;
HSPLandroidx/appcompat/widget/j;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/j;->setBackgroundDrawable(Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/j;->setImageDrawable(Landroid/graphics/drawable/Drawable;)V
Landroidx/appcompat/widget/k;
HSPLandroidx/appcompat/widget/k;-><init>(Landroid/widget/ImageView;)V
HSPLandroidx/appcompat/widget/k;->b()V
HSPLandroidx/appcompat/widget/k;->c()V
HSPLandroidx/appcompat/widget/k;->g(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/l;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/l;->setBackgroundDrawable(Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/l;->setImageDrawable(Landroid/graphics/drawable/Drawable;)V
Landroidx/appcompat/widget/n;
HSPLandroidx/appcompat/widget/n;-><init>(Landroid/widget/TextView;)V
Landroidx/appcompat/widget/o$a;
Landroidx/core/content/res/h$e;
HSPLandroidx/appcompat/widget/o$a;-><init>(Landroidx/appcompat/widget/o;IILjava/lang/ref/WeakReference;)V
HSPLandroidx/appcompat/widget/o$a;->f(I)V
Landroidx/appcompat/widget/o;
HSPLandroidx/appcompat/widget/o;-><init>(Landroid/widget/TextView;)V
HSPLandroidx/appcompat/widget/o;->b()V
HSPLandroidx/appcompat/widget/o;->d(Landroid/content/Context;Landroidx/appcompat/widget/f;I)Landroidx/appcompat/widget/J;
HSPLandroidx/appcompat/widget/o;->m(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/o;->o(ZIIII)V
HSPLandroidx/appcompat/widget/o;->q(Landroid/content/Context;I)V
HSPLandroidx/appcompat/widget/o;->y(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/o;->C(Landroid/content/Context;Landroidx/appcompat/widget/L;)V
Landroidx/appcompat/widget/p;
Landroidx/core/widget/h;
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;)V
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/p;->r()V
HSPLandroidx/appcompat/widget/p;->drawableStateChanged()V
HSPLandroidx/appcompat/widget/p;->getEmojiTextViewHelper()Landroidx/appcompat/widget/h;
HSPLandroidx/appcompat/widget/p;->getText()Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/p;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/p;->onMeasure(II)V
HSPLandroidx/appcompat/widget/p;->onTextChanged(Ljava/lang/CharSequence;III)V
HSPLandroidx/appcompat/widget/p;->setCompoundDrawables(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/p;->setCompoundDrawablesWithIntrinsicBounds(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/p;->setFilters([Landroid/text/InputFilter;)V
HSPLandroidx/appcompat/widget/p;->setTextAppearance(Landroid/content/Context;I)V
HSPLandroidx/appcompat/widget/p;->setTypeface(Landroid/graphics/Typeface;I)V
Landroidx/appcompat/widget/q$b;
Landroidx/appcompat/widget/q$d;
HSPLandroidx/appcompat/widget/q$b;-><init>()V
Landroidx/appcompat/widget/q$c;
HSPLandroidx/appcompat/widget/q$c;-><init>()V
HSPLandroidx/appcompat/widget/q$d;-><init>()V
Landroidx/appcompat/widget/q;
HSPLandroidx/appcompat/widget/q;-><clinit>()V
HSPLandroidx/appcompat/widget/q;-><init>(Landroid/widget/TextView;)V
HSPLandroidx/appcompat/widget/q;->j()I
HSPLandroidx/appcompat/widget/q;->o(Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/q;->y()Z
Landroidx/appcompat/widget/ContentFrameLayout$a;
Landroidx/appcompat/widget/ContentFrameLayout;
HSPLandroidx/appcompat/widget/ContentFrameLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/ContentFrameLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/ContentFrameLayout;->getMinWidthMajor()Landroid/util/TypedValue;
HSPLandroidx/appcompat/widget/ContentFrameLayout;->getMinWidthMinor()Landroid/util/TypedValue;
HSPLandroidx/appcompat/widget/ContentFrameLayout;->onAttachedToWindow()V
PLandroidx/appcompat/widget/ContentFrameLayout;->onDetachedFromWindow()V
HSPLandroidx/appcompat/widget/ContentFrameLayout;->onMeasure(II)V
HSPLandroidx/appcompat/widget/ContentFrameLayout;->setAttachListener(Landroidx/appcompat/widget/ContentFrameLayout$a;)V
Landroidx/appcompat/widget/u;
Landroidx/appcompat/widget/v;
HSPLandroidx/appcompat/widget/y;-><init>(Landroid/view/View;)V
HSPLandroidx/appcompat/widget/z;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/z;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/z;->getVirtualChildCount()I
HSPLandroidx/appcompat/widget/z;->p(IIII)V
HSPLandroidx/appcompat/widget/z;->s(II)V
HSPLandroidx/appcompat/widget/z;->onInitializeAccessibilityNodeInfo(Landroid/view/accessibility/AccessibilityNodeInfo;)V
HSPLandroidx/appcompat/widget/z;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/z;->onMeasure(II)V
HSPLandroidx/appcompat/widget/z;->setBaselineAligned(Z)V
HSPLandroidx/appcompat/widget/z;->setDividerDrawable(Landroid/graphics/drawable/Drawable;)V
Landroidx/appcompat/widget/D$c;
Lp/h;
Landroidx/appcompat/widget/D;
Landroidx/appcompat/widget/E;
Landroidx/appcompat/widget/F;
HSPLandroidx/appcompat/widget/F;-><init>()V
HSPLandroidx/appcompat/widget/F;->a()I
HSPLandroidx/appcompat/widget/F;->d()I
HSPLandroidx/appcompat/widget/F;->e(II)V
HSPLandroidx/appcompat/widget/F;->f(Z)V
HSPLandroidx/appcompat/widget/F;->g(II)V
Landroidx/appcompat/widget/H;
HSPLandroidx/appcompat/widget/H;-><clinit>()V
HSPLandroidx/appcompat/widget/H;->a(Landroid/view/View;Landroid/content/Context;)V
Landroidx/appcompat/widget/I;
HSPLandroidx/appcompat/widget/I;-><clinit>()V
HSPLandroidx/appcompat/widget/I;->a(Landroid/content/Context;)Z
HSPLandroidx/appcompat/widget/I;->b(Landroid/content/Context;)Landroid/content/Context;
Landroidx/appcompat/widget/K;
Landroidx/appcompat/widget/L;
HSPLandroidx/appcompat/widget/L;-><init>(Landroid/content/Context;Landroid/content/res/TypedArray;)V
HSPLandroidx/appcompat/widget/L;->a(IZ)Z
HSPLandroidx/appcompat/widget/L;->b(II)I
HSPLandroidx/appcompat/widget/L;->c(I)Landroid/content/res/ColorStateList;
HSPLandroidx/appcompat/widget/L;->d(II)I
HSPLandroidx/appcompat/widget/L;->e(II)I
HSPLandroidx/appcompat/widget/L;->f(I)Landroid/graphics/drawable/Drawable;
HSPLandroidx/appcompat/widget/L;->g(IF)F
HSPLandroidx/appcompat/widget/L;->h(IILandroidx/core/content/res/h$e;)Landroid/graphics/Typeface;
HSPLandroidx/appcompat/widget/L;->i(II)I
HSPLandroidx/appcompat/widget/L;->j(II)I
HSPLandroidx/appcompat/widget/L;->k(II)I
HSPLandroidx/appcompat/widget/L;->l(II)I
HSPLandroidx/appcompat/widget/L;->m(I)Ljava/lang/String;
HSPLandroidx/appcompat/widget/L;->n(I)Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/L;->o()Landroid/content/res/TypedArray;
HSPLandroidx/appcompat/widget/L;->p(I)Z
HSPLandroidx/appcompat/widget/L;->q(Landroid/content/Context;I[I)Landroidx/appcompat/widget/L;
HSPLandroidx/appcompat/widget/L;->r(Landroid/content/Context;Landroid/util/AttributeSet;[I)Landroidx/appcompat/widget/L;
HSPLandroidx/appcompat/widget/L;->s(Landroid/content/Context;Landroid/util/AttributeSet;[III)Landroidx/appcompat/widget/L;
HSPLandroidx/appcompat/widget/L;->u()V
Landroidx/appcompat/widget/Toolbar$a;
HSPLandroidx/appcompat/widget/Toolbar$a;-><init>(Landroidx/appcompat/widget/Toolbar;)V
Landroidx/appcompat/widget/Toolbar$b;
HSPLandroidx/appcompat/widget/Toolbar$b;-><init>(Landroidx/appcompat/widget/Toolbar;)V
Landroidx/appcompat/widget/Toolbar$f;
HSPLandroidx/appcompat/widget/Toolbar$f;-><init>(Landroidx/appcompat/widget/Toolbar;)V
HSPLandroidx/appcompat/widget/Toolbar$f;->h()Z
HSPLandroidx/appcompat/widget/Toolbar$f;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
PLandroidx/appcompat/widget/Toolbar$f;->a(Landroidx/appcompat/view/menu/d;Z)V
HSPLandroidx/appcompat/widget/Toolbar$f;->j(Z)V
Landroidx/appcompat/widget/Toolbar$g;
HSPLandroidx/appcompat/widget/Toolbar$g;-><init>(II)V
Landroidx/appcompat/widget/Toolbar;
HSPLandroidx/appcompat/widget/Toolbar;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
HSPLandroidx/appcompat/widget/Toolbar;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
HSPLandroidx/appcompat/widget/Toolbar;->b(Ljava/util/List;I)V
HSPLandroidx/appcompat/widget/Toolbar;->c(Landroid/view/View;Z)V
HSPLandroidx/appcompat/widget/Toolbar;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
HSPLandroidx/appcompat/widget/Toolbar;->f()V
HSPLandroidx/appcompat/widget/Toolbar;->i()V
HSPLandroidx/appcompat/widget/Toolbar;->j()V
HSPLandroidx/appcompat/widget/Toolbar;->k()Landroidx/appcompat/widget/Toolbar$g;
HSPLandroidx/appcompat/widget/Toolbar;->o(Landroid/view/View;I)I
HSPLandroidx/appcompat/widget/Toolbar;->p(I)I
HSPLandroidx/appcompat/widget/Toolbar;->getContentInsetEnd()I
HSPLandroidx/appcompat/widget/Toolbar;->getContentInsetStart()I
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetEnd()I
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetLeft()I
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetRight()I
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetStart()I
HSPLandroidx/appcompat/widget/Toolbar;->q(Landroid/view/View;)I
HSPLandroidx/appcompat/widget/Toolbar;->getNavigationContentDescription()Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/Toolbar;->getNavigationIcon()Landroid/graphics/drawable/Drawable;
HSPLandroidx/appcompat/widget/Toolbar;->getSubtitle()Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/Toolbar;->getTitle()Ljava/lang/CharSequence;
HSPLandroidx/appcompat/widget/Toolbar;->r(Landroid/view/View;)I
HSPLandroidx/appcompat/widget/Toolbar;->s(Ljava/util/List;[I)I
HSPLandroidx/appcompat/widget/Toolbar;->getWrapper()Landroidx/appcompat/widget/u;
PLandroidx/appcompat/widget/Toolbar;->t()Z
HSPLandroidx/appcompat/widget/Toolbar;->w(Landroid/view/View;)Z
HSPLandroidx/appcompat/widget/Toolbar;->z(Landroid/view/View;I[II)I
HSPLandroidx/appcompat/widget/Toolbar;->A(Landroid/view/View;IIII[I)I
HSPLandroidx/appcompat/widget/Toolbar;->B(Landroid/view/View;IIIII)V
PLandroidx/appcompat/widget/Toolbar;->onDetachedFromWindow()V
HSPLandroidx/appcompat/widget/Toolbar;->onLayout(ZIIII)V
HSPLandroidx/appcompat/widget/Toolbar;->onMeasure(II)V
HSPLandroidx/appcompat/widget/Toolbar;->onRtlPropertiesChanged(I)V
HSPLandroidx/appcompat/widget/Toolbar;->setCollapsible(Z)V
HSPLandroidx/appcompat/widget/Toolbar;->F(II)V
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationContentDescription(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationIcon(Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationOnClickListener(Landroid/view/View$OnClickListener;)V
HSPLandroidx/appcompat/widget/Toolbar;->setPopupTheme(I)V
HSPLandroidx/appcompat/widget/Toolbar;->setSubtitle(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/Toolbar;->G(Landroid/content/Context;I)V
HSPLandroidx/appcompat/widget/Toolbar;->setTitle(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/Toolbar;->H(Landroid/content/Context;I)V
HSPLandroidx/appcompat/widget/Toolbar;->I()Z
HSPLandroidx/appcompat/widget/Toolbar;->J(Landroid/view/View;)Z
Landroidx/appcompat/widget/P$a;
HSPLandroidx/appcompat/widget/P$a;-><init>(Landroidx/appcompat/widget/P;)V
Landroidx/appcompat/widget/P;
HSPLandroidx/appcompat/widget/P;-><init>(Landroidx/appcompat/widget/Toolbar;Z)V
HSPLandroidx/appcompat/widget/P;-><init>(Landroidx/appcompat/widget/Toolbar;ZII)V
HSPLandroidx/appcompat/widget/P;->e()Landroid/content/Context;
HSPLandroidx/appcompat/widget/P;->g(I)V
HSPLandroidx/appcompat/widget/P;->h(I)V
HSPLandroidx/appcompat/widget/P;->l(Landroid/graphics/drawable/Drawable;)V
HSPLandroidx/appcompat/widget/P;->o(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/P;->c(Landroid/view/Window$Callback;)V
HSPLandroidx/appcompat/widget/P;->b(Ljava/lang/CharSequence;)V
HSPLandroidx/appcompat/widget/P;->q()V
Landroidx/appcompat/widget/Q;
HSPLandroidx/appcompat/widget/Q;->a(Landroid/view/View;Ljava/lang/CharSequence;)V
Landroidx/appcompat/widget/W;
HSPLandroidx/appcompat/widget/W;-><clinit>()V
HSPLandroidx/appcompat/widget/W;->b()Z
HSPLandroidx/appcompat/widget/W;->c()Z
Landroidx/appcompat/widget/X;
HSPLandroidx/appcompat/widget/X;-><clinit>()V
HSPLandroidx/appcompat/widget/X;->a(Landroid/view/View;)Z
LE/b;
LE/c;
HSPLE/c;-><clinit>()V
Landroidx/fragment/app/a;
Landroidx/fragment/app/y;
Landroidx/fragment/app/q$h;
HSPLandroidx/fragment/app/a;-><init>(Landroidx/fragment/app/q;)V
HSPLandroidx/fragment/app/a;->i(I)V
HSPLandroidx/fragment/app/a;->g(ILandroidx/fragment/app/Fragment;Ljava/lang/String;I)V
HSPLandroidx/fragment/app/a;->k()V
HSPLandroidx/fragment/app/a;->m(Ljava/util/ArrayList;Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/a;->a(Ljava/util/ArrayList;Ljava/util/ArrayList;)Z
HSPLandroidx/fragment/app/a;->n()V
Landroidx/fragment/app/d;
Landroidx/fragment/app/E;
HSPLandroidx/fragment/app/d;-><init>(Landroid/view/ViewGroup;)V
Landroidx/fragment/app/Fragment$a;
HSPLandroidx/fragment/app/Fragment$a;-><init>(Landroidx/fragment/app/Fragment;)V
Landroidx/fragment/app/Fragment$c;
Landroidx/fragment/app/g;
HSPLandroidx/fragment/app/Fragment$c;-><init>(Landroidx/fragment/app/Fragment;)V
Landroidx/fragment/app/Fragment$d;
HSPLandroidx/fragment/app/Fragment$d;-><init>()V
Landroidx/fragment/app/Fragment$e;
PLandroidx/fragment/app/Fragment$e;->a(Landroid/view/View;)V
Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/Fragment;-><clinit>()V
HSPLandroidx/fragment/app/Fragment;-><init>()V
HSPLandroidx/fragment/app/Fragment;->h()Landroidx/fragment/app/g;
HSPLandroidx/fragment/app/Fragment;->i()Landroidx/fragment/app/Fragment$d;
HSPLandroidx/fragment/app/Fragment;->equals(Ljava/lang/Object;)Z
HSPLandroidx/fragment/app/Fragment;->j()Landroidx/fragment/app/e;
HSPLandroidx/fragment/app/Fragment;->n()Landroidx/fragment/app/q;
HSPLandroidx/fragment/app/Fragment;->o()Landroid/content/Context;
HSPLandroidx/fragment/app/Fragment;->v()Landroid/view/View;
HSPLandroidx/fragment/app/Fragment;->w(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
HSPLandroidx/fragment/app/Fragment;->g()Landroidx/lifecycle/g;
HSPLandroidx/fragment/app/Fragment;->x()I
HSPLandroidx/fragment/app/Fragment;->z()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/Fragment;->A()Landroidx/fragment/app/q;
HSPLandroidx/fragment/app/Fragment;->E()F
HSPLandroidx/fragment/app/Fragment;->b()Landroidx/savedstate/a;
HSPLandroidx/fragment/app/Fragment;->M()Landroid/view/View;
HSPLandroidx/fragment/app/Fragment;->c()Landroidx/lifecycle/D;
HSPLandroidx/fragment/app/Fragment;->N()V
PLandroidx/fragment/app/Fragment;->O()V
HSPLandroidx/fragment/app/Fragment;->P()Z
HSPLandroidx/fragment/app/Fragment;->S()Z
HSPLandroidx/fragment/app/Fragment;->U(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->V(Landroid/os/Bundle;)V
PLandroidx/fragment/app/Fragment;->b0()V
PLandroidx/fragment/app/Fragment;->c0()V
HSPLandroidx/fragment/app/Fragment;->d0(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
HSPLandroidx/fragment/app/Fragment;->f0(Landroid/content/Context;Landroid/util/AttributeSet;Landroid/os/Bundle;)V
PLandroidx/fragment/app/Fragment;->h0()V
HSPLandroidx/fragment/app/Fragment;->j0(Z)V
HSPLandroidx/fragment/app/Fragment;->k0()V
HSPLandroidx/fragment/app/Fragment;->m0()V
PLandroidx/fragment/app/Fragment;->n0()V
HSPLandroidx/fragment/app/Fragment;->o0(Landroid/view/View;Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->p0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->q0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->r0()V
HSPLandroidx/fragment/app/Fragment;->t0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->u0(Landroid/view/Menu;Landroid/view/MenuInflater;)Z
HSPLandroidx/fragment/app/Fragment;->v0(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)V
PLandroidx/fragment/app/Fragment;->w0()V
PLandroidx/fragment/app/Fragment;->x0()V
HSPLandroidx/fragment/app/Fragment;->y0(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
PLandroidx/fragment/app/Fragment;->B0()V
HSPLandroidx/fragment/app/Fragment;->C0(Landroid/view/Menu;)Z
HSPLandroidx/fragment/app/Fragment;->D0()V
HSPLandroidx/fragment/app/Fragment;->E0()V
HSPLandroidx/fragment/app/Fragment;->G0()V
PLandroidx/fragment/app/Fragment;->H0()V
HSPLandroidx/fragment/app/Fragment;->I0()V
HSPLandroidx/fragment/app/Fragment;->L0()Landroid/content/Context;
HSPLandroidx/fragment/app/Fragment;->M0()Landroid/view/View;
HSPLandroidx/fragment/app/Fragment;->N0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->O0()V
HSPLandroidx/fragment/app/Fragment;->P0(Landroid/os/Bundle;)V
HSPLandroidx/fragment/app/Fragment;->Q0(IIII)V
HSPLandroidx/fragment/app/Fragment;->R0(Landroid/view/View;)V
HSPLandroidx/fragment/app/Fragment;->S0(I)V
HSPLandroidx/fragment/app/Fragment;->T0(Z)V
HSPLandroidx/fragment/app/Fragment;->U0(F)V
HSPLandroidx/fragment/app/Fragment;->V0(Ljava/util/ArrayList;Ljava/util/ArrayList;)V
HSPLandroidx/fragment/app/Fragment;->toString()Ljava/lang/String;
Landroidx/fragment/app/e;
HSPLandroidx/fragment/app/g;-><init>()V
Landroidx/fragment/app/h;
PLandroidx/fragment/app/h;->a(Landroid/view/View;)V
HSPLandroidx/fragment/app/h;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
HSPLandroidx/fragment/app/h;->dispatchDraw(Landroid/graphics/Canvas;)V
HSPLandroidx/fragment/app/h;->drawChild(Landroid/graphics/Canvas;Landroid/view/View;J)Z
PLandroidx/fragment/app/h;->removeView(Landroid/view/View;)V
Landroidx/fragment/app/i;
HSPLandroidx/fragment/app/i;-><clinit>()V
HSPLandroidx/fragment/app/i;-><init>()V
HSPLandroidx/fragment/app/i;->b(Ljava/lang/ClassLoader;Ljava/lang/String;)Z
HSPLandroidx/fragment/app/i;->c(Ljava/lang/ClassLoader;Ljava/lang/String;)Ljava/lang/Class;
Landroidx/fragment/app/j;
Landroidx/fragment/app/k;
HSPLandroidx/fragment/app/k;-><init>(Landroidx/fragment/app/q;)V
HSPLandroidx/fragment/app/k;->onCreateView(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;
Landroidx/fragment/app/l;
HSPLandroidx/fragment/app/l;-><init>(Landroidx/fragment/app/q;)V
HSPLandroidx/fragment/app/l;->a(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
HSPLandroidx/fragment/app/l;->b(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->c(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
PLandroidx/fragment/app/l;->d(Landroidx/fragment/app/Fragment;Z)V
PLandroidx/fragment/app/l;->e(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->f(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->g(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
HSPLandroidx/fragment/app/l;->h(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->j(Landroidx/fragment/app/Fragment;Z)V
PLandroidx/fragment/app/l;->k(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/l;->l(Landroidx/fragment/app/Fragment;Landroid/view/View;Landroid/os/Bundle;Z)V
PLandroidx/fragment/app/l;->m(Landroidx/fragment/app/Fragment;Z)V
Landroidx/fragment/app/q$a;
HSPLandroidx/fragment/app/q$a;-><init>(Landroidx/fragment/app/q;Z)V
Landroidx/fragment/app/q$b;
Landroidx/core/view/l;
HSPLandroidx/fragment/app/q$b;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/q$c;
HSPLandroidx/fragment/app/q$c;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/q$d;
Landroidx/fragment/app/F;
HSPLandroidx/fragment/app/q$d;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/FragmentManager$6;
Landroidx/fragment/app/q$f;
Landroidx/fragment/app/u;
Landroidx/fragment/app/q;
HSPLandroidx/fragment/app/q;-><clinit>()V
HSPLandroidx/fragment/app/q;-><init>()V
HSPLandroidx/fragment/app/q;->h(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/w;
HSPLandroidx/fragment/app/q;->i(Landroidx/fragment/app/u;)V
HSPLandroidx/fragment/app/q;->j(Landroidx/fragment/app/j;Landroidx/fragment/app/g;Landroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/q;->l()Landroidx/fragment/app/y;
HSPLandroidx/fragment/app/q;->m()Z
HSPLandroidx/fragment/app/q;->n()V
PLandroidx/fragment/app/q;->o()V
HSPLandroidx/fragment/app/q;->p()Ljava/util/Set;
HSPLandroidx/fragment/app/q;->q(Ljava/util/ArrayList;II)Ljava/util/Set;
HSPLandroidx/fragment/app/q;->r(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/w;
HSPLandroidx/fragment/app/q;->t()V
HSPLandroidx/fragment/app/q;->v()V
HSPLandroidx/fragment/app/q;->w(Landroid/view/Menu;Landroid/view/MenuInflater;)Z
PLandroidx/fragment/app/q;->x()V
PLandroidx/fragment/app/q;->y()V
HSPLandroidx/fragment/app/q;->C(Landroidx/fragment/app/Fragment;)V
PLandroidx/fragment/app/q;->D()V
HSPLandroidx/fragment/app/q;->E(Landroid/view/Menu;)Z
HSPLandroidx/fragment/app/q;->F()V
HSPLandroidx/fragment/app/q;->G()V
HSPLandroidx/fragment/app/q;->H()V
HSPLandroidx/fragment/app/q;->I(I)V
PLandroidx/fragment/app/q;->J()V
HSPLandroidx/fragment/app/q;->K()V
HSPLandroidx/fragment/app/q;->L()V
PLandroidx/fragment/app/q;->M()V
HSPLandroidx/fragment/app/q;->N(Z)V
HSPLandroidx/fragment/app/q;->O(Z)Z
HSPLandroidx/fragment/app/q;->Q(Ljava/util/ArrayList;Ljava/util/ArrayList;II)V
HSPLandroidx/fragment/app/q;->R(Ljava/util/ArrayList;Ljava/util/ArrayList;II)V
HSPLandroidx/fragment/app/q;->S(Ljava/lang/String;)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->T(I)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->Y(Ljava/util/ArrayList;Ljava/util/ArrayList;)Z
HSPLandroidx/fragment/app/q;->Z()I
HSPLandroidx/fragment/app/q;->a0(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/t;
HSPLandroidx/fragment/app/q;->b0()Landroidx/fragment/app/g;
HSPLandroidx/fragment/app/q;->c0(Landroidx/fragment/app/Fragment;)Landroid/view/ViewGroup;
HSPLandroidx/fragment/app/q;->d0()Landroidx/fragment/app/i;
HSPLandroidx/fragment/app/q;->e0()Landroidx/fragment/app/j;
HSPLandroidx/fragment/app/q;->f0()Landroidx/fragment/app/l;
HSPLandroidx/fragment/app/q;->g0()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->h0()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->i0()Landroidx/fragment/app/F;
HSPLandroidx/fragment/app/q;->j0()LF/c$c;
HSPLandroidx/fragment/app/q;->k0(Landroid/view/View;)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/q;->l0(Landroidx/fragment/app/Fragment;)Landroidx/lifecycle/D;
PLandroidx/fragment/app/q;->o0()Z
HSPLandroidx/fragment/app/q;->p0(I)Z
HSPLandroidx/fragment/app/q;->q0(Landroidx/fragment/app/Fragment;)Z
HSPLandroidx/fragment/app/q;->t0(Landroidx/fragment/app/Fragment;)Z
HSPLandroidx/fragment/app/q;->u0(Landroidx/fragment/app/Fragment;)Z
HSPLandroidx/fragment/app/q;->v0(I)Z
HSPLandroidx/fragment/app/q;->w0()Z
HSPLandroidx/fragment/app/q;->x0(IZ)V
HSPLandroidx/fragment/app/q;->y0()V
HSPLandroidx/fragment/app/q;->A0(Landroidx/fragment/app/w;)V
HSPLandroidx/fragment/app/q;->C0(Ljava/util/ArrayList;Ljava/util/ArrayList;)V
HSPLandroidx/fragment/app/q;->H0(Landroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/q;->J0(Landroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/q;->M0()V
HSPLandroidx/fragment/app/q;->N0()V
Landroidx/fragment/app/r;
HSPLandroidx/fragment/app/r;-><init>()V
Landroidx/fragment/app/t$a;
Landroidx/lifecycle/A$b;
HSPLandroidx/fragment/app/t$a;-><init>()V
HSPLandroidx/fragment/app/t$a;->b(Ljava/lang/Class;)Landroidx/lifecycle/z;
Landroidx/fragment/app/t;
Landroidx/lifecycle/z;
HSPLandroidx/fragment/app/t;-><clinit>()V
HSPLandroidx/fragment/app/t;-><init>(Z)V
PLandroidx/fragment/app/t;->f(Landroidx/fragment/app/Fragment;)V
PLandroidx/fragment/app/t;->g(Ljava/lang/String;)V
HSPLandroidx/fragment/app/t;->i(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/t;
HSPLandroidx/fragment/app/t;->k(Landroidx/fragment/app/Fragment;)Landroidx/lifecycle/D;
PLandroidx/fragment/app/t;->d()V
HSPLandroidx/fragment/app/t;->m(Z)V
PLandroidx/fragment/app/t;->n(Landroidx/fragment/app/Fragment;)Z
Landroidx/fragment/app/w$a;
HSPLandroidx/fragment/app/w$a;-><init>(Landroidx/fragment/app/w;Landroid/view/View;)V
HSPLandroidx/fragment/app/w$a;->onViewAttachedToWindow(Landroid/view/View;)V
Landroidx/fragment/app/w$b;
HSPLandroidx/fragment/app/w$b;-><clinit>()V
Landroidx/fragment/app/w;
HSPLandroidx/fragment/app/w;-><init>(Landroidx/fragment/app/l;Landroidx/fragment/app/x;Landroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/w;->a()V
HSPLandroidx/fragment/app/w;->b()V
HSPLandroidx/fragment/app/w;->c()V
HSPLandroidx/fragment/app/w;->d()I
HSPLandroidx/fragment/app/w;->e()V
HSPLandroidx/fragment/app/w;->f()V
PLandroidx/fragment/app/w;->g()V
PLandroidx/fragment/app/w;->h()V
PLandroidx/fragment/app/w;->i()V
HSPLandroidx/fragment/app/w;->j()V
HSPLandroidx/fragment/app/w;->k()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/w;->m()V
PLandroidx/fragment/app/w;->n()V
HSPLandroidx/fragment/app/w;->o()V
PLandroidx/fragment/app/w;->r()V
HSPLandroidx/fragment/app/w;->s(I)V
HSPLandroidx/fragment/app/w;->t()V
PLandroidx/fragment/app/w;->u()V
Landroidx/fragment/app/x;
HSPLandroidx/fragment/app/x;-><init>()V
HSPLandroidx/fragment/app/x;->a(Landroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/x;->b()V
HSPLandroidx/fragment/app/x;->c(Ljava/lang/String;)Z
HSPLandroidx/fragment/app/x;->d(I)V
HSPLandroidx/fragment/app/x;->e(Ljava/lang/String;)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/x;->f(I)Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/x;->h(Landroidx/fragment/app/Fragment;)I
HSPLandroidx/fragment/app/x;->i()Ljava/util/List;
HSPLandroidx/fragment/app/x;->j()Ljava/util/List;
HSPLandroidx/fragment/app/x;->l(Ljava/lang/String;)Landroidx/fragment/app/w;
HSPLandroidx/fragment/app/x;->m()Ljava/util/List;
PLandroidx/fragment/app/x;->n()Landroidx/fragment/app/t;
HSPLandroidx/fragment/app/x;->p(Landroidx/fragment/app/w;)V
PLandroidx/fragment/app/x;->q(Landroidx/fragment/app/w;)V
HSPLandroidx/fragment/app/x;->r()V
HSPLandroidx/fragment/app/x;->y(Landroidx/fragment/app/t;)V
Landroidx/fragment/app/y$a;
HSPLandroidx/fragment/app/y$a;-><init>(ILandroidx/fragment/app/Fragment;)V
HSPLandroidx/fragment/app/y$a;-><init>(ILandroidx/fragment/app/Fragment;Z)V
HSPLandroidx/fragment/app/y;-><init>(Landroidx/fragment/app/i;Ljava/lang/ClassLoader;)V
HSPLandroidx/fragment/app/y;->d(Landroidx/fragment/app/y$a;)V
HSPLandroidx/fragment/app/y;->g(ILandroidx/fragment/app/Fragment;Ljava/lang/String;I)V
HSPLandroidx/fragment/app/y;->h(Z)Landroidx/fragment/app/y;
Landroidx/fragment/app/C;
HSPLandroidx/fragment/app/C;-><init>(Landroidx/fragment/app/Fragment;Landroidx/lifecycle/D;)V
HSPLandroidx/fragment/app/C;->g()Landroidx/lifecycle/g;
HSPLandroidx/fragment/app/C;->b()Landroidx/savedstate/a;
HSPLandroidx/fragment/app/C;->d(Landroidx/lifecycle/g$a;)V
HSPLandroidx/fragment/app/C;->e()V
HSPLandroidx/fragment/app/C;->h(Landroid/os/Bundle;)V
PLandroidx/fragment/app/C;->i(Landroid/os/Bundle;)V
Landroidx/fragment/app/E$a;
HSPLandroidx/fragment/app/E$a;-><init>(Landroidx/fragment/app/E;Landroidx/fragment/app/E$d;)V
HSPLandroidx/fragment/app/E$a;->run()V
Landroidx/fragment/app/E$b;
HSPLandroidx/fragment/app/E$b;-><init>(Landroidx/fragment/app/E;Landroidx/fragment/app/E$d;)V
HSPLandroidx/fragment/app/E$b;->run()V
Landroidx/fragment/app/E$c;
HSPLandroidx/fragment/app/E$c;-><clinit>()V
Landroidx/fragment/app/E$d;
Landroidx/fragment/app/E$e;
HSPLandroidx/fragment/app/E$d;-><init>(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/w;Landroidx/core/os/b;)V
HSPLandroidx/fragment/app/E$d;->c()V
HSPLandroidx/fragment/app/E$d;->l()V
Landroidx/fragment/app/E$e$a;
Landroidx/core/os/b$a;
HSPLandroidx/fragment/app/E$e$a;-><init>(Landroidx/fragment/app/E$e;)V
Landroidx/fragment/app/E$e$b;
HSPLandroidx/fragment/app/E$e$b;-><clinit>()V
HSPLandroidx/fragment/app/E$e$b;-><init>(Ljava/lang/String;I)V
HSPLandroidx/fragment/app/E$e$b;->values()[Landroidx/fragment/app/E$e$b;
Landroidx/fragment/app/E$e$c;
HSPLandroidx/fragment/app/E$e$c;-><clinit>()V
HSPLandroidx/fragment/app/E$e$c;-><init>(Ljava/lang/String;I)V
HSPLandroidx/fragment/app/E$e$c;->a(Landroid/view/View;)V
HSPLandroidx/fragment/app/E$e$c;->b(I)Landroidx/fragment/app/E$e$c;
HSPLandroidx/fragment/app/E$e$c;->values()[Landroidx/fragment/app/E$e$c;
HSPLandroidx/fragment/app/E$e;-><init>(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/Fragment;Landroidx/core/os/b;)V
HSPLandroidx/fragment/app/E$e;->a(Ljava/lang/Runnable;)V
HSPLandroidx/fragment/app/E$e;->b()V
HSPLandroidx/fragment/app/E$e;->c()V
HSPLandroidx/fragment/app/E$e;->e()Landroidx/fragment/app/E$e$c;
HSPLandroidx/fragment/app/E$e;->f()Landroidx/fragment/app/Fragment;
HSPLandroidx/fragment/app/E$e;->g()Landroidx/fragment/app/E$e$b;
HSPLandroidx/fragment/app/E$e;->h()Z
HSPLandroidx/fragment/app/E$e;->k(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;)V
HSPLandroidx/fragment/app/E;-><init>(Landroid/view/ViewGroup;)V
HSPLandroidx/fragment/app/E;->a(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/w;)V
HSPLandroidx/fragment/app/E;->b(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/w;)V
PLandroidx/fragment/app/E;->d(Landroidx/fragment/app/w;)V
HSPLandroidx/fragment/app/E;->g()V
HSPLandroidx/fragment/app/E;->h(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/E$e;
HSPLandroidx/fragment/app/E;->i(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/E$e;
HSPLandroidx/fragment/app/E;->j()V
HSPLandroidx/fragment/app/E;->l(Landroidx/fragment/app/w;)Landroidx/fragment/app/E$e$b;
HSPLandroidx/fragment/app/E;->n(Landroid/view/ViewGroup;Landroidx/fragment/app/q;)Landroidx/fragment/app/E;
HSPLandroidx/fragment/app/E;->o(Landroid/view/ViewGroup;Landroidx/fragment/app/F;)Landroidx/fragment/app/E;
HSPLandroidx/fragment/app/E;->p()V
HSPLandroidx/fragment/app/E;->q()V
HSPLandroidx/fragment/app/E;->r(Z)V
LF/c$a;
HSPLF/c$a;->a()[LF/c$a;
HSPLF/c$a;-><clinit>()V
HSPLF/c$a;-><init>(Ljava/lang/String;I)V
LF/c$c$a;
HSPLF/c$c$a;-><init>()V
HSPLF/c$c$a;-><init>(Lt0/d;)V
LF/c$c;
HSPLF/c$c;-><clinit>()V
HSPLF/c$c;-><init>(Ljava/util/Set;LF/c$b;Ljava/util/Map;)V
HSPLF/c$c;->a()Ljava/util/Set;
LF/c;
HSPLF/c;-><clinit>()V
HSPLF/c;-><init>()V
HSPLF/c;->b(Landroidx/fragment/app/Fragment;)LF/c$c;
HSPLF/c;->e(LF/d;)V
LF/d;
HSPLF/d;-><init>(Landroidx/fragment/app/Fragment;Ljava/lang/String;)V
Landroidx/lifecycle/d;
HSPLandroidx/lifecycle/d;-><init>()V
HSPLandroidx/lifecycle/d;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
PLandroidx/lifecycle/d;->onActivityDestroyed(Landroid/app/Activity;)V
PLandroidx/lifecycle/d;->onActivityPaused(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/d;->onActivityResumed(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/d;->onActivityStarted(Landroid/app/Activity;)V
PLandroidx/lifecycle/d;->onActivityStopped(Landroid/app/Activity;)V
Landroidx/lifecycle/i$a;
HSPLandroidx/lifecycle/i$a;-><init>()V
HSPLandroidx/lifecycle/i$a;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
Landroidx/lifecycle/i;
HSPLandroidx/lifecycle/i;-><clinit>()V
HSPLandroidx/lifecycle/i;->a(Landroid/content/Context;)V
Landroidx/lifecycle/m$b;
HSPLandroidx/lifecycle/m$b;-><init>(Landroidx/lifecycle/k;Landroidx/lifecycle/g$b;)V
HSPLandroidx/lifecycle/m$b;->a(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
Landroidx/lifecycle/m;
Landroidx/lifecycle/g;
HSPLandroidx/lifecycle/m;-><init>(Landroidx/lifecycle/l;)V
HSPLandroidx/lifecycle/m;-><init>(Landroidx/lifecycle/l;Z)V
HSPLandroidx/lifecycle/m;->a(Landroidx/lifecycle/k;)V
HPLandroidx/lifecycle/m;->d(Landroidx/lifecycle/l;)V
HSPLandroidx/lifecycle/m;->e(Landroidx/lifecycle/k;)Landroidx/lifecycle/g$b;
HSPLandroidx/lifecycle/m;->f(Ljava/lang/String;)V
HSPLandroidx/lifecycle/m;->g(Landroidx/lifecycle/l;)V
HSPLandroidx/lifecycle/m;->b()Landroidx/lifecycle/g$b;
HSPLandroidx/lifecycle/m;->h(Landroidx/lifecycle/g$a;)V
HSPLandroidx/lifecycle/m;->i()Z
HSPLandroidx/lifecycle/m;->j(Landroidx/lifecycle/g$b;)V
HSPLandroidx/lifecycle/m;->k()V
HSPLandroidx/lifecycle/m;->l(Landroidx/lifecycle/g$b;)V
HSPLandroidx/lifecycle/m;->c(Landroidx/lifecycle/k;)V
HSPLandroidx/lifecycle/m;->m()V
Landroidx/lifecycle/LiveData$a;
HSPLandroidx/lifecycle/LiveData$a;-><init>(Landroidx/lifecycle/LiveData;)V
HSPLandroidx/lifecycle/LiveData$a;->run()V
Landroidx/lifecycle/LiveData$LifecycleBoundObserver;
Landroidx/lifecycle/LiveData$b;
PLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->i()V
HSPLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
HSPLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->j()Z
HSPLandroidx/lifecycle/LiveData$b;->d(Z)V
HSPLandroidx/lifecycle/LiveData$b;->i()V
Landroidx/lifecycle/LiveData;
HSPLandroidx/lifecycle/LiveData;-><clinit>()V
HSPLandroidx/lifecycle/LiveData;-><init>()V
HSPLandroidx/lifecycle/LiveData;->a(Ljava/lang/String;)V
HSPLandroidx/lifecycle/LiveData;->b(I)V
HSPLandroidx/lifecycle/LiveData;->c(Landroidx/lifecycle/LiveData$b;)V
HSPLandroidx/lifecycle/LiveData;->d(Landroidx/lifecycle/LiveData$b;)V
HSPLandroidx/lifecycle/LiveData;->e()V
HSPLandroidx/lifecycle/LiveData;->f()V
HSPLandroidx/lifecycle/LiveData;->g(Landroidx/lifecycle/q;)V
HSPLandroidx/lifecycle/LiveData;->h(Ljava/lang/Object;)V
Landroidx/lifecycle/p;
HSPLandroidx/lifecycle/p;-><init>()V
HSPLandroidx/lifecycle/p;->h(Ljava/lang/Object;)V
Landroidx/lifecycle/ProcessLifecycleInitializer;
LJ/a;
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;-><init>()V
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->c(Landroid/content/Context;)Landroidx/lifecycle/l;
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->b(Landroid/content/Context;)Ljava/lang/Object;
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->a()Ljava/util/List;
Landroidx/lifecycle/t;
HSPLandroidx/lifecycle/t;-><clinit>()V
HSPLandroidx/lifecycle/t;-><init>()V
HSPLandroidx/lifecycle/t;->g()Landroidx/lifecycle/g;
Landroidx/lifecycle/u$c;
HSPLandroidx/lifecycle/u$c;-><init>()V
HSPLandroidx/lifecycle/u$c;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
PLandroidx/lifecycle/u$c;->onActivityDestroyed(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityPaused(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->onActivityPostCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
HSPLandroidx/lifecycle/u$c;->onActivityPostResumed(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->onActivityPostStarted(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityPreDestroyed(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityPrePaused(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityPreStopped(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->onActivityResumed(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->onActivityStarted(Landroid/app/Activity;)V
PLandroidx/lifecycle/u$c;->onActivityStopped(Landroid/app/Activity;)V
HSPLandroidx/lifecycle/u$c;->registerIn(Landroid/app/Activity;)V
Landroidx/lifecycle/u;
HSPLandroidx/lifecycle/u;-><init>()V
HSPLandroidx/lifecycle/u;->a(Landroidx/lifecycle/g$a;)V
HSPLandroidx/lifecycle/u;->b(Landroidx/lifecycle/u$a;)V
HSPLandroidx/lifecycle/u;->c(Landroidx/lifecycle/u$a;)V
HSPLandroidx/lifecycle/u;->d(Landroidx/lifecycle/u$a;)V
HSPLandroidx/lifecycle/u;->onActivityCreated(Landroid/os/Bundle;)V
PLandroidx/lifecycle/u;->onDestroy()V
PLandroidx/lifecycle/u;->onPause()V
HSPLandroidx/lifecycle/u;->onResume()V
HSPLandroidx/lifecycle/u;->onStart()V
PLandroidx/lifecycle/u;->onStop()V
HSPLandroidx/lifecycle/z;-><init>()V
PLandroidx/lifecycle/z;->a()V
PLandroidx/lifecycle/z;->d()V
Landroidx/lifecycle/A;
HSPLandroidx/lifecycle/A;-><init>(Landroidx/lifecycle/D;Landroidx/lifecycle/A$b;)V
HSPLandroidx/lifecycle/A;->a(Ljava/lang/Class;)Landroidx/lifecycle/z;
HSPLandroidx/lifecycle/A;->b(Ljava/lang/String;Ljava/lang/Class;)Landroidx/lifecycle/z;
Landroidx/lifecycle/D;
HSPLandroidx/lifecycle/D;-><init>()V
PLandroidx/lifecycle/D;->a()V
HSPLandroidx/lifecycle/D;->b(Ljava/lang/String;)Landroidx/lifecycle/z;
HSPLandroidx/lifecycle/D;->d(Ljava/lang/String;Landroidx/lifecycle/z;)V
Landroidx/lifecycle/F;
HSPLandroidx/lifecycle/F;->a(Landroid/view/View;Landroidx/lifecycle/l;)V
Landroidx/lifecycle/G;
HSPLandroidx/lifecycle/G;->a(Landroid/view/View;Landroidx/lifecycle/E;)V
Landroidx/startup/a;
HSPLandroidx/startup/a;-><clinit>()V
HSPLandroidx/startup/a;-><init>(Landroid/content/Context;)V
HSPLandroidx/startup/a;->a()V
HSPLandroidx/startup/a;->b(Landroid/os/Bundle;)V
HSPLandroidx/startup/a;->c(Ljava/lang/Class;)Ljava/lang/Object;
HSPLandroidx/startup/a;->d(Ljava/lang/Class;Ljava/util/Set;)Ljava/lang/Object;
HSPLandroidx/startup/a;->e(Landroid/content/Context;)Landroidx/startup/a;
HSPLandroidx/startup/a;->f(Ljava/lang/Class;)Ljava/lang/Object;
HSPLandroidx/startup/a;->g(Ljava/lang/Class;)Z
Landroidx/appcompat/widget/N;
HSPLandroidx/appcompat/widget/N;-><init>(Landroidx/appcompat/widget/Toolbar;)V
HSPLandroidx/appcompat/widget/N;->run()V
Landroidx/fragment/app/m;
Lw/a;
HSPLandroidx/fragment/app/m;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/n;
HSPLandroidx/fragment/app/n;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/o;
HSPLandroidx/fragment/app/o;-><init>(Landroidx/fragment/app/q;)V
Landroidx/fragment/app/p;
HSPLandroidx/fragment/app/p;-><init>(Landroidx/fragment/app/q;)V
Landroidx/lifecycle/s;
HSPLandroidx/lifecycle/s;-><init>(Landroidx/lifecycle/t;)V
Lh/c;
HSPLh/c;->a(Ljava/lang/Object;)V
@@ -1,10 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "COMPATIBLE_SCREEN_MANIFEST",
"kind": "Directory"
},
"applicationId": "io.truckwash.twa",
"variantName": "release",
"elements": []
}

Some files were not shown because too many files have changed in this diff Show More