Gå til hovedindhold
Hjem
GrN.dk

Main navigation

  • Artikler
  • Cases
  • Ydelser
  • Din digitale projektleder
  • Om Greg Nowak
  • Billedgalleri
  • Kontakt
User account menu
  • Log ind

Bliv en del af mit community / gratis nyhedsbrev — tilmeld dig her

Brødkrumme

  1. Hjem
  2. Cases

Mit Håndværk udgiver sig selv i to app stores — og en rigtig telefon har vetoret

Client
Mit Håndværk — dansk markedsplads-app for håndværk, kunst og design (mithaandvaerk.dk); Flutter på Android og iOS, Laravel-backend
Sector
Automatisering af mobiludgivelse — Flutter CI/CD, on-device acceptancetest, udgivelse til Google Play og App Store
Periode
Pipeline i drift siden 12. juli 2026 · 18 produktionsreleases på Play og 20 grønne device-smokes pr. 26. juli 2026

At a glance

Mit Håndværk er en dansk markedsplads-app for håndværk, kunst og design — en Flutter-app på Android og iOS oven på en Laravel-backend, med abonnementer, realtidschat og push. Denne case handler ikke om appens skærme. Den handler om maskineriet, der sætter en ny version af den foran brugerne: en pipeline, der tager et commit på main og cirka ti minutter senere har udgivet det til Google Play production — men kun efter at releasen er blevet installeret og koldstartet på en rigtig Android-enhed og har overlevet det.

Samme pipeline dækker Apple fra en Linux-server, der slet ikke kan bygge iOS: et release-tag overdrager buildet til en cloud-macOS-maskine, som signerer det og uploader det til TestFlight — og selve tagget kan ikke pushes, medmindre appens abonnementer faktisk er godkendt og prissat i App Store Connect.

Alt i billedet ovenfor er aflæst af det kørende system: tællerne kommer ud af pipelinens egen log, og telefonpanelet er det screenshot, publiceringsgaten trækker af emulatoren ved hver eneste release.

10 minutter, ubemandet, fra et commit på main til en udgivet produktionsrelease på Google Play
18 produktionsreleases siden 12. juli 2026 og 20 device-smokes før udgivelse — alle grønne
44 tests skal bestå, før der overhovedet bygges en APK — og en fejlet start på enheden kan aldrig omgås

The challenge

At sende en Flutter-app til begge stores ligner et løst problem, indtil man skriver ned, hvad der faktisk går galt.

»Den kompilerer« og »den starter« er to forskellige påstande. Et release-build kan bestå statisk analyse og hele unit-suiten og alligevel dø i det øjeblik, det starter på en enhed — en manglende native nøgle, et plugin der kun opfører sig forkert i release mode, en shrinker der har fjernet noget, reflection havde brug for. Intet af det kan ses på en build-server. Det kan kun ses på en telefon.

Der er ingen blød landing på Google Play her. Releases går direkte til production-tracket, så skadesradius for et dårligt build er hele brugerbasen, ikke en 5 %-canary. Den ene kendsgerning afgør hele designet: gaten skal sidde før uploaden, og den skal være umulig at omgå.

Apples karakteristiske fejl er slet ikke buildet. Det brud, denne pipeline er hærdet imod, fejlede hverken i kompilering, signering eller upload — det blev shippet perfekt og fortalte derefter brugerne »no products available«, fordi et abonnementsprodukt havde forladt APPROVED-tilstanden i App Store Connect. Sandbox og TestFlight er strukturelt blinde for det: de svarer ud fra en anden konfiguration end produktion. Så tjekket kan ikke bo i appen eller i testkontiene; det skal være en assertion mod selve App Store Connect, foretaget før en release overhovedet findes.

Begge stores afviser et genbrugt build-nummer — og Play beviste det ved at afvise en upload blankt med »versionCode already used«, fordi et menneske forventedes at bumpe et tal i en fil og én gang lod være.

Build-hosten kan ikke køre en emulator. Den har ingen hardware-virtualisering, så ingen Android-AVD kan boote på den, og som Linux-maskine kan den heller ikke røre Xcode. Enhver påstand om, hvad der sker på en enhed, afhænger derfor af en anden maskine — hvilket introducerer det sidste problem: en robot, der udgiver, skal kunne skelne »appen er i stykker« fra »min testrig er offline«. Blander man de to sammen, blokerer den enten releases for evigt på infrastrukturstøj — eller trækker på skuldrene og udgiver i blinde.

The solution

En pollende CI, ikke en webhook. Hvert tiende minut spørger build-hosten repositoryet, om main har flyttet sig. Et nyt commit tager en single-flight-lås (et signeret build tager længere end ti-minutters-ticket) og registreres, når det er bygget — med store-uploaden sporet i sin egen tilstand, så en forbigående upload-fejl prøver igen på næste tick i stedet for stille at blive markeret som færdig. Kun main bygges nogensinde: testbranches kan pushes hele dagen uden at udgive noget.

