GitHub Action & Azure Static Web Apps

Conosci le Static Web Apps disponibili su Azure? In questo articolo voglio raccontarti come di recente ho migrato il mio blog su Cloud. Da dove comincio? Chi mi segue da tanti anni ricordera’ il passaggio da WordPress a GoHugo effettuato durante il lockdown. Inizialmente utilizzavo un classico hosting. In seguito (per alcuni problemi) sono passato alle CloudFlare Page e di recente ho fatto la migrazione che tanto desideravo.

Prima di procedere nella lettura di questo articolo ci tengo a dirvi una cosa. Quanto segue non corrisponde alla prima versione scritta. Dopo avere raccontato la mia esperienza e salvato il file in bozza non ero convinto. Nulla che una serata davanti al monitor non possa risolvere.

Premessa: Quello che segue e’ la soluzione che ho adottato per gestire il mio blog e le pubblicazione. Sono certo non sia la sola e non sia in assoluto la migliore. Se volete vedere la creazione e pubblicazione pulita di un caso standard sfruttando le GitHub Action vi rimando al video specifico su BugsInCloud dal titolo GitHub: Azure Static App Build & Deploy

Ora sono felice di raccontarvi la mia espererienza per come l’ho vissuta e trasportata dalla mia mente alle GitHub Actions.

GitHub

Prima di passare allo pensiero successivo credo sia meglio spiegarvi come e’ strutturato il mio repository del blog

1
2
3
4
5
main
- branch-articolo-1
- branch-articolo-2
- branch-articolo-N
gh-pages

Come avrete notato a livello “alto” esistono due branch importanti per me. Il primo si tratta di main dove trovo l’ultima versione del mio blog. Allo stesso livello ho messo il branch “gh-pages”. Al suo interno una GitHub Action dedicata andrà a creare tutti i contenuti HTML del blog partendo dai file MarkDown.

Static Web Apps

Indovinate quale è stato il primo passaggio per potere migrare il mio blog su Static Web Apps ? Lasciando poco spazio alla fantasia vi “deluderò” ma è stato creare una risorsa di questo tipo. Per farlo -questa volta- ho utilizzato la UI fornita da Microsoft e non TerraForm.

Nella seguente immagine vi mostro come ho valorizzato le diversi voci necessarie. Per come utilizzo il blog ho deciso di impostare la Build Preset ad HTML in quanto desidero sia la GitHub Action appena vista a creare i contenuti in quanto nello step Build environment production sono presente delle personalizzazioni omesse nel codice precedente.

Static Web Apps - Build Preset

Ora -dopo avere dato il via alla creazione- non mi resterà che attendere per potere cominciare a lavorare sulla nuova pipeline. Di quale pipeline parlo? Avendo dato alla Static Web Apps l’indicazione di essere associata ad un repository GitHub verra’ generata una pipeline per la pubblicazione da GitHub ad Azure.

Nello screen -come avrete visto- ho selezionato il ramo gh-pages e sarà proprio in questo ramo che vi ritroverete il nuovo file yml. Purtroppo (dopo una serie di esperimenti) ho dovuto spostare questo file su main e fare delle modifiche (divertenti) per raggiungere la pubblicazione come la volevo io.

Pipeline CI/CD

Dopo avere creato tutto quanto necessario ho dato il via alla creazione della Pipeline per la pubblicazione del blog. Per come la concepivo io doveva essere strutturata in quattro step. Ogni step ha un compito ben specifico, ma avrà un modo di interagire coi successivi quando necessario.

Prima di entrare nel vivo vi voglio mostrare lo scheletro ad alto livello con la lista dei jobs inseriti al suo interno.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
name: Pipeline Azure Static Web Apps CI/CD

on:
  [...]
  
jobs:

  actual_gh_pages_hash_job:
    [...]

  deploy_job:
    [...] 
    
  calculate_gh_pages_hash_job:
    [...] 

  azure_static_web_apps_job:
    [...]

Ed ora sorge la domanda: cosa accadrà nei singoli passaggi per portare il blog su Static Web Apps?

Prima di addentrarci nei singoli passaggi credo sia doveroso dare una risposta alla seguente domanda: Perché la soluzione proposta nel template di Microsoft non mi andava bene ed ho inventato la ruota da capo?

