Turning Complex Problems Into Confident Technology
Shipping an internal app to my own phone
Integrating other people’s systems · Production · Inside the Workshop
The problem
My internal dashboard runs in a browser and as a native app on Windows and Android, built from one codebase and delivered to my own devices through the device-management platform, so that a release is a pipeline run rather than an afternoon. Writing the screens was the easy part. Roughly half of the commits in that repository went on packaging, signing, distribution and sign-in: getting a working build onto a device, and then letting me in.
This is not a tutorial on any of those tools. It is what the failures had in common, because every one of them happened after the build had gone green.
Five failures after a green build
Installed, and reported as not installed. Every Windows install was reported as failed with the same error, “application not detected after installation”, while the app was installed and running. The first diagnosis was that the rule checking whether the app is present compared the wrong field of the package. The fix switched it to a different field, and that was wrong too, because the new field holds the full package name with the version and publisher attached, so it could never match. A second fix put the right field back, and the error stayed. Three weeks later the log on an enrolled device showed the real requirement: the device-management agent only counts a detection script as successful if it exits cleanly and also writes something to its output, and mine only set the exit code. One error code had more than one cause, and only the device's own log could tell them apart. On Android there was a quieter version of the same gap: publishing to the store does not make an app visible to device management until it is synced and assigned, and neither step was automated, so a fully successful run left nothing for a device to see.
Signed with the wrong key, and told it was a permissions problem. The store rejected the Android upload with “APK has been signed in debug mode”, returned as a permission-denied error. The build had been given four signing settings and had accepted and silently ignored all four, because a fifth setting that switches signing on at all defaults to off. Nothing in the build output hinted at it: the key was downloaded, the arguments looked right and a package came out. My own publishing script then made it worse. It assumed every permission-denied response meant a missing account grant, and printed confident, detailed and entirely wrong instructions.
A name that can never be used again. The app already in the store carried a signing certificate whose key could no longer be found. Android does not let an app change its certificate, the store's API offers no way to delete an app, and a package name is reserved permanently once it has been used. The only way forward was a new application identity, and that identity is also part of the address sign-in returns to, so changing one without the other would have produced an app that installed cleanly and then failed at login.
A black screen with no logs. The Android app then installed and opened to a black screen that never changed. There was no crash, no exception and nothing in the device logs, so every diagnosis driven by logs came back clean. Code trimming and ahead-of-time compilation were the obvious suspects. I switched both off and nothing changed, which cost a build cycle but stopped a guess shipping as a fix. The cause was a missing start-up class that Android needs and Windows does not, which is why the same commit worked on one platform and not the other. That asymmetry was the clue.
Sign-in that worked everywhere I tested it. The sign-in return address includes a fingerprint of the app's signing certificate. The one committed to the code came from my local debug key, so every local and emulator test passed, while every build the pipeline signed and the store shipped could not complete sign-in. Fixing that exposed the next problem. On first start the app fetched three things in parallel, each found nobody signed in, and each opened its own sign-in screen. Measured on an emulator: three sign-in screens before the fix, one after.
What I chose, and why
In every case the failure surfaced later and somewhere else (on a device, at the store, at sign-in) with an error that pointed at the wrong layer: a permissions error for a signing mistake, “not installed” for an installed app, a blank screen for a missing class. So the fixes move the failure back into the job that caused it.
- Check the artefact, not the step. The build now reads the signer of the package it has just made and fails if it is the debug key. “Signing succeeded” is a claim; the certificate on the package is evidence.
- Read the message, not the status code. The publishing script now reads the store's error text and only gives the account-permissions walkthrough when the message is actually about permissions.
- Derive identity, never type it. The certificate fingerprint in the sign-in address is now worked out at run time from the certificate the running app is actually signed with. A value fixed in the code can only ever be right for one kind of build, and it was right for the kind I tested.
- One sign-in at a time. A lock around sign-in means the first request prompts and the others wait, then find the account and carry on.
- Automate the whole route to the device. Store sync and group assignment are part of the pipeline now, so a successful run means a device can see the app, not just that the store accepted it.
What I would do differently
I would put a release-signed build on a real device in the first week, before writing a single screen. Almost all of this was invisible until the shipped package met a device: local builds use local keys, emulators use debug builds, and the pipeline reports what it did rather than what arrived. The first commit was in June and the last of these fixes was at the end of September, and most of these would have surfaced in that first week instead.
I would also count this cost before choosing a native app for an internal tool. The browser version shows the same screens and needs none of it. That is not an argument against the app, but the decision should be made knowing that the screens are the cheap half.
Related
If this is your problem too
If your releases report success and your users report something else, that gap is a short piece of work in a diagnosis. Another case of an integration that compiled cleanly and failed against the real thing is fifteen API routes that never existed, and the sign-in side is covered in more depth in three identity providers, one flow.
Got a problem like this?
A free 20-minute call: you’ll know whether I can help, what is probably wrong, and what the next step involves. No obligation.