Contents

Automated publishing of packages to pub.dev

You can automate publishing from:

The following sections explain how automated publishing is configured, and how you can customize publishing flows in line with your preferences.

When configuring automated publishing you don't need to create a long-lived secret that is copied into your automated deployment environment. Instead, authentication relies on temporary OpenID-Connect tokens signed by either GitHub Actions (See OIDC for GitHub Actions) or Google Cloud IAM.

You can use exported service account keys for deployment environments where an identity service isn't present. Such exported service account keys are long-lived secrets, they might be easier to use in some environments, but also pose a larger risk if accidentally leaked.

Publishing packages using GitHub Actions

#

You can configure automated publishing using GitHub Actions. This involves:

  • Enabling automated publishing on pub.dev, specifying:

    • The GitHub repository and,
    • A tag-pattern that must match to allow publishing.
  • Creating a GitHub Actions workflow for publishing to pub.dev.

  • Pushing a git tag for the version to be published.

The following sections outline how to complete these steps.

Configuring automated publishing from GitHub Actions on pub.dev

#

To enable automated publication from GitHub Actions to pub.dev, you must be:

  • An uploader on the package, or,
  • An admin of the publisher (if the package is owned by a publisher).

If you have sufficient permission, you can enable automated publishing by:

  1. Navigating to the Admin tab (pub.dev/packages/<package>/admin).

  2. Find the Automated publishing section.

  3. Click Enable publishing from GitHub Actions, this prompts you to specify:

    • A repository (<organization>/<repository>, example: dart-lang/pana),
    • A tag-pattern (a string containing {{version}}).

The repository is the <organization>/<repository> on GitHub. For example, if your repository is https://github.com/dart-lang/pana you must specify dart-lang/pana in the repository field.

The tag pattern is a string that must contain {{version}}. Only GitHub Actions triggered by a push of a tag that matches this tag pattern will be allowed to publish your package.

Configuration of publishing from GitHub Actions on pub.dev

Example: a tag pattern like v{{version}} allows GitHub Actions (triggered by git tag v1.2.3 && git push v1.2.3) to publish version 1.2.3 of your package. Thus, it's also important that the version key in pubspec.yaml matches this version number.

If your repository contains multiple packages, give each a separate tag-pattern. Consider using a tag-pattern like my_package_name-v{{version}} for a package named my_package_name.

Configuring a GitHub Action workflow for publishing to pub.dev

#

When automated publishing from GitHub Actions is enabled on pub.dev, you can create a GitHub Actions workflow for publishing. This is done by creating a .github/workflows/publish.yml file as follows:

yaml
# .github/workflows/publish.yml
name: Publish to pub.dev

on:
  push:
    tags:
    # must align with the tag-pattern configured on pub.dev, often just replace
      # {{version}} with [0-9]+.[0-9]+.[0-9]+
    - 'v[0-9]+.[0-9]+.[0-9]+' # tag-pattern on pub.dev: 'v{{version}}'
    # If you prefer tags like '1.2.3', without the 'v' prefix, then use:
    # - '[0-9]+.[0-9]+.[0-9]+' # tag-pattern on pub.dev: '{{version}}'
    # If your repository contains multiple packages consider a pattern like:
    # - 'my_package_name-v[0-9]+.[0-9]+.[0-9]+'

# Publish using the reusable workflow from dart-lang.
jobs:
  publish:
    permissions:
      id-token: write # Required for authentication using OIDC
    uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1
    # with:
    #   working-directory: path/to/package/within/repository

Make sure to match the pattern in on.push.tags with the tag pattern specified on pub.dev. Otherwise, the GitHub Action workflow won't work. If publishing multiple packages from the same repository, use a per-package tag pattern like my_package_name-v{{version}} and create a separate workflow file for each package.

The workflow file above uses dart-lang/setup-dart/.github/workflows/publish.yml to publish the package. This is a reusable workflow that allows the Dart team to maintain the publishing logic and enables pub.dev to know how the package was published. Using this reusable workflow is strongly encouraged.

