Skip to content

Protecting tags and releases

Release tags identify the code that was released. If a tag can be moved or deleted, users who download a release later might get different code than what was reviewed, tested or signed. This guide shows how to protect tags with rulesets and how to make releases immutable.

Note

GitHub retired tag protection rules and migrated them to tag rulesets, see the GitHub changelog. Otterdog does not support tag protection rules, use rulesets with target: "tag" instead.

Prevent tags from being moved or deleted

The following ruleset allows users with write access to create tags, but nobody can move or delete them afterwards:

orgs.newOrg('my-project', 'my-org') {
  _repositories+:: [
    orgs.newRepo('my-repo') {
      rulesets: [
        orgs.newRepoRuleset('tags') {
          target: "tag",
          include_refs+: [
            "~ALL",
          ],
          allows_creations: true,
          allows_updates: false, // (1)!
          required_pull_request: null, // (2)!
          required_status_checks: null,
        },
      ],
    },
  ],
}
  1. Deletions and force pushes are blocked by default (allows_deletions and allows_force_pushes are false).
  2. Pull requests do not apply to tags, and this ruleset does not require status checks.

Open in the Playground

For tag rulesets, include_refs accepts ~ALL or patterns of the form refs/tags/<pattern>, see Ref Matcher.

Restrict who can create release tags

To ensure that releases are only created by the release process, restrict the creation of release tags to a release app or a team of release managers:

orgs.newOrg('my-project', 'my-org') {
  _repositories+:: [
    orgs.newRepo('my-repo') {
      rulesets: [
        orgs.newRepoRuleset('release-tags') {
          target: "tag",
          include_refs+: [
            "refs/tags/v*",
          ],
          allows_creations: false,
          allows_updates: false,
          bypass_actors+: [
            "my-release-app",
            "@my-org/release-managers",
          ],
          required_pull_request: null,
          required_status_checks: null,
        },
      ],
    },
  ],
}

Open in the Playground

Warning

With allows_creations: false and no bypass actor, nobody can create matching tags, which blocks every release. If tags are created by a workflow, make sure that the identity used by the workflow is allowed to create them, e.g. by using a token of a GitHub App that is listed as bypass actor.

Bypass actors can also move or delete the matching tags. Combine both rulesets if tags must stay unchanged for everybody: since all matching rulesets apply, the first ruleset still blocks updates and deletions for the bypass actors of the second one.

Restrict the names of tags

A pattern can enforce a naming scheme for tags, e.g. semantic versions:

orgs.newOrg('my-project', 'my-org') {
  settings+: {
    plan: "enterprise", // (1)!
  },
  _repositories+:: [
    orgs.newRepo('my-repo') {
      rulesets: [
        orgs.newRepoRuleset('tag-names') {
          target: "tag",
          include_refs+: [
            "~ALL",
          ],
          allows_creations: true,
          required_pull_request: null,
          required_status_checks: null,
          tag_name_pattern: orgs.newPattern() {
            name: "Semantic version",
            operator: "regex", // (2)!
            pattern: "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
          },
        },
      ],
    },
  ],
}
  1. Read-only property that reflects the billing plan of the organization. Restricting the names of tags requires GitHub Enterprise, validate warns otherwise.
  2. The operator is one of starts_with, ends_with, contains or regex, set negate: true to reject the tags that match the pattern instead, see Pattern Settings.

Open in the Playground

Only tags like v1.2.3 can be pushed. A tag name pattern is only supported by rulesets with target: "tag".

Make releases immutable

When a release is immutable, its tag cannot be moved or deleted while the release exists, and its assets cannot be modified or deleted. Immutability only applies to releases published after it has been enabled, see Immutable releases.

Immutable releases can be enforced for some or all repositories of an organization, or enabled per repository:

orgs.newOrg('my-project', 'my-org') {
  settings+: {
    immutable_releases_enforced_repositories: "selected",
    immutable_releases_selected_repositories+: [
      "my-library",
    ],
  },
  _repositories+:: [
    orgs.newRepo('my-library'),
    orgs.newRepo('my-tool') {
      immutable_releases_enabled: true,
    },
  ],
}

Open in the Playground

When the organization enforces immutable releases for a repository, the repository setting is ignored, see Organization settings take precedence. These settings are only managed if the default configuration of your organization defines them, see The default configuration also defines what is managed.

Recommendation

Protect release tags with a ruleset in every repository that publishes releases. Immutable releases additionally protect the release assets, but only for releases published after enabling them, while a tag ruleset protects all matching tags, with or without a release.

Deploy only from tags

To ensure that deployment credentials are only available to workflows that run for a release tag, restrict the environment used for publishing to tags, see Deployment environments.