From 6da0b32ca78854419f88d1b314f3363a2451a380 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 18 Sep 2026 22:21:46 +0200 Subject: [PATCH] Add the `upload-source-to-atr` action Compose a source release candidate in one step: create a reproducible source archive of `HEAD` with `git archive | gzip -n`, compute its SHA-512 checksum and the SWHID of the expanded tree, sign it with the PMC key imported through `crazy-max/ghaction-import-gpg` and upload the files to ATR through the existing `upload-to-atr` action. The shell and Python logic lives in scripts next to `action.yml`, and the `asfswhid` dependency is pinned with hashes and tracked by Dependabot. zizmor is bumped to a version that understands the `$/` self-repository reference used to call `upload-to-atr`. Assisted-By: Claude Fable 5.1 --- .github/dependabot.yml | 7 ++ .pre-commit-config.yaml | 10 +- README.md | 37 +++++++ upload-source-to-atr/README.md | 137 ++++++++++++++++++++++++++ upload-source-to-atr/action.yml | 99 +++++++++++++++++++ upload-source-to-atr/archive.sh | 84 ++++++++++++++++ upload-source-to-atr/requirements.txt | 7 ++ upload-source-to-atr/sign.sh | 43 ++++++++ upload-source-to-atr/swhid.py | 77 +++++++++++++++ 9 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 upload-source-to-atr/README.md create mode 100644 upload-source-to-atr/action.yml create mode 100755 upload-source-to-atr/archive.sh create mode 100644 upload-source-to-atr/requirements.txt create mode 100755 upload-source-to-atr/sign.sh create mode 100644 upload-source-to-atr/swhid.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dac1308..db98618 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,3 +24,10 @@ updates: day: "monday" cooldown: default-days: 7 + - package-ecosystem: "pip" + directory: "/upload-source-to-atr" + schedule: + interval: "weekly" + day: "monday" + cooldown: + default-days: 7 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41f5e94..e680b04 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,6 +56,14 @@ repos: - "|#|" - --license-filepath - share/apache-2.0_header.txt + - id: insert-license + name: Add license header to all shell and Python files + files: ^.*\.(sh|py)$ + args: + - --comment-style + - "#" + - --license-filepath + - share/apache-2.0_header.txt - repo: https://github.com/adrienverge/yamllint rev: v1.37.1 @@ -72,7 +80,7 @@ repos: args: ["--verbose"] - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.23.0 + rev: v1.30.1 hooks: - id: zizmor args: [--min-severity, low] diff --git a/README.md b/README.md index 4bdb857..d3fa251 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,40 @@ jobs: # Add specific inputs here that are required by the action # project: example # version: ${{ github.ref_name }} + +### `apache/tooling-actions/upload-source-to-atr` + +![Status: Experimental](https://img.shields.io/badge/Status-EXPERIMENTAL-orange) + +Compose a source release candidate: +create a reproducible source archive with `git archive`, +compute its SHA-512 checksum and SWHID, +sign it with the project's GPG key +and upload the files to ATR. +See the [action README](upload-source-to-atr/README.md) for all inputs. + +#### Usage Example + +```yaml +jobs: + compose: + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write # Required for OIDC + contents: read + steps: + - name: Checkout code + uses: actions/checkout@ + with: + persist-credentials: false + + - name: Compose, sign and upload the source release + uses: apache/tooling-actions/upload-source-to-atr@ + with: + project: example + version: 1.2.3 + gpg-private-key: ${{ secrets.EXAMPLE_GPG_SECRET_KEY }} + gpg-passphrase: ${{ secrets.EXAMPLE_GPG_PASSPHRASE }} + gpg-fingerprint: ${{ vars.EXAMPLE_GPG_FINGERPRINT }} +``` diff --git a/upload-source-to-atr/README.md b/upload-source-to-atr/README.md new file mode 100644 index 0000000..707f64f --- /dev/null +++ b/upload-source-to-atr/README.md @@ -0,0 +1,137 @@ +# Compose, sign, and upload a source release to ATR + +``` +apache/tooling-actions/upload-source-to-atr +``` + +This composite GitHub Action creates a reproducible source archive of the checked-out commit with `git archive`, +computes its SHA-512 checksum and the [SWHID](https://swhid.org/) of its content, +signs it with a GPG key, +and uploads the three files to ATR at `///` using the [`upload-to-atr`](../upload-to-atr/README.md) action. +Use it to compose a source release candidate from a tag or branch without long-lived ATR credentials. + +Status: EXPERIMENTAL + +## Inputs + +- **project (required)**: ATR project name segment in the remote path. +- **version (required)**: ATR release version segment in the remote path, for example `1.2.3`. +- **archive-prefix**: Archive file name without the `.tar.gz` suffix. + Default: `apache---src`. +- **archive-directory**: Name of the single top-level directory inside the archive. + Default: same as `archive-prefix`. +- **gpg-private-key (required)**: ASCII-armored GPG private key used for signing. +- **gpg-passphrase**: Passphrase of the GPG private key, if it has one. + Default: empty. +- **gpg-fingerprint**: Expected fingerprint of the primary signing key. + When set, the action fails if the imported key has a different fingerprint. + Default: empty (no check). +- **swhid**: If `"true"`, compute the directory SWHID of the expanded archive. + Default: `"true"`. +- **atr-host**: ATR host to upload to. + Default: `releases.apache.org`. + Must match `*.apache.org`. +- **ssh-port**: SSH port on ATR. + Default: `2222`. + +## Outputs + +- **directory**: Directory containing `.tar.gz`, `.tar.gz.sha512` and `.tar.gz.asc`. +- **archive**: File name of the archive. +- **commit**: Commit that was archived. +- **sha512**: SHA-512 checksum of the archive. +- **swhid**: Directory SWHID (`swh:1:dir:...`) of the expanded archive. + Empty when `swhid` is `"false"`. + +## Example workflow + +The `id-token` write permission is **required** when using this GitHub Action. +Tagged versions of this action are not available. +Replace `` in this example with your chosen commit. + +```yaml +name: Compose source release + +on: + push: + tags: ["v*.*.*-rc.*"] + +permissions: + contents: read + +jobs: + compose: + if: github.repository == 'apache/example' + runs-on: ubuntu-latest + # Keep the signing secrets in an environment that requires approval. + environment: + name: release + url: https://releases.apache.org/projects/example + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@ + with: + persist-credentials: false + + - name: Extract version + id: version + run: | + version="${GITHUB_REF_NAME#v}" + echo "version=${version%-rc.*}" >> "$GITHUB_OUTPUT" + + - name: Compose, sign and upload the source release + uses: apache/tooling-actions/upload-source-to-atr@ + with: + project: example + version: ${{ steps.version.outputs.version }} + # archive-prefix defaults to apache-example--src + gpg-private-key: ${{ secrets.EXAMPLE_GPG_SECRET_KEY }} + gpg-passphrase: ${{ secrets.EXAMPLE_GPG_PASSPHRASE }} + gpg-fingerprint: ${{ vars.EXAMPLE_GPG_FINGERPRINT }} +``` + +## Reproducibility + +Anyone who archives the same commit with the same version of `gzip` obtains a byte-for-byte identical file. +The action follows the [reproducible archives](https://reproducible-builds.org/docs/archives/) guidance: + +- `git archive` sets the modification time of every entry to the commit time and the owner to `root`, +- `tar.umask=0022` gives every entry the same permissions regardless of the runner, +- `gzip -n` leaves the file name and timestamp out of the gzip header. + +[Trusted Publishing](https://releases.apache.org/docs/trusted-publishing) at the ASF requires releases built on CI to be reproducible, +and the ASF Security team confirms this for each pipeline. +A release that consists of this source archive alone meets the requirement out of the box. + +### Signing secrets + +The signing key is provisioned by ASF Infrastructure as part of [Trusted Publishing](https://releases.apache.org/docs/trusted-publishing). +Open an [INFRA ticket](https://issues.apache.org/jira/projects/INFRA/issues) and ask for a release signing key for your repository, with: + +- two repository secrets holding the private key and its passphrase, + named after the PMC, for example `EXAMPLE_GPG_SECRET_KEY` and `EXAMPLE_GPG_PASSPHRASE`, +- one organization variable holding the fingerprint of the key, for example `EXAMPLE_GPG_FINGERPRINT`, + shared with all the repositories of the PMC. + +You receive the public key, which should be added to ATR. + +A composite action cannot read secrets or variables on its own, +so pass them to the `gpg-private-key`, `gpg-passphrase` and `gpg-fingerprint` inputs as in the example above. + +## SWHID + +The action also computes the [Software Heritage identifier](https://swhid.org/) (SWHID, ISO/IEC 18670:2025) of the source tree in the archive. +A directory SWHID is derived from the names, modes, and contents of the files alone. +Timestamps, ownership, and compression play no part, +so the same sources produce the same SWHID whichever archive format carries them. + +Git builds its tree ids the same way, +so the SWHID is `swh:1:dir:` of the archived commit as long as `git archive` exported the tree as is. +The two diverge when git attributes rewrite the exported files, for example: + +- `export-ignore` and `export-subst`, +- `text` and `eol` attributes or `core.autocrlf`. + +To skip the SWHID computation, set `swhid` to `false`. diff --git a/upload-source-to-atr/action.yml b/upload-source-to-atr/action.yml new file mode 100644 index 0000000..89b8052 --- /dev/null +++ b/upload-source-to-atr/action.yml @@ -0,0 +1,99 @@ +--- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: "Compose, sign and upload a source release to ATR" +description: "Create a reproducible source tarball with git archive, compute its SHA-512 checksum and SWHID, sign it and upload the files to ATR." +branding: {icon: upload-cloud, color: blue} + +inputs: + project: {description: "ATR project", required: true} + version: {description: "ATR release version", required: true} + archive-prefix: {description: "Archive file name without the .tar.gz suffix (default: apache---src)", default: ""} + archive-directory: {description: "Name of the single top-level directory inside the archive (default: same as archive-prefix)", default: ""} + gpg-private-key: {description: "ASCII-armored GPG private key used for signing (usually secrets._GPG_SECRET_KEY)", required: true} + gpg-passphrase: {description: "Passphrase of the GPG private key, if any (usually secrets._GPG_PASSPHRASE)", default: ""} + gpg-fingerprint: {description: "Expected fingerprint of the primary signing key; the action fails if the imported key differs (usually vars._GPG_FINGERPRINT)", default: ""} + swhid: {description: "Compute the SWHID of the expanded archive (true or false)", default: "true"} + atr-host: {description: "ATR host", default: "releases.apache.org"} + ssh-port: {description: "SSH port", default: "2222"} + +outputs: + directory: {description: "Directory containing the archive, its checksum and its signature", value: "${{ steps.archive.outputs.directory }}"} + archive: {description: "File name of the archive", value: "${{ steps.archive.outputs.archive }}"} + commit: {description: "Commit that was archived", value: "${{ steps.archive.outputs.commit }}"} + sha512: {description: "SHA-512 checksum of the archive", value: "${{ steps.archive.outputs.sha512 }}"} + swhid: {description: "Directory SWHID of the expanded archive (empty if swhid is false)", value: "${{ steps.swhid.outputs.swhid }}"} + +runs: + using: "composite" + steps: + - name: Create the source archive + id: archive + shell: bash + run: "$GITHUB_ACTION_PATH/archive.sh" + env: + INPUTS_PROJECT: ${{ inputs.project }} + INPUTS_VERSION: ${{ inputs.version }} + INPUTS_ARCHIVE_PREFIX: ${{ inputs.archive-prefix }} + INPUTS_ARCHIVE_DIRECTORY: ${{ inputs.archive-directory }} + + - name: Set up Python for the SWHID computation + if: ${{ inputs.swhid == 'true' }} + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + + - name: Compute the SWHID of the expanded archive + id: swhid + if: ${{ inputs.swhid == 'true' }} + shell: bash + run: | + set -euo pipefail + python -m venv "${RUNNER_TEMP}/upload-source-to-atr-venv" + "${RUNNER_TEMP}/upload-source-to-atr-venv/bin/python" -m pip install --quiet --require-hashes -r "${GITHUB_ACTION_PATH}/requirements.txt" + "${RUNNER_TEMP}/upload-source-to-atr-venv/bin/python" "${GITHUB_ACTION_PATH}/swhid.py" + env: + DIRECTORY: ${{ steps.archive.outputs.directory }} + ARCHIVE: ${{ steps.archive.outputs.archive }} + ARCHIVE_DIRECTORY: ${{ steps.archive.outputs.archive-directory }} + TREE: ${{ steps.archive.outputs.tree }} + + - name: Import the GPG signing key + id: gpg + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 + with: + gpg_private_key: ${{ inputs.gpg-private-key }} + passphrase: ${{ inputs.gpg-passphrase }} + + - name: Sign the source archive + shell: bash + run: "$GITHUB_ACTION_PATH/sign.sh" + env: + DIRECTORY: ${{ steps.archive.outputs.directory }} + ARCHIVE: ${{ steps.archive.outputs.archive }} + IMPORTED_FINGERPRINT: ${{ steps.gpg.outputs.fingerprint }} + EXPECTED_FINGERPRINT: ${{ inputs.gpg-fingerprint }} + + - name: Upload to ATR + uses: $/upload-to-atr + with: + project: ${{ inputs.project }} + version: ${{ inputs.version }} + src: ${{ steps.archive.outputs.directory }} + atr-host: ${{ inputs.atr-host }} + ssh-port: ${{ inputs.ssh-port }} diff --git a/upload-source-to-atr/archive.sh b/upload-source-to-atr/archive.sh new file mode 100755 index 0000000..7b355eb --- /dev/null +++ b/upload-source-to-atr/archive.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Create a reproducible source archive of HEAD and its SHA-512 checksum. +# See https://reproducible-builds.org/docs/archives/ +# +# Inputs (environment): INPUTS_PROJECT, INPUTS_VERSION, INPUTS_ARCHIVE_PREFIX, INPUTS_ARCHIVE_DIRECTORY +# Outputs (GITHUB_OUTPUT): directory, archive, archive-directory, commit, tree, sha512 + +set -euo pipefail + +: "${INPUTS_PROJECT:?project is required}" +: "${INPUTS_VERSION:?version is required}" +name_pattern='^[A-Za-z0-9][A-Za-z0-9._+-]*$' +for name in "$INPUTS_PROJECT" "$INPUTS_VERSION"; do + if ! [[ "$name" =~ $name_pattern ]]; then + echo "::error::project and version must only contain letters, digits, '.', '_', '+' or '-'" + exit 1 + fi +done +archive_prefix="${INPUTS_ARCHIVE_PREFIX:-apache-${INPUTS_PROJECT}-${INPUTS_VERSION}-src}" +archive_directory="${INPUTS_ARCHIVE_DIRECTORY:-${archive_prefix}}" +for name in "$archive_prefix" "$archive_directory"; do + if ! [[ "$name" =~ $name_pattern ]]; then + echo "::error::archive-prefix and archive-directory must only contain letters, digits, '.', '_', '+' or '-'" + exit 1 + fi +done +case "$archive_prefix" in + *.tar.gz|*.tgz|*.tar) + echo "::error::archive-prefix must not include the .tar.gz suffix; it is appended automatically" + exit 1;; +esac +if ! commit="$(git rev-parse --verify --quiet 'HEAD^{commit}')"; then + echo "::error::No git commit found in the working directory; check out the repository first (actions/checkout)" + exit 1 +fi +tree="$(git rev-parse "${commit}^{tree}")" + +archive="${archive_prefix}.tar.gz" +directory="${RUNNER_TEMP}/upload-source-to-atr" +rm -rf "$directory" +mkdir "$directory" + +# git archive uses the commit time as mtime and root as owner, +# tar.umask normalizes permissions, +# and gzip -n omits the file name and timestamp from the gzip header. +git -c tar.umask=0022 archive --format=tar --prefix="${archive_directory}/" "$commit" | + gzip -n -9 > "${directory}/${archive}" + +cd "$directory" +sha512sum "$archive" > "${archive}.sha512" +sha512="$(cut -d ' ' -f 1 "${archive}.sha512")" + +{ + echo "directory=${directory}" + echo "archive=${archive}" + echo "archive-directory=${archive_directory}" + echo "commit=${commit}" + echo "tree=${tree}" + echo "sha512=${sha512}" +} >> "$GITHUB_OUTPUT" +{ + echo "### Source release" + echo + echo "- Commit: \`${commit}\`" + echo "- Archive: \`${archive}\` (top-level directory \`${archive_directory}/\`)" + echo "- SHA-512: \`${sha512}\`" +} >> "$GITHUB_STEP_SUMMARY" diff --git a/upload-source-to-atr/requirements.txt b/upload-source-to-atr/requirements.txt new file mode 100644 index 0000000..764a174 --- /dev/null +++ b/upload-source-to-atr/requirements.txt @@ -0,0 +1,7 @@ +# Dependencies of swhid.py, installed with `pip install --require-hashes`. +asfswhid==0.1.1 \ + --hash=sha256:c53463209b51ce81badffb0e7f4afdc3adb028521e22783fe99e2d7654843785 \ + --hash=sha256:efad55e2b92453d7c60b7d722779e4fd98010d0fa4a5bc7537c7065791773c2f \ + --hash=sha256:2f2b4b2ba269b05bddcfb086061c96f671e93cdf30ae63d30eba05061f90ee21 \ + --hash=sha256:34be890bb403a8248263e7b628632e7d1bcaf11f1b620a1b0e9c07c9690376d8 \ + --hash=sha256:2cb15a7859c779b756a033122ad0ab15be70f228a1c9175cf601fc62a1fbf09f diff --git a/upload-source-to-atr/sign.sh b/upload-source-to-atr/sign.sh new file mode 100755 index 0000000..a08588a --- /dev/null +++ b/upload-source-to-atr/sign.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Sign the source archive with the imported GPG key and verify the signature and checksum. +# +# Inputs (environment): DIRECTORY, ARCHIVE, IMPORTED_FINGERPRINT, EXPECTED_FINGERPRINT + +set -euo pipefail + +if [[ -n "$EXPECTED_FINGERPRINT" ]]; then + if ! [[ "$EXPECTED_FINGERPRINT" =~ ^([0-9A-Fa-f]{40}|[0-9A-Fa-f]{64})$ ]]; then + echo "::error::gpg-fingerprint must be the full fingerprint of the primary key (40 or 64 hex digits)" + exit 1 + fi + if [[ "${IMPORTED_FINGERPRINT^^}" != "${EXPECTED_FINGERPRINT^^}" ]]; then + echo "::error::The imported GPG key (${IMPORTED_FINGERPRINT}) does not match gpg-fingerprint" + exit 1 + fi +fi + +cd "$DIRECTORY" +gpg --batch --yes --armor --detach-sign --local-user "$IMPORTED_FINGERPRINT" --output "${ARCHIVE}.asc" "$ARCHIVE" +gpg --batch --status-fd 1 --verify "${ARCHIVE}.asc" "$ARCHIVE" | + awk -v fingerprint="$IMPORTED_FINGERPRINT" \ + '$1 == "[GNUPG:]" && $2 == "VALIDSIG" && ($3 == fingerprint || $NF == fingerprint) { valid = 1 } END { exit !valid }' +sha512sum --check "${ARCHIVE}.sha512" + +echo "- Signed with: \`${IMPORTED_FINGERPRINT}\`" >> "$GITHUB_STEP_SUMMARY" diff --git a/upload-source-to-atr/swhid.py b/upload-source-to-atr/swhid.py new file mode 100644 index 0000000..b6cbe92 --- /dev/null +++ b/upload-source-to-atr/swhid.py @@ -0,0 +1,77 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Compute the SWHID of the source tree contained in the release archive. + +The archive is extracted into a temporary directory, +and the directory SWHID (https://swhid.org/) of its single top-level directory +is computed with asfswhid (https://github.com/apache/tooling-asfswhid). +The result is compared with the SWHID of the archived git tree. +They are identical unless git attributes altered the archive content: +export-ignore and export-subst, +but also line ending conversion (text, eol, core.autocrlf) and clean/smudge filters, +since git archive applies the same conversions as a checkout. + +Inputs (environment): DIRECTORY, ARCHIVE, ARCHIVE_DIRECTORY, TREE +Outputs (GITHUB_OUTPUT): swhid +""" + +import os +import sys +import tarfile +import tempfile +from pathlib import Path + +from asfswhid import directory_id + + +def error(message: str) -> None: + print(f"::error::{message}", file=sys.stderr) + sys.exit(1) + + +def main() -> None: + directory = Path(os.environ["DIRECTORY"]) + archive = os.environ["ARCHIVE"] + archive_directory = os.environ["ARCHIVE_DIRECTORY"] + tree = os.environ["TREE"] + + with tempfile.TemporaryDirectory(prefix="upload-source-to-atr-") as tmp: + with tarfile.open(directory / archive, "r:gz") as tar: + tar.extractall(tmp, filter="data") + entries = sorted(os.listdir(tmp)) + if entries != [archive_directory]: + error(f"Expected {archive} to contain the single directory {archive_directory}/, found: {entries}") + swhid = str(directory_id(os.path.join(tmp, archive_directory))) + + tree_swhid = f"swh:1:dir:{tree}" if len(tree) == 40 else None + if tree_swhid is None: + comparison = "not compared with the git tree (repository does not use SHA-1)" + elif swhid == tree_swhid: + comparison = "identical to the git tree of the archived commit" + else: + comparison = f"differs from the git tree `{tree_swhid}`, git attributes such as export-ignore, export-subst or eol altered the content" + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"swhid={swhid}\n") + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary: + summary.write(f"- SWHID: `{swhid}` ({comparison})\n") + print(f"{swhid} ({comparison})") + + +if __name__ == "__main__": + main()