En host-gate, der kører, før der overhovedet findes et artefakt. Dependencies resolves, statisk analyse kører, unit- og widget-suiten kører som en hård gate — 44 tests i dag, op fra 3 da gaten blev indført — repository-hygiejnen tjekkes, og den live backend probes read-only: REST-endpointet skal svare 200, og den offentlige WebSocket skal gennemføre et rigtigt handshake gennem Cloudflare. En fejl her betyder, at der slet ikke produceres nogen APK, en alarmmail sendes, og det dårlige commit markeres, så det ikke prøves igen hvert tiende minut i al evighed.

Så får telefonen vetoret. Det signerede build kopieres til en anden maskine, der har virtualisering, en Android-emulator booter headless, og appen installeres forfra: lageret ryddes, loggen ryddes, og appen startes gennem den rigtige launcher-intent. Ti sekunder senere assertes to ting: processen lever stadig, og der står ingen fatal exception i loggen. Et screenshot hentes tilbage som bevis — det er telefonpanelet i billedet ovenfor, produceret af pipelinen selv, ikke iscenesat til denne side.

Dommen er trevejs, og det er den vigtige del. Pass udgiver. En reel launch-fejl blokerer altid udgivelsen, kan aldrig omgås af noget flag og er terminal, så et ødelagt commit ikke testes og alarmeres igen hvert tiende minut. »Testrig utilgængelig« er sit eget udfald: det blokerer også som standard, men behandles som forbigående — releasen prøver simpelthen igen, når riggen kommer tilbage. En operatør kan aktivt slå udgivelse uden device-tjek til, og den kontakt leveres slået fra.

Ingen bumper en version i hånden. Før upload spørger pipelinen Google Play om den højeste versionCode, den nogensinde har set på tværs af bundles, APK'er og track-releases, lægger én til og stempler den på buildet. Uploaden går til production-tracket som en færdig release, og en mail rapporterer version, build-nummer, commit og smoke-udfaldet — med en emnelinje, der siger, om device-tjekket bestod, blev sprunget over eller blokerede releasen.

Apple, styret fra en Linux-host. Et release-tag udløser en cloud-macOS-builder, og tagget gates, før det overhovedet pushes: et script forespørger App Store Connect og asserter, at hvert abonnementsprodukt er APPROVED til den forventede pris i danske kroner — hvis ikke, pushes tagget ikke, og intet shippes. Det identiske tjek er vendoreret ind i cloud-buildet som sit eget trin, så selv et håndpushet tag fejler ved gaten i stedet for at producere en signeret release med et ødelagt købsflow. Det er bevist i begge retninger: en bevidst falsk produktliste fejlede et scratch-build ved gaten med nul artefakter, og det første rigtige gatede release-tag gik gennem alle fjorten trin ind i TestFlight.

iOS-banens øvrige trin er alle sammen lærepenge. Swift Package Manager er slået fra, så CocoaPods alene embedder push-frameworket (begge på én gang gav duplikerede frameworks ved archive-tid); de fem credentials valideres, før noget dyrt går i gang; den committede lock-fil droppes, så pods resolver forfra; automatisk kodesignering tvinges til manuel — kun i CI — fordi det committede projekt er rettet mod en udviklers maskine, og en ubemandet runner ikke har nogen Apple-konto at signere interaktivt med; signeringsfiler hentes til begge bundle-identifiers — appen og dens notification extension; og build-nummeret tages fra det seneste TestFlight-build plus én, så et re-tag aldrig kræver en versionsrettelse.

iOS-tests kører også i skyen. Et andet workflow uden egen trigger startes gennem build-udbyderens REST-API, booter en iOS-simulator og kører integrationstestene — og sådan får en ren iOS-ændring et rigtigt testresultat fra en Linux-host, der ikke kan bygge til iOS.

Og hver morgen, uafhængigt af enhver release, bruges den samme enhed igen. Det nyeste build geninstalleres, og det assertes, at det starter; derefter reseedes fire abonnementstilstande på backenden — aktiv, to varianter af opsagt og udløbet — appen drives efter fixture, og abonnementsskærmen læses tilbage med OCR. Det er ikke et stilistisk valg: et Flutter-view når aldrig accessibility-tilstanden »idle« og eksponerer ingen tekstnoder til det standardiserede Android-tooling, så at læse pixels er den eneste pålidelige måde at asserte, hvad brugeren faktisk får at vide. Det findes på grund af en rigtig bug — et opsagt abonnement, der meldte, at det udløb »om 0 dage« — og det vogter hele kæden, fra gemt ordretilstand gennem den live reconcile til det, API'et og socketen rækker appen. Det alarmerer kun på en reel app-defekt og tier stille, når riggen bare er slukket; infrastruktur-hikke råber aldrig ulven kommer.