La risposta è molto semplice quanto per molti potrà sembrare banale. Sono a favore del risparmio energetico e se posso evitare di fare lavorare delle CPU o trasferimenti dati inulti lo faccio molto volentieri. Inoltre -giusto per rovinarvi l’appetito- vi ricordo che il piano free di GitHub ha un limite di Actions minutes/month e magari questo vi aiuta a risparmiare.

Nella pipeline che vedrete verrà effettuata la pubblicazione su Azure solamente se vi è stata la modifica ad almeno un file. In caso contrario non verrà fatto nulla. I contenuti sono gli stessi e non ha senso ripubblicare il tutto.

A questo punto (non sentendo la necessità) di uno staging ho “ripulito” la pipeline di default introducendo dei grossi cambiamenti:

  • Rimozione dello staging
  • Creazione di un “super hash” per monitorare i cambiamenti del blog
  • Se ho almeno una modifica posso procedere alla pubblicazione su Azure
  • In caso contrario la pipeline verrà interrotta in maniera forzata dandomi errore

Siete curiosi di vedere come ho realizzato tutto questo?

actual_gh_pages_hash_job:

Per capire se ci sono stati oppure no dei cambiamenti nel mio blog, la prima operazione da fare sarà sicuramente quella di fare una “fotografia” del contenuto di tutti i file presenti in esso. Come posso fare?

In questo blocco non farò altro che leggere il contento dei un file ghpageshash.txt dove al suo interno è presente uno sha256 che riassume lo stato del blog. Una volta ottenuto il valore presente in esso, lo rendiamo disponibile come outputs del job corrente.

Tips: Per il primo run della pipeline questo file deve essere presente. Vi conviene crearlo a mano nel brach gh-pages e valorizzarlo anche semplicemente con uno spazio oppure un trattino.

deploy_job:

Questo job non è altro che la precedente pipeline di build del blog. Il suo compito consiste nel costruire il blog generando i file statici per salvarli nel ramo dedicato.

calculate_gh_pages_hash_job:

Questo step è molto simile al primo, ma verrà eseguito sui nuovi contenuti presenti nel blog dopo la build appena avvenuta. Come vedrete (la pipeline intera è sotto) al suo interno sono presenti diversi steps:

  • Calcolo del nuovo hash escludendo le directory nascoste ed il file di hash esistente nel branch
  • Salvataggio del valore appena calcolato nel brach dedicato
  • Lettura del file
  • Log del valore calcolato nella pipeline

Inoltre il nuovo valore verrà esposto in output per potere essere usato in seguito

Warning: Dall’esecuzione di questo step potreste ricevere il seguente

Warning: Unexpected input(s) ‘files’, valid inputs are [‘commit_message’, ‘branch’, ‘commit_options’, ‘add_options’, ‘status_options’, ‘file_pattern’, ‘repository’, ‘commit_user_name’, ‘commit_user_email’, ‘commit_author’, ‘tagging_message’, ‘push_options’, ‘skip_dirty_check’, ‘skip_fetch’, ‘skip_checkout’, ‘disable_globbing’, ‘create_branch’, ‘internal_git_binary’]

Onestamente dopo avere realizzato la pipeline (credetemi non è stata veloce da fare) ho deciso di fermarmi qua rimandando la risoluzione del problema ad un secondo momento. Ho deciso di scrivere ugualmente l’articolo per evitare di perdere il contenuto come ho fatto in passato aspettando di finire come dico io.

azure_static_web_apps_job:

Ora arriviamo al punto cruciale della pipeline: la pubblicazione su Static Web App. Mi serve veramente pubblicare? La risposta la possiamo sapere solamente confrontando i valori presenti nell’output di actual_gh_pages_hash_job e calculate_gh_pages_hash_job.

Vi state chiedendo in che senso?

Se questi valori sono uguali tra di loro il job in questione non verrà eseguito.

Static Web Apps - Build Preset

Se invece i valori sono diversi tra loro il job provvederà a pubblicare i nuovi contenuti per renderli accessibili a tutti.

Static Web Apps - Build Preset

YAML

Siete increduli sul funzionamento del tutto? Sappiate che se state leggendo questo articolo la pipeline ed i jobs stanno funzionando in perfetta armonia tra loro. Se invece non state leggendo questo articolo significa che ho un problema da sistemare.

Ora -finalmente e dopo una lunga attesa- vi mostro la pipeline integrale pronta all’uso SOTTO LA VOSTRA RESPONSABILITÀ:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
name: Pipeline Azure Static Web Apps CI/CD