If you need generated code in your package, then it is preferable to check this generated code into your repository. This simplifies verifying that the files published on pub.dev match the files from your repository. If checking generated or built artifact into your repository is not reasonable, you can create a custom workflow along the lines of:

yaml
# .github/workflows/publish.yml
name: Publish to pub.dev

on:
  push:
    tags:
    - 'v[0-9]+.[0-9]+.[0-9]+' # tag pattern on pub.dev: 'v{{version}'

# Publish using custom workflow
jobs:
  publish:
    permissions:
      id-token: write # Required for authentication using OIDC
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: dart-lang/setup-dart@v1
      - name: Install dependencies
        run: dart pub get
      # Here you can insert custom steps you need
      # - run: dart tool/generate-code.dart
      - name: Publish
        run: dart pub publish --force

The workflow authenticates to pub.dev using a temporary GitHub-signed OIDC token, the token is created and configured in the dart-lang/setup-dart step. To publish to pub.dev, subsequent steps can run dart pub publish --force.

Triggering automated publishing from GitHub Actions

#

After you've configured automated publishing on pub.dev and created a GitHub Actions workflow, you can publish a new version of your package. To publish, push a git tag matching the configured tag pattern.

$ cat pubspec.yaml
yaml
package: my_package_name
version: 1.2.3            # must match the version number used in the git tag
environment:
  sdk: ^2.19.0
$ git tag v1.2.3          # assuming my tag pattern is: 'v{{version}}'
$ git push origin v1.2.3  # triggers the action that publishes my package.

Once pushed, review the workflow logs at https://github.com/<organization>/<repository>/actions.

If the Action didn't trigger, check that the pattern configured in .github/workflows/publish.yml matches the pushed git tag. If the Action failed, the logs might contain clues as to why it failed.

Once published, you can see the publication event in the audit-log on pub.dev. The audit-log entry should contain a link to the GitHub Action run that published the package version.

Audit log after publishing from GitHub Actions

If you don't like using the git CLI to create tags, you can create releases on GitHub from https://github.com/<organization>/<repository>/releases/new. To learn more, check out managing releases in a repository from GitHub.

Hardening security with tag protection rules on GitHub

#

Configuring automated publishing from GitHub Actions allows anyone who can push a tag to your repository to trigger publishing to pub.dev. You can restrict who can push tags to your repository using tag protection rules on GitHub.

By limiting who can create tags matching your tag pattern, you can limit who can publish the package.

At this time, the tag protection rules lack flexibility. You might want to restrict who can trigger publishing using GitHub Deployment Environments, as outlined in the next section.

Hardening security with GitHub Deployment Environments

#

When configuring automated publishing from GitHub Actions on pub.dev, you can require a GitHub Actions environment. To require a GitHub Actions environment for publishing you must:

  1. Navigate to the Admin tab (pub.dev/packages/<package>/admin).
  2. Find the Automated publishing section.
  3. Click Require GitHub Actions environment.
  4. Specify an Environment name, (pub.dev is typically a good name)

Configure pub.dev to require a GitHub deployment environment

When an environment is required on pub.dev, GitHub Actions won't be able to publish unless they have environment: pub.dev. Thus, you must:

  1. Create an environment with the same name on GitHub (typically pub.dev)
  2. Alter your .github/workflows/publish.yml workflow file to specify environment: pub.dev, as follows:
yaml
# .github/workflows/publish.yml
name: Publish to pub.dev

on:
  push:
    tags:
    - 'v[0-9]+.[0-9]+.[0-9]+' # for tags like: 'v1.2.3'

jobs:
  publish:
    permissions:
      id-token: write # Required for authentication using OIDC
    uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1
    with:
      # Specify the github actions deployment environment
      environment: pub.dev
      # working-directory: path/to/package/within/repository

The environment is reflected in the temporary GitHub-signed OIDC token used for authentication with pub.dev. Thus, a user with permission to push to your repository cannot circumvent environment protection rules by modifying the workflow file.