The results

Hvad pipelinen faktisk har udrettet, aflæst af dens egen log den 26. juli 2026: i drift siden 12. juli, 1.838 polling-kørsler, 18 releases udgivet til Google Plays production-track (versionCode 116 til 133) og 20 device-smokes før udgivelse — alle sammen grønne. Ingen release er nogensinde udgivet uden én, og bypass-kontakten, der ville tillade det, er aldrig blevet brugt. I 17 af de 18 releases blev hele kørslen — host-gate, signeret build, device-smoke, bundle-upload — færdig inden for ét ti-minutters vindue; den ene undtagelse var en Gradle build-daemon, der crashede midt i buildet, hvorefter releasen blev udgivet på næste tick i stedet. Host-gaten er i samme periode vokset fra 3 tests til 44, og de fem fejlede builds i hele perioden var alle miljø, ikke app-kode: fire på førstedagen, om filejerskab mellem det delte Flutter-SDK og checkoutet, plus den ene crashede daemon.

På Apple-siden er gaten live på den rigtige udgivelsesvej, ikke kun i en testbane: det første produktions-release-tag gik igennem den 24. juli 2026, klarede alle fjorten build-trin — validering af credentials, abonnements-godkendelses-gaten mod App Store Connect, friske pods, tvungen manuel signering, hentning af profiler, et auto-inkrementeret build-nummer — og landede i TestFlight som et signeret build. Selve App Store-listningen ligger med vilje én version bag Google Play: Apple menneske-reviewer alligevel produktionsreleases, så at indsende til review forbliver en bevidst menneskelig beslutning — og er én udkommenteret linje fra at være automatisk.

Hvad dette ikke påstår. En grøn launch-smoke beviser, at appen installerer og koldstarter uden at crashe — den beviser ikke, at hver skærm virker. Tilliden på skærmniveau kommer fra host-suitens 44 tests, morgenens OCR-regression over fire abonnementstilstande og simulator-integrationsbanen, der køres on demand; der citeres ingen crash-rate eller adoptionstal her, for at måle dét er store-konsollernes job, ikke denne pipelines.

Hvad det koster, sagt ligeud. At udgive direkte til 100 % produktion betyder, at en stor dependency-opgradering er sin egen release værd frem for at blive batchet, og at to af dem aldrig må være i gang samtidig — så de kørsler serialiserer bevidst og sætter tidsplanen på pause, mens de arbejder. Fordi build-hosten ikke kan emulere noget, afhænger enhver on-device-påstand af, at en anden maskine er oppe — hvilket er præcis grunden til, at »rig offline« er en førsteklasses dom i stedet for et stille pass. Og den ærlige opsummering af hele designet er reglen, publiceringsscriptet håndhæver: maskinen må udgive, men den må aldrig udgive noget, der ikke kunne starte.

En reel fejl ved app-start blokerer altid udgivelsen — kan aldrig omgås, og er terminal, så et ødelagt commit ikke bliver re-smoket og re-alarmeret hvert tiende minut.

Reglen i publiceringsscriptet, der afgør, om et build når Google Play

Got a project that needs the same kind of hands-on delivery?

Your digital project manager

Anmeld Greg på Google

Greg Nowak Google-anmeldelser

 

Skriftlige anbefalinger fra Trafik og Veje, Aarhus Kommune (2011) og AgroTech (2010) — læs dem på LinkedIn.

Illustrated infographic summarizing: OpenAI File Search: Interne dokumenter kræver governance, før du kan stole på dem
OpenAI File Search: Interne dokumenter kræver governance, før du kan stole på dem
2026-08-18

OpenAI File Search gør retrieval let at demonstrere, men produktion kræver opryddede dokumenter, metadata, en gennemtænkt vector store-struktur, regler for udløb og omkostningsstyring.

Illustrated infographic summarizing: Fra leverandørfaktura til bogføring: AI med en kontrolpost
Fra leverandørfaktura til bogføring: AI med en kontrolpost
2026-08-18

AI kan lette arbejdet med leverandørfakturaer, men sikker bogføring kræver validering, dubletkontrol, godkendelse og et tydeligt kontrolspor.

Flere artikler
RSS feed

Footer

  • Alle artikler
  • Kontakt

GrN.dk — AI-automatisering, webplatforme, weboptimering, datahåndtering og logistik.

© 2026 GrN.dk · LinkedIn · Kontakt · AI-automatisering på dansk: nowa.dk

Bag GrN.dk: Individual Entrepreneur Codecrafter · Tax ID 305669096 · Bakhtrioni St. 22, 0194 Tbilisi, Georgien · officielt virksomhedsregister