There is a question this course has not asked yet, and it is worth asking bluntly: where is MercadoFresco's code? The honest answer is that it is on Luis's laptop, in ~/proyectos/tienda, with a git init from fourteen months ago and 1,847 commits nobody else has ever seen. There is a compressed copy on a USB stick from September. Marta has a folder called tienda-v2-FINAL and does not know whether it is older or newer. And when Luis deploys, he scps the whole folder to an instance of the ASG asg-mercadofresco-tienda and restarts the service by hand.

Everything built over seven modules rests on source code that lives on a single hard disk with no reliable backup. This module fixes that, and it starts at the beginning: where the code lives and how changes are governed.

Cost warning. CodeCommit charges 1 USD per active user and month from the sixth onwards, with 50 GB and 10,000 Git requests included: for MercadoFresco's team of three, zero. CodeConnections connections to GitHub or GitLab are free too; you pay for the downstream usage, which we will see in 08-02 and 08-04. All the data and credentials in this lesson are fictitious.

Contents

  1. The starting point: a repository on a laptop
  2. What a managed Git repository gives you
  3. Where CodeCommit stands, without the varnish
  4. Comparison: CodeCommit, GitHub, GitLab and Bitbucket
  5. CodeConnections: the bridge between AWS and your Git provider
  6. MercadoFresco's repositories
  7. Authentication: HTTPS, SSH, the CLI credential helper and roles
  8. IAM policies: who can push to main
  9. Branching strategy: trunk-based versus GitFlow
  10. MercadoFresco's branching flow
  11. Pull requests: review, approval and merge
  12. Approval rules and protecting main
  13. Conventions: commits, semantic versioning and tags
  14. The .gitignore and why you never commit a .env
  15. Notifications and triggers
  16. Migrating Luis's repository
  17. Cost and cleanup
  18. Common mistakes and tips
  19. Exercises
  20. Conclusion

The starting point: a repository on a laptop

Marta drew up a risk inventory before touching anything:

Risk Current situation Consequence
Losing the laptop USB copy from 5 months ago 5 months of work lost
Two people touch the same thing They warn each other on chat Silent overwrite
Knowing what is in production "Whatever I uploaded on Friday" Nobody can reproduce a failure
Going back to the previous version Search the recycle bin Minutes or hours of downtime
Reviewing before it goes in Not done Errors seen in production
Secrets in the code There is a .env with the RDS password Leak when sharing the repository

The first four are solved by any remote repository. The last three also need governance: rules about who can do what and under what conditions a change goes in. That distinction is the whole lesson: hosting the code is the easy part.

What a managed Git repository gives you

Git is distributed and nothing forces you to have a server, but every team designates one copy as the reference, and that copy needs to be available, backed up and to control who writes to it.

Aspect Your own server (EC2 + Gitea/GitLab CE) Managed
Durability That of EBS; you do the backups Replicated across AZs, with no action from you
Availability One instance = a single point of failure The provider's service level agreement
Encryption at rest You configure it Automatic; in CodeCommit, with KMS
Identity System users or your own LDAP IAM (CodeCommit) or OIDC/SAML
Patching and CVEs Yours The provider's
Real cost Instance + EBS + your time Per user or included
Auditing Server logs, if you keep them CloudTrail or the provider's log

For three people, running a Git server is time that does not go into the product: MercadoFresco does not set one up. CodeCommit's encryption at rest deserves a paragraph, because it connects with module 4: it always encrypts, with no option to turn it off, and it can do so with a customer key. Here that means reusing alias/mercadofresco-datos, the same key that already encrypts Aurora and the buckets: one key, one policy, one rotation and a single place to revoke access. That is the concrete advantage of the integration: there is no second encryption system to govern.

Where CodeCommit stands, without the varnish

Since July 2024, AWS has not accepted new customers on CodeCommit. If your account was already using it before, it keeps working with support and security fixes. If it is new, the console will not let you create repositories. No end-of-life date has been announced, but there are no new features either: the official recommendation for new projects is an external Git provider connected with CodeConnections.

Three practical consequences: you are going to run into it in companies that adopted it between 2015 and 2024 with hundreds of repositories, so knowing how to operate it is useful; do not choose it for a new project, not even if your old account lets you, because you would be building on a service with no evolution; and what matters here is not the service, it is the concepts — branches, pull requests, approval rules, branch protection, commit and versioning conventions are identical in GitHub, GitLab or Bitbucket, and they are what 08-02, 08-03 and 08-04 will consume.

Marta makes the decision you would make today: the code goes to GitHub, and AWS connects with CodeConnections. The lesson goes on explaining CodeCommit because you will run into it and because its permission model — pure IAM — is didactically very clear.

Comparison: CodeCommit, GitHub, GitLab and Bitbucket