In GitHub repository settings, you can use environment protection rules to configure required reviewers. If you configure this option, GitHub prevents actions with the environment from running until one of the required reviewers have approved the run.

GitHub Action waiting for deployment review

Publishing from Google Cloud Build

#

You can configure automated publishing from Google Cloud Build. This involves:

  • Register a Google Cloud Project (or using an existing project),
  • Create a service account for publishing to pub.dev,
  • Enable automated publishing in the admin tab for the package on pub.dev, specifying the email of the service account created for publishing.
  • Grant the default Cloud Build service account permission to impersonate the service account created for publishing.
  • Create a cloudbuild.yaml file that obtains a temporary OIDC id_token and uses it for publishing to pub.dev
  • Configure a Cloud Build trigger, for running the steps in cloudbuild.yaml in your project on Google Cloud Build.

The following sections outline how to complete these steps.

Creating a service account for publishing

#

For publishing to pub.dev you are going to create a service account that is granted permission to publish your package on pub.dev. You are then going to grant Cloud Build permission to impersonate this service account.

  1. Create a cloud project, if you don't have an existing project.

  2. Create a service account as follows:

    $ gcloud iam service-accounts create pub-dev \
      --description='Service account to be impersonated when publishing to pub.dev' \
      --display-name='pub-dev'

    This creates a service account named pub-dev@$PROJECT_ID.iam.gserviceaccount.com.

  3. Grant the service account permission to publish your package.

    To complete this step, you must have uploader permission on the package or be an admin of the publisher that owns the package.

    a. Navigate to the Admin tab (pub.dev/packages/<package>/admin). a. Click Enable publishing with Google Cloud Service account. a. Type the email of the service account into the Service account email field. You created this account in the previous step: pub-dev@$PROJECT_ID.iam.gserviceaccount.com

Configuration that allows service account to publish on pub.dev

With this procedure complete, anyone who can impersonate the service account can publish new versions of the package. Make sure to review who has permissions to impersonate the service account and change permissions in the cloud project as needed.

Granting Cloud Build permission to publish

#

To publish from Cloud Build you must give the default Cloud Build service account permission to impersonate the service account created for publishing in the previous section.

  1. Enable the IAM Service Account Credentials API in the cloud project. Attempts to impersonate a service account will fail without this API.

    # Enable IAM Service Account Credentials API
    $ gcloud services enable iamcredentials.googleapis.com
  2. Find the project number.

    # The PROJECT_NUMBER can be obtained as follows:
    $ gcloud projects describe $PROJECT_ID --format='value(projectNumber)'
  3. Grant the permission to impersonate the publishing service account.

    # Grant default cloud
    $ gcloud iam service-accounts add-iam-policy-binding \
      pub-dev@$PROJECT_ID.iam.gserviceaccount.com \
      --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
      --role=roles/iam.serviceAccountTokenCreator

Writing a Cloud Build configuration file

#

To publish from Cloud Build, you must specify steps for Cloud Build to:

  • Impersonate the service account to obtain a temporary OIDC token.
  • Provide the temporary OIDC token to dart pub for use when publishing.
  • Calling dart pub publish to publish the package.

Steps for Google Cloud Build are provided in a cloudbuild.yaml file, see build configuration file schema for full documentation of the format.

For publishing to pub.dev from Google Cloud Build, a cloudbuild.yaml file as follows will do:

yaml
# cloudbuild.yaml
steps:
- id: Create temporary token
  name: gcr.io/cloud-builders/gcloud
  volumes:
  - name: temporary-secrets
    path: /secrets
  script: |
    gcloud auth print-identity-token \
      --impersonate-service-account=pub-dev@$PROJECT_ID.iam.gserviceaccount.com \
      --audiences=https://pub.dev \
      --include-email > /secrets/temporary-pub-token.txt
  env:
  - PROJECT_ID=$PROJECT_ID
