diff --git a/.github/workflows/rollback.yml b/.github/workflows/deploy-existing-release.yml similarity index 78% rename from .github/workflows/rollback.yml rename to .github/workflows/deploy-existing-release.yml index 366ca1a..fe186a9 100644 --- a/.github/workflows/rollback.yml +++ b/.github/workflows/deploy-existing-release.yml @@ -1,4 +1,4 @@ -name: Rollback +name: Deploy Existing Release on: workflow_dispatch: @@ -10,23 +10,29 @@ jobs: packages: write contents: write # Write is required to create/update releases steps: - - name: Ensure pre-requisites for rollback are met - id: validate_rollback + - name: Ensure pre-requisites for deployment are met + id: validate_deployment uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const { owner, repo } = context.repo; /* - * We load 20 releases here, it's unlikely that we will want to roll back - * to a release older than that. - */ + * We load 20 releases here, it's unlikely that we will want to roll back + * to a release older than that. + */ const PAGE_SIZE = 20; - const releases = await github.rest.repos.listReleases({ owner, repo }); + const releases = await github.rest.repos.listReleases({ + owner, + repo, + page: PAGE_SIZE, + }); - // Pre-releases should not be considered for rollback + /* + * Pre-releases should not be considered for deployment here, the `publish-new-release` action + * should be used instead. + */ const availableReleases = releases.data.filter((release) => !release.prerelease); - const targetRelease = availableReleases.find( (release) => release.tag_name === "${{github.ref_name}}" ); @@ -35,7 +41,7 @@ jobs: } console.log( - `Found release ${targetRelease.id}, proceeding with rollback: ${targetRelease.url}` + `Found release ${targetRelease.id}, proceeding with deployment: ${targetRelease.html_url}` ); return targetRelease.id; @@ -73,6 +79,6 @@ jobs: await github.rest.repos.updateRelease({ owner, repo, - release_id: ${{ steps.validate_rollback.outputs.result }}, + release_id: ${{ steps.validate_deployment.outputs.result }}, make_latest: true }); diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dbd7e3c..76d7083 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest permissions: packages: write - contents: write # Write is required to create/update releases + contents: write # Write is required to create/update releases AND to write tags steps: - name: Checkout uses: actions/checkout@v3 @@ -51,27 +51,25 @@ jobs: const { owner, repo } = context.repo; /* * We should only need to load 2 releases, as either both will be latest, or one will be latest and - * the other will be pending. We're loading a few extras here in case we get into a weird state. + * the other will be pending. We're loading a few extras here so that if we get into a weird state + * we can provide a better error. */ - const PAGE_SIZE = 5; - const releases = await github.rest.repos.listReleases({ owner, repo }); + const PAGE_SIZE = 10; + const releases = await github.rest.repos.listReleases({ + owner, + repo, + page: PAGE_SIZE, + }); const pendingReleases = releases.data.filter(release => release.prerelease); if (pendingReleases.length > 1) { throw new Error(`Found more than one pending release: ${pendingReleases.map(release => release.tag_name).join(', ')}`); } - - const latestRelease = releases.data.find(release => !release.prerelease); - if (!latestRelease) { - throw new Error('No latest release found'); - } - if (pendingReleases.length === 1) { console.log(`Found existing pending release: ${pendingReleases[0].tag_name}, replacing it`); await github.rest.repos.deleteRelease({ owner, repo, release_id: pendingReleases[0].id }); } - console.log('No pending release found, creating one'); const newRelease = await github.rest.repos.createRelease({ owner, repo, diff --git a/.github/workflows/deploy.yml b/.github/workflows/publish-new-release.yml similarity index 91% rename from .github/workflows/deploy.yml rename to .github/workflows/publish-new-release.yml index 6a1f4f9..948da95 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/publish-new-release.yml @@ -1,4 +1,4 @@ -name: Production Deploy +name: Publish New Release on: workflow_dispatch: @@ -21,8 +21,12 @@ jobs: * We should only need to load 1 release here, as pending should be at the top of the list. * We're loading a few extras here in case we get into a weird state. */ - const PAGE_SIZE = 5; - const releases = await github.rest.repos.listReleases({ owner, repo }); + const PAGE_SIZE = 10; + const releases = await github.rest.repos.listReleases({ + owner, + repo, + page: PAGE_SIZE, + }); const pendingReleases = releases.data.filter((release) => release.prerelease); if (pendingReleases.length > 1) { @@ -41,7 +45,7 @@ jobs: } console.log( - `Found pending release ${targetRelease.id}, proceeding with deployment: ${targetRelease.url}` + `Found pending release, proceeding with deployment: ${targetRelease.html_url}` ); return targetRelease.id; diff --git a/README.md b/README.md index e426cb0..4fe1d38 100644 --- a/README.md +++ b/README.md @@ -6,34 +6,43 @@ Proof-of-concept for a deployment/rollback flow using github actions and release - When a merge is done to the `main` branch, a pending release should be created / updated - A tag should be created and used for the release based on the current date and time - - The release should be created with the tag and the release notes should be the commit messages since the last release + - The release should be created with the tag and notes describing the changes since the last release - The release should be marked as a prerelease - - Any artifacts required for the deployment should be uploaded to the release + - Any artifacts required for the deployment should be uploaded to the release (none yet but could be in the future) - The release should be automatically deployed to the `staging` (`dev`) environment -- When the release action is manually triggered - - The release should be marked as a non-draft - - A deployment should be done to the `production` environment -- When the rollback action is manually triggered - - The last release should be marked as a draft +- When the `publish-new-release` action is manually triggered + - A deployment should be done to the `production` environment using the selected pre-release + - The release should be set to `latest` +- When the `deploy-existing-release` is manually triggered - A deployment should be done to the `production` environment using the selected release + - The current release should have it's `latest` status removed + - The target selected release should be set to `latest` + +## Limitations + +- We're only providing the option to deploy ALL pending releases rather than pick and choose an older one like we have with the current behaviour ## Implementation - ~~https://github.com/marketplace/actions/get-release is used to get the id of the last release~~ - ~~https://github.com/softprops/action-gh-release is used to create the new release~~ - ~~https://github.com/irongut/EditRelease is used to edit the release if it already exists~~ -- Instead of using multiple poorly maintained I instead opted to make heavy use of `actions/github-script@v7` +- Instead of using multiple poorly maintained actions I instead opted to make heavy use of `actions/github-script@v7` - ~~`git log --pretty=oneline tagA...tagB` is used to get the commit messages since the last release~~ -- It turned out github automatic release notes are good enough +- It turned out github automatic release notes are actually better, but rely heavily on PRs (which should suit us). ## Run Sample App Locally +### From Github Registry + ```sh -docker build --build-arg APP_VERSION=local -t action-deployment-poc:local . -docker run -p 7890:80 action-deployment-poc:local +docker pull docker pull ghcr.io/danielemery/action-deployment-poc:latest +docker run -p 7890:80 ghcr.io/danielemery/action-deployment-poc:latest ``` -# Initial setup +### With Local Build -- The latest state of the main branch should be deployed to production -- A date tag and release should be up to date with the latest state of the main branch +```sh +docker build --build-arg APP_VERSION=local -t action-deployment-poc:local . +docker run -p 7890:80 action-deployment-poc:local +```