Create Reproducible Release Archives with git archive
You will finish with a tar.gz or Zip archive made from an exact Git commit, with a predictable top-level directory and a quick check that the contents are right. The examples use Git 2.43.0 from Ubuntu package git 1:2.43.0-1ubuntu7.3, with the matching git-man package installed.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need a Git repository containing the commit or tag you want to distribute, plus tar for tar archives or unzip for Zip checks. The command only reads the repository and writes the archive, so it normally needs no elevated privileges. Do not use sudo to work around a path or permission mistake.
1. Check the version and choose the tree
Run these ordinary, read-only checks from the repository. A tag is usually the clearest release input because it names one fixed commit:
$ git --version
git version 2.43.0
$ git show --no-patch --format='%H%n%h %s' RELEASE_TAG
FULL_COMMIT_ID
SHORT_ID release: prepare 1.0.0
Replace RELEASE_TAG with an existing tag such as v1.0.0. If you use HEAD, the archive changes when the checked-out branch moves. If you use a commit ID, record it alongside the archive so another person can identify the source exactly.
Checkpoint: the second command must identify the intended commit. If Git says that the name is unknown, stop and correct the revision before creating any file.
2. Create a tar.gz archive with a safe prefix
A prefix prevents files from spilling directly into the extraction directory. The trailing slash is part of the archive path:
$ git archive --format=tar.gz \
--prefix=project-1.0.0/ \
--output=project-1.0.0.tar.gz RELEASE_TAG
$ tar tzf project-1.0.0.tar.gz | sed -n '1,6p'
project-1.0.0/
project-1.0.0/README.md
project-1.0.0/src/
project-1.0.0/src/main.c
--output selects the destination. If you omit --format, Git infers the format from an output name such as project-1.0.0.zip; with no output file, the default is tar on standard output. Naming the format explicitly makes scripts easier to review.
Do not overwrite a release archive casually. The output file is replaced if it already exists. Choose a new name, or move the old file to a safe backup first. If the command fails, check the exit status before distributing anything:
$ test -s project-1.0.0.tar.gz && echo 'archive is non-empty'
archive is non-empty
3. Verify the archive before sharing it
Listing is safer than extracting: it does not change your working tree or write files elsewhere. Check the prefix, expected files and absence of private material:
$ tar tzf project-1.0.0.tar.gz | grep -E '(^|/)(README.md|src/|\.env|secret)'
project-1.0.0/README.md
project-1.0.0/src/
project-1.0.0/src/main.c
$ tar tzf project-1.0.0.tar.gz | grep -E '(^|/)(\.env|secret)' || echo 'no named private files'
no named private files
This check is only as good as the names you search for. Inspect the complete listing when the project contains credentials, build outputs or generated files. Never treat git archive as a secret scanner.
For a stronger smoke test, extract into a new temporary directory and compare selected files with the commit. Extraction can create or overwrite files, so use a directory created solely for this check, not a source checkout:
$ check_dir=$(mktemp -d)
$ tar xzf project-1.0.0.tar.gz -C "$check_dir"
$ test -f "$check_dir/project-1.0.0/README.md" && echo 'README present'
README present
When the check is complete, remove only that known temporary directory with your normal cleanup process. Do not run a broad recursive deletion copied from an example.
4. Make a Zip archive or archive one path
Use Zip when the recipient expects a .zip file. The prefix and output behaviour are the same:
$ git archive --format=zip \
--prefix=project-1.0.0/ \
--output=project-1.0.0.zip RELEASE_TAG
$ unzip -l project-1.0.0.zip | sed -n '1,8p'
Archive: project-1.0.0.zip
Length Date Time Name
--------- ---------- ----- ----
... ... ... project-1.0.0/README.md
The exact lengths and timestamps vary. Git uses compression level 6 for Zip by default; pass a backend option such as -9 only when the smaller result is worth the extra work.
Put one or more paths after the tree name to include only those tracked paths:
$ git archive --format=zip \
--prefix=project-docs/ \
--output=project-docs.zip RELEASE_TAG docs/ README.md
$ unzip -l project-docs.zip | grep -E 'project-docs/(README.md|docs/)'
project-docs/README.md
project-docs/docs/
The paths must exist in the selected tree. A path typo makes Git fail rather than silently creating a partial archive.
5. Control exclusions with .gitattributes
Git reads attributes from the tree being archived. Mark generated material or private files with export-ignore in the repository's .gitattributes file:
.env export-ignore
private/ export-ignore
build/ export-ignore
Commit that rule before creating the release archive. Then verify that the excluded paths do not appear. This is a repository change, so review it and commit it through your normal workflow; git archive itself does not edit the file.
If you need to test a checked-out attribute file without committing it, add --worktree-attributes. That makes the result depend on the current working tree rather than only on the selected commit. Use it deliberately and record the working-tree state, especially in a release script.
The inverse attribute, export-subst, expands supported placeholders in marked files. It is useful for embedding revision information, but it changes the archived file's contents. Verify the expanded result rather than assuming it is a literal copy.
6. Preserve or inspect the source commit ID
When the input is a commit or tag, Git records the commit ID in tar metadata and in a Zip comment. For a tar archive, read it back without extracting:
$ git archive --format=tar RELEASE_TAG | git get-tar-commit-id
FULL_COMMIT_ID
A tree ID behaves differently: Git uses the current time for entry modification times and does not provide the same commit identity. Prefer a commit or tag for release artefacts when provenance matters. The timestamp stored for a commit or tag comes from the commit, not from the time you ran the command.
Common failure points
- Unexpected files: inspect
.gitattributesin the selected tree and check for untracked files. Ordinary untracked files are not included unless you explicitly use--add-fileor--add-virtual-file. - Wrong format: specify
--formatwhen the output is redirected or passed through another command. Otherwise the default is tar. - Missing top-level directory: add
--prefix=name/. Do not fix this by extracting into a shared directory. - Remote archive:
--remoteasks the remote server to rungit-upload-archiveand may be restricted to particular revisions. Treat remote output as untrusted input and verify it before extraction.
Done means
- The archive names an intended tag or commit, not an unrecorded moving branch.
- The format, output path and top-level prefix are explicit and verified.
- The listing contains the expected files and no known private material.
- Exclusions live in reviewed
.gitattributesrules when needed. - The archive is non-empty and, where provenance matters, its recorded commit ID has been checked.