- id: Publish to pub.dev
  name: dart
  volumes:
  - name: temporary-secrets
    path: /secrets
  script: | 
    cat /secrets/temporary-pub-token.txt | dart pub token add https://pub.dev
    dart pub publish --force

The gcloud auth print-identity-token creates an OIDC id_token impersonating the specified service account. This id_token is signed by Google, with a signature that expires within 1 hour. The audiences parameter lets pub.dev know that it is the intended recipient of the token. The --include-email option is necessary for pub.dev to recognize the service account.

Once the id_token is created, it's written to a file that resides in a volume; this mechanism is used to pass data between steps. Don't store the token in /workspace. Since /workspace is where the repository from which you wish to publish is checked out. Not using /workspace for storing the token reduces the risk that you accidentally include it in your package when publishing.

Creating a Cloud Build trigger

#

With service accounts configured and a cloudbuild.yaml file in the repository you can create a Cloud Build Trigger using the console.cloud.google.com dashboard. To create a build trigger, you need to connect to a source repository and specify which events should trigger a build. You can use GitHub, Cloud Source Repository, or one of the other options. To learn how to configure a Cloud Build Trigger, check out creating and managing build triggers.

To use the cloudbuild.yaml from the previous step, configure the Cloud Build Trigger type as "Cloud Build Configuration" located in the repository in the /cloudbuild.yaml file. Do not specify a service account for the build to be triggered with. Instead you'll want to use the default service account for Cloud Build.

Configuration for trigger

When configuring your Cloud Build trigger, consider who can trigger the build. Because triggering a build might publish a new version of your package. Consider only allowing manual builds or use Cloud Build approvals to gate builds as outlined in next section.

Hardening security with Cloud Build Approvals

#

When configuring a Cloud Build trigger, you can select require approval before build executes. If a Cloud Build trigger requires approval, it won't run when triggered. Instead, it'll wait for approval. This can be used to limit who can publish new versions of your package.

Enabling approvals in configuration of the Cloud Build trigger

Only a user with the Cloud Build Approver role can give approval. When giving a approval, the approver can specify a URL and comment.

Cloud Build run waiting for approval to run

You can also configure notifications for pending approvals. To learn more, check out gate build on approval.

Publish from anywhere using a Service Account

#

To allow automated publishing outside of GitHub Actions, you might authenticate using service accounts in way similar to Cloud Build.

This usually involves:

The section for Cloud Build outlined how to create a service account for publishing. This should provide a service account, such as pub-dev@$PROJECT_ID.iam.gserviceaccount.com.

Publish using Workload Identity Federation

#

When running on a cloud service that supports OIDC or SAML, you can impersonate a GCP service account using Workload Identity Federation. This enables you to leverage your cloud provider's identity services.

For example, if deploying on EC2, you can configure workload identity federation with AWS, allowing temporary AWS tokens from the EC2 metadata service to impersonate a service account. To learn how to configure these flows, check out workload identity federation.

Publish using Exported Service Account Keys

#

When running on a custom system without identity services, you can export service account keys. Exported service account keys allows you to authenticate as said service account. To learn more, check out how to create and manage service account keys.

Export service account keys

#
  1. Create exported service account keys for an existing service account.

    $ gcloud iam service-accounts keys create key-file.json \
      --iam-account=pub-dev@$PROJECT_ID.iam.gserviceaccount.com
  2. Save the key-file.json file for later use.

Publish packages using exported service account keys

#

To publish a package using exported service account keys:

  1. Setup gcloud to authenticate using key-file.json (created in the previous step)

    $ gcloud auth activate-service-account --key-file=key-file.json
  2. Create a temporary token for pub.dev and pass it to dart pub token add https://pub.dev. To impersonate service account, include the --include-email option.

    $ gcloud auth print-identity-token \
      --audiences=https://pub.dev \
      | dart pub token add https://pub.dev
  3. Publish using the temporary token. Add the --force option to skip the yes/no prompt.

    $ dart pub publish --force