Firebase Get Git Commit Hash of Latest Deployment
There’s a question often asked within development teams – what branch and/or commit ID is deployed on production (or staging) environment ? Firebase doesn’t let us answer that in any built-in or straightforward way. In some cases it might be a requirement to answer this question, primarily for services like firebase functions and firebase hosting.
We can always get the last deployment time and version identifier of firebase functions or go through the release history of firebase hosting in firebase console (project’s hosting section) but that won’t provide any git related information (commit hashes, branch names, etc.) because the firebase deployment processes has got nothing to do with the version control in use.
There may be a few ways to solve for the question given above, but my solution is to use a combination of:
- Environment configuration with
functions.config
to store the latest git branch and commit from whatever environment the deployment happens, and https
functions to output the stored information at an endpoint that anybody from the team (or an internal tool) can quickly check.
Environment Config
Let’s first write a short and simple bash script that can resolve the required details like git commit ID and branch that we’d store in the functions environment configuration (and eventually show in the https
endpoint):
#!/usr/bin/env bash
# Generate Deployment ID
BRANCH=$(git rev-parse --abbrev-ref HEAD)
COMMIT=$(git rev-parse HEAD)
DEPLOYMENT_ID="${BRANCH}-${COMMIT}"
# Set Deployment ID in config
firebase functions:config:set app.deployment_id="${DEPLOYMENT_ID}"
Save it in your functions source
folder that contains index.js
along with package.json
. For me this path is functions/deploy.sh
.
How will the execution of this script happen ? We’ll use firebase predeploy scripts for that.
Predeploy Hook
Just like Git hooks, firebase CLI tool also provides us with the functionality of attaching pre and post deploy hooks to the firebase deploy
command. We can add this to our service (hosting
, functions
, storage
, firestore
, etc.) keys in our firebase.json
configuration file.
Let’s say if I wanted this script to run before every deployment to the functions service, then this is how my firebase.json
functions
section would look like:
{
// ...
"functions": {
"predeploy": "functions/deploy.sh",
"source": "functions"
}
// ...
}
HTTPS Function
Finally we will use functions.https
to create a firebase cloud function that will retrieve the deployment information stored in the environment configuration and send that as a response to HTTP requests that are sent by a browser or some code. Here’s how the function code would look like (exported in index.js
):
const functions = require('firebase-functions');
exports.deploymentEndpoint = functions.https.onRequest(async (req, res) => {
const deploymentId = functions.config().app?.deployment_id || 'Not Found';
res.status(200).send(deploymentId);
});
To make sure that firebase hosting service forwards requests to this https function, add a rewrite entry into your firebase.json
:
{
"hosting": {
"rewrites": [
// ...
{
"source": "/deployment",
"function": "deploymentEndpoint"
},
// ...
]
}
// ...
}
That’s all! Now all you gotta do is deploy your functions and hosting code with:
$ firebase deploy --only hosting,functions
The deployment will run the predeploy hook (our functions/deploy.sh
) which resolves and sets the deployment commit and branch details into the functions configuration. After the deployment, you’ll have access to https://yoursite/deployment
endpoint that will spit out the config value stored.
Bonus
We can always add the predeploy hook (deploy.sh
in our case), to multiple service’s in the firebase.json
. So we can add it to both functions
and hosting
services. In such cases the predeploy hook will run once for each service deployment, so in total twice for firebase deploy --only functions,hosting
.
If you want to store separate deployment information for each service deployment, then you can pass separate arguments to the script in your predeploy field’s value and do an if/else
based on that. In such a case, firebase.json
would look something like this:
{
// ...
"hosting": {
"predeploy": "functions/deploy.sh hosting",
// ...
},
"functions": {
"predeploy": "functions/deploy.sh functions",
// ...
}
}
And the deploy.sh
would look something like this:
#!/usr/bin/env bash
if [ $# -gt 0 ]; then
# Generate Deployment information
# Set Deployment ID in config
if [ "$1" = "hosting" ]; then
# Set deployment information in app.hosting_deployment_id
fi
if [ "$1" = "functions" ]; then
# Set deployment information in app.functions_deployment_id
fi
else
echo "ERROR: First argument missing. Accepted values: hosting, functions"
exit 1
fi
Hello.
Maybe, I’ve missed something, but I see separation of hosting and functions services to handle commit hash tagging through the environment variables. For me, the whole allure of functions is the ability to separate logic between different functions – having micro-services around the same product. For instance, we have more than 10 functions around the same product. They are independent of each other but do share some underlying things, like database and so on. I would never deploy all of these functions at once – it would take longer and is not necessary. There are functions that we don’t deploy for months. Yet, all of these functions share the same repository, as it makes a lot of sense from the code management perspective.
Basically, if we had only one function or always deployed all the functions, I don’t even see much sense in tagging through the Firebase Functions config – we could easily agree to have the latest commit in the stable branch to be the one deployed. As it stands, in our case, it’s not always clear which functions were deployed from the latest commit though.
firebase functions:config:set
is something used across all the deployed functions, which makes it useless to separate metadata for each of the functions (which share the same repo but are not deployed at once). Even if I deployed a single function,firebase functions:config:set
would set the commit hash accessible by functions that were not deployed, basically giving wrong information.I agree with everything that you said. You also pretty much pointed out the flaws (especially in the last para) in the solution that I have described in this post. So yeah, setting config (or environment variables) to store git commit hashes for functions and hosting is not a great solution to reflect them for whatever use.
In fact it is no more recommended (nor even possible in functions v2) to use
functions:config:set
orfunctions.config()
. Due to this I have personally moved away from the approach described in this article. Although one could continue to pass the branch/commit information as environment variables (or even secrets but that would be weird), it would still be “broken” because we can have partial deployments for functions. For hosting it may suffice.I guess the only viable approach at this moment to figure out what branch/commit is deployed in any environment (production or staging), is by having internal communication with the team before/after every release (over slack, email, etc.).