Criterion CodeCommit GitHub GitLab Bitbucket
New customers Not accepted Yes Yes Yes
Identity Native IAM Its own; paid SSO Its own; SSO Atlassian
Code review Functional but poor The best on the market Very complete Adequate
Own CI/CD No (uses CodeBuild) GitHub Actions GitLab CI Bitbucket Pipelines
AWS integration Fully native CodeConnections + OIDC CodeConnections + OIDC CodeConnections
Self-hostable No Enterprise Server Yes, self-managed Data Center
Ecosystem None Enormous Broad and integrated Tied to Jira
Code residency Your AWS account and region GitHub's servers GitLab or your server Atlassian
Cost 3 / 30 people 0 / ~25 USD month 0 / ~120 USD month 0 / ~870 USD month 0 / ~90 USD month

Choice criteria, in the order in which they must be applied:

  1. Is there a legal or contractual requirement that the code must not leave your account or the EU? If it is non-negotiable, your options are CodeCommit (only if you already had it) or self-hosted GitLab. It is the only criterion that overrides all the others.
  2. Where is your team already? Moving people who have spent years on GitHub has a daily friction cost that you pay forever. This criterion decides most cases.
  3. Will you use the provider's CI/CD or the AWS one? Actions and GitLab CI are excellent and deploy to AWS with OIDC and no keys; if you prefer building and deploying inside AWS — with its roles, its VPCs and its bill — CodeConnections gives you the best of both.
  4. How much ecosystem do you need? In dependency scanning, issues and boards, GitHub and GitLab win without argument.

MercadoFresco: the team is already on GitHub, there is no restriction on the source code (customer data is restricted to eu-west-1, but that is another matter) and they want building and deployment in AWS to reuse roles and alarms. GitHub + CodeConnections + CodeBuild/CodeDeploy/CodePipeline.

CodeConnections: the bridge between AWS and your Git provider

CodeConnections — until 2023, AWS CodeStar Connections; you will see both names in the documentation and in old ARNs — is an AWS resource representing an OAuth authorisation between your account and a GitHub, GitLab or Bitbucket organisation. Once created, CodePipeline or CodeBuild read the repository and receive its events without you storing any token anywhere.

It is created in two steps, and the second one is necessarily manual:

# Step 1: create the connection. It is born PENDING and is not usable yet.
aws codeconnections create-connection \
  --provider-type GitHub \
  --connection-name conn-mercadofresco-github \
  --tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
         Key=Componente,Value=cicd Key=Propietario,Value=marta Key=CentroCoste,Value=plataforma \
  --profile mercadofresco-dev --region eu-west-1

Step 2, in the console and only in the console. Developer Tools → Settings → Connections, select the pending connection and click "Update pending connection". GitHub's OAuth flow opens, the AWS Connector for GitHub app is installed in the organisation and the authorised repositories are chosen. This step cannot be automated by design: it is a human authorisation in an external provider, not a call to the AWS API. If a CloudFormation template "creates ready-made connections", it creates the resource and somebody authorises it by hand once.

Check with aws codeconnections list-connections that the status is AVAILABLE before moving on: a connection in PENDING is the number one cause of a pipeline's source action failing with an uninformative permissions error, and we will come back to it in 08-04.

Grant access only to the repositories you need. GitHub offers to give access to the whole organisation; choose "Only select repositories" and tick MercadoFresco's two. It is the least privilege of 04-01 applied outside AWS.

MercadoFresco's repositories

Repository Contents Who touches it Cadence
mercadofresco-tienda Web, API, queue consumers, Lambda code, tests, buildspec.yml, appspec.yml Luis Several times a day
mercadofresco-infra Infrastructure definition, operations scripts, runbooks Marta Weekly

Separating them makes it possible to deploy the application twenty times a day without dragging infrastructure changes along, and to subject a network change to stricter review. We will fill in the contents of mercadofresco-infra in module 9: today only the repository exists.

# In CodeCommit
aws codecommit create-repository \
  --repository-name mercadofresco-tienda \
  --repository-description "MercadoFresco web application, API and consumers" \
  --kms-key-id alias/mercadofresco-datos \
  --tags Proyecto=mercadofresco,Entorno=produccion,Componente=codigo,Propietario=luis,CentroCoste=plataforma \
  --profile mercadofresco-dev --region eu-west-1

# In GitHub (the chosen option)
gh repo create mercadofresco/mercadofresco-tienda --private
gh repo create mercadofresco/mercadofresco-infra --private

--kms-key-id is where encryption with your own key materialises: if one day you revoke that key, the repository stops being readable, which is exactly what you want in the face of a leak. And the repository is always private, with no "public for now, we will change it later": the crawlers that look for credentials in public repositories take minutes, not days.

Authentication: HTTPS, SSH, the CLI credential helper and roles

Here is CodeCommit's most interesting conceptual difference: there are no Git users, there are IAM identities. There is no "user luis" on the server with his password; there is his IAM user or role, and permission to push is a policy. Removing somebody's access means removing IAM, not remembering a second system.

Method How it works When Drawback
IAM HTTPS credentials Username/password generated in IAM Laptop with an IAM user Long-lived material
SSH keys Public key on the IAM user Laptop, if you prefer SSH The same
Credential helper Git asks the CLI for a SigV4 signature Recommended: SSO, MFA, roles Requires the CLI
IAM role The machine assumes a role, no credentials CodeBuild, EC2, Lambda Machines only

Use the credential helper. Its virtue is that it stores nothing: every time Git needs to authenticate it invokes aws codecommit credential-helper, which signs with the profile's current credentials. If they come from IAM Identity Center and expire after eight hours, access expires with them. It is the temporary credentials principle of 04-01 applied to the repository.

git config --global credential.helper '!aws --profile mercadofresco-dev codecommit credential-helper $@'
git config --global credential.UseHttpPath true

git clone https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/mercadofresco-tienda

credential.UseHttpPath true is mandatory and constantly forgotten: without it, Git does not send the repository path, the signature is computed over the wrong resource and you get a 403 that explains nothing.

In GitHub, the modern equivalent for machines is OIDC, not an access key. Instead of storing an AWS_ACCESS_KEY_ID as a repository secret, trust is established between GitHub's OIDC provider and a role in your account, with a condition on the specific organisation and repository:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
      "StringLike": { "token.actions.githubusercontent.com:sub": "repo:mercadofresco/mercadofresco-tienda:*" }
    }
  }]
}

The sub condition stops any repository in the world from assuming your role. A StringLike with repo:* is a serious security hole that has been seen in production more than once.

IAM policies: who can push to main

In CodeCommit, branch protection is expressed as an IAM policy, through the codecommit:References condition. This policy denies pushing directly to main:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenegarEscrituraDirectaEnMain",
    "Effect": "Deny",
    "Action": ["codecommit:GitPush", "codecommit:DeleteBranch",
               "codecommit:PutFile", "codecommit:DeleteFile"],
    "Resource": "arn:aws:codecommit:eu-west-1:111122223333:mercadofresco-tienda",
    "Condition": {
      "StringEqualsIfExists": { "codecommit:References": ["refs/heads/main"] },
      "Null": { "codecommit:References": "false" }
    }
  }]
}

Three details that are often copied wrongly. StringEqualsIfExists limits the denial to what affects refs/heads/main: a push to funcionalidad/franja-horaria does not match and is allowed. Null: {"codecommit:References": "false"} means "apply only if the key is present"; without it you would also deny operations with no branch reference, blocking things you did not intend. And PutFile and DeleteFile close the web console's door, which allows editing without going through Git and is the most forgotten hole — by contrast, do not deny the MergeBranchesBy* actions: they are the ones a PR merge executes, and blocking them prevents even approved PRs from being closed.

Deny always beats Allow, as we saw in 04-01: this is effective even if Luis has AWSCodeCommitPowerUser attached. In GitHub the mechanism changes — branch protection rules or rulesets — but the intent is identical.

Branching strategy: trunk-based versus GitFlow

Aspect Trunk-based GitFlow
Long-lived branches One: main main + develop + release/* + hotfix/*
Life of a working branch Hours or 1-2 days Days or weeks
Merge conflicts Small and frequent Large and painful
Mental complexity Low High; it has to be documented
Continuous delivery It is its premise Badly: the release branch gets in the way
Half-finished features Hidden behind flags Isolated in their branch
Simultaneous versions Hard It is what it solves well

GitFlow was designed in 2010 for software that is published in versions: an installer, a library, a product the customer updates when they want. There it makes sense to maintain 1.4 while you develop 1.5. Its own author later published a note saying that if you deliver continuously — a website, a SaaS — you probably do not need it.

MercadoFresco is a web shop: there is only one version, the one in production right now. A release/* branch adds nothing, and a develop that lives for weeks guarantees painful merges between three people who touch the same files.

Marta's decision: trunk-based with nuances. main always deployable, short feature branches, and a desarrollo branch that exists for one temporary reason: until the 08-04 pipeline is mature and the tests inspire confidence, she wants a place where changes coexist for a day before reaching main. It is a conscious compromise with a declared expiry date: scaffolding, not a cathedral.

MercadoFresco's branching flow

gitGraph
    commit id: "v1.4.0"
    branch desarrollo
    checkout desarrollo
    commit id: "sync"
    branch funcionalidad/franja-horaria
    checkout funcionalidad/franja-horaria
    commit id: "order model"
    commit id: "API + tests"
    checkout desarrollo
    merge funcionalidad/franja-horaria id: "PR 42"
    commit id: "dev smoke OK"
    checkout main
    merge desarrollo tag: "v1.5.0"
    branch correccion/iva-canarias
    checkout correccion/iva-canarias
    commit id: "fix VAT"
    checkout main
    merge correccion/iva-canarias tag: "v1.5.1"
    checkout desarrollo
    merge main id: "sync fix"

The written rules, which live in CONTRIBUTING.md:

Branch From To Life Who merges
main Permanent Nobody directly
desarrollo main main via PR Permanent Marta, after review
funcionalidad/<n> desarrollo desarrollo via PR Max. 3 days Author, 1 approval
correccion/<n> main main and desarrollo Hours Author, 1 approval
experimento/<n> Anywhere Deleted Days Not merged

The three-day limit is not bureaucracy: a branch that lives for two weeks diverges so much that merging it stops being a merge and becomes a reintegration; if a change does not fit in three days, split it. Urgent fixes come off main, not off desarrollo, which would drag half-finished changes into production; and they have to be merged also into desarrollo, or the failure will reappear in the next delivery. And branches are deleted on merge: a repository with 140 dead branches is a repository where nobody finds anything.

Pull requests: review, approval and merge

A pull request is the proposal to merge one branch into another, with a space to discuss it before it happens. It is what turns "Luis uploaded something" into "the team accepted a change".

aws codecommit create-pull-request \
  --title "Add delivery time slot to the order" \
  --description "$(cat <<'EOF'
## What changes
Adds the `franja_entrega` field (manana|tarde|24h) to the order and propagates it to the
`PedidoConfirmado` event as an optional attribute.

## Why
Sara's request: 31 % of delivery incidents are caused by the customer being out.

## How to test it
1. POST /pedidos with "franja_entrega": "tarde"
2. Check the message in cola-mercadofresco-pedidos
3. Check that an order WITHOUT the field still works (backwards compatibility)

## Risks
Schema change in Aurora: NULLABLE column, no default value, no new index.
Old consumers ignore the attribute. See 08-05.
EOF
)" \
  --targets repositoryName=mercadofresco-tienda,sourceReference=funcionalidad/franja-horaria,destinationReference=desarrollo \
  --profile mercadofresco-dev --region eu-west-1

That description is a real template and is worth copying. A PR with no description forces the reviewer to reconstruct the intent by reading the diff, the slowest and least reliable way to understand a change. The four sections are answered in three minutes and save the reviewer twenty.

Comments can be general or anchored to a specific line of the diff with post-comment-for-pull-request and its --location filePath=app/pedidos.py,filePosition=147 parameter, which is where useful review happens: on the line, not in a separate email.

The three merge strategies, identical on any platform:

Strategy What it does Resulting history When
Fast-forward Moves the pointer, no new commit Linear, keeps every commit Branch of 1 commit already rebased
Squash Flattens the branch into one commit Linear and clean: 1 PR = 1 commit Default for features
Three-way Merge commit with two parents Branched, keeps the detail Between long-lived branches

MercadoFresco uses squash for feature branches and three-way for desarrollomain. The reasoning: the fifteen commits of "wip", "fix the test" and "now it works" add nothing six months later, and a git log of main where each line is a complete, described change is an excellent diagnostic tool during an incident; when moving desarrollo into main, on the other hand, you want to keep the individual commits so you can revert just one. One consequence of squashing that surprises people the first time: the resulting commit is new and shares no identifier with the branch's commits, so the branch looks "unmerged" to Git even though its content is inside. Delete it anyway.

aws codecommit merge-pull-request-by-squash \
  --pull-request-id 42 --repository-name mercadofresco-tienda \
  --commit-message "Add delivery time slot to the order (#42)" \
  --profile mercadofresco-dev --region eu-west-1

Approval rules and protecting main

A PR that anybody can merge without anybody looking at it is a formality. What turns it into a real control is an approval rule template: a policy applied automatically to PRs meeting a criterion, which demands approvals from a given set of people.

aws codecommit create-approval-rule-template \
  --approval-rule-template-name plantilla-mf-main-requiere-aprobacion \
  --approval-rule-template-description "Every PR into main needs 1 approval from the team" \
  --approval-rule-template-content '{
    "Version": "2018-11-08",
    "DestinationReferences": ["refs/heads/main"],
    "Statements": [{
      "Type": "Approvers",
      "NumberOfApprovalsNeeded": 1,
      "ApprovalPoolMembers": ["arn:aws:sts::111122223333:assumed-role/rol-mercadofresco-desarrollo/*"]
    }]
  }' \
  --profile mercadofresco-dev --region eu-west-1

# The template does nothing until it is associated with a repository
aws codecommit associate-approval-rule-template-with-repository \
  --approval-rule-template-name plantilla-mf-main-requiere-aprobacion \
  --repository-name mercadofresco-tienda \
  --profile mercadofresco-dev --region eu-west-1

DestinationReferences limits the rule to PRs into main: agility day to day, rigour at the door of production. ApprovalPoolMembers accepts the assumed-role/<role>/* pattern, which is the right one when the team signs in with SSO and identities are role sessions rather than IAM users. NumberOfApprovalsNeeded: 1 is what makes sense for three people; with two, the process would jam as soon as somebody went on holiday. And the author cannot approve their own PR: CodeCommit prevents it natively and in GitHub you have to enable it, but it is half the value of the mechanism.

The second condition — that the build is green — cannot be expressed here, because these rules only count human approvals. There are two paths: CodeBuild approving the PR programmatically when it finishes successfully, or the check living in the Git provider as a required status check. This piece is closed in 08-04, when the pipeline exists and has something to say about the quality of the change.

Conventions: commits, semantic versioning and tags

Conventions look like cosmetics until the day a pipeline reads them and makes decisions with them.

The format is type(scope): description in the imperativefeat(pedidos): add delivery time slot, fix(pagos): avoid double charge when the gateway returns 504, chore(deps): bump boto3:

Type Meaning Effect on the version
feat New feature MINOR
fix, perf Fix or improvement PATCH
refactor, docs, test, chore No functional change None
Any with BREAKING CHANGE: in the body Incompatible change MAJOR

Semantic versioning (MAJOR.MINOR.PATCH) is the other side of the same coin: v1.5.0 versus v1.4.3 tells you what to expect. In a shop whose only client is your own front end, MAJOR matters little; as soon as you publish an API consumed by the warehouse ERP, it matters a lot.

git tag -a v1.5.0 -m "Delivery time slot + catalogue cache"
git push origin v1.5.0
git log --oneline v1.4.3..v1.5.0

The operational value is that the deployed artefact carries that number. When mercadofresco-alb-latencia-alta fires at 19:20 on a Friday, the first question will be "what version is in production?" and the second "what went in since the previous one?". With tags you answer in ten seconds; without them, by reconstructing dates and hoping.

The .gitignore and why you never commit a .env

A correct .gitignore is not hygiene: it is leak control.

# Environment and secrets - NEVER in the repository
.env
.env.*
!.env.example
*.pem
*.key
credenciales*.json

# Python and build artefacts
__pycache__/
.venv/
.pytest_cache/
.coverage
dist/
node_modules/

# Local data: may contain personal data
*.csv
*.sql
volcado_aurora_*.dump
!datos/ejemplo_anonimo.csv

The !.env.example line un-excludes a template of variables with no values, which does need to be there so that somebody new knows what configuration is required. And the last lines stop an Aurora dump with real orders from ending up in the repository: that is a data protection incident, not an oversight.

Why the .env is the worst of all. On Luis's laptop there is one with the mfadmin password, the payment gateway key and the ERP token. If it goes into the repository even once:

it stays in the history forever — deleting it in the next commit does not remove it, git show recovers it, and really cleaning it requires rewriting history and force-pushing, breaking every clone —, it is replicated in every clone, CI cache and provider copy, and the only valid response is to rotate the credentials: if the secret was exposed it is compromised, however long the exposure lasted.

We built the solution in 04-03 and here we just have to use it: passwords in mercadofresco/produccion/rds/mfadmin with automatic rotation every 30 days, and non-sensitive configuration in Parameter Store under /mercadofresco/produccion/. The application reads them at start-up with rol-mercadofresco-tienda. There is not a single secret value in the repository, only the name of the secret, which is not sensitive. And the rotation of 04-03 gains a value here that was not obvious then: it puts an automatic expiry date on the damage of a leak. In 08-02 we will add one more net: a scan that fails the build if it detects something that looks like a credential.

Notifications and triggers

A repository nobody looks at is a repository where PRs wait four days.

Mechanism What it is Typical cases
Triggers Repository configuration: on push, invokes SNS or Lambda Automation tied to the push
Notifications A rule on service events towards SNS or a chat Telling people about PRs and comments
EventBridge Every CodeCommit event reaches the default bus Content-based routing
aws codecommit put-repository-triggers \
  --repository-name mercadofresco-tienda \
  --triggers '[{"name":"aviso-push-main",
    "destinationArn":"arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco",
    "branches":["main"],"events":["updateReference"]}]' \
  --profile mercadofresco-dev --region eu-west-1

And an EventBridge rule that takes advantage of everything from 07-03, with source: ["aws.codecommit"], detail-type: ["CodeCommit Pull Request State Change"] and a filter on detail.event to react to pullRequestCreated. If what you want is to tell people, do not build it with a trigger: that is what notifications are for, with the formatting done and Slack integration without code.

There is an anti-pattern worth naming here: using a trigger to launch the deployment. It is tempting and it works on day one, but there is no build, no tests, no approval, no versioned artefact and no way to retry. It is Luis's scp with more steps. What should react to the push is a pipeline, and that is 08-04.

Migrating Luis's repository

The local repository has 1,847 commits, three branches and a .env tracked since the second commit.

Step 1: inventory. git branch -a, git log --oneline | wc -l and git count-objects -vH to know what is there and how much it weighs.

Step 2: look for secrets in the history before uploading anything. This is not optional.

git log --all --full-history --oneline -- .env
git log --all -p -S "AKIA" --oneline                     # AWS access key
git log --all -p -S "BEGIN RSA PRIVATE KEY" --oneline

Luis finds .env in 312 commits. Two paths: rewrite the history with git filter-repo if the repository will be public or shared with third parties, or upload and rotate if it is private and the team is small. MercadoFresco chooses the second, but rotation is non-negotiable and happens before the push, with aws secretsmanager rotate-secret --secret-id mercadofresco/produccion/rds/mfadmin and regenerating the gateway key and the ERP token in the provider's panel.

Step 3: clean the working tree.

git rm --cached .env                    # stops tracking it, does not delete it from disk
printf '.env\n.env.*\n!.env.example\n' >> .gitignore
git add .gitignore && git commit -m "chore(seguridad): stop tracking .env and add .gitignore"

Steps 4 and 5: remote and upload, with git remote add origin git@github.com:mercadofresco/mercadofresco-tienda.git followed by git push --mirror origin.

--mirror uploads every reference — branches, tags and notes — exactly as they are locally, which is what you want in an initial migration. Two warnings: it is destructive at the destination (it deletes on the remote whatever does not exist locally, so use it only against an empty repository and only the first time), and it is never used again afterwards; day to day is git push origin <branch>. If you prefer fine control, git push origin --all and git push origin --tags.

Step 6: verify with a clean clone, the step people skip and later regret. Clone into /tmp, count the commits — there should be 1,847 —, check branches and tags, and confirm that .env is not there.

Steps 7 and 8: create desarrollo, protect main at the provider (PR mandatory, one approval, no force push), and have Marta and Sara clone. The repository stops living on a laptop the day there are at least two more copies and somebody has checked that they work.

Cost and cleanup

Item Cost at MercadoFresco
CodeCommit, 3 users and 240 MB 0 USD (5 users and 50 GB included)
CodeConnections and GitHub with private repositories 0 USD
Notifications via SNS Cents
Total for governing the code ≈ 0 USD/month

The barrier to adopting this is never economic: it is habit. What costs money starts in 08-02.

# IRREVERSIBLE and with no recycle bin: clone with --mirror to a disk before running it
aws codecommit delete-repository --repository-name pruebas-borrar \
  --profile mercadofresco-dev --region eu-west-1

Common Mistakes and Tips

Choosing CodeCommit for a new project. It does not accept new customers and receives no new features. Learn its concepts, deploy on GitHub or GitLab.

Forgetting credential.UseHttpPath true. It produces a 403 when cloning from CodeCommit that looks like an IAM permissions problem and is not. It is the first place to look.

Leaving the CodeConnections connection in PENDING. The resource exists, the CLI lists it and everything that depends on it fails with a confusing message. Authorising in the browser is manual.

Storing long-lived keys as GitHub secrets. Use OIDC with a sub condition naming your organisation and your repository; a repo:* is a serious hole.

Denying the MergeBranchesBy* actions to "protect main". Nobody can merge, not even an approved PR, and two days later somebody asks for the whole policy to be removed. Protect direct writing, not governed merging.

Feature branches that live for three weeks. Every extra day multiplies the merge cost. If the change is big, split it into backwards-compatible increments. And adopting GitFlow out of habit in a continuous delivery product adds three long-lived branches in exchange for a problem you do not have.

Committing a .env "just for a moment to test". It stays in the history forever and the only correct answer is to rotate. There is no shortcut.

Merging with squash and then trying to revert an intermediate commit. It no longer exists: the whole branch is a single commit. That is the conscious price of squashing.

Tip: write CONTRIBUTING.md on day one, and use .github/pull_request_template.md to preload the four sections. Half a page saves the same five conversations every month, and a reviewer who has context reviews twice as fast.

Tip: enable automatic branch deletion on merge, and tag every version that reaches production without exception: on the day of the incident, the difference between ten seconds and twenty minutes of diagnosis is in that tag.

Exercises

Exercise 1: choosing a provider with criteria

For each scenario, choose between CodeCommit, GitHub, self-hosted GitLab or Bitbucket and justify it in three sentences naming the decisive criterion:

(a) Spanish public-sector organisation, 40 developers. The tender requires the code to reside on infrastructure under their control inside the EU with full access auditing. They have had an AWS account since 2019 with CodeCommit enabled and 60 repositories.

(b) A 6-person startup starting today. They all come from GitHub, they want dependency scanning without having to set it up and they will deploy on AWS with Lambda and S3.

(c) A 25-person team in a company that uses Jira for everything, with quarterly audits that cross-reference issues with commits.

Exercise 2: the policy that blocked the team

Marta applies this to the desarrolladores-mercadofresco group to protect main:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Deny",
    "Action": "codecommit:GitPush",
    "Resource": "arn:aws:codecommit:eu-west-1:111122223333:mercadofresco-tienda",
    "Condition": {
      "StringNotEqualsIfExists": { "codecommit:References": ["refs/heads/main"] }
    }
  }]
}

The next day Luis cannot push to his feature branch, but he can push to main. Answer: (a) what it does exactly and why it produces that effect; (b) write it correctly; (c) what it is missing in order to protect main properly; (d) what check would have caught the mistake before applying it.

Exercise 3: the migration with a surprise

When running step 2, Luis finds: a .env in 312 commits with the mfadmin password and the gateway key; an 18 MB clientes_octubre.csv with 4,200 real names, addresses and phone numbers, added nine months ago; and an experimento/pagos-bizum branch from a year ago that was never merged.

Answer: (a) what you do with each finding and in what order; (b) which one makes rewriting the history stop being optional, and why; (c) the exact command and what you warn the team about first; (d) who inside the company has to be informed about the second finding.

Solutions

Solution 1

(a) CodeCommit, and it is the only scenario in the exercise where it makes sense. The decisive criterion is the first on the list: the legal requirement for residency and control of the code, which overrides the others. The account predates July 2024, so the service is available; the code lives in their AWS account in eu-west-1, encrypted with their KMS key, and CloudTrail gives the auditing the tender asks for. The alternative would be self-hosted GitLab in their VPC, which meets the requirement just as well but forces them to operate and patch the server: with 60 repositories already migrated it is not worth it. What they should do is plan the exit, because they are on a service with no evolution.

(b) GitHub. Decisive criterion: where the team already is, reinforced by the ecosystem. Six people who already know the tool are productive on day one; Dependabot gives dependency scanning with nothing to set up; private repositories are free at that size. To deploy on AWS, OIDC plus CodeConnections covers everything without storing keys, and there is no residency restriction to complicate it.

(c) Bitbucket. Decisive criterion: integration with the rest of the tooling. The native linking between commits, branches and Jira issues is exactly what their quarterly audit consumes; with GitHub they would have to build and maintain that integration. GitLab would be better at CI/CD, but here the saving is in the process, not in the pipeline; and at 25 people Bitbucket is also the cheapest.

Solution 2

(a) The condition is inverted. StringNotEqualsIfExists with refs/heads/main means "deny when the reference is not main". The result is literally the opposite of the one intended: it forbids pushing to any branch except main and leaves main completely open. It is a one-word mistake — Not — with consequences opposite to the intention, and that is why denials with negated conditions have to be read twice, out loud.

(b) The correct one uses StringEqualsIfExists to narrow the denial to main, adds Null so that it does not affect operations with no reference, and broadens the actions:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenegarEscrituraDirectaEnMain",
    "Effect": "Deny",
    "Action": ["codecommit:GitPush", "codecommit:DeleteBranch",
               "codecommit:PutFile", "codecommit:DeleteFile"],
    "Resource": "arn:aws:codecommit:eu-west-1:111122223333:mercadofresco-tienda",
    "Condition": {
      "StringEqualsIfExists": { "codecommit:References": ["refs/heads/main"] },
      "Null": { "codecommit:References": "false" }
    }
  }]
}

(c) DeleteBranch stops main being deleted entirely; PutFile and DeleteFile close editing from the web console, which bypasses Git and is the most forgotten hole. A push --force is already covered because IAM does not distinguish it from a GitPush. But what is missing is what no policy can give: an approval rule associated with the repository requiring at least one approval on PRs into main, plus — when it exists — the check that the build is green. Without that, the policy only forces changes through a PR; it does not force the PR to be good.

(d) The IAM policy simulator from 04-01, or better still: apply it first to a test repository and run the four cases — push to a feature branch, push to main, branch deletion, editing from the console — checking that two pass and two fail. A denial with conditions must never make its debut in the real repository: the failure mode is that it blocks the team or, worse, that it blocks nothing at all and nobody finds out.

Solution 3

(a) The order matters because the first step makes the timing of the others irrelevant. One: immediately rotate the .env secrets — the mfadmin password from Secrets Manager, the gateway key regenerated in the provider's panel — before touching the repository; they have been compromised for fourteen months and deleting them does not un-compromise them. Two: the CSV, decide on the rewrite and upload nothing in the meantime. Three: the experimento/pagos-bizum branch is not migrated; if it has value it is exported with git format-patch and kept outside. Migrating dead branches is dragging noise.

(b) The CSV. The .env is serious but bounded: it is resolved by rotating, because a rotated secret is worth nothing even if it stays in the history. A file with 4,200 real names, addresses and phone numbers cannot be rotated. It is personal data under the GDPR, and the minimisation principle plus the right to erasure make it unacceptable to keep it indefinitely in a code repository, where it will be replicated in every clone, every CI cache and every provider copy. Here rewriting stops being a technical option and becomes an obligation.

(c)

cp -r ~/proyectos/tienda ~/proyectos/tienda-copia-seguridad   # filter-repo is irreversible
cd ~/proyectos/tienda
git filter-repo --path datos/clientes_octubre.csv --invert-paths
git filter-repo --path .env --invert-paths                    # while we are at it

git filter-repo changes the hash of the affected commits and of all their descendants, so you have to warn that any existing clone becomes useless and must be re-cloned: merging an old clone with the rewritten history creates a disaster of duplicated commits. Here the migration has not happened yet and only Luis's clone exists, so the impact is nil — an excellent reason to clean up before uploading. If it were already on the remote, you would also need push --force, deletion of obsolete remote branches and tags, and a request to the provider to purge the unreachable objects.

(d) The data protection officer (DPO or equivalent). Personal data of 4,200 customers outside the authorised systems for nine months is unforeseen processing that has to be documented and assessed. It may not be a breach notifiable to the AEPD — the repository was private and local — but that assessment is not made by the technical team: you provide the complete information (what data, for how long, who had access, what has been done) and whoever holds the responsibility decides. It is the same criterion we applied in 06-03 with the aurora-mf-pruebas-luis clone.

Conclusion

MercadoFresco's code has left Luis's laptop. It is no longer a git init with a USB copy from September: it is a remote repository, replicated, encrypted, with IAM permissions instead of system users and with at least three live copies across the team. The first four risks in Marta's inventory are closed by the simple fact of having uploaded the code.

But the important part of this lesson is not the hosting, it is the governance. You know that CodeCommit no longer accepts new customers and why that does not invalidate learning it — you are going to run into it —, and that the current answer is GitHub or GitLab connected with CodeConnections, with its manual browser authorisation that breaks so many source actions when it is forgotten. You have the choice criteria in order: first the legal requirement if there is one, then where your team already is, then where you want the CI/CD to live, and last the ecosystem. You know how to authenticate with the credential helper — with credential.UseHttpPath true, which avoids the mysterious 403 — and with OIDC instead of long-lived keys for machines. And you know how to write a policy that protects main without blocking the team, and why the Null condition always accompanies StringEqualsIfExists.

You have chosen trunk-based with a desarrollo branch declared as temporary scaffolding, because a web shop has only one version — the one in production right now — and GitFlow solves a problem MercadoFresco does not have. You know what a PR that is worth something looks like — what, why, how to test it, risks —, when to use squash and when three-way, and how an approval rule template turns review into a real control thanks to the fact that the author cannot approve themselves. You have the conventions a pipeline will read later on: conventional commits, semantic versioning and tags that answer "what is in production?" in ten seconds. And you have the rule that admits no exceptions: the .env never gets committed, because it stays in the history forever and the answer is to rotate.

Now there is a repository with rules. What there is not is any guarantee that what goes into it works. Approving the PR means that a person read the diff on a Tuesday afternoon, which is far more than nothing and considerably less than enough. Nobody has run the tests: Luis runs them on his laptop "nearly always", and the last time one failed he decided it was something to do with his environment. Nobody has checked that the dependencies have no known vulnerabilities, nor that a key has not slipped into the code, nor that the project even starts on a clean machine. The phrase "it works on my laptop" is still an admissible argument at MercadoFresco, and there is still no artefact of any kind: what gets deployed is a folder copied with scp.

In 08-02, "AWS CodeBuild", that comes to an end. We will see what continuous integration is and why it turns "it works on my laptop" into a phrase with no value; the anatomy of a build project with its environment, its service role, its cache and its artefacts; a complete, commented buildspec.yml with the install, pre_build, build and post_build phases; how to inject secrets from Parameter Store and Secrets Manager without them appearing in the logs; unit tests with pytest and coverage reports as a quality gate rather than a report nobody reads; dependency scanning and leaked-credential scanning; the building of the package that will go to the mercadofresco-artefactos bucket; and how to run the build inside vpc-mercadofresco to test against the aurora-mf-pruebas-luis clone, with the cost warning that comes with it.

© Copyright 2026. All rights reserved