The first APEX application we wrote in 2017 was in an environment entirely different than today. Let me first set the scene of how deployments were done back then.
The team could freely develop and release in the development (D) and test (T) environment. But when we wanted the key users to have a look, we needed to deploy our application to the acceptance (A) environment, where the team was not allowed to deploy. We only had select privileges to be able to investigate when key users needed assistance. The same lack of privileges applied to the production (P) environment. The DBA team handled the deployments at A and P for security reasons. We needed to provide them with a deployment package and a release document describing how to deploy. Then, the project lead, a developer and the key user went to the DBA where we three were watching him type in the needed commands. When he was done, the key user quickly checked out if the application was indeed upgraded and then we all signed the release document.
In the acceptance environment we did an additional check: after successfully installing the new version, we also ran the uninstall, which is mandatory at this client, and checked if we were indeed back at the original version. And then again, the install. In case a deployment at P failed, we would know we could safely rollback.
So, when I started in 2017, the team was already 3 months on its way, but lacked a deployment pipeline, which I was allowed to create, next to my normal development duties. Fortunately, Jenkins was already in place, so I started with a nightly build in a separate BLD schema. I developed a complete_uninstall.sql and a complete_install.sql and asked my fellow developer to keep these files up to date. And every night a Jenkins shell script ran sqlplus. First, it ran the complete_uninstall which was forgiving, i.e. when an error occurred, the script would continue (whenever sqlerror continue/whenever oserror continue). It does check at the end if the schema is empty. The complete_install, however, was not: each error resulted in a failure of the Jenkins job and the developers receive an e-mail. A nightly build like this is ideal to check if all needed source files are checked in.