on:
  schedule:
    - cron:  '0 1 * * *'  
        
jobs:

  actual_gh_pages_hash_job:
    runs-on: ubuntu-latest
    outputs:
      actual_gh_pages_hash: ${{ steps.actual_ghpageshash.outputs.content  }}
    steps:
      - name: Checkout branch gh-pages
        uses: actions/checkout@v3.5.2
        with:
          ref: gh-pages
      
      - name: Read ghpageshash.txt
        id: actual_ghpageshash
        uses: juliangruber/read-file-action@v1
        with:
          path: ./ghpageshash.txt     
          
      - name: Show file content
        id: show-file
        run: |
          echo ${{ steps.actual_ghpageshash.outputs.content  }}   


  deploy_job:
    runs-on: ubuntu-latest    
    needs: [actual_gh_pages_hash_job]
    steps:
      - uses: actions/checkout@v3.5.2
        with:
          submodules: true  # Fetch Hugo themes
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: '0.74.3'
          extended: true

      - name: Build environment production
        run: hugo --minify  --environment production   

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./public   
    
  calculate_gh_pages_hash_job:
    runs-on: ubuntu-latest
    needs: [deploy_job,actual_gh_pages_hash_job]
    outputs:
      calculate_gh_pages_hash: ${{ steps.ghpageshash.outputs.content  }}  
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3.5.2
        with:
          submodules: true
          ref: gh-pages  

      - name: Calculate hash
        run: |
          hash=$(find . -type f ! -name "ghpageshash.txt" ! -path "./.*" -exec shasum -a 256 {} \; | sort | shasum -a 256)
          echo "$hash" > ghpageshash.txt
          
      - name: Commit and push changes
        uses: stefanzweifel/git-auto-commit-action@v4.16.0
        with:
          commit_message: Update ghpageshash.txt
          branch: gh-pages
          files: |
            ghpageshash.txt    

      - name: Read ghpageshash.txt
        id: ghpageshash
        uses: juliangruber/read-file-action@v1
        with:
          path: ./ghpageshash.txt
          
      - name: Show file content
        id: show-file
        run: |
          echo  ${{ steps.ghpageshash.outputs.content  }}   

  azure_static_web_apps_job:
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    needs: [deploy_job,actual_gh_pages_hash_job,calculate_gh_pages_hash_job]
    if: ${{ needs.actual_gh_pages_hash_job.outputs.actual_gh_pages_hash != needs.calculate_gh_pages_hash_job.outputs.calculate_gh_pages_hash }}
    steps:
      - uses: actions/checkout@v3.5.2
        with:
          submodules: true
          ref: gh-pages 

      - name: Show actual_gh_pages_hash_job
        run: echo "Actual GH Pages hash is ${{ needs.actual_gh_pages_hash_job.outputs.actual_gh_pages_hash }}"  
       
      - name: Show calculate_gh_pages_hash_job
        run: echo "Calculate GH Pages hash is  ${{ needs.calculate_gh_pages_hash_job.outputs.calculate_gh_pages_hash }}"           
                   
      - name: Build And Deploy
        if: ${{ needs.actual_gh_pages_hash_job.outputs.actual_gh_pages_hash != needs.calculate_gh_pages_hash_job.outputs.calculate_gh_pages_hash }}
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: "upload"
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: "/" # App source code path
          api_location: "" # Api source code path - optional
          output_location: "/" # Built app content directory - optional
          ###### End of Repository/Build Configurations ######

Come avrete visto, la pipeline è veramente un bel caso di studio anche per imparare come muoversi all’interno dei file YAML su GitHub. Vi ricordo che tutto quello che avete letto è un passaggio extra. Se volete il caso base e/o non preoccuparvi se vengono effettuate pubblicazioni non necessari questo non è necessario.

GitHub Action & Secret

Leggendo la pipeline precedente avrai notato la presenza di alcuni secrets, tra cui il seguente:

1
${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}

Se avete eseguito l’associazione Static Web App/ GitHub in fase di creazione della risorsa sappiate che non dovrete fare nulla.

Se invece avete già una Static Web App e volete pubblicare tramite GitHub Action allora dovreste sia creare il secret che valorizzarlo correttamente. Come?

Te lo spiego in un video tutoria sul mio canale YouTube: