{"id":20339233,"url":"https://github.com/jgoux/preview-environments-per-pull-request-using-aws-cdk-and-github-actions","last_synced_at":"2025-06-12T21:12:25.176Z","repository":{"id":42054934,"uuid":"347447656","full_name":"jgoux/preview-environments-per-pull-request-using-aws-cdk-and-github-actions","owner":"jgoux","description":null,"archived":false,"fork":false,"pushed_at":"2022-04-15T06:06:04.000Z","size":77,"stargazers_count":73,"open_issues_count":1,"forks_count":13,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-11T23:12:42.197Z","etag":null,"topics":["aws","cdk","githubactions","gitops"],"latest_commit_sha":null,"homepage":"https://dev.to/jgoux/preview-environments-per-pull-request-using-aws-cdk-and-github-actions-bfi","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jgoux.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-03-13T18:26:08.000Z","updated_at":"2025-03-25T19:28:50.000Z","dependencies_parsed_at":"2022-08-12T03:31:17.953Z","dependency_job_id":null,"html_url":"https://github.com/jgoux/preview-environments-per-pull-request-using-aws-cdk-and-github-actions","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jgoux%2Fpreview-environments-per-pull-request-using-aws-cdk-and-github-actions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jgoux%2Fpreview-environments-per-pull-request-using-aws-cdk-and-github-actions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jgoux%2Fpreview-environments-per-pull-request-using-aws-cdk-and-github-actions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jgoux%2Fpreview-environments-per-pull-request-using-aws-cdk-and-github-actions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jgoux","download_url":"https://codeload.github.com/jgoux/preview-environments-per-pull-request-using-aws-cdk-and-github-actions/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248492876,"owners_count":21113163,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aws","cdk","githubactions","gitops"],"created_at":"2024-11-14T21:15:53.612Z","updated_at":"2025-04-11T23:13:04.748Z","avatar_url":"https://github.com/jgoux.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 📖 Table of contents\n\n- [👋 Introduction](#-introduction)\n- [🎯 Target](#-target)\n- [✅ Prerequisites](#-prerequisites)\n- [☁️ AWS](#-aws)\n  - [Architecture](#architecture)\n  - [CDK](#cdk)\n    - [File structure](#file-structure)\n      - [cdk.json](#cdkjson)\n      - [bin/app.ts](#binappts)\n      - [lib/awesome-stack.ts](#libawesome-stackts)\n    - [CLI](#cli)\n      - [yarn bootstrap](#yarn-bootstrap)\n      - [yarn deploy](#yarn-deploy)\n      - [yarn destroy](#yarn-destroy)\n- [🤖 Github automation](#-github-automation)\n  - [Github Deployments API](#github-deployments-api)\n  - [Github Actions](#github-actions)\n    - [Pull Request deploy](#pull-request-deploy)\n    - [Pull Request clean-up](#pull-request-clean-up)\n- [📸 Workflow in pictures](#-workflow-in-pictures)\n- [🌇 Conclusion](#-conclusion)\n\n\n# 👋 Introduction\n\nOne of the biggest challenges today when delivering software is to move **fast**. Moving fast and keeping a high quality and confidence in your code is not an easy task. Having a tight feedback loop is our number one priority at [Clovis](https://www.clovis.pro).\n\nServices like [Vercel](https://vercel.com/), [Netlify](https://www.netlify.com/), [Render](https://render.com/), [Qovery](https://www.qovery.com/) or [Railway](http://railway.app/) are all working toward that goal, making our lives **way easier**. All these platforms allow us to quickly iterate, delivering value by focusing only on our code and forgetting about the nightmare that devops can be. I'm grateful for each of them.\n\nBut what if devops could be **good old regular code**? So accessible that you could actually spin up your entire infrastructure in few lines of code. By directly using a cloud provider you have no more limits, at a lower cost!\n\nAs an example, I'm going to show you how to setup preview environments in your Pull Requests on Github, using AWS CDK and Github Actions. Preview environments are a terrific tool to share your work before it goes to production or to run end-to-end tests. It gives a lot of confidence to have a mirror of your production infrastructure to play with.\n\n# 🎯 Target\nAt the end of this tutorial, we should be able to :\n- Trigger a preview environment deployment by labelling a Pull Request with `🚀 deploy`\n- As long as the Pull Request is labeled with `🚀 deploy`, we should have our preview environment updated on each push\n- When the label `🚀 deploy` is removed from the Pull Request or the Pull Request is closed, the preview environment should be destroyed\n- All the deployments should be integrated with Github Deployments API to have useful feedbacks from within the Pull Requests or on the repository's home page\n\n# ✅ Prerequisites\n- Node.js LTS (v14.16.0)\n- AWS :\n  - Sign up and get your [access keys](https://docs.aws.amazon.com/powershell/latest/userguide/pstools-appendix-sign-up.html)\n  - [Set the access keys as secrets](https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository) under the names `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in your repository.\n  - [Set them on your development machine](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html) to run the various cdk commands. Don't forget to set a default region (previous link) if you want to deploy in a different region than `us-east-1`.\n\n# ☁️ AWS\n\n## Architecture\n\n![post](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wamluxonpu3k7otzf2l2.png)\n\nThe architecture is composed of [CloudFront](https://aws.amazon.com/cloudfront/) (CDN) in front of a private [S3 Bucket](https://aws.amazon.com/s3/) (file storage). CloudFront serves the sources stored in the bucket to the outside world.\n\n## CDK\n\nThe AWS **C**loud **D**evelopment **K**it is an open source software development framework to define your cloud application resources using familiar programming languages such as TypeScript in the context of this post.\n\nTypeScript gives us autocompletion and type safety on every part of our infrastructure, no more back and forth with the documentation, everything is available with a few keystrokes.\n\nYou can forget the hundreds of lines of CloudFormation yaml files or the confusing AWS Console UI. As an infrastructure as code framework, CDK is an abstraction over these low level constructs with sensible defaults.\n\n### File structure\n\nA typical CDK project is composed of :\n- `bin/app.ts` the main file instantiating the stacks and called by the cdk CLI\n- `lib/*-stack.ts` the stacks implementations\n- `cdk.json` the configuration file for the cdk CLI\n\nLet's see what is inside each of these files.\n\n#### cdk.json\n```json\n{\n  \"app\": \"yarn ts-node bin/app.ts\",\n  \"context\": {\n    \"@aws-cdk/core:newStyleStackSynthesis\": true\n  }\n}\n```\n\nThe `app` key is used to tell cdk how to start our app.\n\nThe `context` key can contain feature flags like the one used here. In a future release of the AWS CDK, \"new style\" stack synthesis will become the default, but for now we need to opt in using the feature flag.\n\n#### bin/app.ts\n```ts\nimport * as cdk from '@aws-cdk/core';\n\nimport AwesomeStack from '../lib/awesome-stack';\n\nconst app = new cdk.App();\n\n/**\n * The name of the stack depends on the STAGE environment variable so we can deploy the infrastructure multiple times in parallel\n * @example\n * AwesomeStack-pr-1-awesome-branch\n * AwesomeStack-production\n */\nconst stackName = 'AwesomeStack-' + process.env.STAGE;\n\nnew AwesomeStack(app, stackName);\n```\n\nThe stack's name is an important concept. This is what we will be referring to when deploying with the cdk CLI. You can note that in our case, this name depends on a STAGE environment variable. This allow us to dynamically deploy a whole new stack by Pull Request as the STAGE variable is derived from the Pull Request number and the branch name (more on this later)!\n\n#### lib/awesome-stack.ts\n```ts\nimport * as cloudfront from \"@aws-cdk/aws-cloudfront\";\nimport * as cloudfrontOrigins from \"@aws-cdk/aws-cloudfront-origins\";\nimport * as s3 from \"@aws-cdk/aws-s3\";\nimport * as s3Deployment from \"@aws-cdk/aws-s3-deployment\";\nimport * as cdk from \"@aws-cdk/core\";\n\n/**\n * The CloudFormation stack holding all our resources\n */\nexport default class AwesomeStack extends cdk.Stack {\n  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {\n    super(scope, id, props);\n\n    /**\n     * The S3 Bucket hosting our build\n     */\n    const bucket = new s3.Bucket(this, \"Bucket\", {\n      autoDeleteObjects: true,\n      removalPolicy: cdk.RemovalPolicy.DESTROY,\n    });\n\n    /**\n     * The CloudFront distribution caching and proxying our requests to our bucket\n     */\n    const distribution = new cloudfront.Distribution(this, \"Distribution\", {\n      defaultBehavior: {\n        origin: new cloudfrontOrigins.S3Origin(bucket),\n        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,\n      },\n      defaultRootObject: \"index.html\",\n    });\n\n    /**\n     * Output the distribution's url so we can pass it to external systems\n     */\n    new cdk.CfnOutput(this, \"DeploymentUrl\", {\n      value: \"https://\" + distribution.distributionDomainName\n    });\n\n    /**\n     * Upload our build to the bucket and invalidate the distribution's cache\n     */\n    new s3Deployment.BucketDeployment(this, \"BucketDeployment\", {\n      destinationBucket: bucket,\n      distribution,\n      distributionPaths: [\"/\", \"/index.html\"],\n      sources: [s3Deployment.Source.asset('./website')],\n    });\n  }\n}\n\n```\n\nThe stack class is the unit where we declare all the resources that will be created during the deployment. To create a stack you need to extend the `cdk.Stack` class.\n\nThe `removalPolicy` is often available on stateful resources such as S3 bucket or RDS databases. The default is to keep you from making a mistake so data are kept after destroying an environment. In our case we want to clean-up everything so we have to tell AWS to explicitly `DESTROY` our bucket.\n\nAt the beginning of this post I talked about CDK providing sensible defaults. The general rule is that every resource you create is private by default. It means that if you want to expose them you have to do it explicitly. Security wise this is a very good point. The objects in our bucket here are not publicly accessible, only CloudFront can read them.\n\n`cdk.CfnOutput` is a construct that allow you to export arbitrary values at the end of the deployment. It will be very useful to pass the deployed URL to Github Deployment API.\n\nThis is it, this is our infrastructure, in 30 lines of actual code. Not bad!\n\n### CLI\n\nNow that the infrastructure is sorted out, let's see how we interact with the CDK CLI. I defined 3 main commands as scripts in the `package.json` file.\n\n#### yarn bootstrap\n```bash\nCDK_NEW_BOOTSTRAP=1 cdk bootstrap --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess\n```\n\nDeploying AWS CDK apps into an AWS environment (a combination of an AWS account and region) may require that you provision resources the AWS CDK needs to perform the deployment. These resources include an Amazon S3 bucket for storing files and IAM roles that grant permissions needed to perform deployments. The process of provisioning these initial resources is called bootstrapping. More infos [here](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html).\n\nBasically you need to call this command **manually** only once per account/region you want to deploy to.\n\n#### yarn deploy\n```bash\ncdk deploy \"AwesomeStack-${STAGE}\" --require-approval never --outputs-file cdk.out.json\n```\n\nThis command reads from the `cdk.json` config file and triggers the deployment of the resources. Here I specified that I don't want it to be interactive (I accept all the resources/roles creation). Also I save all the values exported with `cdk.CfnOutput` to a json file.\n\nIf you run the command multiple times, CloudFormation will diff your changes automatically and only update the resources accordingly!\n\nYou can notice that in order to run this command, I expect to have the STAGE environment variable set. Also, I'm only targeting a single stack but `cdk deploy` also accepts globs if you have multiple stacks to deploy.\n\n#### yarn destroy\n```bash\ncdk destroy \"AwesomeStack-${STAGE}\" --force\n```\n\nHere is the destruction of the stack on AWS, the `--force` option is used to be non interactive as we'll call these commands inside our Github Actions.\n\nThis concludes the infrastructure part of this post, I hope you're still there for the automation!\n\n# 🤖 Github automation\n\n## Github Deployments API\n\nGithub has dedicated UIs and status integration when deploying code in a repository. It's pretty deep in their [REST API](https://docs.github.com/en/rest/reference/repos#deployments) and I didn't notice a lot of adoption for this feature.\n\nI see a lot of benefits using it :\n- Having nice feedbacks directly in the Pull Request about the status of the current deployment.\n- Seeing all the active deployments from the repository's home page.\n- Automatic communication with third-party services such as [Checkly](https://www.checklyhq.com/docs/cicd/github/) that need infos about the deployments in order to start their own workflow.\n\nGithub as the concept of environment to group deployments together in a sequential way. In our case we will create one environment per Pull Request.\n\n## Github Actions\nGitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub.\n\nOne of the big strength of Github Actions is its ecosystem and marketplace. As someone using a lot of OSS packages on npm I feel like home, there is always a package to achieve what I want!\n\nTo achieve our automation target, we will need 2 actions. One for the deployment of the stack during the lifecycle of the Pull Request, and one for cleaning everything up when the Pull Request is closed.\n\n### Pull Request deploy\n\nThis first action is triggered in two cases :\n- When you add the label `🚀 deploy` on the Pull Request\n- When you open or push to a Pull Request labeled with `🚀 deploy`. The `push` event on a Pull Request is part of the `synchronized` event.\n\nThe first deployment will create all the resources for the environment and is often the slowest. Subsequent deployments will be way faster as CloudFormation diff the changes and only update the required resources.\n\nIn order to have **one isolated environment per Pull Request** we need to **derive the STAGE environment variable from the Pull Request number and the branch name** so the resulting stack name is unique to our Pull Request on AWS. I made a special step for this purpose.\n\nThere is a last npm script that I didn't mention earlier that eases the passage of the deployment's url from the step `deploy the stack on AWS` to the step `update the github deployment status`, the `postdeploy` script :\n```bash\nnode --eval \"console.log('::set-output name=env_url::' + require('./cdk.out.json')['AwesomeStack-${STAGE}'].DeploymentUrl)\"\n```\nIt reads the `DeploymentUrl` exported by the `cdk.CfnOutput` in the `cdk.out.json` after the deployment is over. Then it set the value as a [step's output](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idoutputs) in order to be accessible by the next steps.\nThe `postdeploy` script is automatically called after the `deploy` script thanks to the [pre and post scripts convention](https://docs.npmjs.com/cli/v7/using-npm/scripts#pre--post-scripts).\n\nNotable actions from the community which were very convenient in this workflow :\n- `rlespinasse/github-slug-action` exposes the slug/short values of some GitHub environment variables inside the workflow. It allowed me to directly get a slug version of the branch name to build the STAGE environment variable. (refs/heads/feat/new_feature -\u003e feat-new-feature)\n- `aws-actions/configure-aws-credentials` configures AWS credential and region environment variables for use in other GitHub Actions. The environment variables will be detected by all AWS tools to determine the credentials and region to use for AWS API calls.\n- `bobheadxi/deployments` is abstracting over the Github Deployments API and makes it a breeze to create/update/delete Github deployments.\n\n```yaml\nname: \"Pull Request deploy\"\n\non:\n  pull_request:\n    types: [labeled, opened, synchronize]\n\njobs:\n  deploy:\n    if: |\n      (github.event.action == 'labeled' \u0026\u0026 github.event.label.name == ':rocket: deploy') ||\n      (github.event.action != 'labeled' \u0026\u0026 contains(github.event.pull_request.labels.*.name, ':rocket: deploy'))\n    runs-on: ubuntu-latest\n    steps:\n      - name: inject slug/short variables\n        uses: rlespinasse/github-slug-action@v3.x\n\n      - name: set STAGE variable in environment for next steps\n        run: echo \"STAGE=pr-${{ github.event.number }}-${{ env.GITHUB_HEAD_REF_SLUG }}\" \u003e\u003e $GITHUB_ENV\n\n      - name: create a github deployment\n        uses: bobheadxi/deployments@v0.5.2\n        id: deployment\n        with:\n          step: start\n          token: ${{ secrets.GITHUB_TOKEN }}\n          env: ${{ env.STAGE }}\n          ref: ${{ github.head_ref }}\n          no_override: false\n          transient: true\n\n      - name: checkout the files\n        uses: actions/checkout@v2\n\n      - name: install node dependencies\n        uses: bahmutov/npm-install@v1\n\n      - name: configure AWS credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: eu-west-3\n\n      - name: deploy the stack on AWS\n        id: cdk_deploy\n        run: yarn deploy\n\n      - name: update the github deployment status\n        uses: bobheadxi/deployments@v0.5.2\n        if: always()\n        with:\n          step: finish\n          token: ${{ secrets.GITHUB_TOKEN }}\n          status: ${{ job.status }}\n          deployment_id: ${{ steps.deployment.outputs.deployment_id }}\n          env_url: ${{ steps.cdk_deploy.outputs.env_url }}\n```\n\n### Pull Request clean-up\n\nThe clean-up action is triggered in two cases :\n- When you remove the label `🚀 deploy` from the Pull Request\n- When you close the Pull Request labeled with `🚀 deploy`. The `closed` event is also emitted after the Pull Request is merged as it's automatically closed by Github.\n\nThis actions ensures that your preview environment is destroyed on AWS as soon as you don't need it anymore. It also delete the associated Github deployments so you don't see them in the UI anymore.\n\nNotable action from the community which was very convenient in this workflow :\n- `strumwolf/delete-deployment-environment` finds and delete all deployments as well as the GitHub environment they are deployed to. We can't delete a Github environment if it contains any non `inactive` deployment. This action marks all the deployments as inactive and delete the whole chain.\n\n```yaml\nname: \"Pull Request clean-up\"\n\non:\n  pull_request:\n    types: [unlabeled, closed]\n\njobs:\n  clean-up:\n    if: |\n      (github.event.action == 'unlabeled' \u0026\u0026 github.event.label.name == ':rocket: deploy') ||\n      (github.event.action == 'closed' \u0026\u0026 contains(github.event.pull_request.labels.*.name, ':rocket: deploy'))\n    runs-on: ubuntu-latest\n    steps:\n      - name: inject slug/short variables\n        uses: rlespinasse/github-slug-action@v3.x\n\n      - name: set STAGE variable in environment for next steps\n        run: echo \"STAGE=pr-${{ github.event.number }}-${{ env.GITHUB_HEAD_REF_SLUG }}\" \u003e\u003e $GITHUB_ENV\n\n      - name: checkout the files\n        uses: actions/checkout@v2\n\n      # there is a bug with the actions/cache used in bahmutov/npm-install@v1 on \"closed\" event\n      # more infos here : https://github.com/actions/cache/issues/478\n      - name: install node dependencies\n        run: yarn --frozen-lockfile\n\n      - name: configure AWS credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: eu-west-3\n\n      - name: destroy the stack on AWS\n        run: yarn destroy\n\n      - name: delete the github deployments and the corresponding environment\n        uses: strumwolf/delete-deployment-environment@v1.1.0\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          environment: ${{ env.STAGE }}\n```\n\n# 📸 Workflow in pictures\n\n`🚀 deploy` added on the Pull Request, imminent take-off\n![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l514vv7lgmdxrrok3jwj.png)\n\n Oops, something went wrong, let's fix this\n![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ax0utzl04vwoxnycjf3i.png)\n\nNew push, here we go again, the previous deployment is marked as Destroyed\n![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmq25qq7abyw6sy8uyy1.png)\n\nYAY it worked!\n![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/63sk4vwifz061ucn6abu.png)\n\nClicking on the `View deployment` link led me here, interesting\n![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t5ouao8108d41i5sxqwa.png)\n\nYou can notice the new Environments block in the bottom right corner\n![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4h91bs5roor9ot92hvw7.png)\n\n# 🌇 Conclusion\n\nYou now have all the building blocks to make **your own workflow**, how cool is that?! The cloud is the limit now!\n\nHere are some ideas (and maybe future posts) :\n- Using your own custom domain to have pretty preview urls like `https://pr-1-my-awesome-branch.jgoux.dev` using AWS Route53.\n- Going full stack and serverless, adding lambdas and a RDS database to the stack with the same previews and isolation guarantees!\n- Generating infrastructure dynamically based on your application's code\n- Strategies to speed up the deployment with CDK and optimizing costs by splitting your stack into an always up and shared `StableStack` and temporaries `DynamicStack-${STAGE}`.\n\nI want to use this post to thanks all the people that took the time to answer my *numerous* questions across Twitter, Github issues, Discord, Zoom, Slack and more recently the [AWS CDK Slack community](https://cdk.dev).\n\nI can't list everyone, I ask too many questions 😂, but I want to give a special thanks to [Thorsten Hoeger](https://twitter.com/hoegertn) and [Kenneth Winner](https://twitter.com/KenWin0x539) for proofreading this post. I'm very grateful to be in communities of such talented, opened and nice people.\n\nThis is my first post ever, over the years I greatly benefited from OSS and I want to give back to the community. ❤️\n\nDon't hesitate to ping me on [Twitter](https://twitter.com/_jgx_) 🐦 if you want to chat about anything, my DMs are always open!","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjgoux%2Fpreview-environments-per-pull-request-using-aws-cdk-and-github-actions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjgoux%2Fpreview-environments-per-pull-request-using-aws-cdk-and-github-actions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjgoux%2Fpreview-environments-per-pull-request-using-aws-cdk-and-github-actions/lists"}