diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index d4c9bbf..2dc0a9d 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -1,85 +1,129 @@ name: 'Docker Build' description: 'Builds docker image' inputs: - push: - required: true - description: "Build and push image to registry (cannot be used together with load)" - default: "false" - password: - required: false - description: "Password for the registry" - username: - required: false - description: "Username for the registry" node_env: required: false description: "Node environment" default: "production" + latest: + required: false + description: "Tag latest version" + default: "false" + target: + required: false + description: "Docker stage to target" + default: "final" + push: + required: false + description: "Should image be pushed to registries" + default: "false" outputs: - tags: - description: "The Docker tags for the image" - value: ${{ steps.meta.outputs.tags }} - version: - description: "The version for the image" - value: ${{ steps.meta.outputs.version }} image: description: "The Docker image" value: ${{ steps.image.outputs.image }} - image_version: + version: + description: "The version for the image" + value: ${{ steps.meta.outputs.version }} + digest: + description: "The build digest for the image" + value: ${{ steps.build.outputs.digest }} + tag: description: "Combines image and version to a valid image tag" - value: ${{ steps.image.outputs.image }}:${{ steps.meta.outputs.version }} + value: ${{ steps.tag.outputs.tag }} runs: using: "composite" steps: - # Setup docker to build for multiple architectures + - name: Login to Container Registry + if: inputs.push == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ inputs.username }} + password: ${{ inputs.password }} + - name: Set up QEMU uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 with: version: latest buildkitd-flags: --debug - # Login to a registry to push the image - - name: Login to Container Registry - # Only login if we are pushing the image - if: ${{ inputs.push == 'true' }} - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ inputs.username }} - password: ${{ inputs.password }} - - name: Docker Image id: image shell: bash run: | - echo "image=ghcr.io/mozilla/test-github-features" >> $GITHUB_OUTPUT + echo "image=${{ github.repository }}" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT - name: Docker meta id: meta uses: docker/metadata-action@v5 with: - images: ${{ steps.image.outputs.image }} + images: | + ghcr.io/${{ steps.image.outputs.image }} + flavor: | + suffix=-next,onlatest=true + latest=${{ inputs.latest == 'true' }} tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=raw,value=staging,enable=${{ github.event_name == 'merge_group' }} type=ref,event=pr - type=sha + type=ref,event=branch + type=ref,event=tag + + - name: Docker tag + id: tag + shell: bash + run: | + # Extract metadata output json + cat < meta.json + ${{ steps.meta.outputs.json }} + EOF + + echo "tag=$(cat meta.json | jq -r '.tags[0]')" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT + + - name: Tar file + id: tar + shell: bash + # image.tar is the name of the compressed image file + # This should be kept in sync with ./.github/actions/run/action.yml + # That loads the image from this file + run: | + echo "path=/tmp/image.tar" >> $GITHUB_OUTPUT - name: Build Image - uses: docker/build-push-action@v5 + id: build + uses: docker/build-push-action@v6 with: - context: . platforms: linux/amd64 - pull: true - push: ${{ inputs.push }} - load: ${{ inputs.push == 'false' }} + # Inject metadata produced earlier tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - VERSION=${{ steps.meta.outputs.tags }} - NODE_ENV=${{ inputs.node_env }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + # Output image to a local tar file to be uploaded + # Also push to registry when appropriate + outputs: | + type=docker,dest=${{ steps.tar.outputs.path }} + type=image,name=${{ steps.tag.outputs.tag }},push=${{ inputs.push }} + # Target a specified stage + target: ${{ inputs.target }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + # The artifact name should be kept in sync with + # ./.github/actions/run/action.yml which downloads the artifact + name: docker-image + path: ${{ steps.tar.outputs.path }} + retention-days: 1 + compression-level: 9 + overwrite: true + + - name: Load Docker image + shell: bash + run: | + # Load the Docker image + docker load -i /tmp/image.tar diff --git a/.github/actions/context/action.yml b/.github/actions/context/action.yml index 284638f..0b69210 100644 --- a/.github/actions/context/action.yml +++ b/.github/actions/context/action.yml @@ -1,6 +1,30 @@ name: 'Dump Context' description: 'Display context for action run' +outputs: + # All github action outputs are strings, even if set to "true" + # so when using these values always assert against strings or convert from json + # \$\{{ needs.context.outputs.is_fork == 'true' }} // true + # \$\{{ fromJson(needs.context.outputs.is_fork) == false }} // true + # \$\{{ needs.context.outputs.is_fork == true }} // false + # \$\{{ needs.context.outputs.is_fork }} // false + is_fork: + description: "" + value: ${{ steps.context.outputs.is_fork }} + is_default_branch: + description: "" + value: ${{ steps.context.outputs.is_default_branch }} + is_release_master: + description: "" + value: ${{ steps.context.outputs.is_release_master }} + is_release_tag: + description: "" + value: ${{ steps.context.outputs.is_release_tag }} + # Hardcode image name + image_name: + description: "" + value: mozilla/addons-server + runs: using: 'composite' steps: @@ -36,3 +60,59 @@ runs: INPUTS_CONTEXT: ${{ toJson(inputs) }} run: | echo "$INPUTS_CONTEXT" + + - name: Set context + id: context + env: + # The default branch of the repository, in this case "master" + default_branch: ${{ github.event.repository.default_branch }} + shell: bash + run: | + event_name="${{ github.event_name }}" + event_action="${{ github.event.action }}" + + # Stable check for if the workflow is running on the default branch + # https://stackoverflow.com/questions/64781462/github-actions-default-branch-variable + is_default_branch="${{ format('refs/heads/{0}', env.default_branch) == github.ref }}" + + # In most events, the epository refers to the head which would be the fork + is_fork="${{ github.event.repository.fork }}" + + # This is different in a pull_request where we need to check the head explicitly + if [[ "${{ github.event_name }}" == 'pull_request' ]]; then + # repository on a pull request refers to the base which is always mozilla/addons-server + is_head_fork="${{ github.event.pull_request.head.repo.fork }}" + # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions + is_dependabot="${{ github.actor == 'dependabot[bot]' }}" + + # If the head repository is a fork or if the PR is opened by dependabot + # we consider the run to be a fork. Dependabot and proper forks are treated + # the same in terms of limited read only github token scope + if [[ "$is_head_fork" == 'true' || "$is_dependabot" == 'true' ]]; then + is_fork="true" + fi + fi + + is_release_master="false" + is_release_tag="false" + + # Releases can only happen if we are NOT on a fork + if [[ "$is_fork" == 'false' ]]; then + # A master release occurs on a push to the default branch of the origin repository + if [[ "$event_name" == 'push' && "$is_default_branch" == 'true' ]]; then + is_release_master="true" + fi + + # A tag release occurs when a release is published + if [[ "$event_name" == 'release' && "$event_action" == 'publish' ]]; then + is_release_tag="true" + fi + fi + + echo "is_default_branch=$is_default_branch" >> $GITHUB_OUTPUT + echo "is_fork=$is_fork" >> $GITHUB_OUTPUT + echo "is_release_master=$is_release_master" >> $GITHUB_OUTPUT + echo "is_release_tag=$is_release_tag" >> $GITHUB_OUTPUT + + echo "event_name: $event_name" + cat $GITHUB_OUTPUT diff --git a/.github/actions/push/action.yml b/.github/actions/push/action.yml new file mode 100644 index 0000000..b349062 --- /dev/null +++ b/.github/actions/push/action.yml @@ -0,0 +1,32 @@ +name: 'Docker Push image to registry' +description: 'Pushes build docker image to registry' +inputs: + tag: + required: true + description: "The full docker tag to push" + password: + required: false + description: "Password for the registry" + username: + required: false + description: "Username for the registry" + registry: + required: false + description: "The registry to push to" + default: "ghcr.io" + +runs: + using: "composite" + steps: + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.username }} + password: ${{ inputs.password }} + + - name: Push Image + shell: bash + run: | + docker image ls + docker image push ${{ inputs.registry }}/${{ inputs.tag }} diff --git a/.github/actions/run/action.yml b/.github/actions/run/action.yml index 3793d4d..3fb45d3 100644 --- a/.github/actions/run/action.yml +++ b/.github/actions/run/action.yml @@ -1,58 +1,42 @@ name: 'Docker Run Action' description: 'Run a command in a new container' inputs: - image: - description: "The Docker image to run" + tag: + description: 'The docker image tag to run.' required: true - options: - description: 'Options' - required: false run: description: 'Run command in container' required: true runs: using: 'composite' steps: - - name: Validate inputs + - uses: actions/download-artifact@v4 + with: + # The artifact name should be kept in sync with + # ./.github/actions/build/action.yml which uploads the artifact + name: docker-image + path: /tmp/ + + # image.tar is the name of the compressed image file + # This should be kept in sync with ./.github/actions/build/action.yml + - name: Load image shell: bash run: | - if [[ -z "${{ inputs.image }}" ]]; then - echo "Image is required" - exit 1 - fi - if [[ -z "${{ inputs.run }}" ]]; then - echo "Run is required" - exit 1 - fi + docker load < /tmp/image.tar + docker image ls + - name: Run Docker Container shell: bash + env: + DOCKER_TAG: ${{ inputs.tag }} run: | - cat < exec.sh - #!/bin/bash - whoami - ${{ inputs.run }} - EOF + # Start the specified services + make up - cat < root.sh - #!/bin/bash - whoami - su -s /bin/bash -c './exec.sh' root + # Exec the run command in the container + # quoted 'EOF' to prevent variable expansion + cat <<'EOF' | docker compose exec --user root app sh + #!/bin/bash + whoami + ${{ inputs.run }} EOF - - # Make both files executable - chmod +x exec.sh - chmod +x root.sh - - # Debug info - echo "############" - cat root.sh - echo "############" - echo "############" - cat exec.sh - echo "############" - - # Execute inside docker container - cat root.sh | docker run ${{ inputs.options }} \ - -i --rm -u 0 \ - -v $(pwd):/app \ - ${{ inputs.image }} bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..30035a7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: Worker (fork) + +on: + workflow_call: + outputs: + image: + description: "The Docker image" + value: '' + version: + description: "The version for the image" + value: '' + digest: + description: "The build digest for the image" + value: '' + tag: + description: "Combines image and version to a valid image tag" + value: '' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + context: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/context + + login_ghcr: + runs-on: ubuntu-latest + steps: + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + logout: false + - shell: bash + run: | + docker system info + cat ~/.docker/config.json + + build: + runs-on: ubuntu-latest + needs: [context, login_ghcr] + steps: + - uses: actions/checkout@v4 + - shell: bash + run: | + docker system info + cat ~/.docker/config.json + + + + +# Build and upload image as artifact + diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..a9f31d4 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,55 @@ +name: Push + +on: + push: + branches: + - main + pull_request: + +permissions: + packages: write + +# TODO: +# 1. split out the push action to separate action +# 2. add caching based on fork behaviour, use GHA cache or registry.. + +jobs: + build_call: + uses: ./.github/workflows/build.yml + + build: + runs-on: ubuntu-latest + + outputs: + tag: ${{ steps.build.outputs.tag }} + + steps: + - uses: actions/checkout@v4 + + - id: context + uses: ./.github/actions/context + + - uses: ./.github/actions/build + id: build + with: + node_env: production + latest: ${{ steps.context.outputs.is_release_master }} + push: ${{ steps.context.outputs.is_fork == 'false' }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + download: + runs-on: ubuntu-latest + needs: [build] + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/run + with: + tag: ${{ needs.build.outputs.tag }} + run: | + echo "Hello world" + npm run test + + diff --git a/.github/workflows/worker.yml b/.github/workflows/worker.yml index c60151b..47f25ef 100644 --- a/.github/workflows/worker.yml +++ b/.github/workflows/worker.yml @@ -1,4 +1,4 @@ -name: Worker +name: Worker (fork) on: workflow_call: diff --git a/docker-compose.yml b/docker-compose.yml index 633f92d..5626c57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,7 @@ version: '3.8' services: app: - build: - context: . - args: - NODE_ENV: development + image: ${DOCKER_TAG:-} volumes: - node_modules:/app/node_modules - dist:/app/dist